mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
add vscode.executeMappedEditsProvider
command & use it to have integration tests for mapped-edits service
This commit is contained in:
parent
3e9e7b3b5a
commit
c3a4fbbe8f
|
@ -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"
|
||||
|
|
|
@ -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
extensions/vscode-api-tests/testWorkspace/myFile.ts
Normal file
3
extensions/vscode-api-tests/testWorkspace/myFile.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// 1
|
||||
// 2
|
||||
// 3
|
|
@ -6,7 +6,7 @@
|
|||
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 { 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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -32,6 +32,7 @@ import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from 'vs/
|
|||
import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import type * as mappedEdits from 'vs/workbench/services/mappedEdits/common/mappedEdits';
|
||||
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/editor';
|
||||
import { IViewBadge } from 'vs/workbench/common/views';
|
||||
import { IChatFollowup, IChatReplyFollowup, IChatResponseCommandFollowup } from 'vs/workbench/contrib/chat/common/chatService';
|
||||
|
@ -1563,6 +1564,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(context: vscode.MappedEditsContext): mappedEdits.MappedEditsContext {
|
||||
return {
|
||||
selections: context.selections.map(s => Selection.from(s)),
|
||||
related: context.related.map(r => ({
|
||||
uri: URI.from(r.uri),
|
||||
range: Range.from(r.range)
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace NotebookRange {
|
||||
|
||||
export function from(range: vscode.NotebookRange): ICellRange {
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMappedEditsService, MappedEditsContext } from 'vs/workbench/services/mappedEdits/common/mappedEdits';
|
||||
|
||||
class ExecuteMappedEditsProvider extends Action2 {
|
||||
|
||||
static readonly ID = '_executeMappedEditsProvider';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ExecuteMappedEditsProvider.ID,
|
||||
title: { value: nls.localize('executeMappedEditsProvider', "Execute Mapped Edits Provider"), original: '' },
|
||||
f1: false,
|
||||
description: {
|
||||
description: nls.localize('executeMappedEditsProvider.description', "Executes Mapped Edits Provider and returns the corresponding WorkspaceEdit or null if no edits are provided."),
|
||||
args: [
|
||||
// FIXME@ulugbekna
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, documentUri: URI, codeBlocks: string[], context: MappedEditsContext) {
|
||||
|
||||
const mappedEditsService = accessor.get(IMappedEditsService);
|
||||
const modelService = accessor.get(ITextModelService);
|
||||
|
||||
const document = await modelService.createModelReference(documentUri);
|
||||
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
const res = await mappedEditsService.provideMappedEdits(document.object.textEditorModel, codeBlocks, context, cancellationTokenSource.token);
|
||||
|
||||
document.dispose();
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(ExecuteMappedEditsProvider);
|
|
@ -10,16 +10,16 @@ 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';
|
||||
import { ISelection } from 'vs/editor/common/core/selection';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
|
||||
export interface RelatedContextItem {
|
||||
readonly uri: URI;
|
||||
readonly range: Range;
|
||||
readonly range: IRange;
|
||||
}
|
||||
|
||||
export interface MappedEditsContext {
|
||||
selections: Selection[];
|
||||
selections: ISelection[];
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
|
|
@ -227,6 +227,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';
|
||||
|
||||
|
|
Loading…
Reference in a new issue