From 8732ffbe891657eb488d4a094c312c41f3740688 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Sun, 15 Sep 2024 22:09:46 +0200 Subject: [PATCH 01/10] registerMappedEditsProvider2 --- .../workbench/api/common/extHost.api.impl.ts | 4 ++ .../workbench/api/common/extHost.protocol.ts | 11 ++++ .../api/common/extHostLanguageFeatures.ts | 57 ++++++++++++++++--- .../vscode.proposed.mappedEditsProvider.d.ts | 27 +++++++++ 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a885606999e..2a09a8b1f41 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1439,6 +1439,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, + registerMappedEditsProvider2(provider: vscode.MappedEditsProvider2) { + checkProposedApiEnabled(extension, 'mappedEditsProvider'); + return extHostLanguageFeatures.registerMappedEditsProvider2(extension, provider); + }, createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { return extHostChatAgents2.createChatAgent(extension, id, handler); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 526e9666065..417748e7fd9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -392,6 +392,16 @@ export interface IMappedEditsContextDto { conversation?: IConversationItemDto[]; } +export interface IMappedEditsRequestDto { + readonly resource: UriComponents; + readonly codeBlock: string; + readonly conversation?: IConversationItemDto[]; +} + +export interface IMappedEditsResultDto { + readonly errorMessage?: string; +} + export interface ISignatureHelpProviderMetadataDto { readonly triggerCharacters: readonly string[]; readonly retriggerCharacters: readonly string[]; @@ -455,6 +465,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; + $registerMappedEditsProvider2(handle: number, displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 9459f74eb4f..cb294bec4be 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2133,6 +2133,12 @@ class DocumentDropEditAdapter { } } +const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ + uri: URI.revive(item.uri), + version: item.version, + ranges: item.ranges.map(r => typeConvert.Range.to(r)), +} satisfies vscode.DocumentContextItem); + class MappedEditsAdapter { constructor( @@ -2149,14 +2155,6 @@ class MappedEditsAdapter { const uri = URI.revive(resource); const doc = this._documents.getDocument(uri); - - const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => typeConvert.Range.to(r)), - } satisfies vscode.DocumentContextItem); - - const usedContext = context.documents.map(docSubArray => docSubArray.map(reviveContextItem)); const ctx = { @@ -2184,6 +2182,41 @@ class MappedEditsAdapter { } } +class MappedEditsAdapter2 { + + constructor( + private readonly _provider: vscode.MappedEditsProvider2, + ) { } + + async provideMappedEdits( + request: extHostProtocol.IMappedEditsRequestDto, + response: extHostProtocol.IMappedEditsResponseDto, + token: CancellationToken + ): Promise { + + const revivedRequest = { + resource: URI.revive(request.resource), + codeBlock: request.codeBlock, + conversation: request.conversation?.map(c => { + if (c.type === 'response') { + return { + type: 'response', + message: c.message, + references: c.references?.map(reviveContextItem) + } satisfies vscode.ConversationResponse; + } else { + return { + type: 'request', + message: c.message, + } satisfies vscode.ConversationRequest; + } + }) + } satisfies vscode.MappedEditsRequest; + + return this._provider.provideMappedEdits(revivedRequest, response, token); + } +} + type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | MultiDocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter @@ -2194,7 +2227,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; + | DocumentDropEditAdapter | MappedEditsAdapter | MappedEditsAdapter2 | NewSymbolNamesAdapter | InlineEditAdapter; class AdapterData { constructor( @@ -2920,6 +2953,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token); } + registerMappedEditsProvider2(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { + const handle = this._addNewAdapter(new MappedEditsAdapter2(provider), extension); + this._proxy.$registerMappedEditsProvider2(handle, extension.displayName ?? extension.name); + return this._createDisposable(handle); + } + // --- copy/paste actions registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 9b7ba15c596..2692669ffb4 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -52,7 +52,34 @@ declare module 'vscode' { ): ProviderResult; } + export interface MappedEditsRequest { + readonly resource: Uri; + readonly codeBlock: string; + readonly conversation?: (ConversationRequest | ConversationResponse)[]; + } + + export interface MappedEditsResposeStream { + edit(value: TextEdit, resource: Uri): void; + } + + export interface MappedEditsResult { + readonly errorMessage?: string; + } + + /** + * Interface for providing mapped edits for a given document. + */ + export interface MappedEditsProvider2 { + provideMappedEdits( + request: MappedEditsRequest, + result: MappedEditsResposeStream, + token: CancellationToken + ): ProviderResult; + } + namespace chat { export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable; + + export function registerMappedEditsProvider2(provider: MappedEditsProvider2): Disposable; } } From 6a81a01ead072cdde60dad56f4f0b38979093336 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 16 Sep 2024 18:08:43 +0200 Subject: [PATCH 02/10] WIP --- src/vs/editor/common/languages.ts | 6 +- .../api/browser/mainThreadChatCodeMapper.ts | 51 +++++++++++ .../api/browser/mainThreadLanguageFeatures.ts | 10 ++- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 39 ++++++++- .../workbench/api/common/extHostCodeMapper.ts | 57 +++++++++++++ .../api/common/extHostLanguageFeatures.ts | 56 ++----------- .../browser/actions/chatCodeblockActions.ts | 28 ++++++- .../chat/common/chatCodeMapperService.ts | 84 +++++++++++++++++++ .../vscode.proposed.mappedEditsProvider.d.ts | 11 ++- 10 files changed, 281 insertions(+), 65 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts create mode 100644 src/vs/workbench/api/common/extHostCodeMapper.ts create mode 100644 src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 4e3a0aa827c..cfd6734adc3 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -14,6 +14,9 @@ import { IMarkdownString } from '../../base/common/htmlContent.js'; import { IDisposable } from '../../base/common/lifecycle.js'; import { ThemeIcon } from '../../base/common/themables.js'; import { URI, UriComponents } from '../../base/common/uri.js'; +import { localize } from '../../nls.js'; +import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; +import { IMarkerData } from '../../platform/markers/common/markers.js'; import { EditOperation, ISingleEditOperation } from './core/editOperation.js'; import { IPosition, Position } from './core/position.js'; import { IRange, Range } from './core/range.js'; @@ -23,9 +26,6 @@ import { LanguageSelector } from './languageSelector.js'; import * as model from './model.js'; import { TokenizationRegistry as TokenizationRegistryImpl } from './tokenizationRegistry.js'; import { ContiguousMultilineTokens } from './tokens/contiguousMultilineTokens.js'; -import { localize } from '../../nls.js'; -import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; -import { IMarkerData } from '../../platform/markers/common/markers.js'; /** * @internal diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts new file mode 100644 index 00000000000..37bc2e61a0a --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { ICodeMapperProvider, ICodeMapperRequest, ICodeMapperResult, ICodeMapperService } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostCodeMapperShape, ExtHostContext, ICodeMapperProgressDto, MainContext, MainThreadCodeMapperShape } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadCodeMapper) +export class MainThreadChatCodemapper extends Disposable implements MainThreadCodeMapperShape { + + private providers = this._register(new DisposableMap()); + private readonly _proxy: ExtHostCodeMapperShape; + private static _requestHandlePool: number = 0; + private static _requestHandleMap: Map = new Map(); + + constructor( + extHostContext: IExtHostContext, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper); + } + $mapCode(request: ICodeMapperRequest, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } + + $registerCodeMapperProvider(handle: number): void { + const impl = { + mapCode: (request: ICodeMapperRequest, token: CancellationToken) => { + return this._proxy.$mapCode(handle, request, token).then((result) => result ?? undefined); + }, + dispose: () => { + this.$unregisterCodeMapperProvider(handle); + } + }; + + this.codeMapperService.registerCodeMapperProvider(handle, impl); + this.providers.set(handle, impl); + } + + $unregisterCodeMapperProvider(handle: number): void { + this.providers.deleteAndDispose(handle); + } + + $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c591ed677f1..b9442009a83 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -27,14 +27,14 @@ import { ILanguageFeaturesService } from '../../../editor/common/services/langua import { decodeSemanticTokensDto } from '../../../editor/common/services/semanticTokensDto.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; -import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js'; -import * as typeConvert from '../common/extHostTypeConverters.js'; -import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; import * as callh from '../../contrib/callHierarchy/common/callHierarchy.js'; import * as search from '../../contrib/search/common/search.js'; import * as typeh from '../../contrib/typeHierarchy/common/typeHierarchy.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js'; +import * as typeConvert from '../common/extHostTypeConverters.js'; +import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; +import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -1006,6 +1006,10 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); } + + $registerMappedEditsProvider2(handle: number, displayName: string): void { + throw new Error('Method not implemented.'); + } } class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2a09a8b1f41..ecd94a0de2a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -108,6 +108,7 @@ import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/ex import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from '../../services/search/common/searchExtTypes.js'; import type * as vscode from 'vscode'; +import { ExtHostCodeMapper } from './extHostCodeMapper.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -191,6 +192,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService, extHostFileSystemInfo, extHostDocumentsAndEditors)); const extHostLanguages = rpcProtocol.set(ExtHostContext.ExtHostLanguages, new ExtHostLanguages(rpcProtocol, extHostDocuments, extHostCommands.converter, uriTransformer)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation, extHostTelemetry)); + const extHostCodeMapper = rpcProtocol.set(ExtHostContext.ExtHostCodeMapper, new ExtHostCodeMapper(rpcProtocol)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostLogService, extHostDocumentsAndEditors)); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, createExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); @@ -1441,7 +1443,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerMappedEditsProvider2(provider: vscode.MappedEditsProvider2) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); - return extHostLanguageFeatures.registerMappedEditsProvider2(extension, provider); + return extHostCodeMapper.registerMappedEditsProvider(extension, provider); }, createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { return extHostChatAgents2.createChatAgent(extension, id, handler); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 417748e7fd9..b3b9f5d16c4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -47,11 +47,11 @@ import { ICreateContributedTerminalProfileOptions, IProcessProperty, IProcessRea import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from '../../../platform/tunnel/common/tunnel.js'; import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js'; import { WorkspaceTrustRequestOptions } from '../../../platform/workspace/common/workspaceTrust.js'; -import * as tasks from './shared/tasks.js'; import { SaveReason } from '../../common/editor.js'; import { IRevealOptions, ITreeItem, IViewBadge } from '../../common/views.js'; import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierarchy.js'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; +import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatProgressResponseContent } from '../../contrib/chat/common/chatModel.js'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from '../../contrib/chat/common/chatVariables.js'; @@ -83,7 +83,8 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic import * as search from '../../services/search/common/search.js'; import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; -import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; +import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; +import * as tasks from './shared/tasks.js'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -392,12 +393,18 @@ export interface IMappedEditsContextDto { conversation?: IConversationItemDto[]; } +export interface ICodeBlockDto { + code: string; + resource: UriComponents; +} + export interface IMappedEditsRequestDto { - readonly resource: UriComponents; - readonly codeBlock: string; + readonly codeBlocks: ICodeBlockDto[]; readonly conversation?: IConversationItemDto[]; } + + export interface IMappedEditsResultDto { readonly errorMessage?: string; } @@ -1275,6 +1282,13 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { $transferActiveChatSession(toWorkspace: UriComponents): void; } +export interface MainThreadCodeMapperShape extends IDisposable { + $registerCodeMapperProvider(handle: number): void; + $unregisterCodeMapperProvider(handle: number): void; + $mapCode(request: ICodeMapperRequest, token: CancellationToken): Promise; + $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise; +} + export interface IChatAgentCompletionItem { id: string; fullName?: string; @@ -1389,6 +1403,8 @@ export type IChatProgressDto = | Dto> | IChatTaskDto; +export type ICodeMapperProgressDto = Dto; + export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; } @@ -1733,6 +1749,19 @@ export interface ICommandMetadataDto { readonly returns?: string; } +export interface IMappedEditsProvider { + provideMappedEdits( + request: ICodeMapperRequest, + result: ICodeMapperResponse, + token: CancellationToken + ): Promise; +} + +export interface ExtHostCodeMapperShape { + registerMappedEditsProvider(extension: IExtensionDescription, provider: IMappedEditsProvider): void; + $mapCode(handle: number, request: ICodeMapperRequest, token: CancellationToken): Promise; +} + export interface ExtHostCommandsShape { $executeContributedCommand(id: string, ...args: any[]): Promise; $getContributedCommandMetadata(): Promise<{ [id: string]: string | ICommandMetadataDto }>; @@ -2889,6 +2918,7 @@ export const MainContext = { MainThreadLanguageModels: createProxyIdentifier('MainThreadLanguageModels'), MainThreadEmbeddings: createProxyIdentifier('MainThreadEmbeddings'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), + MainThreadCodeMapper: createProxyIdentifier('MainThreadCodeMapper'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), MainThreadLanguageModelTools: createProxyIdentifier('MainThreadChatSkills'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), @@ -2957,6 +2987,7 @@ export const MainContext = { }; export const ExtHostContext = { + ExtHostCodeMapper: createProxyIdentifier('ExtHostCodeMapper'), ExtHostCommands: createProxyIdentifier('ExtHostCommands'), ExtHostConfiguration: createProxyIdentifier('ExtHostConfiguration'), ExtHostDiagnostics: createProxyIdentifier('ExtHostDiagnostics'), diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts new file mode 100644 index 00000000000..5b150b462fa --- /dev/null +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; +import * as extHostProtocol from './extHost.protocol.js'; + + +export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { + + private static _providerHandlePool: number = 0; + private readonly _proxy: extHostProtocol.MainThreadCodeMapperShape; + private readonly providers = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCodeMapper); + } + + async $mapCode(handle: number, request: ICodeMapperRequest, token: CancellationToken): Promise { + // Received request to map code from the main thread + const provider = this.providers.get(handle); + if (!provider) { + throw new Error(`Received request to map code for unknown provider handle ${handle}`); + } + + // Construct a response object to pass to the provider + const responseObject = { + textEdit: (textEdit: vscode.TextEdit, resource: vscode.Uri) => { + this._proxy.$handleProgress(requestId, { + kind: 'textEdit', + data: textEdit, + resource: resource.toJSON() + }); + } + }; + + await provider.provideMappedEdits(request, responseObject, token); + } + + registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { + const handle = ExtHostCodeMapper._providerHandlePool++; + this._proxy.$registerCodeMapperProvider(handle); + this.providers.set(handle, provider); + return { + dispose: () => { + return this._proxy.$unregisterCodeMapperProvider(handle); + } + }; + } + +} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index cb294bec4be..780c95fba1c 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import { asArray, coalesce, isFalsyOrEmpty, isNonEmptyArray } from '../../../base/common/arrays.js'; import { raceCancellationError } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; @@ -26,6 +27,10 @@ import { encodeSemanticTokensDto } from '../../../editor/common/services/semanti import { localize } from '../../../nls.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; +import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { Cache } from './cache.js'; +import * as extHostProtocol from './extHost.protocol.js'; import { IExtHostApiDeprecationService } from './extHostApiDeprecationService.js'; import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; import { ExtHostDiagnostics } from './extHostDiagnostics.js'; @@ -33,10 +38,6 @@ import { ExtHostDocuments } from './extHostDocuments.js'; import { ExtHostTelemetry, IExtHostTelemetry } from './extHostTelemetry.js'; import * as typeConvert from './extHostTypeConverters.js'; import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; -import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import type * as vscode from 'vscode'; -import { Cache } from './cache.js'; -import * as extHostProtocol from './extHost.protocol.js'; // --- adapter @@ -2182,41 +2183,6 @@ class MappedEditsAdapter { } } -class MappedEditsAdapter2 { - - constructor( - private readonly _provider: vscode.MappedEditsProvider2, - ) { } - - async provideMappedEdits( - request: extHostProtocol.IMappedEditsRequestDto, - response: extHostProtocol.IMappedEditsResponseDto, - token: CancellationToken - ): Promise { - - const revivedRequest = { - resource: URI.revive(request.resource), - codeBlock: request.codeBlock, - conversation: request.conversation?.map(c => { - if (c.type === 'response') { - return { - type: 'response', - message: c.message, - references: c.references?.map(reviveContextItem) - } satisfies vscode.ConversationResponse; - } else { - return { - type: 'request', - message: c.message, - } satisfies vscode.ConversationRequest; - } - }) - } satisfies vscode.MappedEditsRequest; - - return this._provider.provideMappedEdits(revivedRequest, response, token); - } -} - type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | MultiDocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter @@ -2227,7 +2193,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter - | DocumentDropEditAdapter | MappedEditsAdapter | MappedEditsAdapter2 | NewSymbolNamesAdapter | InlineEditAdapter; + | DocumentDropEditAdapter | MappedEditsAdapter | NewSymbolNamesAdapter | InlineEditAdapter; class AdapterData { constructor( @@ -2255,7 +2221,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF ) { this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadLanguageFeatures); } - + $provideMappedEdits2(handle: number, request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } private _transformDocumentSelector(selector: vscode.DocumentSelector, extension: IExtensionDescription): Array { return typeConvert.DocumentSelector.from(selector, this._uriTransformer, extension); @@ -2953,12 +2921,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token); } - registerMappedEditsProvider2(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { - const handle = this._addNewAdapter(new MappedEditsAdapter2(provider), extension); - this._proxy.$registerMappedEditsProvider2(handle, extension.displayName ?? extension.name); - return this._createDisposable(handle); - } - // --- copy/paste actions registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index fe0c22e9afc..e64886bdd01 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -21,13 +21,14 @@ import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; +import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { CONTEXT_CHAT_EDIT_APPLIED, CONTEXT_CHAT_ENABLED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../common/chatContextKeys.js'; import { ChatCopyKind, IChatService } from '../../common/chatService.js'; import { IChatResponseViewModel, isResponseVM } from '../../common/chatViewModel.js'; import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js'; import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { InsertCodeBlockOperation, ApplyCodeBlockOperation } from './codeBlockOperations.js'; +import { ApplyCodeBlockOperation, InsertCodeBlockOperation } from './codeBlockOperations.js'; const shellLangIds = [ 'fish', @@ -216,6 +217,31 @@ export function registerChatCodeBlockActions() { } }); + registerAction2(class ApplyAllAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.applyAll', + title: localize2('chat.applyAll.label', "Apply All Edits"), + precondition: CONTEXT_CHAT_ENABLED, // improve this condition + f1: true, + category: CHAT_CATEGORY, + icon: Codicon.edit + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]) { + const item = args[0]; + if (!isResponseVM(item)) { // make sure that there are multiple codeblocks with codemapper URIs + return; + } + + // Invoke the code mapper for all the code blocks in this response + const codemapperService = accessor.get(ICodeMapperService); + + await codemapperService.apply(); + } + }); + registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts new file mode 100644 index 00000000000..5ba7df9c62b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IRange } from '../../../../base/common/range.js'; +import { URI } from '../../../../base/common/uri.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export interface DocumentContextItem { + readonly uri: URI; + readonly version: number; + readonly ranges: IRange[]; +} + +export interface ICodeMapperResponse { + textEdit: (textEdit: TextEdit, resource: URI) => void; +} + +export interface ICodeMapperCodeBlock { + code: string; + resource: URI; +} + +export interface ConversationRequest { + readonly type: 'request'; + readonly message: string; +} + +export interface ConversationResponse { + readonly type: 'response'; + readonly message: string; + // readonly references?: DocumentContextItem[]; +} + +export interface ICodeMapperRequest { + codeBlocks: ICodeMapperCodeBlock[]; + conversation: (ConversationRequest | ConversationResponse)[]; +} + +export interface ICodeMapperResult { + errorMessage?: string; +} + +export interface ICodeMapperProvider extends IDisposable { + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; +} + +export const ICodeMapperService = createDecorator('codeMapperService'); + +export interface ICodeMapperService { + readonly _serviceBrand: undefined; + registerCodeMapperProvider(handle: number, response: ICodeMapperResponse, provider: ICodeMapperProvider): void; + mapCode(request: ICodeMapperRequest, token: CancellationToken): void; + dispose(): void; +} + +export class CodeMapperService implements ICodeMapperService { + _serviceBrand: undefined; + + private readonly providers: ICodeMapperProvider[] = []; + + registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): void { + this.providers.push(provider); + } + + 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 result; + } + } + return undefined; + } + + dispose(): void { + this.providers.forEach((p) => p.dispose()); + } + +} diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 2692669ffb4..d3435dc0d9d 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -53,13 +53,12 @@ declare module 'vscode' { } export interface MappedEditsRequest { - readonly resource: Uri; - readonly codeBlock: string; - readonly conversation?: (ConversationRequest | ConversationResponse)[]; + readonly codeBlocks: { code: string; resource: Uri }[]; + readonly conversation: (ConversationRequest | ConversationResponse)[]; // 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 MappedEditsResposeStream { - edit(value: TextEdit, resource: Uri): void; + export interface MappedEditsResponseStream { + textEdit(value: TextEdit, resource: Uri): void; } export interface MappedEditsResult { @@ -72,7 +71,7 @@ declare module 'vscode' { export interface MappedEditsProvider2 { provideMappedEdits( request: MappedEditsRequest, - result: MappedEditsResposeStream, + result: MappedEditsResponseStream, token: CancellationToken ): ProviderResult; } From 88184c6df66b4bb3604835f78f90260cdae313fe Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 18:42:00 +0200 Subject: [PATCH 03/10] Scaffolding --- src/vs/editor/common/languages.ts | 6 +-- .../api/browser/mainThreadChatCodeMapper.ts | 47 ++++++++++++------- .../api/browser/mainThreadLanguageFeatures.ts | 10 ++-- .../workbench/api/common/extHost.protocol.ts | 26 +++++----- .../workbench/api/common/extHostCodeMapper.ts | 30 ++++++++---- .../api/common/extHostLanguageFeatures.ts | 27 +++++------ .../chat/common/chatCodeMapperService.ts | 22 +++++---- 7 files changed, 92 insertions(+), 76 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index cfd6734adc3..4e3a0aa827c 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -14,9 +14,6 @@ import { IMarkdownString } from '../../base/common/htmlContent.js'; import { IDisposable } from '../../base/common/lifecycle.js'; import { ThemeIcon } from '../../base/common/themables.js'; import { URI, UriComponents } from '../../base/common/uri.js'; -import { localize } from '../../nls.js'; -import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; -import { IMarkerData } from '../../platform/markers/common/markers.js'; import { EditOperation, ISingleEditOperation } from './core/editOperation.js'; import { IPosition, Position } from './core/position.js'; import { IRange, Range } from './core/range.js'; @@ -26,6 +23,9 @@ import { LanguageSelector } from './languageSelector.js'; import * as model from './model.js'; import { TokenizationRegistry as TokenizationRegistryImpl } from './tokenizationRegistry.js'; import { ContiguousMultilineTokens } from './tokens/contiguousMultilineTokens.js'; +import { localize } from '../../nls.js'; +import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; +import { IMarkerData } from '../../platform/markers/common/markers.js'; /** * @internal diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index 37bc2e61a0a..62bb1862791 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../base/common/cancellation.js'; -import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; -import { ICodeMapperProvider, ICodeMapperRequest, ICodeMapperResult, ICodeMapperService } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { Disposable, DisposableMap, IDisposable } from '../../../base/common/lifecycle.js'; +import { URI } from '../../../base/common/uri.js'; +import { ICodeMapperProvider, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../contrib/chat/common/chatCodeMapperService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostCodeMapperShape, ExtHostContext, ICodeMapperProgressDto, MainContext, MainThreadCodeMapperShape } from '../common/extHost.protocol.js'; +import { ExtHostCodeMapperShape, ExtHostContext, ICodeMapperProgressDto, ICodeMapperRequestDto, MainContext, MainThreadCodeMapperShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadCodeMapper) export class MainThreadChatCodemapper extends Disposable implements MainThreadCodeMapperShape { - private providers = this._register(new DisposableMap()); + private providers = this._register(new DisposableMap()); private readonly _proxy: ExtHostCodeMapperShape; private static _requestHandlePool: number = 0; - private static _requestHandleMap: Map = new Map(); + private _responseMap = new Map(); constructor( extHostContext: IExtHostContext, @@ -23,22 +24,27 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper); } - $mapCode(request: ICodeMapperRequest, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - } $registerCodeMapperProvider(handle: number): void { - const impl = { - mapCode: (request: ICodeMapperRequest, token: CancellationToken) => { - return this._proxy.$mapCode(handle, request, token).then((result) => result ?? undefined); - }, - dispose: () => { - this.$unregisterCodeMapperProvider(handle); + const impl: ICodeMapperProvider = { + mapCode: (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 + }; + try { + return this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); + } finally { + this._responseMap.delete(requestId); + } } }; - this.codeMapperService.registerCodeMapperProvider(handle, impl); - this.providers.set(handle, impl); + const disposable = this.codeMapperService.registerCodeMapperProvider(handle, impl); + this.providers.set(handle, disposable); } $unregisterCodeMapperProvider(handle: number): void { @@ -46,6 +52,13 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo } $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise { - throw new Error('Method not implemented.'); + const response = this._responseMap.get(requestId); + if (response) { + const resource = URI.revive(data.uri); + for (const edit of data.edits) { + response.textEdit(edit, resource); + } + } + return Promise.resolve(); } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index b9442009a83..c591ed677f1 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -27,14 +27,14 @@ import { ILanguageFeaturesService } from '../../../editor/common/services/langua import { decodeSemanticTokensDto } from '../../../editor/common/services/semanticTokensDto.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; +import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js'; +import * as typeConvert from '../common/extHostTypeConverters.js'; +import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; import * as callh from '../../contrib/callHierarchy/common/callHierarchy.js'; import * as search from '../../contrib/search/common/search.js'; import * as typeh from '../../contrib/typeHierarchy/common/typeHierarchy.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js'; -import * as typeConvert from '../common/extHostTypeConverters.js'; -import { DataTransferFileCache } from '../common/shared/dataTransferCache.js'; -import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -1006,10 +1006,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const provider = new MainThreadMappedEditsProvider(displayName, handle, this._proxy, this._uriIdentService); this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider)); } - - $registerMappedEditsProvider2(handle: number, displayName: string): void { - throw new Error('Method not implemented.'); - } } class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b3b9f5d16c4..cb727104ffe 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { SaveReason } from '../../common/editor.js'; import { IRevealOptions, ITreeItem, IViewBadge } from '../../common/views.js'; import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierarchy.js'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; -import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatProgressResponseContent } from '../../contrib/chat/common/chatModel.js'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from '../../contrib/chat/common/chatVariables.js'; @@ -403,8 +403,6 @@ export interface IMappedEditsRequestDto { readonly conversation?: IConversationItemDto[]; } - - export interface IMappedEditsResultDto { readonly errorMessage?: string; } @@ -472,7 +470,6 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; $registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void; - $registerMappedEditsProvider2(handle: number, displayName: string): void; } export interface MainThreadLanguagesShape extends IDisposable { @@ -1282,10 +1279,16 @@ export interface MainThreadChatAgentsShape2 extends IDisposable { $transferActiveChatSession(toWorkspace: UriComponents): void; } +export interface ICodeMapperTextEdit { + uri: URI; + edits: languages.TextEdit[]; +} + +export type ICodeMapperProgressDto = Dto; + export interface MainThreadCodeMapperShape extends IDisposable { $registerCodeMapperProvider(handle: number): void; $unregisterCodeMapperProvider(handle: number): void; - $mapCode(request: ICodeMapperRequest, token: CancellationToken): Promise; $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise; } @@ -1403,8 +1406,6 @@ export type IChatProgressDto = | Dto> | IChatTaskDto; -export type ICodeMapperProgressDto = Dto; - export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; } @@ -1749,17 +1750,12 @@ export interface ICommandMetadataDto { readonly returns?: string; } -export interface IMappedEditsProvider { - provideMappedEdits( - request: ICodeMapperRequest, - result: ICodeMapperResponse, - token: CancellationToken - ): Promise; +export interface ICodeMapperRequestDto extends Dto { + requestId: string; } export interface ExtHostCodeMapperShape { - registerMappedEditsProvider(extension: IExtensionDescription, provider: IMappedEditsProvider): void; - $mapCode(handle: number, request: ICodeMapperRequest, token: CancellationToken): Promise; + $mapCode(handle: number, request: ICodeMapperRequestDto, token: CancellationToken): Promise; } export interface ExtHostCommandsShape { diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 5b150b462fa..3bf52eedc7f 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -6,9 +6,10 @@ import type * as vscode from 'vscode'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import * as extHostProtocol from './extHost.protocol.js'; - +import { TextEdit } from './extHostTypeConverters.js'; +import { URI } from '../../../base/common/uri.js'; export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { @@ -22,7 +23,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCodeMapper); } - async $mapCode(handle: number, request: ICodeMapperRequest, token: CancellationToken): Promise { + async $mapCode(handle: number, internalRequest: extHostProtocol.ICodeMapperRequestDto, token: CancellationToken): Promise { // Received request to map code from the main thread const provider = this.providers.get(handle); if (!provider) { @@ -30,17 +31,27 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape } // Construct a response object to pass to the provider - const responseObject = { + const stream: vscode.MappedEditsResponseStream = { textEdit: (textEdit: vscode.TextEdit, resource: vscode.Uri) => { - this._proxy.$handleProgress(requestId, { - kind: 'textEdit', - data: textEdit, - resource: resource.toJSON() + this._proxy.$handleProgress(internalRequest.requestId, { + uri: resource, + edits: [TextEdit.from(textEdit)] }); } }; - await provider.provideMappedEdits(request, responseObject, token); + const request: vscode.MappedEditsRequest = { + codeBlocks: internalRequest.codeBlocks.map(block => { + return { + code: block.code, + resource: URI.revive(block.resource) + }; + }), + conversation: internalRequest.conversation + }; + + const result = await provider.provideMappedEdits(request, stream, token); + return result ?? null; } registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable { @@ -53,5 +64,4 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape } }; } - } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 780c95fba1c..9459f74eb4f 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as vscode from 'vscode'; import { asArray, coalesce, isFalsyOrEmpty, isNonEmptyArray } from '../../../base/common/arrays.js'; import { raceCancellationError } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; @@ -27,10 +26,6 @@ import { encodeSemanticTokensDto } from '../../../editor/common/services/semanti import { localize } from '../../../nls.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; -import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; -import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { Cache } from './cache.js'; -import * as extHostProtocol from './extHost.protocol.js'; import { IExtHostApiDeprecationService } from './extHostApiDeprecationService.js'; import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; import { ExtHostDiagnostics } from './extHostDiagnostics.js'; @@ -38,6 +33,10 @@ import { ExtHostDocuments } from './extHostDocuments.js'; import { ExtHostTelemetry, IExtHostTelemetry } from './extHostTelemetry.js'; import * as typeConvert from './extHostTypeConverters.js'; import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from './extHostTypes.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import type * as vscode from 'vscode'; +import { Cache } from './cache.js'; +import * as extHostProtocol from './extHost.protocol.js'; // --- adapter @@ -2134,12 +2133,6 @@ class DocumentDropEditAdapter { } } -const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ - uri: URI.revive(item.uri), - version: item.version, - ranges: item.ranges.map(r => typeConvert.Range.to(r)), -} satisfies vscode.DocumentContextItem); - class MappedEditsAdapter { constructor( @@ -2156,6 +2149,14 @@ class MappedEditsAdapter { const uri = URI.revive(resource); const doc = this._documents.getDocument(uri); + + const reviveContextItem = (item: extHostProtocol.IDocumentContextItemDto) => ({ + uri: URI.revive(item.uri), + version: item.version, + ranges: item.ranges.map(r => typeConvert.Range.to(r)), + } satisfies vscode.DocumentContextItem); + + const usedContext = context.documents.map(docSubArray => docSubArray.map(reviveContextItem)); const ctx = { @@ -2221,9 +2222,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF ) { this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadLanguageFeatures); } - $provideMappedEdits2(handle: number, request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - } + private _transformDocumentSelector(selector: vscode.DocumentSelector, extension: IExtensionDescription): Array { return typeConvert.DocumentSelector.from(selector, this._uriTransformer, extension); diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 5ba7df9c62b..2fdb392f59d 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -45,7 +45,7 @@ export interface ICodeMapperResult { errorMessage?: string; } -export interface ICodeMapperProvider extends IDisposable { +export interface ICodeMapperProvider { mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } @@ -53,9 +53,8 @@ export const ICodeMapperService = createDecorator('codeMappe export interface ICodeMapperService { readonly _serviceBrand: undefined; - registerCodeMapperProvider(handle: number, response: ICodeMapperResponse, provider: ICodeMapperProvider): void; - mapCode(request: ICodeMapperRequest, token: CancellationToken): void; - dispose(): void; + registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): void; } export class CodeMapperService implements ICodeMapperService { @@ -63,8 +62,16 @@ export class CodeMapperService implements ICodeMapperService { private readonly providers: ICodeMapperProvider[] = []; - registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): void { + registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable { this.providers.push(provider); + return { + dispose: () => { + const index = this.providers.indexOf(provider); + if (index >= 0) { + this.providers.splice(index, 1); + } + } + }; } async mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) { @@ -76,9 +83,4 @@ export class CodeMapperService implements ICodeMapperService { } return undefined; } - - dispose(): void { - this.providers.forEach((p) => p.dispose()); - } - } From 4e87d2a754842c39255c1a014c322a0c04de5c94 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Mon, 16 Sep 2024 20:42:22 +0200 Subject: [PATCH 04/10] Register service and implement Apply All command on top of new API --- .../browser/actions/chatCodeblockActions.ts | 55 +++++++++++++++++-- .../contrib/chat/browser/chat.contribution.ts | 2 + .../chat/common/chatCodeMapperService.ts | 2 +- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index e64886bdd01..262922ac874 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -3,25 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js'; import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; -import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; +import { ICodeMapperCodeBlock, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { CONTEXT_CHAT_EDIT_APPLIED, CONTEXT_CHAT_ENABLED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../common/chatContextKeys.js'; import { ChatCopyKind, IChatService } from '../../common/chatService.js'; import { IChatResponseViewModel, isResponseVM } from '../../common/chatViewModel.js'; @@ -230,15 +236,52 @@ export function registerChatCodeBlockActions() { } override async run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { // make sure that there are multiple codeblocks with codemapper URIs + const chatWidgetService = accessor.get(IChatWidgetService); + const codemapperService = accessor.get(ICodeMapperService); + const fileService = accessor.get(IFileService); + const progressService = accessor.get(IProgressService); + const bulkEditService = accessor.get(IBulkEditService); + + const widget = chatWidgetService.lastFocusedWidget; + if (!widget) { return; } - // Invoke the code mapper for all the code blocks in this response - const codemapperService = accessor.get(ICodeMapperService); + const item = widget.getFocus(); + if (!isResponseVM(item)) { + return; + } - await codemapperService.apply(); + const codeblocks = widget.getCodeBlockInfosForResponse(item); + const request: ICodeMapperCodeBlock[] = []; + for (const codeblock of codeblocks) { + if (codeblock.codemapperUri && codeblock.uri) { + const code = await fileService.readFile(codeblock.uri); + request.push({ resource: codeblock.codemapperUri, code: code.value.toString() }); + } + } + + // TODO@joyceerhl @aeschli this should be streamed to the refactoring preview + const resources: ResourceTextEdit[] = []; + const response = { + textEdit: (textEdit: TextEdit, resource: URI) => { + const resourceEdit = new ResourceTextEdit(resource, textEdit, undefined); + resources.push(resourceEdit); + } + }; + + // Invoke the code mapper for all the code blocks in this response + const tokenSource = new CancellationTokenSource(); + await progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize2('chatCodeBlock.generatingEdits', 'Applying all edits').value, + cancellable: true + }, async (task) => { + task.report({ message: localize2('chatCodeBlock.generating', 'Generating edits...').value }); + await codemapperService.mapCode({ codeBlocks: request, conversation: [] }, response, tokenSource.token); + task.report({ message: localize2('chatCodeBlock.applyAllEdits', 'Applying edits to workspace...').value }); + await bulkEditService.apply(resources); + }, () => tokenSource.cancel()); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0be75f31387..f4362987764 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -62,6 +62,7 @@ import { IEditorResolverService, RegisteredEditorPriority } from '../../../servi import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import '../common/chatColors.js'; import { ChatGettingStartedContribution } from './chatGettingStarted.js'; +import { CodeMapperService, ICodeMapperService } from '../common/chatCodeMapperService.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -293,3 +294,4 @@ registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); +registerSingleton(ICodeMapperService, CodeMapperService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 2fdb392f59d..5a614af88fa 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -54,7 +54,7 @@ export const ICodeMapperService = createDecorator('codeMappe export interface ICodeMapperService { readonly _serviceBrand: undefined; registerCodeMapperProvider(handle: number, provider: ICodeMapperProvider): IDisposable; - mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): void; + mapCode(request: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken): Promise; } export class CodeMapperService implements ICodeMapperService { From b3a0c635e287a9abef8471c53922ebdb943ce78f Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 21:26:42 +0200 Subject: [PATCH 05/10] Add missing main thread import --- src/vs/workbench/api/browser/extensionHost.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index ef19bca5a06..2764905f82f 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -20,6 +20,7 @@ import './mainThreadBulkEdits.js'; import './mainThreadLanguageModels.js'; import './mainThreadChatAgents2.js'; import './mainThreadChatVariables.js'; +import './mainThreadChatCodeMapper.js'; import './mainThreadLanguageModelTools.js'; import './mainThreadEmbeddings.js'; import './mainThreadCodeInsets.js'; From b21bcd30e3b4fddd8dded8e281fe625421c87b9f Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 22:32:15 +0200 Subject: [PATCH 06/10] Tackling multiple problems: * in ChatListItemRenderer.renderMarkdown, newer code blocks were being removed from older markdown parts getting disposed * in ChatMarkdownContentPart.codeBlockRendererSync code blocks are being rendered async, so when they get created their uri's are sometimes not available synchronously * in ChatMarkdownContentPart.renderCodeBlock the async update would create a new instance of a IChatCodeBlockInfo, leading to the original instance being held by ChatListItemRenderer not getting updated * in ApplyAllAction, there is no file system provider for the URIs defined for the code blocks --- .../browser/actions/chatCodeblockActions.ts | 6 ++-- src/vs/workbench/contrib/chat/browser/chat.ts | 8 +++-- .../chatMarkdownContentPart.ts | 30 +++++++++++++------ .../contrib/chat/browser/chatListRenderer.ts | 16 ++++++++-- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 262922ac874..c287329ca02 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -18,7 +18,6 @@ import { localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; @@ -238,7 +237,6 @@ export function registerChatCodeBlockActions() { override async run(accessor: ServicesAccessor, ...args: any[]) { const chatWidgetService = accessor.get(IChatWidgetService); const codemapperService = accessor.get(ICodeMapperService); - const fileService = accessor.get(IFileService); const progressService = accessor.get(IProgressService); const bulkEditService = accessor.get(IBulkEditService); @@ -256,8 +254,8 @@ export function registerChatCodeBlockActions() { const request: ICodeMapperCodeBlock[] = []; for (const codeblock of codeblocks) { if (codeblock.codemapperUri && codeblock.uri) { - const code = await fileService.readFile(codeblock.uri); - request.push({ resource: codeblock.codemapperUri, code: code.value.toString() }); + const code = codeblock.getContent(); + request.push({ resource: codeblock.codemapperUri, code }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 05dca109bb9..30a38391a62 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -77,11 +77,13 @@ export interface IChatAccessibilityService { } export interface IChatCodeBlockInfo { - codeBlockIndex: number; - element: ChatTreeItem; - uri: URI | undefined; + readonly ownerMarkdownPartId: string; + readonly codeBlockIndex: number; + readonly element: ChatTreeItem; + readonly uri: URI | undefined; codemapperUri: URI | undefined; focus(): void; + getContent(): string; } export interface IChatFileTreeInfo { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index da42f3e51d3..827df25f07a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -30,6 +30,8 @@ import { URI } from '../../../../../base/common/uri.js'; const $ = dom.$; export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { + private static idPool = 0; + public readonly id = String(++ChatMarkdownContentPart.idPool); public readonly domNode: HTMLElement; private readonly allRefs: IDisposableReference[] = []; @@ -97,15 +99,24 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); - const info: IChatCodeBlockInfo = { - codeBlockIndex: index, - element, - focus() { + const ownerMarkdownPartId = this.id; + const info: IChatCodeBlockInfo = new class { + readonly ownerMarkdownPartId = ownerMarkdownPartId; + readonly codeBlockIndex = index; + readonly element = element; + codemapperUri = undefined; // will be set async + public get uri() { + // here we must do a getter because the ref.object is rendered + // async and the uri might be undefined when it's read immediately + return ref.object.uri; + } + public focus() { ref.object.focus(); - }, - uri: ref.object.uri, - codemapperUri: undefined - }; + } + public getContent(): string { + return ref.object.editor.getValue(); + } + }(); this.codeblocks.push(info); orderedDisposablesList.push(ref); return ref.object.element; @@ -124,7 +135,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const editorInfo = ref.object; if (isResponseVM(data.element)) { this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }).then((e) => { - this.codeblocks[data.codeBlockIndex] = { ...this.codeblocks[data.codeBlockIndex]!, codemapperUri: e.codemapperUri }; + // Update the existing object's codemapperUri + this.codeblocks[data.codeBlockIndex].codemapperUri = e.codemapperUri; }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 13471f365cb..70cc581b738 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -875,6 +875,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0); const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions); + const markdownPartId = markdownPart.id; markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => { markdownPart.layout(this._currentLayoutWidth); this.updateItemHeight(templateData); @@ -885,7 +886,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); if (codeBlocksByResponseId) { - markdownPart.codeblocks.forEach((info, i) => delete codeBlocksByResponseId[codeBlockStartIndex + i]); + // Only delete if this is my code block + markdownPart.codeblocks.forEach((info, i) => { + const codeblock = codeBlocksByResponseId[codeBlockStartIndex + i]; + if (codeblock?.ownerMarkdownPartId === markdownPartId) { + delete codeBlocksByResponseId[codeBlockStartIndex + i]; + } + }); } })); @@ -894,7 +901,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.codeBlocksByEditorUri.delete(uri))); + markdownPart.addDisposable(toDisposable(() => { + const codeblock = this.codeBlocksByEditorUri.get(uri); + if (codeblock?.ownerMarkdownPartId === markdownPartId) { + this.codeBlocksByEditorUri.delete(uri); + } + })); } }); From 10dd5c6aa6fb53e32dede88818860d7c586b2ac9 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 22:58:02 +0200 Subject: [PATCH 07/10] Allow pushing an array of edits --- src/vs/workbench/api/common/extHostCodeMapper.ts | 7 ++++--- src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 3bf52eedc7f..055d52b3d4b 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -32,10 +32,11 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape // Construct a response object to pass to the provider const stream: vscode.MappedEditsResponseStream = { - textEdit: (textEdit: vscode.TextEdit, resource: vscode.Uri) => { + textEdit: (target: vscode.Uri, edits: vscode.TextEdit | vscode.TextEdit[]) => { + edits = (Array.isArray(edits) ? edits : [edits]); this._proxy.$handleProgress(internalRequest.requestId, { - uri: resource, - edits: [TextEdit.from(textEdit)] + uri: target, + edits: edits.map(TextEdit.from) }); } }; diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index d3435dc0d9d..1d71b61768a 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -58,7 +58,7 @@ declare module 'vscode' { } export interface MappedEditsResponseStream { - textEdit(value: TextEdit, resource: Uri): void; + textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; } export interface MappedEditsResult { From c3eed3ff7eef5ff9c1786a54a1d3c4938e6cd329 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 23:19:52 +0200 Subject: [PATCH 08/10] Fix problem with yielding too quickly from the `mapCode` method --- src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index 62bb1862791..c4aac99477a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -27,7 +27,7 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo $registerCodeMapperProvider(handle: number): void { const impl: ICodeMapperProvider = { - mapCode: (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => { + mapCode: async (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => { const requestId = String(MainThreadChatCodemapper._requestHandlePool++); this._responseMap.set(requestId, response); const extHostRequest: ICodeMapperRequestDto = { @@ -36,7 +36,7 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo conversation: uiRequest.conversation }; try { - return this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); + return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined); } finally { this._responseMap.delete(requestId); } From 090ec6322562ae2424fab22a8c3240e6d1484b61 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 23:37:16 +0200 Subject: [PATCH 09/10] Turn on preview for applying edits --- .../contrib/chat/browser/actions/chatCodeblockActions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index c287329ca02..5228ce3708e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -14,7 +14,7 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js'; -import { localize2 } from '../../../../../nls.js'; +import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -278,8 +278,12 @@ export function registerChatCodeBlockActions() { task.report({ message: localize2('chatCodeBlock.generating', 'Generating edits...').value }); await codemapperService.mapCode({ codeBlocks: request, conversation: [] }, response, tokenSource.token); task.report({ message: localize2('chatCodeBlock.applyAllEdits', 'Applying edits to workspace...').value }); - await bulkEditService.apply(resources); }, () => tokenSource.cancel()); + + await bulkEditService.apply(resources, { + showPreview: true, + label: localize('label', "Applying edits"), + }); } }); From 9220dbec4146447236597909f873be953e945da3 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Sep 2024 23:39:31 +0200 Subject: [PATCH 10/10] Remove unused interface --- .../workbench/contrib/chat/common/chatCodeMapperService.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 5a614af88fa..e093b84d058 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -5,16 +5,10 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IRange } from '../../../../base/common/range.js'; import { URI } from '../../../../base/common/uri.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -export interface DocumentContextItem { - readonly uri: URI; - readonly version: number; - readonly ranges: IRange[]; -} export interface ICodeMapperResponse { textEdit: (textEdit: TextEdit, resource: URI) => void;