diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index e10ad0e3000..437c150d5c1 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1758,7 +1758,14 @@ export interface IWorkspaceTextEdit { } export interface WorkspaceEdit { - edits: Array; + edits: Array; +} + +export interface ICustomEdit { + readonly resource: URI; + readonly metadata?: WorkspaceEditMetadata; + undo(): Promise | void; + redo(): Promise | void; } export interface Rejection { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 11b8f7415dc..f9389176183 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7962,7 +7962,14 @@ declare namespace monaco.languages { } export interface WorkspaceEdit { - edits: Array; + edits: Array; + } + + export interface ICustomEdit { + readonly resource: Uri; + readonly metadata?: WorkspaceEditMetadata; + undo(): Promise | void; + redo(): Promise | void; } export interface Rejection { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index 60e1812f433..b20903fbab3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -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 => { @@ -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'); }); } @@ -122,6 +128,8 @@ class BulkEdit { resources.push(await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else if (group[0] instanceof ResourceNotebookCellEdit) { resources.push(await this._performCellEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); + } else if (group[0] instanceof ResourceAttachmentEdit) { + resources.push(await this._performOpaqueEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else { console.log('UNKNOWN EDIT'); } @@ -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): Promise { + 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 { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts new file mode 100644 index 00000000000..a8615ee859e --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts @@ -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((candidate).undo && (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, + readonly redo: () => Promise | void, + metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class OpaqueEdits { + + constructor( + private readonly _undoRedoGroup: UndoRedoGroup, + private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _progress: IProgress, + private readonly _token: CancellationToken, + private readonly _edits: ResourceAttachmentEdit[], + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + ) { } + + async apply(): Promise { + 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; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c151b022811..8900c8509a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -59,6 +59,7 @@ import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; +import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; import { ChatEditorSaving } from './chatEditorSaving.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; @@ -211,6 +212,8 @@ AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); +registerEditorFeature(ChatInputBoxContentProvider); + class ChatSlashStaticSlashCommandsContribution extends Disposable { static readonly ID = 'workbench.contrib.chatSlashStaticSlashCommands'; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index d5936b500bd..3f8a7d9585d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -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'; @@ -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 { @@ -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; diff --git a/src/vs/workbench/contrib/chat/browser/chatEdinputInputContentProvider.ts b/src/vs/workbench/contrib/chat/browser/chatEdinputInputContentProvider.ts new file mode 100644 index 00000000000..cbc084b3b82 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEdinputInputContentProvider.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.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 { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { ChatInputPart } from './chatInputPart.js'; + + +export class ChatInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(ChatInputPart.INPUT_SCHEME, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('chatinput'), resource); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 9820f196b78..5f451220741 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -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 { getBaseLayerHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate2.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; @@ -39,6 +40,7 @@ import { IRange, Range } from '../../../../editor/common/core/range.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 { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; @@ -78,7 +80,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'; @@ -282,6 +284,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IMenuService private readonly menuService: IMenuService, @ILanguageService private readonly languageService: ILanguageService, @IThemeService private readonly themeService: IThemeService, + @ITextModelService private readonly textModelResolverService: ITextModelService, @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -727,9 +730,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { inputModel = this.modelService.createModel('', null, this.inputUri, true); - this._register(inputModel); } + this.textModelResolverService.createModelReference(this.inputUri).then(ref => { + // make sure to hold a reference so that the model doesn't get disposed by the text model service + if (this._store.isDisposed) { + ref.dispose(); + return; + } + this._register(ref); + }); + this.inputModel = inputModel; this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } }); this._inputEditor.setModel(this.inputModel); @@ -864,6 +875,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; diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 7e8a3d7ffa7..62350a6cb09 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IDataTransferItem, IReadonlyVSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { DocumentPasteContext, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; @@ -17,19 +17,25 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; +import { Mimes } from '../../../../base/common/mime.js'; +import { URI } from '../../../../base/common/uri.js'; + +const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; export class PasteImageProvider implements DocumentPasteEditProvider { - public readonly kind = new HierarchicalKind('image'); - public readonly copyMimeTypes = ['image/*']; + public readonly kind = new HierarchicalKind('chat.attach.image'); public readonly providedPasteEditKinds = [this.kind]; + + public readonly copyMimeTypes = []; public readonly pasteMimeTypes = ['image/*']; + constructor( private readonly chatWidgetService: IChatWidgetService, - private readonly extensionService: IExtensionService + private readonly extensionService: IExtensionService, ) { } - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { if (!this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { return; } @@ -63,7 +69,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider { return; } - const widget = this.chatWidgetService.getWidgetByInputUri(_model.uri); + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget) { return; } @@ -88,9 +94,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider { return; } - widget.attachmentModel.addContext(imageContext); - - return; + return getCustomPaste(model, imageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); } } @@ -107,7 +111,6 @@ async function getImageAttachContext(data: Uint8Array, mimeType: string, token: isImage: true, icon: Codicon.fileMedia, isDynamic: true, - isFile: false, mimeType }; } @@ -137,6 +140,126 @@ export function isImage(array: Uint8Array): boolean { ); } +export class CopyTextProvider implements DocumentPasteEditProvider { + public readonly providedPasteEditKinds = []; + public readonly copyMimeTypes = [COPY_MIME_TYPES]; + public readonly pasteMimeTypes = []; + + async prepareDocumentPaste(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + if (model.uri.scheme === ChatInputPart.INPUT_SCHEME) { + return; + } + const customDataTransfer = new VSDataTransfer(); + const rangesString = JSON.stringify({ ranges: ranges[0], uri: model.uri.toString() }); + customDataTransfer.append(COPY_MIME_TYPES, createStringDataTransferItem(rangesString)); + return customDataTransfer; + } +} + +export class PasteTextProvider implements DocumentPasteEditProvider { + + public readonly kind = new HierarchicalKind('chat.attach.text'); + public readonly providedPasteEditKinds = [this.kind]; + + public readonly copyMimeTypes = []; + public readonly pasteMimeTypes = [COPY_MIME_TYPES]; + + constructor( + private readonly chatWidgetService: IChatWidgetService + ) { } + + async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + if (model.uri.scheme !== ChatInputPart.INPUT_SCHEME) { + return; + } + const text = dataTransfer.get(Mimes.text); + const editorData = dataTransfer.get('vscode-editor-data'); + const additionalEditorData = dataTransfer.get(COPY_MIME_TYPES); + + if (!editorData || !text || !additionalEditorData) { + return; + } + + const textdata = await text.asString(); + const metadata = JSON.parse(await editorData.asString()); + const additionalData = JSON.parse(await additionalEditorData.asString()); + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return; + } + + const copiedContext = await getCopiedContext(textdata, additionalData.uri, metadata.mode, additionalData.ranges); + + if (token.isCancellationRequested || !copiedContext) { + return; + } + + const currentContextIds = widget.attachmentModel.getAttachmentIDs(); + if (currentContextIds.has(copiedContext.id)) { + return; + } + + return getCustomPaste(model, copiedContext, Mimes.text, this.kind, localize('pastedCodeAttachment', 'Pasted Code Attachment'), this.chatWidgetService); + } +} + +async function getCopiedContext(code: string, file: string, language: string, ranges: IRange): Promise { + const fileName = file.split('/').pop() || 'unknown file'; + const start = ranges.startLineNumber; + const end = ranges.endLineNumber; + const resultText = `Copied Selection of Code: \n\n\n From the file: ${fileName} From lines ${start} to ${end} \n \`\`\`${code}\`\`\``; + const pastedLines = start === end ? localize('pastedAttachment.oneLine', '1 line') : localize('pastedAttachment.multipleLines', '{0} lines', end + 1 - start); + return { + kind: 'paste', + value: resultText, + id: `${fileName}${start}${end}${ranges.startColumn}${ranges.endColumn}`, + name: `${fileName} ${pastedLines}`, + icon: Codicon.code, + isDynamic: true, + pastedLines, + language, + fileName, + code, + references: [{ + reference: URI.parse(file), + kind: 'reference' + }] + }; +} + +async function getCustomPaste(model: ITextModel, context: IChatRequestVariableEntry, handledMimeType: string, kind: HierarchicalKind, title: string, chatWidgetService: IChatWidgetService): Promise { + const customEdit = { + resource: model.uri, + variable: context, + undo: () => { + const widget = chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for undo'); + } + widget.attachmentModel.delete(context.id); + }, + redo: () => { + const widget = chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for redo'); + } + widget.attachmentModel.addContext(context); + }, + metadata: { needsConfirmation: false, label: context.name } + }; + + return { + edits: [{ + insertText: '', title, kind, handledMimeType, + additionalEdit: { + edits: [customEdit], + } + }], + dispose() { }, + }; +} + export class ChatPasteProvidersFeature extends Disposable { constructor( @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @@ -145,5 +268,7 @@ export class ChatPasteProvidersFeature extends Disposable { ) { super(); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService))); + this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService))); + this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index d699d995740..e1f9bb59ebb 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1307,6 +1307,11 @@ have to be updated for changes to the rules above, or to support more deeply nes border: none; } +.chat-attached-context-attachment .attachment-additional-info { + opacity: 0.7; + font-size: .9em; +} + .chat-attached-context-attachment .chat-attached-context-pill-image { width: 14px; height: 14px; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 18e8eead70a..3c196acf221 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -56,6 +56,14 @@ export interface IChatRequestImplicitVariableEntry extends Omit { + readonly kind: 'paste'; + code: string; + language: string; + fileName: string; + pastedLines: string; +} + export interface ISymbolVariableEntry extends Omit { readonly kind: 'symbol'; readonly isDynamic: true; @@ -67,12 +75,16 @@ export interface ICommandResultVariableEntry extends Omit