Skip to content

Commit

Permalink
Organize imports for js and ts (#45237)
Browse files Browse the repository at this point in the history
* Organize imports for js and ts

Adds a new 'Organize Imports' command for js and ts. This command is only availible on TS 2.8+. We'll hold off on merging this PR until we pick up a TS 2.8 insiders build

Fixes #45108

* Add keybinding for organize imports
  • Loading branch information
mjbvz authored Mar 20, 2018
1 parent 3eff6ac commit 5be9e9e
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 23 deletions.
17 changes: 17 additions & 0 deletions extensions/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"onCommand:javascript.goToProjectConfig",
"onCommand:typescript.goToProjectConfig",
"onCommand:typescript.openTsServerLog",
"onCommand:typescript.organizeImports",
"onCommand:workbench.action.tasks.runTask"
],
"main": "./out/extension",
Expand Down Expand Up @@ -460,6 +461,18 @@
"value": "%typescript.restartTsServer%"
},
"category": "TypeScript"
},
{
"command": "typescript.organizeImports",
"title": "%typescript.organizeImports%",
"category": "TypeScript"
}
],
"keybindings": [
{
"command": "typescript.organizeImports",
"key": "shift+ctrl+o",
"when": "typescript.isManagedFile && typescript.canOrganizeImports"
}
],
"menus": {
Expand Down Expand Up @@ -507,6 +520,10 @@
{
"command": "typescript.restartTsServer",
"when": "typescript.isManagedFile"
},
{
"command": "typescript.organizeImports",
"when": "typescript.isManagedFile && typescript.canOrganizeImports"
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion extensions/typescript/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
"javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable 'experimentalDecorators' for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting. Requires TypeScript >=2.3.1.",
"typescript.autoImportSuggestions.enabled": "Enable/disable auto import suggestions. Requires TypeScript >=2.6.1",
"typescript.experimental.syntaxFolding": "Enables/disables syntax aware folding markers.",
"taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build."
"taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.",
"typescript.organizeImports": "Organize Imports"
}
10 changes: 10 additions & 0 deletions extensions/typescript/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ManagedFileContextManager from './utils/managedFileContext';
import { lazy, Lazy } from './utils/lazy';
import * as fileSchemes from './utils/fileSchemes';
import LogDirectoryProvider from './utils/logDirectoryProvider';
import { OrganizeImportsCommand, OrganizeImportsContextManager } from './features/organizeImports';

export function activate(
context: vscode.ExtensionContext
Expand Down Expand Up @@ -70,7 +71,14 @@ function createLazyClientHost(
plugins,
commandManager,
logDirectoryProvider);

context.subscriptions.push(clientHost);

const organizeImportsContext = new OrganizeImportsContextManager();
clientHost.serviceClient.onTsServerStarted(api => {
organizeImportsContext.onDidChangeApiVersion(api);
}, null, context.subscriptions);

clientHost.serviceClient.onReady(() => {
context.subscriptions.push(
ProjectStatus.create(
Expand All @@ -79,6 +87,7 @@ function createLazyClientHost(
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
context.workspaceState));
});

return clientHost;
});
}
Expand All @@ -94,6 +103,7 @@ function registerCommands(
commandManager.register(new commands.RestartTsServerCommand(lazyClientHost));
commandManager.register(new commands.TypeScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new commands.JavaScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new OrganizeImportsCommand(lazyClientHost));
}

function isSupportedDocument(
Expand Down
85 changes: 85 additions & 0 deletions extensions/typescript/src/features/organizeImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

import * as Proto from '../protocol';
import { Command } from '../utils/commandManager';
import * as typeconverts from '../utils/typeConverters';

import { isSupportedLanguageMode } from '../utils/languageModeIds';
import API from '../utils/api';
import { Lazy } from '../utils/lazy';
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';

export class OrganizeImportsCommand implements Command {
public static readonly ID = 'typescript.organizeImports';
public readonly id = OrganizeImportsCommand.ID;

constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }

public async execute(): Promise<boolean> {
// Don't force activation
if (!this.lazyClientHost.hasValue) {
return false;
}

const client = this.lazyClientHost.value.serviceClient;
if (!client.apiVersion.has280Features()) {
return false;
}

const editor = vscode.window.activeTextEditor;
if (!editor || !isSupportedLanguageMode(editor.document)) {
return false;
}

const file = client.normalizePath(editor.document.uri);
if (!file) {
return false;
}

const args: Proto.OrganizeImportsRequestArgs = {
scope: {
type: 'file',
args: {
file
}
}
};
const response = await client.execute('organizeImports', args);
if (!response || !response.success) {
return false;
}

const edits = typeconverts.WorkspaceEdit.fromFromFileCodeEdits(client, response.body);
return await vscode.workspace.applyEdit(edits);
}
}

/**
* When clause context set when the ts version supports organize imports.
*/
const contextName = 'typescript.canOrganizeImports';

export class OrganizeImportsContextManager {

private currentValue: boolean = false;

public onDidChangeApiVersion(apiVersion: API): any {
this.updateContext(apiVersion.has280Features());
}

private updateContext(newValue: boolean) {
if (newValue === this.currentValue) {
return;
}

vscode.commands.executeCommand('setContext', contextName, newValue);
this.currentValue = newValue;
}
}
14 changes: 1 addition & 13 deletions extensions/typescript/src/features/refactorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ApplyRefactoringCommand implements Command {
return false;
}

const edit = this.toWorkspaceEdit(response.body.edits);
const edit = typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, response.body.edits);
if (!(await vscode.workspace.applyEdit(edit))) {
return false;
}
Expand All @@ -56,18 +56,6 @@ class ApplyRefactoringCommand implements Command {
}
return true;
}

private toWorkspaceEdit(edits: Proto.FileCodeEdits[]): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
for (const edit of edits) {
for (const textChange of edit.textChanges) {
workspaceEdit.replace(this.client.asUrl(edit.fileName),
typeConverters.Range.fromTextSpan(textChange),
textChange.newText);
}
}
return workspaceEdit;
}
}

class SelectRefactorCommand implements Command {
Expand Down
3 changes: 2 additions & 1 deletion extensions/typescript/src/typescriptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ITypeScriptServiceClient {
asUrl(filepath: string): Uri;
getWorkspaceRootForResource(resource: Uri): string | undefined;

onTsServerStarted: Event<void>;
onTsServerStarted: Event<API>;
onProjectLanguageServiceStateChanged: Event<Proto.ProjectLanguageServiceStateEventBody>;
onDidBeginInstallTypings: Event<Proto.BeginInstallTypesEventBody>;
onDidEndInstallTypings: Event<Proto.EndInstallTypesEventBody>;
Expand Down Expand Up @@ -58,5 +58,6 @@ export interface ITypeScriptServiceClient {
execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise<Proto.GetApplicableRefactorsResponse>;
execute(command: 'getEditsForRefactor', args: Proto.GetEditsForRefactorRequestArgs, token?: CancellationToken): Promise<Proto.GetEditsForRefactorResponse>;
execute(command: 'applyCodeActionCommand', args: Proto.ApplyCodeActionCommandRequestArgs, token?: CancellationToken): Promise<Proto.ApplyCodeActionCommandResponse>;
execute(command: 'organizeImports', args: Proto.OrganizeImportsRequestArgs, token?: CancellationToken): Promise<Proto.OrganizeImportsResponse>;
execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise<any>;
}
11 changes: 8 additions & 3 deletions extensions/typescript/src/typescriptServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private requestQueue: RequestQueue;
private callbacks: CallbackMap;

private readonly _onTsServerStarted = new EventEmitter<void>();
private readonly _onTsServerStarted = new EventEmitter<API>();
private readonly _onProjectLanguageServiceStateChanged = new EventEmitter<Proto.ProjectLanguageServiceStateEventBody>();
private readonly _onDidBeginInstallTypings = new EventEmitter<Proto.BeginInstallTypesEventBody>();
private readonly _onDidEndInstallTypings = new EventEmitter<Proto.EndInstallTypesEventBody>();
Expand Down Expand Up @@ -254,6 +254,11 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}

public dispose() {
this._onTsServerStarted.dispose();
this._onDidBeginInstallTypings.dispose();
this._onDidEndInstallTypings.dispose();
this._onTypesInstallerInitializationFailed.dispose();

if (this.servicePromise) {
this.servicePromise.then(childProcess => {
childProcess.kill();
Expand Down Expand Up @@ -285,7 +290,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}
}

get onTsServerStarted(): Event<void> {
get onTsServerStarted(): Event<API> {
return this._onTsServerStarted.event;
}

Expand Down Expand Up @@ -425,7 +430,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient

this._onReady!.resolve();
resolve(handle);
this._onTsServerStarted.fire();
this._onTsServerStarted.fire(currentVersion.version);

this.serviceStarted(resendModels);
});
Expand Down
5 changes: 5 additions & 0 deletions extensions/typescript/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,9 @@ export default class API {
public has270Features(): boolean {
return semver.gte(this.version, '2.7.0');
}

@memoize
public has280Features(): boolean {
return semver.gte(this.version, '2.8.0');
}
}
9 changes: 8 additions & 1 deletion extensions/typescript/src/utils/languageModeIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

export const typescript = 'typescript';
export const typescriptreact = 'typescriptreact';
export const javascript = 'javascript';
export const javascriptreact = 'javascriptreact';
export const jsxTags = 'jsx-tags';
export const jsxTags = 'jsx-tags';


export function isSupportedLanguageMode(doc: vscode.TextDocument) {
return vscode.languages.match([typescript, typescriptreact, javascript, javascriptreact], doc) > 0;
}
5 changes: 1 addition & 4 deletions extensions/typescript/src/utils/managedFileContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as languageModeIds from './languageModeIds';
import { isSupportedLanguageMode } from './languageModeIds';

/**
* When clause context set when the current file is managed by vscode's built-in typescript extension.
Expand Down Expand Up @@ -46,6 +46,3 @@ export default class ManagedFileContextManager {
}
}

function isSupportedLanguageMode(doc: vscode.TextDocument) {
return vscode.languages.match([languageModeIds.typescript, languageModeIds.typescriptreact, languageModeIds.javascript, languageModeIds.javascriptreact], doc) > 0;
}

0 comments on commit 5be9e9e

Please sign in to comment.