Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Oct 18, 2022
36 changes: 36 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
"title": "%typescript.removeUnusedImports%",
"category": "JavaScript"
}
],
"menus": {
Expand Down Expand Up @@ -1328,6 +1348,22 @@
{
"command": "typescript.goToSourceDefinition",
"when": "tsSupportsSourceDefinition && typescript.isManagedFile"
},
{
"command": "typescript.sortImports",
"when": "tsSupportsSortImports && editorLangId =~ /^typescript(react)?$/"
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
},
{
"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": [
Expand Down
4 changes: 3 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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",
Expand All @@ -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: {
Expand All @@ -47,7 +64,9 @@ class OrganizeImportsCommand implements Command {
file
}
},
skipDestructiveCodeActions: sortOnly,
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
// 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) {
Expand All @@ -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");
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
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]
});
Expand All @@ -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));
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand All @@ -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);
andrewbranch marked this conversation as resolved.
Show resolved Hide resolved
action.command = { title: '', command: this.Command.id, arguments: [file] };
return [action];
}
}
Expand All @@ -130,21 +190,26 @@ 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,
selector
),
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,
Expand Down
6 changes: 6 additions & 0 deletions extensions/typescript-language-features/src/protocol.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@ export enum EventName {
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
}

export enum OrganizeImportsMode {
All = 'All',
SortAndCombine = 'SortAndCombine',
RemoveUnused = 'RemoveUnused',
}
2 changes: 2 additions & 0 deletions extensions/typescript-language-features/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}