From 4f2c166752c938b98b14244acb4003126536278f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 8 Jan 2025 18:36:12 +0100 Subject: [PATCH 1/2] Use ICodeMapperService for 'Apply In Editor' --- .../api/browser/mainThreadChatCodeMapper.ts | 6 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostCodeMapper.ts | 19 +- .../browser/actions/codeBlockOperations.ts | 198 +++++------------- .../contrib/chat/browser/tools/tools.ts | 3 +- .../chat/common/chatCodeMapperService.ts | 152 +------------- .../vscode.proposed.mappedEditsProvider.d.ts | 1 - 7 files changed, 72 insertions(+), 309 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index abacb824c24b7..eea3e819b976f 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -25,15 +25,15 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper); } - $registerCodeMapperProvider(handle: number): void { + $registerCodeMapperProvider(handle: number, displayName: string): void { const impl: ICodeMapperProvider = { + displayName, mapCode: async (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => { const requestId = String(MainThreadChatCodemapper._requestHandlePool++); this._responseMap.set(requestId, response); const extHostRequest: ICodeMapperRequestDto = { requestId, - codeBlocks: uiRequest.codeBlocks, - conversation: uiRequest.conversation + codeBlocks: uiRequest.codeBlocks }; try { return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7bccc00f5b9c0..9b7b0424e7a87 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1299,7 +1299,7 @@ export interface ICodeMapperTextEdit { export type ICodeMapperProgressDto = Dto; export interface MainThreadCodeMapperShape extends IDisposable { - $registerCodeMapperProvider(handle: number): void; + $registerCodeMapperProvider(handle: number, displayName: string): void; $unregisterCodeMapperProvider(handle: number): void; $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise; } diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 0f6b9d1292238..4a421e6f23d73 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import * as extHostProtocol from './extHost.protocol.js'; -import { ChatAgentResult, DocumentContextItem, TextEdit } from './extHostTypeConverters.js'; +import { TextEdit } from './extHostTypeConverters.js'; import { URI } from '../../../base/common/uri.js'; export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { @@ -48,21 +48,6 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape resource: URI.revive(block.resource), markdownBeforeBlock: block.markdownBeforeBlock }; - }), - conversation: internalRequest.conversation.map(item => { - if (item.type === 'request') { - return { - type: 'request', - message: item.message - } satisfies vscode.ConversationRequest; - } else { - return { - type: 'response', - message: item.message, - result: item.result ? ChatAgentResult.to(item.result) : undefined, - references: item.references?.map(DocumentContextItem.to) - } satisfies vscode.ConversationResponse; - } }) }; @@ -72,7 +57,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { const handle = ExtHostCodeMapper._providerHandlePool++; - this._proxy.$registerCodeMapperProvider(handle); + this._proxy.$registerCodeMapperProvider(handle, extension.displayName ?? extension.name); this.providers.set(handle, provider); return { dispose: () => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 5462565f4ab3a..c43362c46504b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../../base/common/arrays.js'; import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; @@ -10,28 +9,27 @@ import { CharCode } from '../../../../../base/common/charCode.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { ConversationRequest, ConversationResponse, DocumentContextItem, IWorkspaceFileEdit, IWorkspaceTextEdit } from '../../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; -import { getReferencesAsDocumentContext } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; -import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; export class InsertCodeBlockOperation { @@ -98,7 +96,7 @@ export class InsertCodeBlockOperation { } } -type IComputeEditsResult = { readonly edits?: Array; readonly codeMapper?: string }; +type IComputeEditsResult = { readonly editsProposed: boolean; readonly codeMapper?: string }; export class ApplyCodeBlockOperation { @@ -107,15 +105,13 @@ export class ApplyCodeBlockOperation { constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, - @IBulkEditService private readonly bulkEditService: IBulkEditService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IChatService private readonly chatService: IChatService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IProgressService private readonly progressService: IProgressService, @ILanguageService private readonly languageService: ILanguageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService ) { } @@ -166,7 +162,7 @@ export class ApplyCodeBlockOperation { codeBlockIndex: context.codeBlockIndex, totalCharacters: context.code.length, codeMapper: result?.codeMapper, - editsProposed: !!result?.edits, + editsProposed: !!result?.editsProposed }); } @@ -187,101 +183,60 @@ export class ApplyCodeBlockOperation { return undefined; } - const result = await this.computeEdits(codeEditor, codeBlockContext); - if (result.edits) { - const showWithPreview = await this.applyWithInlinePreview(result.edits, codeEditor); - if (!showWithPreview) { - await this.bulkEditService.apply(result.edits, { showPreview: true }); - const activeModel = codeEditor.getModel(); - this.codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); - } - } - return result; - } + const cancellationTokenSource = new CancellationTokenSource(); + try { + const activeModel = codeEditor.getModel(); + const resource = codeBlockContext.codemapperUri ?? activeModel.uri; + let codeMapper: string | undefined; - private async computeEdits(codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const activeModel = codeEditor.getModel(); - - const mappedEditsProviders = this.languageFeaturesService.mappedEditsProvider.ordered(activeModel); - if (mappedEditsProviders.length > 0) { - - // 0th sub-array - editor selections array if there are any selections - // 1st sub-array - array with documents used to get the chat reply - const docRefs: DocumentContextItem[][] = []; - collectDocumentContextFromSelections(codeEditor, docRefs); - collectDocumentContextFromContext(codeBlockActionContext, docRefs); - - const cancellationTokenSource = new CancellationTokenSource(); - let codeMapper; // the last used code mapper - try { - const result = await this.progressService.withProgress( - { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, - async progress => { - for (const provider of mappedEditsProviders) { - codeMapper = provider.displayName; - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const mappedEdits = await provider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { - documents: docRefs, - conversation: getChatConversation(codeBlockActionContext), - }, - cancellationTokenSource.token - ); - if (mappedEdits) { - return { edits: mappedEdits.edits, codeMapper }; - } - } - return undefined; - }, - () => cancellationTokenSource.cancel() - ); - if (result) { - return result; - } - } catch (e) { - if (!isCancellationError(e)) { - this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); + const iterable = new AsyncIterableObject(async executor => { + const request: ICodeMapperRequest = { + codeBlocks: [{ code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }] + }; + const response: ICodeMapperResponse = { + textEdit: (target: URI, edit: TextEdit[]) => { + executor.emitOne(edit); + } + }; + const result = await this.codeMapperService.mapCode(request, response, cancellationTokenSource.token); + codeMapper = result?.providerName; + if (result?.errorMessage) { + executor.reject(new Error(result.errorMessage)); } - } finally { - cancellationTokenSource.dispose(); + }); + + const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); + let result = false; + if (editorToApply && editorToApply.hasModel()) { + result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); } - return { edits: [], codeMapper }; + return { editsProposed: result, codeMapper }; + } catch (e) { + if (!isCancellationError(e)) { + this.notify(localize('applyCodeBlock.error', "An error occurred while applying the code block.")); + } + } finally { + cancellationTokenSource.dispose(); } - return { edits: [], codeMapper: undefined }; + return undefined; + } + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { + const inlineChatController = InlineChatController.get(codeEditor); + if (inlineChatController) { + let isOpen = true; + const promise = inlineChatController.reviewEdits(codeEditor.getSelection(), edits, tokenSource.token); + promise.finally(() => { + isOpen = false; + tokenSource.dispose(); + }); + this.inlineChatPreview = { + promise, + isOpen: () => isOpen, + cancel: () => tokenSource.cancel(), + }; + return true; - private async applyWithInlinePreview(edits: Array, codeEditor: IActiveCodeEditor): Promise { - const firstEdit = edits[0]; - if (!ResourceTextEdit.is(firstEdit)) { - return false; - } - const resource = firstEdit.resource; - const textEdits = coalesce(edits.map(edit => ResourceTextEdit.is(edit) && isEqual(resource, edit.resource) ? edit.textEdit : undefined)); - if (textEdits.length !== edits.length) { // more than one file has changed, fall back to bulk edit preview - return false; - } - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - if (editorToApply) { - const inlineChatController = InlineChatController.get(editorToApply); - if (inlineChatController) { - const tokenSource = new CancellationTokenSource(); - let isOpen = true; - const firstEdit = textEdits[0]; - editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - const promise = inlineChatController.reviewEdits(textEdits[0].range, AsyncIterableObject.fromArray([textEdits]), tokenSource.token); - promise.finally(() => { - isOpen = false; - tokenSource.dispose(); - }); - this.inlineChatPreview = { - promise, - isOpen: () => isOpen, - cancel: () => tokenSource.cancel(), - }; - return true; - } } return false; } @@ -360,49 +315,6 @@ function isReadOnly(model: ITextModel, textFileService: ITextFileService): boole return !!activeTextModel?.isReadonly(); } -function collectDocumentContextFromSelections(codeEditor: IActiveCodeEditor, result: DocumentContextItem[][]): void { - const activeModel = codeEditor.getModel(); - const currentDocUri = activeModel.uri; - const currentDocVersion = activeModel.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - result.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } -} - - -function collectDocumentContextFromContext(context: ICodeBlockActionContext, result: DocumentContextItem[][]): void { - if (isResponseVM(context.element) && context.element.usedContext?.documents) { - result.push(context.element.usedContext.documents); - } -} - -function getChatConversation(context: ICodeBlockActionContext): (ConversationRequest | ConversationResponse)[] { - // TODO@aeschli for now create a conversation with just the current element - // this will be expanded in the future to include the request and any other responses - - if (isResponseVM(context.element)) { - return [{ - type: 'response', - message: context.element.response.getMarkdown(), - references: getReferencesAsDocumentContext(context.element.contentReferences) - }]; - } else if (isRequestVM(context.element)) { - return [{ - type: 'request', - message: context.element.messageText, - }]; - } else { - return []; - } -} - function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string { const newContent = strings.splitLines(codeBlockContent); if (newContent.length === 0) { diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts index 359b47eb4548b..53320370c5a0b 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -125,8 +125,7 @@ class EditTool implements IToolData, IToolImpl { } const result = await this.codeMapperService.mapCode({ - codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], - conversation: [] + codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }] }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 097ca6d89157c..f8aab6da4b340 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -4,18 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { CharCode } from '../../../../base/common/charCode.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { splitLinesIncludeSeparators } from '../../../../base/common/strings.js'; -import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { DocumentContextItem, isLocation, TextEdit } from '../../../../editor/common/languages.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatAgentResult } from './chatAgents.js'; -import { IChatResponseModel } from './chatModel.js'; -import { IChatContentReference } from './chatService.js'; - export interface ICodeMapperResponse { textEdit: (resource: URI, textEdit: TextEdit[]) => void; @@ -27,21 +19,8 @@ export interface ICodeMapperCodeBlock { readonly markdownBeforeBlock?: string; } -export interface ConversationRequest { - readonly type: 'request'; - readonly message: string; -} - -export interface ConversationResponse { - readonly type: 'response'; - readonly message: string; - readonly result?: IChatAgentResult; - readonly references?: DocumentContextItem[]; -} - export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; - readonly conversation: (ConversationResponse | ConversationRequest)[]; } export interface ICodeMapperResult { @@ -49,16 +28,20 @@ export interface ICodeMapperResult { } export interface ICodeMapperProvider { + readonly displayName: string; mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export const ICodeMapperService = createDecorator('codeMapperService'); +export interface ICodeMapperServiceResult extends ICodeMapperResult { + readonly providerName: string; +} + export interface ICodeMapperService { readonly _serviceBrand: undefined; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; - mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; - mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken): Promise; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { @@ -82,127 +65,12 @@ export class CodeMapperService implements ICodeMapperService { for (const provider of this.providers) { const result = await provider.mapCode(request, response, token); if (result) { - return result; + return { providerName: provider.displayName, ...result }; + } else if (token.isCancellationRequested) { + return undefined; } } return undefined; } - - async mapCodeFromResponse(responseModel: IChatResponseModel, response: ICodeMapperResponse, token: CancellationToken) { - const fenceLanguageRegex = /^`{3,}/; - const codeBlocks: ICodeMapperCodeBlock[] = []; - - const currentBlock = []; - const markdownBeforeBlock = []; - let currentBlockUri = undefined; - - let fence = undefined; // if set, we are in a block - - for (const lineOrUri of iterateLinesOrUris(responseModel)) { - if (isString(lineOrUri)) { - const fenceLanguageIdMatch = lineOrUri.match(fenceLanguageRegex); - if (fenceLanguageIdMatch) { - // we found a line that starts with a fence - if (fence !== undefined && fenceLanguageIdMatch[0] === fence) { - // we are in a code block and the fence matches the opening fence: Close the code block - fence = undefined; - if (currentBlockUri) { - // report the code block if we have a URI - codeBlocks.push({ code: currentBlock.join(''), resource: currentBlockUri, markdownBeforeBlock: markdownBeforeBlock.join('') }); - currentBlock.length = 0; - markdownBeforeBlock.length = 0; - currentBlockUri = undefined; - } - } else { - // we are not in a code block. Open the block - fence = fenceLanguageIdMatch[0]; - } - } else { - if (fence !== undefined) { - currentBlock.push(lineOrUri); - } else { - markdownBeforeBlock.push(lineOrUri); - } - } - } else { - currentBlockUri = lineOrUri; - } - } - const conversation: (ConversationRequest | ConversationResponse)[] = []; - for (const request of responseModel.session.getRequests()) { - const response = request.response; - if (!response || response === responseModel) { - break; - } - conversation.push({ - type: 'request', - message: request.message.text - }); - conversation.push({ - type: 'response', - message: response.response.getMarkdown(), - result: response.result, - references: getReferencesAsDocumentContext(response.contentReferences) - }); - } - return this.mapCode({ codeBlocks, conversation }, response, token); - } -} - -function iterateLinesOrUris(responseModel: IChatResponseModel): Iterable { - return { - *[Symbol.iterator](): Iterator { - let lastIncompleteLine = undefined; - for (const part of responseModel.response.value) { - if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { - const lines = splitLinesIncludeSeparators(part.content.value); - if (lines.length > 0) { - if (lastIncompleteLine !== undefined) { - lines[0] = lastIncompleteLine + lines[0]; // merge the last incomplete line with the first markdown line - } - lastIncompleteLine = isLineIncomplete(lines[lines.length - 1]) ? lines.pop() : undefined; - for (const line of lines) { - yield line; - } - } - } else if (part.kind === 'codeblockUri') { - yield part.uri; - } - } - if (lastIncompleteLine !== undefined) { - yield lastIncompleteLine; - } - } - }; -} - -function isLineIncomplete(line: string) { - const lastChar = line.charCodeAt(line.length - 1); - return lastChar !== CharCode.LineFeed && lastChar !== CharCode.CarriageReturn; } - -export function getReferencesAsDocumentContext(res: readonly IChatContentReference[]): DocumentContextItem[] { - const map = new ResourceMap(); - for (const r of res) { - let uri; - let range; - if (URI.isUri(r.reference)) { - uri = r.reference; - } else if (isLocation(r.reference)) { - uri = r.reference.uri; - range = r.reference.range; - } - if (uri) { - const item = map.get(uri); - if (item) { - if (range) { - item.ranges.push(range); - } - } else { - map.set(uri, { uri, version: -1, ranges: range ? [range] : [] }); - } - } - } - return [...map.values()]; -} diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 04d95579f4d2e..fb10b6a3385ca 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -57,7 +57,6 @@ declare module 'vscode' { export interface MappedEditsRequest { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; - readonly conversation: Array; // for every prior response that contains codeblocks, make sure we pass the code as well as the resources based on the reported codemapper URIs } export interface MappedEditsResponseStream { From 051d2dcfa25cedd28fef871dfeb74792a48bc3cd Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 9 Jan 2025 15:42:35 +0100 Subject: [PATCH 2/2] apply edits: show progress while computing edits --- .../browser/actions/codeBlockOperations.ts | 111 ++++++++++++------ .../browser/chatEditing/chatEditingSession.ts | 5 + .../chat/common/chatCodeMapperService.ts | 14 +-- 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index c43362c46504b..5fafadbd6c02d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { CharCode } from '../../../../../base/common/charCode.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import { isEqual } from '../../../../../base/common/resources.js'; @@ -21,13 +21,14 @@ import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; -import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; import { isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; @@ -111,7 +112,8 @@ export class ApplyCodeBlockOperation { @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, - @ICodeMapperService private readonly codeMapperService: ICodeMapperService + @ICodeMapperService private readonly codeMapperService: ICodeMapperService, + @IProgressService private readonly progressService: IProgressService ) { } @@ -178,49 +180,88 @@ export class ApplyCodeBlockOperation { } private async handleTextEditor(codeEditor: IActiveCodeEditor, codeBlockContext: ICodeBlockActionContext): Promise { - if (isReadOnly(codeEditor.getModel(), this.textFileService)) { + const activeModel = codeEditor.getModel(); + if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const cancellationTokenSource = new CancellationTokenSource(); - try { - const activeModel = codeEditor.getModel(); - const resource = codeBlockContext.codemapperUri ?? activeModel.uri; - let codeMapper: string | undefined; - - const iterable = new AsyncIterableObject(async executor => { - const request: ICodeMapperRequest = { - codeBlocks: [{ code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }] - }; - const response: ICodeMapperResponse = { - textEdit: (target: URI, edit: TextEdit[]) => { - executor.emitOne(edit); - } - }; - const result = await this.codeMapperService.mapCode(request, response, cancellationTokenSource.token); - codeMapper = result?.providerName; - if (result?.errorMessage) { - executor.reject(new Error(result.errorMessage)); - } - }); + const resource = codeBlockContext.codemapperUri ?? activeModel.uri; + const codeBlock = { code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }; + + const codeMapper = this.codeMapperService.providers[0]?.displayName; + if (!codeMapper) { + this.notify(localize('applyCodeBlock.noCodeMapper', "No code mapper available.")); + return undefined; + } - const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); - let result = false; - if (editorToApply && editorToApply.hasModel()) { + const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor); + let result = false; + if (editorToApply && editorToApply.hasModel()) { + + const cancellationTokenSource = new CancellationTokenSource(); + try { + const iterable = await this.progressService.withProgress>( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); + const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + return await this.waitForFirstElement(editsIterable); + }, + () => cancellationTokenSource.cancel() + ); result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource); + } catch (e) { + if (!isCancellationError(e)) { + this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); + } + } finally { + cancellationTokenSource.dispose(); } - return { editsProposed: result, codeMapper }; - } catch (e) { - if (!isCancellationError(e)) { - this.notify(localize('applyCodeBlock.error', "An error occurred while applying the code block.")); + } + return { + editsProposed: result, + codeMapper + }; + } + + private getEdits(codeBlock: ICodeMapperCodeBlock, token: CancellationToken): AsyncIterable { + return new AsyncIterableObject(async executor => { + const request: ICodeMapperRequest = { + codeBlocks: [codeBlock] + }; + const response: ICodeMapperResponse = { + textEdit: (target: URI, edit: TextEdit[]) => { + executor.emitOne(edit); + } + }; + const result = await this.codeMapperService.mapCode(request, response, token); + if (result?.errorMessage) { + executor.reject(new Error(result.errorMessage)); } - } finally { - cancellationTokenSource.dispose(); + }); + } + + private async waitForFirstElement(iterable: AsyncIterable): Promise> { + const iterator = iterable[Symbol.asyncIterator](); + const firstResult = await iterator.next(); + + if (firstResult.done) { + return { + async *[Symbol.asyncIterator]() { + return; + } + }; } - return undefined; + return { + async *[Symbol.asyncIterator]() { + yield firstResult.value; + yield* iterable; + } + }; } + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { const inlineChatController = InlineChatController.get(codeEditor); if (inlineChatController) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 134c3de87edb3..3845b0642f9c2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -640,6 +640,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.Other); } + /** + * Retrieves or creates a modified file entry. + * + * @returns The modified file entry. + */ private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise { const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)); if (existingEntry) { diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index f8aab6da4b340..809a37714805f 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -34,20 +34,17 @@ export interface ICodeMapperProvider { export const ICodeMapperService = createDecorator('codeMapperService'); -export interface ICodeMapperServiceResult extends ICodeMapperResult { - readonly providerName: string; -} - export interface ICodeMapperService { readonly _serviceBrand: undefined; + readonly providers: ICodeMapperProvider[]; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; - mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { _serviceBrand: undefined; - private readonly providers: ICodeMapperProvider[] = []; + public readonly providers: ICodeMapperProvider[] = []; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable { this.providers.push(provider); @@ -64,11 +61,10 @@ export class CodeMapperService implements ICodeMapperService { async mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) { for (const provider of this.providers) { const result = await provider.mapCode(request, response, token); - if (result) { - return { providerName: provider.displayName, ...result }; - } else if (token.isCancellationRequested) { + if (token.isCancellationRequested) { return undefined; } + return result; } return undefined; }