Skip to content

Commit 1fb956d

Browse files
authored
[typescript-language-features] Add removeUnusedImports command (microsoft#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?
1 parent 4d183bd commit 1fb956d

File tree

6 files changed

+145
-24
lines changed

6 files changed

+145
-24
lines changed

extensions/typescript-language-features/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,26 @@
12731273
"command": "typescript.goToSourceDefinition",
12741274
"title": "%typescript.goToSourceDefinition%",
12751275
"category": "TypeScript"
1276+
},
1277+
{
1278+
"command": "typescript.sortImports",
1279+
"title": "%typescript.sortImports%",
1280+
"category": "TypeScript"
1281+
},
1282+
{
1283+
"command": "javascript.sortImports",
1284+
"title": "%typescript.sortImports%",
1285+
"category": "JavaScript"
1286+
},
1287+
{
1288+
"command": "typescript.removeUnusedImports",
1289+
"title": "%typescript.removeUnusedImports%",
1290+
"category": "TypeScript"
1291+
},
1292+
{
1293+
"command": "javascript.removeUnusedImports",
1294+
"title": "%typescript.removeUnusedImports%",
1295+
"category": "JavaScript"
12761296
}
12771297
],
12781298
"menus": {
@@ -1328,6 +1348,22 @@
13281348
{
13291349
"command": "typescript.goToSourceDefinition",
13301350
"when": "tsSupportsSourceDefinition && typescript.isManagedFile"
1351+
},
1352+
{
1353+
"command": "typescript.sortImports",
1354+
"when": "tsSupportsSortImports && editorLangId =~ /^typescript(react)?$/"
1355+
},
1356+
{
1357+
"command": "javascript.sortImports",
1358+
"when": "tsSupportsSortImports && editorLangId =~ /^javascript(react)?$/"
1359+
},
1360+
{
1361+
"command": "typescript.removeUnusedImports",
1362+
"when": "tsSupportsRemoveUnusedImports && editorLangId =~ /^typescript(react)?$/"
1363+
},
1364+
{
1365+
"command": "javascript.removeUnusedImports",
1366+
"when": "tsSupportsRemoveUnusedImports && editorLangId =~ /^javascript(react)?$/"
13311367
}
13321368
],
13331369
"editor/context": [

extensions/typescript-language-features/package.nls.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@
187187
"codeActions.refactor.rewrite.parameters.toDestructured.title": "Convert parameters to destructured object",
188188
"codeActions.refactor.rewrite.property.generateAccessors.title": "Generate accessors",
189189
"codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors",
190-
"codeActions.source.organizeImports.title": "Organize imports",
190+
"codeActions.source.organizeImports.title": "Organize Imports",
191+
"typescript.sortImports": "Sort Imports",
192+
"typescript.removeUnusedImports": "Remove Unused Imports",
191193
"typescript.findAllFileReferences": "Find File References",
192194
"typescript.goToSourceDefinition": "Go to Source Definition",
193195
"configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace",

extensions/typescript-language-features/src/languageFeatures/organizeImports.ts

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import * as nls from 'vscode-nls';
88
import { Command, CommandManager } from '../commands/commandManager';
99
import type * as Proto from '../protocol';
10+
import { OrganizeImportsMode } from '../protocol.const';
1011
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
1112
import API from '../utils/api';
1213
import { nulToken } from '../utils/cancellation';
@@ -19,17 +20,16 @@ import FileConfigurationManager from './fileConfigurationManager';
1920
const localize = nls.loadMessageBundle();
2021

2122

22-
class OrganizeImportsCommand implements Command {
23-
public static readonly Id = '_typescript.organizeImports';
24-
25-
public readonly id = OrganizeImportsCommand.Id;
23+
abstract class BaseOrganizeImportsCommand implements Command {
24+
protected abstract readonly mode: OrganizeImportsMode;
2625

2726
constructor(
27+
public id: string,
2828
private readonly client: ITypeScriptServiceClient,
2929
private readonly telemetryReporter: TelemetryReporter,
3030
) { }
3131

32-
public async execute(file: string, sortOnly = false): Promise<any> {
32+
public async execute(file?: string): Promise<any> {
3333
/* __GDPR__
3434
"organizeImports.execute" : {
3535
"owner": "mjbvz",
@@ -39,6 +39,23 @@ class OrganizeImportsCommand implements Command {
3939
}
4040
*/
4141
this.telemetryReporter.logTelemetry('organizeImports.execute', {});
42+
if (!file) {
43+
const activeEditor = vscode.window.activeTextEditor;
44+
if (!activeEditor) {
45+
vscode.window.showErrorMessage(localize('error.organizeImports.noResource', "Organize Imports failed. No resource provided."));
46+
return;
47+
}
48+
49+
const resource = activeEditor.document.uri;
50+
const document = await vscode.workspace.openTextDocument(resource);
51+
const openedFiledPath = this.client.toOpenedFilePath(document);
52+
if (!openedFiledPath) {
53+
vscode.window.showErrorMessage(localize('error.organizeImports.unknownFile', "Organize Imports failed. Unknown file type."));
54+
return;
55+
}
56+
57+
file = openedFiledPath;
58+
}
4259

4360
const args: Proto.OrganizeImportsRequestArgs = {
4461
scope: {
@@ -47,7 +64,9 @@ class OrganizeImportsCommand implements Command {
4764
file
4865
}
4966
},
50-
skipDestructiveCodeActions: sortOnly,
67+
// Deprecated in 4.9; `mode` takes priority
68+
skipDestructiveCodeActions: this.mode === OrganizeImportsMode.SortAndCombine,
69+
mode: typeConverters.OrganizeImportsMode.toProtocolOrganizeImportsMode(this.mode),
5170
};
5271
const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken));
5372
if (response.type !== 'response' || !response.body) {
@@ -61,24 +80,53 @@ class OrganizeImportsCommand implements Command {
6180
}
6281
}
6382

83+
class OrganizeImportsCommand extends BaseOrganizeImportsCommand {
84+
public static readonly id = 'organizeImports';
85+
public static minVersion = API.v280;
86+
public static title = localize('organizeImportsAction.title', "Organize Imports");
87+
public readonly mode = OrganizeImportsMode.All;
88+
}
89+
90+
class SortImportsCommand extends BaseOrganizeImportsCommand {
91+
public static readonly id = 'sortImports';
92+
public static minVersion = API.v430;
93+
public static title = localize('sortImportsAction.title', "Sort Imports");
94+
public readonly mode = OrganizeImportsMode.SortAndCombine;
95+
public static context = 'tsSupportsSortImports';
96+
}
97+
98+
class RemoveUnusedImportsCommand extends BaseOrganizeImportsCommand {
99+
public static readonly id = 'removeUnusedImports';
100+
public static minVersion = API.v490;
101+
public static title = localize('removeUnusedImportsAction.title', "Remove Unused Imports");
102+
public readonly mode = OrganizeImportsMode.RemoveUnused;
103+
public static context = 'tsSupportsRemoveUnusedImports';
104+
}
105+
106+
interface OrganizeImportsCommandClass {
107+
readonly id: string;
108+
readonly title: string;
109+
readonly context?: string;
110+
readonly minVersion: API;
111+
new(id: string, client: ITypeScriptServiceClient, telemetryReporter: TelemetryReporter): BaseOrganizeImportsCommand;
112+
}
113+
64114
class ImportsCodeActionProvider implements vscode.CodeActionProvider {
65115

66116
static register(
67117
client: ITypeScriptServiceClient,
68-
minVersion: API,
69118
kind: vscode.CodeActionKind,
70-
title: string,
71-
sortOnly: boolean,
119+
Command: OrganizeImportsCommandClass,
72120
commandManager: CommandManager,
73121
fileConfigurationManager: FileConfigurationManager,
74122
telemetryReporter: TelemetryReporter,
75123
selector: DocumentSelector
76124
): vscode.Disposable {
77125
return conditionalRegistration([
78-
requireMinVersion(client, minVersion),
126+
requireMinVersion(client, Command.minVersion),
79127
requireSomeCapability(client, ClientCapability.Semantic),
80128
], () => {
81-
const provider = new ImportsCodeActionProvider(client, kind, title, sortOnly, commandManager, fileConfigurationManager, telemetryReporter);
129+
const provider = new ImportsCodeActionProvider(client, kind, Command, commandManager, fileConfigurationManager, telemetryReporter);
82130
return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, {
83131
providedCodeActionKinds: [kind]
84132
});
@@ -88,13 +136,25 @@ class ImportsCodeActionProvider implements vscode.CodeActionProvider {
88136
public constructor(
89137
private readonly client: ITypeScriptServiceClient,
90138
private readonly kind: vscode.CodeActionKind,
91-
private readonly title: string,
92-
private readonly sortOnly: boolean,
139+
private readonly Command: OrganizeImportsCommandClass,
93140
commandManager: CommandManager,
94141
private readonly fileConfigManager: FileConfigurationManager,
95142
telemetryReporter: TelemetryReporter,
96143
) {
97-
commandManager.register(new OrganizeImportsCommand(client, telemetryReporter));
144+
commandManager.register(new Command(`typescript.${Command.id}`, client, telemetryReporter));
145+
if (Command !== OrganizeImportsCommand) {
146+
// The non-built-in variants have get duplicated with javascript-specific ids
147+
// can show "JavasScript" as the category
148+
commandManager.register(new Command(`javascript.${Command.id}`, client, telemetryReporter));
149+
}
150+
151+
if (Command.context) {
152+
updateContext();
153+
client.onTsServerStarted(() => updateContext());
154+
function updateContext() {
155+
vscode.commands.executeCommand('setContext', Command.context, client.apiVersion.gte(Command.minVersion));
156+
}
157+
}
98158
}
99159

100160
public provideCodeActions(
@@ -114,8 +174,8 @@ class ImportsCodeActionProvider implements vscode.CodeActionProvider {
114174

115175
this.fileConfigManager.ensureConfigurationForDocument(document, token);
116176

117-
const action = new vscode.CodeAction(this.title, this.kind);
118-
action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file, this.sortOnly] };
177+
const action = new vscode.CodeAction(this.Command.title, this.kind);
178+
action.command = { title: '', command: this.Command.id, arguments: [file] };
119179
return [action];
120180
}
121181
}
@@ -130,21 +190,26 @@ export function register(
130190
return vscode.Disposable.from(
131191
ImportsCodeActionProvider.register(
132192
client,
133-
API.v280,
134193
vscode.CodeActionKind.SourceOrganizeImports,
135-
localize('organizeImportsAction.title', "Organize Imports"),
136-
false,
194+
OrganizeImportsCommand,
195+
commandManager,
196+
fileConfigurationManager,
197+
telemetryReporter,
198+
selector
199+
),
200+
ImportsCodeActionProvider.register(
201+
client,
202+
vscode.CodeActionKind.Source.append(SortImportsCommand.id),
203+
SortImportsCommand,
137204
commandManager,
138205
fileConfigurationManager,
139206
telemetryReporter,
140207
selector
141208
),
142209
ImportsCodeActionProvider.register(
143210
client,
144-
API.v430,
145-
vscode.CodeActionKind.Source.append('sortImports'),
146-
localize('sortImportsAction.title', "Sort Imports"),
147-
true,
211+
vscode.CodeActionKind.Source.append(RemoveUnusedImportsCommand.id),
212+
RemoveUnusedImportsCommand,
148213
commandManager,
149214
fileConfigurationManager,
150215
telemetryReporter,

extensions/typescript-language-features/src/protocol.const.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,9 @@ export enum EventName {
8989
projectLoadingStart = 'projectLoadingStart',
9090
projectLoadingFinish = 'projectLoadingFinish',
9191
}
92+
93+
export enum OrganizeImportsMode {
94+
All = 'All',
95+
SortAndCombine = 'SortAndCombine',
96+
RemoveUnused = 'RemoveUnused',
97+
}

extensions/typescript-language-features/src/utils/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export default class API {
4040
public static readonly v440 = API.fromSimpleString('4.4.0');
4141
public static readonly v460 = API.fromSimpleString('4.6.0');
4242
public static readonly v470 = API.fromSimpleString('4.7.0');
43+
public static readonly v480 = API.fromSimpleString('4.8.0');
44+
public static readonly v490 = API.fromSimpleString('4.9.0');
4345

4446
public static fromVersionString(versionString: string): API {
4547
let version = semver.valid(versionString);

extensions/typescript-language-features/src/utils/typeConverters.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,13 @@ export namespace CompletionTriggerKind {
136136
}
137137
}
138138
}
139+
140+
export namespace OrganizeImportsMode {
141+
export function toProtocolOrganizeImportsMode(mode: PConst.OrganizeImportsMode): Proto.OrganizeImportsMode {
142+
switch (mode) {
143+
case PConst.OrganizeImportsMode.All: return 'All' as Proto.OrganizeImportsMode.All;
144+
case PConst.OrganizeImportsMode.SortAndCombine: return 'SortAndCombine' as Proto.OrganizeImportsMode.SortAndCombine;
145+
case PConst.OrganizeImportsMode.RemoveUnused: return 'RemoveUnused' as Proto.OrganizeImportsMode.RemoveUnused;
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)