Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MappedEditsProvider API #190649

Merged
merged 8 commits into from Aug 23, 2023
4 changes: 4 additions & 0 deletions build/lib/i18n.resources.json
Expand Up @@ -58,6 +58,10 @@
"name": "vs/workbench/contrib/commands",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/mappedEdits",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/comments",
"project": "vscode-workbench"
Expand Down
102 changes: 102 additions & 0 deletions extensions/vscode-api-tests/src/singlefolder-tests/mappedEdits.test.ts
@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as vscode from 'vscode';
import * as assert from 'assert';

suite('mapped edits provider', () => {

test('mapped edits does not provide edits for unregistered langs', async function () {

const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts'));

const tsDocFilter = [{ language: 'json' }];

const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, {
provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => {

assert(context.selections.length === 1);
assert(context.related.length === 1);
assert('uri' in context.related[0] && 'range' in context.related[0]);

const edit = new vscode.WorkspaceEdit();
const text = codeBlocks.join('\n//----\n');
edit.replace(uri, context.selections[0], text);
return edit;
}
});
await vscode.workspace.openTextDocument(uri);
const result = await vscode.commands.executeCommand<vscode.ProviderResult<vscode.WorkspaceEdit | null>>(
'vscode.executeMappedEditsProvider',
uri,
[
'// hello',
`function foo() {\n\treturn 1;\n}`,
],
{
selections: [new vscode.Selection(0, 0, 1, 0)],
related: [
{
uri,
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0))
}
]
}
);
r1.dispose();

assert(result === null, 'returned null');
});

test('mapped edits provides a single edit replacing the selection', async function () {

const uri = vscode.Uri.file(path.join(vscode.workspace.rootPath || '', './myFile.ts'));

const tsDocFilter = [{ language: 'typescript' }];

const r1 = vscode.chat.registerMappedEditsProvider(tsDocFilter, {
provideMappedEdits: (_doc: vscode.TextDocument, codeBlocks: string[], context: vscode.MappedEditsContext, _token: vscode.CancellationToken) => {

assert(context.selections.length === 1);
assert(context.related.length === 1);
assert('uri' in context.related[0] && 'range' in context.related[0]);

const edit = new vscode.WorkspaceEdit();
const text = codeBlocks.join('\n//----\n');
edit.replace(uri, context.selections[0], text);
return edit;
}
});

await vscode.workspace.openTextDocument(uri);
const result = await vscode.commands.executeCommand<vscode.ProviderResult<vscode.WorkspaceEdit | null>>(
'vscode.executeMappedEditsProvider',
uri,
[
'// hello',
`function foo() {\n\treturn 1;\n}`,
],
{
selections: [new vscode.Selection(0, 0, 1, 0)],
related: [
{
uri,
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0))
}
]
}
);
r1.dispose();

assert(result, 'non null response');
const edits = result.get(uri);
assert(edits.length === 1);
assert(edits[0].range.start.line === 0);
assert(edits[0].range.start.character === 0);
assert(edits[0].range.end.line === 1);
assert(edits[0].range.end.character === 0);
});
});
3 changes: 3 additions & 0 deletions extensions/vscode-api-tests/testWorkspace/myFile.ts
@@ -0,0 +1,3 @@
// 1
// 2
// 3
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Expand Up @@ -86,6 +86,7 @@ import './mainThreadTimeline';
import './mainThreadTesting';
import './mainThreadSecretState';
import './mainThreadShare';
import './mainThreadMappedEdits';
import './mainThreadProfilContentHandlers';
import './mainThreadSemanticSimilarity';
import './mainThreadIssueReporter';
Expand Down
57 changes: 57 additions & 0 deletions src/vs/workbench/api/browser/mainThreadMappedEdits.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 { 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, 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();
}
}
8 changes: 7 additions & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -1340,9 +1342,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatRequestAccess');
return extHostChatProvider.requestChatResponseProvider(extension.identifier, id);
},
registerVariable(name, description, resolver) {
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);
}
};

Expand Down
21 changes: 21 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Expand Up @@ -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?
ulugbekna marked this conversation as resolved.
Show resolved Hide resolved
related: IRelatedContextItemDto[];
}

export interface ISignatureHelpProviderMetadataDto {
readonly triggerCharacters: readonly string[];
readonly retriggerCharacters: readonly string[];
Expand Down Expand Up @@ -1315,6 +1325,11 @@ export interface MainThreadShareShape extends IDisposable {
$unregisterShareProvider(handle: number): void;
}

export interface MainThreadMappedEditsShape extends IDisposable {
ulugbekna marked this conversation as resolved.
Show resolved Hide resolved
$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>;
Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand Down
21 changes: 20 additions & 1 deletion src/vs/workbench/api/common/extHostApiCommands.ts
Expand Up @@ -464,7 +464,26 @@ const newCommands: ApiCommand[] = [
new ApiCommandArgument('value', 'The context key value', () => true, v => v),
],
ApiCommandResult.Void
)
),
// --- mapped edits
new ApiCommand(
'vscode.executeMappedEditsProvider', '_executeMappedEditsProvider', 'Execute Mapped Edits Provider',
[
ApiCommandArgument.Uri,
ApiCommandArgument.StringArray,
new ApiCommandArgument(
'MappedEditsContext',
'Mapped Edits Context',
(v: unknown) => typeConverters.MappedEditsContext.is(v),
(v: vscode.MappedEditsContext) => typeConverters.MappedEditsContext.from(v)
)
],
new ApiCommandResult<IWorkspaceEditDto, vscode.WorkspaceEdit>(
'A promise that resolves to a workspace edit or null',
(value) => {
return typeConverters.WorkspaceEdit.to(value);
})
),
];

//#endregion
Expand Down
10 changes: 10 additions & 0 deletions src/vs/workbench/api/common/extHostCommands.ts
Expand Up @@ -444,6 +444,16 @@ export class ApiCommandArgument<V, O = V> {
static readonly Selection = new ApiCommandArgument<extHostTypes.Selection, ISelection>('selection', 'A selection in a text document', v => extHostTypes.Selection.isSelection(v), extHostTypeConverter.Selection.from);
static readonly Number = new ApiCommandArgument<number>('number', '', v => typeof v === 'number', v => v);
static readonly String = new ApiCommandArgument<string>('string', '', v => typeof v === 'string', v => v);
static readonly StringArray = ApiCommandArgument.Arr(ApiCommandArgument.String);

static Arr<T, K = T>(element: ApiCommandArgument<T, K>) {
return new ApiCommandArgument(
`${element.name}_array`,
`Array of ${element.name}, ${element.description}`,
(v: unknown) => Array.isArray(v) && v.every(e => element.validate(e)),
(v: T[]) => v.map(e => element.convert(e))
);
}

static readonly CallHierarchyItem = new ApiCommandArgument('item', 'A call hierarchy item', v => v instanceof extHostTypes.CallHierarchyItem, extHostTypeConverter.CallHierarchyItem.from);
static readonly TypeHierarchyItem = new ApiCommandArgument('item', 'A type hierarchy item', v => v instanceof extHostTypes.TypeHierarchyItem, extHostTypeConverter.TypeHierarchyItem.from);
Expand Down
61 changes: 61 additions & 0 deletions src/vs/workbench/api/common/extHostMappedEdits.ts
@@ -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);
}
};
}

}