From eccf728e6444ceed2ed50e833f6977ca5c2a8c6f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 22 Jan 2018 11:45:22 -0800 Subject: [PATCH] CodeActionScope (#41782) * Add CodeActionScope * Replace matches with contains, try using in ts extension * Move filtering to getCodeActions * Basic test * Docs * Fix tests * Hooking up requested scope * Add basic test for requestedScope * Added auto apply logic * Gate refactor provider to only compute refactorings when requested * Making suggested renames * Clean up code action trigger impl to use single Trrigger info object * Rename codeActionScope file and internal CodeActionScope class * Add quick fix base type * Make keybinding API more similar to insertSnippet Take args as an object instead of as an array of values * Clean up docs * scope -> kind * Fixing examples to match Refactor kind --- .../src/features/refactorProvider.ts | 19 ++++- src/vs/editor/common/modes.ts | 10 ++- .../contrib/quickFix/codeActionTrigger.ts | 32 +++++++ src/vs/editor/contrib/quickFix/quickFix.ts | 9 +- .../contrib/quickFix/quickFixCommands.ts | 85 +++++++++++++++++-- .../editor/contrib/quickFix/quickFixModel.ts | 29 ++++--- .../contrib/quickFix/test/quickFix.test.ts | 51 ++++++++++- .../quickFix/test/quickFixModel.test.ts | 6 +- .../standalone/browser/standaloneLanguages.ts | 9 +- src/vs/monaco.d.ts | 5 ++ src/vs/vscode.d.ts | 74 ++++++++++++++++ .../mainThreadLanguageFeatures.ts | 4 +- src/vs/workbench/api/node/extHost.api.impl.ts | 1 + src/vs/workbench/api/node/extHost.protocol.ts | 3 +- .../workbench/api/node/extHostApiCommands.ts | 3 + .../api/node/extHostLanguageFeatures.ts | 15 ++-- src/vs/workbench/api/node/extHostTypes.ts | 28 ++++++ .../api/extHostLanguageFeatures.test.ts | 30 ++++++- 18 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 src/vs/editor/contrib/quickFix/codeActionTrigger.ts diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 14dca20a4b5..45e47597017 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -108,13 +108,17 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv public async provideCodeActions( document: vscode.TextDocument, _range: vscode.Range, - _context: vscode.CodeActionContext, + context: vscode.CodeActionContext, token: vscode.CancellationToken ): Promise { if (!this.client.apiVersion.has240Features()) { return []; } + if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) { + return []; + } + if (!vscode.window.activeTextEditor) { return []; } @@ -146,7 +150,8 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv title: info.description, command: SelectRefactorCommand.ID, arguments: [document, file, info, range] - } + }, + kind: vscode.CodeActionKind.Refactor }); } else { for (const action of info.actions) { @@ -156,7 +161,8 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv title: action.description, command: ApplyRefactoringCommand.ID, arguments: [document, file, info.name, action.name, range] - } + }, + kind: TypeScriptRefactorProvider.getKind(action) }); } } @@ -166,4 +172,11 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv return []; } } + + private static getKind(refactor: Proto.RefactorActionInfo) { + if (refactor.name.startsWith('function_')) { + return vscode.CodeActionKind.RefactorExtract.append('function'); + } + return vscode.CodeActionKind.Refactor; + } } \ No newline at end of file diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c3c19319c48..6b573d085a9 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -343,6 +343,14 @@ export interface CodeAction { command?: Command; edit?: WorkspaceEdit; diagnostics?: IMarkerData[]; + kind?: string; +} + +/** + * @internal + */ +export interface CodeActionContext { + only?: string; } /** @@ -354,7 +362,7 @@ export interface CodeActionProvider { /** * Provide commands for the given document and range. */ - provideCodeActions(model: model.ITextModel, range: Range, token: CancellationToken): CodeAction[] | Thenable; + provideCodeActions(model: model.ITextModel, range: Range, context: CodeActionContext, token: CancellationToken): CodeAction[] | Thenable; } /** diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts new file mode 100644 index 00000000000..0cd81d55df8 --- /dev/null +++ b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { startsWith } from 'vs/base/common/strings'; + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + + constructor( + public readonly value: string + ) { } + + public contains(other: string): boolean { + return this.value === other || startsWith(other, this.value + CodeActionKind.sep); + } +} + +export enum CodeActionAutoApply { + IfSingle = 1, + First = 2, + Never = 3 +} + +export interface CodeActionTrigger { + type: 'auto' | 'manual'; + kind?: CodeActionKind; + autoApply?: CodeActionAutoApply; +} \ No newline at end of file diff --git a/src/vs/editor/contrib/quickFix/quickFix.ts b/src/vs/editor/contrib/quickFix/quickFix.ts index 8e619e15c03..10bbbe4d5e8 100644 --- a/src/vs/editor/contrib/quickFix/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/quickFix.ts @@ -14,16 +14,19 @@ import { onUnexpectedExternalError, illegalArgument } from 'vs/base/common/error import { IModelService } from 'vs/editor/common/services/modelService'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { CodeActionKind } from './codeActionTrigger'; -export function getCodeActions(model: ITextModel, range: Range): TPromise { +export function getCodeActions(model: ITextModel, range: Range, scope?: CodeActionKind): TPromise { const allResults: CodeAction[] = []; const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, range, token)).then(result => { + return asWinJsPromise(token => support.provideCodeActions(model, range, { only: scope ? scope.value : undefined }, token)).then(result => { if (Array.isArray(result)) { for (const quickFix of result) { if (quickFix) { - allResults.push(quickFix); + if (!scope || (quickFix.kind && scope.contains(quickFix.kind))) { + allResults.push(quickFix); + } } } } diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/quickFixCommands.ts index 2d067db0bf0..9850c794400 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/quickFixCommands.ts @@ -15,11 +15,12 @@ import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { QuickFixContextMenu } from './quickFixWidget'; import { LightBulbWidget } from './lightBulbWidget'; import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel'; +import { CodeActionKind, CodeActionAutoApply } from './codeActionTrigger'; import { TPromise } from 'vs/base/common/winjs.base'; import { CodeAction } from 'vs/editor/common/modes'; import { createBulkEdit } from 'vs/editor/browser/services/bulkEdit'; @@ -57,7 +58,7 @@ export class QuickFixController implements IEditorContribution { this._updateLightBulbTitle(); this._disposables.push( - this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger('auto')), + this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto' })), this._lightBulbWidget.onClick(this._handleLightBulbSelect, this), this._model.onDidChangeFixes(e => this._onQuickFixEvent(e)), this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this) @@ -70,9 +71,21 @@ export class QuickFixController implements IEditorContribution { } private _onQuickFixEvent(e: QuickFixComputeEvent): void { - if (e && e.type === 'manual') { - this._quickFixContextMenu.show(e.fixes, e.position); + if (e && e.trigger.kind) { + // Triggered for specific scope + // Apply if we only have one action or requested autoApply, otherwise show menu + e.fixes.then(fixes => { + if (e.trigger.autoApply === CodeActionAutoApply.First || (e.trigger.autoApply === CodeActionAutoApply.IfSingle && fixes.length === 1)) { + this._onApplyCodeAction(fixes[0]); + } else { + this._quickFixContextMenu.show(e.fixes, e.position); + } + }); + return; + } + if (e && e.trigger.type === 'manual') { + this._quickFixContextMenu.show(e.fixes, e.position); } else if (e && e.fixes) { // auto magically triggered // * update an existing list of code actions @@ -96,7 +109,11 @@ export class QuickFixController implements IEditorContribution { } public triggerFromEditorSelection(): void { - this._model.trigger('manual'); + this._model.trigger({ type: 'manual' }); + } + + public triggerCodeActionFromEditorSelection(kind?: CodeActionKind, autoApply?: CodeActionAutoApply): void { + this._model.trigger({ type: 'manual', kind, autoApply }); } private _updateLightBulbTitle(): void { @@ -148,5 +165,63 @@ export class QuickFixAction extends EditorAction { } } + +class CodeActionCommandArgs { + public static fromUser(arg: any): CodeActionCommandArgs { + if (!arg || typeof arg !== 'object') { + return new CodeActionCommandArgs(CodeActionKind.Empty, CodeActionAutoApply.IfSingle); + } + return new CodeActionCommandArgs( + CodeActionCommandArgs.getKindFromUser(arg), + CodeActionCommandArgs.getApplyFromUser(arg)); + } + + private static getApplyFromUser(arg: any) { + switch (typeof arg.apply === 'string' ? arg.apply.toLowerCase() : '') { + case 'first': + return CodeActionAutoApply.First; + + case 'never': + return CodeActionAutoApply.Never; + + case 'ifsingle': + default: + return CodeActionAutoApply.IfSingle; + } + } + + private static getKindFromUser(arg: any) { + return typeof arg.kind === 'string' + ? new CodeActionKind(arg.kind) + : CodeActionKind.Empty; + } + + private constructor( + public readonly kind: CodeActionKind, + public readonly apply: CodeActionAutoApply + ) { } +} + +export class CodeActionCommand extends EditorCommand { + + static readonly Id = 'editor.action.codeAction'; + + constructor() { + super({ + id: CodeActionCommand.Id, + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider) + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { + const controller = QuickFixController.get(editor); + if (controller) { + const args = CodeActionCommandArgs.fromUser(userArg); + controller.triggerCodeActionFromEditorSelection(args.kind, args.apply); + } + } +} + registerEditorContribution(QuickFixController); registerEditorAction(QuickFixAction); +registerEditorCommand(new CodeActionCommand()); diff --git a/src/vs/editor/contrib/quickFix/quickFixModel.ts b/src/vs/editor/contrib/quickFix/quickFixModel.ts index e2ff0dcd2a1..3e4366c4d63 100644 --- a/src/vs/editor/contrib/quickFix/quickFixModel.ts +++ b/src/vs/editor/contrib/quickFix/quickFixModel.ts @@ -13,6 +13,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { CodeActionProviderRegistry, CodeAction } from 'vs/editor/common/modes'; import { getCodeActions } from './quickFix'; +import { CodeActionTrigger } from './codeActionTrigger'; import { Position } from 'vs/editor/common/core/position'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -36,26 +37,26 @@ export class QuickFixOracle { this._disposables = dispose(this._disposables); } - trigger(type: 'manual' | 'auto'): void { + trigger(trigger: CodeActionTrigger): void { let rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfSelectionUnlessWhitespaceEnclosed(); - if (!rangeOrSelection && type === 'manual') { + if (!rangeOrSelection && trigger.type === 'manual') { rangeOrSelection = this._editor.getSelection(); } - this._createEventAndSignalChange(type, rangeOrSelection); + this._createEventAndSignalChange(trigger, rangeOrSelection); } private _onMarkerChanges(resources: URI[]): void { const { uri } = this._editor.getModel(); for (const resource of resources) { if (resource.toString() === uri.toString()) { - this.trigger('auto'); + this.trigger({ type: 'auto' }); return; } } } private _onCursorChange(): void { - this.trigger('auto'); + this.trigger({ type: 'auto' }); } private _getRangeOfMarker(): Range { @@ -98,24 +99,24 @@ export class QuickFixOracle { return selection; } - private _createEventAndSignalChange(type: 'auto' | 'manual', rangeOrSelection: Range | Selection): void { + private _createEventAndSignalChange(trigger: CodeActionTrigger, rangeOrSelection: Range | Selection): void { if (!rangeOrSelection) { // cancel this._signalChange({ - type, + trigger, range: undefined, position: undefined, - fixes: undefined + fixes: undefined, }); } else { // actual const model = this._editor.getModel(); const range = model.validateRange(rangeOrSelection); const position = rangeOrSelection instanceof Selection ? rangeOrSelection.getPosition() : rangeOrSelection.getStartPosition(); - const fixes = getCodeActions(model, range); + const fixes = getCodeActions(model, range, trigger && trigger.kind); this._signalChange({ - type, + trigger, range, position, fixes @@ -125,7 +126,7 @@ export class QuickFixOracle { } export interface QuickFixComputeEvent { - type: 'auto' | 'manual'; + trigger: CodeActionTrigger; range: Range; position: Position; fixes: TPromise; @@ -172,13 +173,13 @@ export class QuickFixModel { && !this._editor.getConfiguration().readOnly) { this._quickFixOracle = new QuickFixOracle(this._editor, this._markerService, p => this._onDidChangeFixes.fire(p)); - this._quickFixOracle.trigger('auto'); + this._quickFixOracle.trigger({ type: 'auto' }); } } - trigger(type: 'auto' | 'manual'): void { + trigger(trigger: CodeActionTrigger): void { if (this._quickFixOracle) { - this._quickFixOracle.trigger(type); + this._quickFixOracle.trigger(trigger); } } } diff --git a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts index 8e1fdc79473..ba52da9dde7 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts @@ -8,10 +8,11 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, IResourceEdit } from 'vs/editor/common/modes'; +import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, IResourceEdit, CodeAction, CodeActionContext } from 'vs/editor/common/modes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { getCodeActions } from 'vs/editor/contrib/quickFix/quickFix'; +import { CodeActionKind } from 'vs/editor/contrib/quickFix/codeActionTrigger'; suite('QuickFix', () => { @@ -120,4 +121,52 @@ suite('QuickFix', () => { assert.equal(actions.length, 6); assert.deepEqual(actions, expected); }); + + test('getCodeActions should filter by scope', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(): CodeAction[] { + return [ + { title: 'a', kind: 'a' }, + { title: 'b', kind: 'b' }, + { title: 'a.b', kind: 'a.b' } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + assert.equal(actions.length, 2); + assert.strictEqual(actions[0].title, 'a'); + assert.strictEqual(actions[1].title, 'a.b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b')); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a.b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a.b.c')); + assert.equal(actions.length, 0); + } + }); + + test('getCodeActions should forward requested scope to providers', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(_model: any, _range: Range, context: CodeActionContext, _token: any): CodeAction[] { + return [ + { title: context.only, kind: context.only } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a'); + }); }); diff --git a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts b/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts index c2806d7d31e..a6c3b4663a7 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFixModel.test.ts @@ -47,7 +47,7 @@ suite('QuickFix', () => { test('Orcale -> marker added', done => { const oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.ok(e.fixes); e.fixes.then(fixes => { @@ -83,7 +83,7 @@ suite('QuickFix', () => { return new Promise((resolve, reject) => { const oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.ok(e.fixes); e.fixes.then(fixes => { oracle.dispose(); @@ -160,7 +160,7 @@ suite('QuickFix', () => { await new Promise(resolve => { let oracle = new QuickFixOracle(editor, markerService, e => { - assert.equal(e.type, 'auto'); + assert.equal(e.trigger.type, 'auto'); assert.deepEqual(e.range, { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4 }); assert.deepEqual(e.position, { lineNumber: 3, column: 1 }); diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5c5b72caa39..1e4f3f4fc29 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -329,11 +329,11 @@ export function registerCodeLensProvider(languageId: string, provider: modes.Cod */ export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider): IDisposable { return modes.CodeActionProviderRegistry.register(languageId, { - provideCodeActions: (model: model.ITextModel, range: Range, token: CancellationToken): (modes.Command | modes.CodeAction)[] | Thenable<(modes.Command | modes.CodeAction)[]> => { + provideCodeActions: (model: model.ITextModel, range: Range, context: modes.CodeActionContext, token: CancellationToken): (modes.Command | modes.CodeAction)[] | Thenable<(modes.Command | modes.CodeAction)[]> => { let markers = StaticServices.markerService.get().read({ resource: model.uri }).filter(m => { return Range.areIntersectingOrTouching(m, range); }); - return provider.provideCodeActions(model, range, { markers }, token); + return provider.provideCodeActions(model, range, { markers, only: context.only }, token); } }); } @@ -401,6 +401,11 @@ export interface CodeActionContext { * @readonly */ readonly markers: IMarkerData[]; + + /** + * Requested kind of actions to return. + */ + readonly only?: string; } /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b419e7e58ba..dd87b203a17 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4043,6 +4043,10 @@ declare module monaco.languages { * @readonly */ readonly markers: editor.IMarkerData[]; + /** + * Requested kind of actions to return. + */ + readonly only?: string; } /** @@ -4495,6 +4499,7 @@ declare module monaco.languages { command?: Command; edit?: WorkspaceEdit; diagnostics?: editor.IMarkerData[]; + kind?: string; } /** diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 4f851542eff..b0582117933 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1812,6 +1812,66 @@ declare module 'vscode' { */ export type ProviderResult = T | undefined | null | Thenable; + /** + * Kind of a code action. + * + * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. + */ + export class CodeActionKind { + /** + * Empty kind. + */ + static readonly Empty: CodeActionKind; + + /** + * Base kind for quickfix actions. + */ + static readonly QuickFix: CodeActionKind; + + /** + * Base kind for refactoring actions. + */ + static readonly Refactor: CodeActionKind; + + /** + * Base kind for refactoring extraction actions. + */ + static readonly RefactorExtract: CodeActionKind; + + /** + * Base kind for refactoring inline actions. + */ + static readonly RefactorInline: CodeActionKind; + + /** + * Base kind for refactoring rewite actions. + */ + static readonly RefactorRewrite: CodeActionKind; + + private constructor(value: string); + + /** + * String value of the kind, e.g. `"refactor.extract.function"`. + */ + readonly value?: string; + + /** + * Create a new kind by appending a more specific selector to the current kind. + * + * Does not modify the current kind. + */ + append(parts: string): CodeActionKind; + + /** + * Does this kind contain `other`? + * + * The kind `"refactor"` for example contains `"refactor.extract"` and ``"refactor.extract.function"`, but not `"unicorn.refactor.extract"` or `"refactory.extract"` + * + * @param other Kind to check. + */ + contains(other: CodeActionKind): boolean; + } + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. @@ -1821,6 +1881,13 @@ declare module 'vscode' { * An array of diagnostics. */ readonly diagnostics: Diagnostic[]; + + /** + * Requested kind of actions to return. + * + * Actions not of this kind are filtered out before being shown by the lightbulb. + */ + readonly only?: CodeActionKind; } /** @@ -1853,6 +1920,13 @@ declare module 'vscode' { */ command?: Command; + /** + * Kind of the code action. + * + * Used to filter code actions. + */ + readonly kind?: CodeActionKind; + /** * Creates a new code action. * diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index d60034d69a2..1b6408a0ec8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -206,8 +206,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerQuickFixSupport(handle: number, selector: vscode.DocumentSelector): void { this._registrations[handle] = modes.CodeActionProviderRegistry.register(toLanguageSelector(selector), { - provideCodeActions: (model: ITextModel, range: EditorRange, token: CancellationToken): Thenable => { - return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range))).then(MainThreadLanguageFeatures._reviveCodeActionDto); + provideCodeActions: (model: ITextModel, range: EditorRange, context: modes.CodeActionContext, token: CancellationToken): Thenable => { + return this._heapService.trackRecursive(wireCancellationToken(token, this._proxy.$provideCodeActions(handle, model.uri, range, context))).then(MainThreadLanguageFeatures._reviveCodeActionDto); } }); } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4d6eb312357..cc7aafe634b 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -556,6 +556,7 @@ export function createApiFactory( Breakpoint: extHostTypes.Breakpoint, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, + CodeActionKind: extHostTypes.CodeActionKind, CodeLens: extHostTypes.CodeLens, Color: extHostTypes.Color, ColorPresentation: extHostTypes.ColorPresentation, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index ea9d6e8b960..d4dcd03d363 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -639,6 +639,7 @@ export interface CodeActionDto { edit?: WorkspaceEditDto; diagnostics?: IMarkerData[]; command?: modes.Command; + scope?: string; } export interface ExtHostLanguageFeaturesShape { @@ -651,7 +652,7 @@ export interface ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext): TPromise; - $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise; + $provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions): TPromise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions): TPromise; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions): TPromise; diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index bf16fdb2c52..9790068a020 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -419,6 +419,9 @@ export class ExtHostApiCommands { codeAction.title, typeConverters.WorkspaceEdit.to(codeAction.edit) ); + if (codeAction.kind) { + ret.scope = new types.CodeActionKind(codeAction.kind); + } return ret; } }); diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 37ac8015683..9ebfc217656 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, Color } from 'vs/workbench/api/node/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, Color, CodeActionKind } from 'vs/workbench/api/node/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; @@ -273,7 +273,7 @@ class CodeActionAdapter { this._provider = provider; } - provideCodeActions(resource: URI, range: IRange): TPromise { + provideCodeActions(resource: URI, range: IRange, context: modes.CodeActionContext): TPromise { const doc = this._documents.getDocumentData(resource).document; const ran = TypeConverters.toRange(range); @@ -289,8 +289,12 @@ class CodeActionAdapter { } }); + const codeActionContext: vscode.CodeActionContext = { + diagnostics: allDiagnostics, + only: context.only ? new CodeActionKind(context.only) : undefined + }; return asWinJsPromise(token => - this._provider.provideCodeActions(doc, ran, { diagnostics: allDiagnostics }, token) + this._provider.provideCodeActions(doc, ran, codeActionContext, token) ).then(commandsOrActions => { if (isFalsyOrEmpty(commandsOrActions)) { return undefined; @@ -314,6 +318,7 @@ class CodeActionAdapter { command: candidate.command && this._commands.toInternal(candidate.command), diagnostics: candidate.diagnostics && candidate.diagnostics.map(DiagnosticCollection.toMarkerData), edit: candidate.edit && TypeConverters.WorkspaceEdit.from(candidate.edit), + kind: candidate.kind && candidate.kind.value }); } } @@ -943,8 +948,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise { - return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range)); + $provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise { + return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range, context)); } // --- formatting diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 76c30af3a0f..f09585c5473 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -12,6 +12,7 @@ import * as vscode from 'vscode'; import { isMarkdownString } from 'vs/base/common/htmlContent'; import { IRelativePattern } from 'vs/base/common/glob'; import { relative } from 'path'; +import { startsWith } from 'vs/base/common/strings'; export class Disposable { @@ -818,12 +819,39 @@ export class CodeAction { dianostics?: Diagnostic[]; + scope?: CodeActionKind; + constructor(title: string, edit?: WorkspaceEdit) { this.title = title; this.edit = edit; } } + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = new CodeActionKind('quickfix'); + public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); + public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); + public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + + constructor( + public readonly value: string + ) { } + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public contains(other: CodeActionKind): boolean { + return this.value === other.value || startsWith(other.value, this.value + CodeActionKind.sep); + } +} + + export class CodeLens { range: Range; diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 256ebb6879f..e86703048ec 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -641,7 +641,7 @@ suite('ExtHostLanguageFeatures', function () { // --- quick fix - test('Quick Fix, data conversion', function () { + test('Quick Fix, command data conversion', function () { disposables.push(extHost.registerCodeActionProvider(defaultSelector, { provideCodeActions(): vscode.Command[] { @@ -665,6 +665,34 @@ suite('ExtHostLanguageFeatures', function () { }); }); + test('Quick Fix, code action data conversion', function () { + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): vscode.CodeAction[] { + return [ + { + title: 'Testing1', + command: { title: 'Testing1Command', command: 'test1' }, + kind: types.CodeActionKind.Empty.append('test.scope') + } + ]; + } + })); + + return rpcProtocol.sync().then(() => { + return getCodeActions(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 1); + + const [first] = value; + assert.equal(first.title, 'Testing1'); + assert.equal(first.command.title, 'Testing1Command'); + assert.equal(first.command.id, 'test1'); + assert.equal(first.kind, 'test.scope'); + }); + }); + }); + + test('Cannot read property \'id\' of undefined, #29469', function () { disposables.push(extHost.registerCodeActionProvider(defaultSelector, {