diff --git a/extensions/typescript-language-features/src/features/organizeImports.ts b/extensions/typescript-language-features/src/features/organizeImports.ts index a92aabc9f9f..8dd28828c34 100644 --- a/extensions/typescript-language-features/src/features/organizeImports.ts +++ b/extensions/typescript-language-features/src/features/organizeImports.ts @@ -13,6 +13,10 @@ import { isSupportedLanguageMode } from '../utils/languageModeIds'; import API from '../utils/api'; import { Lazy } from '../utils/lazy'; import TypeScriptServiceClientHost from '../typeScriptServiceClientHost'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + export class OrganizeImportsCommand implements Command { public static readonly Ids = ['javascript.organizeImports', 'typescript.organizeImports']; @@ -84,3 +88,30 @@ export class OrganizeImportsContextManager { this.currentValue = newValue; } } + + +export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvider { + + public constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range, + _context: vscode.CodeActionContext, + _token: vscode.CancellationToken + ): vscode.CodeAction[] { + if (!isSupportedLanguageMode(document)) { + return []; + } + + if (!this.client.apiVersion.has280Features()) { + return []; + } + + const action = new vscode.CodeAction(localize('oraganizeImportsAction.title', "Organize Imports"), vscode.CodeActionKind.Source.append('organizeImports')); + action.command = { title: '', command: OrganizeImportsCommand.Ids[0] }; + return [action]; + } +} \ No newline at end of file diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 9da522bc284..8198daf9e74 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -120,6 +120,7 @@ export default class LanguageProvider { this.disposables.push(languages.registerRenameProvider(selector, new (await import('./features/renameProvider')).default(client))); this.disposables.push(languages.registerCodeActionsProvider(selector, new (await import('./features/quickFixProvider')).default(client, this.formattingOptionsManager, commandManager, this.diagnosticsManager, this.bufferSyncSupport))); this.disposables.push(languages.registerCodeActionsProvider(selector, new (await import('./features/refactorProvider')).default(client, this.formattingOptionsManager, commandManager))); + this.disposables.push(languages.registerCodeActionsProvider(selector, new (await import('./features/organizeImports')).OrganizeImportsCodeActionProvider(client))); await this.initFoldingProvider(); this.disposables.push(workspace.onDidChangeConfiguration(c => { diff --git a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts index a210e484446..382c8ebda68 100644 --- a/src/vs/editor/contrib/quickFix/codeActionTrigger.ts +++ b/src/vs/editor/contrib/quickFix/codeActionTrigger.ts @@ -10,6 +10,7 @@ export class CodeActionKind { public static readonly Empty = new CodeActionKind(''); public static readonly Refactor = new CodeActionKind('refactor'); + public static readonly Source = new CodeActionKind('source'); constructor( public readonly value: string @@ -26,8 +27,13 @@ export enum CodeActionAutoApply { Never = 3 } +export interface CodeActionFilter { + readonly kind?: CodeActionKind; + readonly includeSourceActions?: boolean; +} + export interface CodeActionTrigger { - type: 'auto' | 'manual'; - kind?: CodeActionKind; - autoApply?: CodeActionAutoApply; + readonly type: 'auto' | 'manual'; + readonly filter?: CodeActionFilter; + readonly 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 8e273057dc1..bcb1456911f 100644 --- a/src/vs/editor/contrib/quickFix/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/quickFix.ts @@ -14,19 +14,24 @@ 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, mergeSort } from 'vs/base/common/arrays'; -import { CodeActionKind } from './codeActionTrigger'; +import { CodeActionKind, CodeActionFilter } from './codeActionTrigger'; -export function getCodeActions(model: ITextModel, range: Range, scope?: CodeActionKind): TPromise { +export function getCodeActions(model: ITextModel, range: Range, filter?: CodeActionFilter): TPromise { const allResults: CodeAction[] = []; const promises = CodeActionProviderRegistry.all(model).map(support => { - return asWinJsPromise(token => support.provideCodeActions(model, range, { only: scope ? scope.value : undefined }, token)).then(result => { + return asWinJsPromise(token => support.provideCodeActions(model, range, { only: filter && filter.kind ? filter.kind.value : undefined }, token)).then(result => { if (Array.isArray(result)) { for (const quickFix of result) { - if (quickFix) { - if (!scope || (quickFix.kind && scope.contains(quickFix.kind))) { - allResults.push(quickFix); + if (!quickFix) { + continue; + } + + if (!filter || !filter.kind || (quickFix.kind && filter.kind.contains(quickFix.kind))) { + if (quickFix.kind && CodeActionKind.Source.contains(quickFix.kind) && (!filter || !filter.includeSourceActions)) { + continue; } + allResults.push(quickFix); } } } diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/quickFixCommands.ts index 9d1a7db3a8a..18a5e733af9 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/quickFixCommands.ts @@ -4,29 +4,29 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as nls from 'vs/nls'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -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 { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; -import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { MessageController } from 'vs/editor/contrib/message/messageController'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IFileService } from 'vs/platform/files/common/files'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeActionTrigger'; +import { LightBulbWidget } from './lightBulbWidget'; +import { QuickFixComputeEvent, QuickFixModel } from './quickFixModel'; +import { QuickFixContextMenu } from './quickFixWidget'; export class QuickFixController implements IEditorContribution { @@ -59,7 +59,7 @@ export class QuickFixController implements IEditorContribution { this._updateLightBulbTitle(); this._disposables.push( - this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto' })), + this._quickFixContextMenu.onDidExecuteCodeAction(_ => this._model.trigger({ type: 'auto', filter: {} })), this._lightBulbWidget.onClick(this._handleLightBulbSelect, this), this._model.onDidChangeFixes(e => this._onQuickFixEvent(e)), this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitle, this) @@ -72,7 +72,7 @@ export class QuickFixController implements IEditorContribution { } private _onQuickFixEvent(e: QuickFixComputeEvent): void { - if (e && e.trigger.kind) { + if (e && e.trigger.filter && e.trigger.filter.kind) { // Triggered for specific scope // Apply if we only have one action or requested autoApply, otherwise show menu e.fixes.then(fixes => { @@ -109,8 +109,8 @@ export class QuickFixController implements IEditorContribution { this._quickFixContextMenu.show(this._lightBulbWidget.model.fixes, coords); } - public triggerFromEditorSelection(kind?: CodeActionKind, autoApply?: CodeActionAutoApply): TPromise { - return this._model.trigger({ type: 'manual', kind, autoApply }); + public triggerFromEditorSelection(filter?: CodeActionFilter, autoApply?: CodeActionAutoApply): TPromise { + return this._model.trigger({ type: 'manual', filter, autoApply }); } private _updateLightBulbTitle(): void { @@ -138,7 +138,7 @@ export class QuickFixController implements IEditorContribution { function showCodeActionsForEditorSelection( editor: ICodeEditor, notAvailableMessage: string, - kind?: CodeActionKind, + filter?: CodeActionFilter, autoApply?: CodeActionAutoApply ) { const controller = QuickFixController.get(editor); @@ -147,7 +147,7 @@ function showCodeActionsForEditorSelection( } const pos = editor.getPosition(); - controller.triggerFromEditorSelection(kind, autoApply).then(codeActions => { + controller.triggerFromEditorSelection(filter, autoApply).then(codeActions => { if (!codeActions || !codeActions.length) { MessageController.get(editor).showMessage(notAvailableMessage, pos); } @@ -226,7 +226,7 @@ export class CodeActionCommand extends EditorCommand { public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, userArg: any) { const args = CodeActionCommandArgs.fromUser(userArg); - return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), args.kind, args.apply); + return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), { kind: args.kind, includeSourceActions: true }, args.apply); } } @@ -255,13 +255,39 @@ export class RefactorAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { return showCodeActionsForEditorSelection(editor, nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), - CodeActionKind.Refactor, + { kind: CodeActionKind.Refactor }, CodeActionAutoApply.Never); } } +export class SourceAction extends EditorAction { + + static readonly Id = 'editor.action.sourceAction'; + + constructor() { + super({ + id: SourceAction.Id, + label: nls.localize('source.label', "Source Action"), + alias: 'Source Action', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), + menuOpts: { + group: '1_modification', + order: 2.1 + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + return showCodeActionsForEditorSelection(editor, + nls.localize('editor.action.source.noneMessage', "No source actions available"), + { kind: CodeActionKind.Source, includeSourceActions: true }, + CodeActionAutoApply.Never); + } +} + registerEditorContribution(QuickFixController); registerEditorAction(QuickFixAction); registerEditorAction(RefactorAction); +registerEditorAction(SourceAction); registerEditorCommand(new CodeActionCommand()); diff --git a/src/vs/editor/contrib/quickFix/quickFixModel.ts b/src/vs/editor/contrib/quickFix/quickFixModel.ts index 0d08f4d4a8e..5b6dc111745 100644 --- a/src/vs/editor/contrib/quickFix/quickFixModel.ts +++ b/src/vs/editor/contrib/quickFix/quickFixModel.ts @@ -114,7 +114,7 @@ export class QuickFixOracle { const model = this._editor.getModel(); const range = model.validateRange(rangeOrSelection); const position = rangeOrSelection instanceof Selection ? rangeOrSelection.getPosition() : rangeOrSelection.getStartPosition(); - const fixes = getCodeActions(model, range, trigger && trigger.kind); + const fixes = getCodeActions(model, range, trigger && trigger.filter); this._signalChange({ trigger, diff --git a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts index 9693fce61de..61749720aa8 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts @@ -136,20 +136,20 @@ suite('QuickFix', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: 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')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: 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')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b.c') }); assert.equal(actions.length, 0); } }); @@ -165,8 +165,33 @@ suite('QuickFix', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), new CodeActionKind('a')); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); }); + + test('getCodeActions should not return source code action by default', async function () { + const provider = new class implements CodeActionProvider { + provideCodeActions(): CodeAction[] { + return [ + { title: 'a', kind: CodeActionKind.Source.value }, + { title: 'b', kind: 'b' } + ]; + } + }; + + disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), {}); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'b'); + } + + { + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: CodeActionKind.Source, includeSourceActions: true }); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'a'); + } + }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 8ab2ed8baec..4d1af2090d5 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1931,6 +1931,13 @@ declare module 'vscode' { */ static readonly RefactorRewrite: CodeActionKind; + /** + * Base kind for source actions. + * + * Source code actions apply to the entire file. + */ + static readonly Source: CodeActionKind; + private constructor(value: string); /** diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 1fe1b5bd13b..1423c277882 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -903,6 +903,7 @@ export class CodeActionKind { public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + public static readonly Source = CodeActionKind.Empty.append('source'); constructor( public readonly value: string