Initial implementation of mappedEditsProvider proposed API

This commit is contained in:
Ulugbek Abdullaev 2023-08-17 17:10:35 +02:00
parent 0e2849672f
commit 3e9e7b3b5a
11 changed files with 334 additions and 7 deletions

View file

@ -86,6 +86,7 @@ import './mainThreadTimeline';
import './mainThreadTesting';
import './mainThreadSecretState';
import './mainThreadShare';
import './mainThreadMappedEdits';
import './mainThreadProfilContentHandlers';
import './mainThreadSemanticSimilarity';
import './mainThreadIssueReporter';

View file

@ -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 { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits';
import { ExtHostContext, ExtHostMappedEditsShape, IDocumentFilterDto, IMappedEditsContextDto, MainContext, MainThreadMappedEditsShape } from 'vs/workbench/api/common/extHost.protocol';
import { IMappedEditsProvider, IMappedEditsService } from 'vs/workbench/services/mappedEdits/common/mappedEdits';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadMappedEdits)
export class MainThreadMappedEdits implements MainThreadMappedEditsShape {
private readonly proxy: ExtHostMappedEditsShape;
private providers = new Map<number, IMappedEditsProvider>();
private providerDisposables = new Map<number, IDisposable>();
constructor(
extHostContext: IExtHostContext,
@IMappedEditsService private readonly mappedEditsService: IMappedEditsService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
) {
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostMappedEdits);
}
$registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void {
const provider: IMappedEditsProvider = {
selector,
provideMappedEdits: async (document, codeBlocks, context, token) => {
const result = await this.proxy.$provideMappedEdits(handle, document.uri, codeBlocks, context, token);
return result ? reviveWorkspaceEditDto(result, this.uriIdentityService) : null;
}
};
this.providers.set(handle, provider);
const disposable = this.mappedEditsService.registerMappedEditsProvider(provider);
this.providerDisposables.set(handle, disposable);
}
$unregisterMappedEditsProvider(handle: number): void {
if (this.providers.has(handle)) {
this.providers.delete(handle);
}
if (this.providerDisposables.has(handle)) {
this.providerDisposables.delete(handle);
}
}
dispose(): void {
this.providers.clear();
dispose(this.providerDisposables.values());
this.providerDisposables.clear();
}
}

View file

@ -106,6 +106,7 @@ import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSo
import { ExtHostShare } from 'vs/workbench/api/common/extHostShare';
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
import { ExtHostChatSlashCommands } from 'vs/workbench/api/common/extHostChatSlashCommand';
import { ExtHostMappedEdits } from 'vs/workbench/api/common/extHostMappedEdits';
import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables';
export interface IExtensionRegistries {
@ -210,6 +211,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostChatSlashCommands = rpcProtocol.set(ExtHostContext.ExtHostChatSlashCommands, new ExtHostChatSlashCommands(rpcProtocol, extHostChatProvider, extHostLogService));
const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol));
const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService));
const extHostMappedEdits = rpcProtocol.set(ExtHostContext.ExtHostMappedEdits, new ExtHostMappedEdits(rpcProtocol, extHostDocuments, uriTransformer));
const extHostSemanticSimilarity = rpcProtocol.set(ExtHostContext.ExtHostSemanticSimilarity, new ExtHostSemanticSimilarity(rpcProtocol));
const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol));
const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter));
@ -1343,6 +1345,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerVariable(name: string, description: string, resolver: vscode.ChatVariableResolver) {
checkProposedApiEnabled(extension, 'chatVariables');
return extHostChatVariables.registerVariableResolver(extension, name, description, resolver);
},
registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) {
checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostMappedEdits.registerMappedEditsProvider(selector, provider);
}
};

View file

@ -371,6 +371,16 @@ export interface IShareableItemDto {
selection?: IRange;
}
export interface IRelatedContextItemDto {
readonly uri: UriComponents;
readonly range: IRange;
}
export interface IMappedEditsContextDto {
selections: ISelection[]; //FIXME@ulugbekna: is this serializable? should I use ISelection?
related: IRelatedContextItemDto[];
}
export interface ISignatureHelpProviderMetadataDto {
readonly triggerCharacters: readonly string[];
readonly retriggerCharacters: readonly string[];
@ -1315,6 +1325,11 @@ export interface MainThreadShareShape extends IDisposable {
$unregisterShareProvider(handle: number): void;
}
export interface MainThreadMappedEditsShape extends IDisposable {
$registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void;
$unregisterMappedEditsProvider(handle: number): void;
}
export interface MainThreadTaskShape extends IDisposable {
$createTaskId(task: tasks.ITaskDTO): Promise<string>;
$registerTaskProvider(handle: number, type: string): Promise<void>;
@ -2098,6 +2113,10 @@ export interface ExtHostShareShape {
$provideShare(handle: number, shareableItem: IShareableItemDto, token: CancellationToken): Promise<UriComponents | string | undefined>;
}
export interface ExtHostMappedEditsShape {
$provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise<IWorkspaceEditDto | null>;
}
export interface ExtHostTaskShape {
$provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.ITaskSetDTO>;
$resolveTask(handle: number, taskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined>;
@ -2623,6 +2642,7 @@ export const MainContext = {
MainThreadSCM: createProxyIdentifier<MainThreadSCMShape>('MainThreadSCM'),
MainThreadSearch: createProxyIdentifier<MainThreadSearchShape>('MainThreadSearch'),
MainThreadShare: createProxyIdentifier<MainThreadShareShape>('MainThreadShare'),
MainThreadMappedEdits: createProxyIdentifier<MainThreadMappedEditsShape>('MainThreadMappedEdits'),
MainThreadTask: createProxyIdentifier<MainThreadTaskShape>('MainThreadTask'),
MainThreadWindow: createProxyIdentifier<MainThreadWindowShape>('MainThreadWindow'),
MainThreadLabelService: createProxyIdentifier<MainThreadLabelServiceShape>('MainThreadLabelService'),
@ -2700,6 +2720,7 @@ export const ExtHostContext = {
ExtHostChatSlashCommands: createProxyIdentifier<ExtHostChatSlashCommandsShape>('ExtHostChatSlashCommands'),
ExtHostChatVariables: createProxyIdentifier<ExtHostChatVariablesShape>('ExtHostChatVariables'),
ExtHostChatProvider: createProxyIdentifier<ExtHostChatProviderShape>('ExtHostChatProvider'),
ExtHostMappedEdits: createProxyIdentifier<ExtHostMappedEditsShape>('ExtHostMappedEdits'),
ExtHostSemanticSimilarity: createProxyIdentifier<ExtHostSemanticSimilarityShape>('ExtHostSemanticSimilarity'),
ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'),
ExtHostTunnelService: createProxyIdentifier<ExtHostTunnelServiceShape>('ExtHostTunnelService'),

View file

@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* 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 'vs/base/common/cancellation';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { ExtHostMappedEditsShape, IMainContext, IMappedEditsContextDto, IWorkspaceEditDto, MainContext, MainThreadMappedEditsShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { Range, Selection, DocumentSelector, WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters';
import type * as vscode from 'vscode';
export class ExtHostMappedEdits implements ExtHostMappedEditsShape {
private static handlePool: number = 0;
private proxy: MainThreadMappedEditsShape;
private providers = new Map<number, vscode.MappedEditsProvider>();
constructor(
mainContext: IMainContext,
private readonly _documents: ExtHostDocuments,
private readonly uriTransformer: IURITransformer | undefined
) {
this.proxy = mainContext.getProxy(MainContext.MainThreadMappedEdits);
}
async $provideMappedEdits(handle: number, docUri: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise<IWorkspaceEditDto | null> {
const provider = this.providers.get(handle);
if (!provider) {
return null;
}
const uri = URI.revive(docUri);
const doc = this._documents.getDocument(uri);
const ctx = {
selections: context.selections.map(s => Selection.to(s)),
related: context.related.map(r => ({ uri: URI.revive(r.uri), range: Range.to(r.range) })),
};
const mappedEdits = await provider.provideMappedEdits(doc, codeBlocks, ctx, token);
if (!mappedEdits) {
return null;
}
return WorkspaceEdit.from(mappedEdits);
}
registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable {
const handle = ExtHostMappedEdits.handlePool++;
this.providers.set(handle, provider);
this.proxy.$registerMappedEditsProvider(handle, DocumentSelector.from(selector, this.uriTransformer));
return {
dispose: () => {
ExtHostMappedEdits.handlePool--;
this.proxy.$unregisterMappedEditsProvider(handle);
this.providers.delete(handle);
}
};
}
}

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
@ -24,6 +25,7 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatCopyAction, IChatService, IChatUserActionEvent, InteractiveSessionCopyKind } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IMappedEditsService, RelatedContextItem } from 'vs/workbench/services/mappedEdits/common/mappedEdits';
import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon';
@ -232,17 +234,30 @@ export function registerChatCodeBlockActions() {
this.notifyUserAction(accessor, context);
}
private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, context: IChatCodeBlockActionContext) {
this.notifyUserAction(accessor, context);
private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, chatCodeBlockActionContext: IChatCodeBlockActionContext) {
this.notifyUserAction(accessor, chatCodeBlockActionContext);
const bulkEditService = accessor.get(IBulkEditService);
const codeEditorService = accessor.get(ICodeEditorService);
const mappedEditsService = accessor.get(IMappedEditsService);
const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1);
await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: context.code,
})]);
// try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection
const selections = codeEditor.getSelections() ?? [];
const mappedEditsContext = {
selections,
related: [] as RelatedContextItem[], // FIXME@ulugbekna: this needs to be populated but we don't yet have a way to get this info from extensions
};
const cancellationTokenSource = new CancellationTokenSource();
const workspaceEdit = await mappedEditsService.provideMappedEdits(activeModel, [chatCodeBlockActionContext.code], mappedEditsContext, cancellationTokenSource.token);
if (workspaceEdit) {
await bulkEditService.apply(workspaceEdit);
} else {
const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1);
await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: chatCodeBlockActionContext.code,
})]);
}
codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus();
}

View file

@ -58,6 +58,7 @@ export const allApiProposals = Object.freeze({
interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts',
ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts',
languageConfigurationAutoClosingPairs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts',
mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts',
notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts',
notebookCodeActions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts',
notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts',

View file

@ -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 'vs/base/common/cancellation';
import { ITextModel } from 'vs/editor/common/model';
import { IMappedEditsProvider, IMappedEditsService, MappedEditsContext } from 'vs/workbench/services/mappedEdits/common/mappedEdits';
import { score } from 'vs/editor/common/languageSelector';
import { WorkspaceEdit } from 'vs/editor/common/languages';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class MappedEditsService implements IMappedEditsService {
readonly _serviceBrand: undefined;
private readonly _providers = new Set<IMappedEditsProvider>();
constructor() { }
registerMappedEditsProvider(provider: IMappedEditsProvider) {
this._providers.add(provider);
return {
dispose: () => {
this._providers.delete(provider);
}
};
}
async provideMappedEdits(document: ITextModel, codeBlocks: string[], context: MappedEditsContext, token: CancellationToken): Promise<WorkspaceEdit | null> {
const language = document.getLanguageId();
const providers = [...this._providers.values()]
.map((p): [IMappedEditsProvider, number] => {
const pts = score(p.selector, document.uri, language, true, undefined, undefined);
return [p, pts];
})
.filter(([p, pts]) => pts > 0)
.sort((a, b) => b[1] - a[1]);
if (providers.length === 0) {
return null;
}
const provider = providers[0][0];
return provider.provideMappedEdits(document, codeBlocks, context, token);
}
}
registerSingleton(IMappedEditsService, MappedEditsService, InstantiationType.Delayed);

View file

@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { CancellationToken } from 'vs/base/common/cancellation';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { WorkspaceEdit } from 'vs/editor/common/languages';
import { Selection } from 'vs/editor/common/core/selection';
import { Range } from 'vs/editor/common/core/range';
export interface RelatedContextItem {
readonly uri: URI;
readonly range: Range;
}
export interface MappedEditsContext {
selections: Selection[];
/**
* If there's no context, the array should be empty. It's also empty until we figure out how to compute this or retrieve from an extension (eg, copilot chat)
*
* TODO@ulugbekna: should this array be sorted from highest priority to lowest?
*/
related: RelatedContextItem[];
}
export interface IMappedEditsProvider {
selector: LanguageSelector;
/**
* Provide mapped edits for a given document.
*
* @param document The document to provide mapped edits for.
* @param codeBlocks Code blocks that come from an LLM's reply.
* "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them.
* @param context The context for providing mapped edits.
* @param token A cancellation token.
* @returns A provider result of text edits.
*/
provideMappedEdits(
document: ITextModel,
codeBlocks: string[],
context: MappedEditsContext,
token: CancellationToken
): Promise<WorkspaceEdit | null>;
}
export const IMappedEditsService = createDecorator<IMappedEditsService>('mappedEditsService');
export interface IMappedEditsService {
_serviceBrand: undefined;
registerMappedEditsProvider(provider: IMappedEditsProvider): IDisposable;
provideMappedEdits(
document: ITextModel,
codeBlocks: string[],
context: MappedEditsContext,
token: CancellationToken): Promise<WorkspaceEdit | null>;
}

View file

@ -111,6 +111,7 @@ import 'vs/workbench/services/textMate/browser/textMateTokenizationFeature.contr
import 'vs/workbench/services/userActivity/common/userActivityService';
import 'vs/workbench/services/userActivity/browser/userActivityBrowser';
import 'vs/workbench/services/issue/browser/issueTroubleshoot';
import 'vs/workbench/services/mappedEdits/browser/mappedEditsService';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';

View file

@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
export interface RelatedContextItem {
readonly uri: Uri;
readonly range: Range;
}
export interface MappedEditsContext {
selections: Selection[];
/**
* If there's no context, the array should be empty. It's also empty until we figure out how to compute this or retrieve from an extension (eg, copilot chat)
*
* TODO: it was suggested initially to be sorted from highest priority to lowest. How would it look like?
*/
related: RelatedContextItem[];
}
/**
* Interface for providing mapped edits for a given document.
*/
export interface MappedEditsProvider {
/**
* Provide mapped edits for a given document.
* @param document The document to provide mapped edits for.
* @param codeBlocks Code blocks that come from an LLM's reply.
* "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them.
* @param context The context for providing mapped edits.
* @param token A cancellation token.
* @returns A provider result of text edits.
*/
provideMappedEdits(
document: TextDocument,
codeBlocks: string[],
context: MappedEditsContext,
token: CancellationToken
): ProviderResult<WorkspaceEdit | null>;
}
namespace chat {
export function registerMappedEditsProvider(documentSelector: DocumentSelector, provider: MappedEditsProvider): Disposable;
}
}