Merge pull request #190649 from microsoft/ulugbekna/insert-at-cursor-api

MappedEditsProvider API
This commit is contained in:
Ulugbek Abdullaev 2023-08-23 12:28:24 +02:00 committed by GitHub
commit e4bfe48d92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 453 additions and 15 deletions

View file

@ -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"

View file

@ -19,6 +19,7 @@
"fileSearchProvider",
"findTextInFiles",
"fsChunks",
"mappedEditsProvider",
"notebookCellExecutionState",
"notebookDeprecated",
"notebookLiveShare",
@ -45,7 +46,7 @@
"timeline",
"tokenInformation",
"treeItemCheckbox",
"treeViewActiveItem",
"treeViewActiveItem",
"treeViewReveal",
"workspaceTrust",
"telemetry",

View file

@ -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);
});
});

View file

@ -0,0 +1,3 @@
// 1
// 2
// 3

View file

@ -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';
@ -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>;
}

View file

@ -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');
@ -71,6 +71,8 @@ export interface ILanguageFeaturesService {
readonly documentOnDropEditProvider: LanguageFeatureRegistry<DocumentOnDropEditProvider>;
readonly mappedEditsProvider: LanguageFeatureRegistry<MappedEditsProvider>;
// --
setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void;

View file

@ -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';
@ -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;

24
src/vs/monaco.d.ts vendored
View file

@ -7893,6 +7893,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[];

View file

@ -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 {
@ -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;
}
}

View file

@ -1356,9 +1356,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);
}
};

View file

@ -372,6 +372,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[];
@ -427,6 +437,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 {
@ -2019,6 +2030,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 {

View file

@ -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

View file

@ -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);

View file

@ -1817,6 +1817,34 @@ class DocumentOnDropEditAdapter {
}
}
class MappedEditsAdapter {
constructor(
private readonly _documents: ExtHostDocuments,
private readonly _provider: vscode.MappedEditsProvider,
) { }
async provideMappedEdits(
resource: UriComponents,
codeBlocks: string[],
context: extHostProtocol.IMappedEditsContextDto,
token: CancellationToken
): Promise<extHostProtocol.IWorkspaceEditDto | null> {
const uri = URI.revive(resource);
const doc = this._documents.getDocument(uri);
const ctx = {
selections: context.selections.map(s => typeConvert.Selection.to(s)),
related: context.related.map(r => ({ uri: URI.revive(r.uri), range: typeConvert.Range.to(r.range) })),
};
const mappedEdits = await this._provider.provideMappedEdits(doc, codeBlocks, ctx, token);
return mappedEdits ? typeConvert.WorkspaceEdit.from(mappedEdits) : null;
}
}
type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter
| DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter
| RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter
@ -1826,7 +1854,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov
| DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter
| EvaluatableExpressionAdapter | InlineValuesAdapter
| LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter
| DocumentOnDropEditAdapter;
| DocumentOnDropEditAdapter | MappedEditsAdapter;
class AdapterData {
constructor(
@ -2383,7 +2411,14 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
}
$provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Promise<languages.FoldingRange[] | undefined> {
return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token), undefined, token);
return this._withAdapter(
handle,
FoldingProviderAdapter,
(adapter) =>
adapter.provideFoldingRanges(URI.revive(resource), context, token),
undefined,
token
);
}
// --- smart select
@ -2462,6 +2497,19 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined);
}
// --- mapped edits
registerMappedEditsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider): vscode.Disposable {
const handle = this._addNewAdapter(new MappedEditsAdapter(this._documents, provider), extension);
this._proxy.$registerMappedEditsProvider(handle, this._transformDocumentSelector(selector, extension));
return this._createDisposable(handle);
}
$provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: extHostProtocol.IMappedEditsContextDto, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto | null> {
return this._withAdapter(handle, MappedEditsAdapter, adapter =>
Promise.resolve(adapter.provideMappedEdits(document, codeBlocks, context, token)), null, token);
}
// --- copy/paste actions
registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable {

View file

@ -1563,6 +1563,30 @@ export namespace LanguageSelector {
}
}
export namespace MappedEditsContext {
export function is(v: unknown): v is vscode.MappedEditsContext {
return (!!v &&
typeof v === 'object' &&
'selections' in v &&
Array.isArray(v.selections) &&
v.selections.every(s => s instanceof types.Selection) &&
'related' in v &&
Array.isArray(v.related) &&
v.related.every(e => e && typeof e === 'object' && URI.isUri(e.uri) && e.range instanceof types.Range));
}
export function from(extContext: vscode.MappedEditsContext): languages.MappedEditsContext {
return {
selections: extContext.selections.map(s => Selection.from(s)),
related: extContext.related.map(r => ({
uri: URI.from(r.uri),
range: Range.from(r.range)
}))
};
}
}
export namespace NotebookRange {
export function from(range: vscode.NotebookRange): ICellRange {

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';
@ -30,6 +31,8 @@ import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm
import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { WorkspaceEdit, RelatedContextItem } from 'vs/editor/common/languages';
export interface IChatCodeBlockActionContext {
code: string;
@ -232,17 +235,44 @@ 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 activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1);
await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: context.code,
})]);
const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel);
// try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection
let workspaceEdit: WorkspaceEdit | null = null;
if (mappedEditsProviders.length > 0) {
const mostRelevantProvider = mappedEditsProviders[0];
const selections = codeEditor.getSelections() ?? [];
const mappedEditsContext = {
selections,
related: [] as RelatedContextItem[], // TODO@ulugbekna: we do have not yet decided what to populate this with
};
const cancellationTokenSource = new CancellationTokenSource();
workspaceEdit = await mostRelevantProvider.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

@ -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 { CancellationTokenSource } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import * as languages from 'vs/editor/common/languages';
CommandsRegistry.registerCommand(
'_executeMappedEditsProvider',
async (
accessor: ServicesAccessor,
documentUri: URI,
codeBlocks: string[],
context: languages.MappedEditsContext
): Promise<languages.WorkspaceEdit | null> => {
const modelService = accessor.get(ITextModelService);
const langFeaturesService = accessor.get(ILanguageFeaturesService);
const document = await modelService.createModelReference(documentUri);
let result: languages.WorkspaceEdit | null = null;
try {
const providers = langFeaturesService.mappedEditsProvider.ordered(document.object.textEditorModel);
if (providers.length > 0) {
const mostRelevantProvider = providers[0];
const cancellationTokenSource = new CancellationTokenSource();
result = await mostRelevantProvider.provideMappedEdits(
document.object.textEditorModel,
codeBlocks,
context,
cancellationTokenSource.token
);
}
} finally {
document.dispose();
}
return result;
}
);

View file

@ -59,6 +59,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

@ -228,6 +228,9 @@ import 'vs/workbench/contrib/markers/browser/markers.contribution';
// Merge Editor
import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution';
// Mapped Edits
import 'vs/workbench/contrib/mappedEdits/common/mappedEdits.contribution';
// Commands
import 'vs/workbench/contrib/commands/common/commands.contribution';

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;
}
}