[typescript-language-features] Add removeUnusedImports command (#161654)

* Add `removeUnusedImports` command

* Continue to send `skipDestructiveCodeActions` for older TS versions

* Expose Sort Imports and Remove Unused Imports commands

* Update localization keys

* Update for 4.9 protocol

* Proto must be type only import?
This commit is contained in:
Andrew Branch
2022-10-18 09:00:16 -07:00
committed by GitHub
parent 4d183bd274
commit 1fb956d2f5
6 changed files with 145 additions and 24 deletions

View File

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

View File

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

View File

@@ -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<any> {
public async execute(file?: string): Promise<any> {
/* __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,8 @@ 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,
@@ -141,10 +199,17 @@ export function register(
),
ImportsCodeActionProvider.register(
client,
API.v430,
vscode.CodeActionKind.Source.append('sortImports'),
localize('sortImportsAction.title', "Sort Imports"),
true,
vscode.CodeActionKind.Source.append(SortImportsCommand.id),
SortImportsCommand,
commandManager,
fileConfigurationManager,
telemetryReporter,
selector
),
ImportsCodeActionProvider.register(
client,
vscode.CodeActionKind.Source.append(RemoveUnusedImportsCommand.id),
RemoveUnusedImportsCommand,
commandManager,
fileConfigurationManager,
telemetryReporter,

View File

@@ -89,3 +89,9 @@ export enum EventName {
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
}
export enum OrganizeImportsMode {
All = 'All',
SortAndCombine = 'SortAndCombine',
RemoveUnused = 'RemoveUnused',
}

View File

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

View File

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