diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 3ce0e33921c43..328261b508758 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1273,6 +1273,26 @@ "command": "typescript.goToSourceDefinition", "title": "%typescript.goToSourceDefinition%", "category": "TypeScript" + }, + { + "command": "typescript.sortImports", + "title": "%typescript.sortImports%", + "category": "TypeScript" + }, + { + "command": "javascript.sortImports", + "title": "%typescript.sortImports%", + "category": "JavaScript" + }, + { + "command": "typescript.removeUnusedImports", + "title": "%typescript.removeUnusedImports%", + "category": "TypeScript" + }, + { + "command": "javascript.removeUnusedImports", + "title": "%typescript.removeUnusedImports%", + "category": "JavaScript" } ], "menus": { @@ -1328,6 +1348,22 @@ { "command": "typescript.goToSourceDefinition", "when": "tsSupportsSourceDefinition && typescript.isManagedFile" + }, + { + "command": "typescript.sortImports", + "when": "tsSupportsSortImports && editorLangId =~ /^typescript(react)?$/" + }, + { + "command": "javascript.sortImports", + "when": "tsSupportsSortImports && editorLangId =~ /^javascript(react)?$/" + }, + { + "command": "typescript.removeUnusedImports", + "when": "tsSupportsRemoveUnusedImports && editorLangId =~ /^typescript(react)?$/" + }, + { + "command": "javascript.removeUnusedImports", + "when": "tsSupportsRemoveUnusedImports && editorLangId =~ /^javascript(react)?$/" } ], "editor/context": [ diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 89e7a9216ac8b..80c6b23964a80 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -187,7 +187,9 @@ "codeActions.refactor.rewrite.parameters.toDestructured.title": "Convert parameters to destructured object", "codeActions.refactor.rewrite.property.generateAccessors.title": "Generate accessors", "codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors", - "codeActions.source.organizeImports.title": "Organize imports", + "codeActions.source.organizeImports.title": "Organize Imports", + "typescript.sortImports": "Sort Imports", + "typescript.removeUnusedImports": "Remove Unused Imports", "typescript.findAllFileReferences": "Find File References", "typescript.goToSourceDefinition": "Go to Source Definition", "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace", diff --git a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index cd1aeffa823ee..4abcabf9de217 100644 --- a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; +import { OrganizeImportsMode } from '../protocol.const'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; @@ -19,17 +20,16 @@ import FileConfigurationManager from './fileConfigurationManager'; const localize = nls.loadMessageBundle(); -class OrganizeImportsCommand implements Command { - public static readonly Id = '_typescript.organizeImports'; - - public readonly id = OrganizeImportsCommand.Id; +abstract class BaseOrganizeImportsCommand implements Command { + protected abstract readonly mode: OrganizeImportsMode; constructor( + public id: string, private readonly client: ITypeScriptServiceClient, private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute(file: string, sortOnly = false): Promise { + public async execute(file?: string): Promise { /* __GDPR__ "organizeImports.execute" : { "owner": "mjbvz", @@ -39,6 +39,23 @@ class OrganizeImportsCommand implements Command { } */ this.telemetryReporter.logTelemetry('organizeImports.execute', {}); + if (!file) { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + vscode.window.showErrorMessage(localize('error.organizeImports.noResource', "Organize Imports failed. No resource provided.")); + return; + } + + const resource = activeEditor.document.uri; + const document = await vscode.workspace.openTextDocument(resource); + const openedFiledPath = this.client.toOpenedFilePath(document); + if (!openedFiledPath) { + vscode.window.showErrorMessage(localize('error.organizeImports.unknownFile', "Organize Imports failed. Unknown file type.")); + return; + } + + file = openedFiledPath; + } const args: Proto.OrganizeImportsRequestArgs = { scope: { @@ -47,7 +64,9 @@ class OrganizeImportsCommand implements Command { file } }, - skipDestructiveCodeActions: sortOnly, + // Deprecated in 4.9; `mode` takes priority + skipDestructiveCodeActions: this.mode === OrganizeImportsMode.SortAndCombine, + mode: typeConverters.OrganizeImportsMode.toProtocolOrganizeImportsMode(this.mode), }; const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken)); if (response.type !== 'response' || !response.body) { @@ -61,24 +80,53 @@ class OrganizeImportsCommand implements Command { } } +class OrganizeImportsCommand extends BaseOrganizeImportsCommand { + public static readonly id = 'organizeImports'; + public static minVersion = API.v280; + public static title = localize('organizeImportsAction.title', "Organize Imports"); + public readonly mode = OrganizeImportsMode.All; +} + +class SortImportsCommand extends BaseOrganizeImportsCommand { + public static readonly id = 'sortImports'; + public static minVersion = API.v430; + public static title = localize('sortImportsAction.title', "Sort Imports"); + public readonly mode = OrganizeImportsMode.SortAndCombine; + public static context = 'tsSupportsSortImports'; +} + +class RemoveUnusedImportsCommand extends BaseOrganizeImportsCommand { + public static readonly id = 'removeUnusedImports'; + public static minVersion = API.v490; + public static title = localize('removeUnusedImportsAction.title', "Remove Unused Imports"); + public readonly mode = OrganizeImportsMode.RemoveUnused; + public static context = 'tsSupportsRemoveUnusedImports'; +} + +interface OrganizeImportsCommandClass { + readonly id: string; + readonly title: string; + readonly context?: string; + readonly minVersion: API; + new(id: string, client: ITypeScriptServiceClient, telemetryReporter: TelemetryReporter): BaseOrganizeImportsCommand; +} + class ImportsCodeActionProvider implements vscode.CodeActionProvider { static register( client: ITypeScriptServiceClient, - minVersion: API, kind: vscode.CodeActionKind, - title: string, - sortOnly: boolean, + Command: OrganizeImportsCommandClass, commandManager: CommandManager, fileConfigurationManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, selector: DocumentSelector ): vscode.Disposable { return conditionalRegistration([ - requireMinVersion(client, minVersion), + requireMinVersion(client, Command.minVersion), requireSomeCapability(client, ClientCapability.Semantic), ], () => { - const provider = new ImportsCodeActionProvider(client, kind, title, sortOnly, commandManager, fileConfigurationManager, telemetryReporter); + const provider = new ImportsCodeActionProvider(client, kind, Command, commandManager, fileConfigurationManager, telemetryReporter); return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, { providedCodeActionKinds: [kind] }); @@ -88,13 +136,25 @@ class ImportsCodeActionProvider implements vscode.CodeActionProvider { public constructor( private readonly client: ITypeScriptServiceClient, private readonly kind: vscode.CodeActionKind, - private readonly title: string, - private readonly sortOnly: boolean, + private readonly Command: OrganizeImportsCommandClass, commandManager: CommandManager, private readonly fileConfigManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, ) { - commandManager.register(new OrganizeImportsCommand(client, telemetryReporter)); + commandManager.register(new Command(`typescript.${Command.id}`, client, telemetryReporter)); + if (Command !== OrganizeImportsCommand) { + // The non-built-in variants have get duplicated with javascript-specific ids + // can show "JavasScript" as the category + commandManager.register(new Command(`javascript.${Command.id}`, client, telemetryReporter)); + } + + if (Command.context) { + updateContext(); + client.onTsServerStarted(() => updateContext()); + function updateContext() { + vscode.commands.executeCommand('setContext', Command.context, client.apiVersion.gte(Command.minVersion)); + } + } } public provideCodeActions( @@ -114,8 +174,8 @@ class ImportsCodeActionProvider implements vscode.CodeActionProvider { this.fileConfigManager.ensureConfigurationForDocument(document, token); - const action = new vscode.CodeAction(this.title, this.kind); - action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file, this.sortOnly] }; + const action = new vscode.CodeAction(this.Command.title, this.kind); + action.command = { title: '', command: this.Command.id, arguments: [file] }; return [action]; } } @@ -130,10 +190,17 @@ export function register( return vscode.Disposable.from( ImportsCodeActionProvider.register( client, - API.v280, vscode.CodeActionKind.SourceOrganizeImports, - localize('organizeImportsAction.title', "Organize Imports"), - false, + OrganizeImportsCommand, + commandManager, + fileConfigurationManager, + telemetryReporter, + selector + ), + ImportsCodeActionProvider.register( + client, + vscode.CodeActionKind.Source.append(SortImportsCommand.id), + SortImportsCommand, commandManager, fileConfigurationManager, telemetryReporter, @@ -141,10 +208,8 @@ export function register( ), ImportsCodeActionProvider.register( client, - API.v430, - vscode.CodeActionKind.Source.append('sortImports'), - localize('sortImportsAction.title', "Sort Imports"), - true, + vscode.CodeActionKind.Source.append(RemoveUnusedImportsCommand.id), + RemoveUnusedImportsCommand, commandManager, fileConfigurationManager, telemetryReporter, diff --git a/extensions/typescript-language-features/src/protocol.const.ts b/extensions/typescript-language-features/src/protocol.const.ts index 9571d1c0e63c2..deb3357e6ae30 100644 --- a/extensions/typescript-language-features/src/protocol.const.ts +++ b/extensions/typescript-language-features/src/protocol.const.ts @@ -89,3 +89,9 @@ export enum EventName { projectLoadingStart = 'projectLoadingStart', projectLoadingFinish = 'projectLoadingFinish', } + +export enum OrganizeImportsMode { + All = 'All', + SortAndCombine = 'SortAndCombine', + RemoveUnused = 'RemoveUnused', +} diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index e9e85091fa44e..4af828ed45af3 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -40,6 +40,8 @@ export default class API { public static readonly v440 = API.fromSimpleString('4.4.0'); public static readonly v460 = API.fromSimpleString('4.6.0'); public static readonly v470 = API.fromSimpleString('4.7.0'); + public static readonly v480 = API.fromSimpleString('4.8.0'); + public static readonly v490 = API.fromSimpleString('4.9.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/utils/typeConverters.ts b/extensions/typescript-language-features/src/utils/typeConverters.ts index 1e81c133262d5..2574fae406475 100644 --- a/extensions/typescript-language-features/src/utils/typeConverters.ts +++ b/extensions/typescript-language-features/src/utils/typeConverters.ts @@ -136,3 +136,13 @@ export namespace CompletionTriggerKind { } } } + +export namespace OrganizeImportsMode { + export function toProtocolOrganizeImportsMode(mode: PConst.OrganizeImportsMode): Proto.OrganizeImportsMode { + switch (mode) { + case PConst.OrganizeImportsMode.All: return 'All' as Proto.OrganizeImportsMode.All; + case PConst.OrganizeImportsMode.SortAndCombine: return 'SortAndCombine' as Proto.OrganizeImportsMode.SortAndCombine; + case PConst.OrganizeImportsMode.RemoveUnused: return 'RemoveUnused' as Proto.OrganizeImportsMode.RemoveUnused; + } + } +}