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
3 changes: 2 additions & 1 deletion extensions/vscode-api-tests/package.json
Expand Up @@ -19,6 +19,7 @@
"fileSearchProvider",
"findTextInFiles",
"fsChunks",
"mappedEditsProvider",
"notebookCellExecutionState",
"notebookDeprecated",
"notebookLiveShare",
Expand All @@ -45,7 +46,7 @@
"timeline",
"tokenInformation",
"treeItemCheckbox",
"treeViewActiveItem",
"treeViewActiveItem",
"treeViewReveal",
"workspaceTrust",
"telemetry",
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
32 changes: 31 additions & 1 deletion src/vs/editor/common/languages.ts
Expand Up @@ -16,7 +16,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { LanguageId } from 'vs/editor/common/encodedTokenAttributes';
import * as model from 'vs/editor/common/model';
import { TokenizationRegistry as TokenizationRegistryImpl } from 'vs/editor/common/tokenizationRegistry';
Expand Down Expand Up @@ -2033,3 +2033,33 @@ export interface DocumentOnDropEditProvider {

provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;
}

export interface RelatedContextItem {
readonly uri: URI;
readonly range: IRange;
}

export interface MappedEditsContext {
selections: ISelection[];
related: RelatedContextItem[];
}

export interface MappedEditsProvider {

/**
* Provider maps code blocks from the chat into a workspace edit.
*
* @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: model.ITextModel,
codeBlocks: string[],
context: MappedEditsContext,
token: CancellationToken
): Promise<WorkspaceEdit | null>;
}
4 changes: 3 additions & 1 deletion src/vs/editor/common/services/languageFeatures.ts
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { LanguageFeatureRegistry, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry';
import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages';
import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, MappedEditsProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

export const ILanguageFeaturesService = createDecorator<ILanguageFeaturesService>('ILanguageFeaturesService');
Expand Down Expand Up @@ -71,6 +71,8 @@ export interface ILanguageFeaturesService {

readonly documentOnDropEditProvider: LanguageFeatureRegistry<DocumentOnDropEditProvider>;

readonly mappedEditsProvider: LanguageFeatureRegistry<MappedEditsProvider>;

// --

setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void;
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/common/services/languageFeaturesService.ts
Expand Up @@ -5,7 +5,7 @@

import { URI } from 'vs/base/common/uri';
import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry';
import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages';
import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, MappedEditsProvider } from 'vs/editor/common/languages';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';

Expand Down Expand Up @@ -42,6 +42,7 @@ export class LanguageFeaturesService implements ILanguageFeaturesService {
readonly documentSemanticTokensProvider = new LanguageFeatureRegistry<DocumentSemanticTokensProvider>(this._score.bind(this));
readonly documentOnDropEditProvider = new LanguageFeatureRegistry<DocumentOnDropEditProvider>(this._score.bind(this));
readonly documentPasteEditProvider = new LanguageFeatureRegistry<DocumentPasteEditProvider>(this._score.bind(this));
readonly mappedEditsProvider: LanguageFeatureRegistry<MappedEditsProvider> = new LanguageFeatureRegistry<MappedEditsProvider>(this._score.bind(this));

private _notebookTypeResolver?: NotebookInfoResolver;

Expand Down
24 changes: 24 additions & 0 deletions src/vs/monaco.d.ts
Expand Up @@ -7891,6 +7891,30 @@ declare namespace monaco.languages {
provideDocumentRangeSemanticTokens(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult<SemanticTokens>;
}

export interface RelatedContextItem {
readonly uri: Uri;
readonly range: IRange;
}

export interface MappedEditsContext {
selections: ISelection[];
related: RelatedContextItem[];
}

export interface MappedEditsProvider {
/**
* Provider maps code blocks from the chat into a workspace edit.
*
* @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: editor.ITextModel, codeBlocks: string[], context: MappedEditsContext, token: CancellationToken): Promise<WorkspaceEdit | null>;
}

export interface ILanguageExtensionPoint {
id: string;
extensions?: string[];
Expand Down
21 changes: 21 additions & 0 deletions src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Expand Up @@ -931,6 +931,13 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
}
return provider.resolveDocumentOnDropFileData(requestId, dataId);
}

// --- mapped edits

$registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void {
const provider = new MainThreadMappedEditsProvider(handle, this._proxy, this._uriIdentService);
this._registrations.set(handle, this._languageFeaturesService.mappedEditsProvider.register(selector, provider));
}
}

class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider {
Expand Down Expand Up @@ -1124,3 +1131,17 @@ export class MainThreadDocumentRangeSemanticTokensProvider implements languages.
throw new Error(`Unexpected`);
}
}

export class MainThreadMappedEditsProvider implements languages.MappedEditsProvider {

constructor(
private readonly _handle: number,
private readonly _proxy: ExtHostLanguageFeaturesShape,
private readonly _uriService: IUriIdentityService,
) { }

async provideMappedEdits(document: ITextModel, codeBlocks: string[], context: languages.MappedEditsContext, token: CancellationToken): Promise<languages.WorkspaceEdit | null> {
const res = await this._proxy.$provideMappedEdits(this._handle, document.uri, codeBlocks, context, token);
return res ? reviveWorkspaceEditDto(res, this._uriService) : null;
}
}
6 changes: 5 additions & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Expand Up @@ -1340,9 +1340,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 extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider);
}
};

Expand Down
12 changes: 12 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[];
related: IRelatedContextItemDto[];
}

export interface ISignatureHelpProviderMetadataDto {
readonly triggerCharacters: readonly string[];
readonly retriggerCharacters: readonly string[];
Expand Down Expand Up @@ -426,6 +436,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
$resolvePasteFileData(handle: number, requestId: number, dataId: string): Promise<VSBuffer>;
$resolveDocumentOnDropFileData(handle: number, requestId: number, dataId: string): Promise<VSBuffer>;
$setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void;
$registerMappedEditsProvider(handle: number, selector: IDocumentFilterDto[]): void;
}

export interface MainThreadLanguagesShape extends IDisposable {
Expand Down Expand Up @@ -1999,6 +2010,7 @@ export interface ExtHostLanguageFeaturesShape {
$provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>;
$releaseTypeHierarchy(handle: number, sessionId: string): void;
$provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<IDocumentOnDropEditDto | undefined>;
$provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise<IWorkspaceEditDto | null>;
}

export interface ExtHostQuickOpenShape {
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 | null, vscode.WorkspaceEdit | null>(
'A promise that resolves to a workspace edit or null',
(value) => {
return value ? typeConverters.WorkspaceEdit.to(value) : null;
})
),
];

//#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