Skip to content

Commit

Permalink
Merge pull request microsoft#228683 from microsoft/aeschli/respectabl…
Browse files Browse the repository at this point in the history
…e-sailfish-565

registerMappedEditsProvider2
  • Loading branch information
alexdima authored Sep 16, 2024
2 parents 777f691 + 9220dbe commit c4efe1d
Show file tree
Hide file tree
Showing 12 changed files with 400 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
64 changes: 64 additions & 0 deletions src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* 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, 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, ICodeMapperRequestDto, MainContext, MainThreadCodeMapperShape } from '../common/extHost.protocol.js';

@extHostNamedCustomer(MainContext.MainThreadCodeMapper)
export class MainThreadChatCodemapper extends Disposable implements MainThreadCodeMapperShape {

private providers = this._register(new DisposableMap<number, IDisposable>());
private readonly _proxy: ExtHostCodeMapperShape;
private static _requestHandlePool: number = 0;
private _responseMap = new Map<string, ICodeMapperResponse>();

constructor(
extHostContext: IExtHostContext,
@ICodeMapperService private readonly codeMapperService: ICodeMapperService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper);
}

$registerCodeMapperProvider(handle: number): void {
const impl: ICodeMapperProvider = {
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
};
try {
return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined);
} finally {
this._responseMap.delete(requestId);
}
}
};

const disposable = this.codeMapperService.registerCodeMapperProvider(handle, impl);
this.providers.set(handle, disposable);
}

$unregisterCodeMapperProvider(handle: number): void {
this.providers.deleteAndDispose(handle);
}

$handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise<void> {
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();
}
}
6 changes: 6 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -1439,6 +1441,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider);
},
registerMappedEditsProvider2(provider: vscode.MappedEditsProvider2) {
checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostCodeMapper.registerMappedEditsProvider(extension, provider);
},
createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) {
return extHostChatAgents2.createChatAgent(extension, id, handler);
},
Expand Down
42 changes: 40 additions & 2 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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';
Expand Down Expand Up @@ -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 }[];
Expand Down Expand Up @@ -392,6 +393,20 @@ export interface IMappedEditsContextDto {
conversation?: IConversationItemDto[];
}

export interface ICodeBlockDto {
code: string;
resource: UriComponents;
}

export interface IMappedEditsRequestDto {
readonly codeBlocks: ICodeBlockDto[];
readonly conversation?: IConversationItemDto[];
}

export interface IMappedEditsResultDto {
readonly errorMessage?: string;
}

export interface ISignatureHelpProviderMetadataDto {
readonly triggerCharacters: readonly string[];
readonly retriggerCharacters: readonly string[];
Expand Down Expand Up @@ -1264,6 +1279,19 @@ export interface MainThreadChatAgentsShape2 extends IDisposable {
$transferActiveChatSession(toWorkspace: UriComponents): void;
}

export interface ICodeMapperTextEdit {
uri: URI;
edits: languages.TextEdit[];
}

export type ICodeMapperProgressDto = Dto<ICodeMapperTextEdit>;

export interface MainThreadCodeMapperShape extends IDisposable {
$registerCodeMapperProvider(handle: number): void;
$unregisterCodeMapperProvider(handle: number): void;
$handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise<void>;
}

export interface IChatAgentCompletionItem {
id: string;
fullName?: string;
Expand Down Expand Up @@ -1722,6 +1750,14 @@ export interface ICommandMetadataDto {
readonly returns?: string;
}

export interface ICodeMapperRequestDto extends Dto<ICodeMapperRequest> {
requestId: string;
}

export interface ExtHostCodeMapperShape {
$mapCode(handle: number, request: ICodeMapperRequestDto, token: CancellationToken): Promise<ICodeMapperResult | null | undefined>;
}

export interface ExtHostCommandsShape {
$executeContributedCommand(id: string, ...args: any[]): Promise<unknown>;
$getContributedCommandMetadata(): Promise<{ [id: string]: string | ICommandMetadataDto }>;
Expand Down Expand Up @@ -2879,6 +2915,7 @@ export const MainContext = {
MainThreadLanguageModels: createProxyIdentifier<MainThreadLanguageModelsShape>('MainThreadLanguageModels'),
MainThreadEmbeddings: createProxyIdentifier<MainThreadEmbeddingsShape>('MainThreadEmbeddings'),
MainThreadChatAgents2: createProxyIdentifier<MainThreadChatAgentsShape2>('MainThreadChatAgents2'),
MainThreadCodeMapper: createProxyIdentifier<MainThreadCodeMapperShape>('MainThreadCodeMapper'),
MainThreadChatVariables: createProxyIdentifier<MainThreadChatVariablesShape>('MainThreadChatVariables'),
MainThreadLanguageModelTools: createProxyIdentifier<MainThreadLanguageModelToolsShape>('MainThreadChatSkills'),
MainThreadClipboard: createProxyIdentifier<MainThreadClipboardShape>('MainThreadClipboard'),
Expand Down Expand Up @@ -2947,6 +2984,7 @@ export const MainContext = {
};

export const ExtHostContext = {
ExtHostCodeMapper: createProxyIdentifier<ExtHostCodeMapperShape>('ExtHostCodeMapper'),
ExtHostCommands: createProxyIdentifier<ExtHostCommandsShape>('ExtHostCommands'),
ExtHostConfiguration: createProxyIdentifier<ExtHostConfigurationShape>('ExtHostConfiguration'),
ExtHostDiagnostics: createProxyIdentifier<ExtHostDiagnosticsShape>('ExtHostDiagnostics'),
Expand Down
68 changes: 68 additions & 0 deletions src/vs/workbench/api/common/extHostCodeMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* 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 { 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 {

private static _providerHandlePool: number = 0;
private readonly _proxy: extHostProtocol.MainThreadCodeMapperShape;
private readonly providers = new Map<number, vscode.MappedEditsProvider2>();

constructor(
mainContext: extHostProtocol.IMainContext
) {
this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCodeMapper);
}

async $mapCode(handle: number, internalRequest: extHostProtocol.ICodeMapperRequestDto, token: CancellationToken): Promise<ICodeMapperResult | null> {
// 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 stream: vscode.MappedEditsResponseStream = {
textEdit: (target: vscode.Uri, edits: vscode.TextEdit | vscode.TextEdit[]) => {
edits = (Array.isArray(edits) ? edits : [edits]);
this._proxy.$handleProgress(internalRequest.requestId, {
uri: target,
edits: edits.map(TextEdit.from)
});
}
};

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 {
const handle = ExtHostCodeMapper._providerHandlePool++;
this._proxy.$registerCodeMapperProvider(handle);
this.providers.set(handle, provider);
return {
dispose: () => {
return this._proxy.$unregisterCodeMapperProvider(handle);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,37 @@
* 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 { 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';
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 { 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';
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',
Expand Down Expand Up @@ -216,6 +222,71 @@ 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 chatWidgetService = accessor.get(IChatWidgetService);
const codemapperService = accessor.get(ICodeMapperService);
const progressService = accessor.get(IProgressService);
const bulkEditService = accessor.get(IBulkEditService);

const widget = chatWidgetService.lastFocusedWidget;
if (!widget) {
return;
}

const item = widget.getFocus();
if (!isResponseVM(item)) {
return;
}

const codeblocks = widget.getCodeBlockInfosForResponse(item);
const request: ICodeMapperCodeBlock[] = [];
for (const codeblock of codeblocks) {
if (codeblock.codemapperUri && codeblock.uri) {
const code = codeblock.getContent();
request.push({ resource: codeblock.codemapperUri, code });
}
}

// 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 });
}, () => tokenSource.cancel());

await bulkEditService.apply(resources, {
showPreview: true,
label: localize('label', "Applying edits"),
});
}
});

registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction {
constructor() {
super({
Expand Down
Loading

0 comments on commit c4efe1d

Please sign in to comment.