diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index bd065a313f7..b1520c8821f 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -298,7 +298,10 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo if (this._cancellationTokenSource.token.isCancellationRequested) { return; } - + if (model.original.isDisposed() || model.modified.isDisposed()) { + // TODO@hediet fishy? + return; + } result = normalizeDocumentDiff(result, model.original, model.modified); result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified) ?? result; result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified) ?? result; diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index 1cbe63ceba1..603541cbb86 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -7,6 +7,7 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind'; +import { ITextModel } from 'vs/editor/common/model'; /** * A range of lines (1-based). @@ -76,6 +77,32 @@ export class LineRange { return new LineRange(lineRange[0], lineRange[1]); } + /** + * @internal + */ + public static invert(range: LineRange, model: ITextModel): LineRange[] { + if (range.isEmpty) { + return []; + } + const result: LineRange[] = []; + if (range.startLineNumber > 1) { + result.push(new LineRange(1, range.startLineNumber)); + } + if (range.endLineNumberExclusive < model.getLineCount() + 1) { + result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); + } + return result.filter(r => !r.isEmpty); + } + + /** + * @internal + */ + public static asRange(lineRange: LineRange, model: ITextModel): Range { + return lineRange.isEmpty + ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) + : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); + } + /** * The start line number. */ diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index b3e4170f81f..dc32c7728da 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -216,6 +216,7 @@ export class MenuId { static readonly InlineEditToolbar = new MenuId('InlineEditToolbar'); static readonly ChatContext = new MenuId('ChatContext'); static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); + static readonly ChatCompareBlock = new MenuId('ChatCompareBlock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index af66c85348e..297c4850d2c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1699,6 +1699,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, + ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 80a4beaffaf..bd008e874b5 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -82,35 +82,35 @@ class ChatAgentResponseStream { markdown(value) { throwIfDone(this.markdown); const part = new extHostTypes.ChatResponseMarkdownPart(value); - const dto = typeConvert.ChatResponseMarkdownPart.to(part); + const dto = typeConvert.ChatResponseMarkdownPart.from(part); _report(dto); return this; }, filetree(value, baseUri) { throwIfDone(this.filetree); const part = new extHostTypes.ChatResponseFileTreePart(value, baseUri); - const dto = typeConvert.ChatResponseFilesPart.to(part); + const dto = typeConvert.ChatResponseFilesPart.from(part); _report(dto); return this; }, anchor(value, title?: string) { throwIfDone(this.anchor); const part = new extHostTypes.ChatResponseAnchorPart(value, title); - const dto = typeConvert.ChatResponseAnchorPart.to(part); + const dto = typeConvert.ChatResponseAnchorPart.from(part); _report(dto); return this; }, button(value) { throwIfDone(this.anchor); const part = new extHostTypes.ChatResponseCommandButtonPart(value); - const dto = typeConvert.ChatResponseCommandButtonPart.to(part, that._commandsConverter, that._sessionDisposables); + const dto = typeConvert.ChatResponseCommandButtonPart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); return this; }, progress(value) { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.to(part); + const dto = typeConvert.ChatResponseProgressPart.from(part); _report(dto); return this; }, @@ -130,7 +130,7 @@ class ChatAgentResponseStream { } else { // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const dto = typeConvert.ChatResponseReferencePart.from(part); references = [dto]; } @@ -141,20 +141,33 @@ class ChatAgentResponseStream { } } else { const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); } return this; }, + textEdit(target, edits) { + throwIfDone(this.textEdit); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseTextEditPart(target, edits); + const dto = typeConvert.ChatResponseTextEditPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); + if (part instanceof extHostTypes.ChatResponseTextEditPart) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + if (part instanceof extHostTypes.ChatResponseReferencePart) { // Ensure variable reference values get fixed up this.reference(part.value); } else { - const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables); + const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); } @@ -277,7 +290,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId)); // RESPONSE turn - const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter))); + const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.toContent(r, this.commands.converter))); res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command)); } diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 26fac7e95e0..afc7ef7d1d5 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -96,14 +96,14 @@ class ChatVariableResolverResponseStream { progress(value) { throwIfDone(this.progress); const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.to(part); + const dto = typeConvert.ChatResponseProgressPart.from(part); _report(dto); return this; }, reference(value) { throwIfDone(this.reference); const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.ChatResponseReferencePart.to(part); + const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); return this; }, @@ -111,9 +111,9 @@ class ChatVariableResolverResponseStream { throwIfDone(this.push); if (part instanceof extHostTypes.ChatResponseReferencePart) { - _report(typeConvert.ChatResponseReferencePart.to(part)); + _report(typeConvert.ChatResponseReferencePart.from(part)); } else if (part instanceof extHostTypes.ChatResponseProgressPart) { - _report(typeConvert.ChatResponseProgressPart.to(part)); + _report(typeConvert.ChatResponseProgressPart.from(part)); } return this; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index caf7fcd3607..fb64c34d72a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -39,7 +39,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTreeData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; @@ -2350,19 +2350,19 @@ export namespace InteractiveEditorResponseFeedbackKind { } export namespace ChatResponseMarkdownPart { - export function to(part: vscode.ChatResponseMarkdownPart): Dto { + export function from(part: vscode.ChatResponseMarkdownPart): Dto { return { kind: 'markdownContent', content: MarkdownString.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseMarkdownPart { + export function to(part: Dto): vscode.ChatResponseMarkdownPart { return new types.ChatResponseMarkdownPart(MarkdownString.to(part.content)); } } export namespace ChatResponseFilesPart { - export function to(part: vscode.ChatResponseFileTreePart): IChatTreeData { + export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { const { value, baseUri } = part; function convert(items: vscode.ChatResponseFileTree[], baseUri: URI): extHostProtocol.IChatResponseProgressFileTreeData[] { return items.map(item => { @@ -2383,7 +2383,7 @@ export namespace ChatResponseFilesPart { } }; } - export function from(part: Dto): vscode.ChatResponseFileTreePart { + export function to(part: Dto): vscode.ChatResponseFileTreePart { const treeData = revive(part.treeData); function convert(items: extHostProtocol.IChatResponseProgressFileTreeData[]): vscode.ChatResponseFileTree[] { return items.map(item => { @@ -2401,7 +2401,7 @@ export namespace ChatResponseFilesPart { } export namespace ChatResponseAnchorPart { - export function to(part: vscode.ChatResponseAnchorPart): Dto { + export function from(part: vscode.ChatResponseAnchorPart): Dto { return { kind: 'inlineReference', name: part.title, @@ -2409,7 +2409,7 @@ export namespace ChatResponseAnchorPart { }; } - export function from(part: Dto): vscode.ChatResponseAnchorPart { + export function to(part: Dto): vscode.ChatResponseAnchorPart { const value = revive(part); return new types.ChatResponseAnchorPart( URI.isUri(value.inlineReference) ? value.inlineReference : Location.to(value.inlineReference), @@ -2419,19 +2419,19 @@ export namespace ChatResponseAnchorPart { } export namespace ChatResponseProgressPart { - export function to(part: vscode.ChatResponseProgressPart): Dto { + export function from(part: vscode.ChatResponseProgressPart): Dto { return { kind: 'progressMessage', content: MarkdownString.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseProgressPart { + export function to(part: Dto): vscode.ChatResponseProgressPart { return new types.ChatResponseProgressPart(part.content.value); } } export namespace ChatResponseCommandButtonPart { - export function to(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { + export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore const command = commandsConverter.toInternal(part.value, commandDisposables) ?? { command: part.value.command, title: part.value.title }; return { @@ -2439,14 +2439,28 @@ export namespace ChatResponseCommandButtonPart { command }; } - export function from(part: Dto, commandsConverter: CommandsConverter): vscode.ChatResponseCommandButtonPart { + export function to(part: Dto, commandsConverter: CommandsConverter): vscode.ChatResponseCommandButtonPart { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore return new types.ChatResponseCommandButtonPart(commandsConverter.fromInternal(part.command) ?? { command: part.command.id, title: part.command.title }); } } +export namespace ChatResponseTextEditPart { + export function from(part: vscode.ChatResponseTextEditPart): Dto { + return { + kind: 'textEdit', + uri: part.uri, + edits: part.edits.map(e => TextEdit.from(e)) + }; + } + export function to(part: Dto): vscode.ChatResponseTextEditPart { + return new types.ChatResponseTextEditPart(URI.revive(part.uri), part.edits.map(e => TextEdit.to(e))); + } + +} + export namespace ChatResponseReferencePart { - export function to(part: vscode.ChatResponseReferencePart): Dto { + export function from(part: vscode.ChatResponseReferencePart): Dto { if ('variableName' in part.value) { return { kind: 'reference', @@ -2466,7 +2480,7 @@ export namespace ChatResponseReferencePart { Location.from(part.value) }; } - export function from(part: Dto): vscode.ChatResponseReferencePart { + export function to(part: Dto): vscode.ChatResponseReferencePart { const value = revive(part); const mapValue = (value: URI | languages.Location): vscode.Uri | vscode.Location => URI.isUri(value) ? @@ -2485,19 +2499,21 @@ export namespace ChatResponseReferencePart { export namespace ChatResponsePart { - export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { - return ChatResponseMarkdownPart.to(part); + return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { - return ChatResponseAnchorPart.to(part); + return ChatResponseAnchorPart.from(part); } else if (part instanceof types.ChatResponseReferencePart) { - return ChatResponseReferencePart.to(part); + return ChatResponseReferencePart.from(part); } else if (part instanceof types.ChatResponseProgressPart) { - return ChatResponseProgressPart.to(part); + return ChatResponseProgressPart.from(part); } else if (part instanceof types.ChatResponseFileTreePart) { - return ChatResponseFilesPart.to(part); + return ChatResponseFilesPart.from(part); } else if (part instanceof types.ChatResponseCommandButtonPart) { - return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables); + return ChatResponseCommandButtonPart.from(part, commandsConverter, commandDisposables); + } else if (part instanceof types.ChatResponseTextEditPart) { + return ChatResponseTextEditPart.from(part); } return { kind: 'content', @@ -2506,26 +2522,26 @@ export namespace ChatResponsePart { } - export function from(part: extHostProtocol.IChatProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponsePart | undefined { + export function to(part: extHostProtocol.IChatProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponsePart | undefined { switch (part.kind) { - case 'reference': return ChatResponseReferencePart.from(part); + case 'reference': return ChatResponseReferencePart.to(part); case 'markdownContent': case 'inlineReference': case 'progressMessage': case 'treeData': case 'command': - return fromContent(part, commandsConverter); + return toContent(part, commandsConverter); } return undefined; } - export function fromContent(part: extHostProtocol.IChatContentProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponseMarkdownPart | vscode.ChatResponseFileTreePart | vscode.ChatResponseAnchorPart | vscode.ChatResponseCommandButtonPart | undefined { + export function toContent(part: extHostProtocol.IChatContentProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponseMarkdownPart | vscode.ChatResponseFileTreePart | vscode.ChatResponseAnchorPart | vscode.ChatResponseCommandButtonPart | undefined { switch (part.kind) { - case 'markdownContent': return ChatResponseMarkdownPart.from(part); - case 'inlineReference': return ChatResponseAnchorPart.from(part); + case 'markdownContent': return ChatResponseMarkdownPart.to(part); + case 'inlineReference': return ChatResponseAnchorPart.to(part); case 'progressMessage': return undefined; - case 'treeData': return ChatResponseFilesPart.from(part); - case 'command': return ChatResponseCommandButtonPart.from(part, commandsConverter); + case 'treeData': return ChatResponseFilesPart.to(part); + case 'command': return ChatResponseCommandButtonPart.to(part, commandsConverter); } return undefined; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index ce901a21850..addaa76b683 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4310,6 +4310,14 @@ export class ChatResponseReferencePart { } } +export class ChatResponseTextEditPart { + uri: vscode.Uri; + edits: vscode.TextEdit[]; + constructor(uri: vscode.Uri, edits: vscode.TextEdit | vscode.TextEdit[]) { + this.uri = uri; + this.edits = Array.isArray(edits) ? edits : [edits]; + } +} export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 3cd6e415c9c..145a74b8f08 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -12,10 +12,11 @@ import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/b import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { DocumentContextItem, WorkspaceEdit } from 'vs/editor/common/languages'; +import { DocumentContextItem, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -27,7 +28,7 @@ import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService, IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ICodeBlockActionContext, ICodeCompareBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatCopyKind, IChatService, IDocumentContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -46,6 +47,10 @@ export function isCodeBlockActionContext(thing: unknown): thing is ICodeBlockAct return typeof thing === 'object' && thing !== null && 'code' in thing && 'element' in thing; } +export function isCodeCompareBlockActionContext(thing: unknown): thing is ICodeCompareBlockActionContext { + return typeof thing === 'object' && thing !== null && 'element' in thing; +} + function isResponseFiltered(context: ICodeBlockActionContext) { return isResponseVM(context.element) && context.element.errorDetails?.responseIsFiltered; } @@ -582,3 +587,53 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): languageId: editor.getModel()!.getLanguageId(), }; } + +export function registerChatCodeCompareBlockActions() { + + abstract class ChatCompareCodeBlockAction extends Action2 { + run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + if (!isCodeCompareBlockActionContext(context)) { + return; + // TODO@jrieken derive context + } + + return this.runWithContext(accessor, context); + } + + abstract runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): any; + } + + registerAction2(class ApplyEditsCompareBlockAction extends ChatCompareCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.applyCompareEdits', + title: localize2('interactive.compare.apply', "Apply Edits"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.check, + menu: { + id: MenuId.ChatCompareBlock, + group: 'navigation' + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + if (!isResponseVM(context.element)) { + return; + } + const modelService = accessor.get(ITextModelService); + const ref = await modelService.createModelReference(context.uri); + try { + const edits = context.edits.map(TextEdit.asEditOperation); + ref.object.textEditorModel.pushStackElement(); + ref.object.textEditorModel.pushEditOperations(null, edits, () => null); + ref.object.textEditorModel.pushStackElement(); + } finally { + ref.dispose(); + } + } + }); + +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 85c687b0cff..f9b88620377 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -24,7 +24,7 @@ import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; +import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; @@ -318,6 +318,7 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit registerChatActions(); registerChatCopyActions(); registerChatCodeBlockActions(); +registerChatCodeCompareBlockActions(); registerChatFileTreeActions(); registerChatTitleActions(); registerChatExecuteActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index adfb6c0b6d9..98028d2d803 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -87,12 +87,20 @@ export interface IChatFileTreeInfo { export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; +export interface IChatListItemRendererOptions { + readonly renderStyle?: 'default' | 'compact'; + readonly noHeader?: boolean; + readonly noPadding?: boolean; + readonly editableCodeBlock?: boolean; + readonly renderTextEditsAsSummary?: (uri: URI) => boolean; +} + export interface IChatWidgetViewOptions { renderInputOnTop?: boolean; renderStyle?: 'default' | 'compact'; supportsFileReferences?: boolean; filter?: (item: ChatTreeItem) => boolean; - editableCodeBlocks?: boolean; + rendererOptions?: IChatListItemRendererOptions; menus?: { executeToolbar?: MenuId; inputSideToolbar?: MenuId; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 641c8f087a5..a5e90978f68 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -20,7 +20,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; @@ -53,12 +53,12 @@ import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider, CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider, CodeBlockPart, CodeCompareBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -66,6 +66,11 @@ import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/f import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; +import { IModelService } from 'vs/editor/common/services/model'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { TextEdit } from 'vs/editor/common/languages'; +import { IChatListItemRendererOptions } from './chat'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; const $ = dom.$; @@ -96,13 +101,6 @@ export interface IChatRendererDelegate { readonly onDidScroll?: Event; } -export interface IChatListItemRendererOptions { - readonly renderStyle?: 'default' | 'compact'; - readonly noHeader?: boolean; - readonly noPadding?: boolean; - readonly editableCodeBlock?: boolean; -} - export class ChatListItemRenderer extends Disposable implements ITreeRenderer { static readonly ID = 'item'; @@ -122,6 +120,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer = this._onDidChangeItemHeight.event; private readonly _editorPool: EditorPool; + private readonly _diffEditorPool: DiffEditorPool; private readonly _treePool: TreePool; private readonly _contentReferencesListPool: ContentReferencesListPool; @@ -146,12 +145,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer 0) { - madeChanges = true; - break; - } - } - if (madeChanges) { - dom.append(templateData.value, $('.interactive-edits-summary', undefined, localize('editsSummary', "Made changes."))); - } - } + const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; @@ -582,6 +576,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer item.kind === 'textEdit')) { + return { + element: $('.interactive-edits-summary', undefined, localize('editsSummary', "Made changes.")), + dispose() { } + }; + } + return undefined; + } + + const store = new DisposableStore(); + const cts = new CancellationTokenSource(); + + let isDisposed = false; + store.add(toDisposable(() => { + isDisposed = true; + cts.dispose(true); + })); + + const ref = this._diffEditorPool.get(); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + store.add(ref.object.onDidChangeContentHeight(() => { + ref.object.layout(this._currentLayoutWidth); + this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + })); + const handleReference = (reference: IReference) => { + if (isDisposed) { + reference.dispose(); + return undefined; + } + store.add(reference); + return reference.object.textEditorModel; + }; + + ref.object.render({ + element, + edits: textEdit.edits, + originalTextModel: this.textModelService.createModelReference(textEdit.uri).then(handleReference), + modifiedTextModel: this.textModelService.createModelReference(textEdit.uri).then(handleReference).then(model => { + + if (!model) { + return undefined; + } + + const modelN = this.modelService.createModel( + createTextBufferFactoryFromSnapshot(model.createSnapshot()), + { languageId: model.getLanguageId(), onDidChange: Event.None }, + undefined, false + ); + store.add(modelN); + const edits = textEdit.edits.map(TextEdit.asEditOperation); + modelN.pushEditOperations(null, edits, () => null); + return modelN; + }), + }, this._currentLayoutWidth, cts.token); + + return { + element: ref.object.element, + dispose() { + store.dispose(); + }, + }; + } + private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, templateData: IChatListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { const disposables = new DisposableStore(); @@ -1081,6 +1149,41 @@ class EditorPool extends Disposable { } } +class DiffEditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} + class TreePool extends Disposable { private _pool: ResourcePool>; @@ -1419,6 +1522,10 @@ function isCommandButtonRenderData(item: IChatRenderData): item is IChatCommandB return item && 'kind' in item && item.kind === 'command'; } +function isTextEditRenderData(item: IChatRenderData): item is IChatTextEdit { + return item && 'kind' in item && item.kind === 'textEdit'; +} + function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { return item && 'renderedWordCount' in item; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index a6cbb7ae100..d54cfb7cfae 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -28,7 +28,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityProvider } from 'vs/workbench/contrib/chat/browser/chatAccessibilityProvider'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { IChatListItemRendererOptions } from './chat'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; @@ -276,7 +277,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.createInput(this.container, { renderFollowups: true, renderStyle }); } - this.createList(this.listContainer, { renderStyle, editableCodeBlock: this.viewOptions.editableCodeBlocks }); + this.createList(this.listContainer, { ...this.viewOptions.rendererOptions, renderStyle }); this._register(this.editorOptions.onDidChange(() => this.onDidStyleChange())); this.onDidStyleChange(); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 9b89c817381..e5e276fbf6d 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -9,15 +9,15 @@ import * as dom from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IDiffEditorViewModel, ScrollType } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -45,6 +45,10 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IMarkdownVulnerability } from '../common/annotations'; import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { TextEdit } from 'vs/editor/common/languages'; +import { CancellationToken } from 'vs/base/common/cancellation'; const $ = dom.$; @@ -416,3 +420,286 @@ export class ChatCodeBlockContentProvider extends Disposable implements ITextMod return this._modelService.createModel('', null, resource); } } + +// + +export interface ICodeCompareBlockActionContext { + element: ChatTreeItem; + readonly uri: URI; + readonly edits: readonly TextEdit[]; +} + +export interface ICodeCompareBlockData { + readonly element: ChatTreeItem; + + readonly originalTextModel: Promise; + readonly modifiedTextModel: Promise; + readonly edits: readonly TextEdit[]; + + readonly parentContextKeyService?: IContextKeyService; + readonly hideToolbar?: boolean; +} + + +export class CodeCompareBlockPart extends Disposable { + protected readonly _onDidChangeContentHeight = this._register(new Emitter()); + public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; + + private readonly contextKeyService: IContextKeyService; + public readonly diffEditor: DiffEditorWidget; + protected readonly toolbar: MenuWorkbenchToolBar; + public readonly element: HTMLElement; + + private readonly _lastDiffEditorViewModel = this._store.add(new MutableDisposable()); + private currentScrollWidth = 0; + + constructor( + private readonly options: ChatEditorOptions, + readonly menuId: MenuId, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IModelService protected readonly modelService: IModelService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + ) { + super(); + this.element = $('.interactive-result-code-block'); + this.element.classList.add('compare'); + + this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); + const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const editorElement = dom.append(this.element, $('.interactive-result-editor')); + this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, { + ...getSimpleEditorOptions(this.configurationService), + readOnly: true, + lineNumbers: 'on', + selectOnLineNumbers: true, + scrollBeyondLastLine: false, + lineDecorationsWidth: 12, + dragAndDrop: false, + padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, + mouseWheelZoom: false, + scrollbar: { + vertical: 'hidden', + alwaysConsumeMouseWheel: false + }, + definitionLinkOpensInPeek: false, + gotoLocation: { + multiple: 'goto', + multipleDeclarations: 'goto', + multipleDefinitions: 'goto', + multipleImplementations: 'goto', + }, + ariaLabel: localize('chat.codeBlockHelp', 'Code block'), + overflowWidgetsDomNode, + ...this.getEditorOptionsFromConfig(), + }); + + const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); + const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(toolbarElement); + const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); + this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { + menuOptions: { + shouldForwardArgs: true + } + })); + + + this._register(this.toolbar.onDidChangeDropdownVisibility(e => { + toolbarElement.classList.toggle('force-visibility', e); + })); + + this._configureForScreenReader(); + this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); + this._register(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) { + this._configureForScreenReader(); + } + })); + + this._register(this.options.onDidChange(() => { + this.diffEditor.updateOptions(this.getEditorOptionsFromConfig()); + })); + + this._register(this.diffEditor.getModifiedEditor().onDidScrollChange(e => { + this.currentScrollWidth = e.scrollWidth; + })); + this._register(this.diffEditor.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._onDidChangeContentHeight.fire(); + } + })); + this._register(this.diffEditor.getModifiedEditor().onDidBlurEditorWidget(() => { + this.element.classList.remove('focused'); + WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.stopHighlighting(); + this.clearWidgets(); + })); + this._register(this.diffEditor.getModifiedEditor().onDidFocusEditorWidget(() => { + this.element.classList.add('focused'); + WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.restoreViewState(true); + })); + + + // Parent list scrolled + if (delegate.onDidScroll) { + this._register(delegate.onDidScroll(e => { + this.clearWidgets(); + })); + } + } + + get uri(): URI | undefined { + return this.diffEditor.getModifiedEditor().getModel()?.uri; + } + + private createDiffEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): DiffEditorWidget { + const widgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + + WordHighlighterContribution.ID, + ViewportSemanticTokensContribution.ID, + BracketMatchingController.ID, + SmartSelectController.ID, + HoverController.ID, + GotoDefinitionAtPositionEditorContribution.ID, + ]) + }; + + return this._register(instantiationService.createInstance(DiffEditorWidget, parent, { + scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, + renderMarginRevertIcon: false, + diffCodeLens: false, + scrollBeyondLastLine: false, + stickyScroll: { enabled: false }, + originalAriaLabel: localize('original', 'Original'), + modifiedAriaLabel: localize('modified', 'Modified'), + diffAlgorithm: 'advanced', + readOnly: true, + isInEmbeddedEditor: true, + useInlineViewWhenSpaceIsLimited: false, + hideUnchangedRegions: { enabled: true, contextLineCount: 1 }, + ...options + }, { originalEditor: widgetOptions, modifiedEditor: widgetOptions })); + } + + focus(): void { + this.diffEditor.focus(); + } + + private updatePaddingForLayout() { + // scrollWidth = "the width of the content that needs to be scrolled" + // contentWidth = "the width of the area where content is displayed" + const horizontalScrollbarVisible = this.currentScrollWidth > this.diffEditor.getModifiedEditor().getLayoutInfo().contentWidth; + const scrollbarHeight = this.diffEditor.getModifiedEditor().getLayoutInfo().horizontalScrollbarHeight; + const bottomPadding = horizontalScrollbarVisible ? + Math.max(defaultCodeblockPadding - scrollbarHeight, 2) : + defaultCodeblockPadding; + this.diffEditor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } }); + } + + private _configureForScreenReader(): void { + const toolbarElt = this.toolbar.getElement(); + if (this.accessibilityService.isScreenReaderOptimized()) { + toolbarElt.style.display = 'block'; + toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); + } else { + toolbarElt.style.display = ''; + } + } + + private getEditorOptionsFromConfig(): IEditorOptions { + return { + wordWrap: this.options.configuration.resultEditor.wordWrap, + fontLigatures: this.options.configuration.resultEditor.fontLigatures, + bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization, + fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ? + EDITOR_FONT_DEFAULTS.fontFamily : + this.options.configuration.resultEditor.fontFamily, + fontSize: this.options.configuration.resultEditor.fontSize, + fontWeight: this.options.configuration.resultEditor.fontWeight, + lineHeight: this.options.configuration.resultEditor.lineHeight, + }; + } + + layout(width: number): void { + const contentHeight = this.getContentHeight(); + const editorBorder = 2; + const dimension = { width: width - editorBorder, height: contentHeight }; + this.element.style.height = `${dimension.height}px`; + this.element.style.width = `${dimension.width}px`; + this.diffEditor.layout(dimension); + this.updatePaddingForLayout(); + } + + private getContentHeight() { + return this.diffEditor.getContentHeight(); + } + + async render(data: ICodeCompareBlockData, width: number, token: CancellationToken) { + if (data.parentContextKeyService) { + this.contextKeyService.updateParent(data.parentContextKeyService); + } + + if (this.options.configuration.resultEditor.wordWrap === 'on') { + // Initialize the editor with the new proper width so that getContentHeight + // will be computed correctly in the next call to layout() + this.layout(width); + } + + await this.updateEditor(data, token); + + this.layout(width); + this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") }); + + if (data.hideToolbar) { + dom.hide(this.toolbar.getElement()); + } else { + dom.show(this.toolbar.getElement()); + } + } + + reset() { + this.clearWidgets(); + } + + private clearWidgets() { + HoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); + HoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover(); + } + + private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise { + + const originalTextModel = await data.originalTextModel; + const modifiedTextModel = await data.modifiedTextModel; + + if (!originalTextModel || !modifiedTextModel) { + return; + } + + const viewModel = this.diffEditor.createViewModel({ + original: originalTextModel, + modified: modifiedTextModel + }); + + await viewModel.waitForDiff(); + + if (token.isCancellationRequested) { + return; + } + + this.diffEditor.setModel(viewModel); + this._lastDiffEditorViewModel.value = viewModel; + + this.toolbar.context = { + uri: originalTextModel.uri, + edits: data.edits, + element: data.element, + } satisfies ICodeCompareBlockActionContext; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 84913ffc2c2..17813f0d464 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -8,19 +8,17 @@ import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; import { revive } from 'vs/base/common/marshalling'; -import { basename } from 'vs/base/common/resources'; +import { basename, isEqual } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; -import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatPromptVariableData { @@ -54,7 +52,8 @@ export type IChatProgressResponseContent = | IChatTreeData | IChatContentInlineReference | IChatProgressMessage - | IChatCommandButton; + | IChatCommandButton + | IChatTextEdit; export type IChatProgressRenderableResponseContent = Exclude; @@ -78,7 +77,6 @@ export interface IChatResponseModel { readonly slashCommand?: IChatAgentCommand; readonly agentOrSlashCommandDetected: boolean; readonly response: IResponse; - readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ @@ -180,8 +178,25 @@ export class Response implements IResponse { } else { this._responseParts[responsePartLength] = { content: new MarkdownString(lastResponsePart.content.value + progress.content, lastResponsePart.content), kind: 'markdownContent' }; } - this._updateRepr(quiet); + + } else if (progress.kind === 'textEdit') { + if (progress.edits.length > 0) { + // merge text edits for the same file no matter when they come in + let found = false; + for (let i = 0; !found && i < this._responseParts.length; i++) { + const candidate = this._responseParts[i]; + if (candidate.kind === 'textEdit' && isEqual(candidate.uri, progress.uri)) { + candidate.edits.push(...progress.edits); + found = true; + } + } + if (!found) { + this._responseParts.push(progress); + } + this._updateRepr(quiet); + } + } else { this._responseParts.push(progress); this._updateRepr(quiet); @@ -196,6 +211,8 @@ export class Response implements IResponse { return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); } else if (part.kind === 'command') { return part.command.title; + } else if (part.kind === 'textEdit') { + return ''; } else { return part.content.value; } @@ -239,11 +256,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._response; } - private _edits: ResourceMap; - public get edits(): ResourceMap { - return this._edits; - } - public get result(): IChatAgentResult | undefined { return this._result; } @@ -314,7 +326,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._followups = followups ? [...followups] : undefined; this._response = new Response(_response); - this._edits = new ResourceMap(); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); this._id = 'response_' + ChatResponseModel.nextId++; } @@ -326,16 +337,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._response.updateContent(responsePart, quiet); } - updateTextEdits(uri: URI, edits: TextEdit[]) { - const array = this._edits.get(uri); - if (!array) { - this._edits.set(uri, edits); - } else { - array.push(...edits); - } - this._onDidChange.fire(); - } - /** * Apply one of the progress updates that are not part of the actual response content. */ @@ -735,7 +736,7 @@ export class ChatModel extends Disposable implements IChatModel { if (progress.kind === 'vulnerability') { request.response.updateContent({ kind: 'markdownVuln', content: { value: progress.content }, vulnerabilities: progress.vulnerabilities }, quiet); - } else if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command') { + } else if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || progress.kind === 'textEdit') { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); @@ -744,8 +745,6 @@ export class ChatModel extends Disposable implements IChatModel { if (agent) { request.response.setAgent(agent, progress.command); } - } else if (progress.kind === 'textEdit') { - request.response.updateTextEdits(progress.uri, progress.edits); } else { this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 0892f679d49..b03b90f16cf 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -5,18 +5,16 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; import { marked } from 'vs/base/common/marked/marked'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTextEdit, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; @@ -95,7 +93,7 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton; +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEdit; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } @@ -124,7 +122,6 @@ export interface IChatResponseViewModel { readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; readonly progressMessages: ReadonlyArray; - readonly edits: ResourceMap; readonly isComplete: boolean; readonly isCanceled: boolean; readonly isStale: boolean; @@ -410,10 +407,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.progressMessages; } - get edits(): ResourceMap { - return this._model.edits; - } - get isComplete() { return this._model.isComplete; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index ecbce151de7..d13c2fae2dc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -50,6 +50,7 @@ import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineC import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { isEqual } from 'vs/base/common/resources'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -697,10 +698,22 @@ export class InlineChatController implements IEditorContribution { return; } + // if ("1") { + // return; + // } + // TODO@jrieken const editsShouldBeInstant = false; - const edits = response.edits.get(this._session!.textModelN.uri) ?? []; + const edits = response.response.value.map(part => { + if (part.kind === 'textEdit' && isEqual(part.uri, this._session?.textModelN.uri)) { + return part.edits; + } else { + return []; + } + }).flat(); + + // const edits = response.edits.get(this._session!.textModelN.uri) ?? []; const newEdits = edits.slice(lastLength); // console.log('NEW edits', newEdits, edits); if (newEdits.length === 0) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 304648019ea..b63d0d4355f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -25,7 +25,6 @@ import { isEqual } from 'vs/base/common/resources'; import { IInlineChatSessionService, Recording } from './inlineChatSessionService'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { asRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { coalesceInPlace } from 'vs/base/common/arrays'; import { Iterable } from 'vs/base/common/iterator'; import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; @@ -660,8 +659,8 @@ export class HunkData { const textModelNDecorations: string[] = []; const textModel0Decorations: string[] = []; - textModelNDecorations.push(accessorN.addDecoration(asRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(asRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); + textModelNDecorations.push(accessorN.addDecoration(LineRange.asRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); + textModel0Decorations.push(accessor0.addDecoration(LineRange.asRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); for (const change of hunk.changes) { textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 525cb78569a..eb0c805608f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -40,7 +40,6 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; @@ -55,6 +54,7 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; export interface InlineChatWidgetViewState { @@ -85,15 +85,9 @@ export interface IInlineChatWidgetConstructionOptions { */ feedbackMenuId?: MenuId; - /** - * @deprecated - * TODO@meganrogge,jrieken - * We need a way to make this configurable per editor/resource and not - * globally. - */ - editableCodeBlocks?: boolean; - editorOverflowWidgetsDomNode?: HTMLElement; + + rendererOptions?: IChatListItemRendererOptions; } export interface IInlineChatMessage { @@ -182,7 +176,7 @@ export class InlineChatWidget { renderInputOnTop: true, supportsFileReferences: true, editorOverflowWidgetsDomNode: options.editorOverflowWidgetsDomNode, - editableCodeBlocks: options.editableCodeBlocks, + rendererOptions: options.rendererOptions, menus: { executeToolbar: options.inputMenuId, inputSideToolbar: options.widgetMenuId, @@ -753,10 +747,10 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { return; } - const hiddenOriginal = invertLineRange(originalLineRange, textModel0); - const hiddenModified = invertLineRange(modifiedLineRange, textModelN); - this._previewDiffEditor.value.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lr => asRange(lr, textModel0)), 'diff-hidden'); - this._previewDiffEditor.value.getModifiedEditor().setHiddenAreas(hiddenModified.map(lr => asRange(lr, textModelN)), 'diff-hidden'); + const hiddenOriginal = LineRange.invert(originalLineRange, textModel0); + const hiddenModified = LineRange.invert(modifiedLineRange, textModelN); + this._previewDiffEditor.value.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lr => LineRange.asRange(lr, textModel0)), 'diff-hidden'); + this._previewDiffEditor.value.getModifiedEditor().setHiddenAreas(hiddenModified.map(lr => LineRange.asRange(lr, textModelN)), 'diff-hidden'); this._previewDiffEditor.value.revealLine(modifiedLineRange.startLineNumber, ScrollType.Immediate); this._onDidChangeHeight.fire(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 9d28e8d87a4..287c7ab529f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -17,6 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { isEqual } from 'vs/base/common/resources'; export class InlineChatZoneWidget extends ZoneWidget { @@ -57,6 +58,13 @@ export class InlineChatZoneWidget extends ZoneWidget { } } } + }, + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return isEqual(uri, editor.getModel()?.uri) + // && !"true" + ; + }, } }); this._disposables.add(this.widget.onDidChangeHeight(() => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/src/vs/workbench/contrib/inlineChat/browser/utils.ts index ef84a63f4e9..0460ee11f02 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/utils.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { LineRange } from 'vs/editor/common/core/lineRange'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { IRange } from 'vs/editor/common/core/range'; import { IIdentifiedSingleEditOperation, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; import { IEditObserver } from './inlineChatStrategies'; import { IProgress } from 'vs/platform/progress/common/progress'; @@ -13,25 +12,7 @@ import { IntervalTimer, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -export function invertLineRange(range: LineRange, model: ITextModel): LineRange[] { - if (range.isEmpty) { - return []; - } - const result: LineRange[] = []; - if (range.startLineNumber > 1) { - result.push(new LineRange(1, range.startLineNumber)); - } - if (range.endLineNumberExclusive < model.getLineCount() + 1) { - result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); - } - return result.filter(r => !r.isEmpty); -} -export function asRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); -} // --- async edit diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index f92ef6b4468..23e16594725 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -67,7 +67,7 @@ export class TerminalChatWidget extends Disposable { }, feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, telemetrySource: 'terminal-inline-chat', - editableCodeBlocks: true + rendererOptions: { editableCodeBlock: true } } ); this._register(Event.any( @@ -183,4 +183,3 @@ export class TerminalChatWidget extends Disposable { return this._focusTracker; } } - diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 59487cbe55d..e83bb2ab8e2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -122,6 +122,18 @@ declare module 'vscode' { ranges: Range[]; } + export class ChatResponseTextEditPart { + uri: Uri; + edits: TextEdit[]; + constructor(uri: Uri, edits: TextEdit | TextEdit[]); + } + + export interface ChatResponseStream { + textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; + + push(part: ChatResponsePart | ChatResponseTextEditPart): ChatResponseStream; + } + // TODO@API fit this into the stream export interface ChatUsedContext { documents: ChatDocumentContext[];