Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code block pasting in chat #233358

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
9 changes: 8 additions & 1 deletion src/vs/editor/common/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,14 @@ export interface IWorkspaceTextEdit {
}

export interface WorkspaceEdit {
edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit>;
edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit | ICustomEdit>;
}

export interface ICustomEdit {
readonly resource: URI;
readonly metadata?: WorkspaceEditMetadata;
undo(): Promise<void> | void;
redo(): Promise<void> | void;
}

export interface Rejection {
Expand Down
9 changes: 8 additions & 1 deletion src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7954,7 +7954,14 @@ declare namespace monaco.languages {
}

export interface WorkspaceEdit {
edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit>;
edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit | ICustomEdit>;
}

export interface ICustomEdit {
readonly resource: Uri;
readonly metadata?: WorkspaceEditMetadata;
undo(): Promise<void> | void;
redo(): Promise<void> | void;
}

export interface Rejection {
Expand Down
14 changes: 14 additions & 0 deletions src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { BulkTextEdits } from './bulkTextEdits.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';
import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';
import { OpaqueEdits, ResourceAttachmentEdit } from './opaqueEdits.js';

function liftEdits(edits: ResourceEdit[]): ResourceEdit[] {
return edits.map(edit => {
Expand All @@ -40,6 +41,11 @@ function liftEdits(edits: ResourceEdit[]): ResourceEdit[] {
if (ResourceNotebookCellEdit.is(edit)) {
return ResourceNotebookCellEdit.lift(edit);
}

if (ResourceAttachmentEdit.is(edit)) {
return ResourceAttachmentEdit.lift(edit);
}

throw new Error('Unsupported edit');
});
}
Expand Down Expand Up @@ -122,6 +128,8 @@ class BulkEdit {
resources.push(await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
} else if (group[0] instanceof ResourceNotebookCellEdit) {
resources.push(await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
} else if (group[0] instanceof ResourceAttachmentEdit) {
resources.push(await this._performOpaqueEdits(<ResourceAttachmentEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress));
} else {
console.log('UNKNOWN EDIT');
}
Expand All @@ -148,6 +156,12 @@ class BulkEdit {
const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits);
return await model.apply();
}

private async _performOpaqueEdits(edits: ResourceAttachmentEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> {
this._logService.debug('_performOpaqueEdits', JSON.stringify(edits));
const model = this._instaService.createInstance(OpaqueEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits);
return await model.apply();
}
}

export class BulkEditService implements IBulkEditService {
Expand Down
79 changes: 79 additions & 0 deletions src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from '../../../../base/common/cancellation.js';
import { isObject } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { ResourceEdit } from '../../../../editor/browser/services/bulkEditService.js';
import { ICustomEdit, WorkspaceEditMetadata } from '../../../../editor/common/languages.js';
import { IProgress } from '../../../../platform/progress/common/progress.js';
import { IUndoRedoService, UndoRedoElementType, UndoRedoGroup, UndoRedoSource } from '../../../../platform/undoRedo/common/undoRedo.js';

export class ResourceAttachmentEdit extends ResourceEdit implements ICustomEdit {

static is(candidate: any): candidate is ICustomEdit {
if (candidate instanceof ResourceAttachmentEdit) {
return true;
} else {
return isObject(candidate)
&& (Boolean((<ICustomEdit>candidate).undo && (<ICustomEdit>candidate).redo));
}
}

static lift(edit: ICustomEdit): ResourceAttachmentEdit {
if (edit instanceof ResourceAttachmentEdit) {
return edit;
} else {
return new ResourceAttachmentEdit(edit.resource, edit.undo, edit.redo, edit.metadata);
}
}

constructor(
readonly resource: URI,
readonly undo: () => Promise<void> | void,
readonly redo: () => Promise<void> | void,
metadata?: WorkspaceEditMetadata
) {
super(metadata);
}
}

export class OpaqueEdits {

constructor(
private readonly _undoRedoGroup: UndoRedoGroup,
private readonly _undoRedoSource: UndoRedoSource | undefined,
private readonly _progress: IProgress<void>,
private readonly _token: CancellationToken,
private readonly _edits: ResourceAttachmentEdit[],
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
) { }

async apply(): Promise<readonly URI[]> {
const resources: URI[] = [];

for (const edit of this._edits) {
if (this._token.isCancellationRequested) {
break;
}

await edit.redo();

this._undoRedoService.pushElement({
type: UndoRedoElementType.Resource,
resource: edit.resource,
label: edit.metadata?.label || 'Custom Edit',
code: 'paste',
undo: edit.undo,
redo: edit.redo,
}, this._undoRedoGroup, this._undoRedoSource);

this._progress.report(undefined);
resources.push(edit.resource);
}

return resources;
}
}
29 changes: 29 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f
import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js';
import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js';
import { ChatEditorOverlayController } from './chatEditorOverlay.js';
import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { URI } from '../../../../base/common/uri.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ChatInputPart } from './chatInputPart.js';

// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
Expand Down Expand Up @@ -182,6 +188,9 @@ class ChatResolverContribution extends Disposable {
constructor(
@IEditorResolverService editorResolverService: IEditorResolverService,
@IInstantiationService instantiationService: IInstantiationService,
@ITextModelService private readonly textModelService: ITextModelService,
@IModelService private readonly modelService: IModelService,
@ILanguageService private readonly languageService: ILanguageService
) {
super();

Expand All @@ -202,12 +211,32 @@ class ChatResolverContribution extends Disposable {
}
}
));

this._register(new ChatInputBoxContentProvider(this.textModelService, this.modelService, this.languageService));
}
}

AccessibleViewRegistry.register(new ChatResponseAccessibleView());
AccessibleViewRegistry.register(new PanelChatAccessibilityHelp());
AccessibleViewRegistry.register(new QuickChatAccessibilityHelp());
class ChatInputBoxContentProvider extends Disposable implements ITextModelContentProvider {
constructor(
textModelService: ITextModelService,
private readonly modelService: IModelService,
private readonly languageService: ILanguageService,
) {
super();
this._register(textModelService.registerTextModelContentProvider(ChatInputPart.INPUT_SCHEME, this));
}

async provideTextContent(resource: URI): Promise<ITextModel | null> {
const existing = this.modelService.getModel(resource);
if (existing) {
return existing;
}
return this.modelService.createModel('', this.languageService.createById('chatinput'), resource);
}
}

class ChatSlashStaticSlashCommandsContribution extends Disposable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as dom from '../../../../../base/browser/dom.js';
import { IManagedHoverTooltipMarkdownString } from '../../../../../base/browser/ui/hover/hover.js';
import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
Expand All @@ -19,8 +20,8 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com
import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js';
import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js';
import { ResourceLabels } from '../../../../browser/labels.js';
import { IChatRequestVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js';
import { revealInsideBarCommand } from '../../../files/browser/fileActions.contribution.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js';

export class ChatAttachmentsContentPart extends Disposable {
Expand Down Expand Up @@ -126,6 +127,25 @@ export class ChatAttachmentsContentPart extends Disposable {
if (!this.attachedContextDisposables.isDisposed) {
this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement));
}
} else if (isPasteVariableEntry(attachment)) {
ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);

const hoverContent: IManagedHoverTooltipMarkdownString = {
markdown: {
value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``,
},
markdownNotSupportedFallback: attachment.code,
};

const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];
label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });
widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));

widget.style.position = 'relative';

if (!this.attachedContextDisposables.isDisposed) {
this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true }));
}
} else {
const attachmentLabel = attachment.fullName ?? attachment.name;
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel;
Expand Down
20 changes: 19 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
import * as aria from '../../../../base/browser/ui/aria/aria.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
Expand Down Expand Up @@ -75,7 +76,7 @@ import { revealInsideBarCommand } from '../../files/browser/fileActions.contribu
import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../common/chatEditingService.js';
import { IChatRequestVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js';
import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js';
import { IChatFollowup } from '../common/chatService.js';
import { IChatResponseViewModel } from '../common/chatViewModel.js';
Expand Down Expand Up @@ -829,6 +830,23 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false }));
resolve();
}));
} else if (isPasteVariableEntry(attachment)) {
ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);

const hoverContent: IManagedHoverTooltipMarkdownString = {
markdown: {
value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``,
},
markdownNotSupportedFallback: attachment.code,
};

const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];
label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });
widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));

widget.style.position = 'relative';
store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true }));
this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate);
} else {
const attachmentLabel = attachment.fullName ?? attachment.name;
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel;
Expand Down
Loading
Loading