Organize imports for js and ts (#45237)

* 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
This commit is contained in:
Matt Bierner 2018-03-19 21:30:01 -07:00 committed by GitHub
parent 3eff6ac72d
commit 5be9e9e7ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 23 deletions

View file

@ -36,6 +36,7 @@
"onCommand:javascript.goToProjectConfig",
"onCommand:typescript.goToProjectConfig",
"onCommand:typescript.openTsServerLog",
"onCommand:typescript.organizeImports",
"onCommand:workbench.action.tasks.runTask"
],
"main": "./out/extension",
@ -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": {
@ -507,6 +520,10 @@
{
"command": "typescript.restartTsServer",
"when": "typescript.isManagedFile"
},
{
"command": "typescript.organizeImports",
"when": "typescript.isManagedFile && typescript.canOrganizeImports"
}
]
},

View file

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

View file

@ -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
@ -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(
@ -79,6 +87,7 @@ function createLazyClientHost(
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
context.workspaceState));
});
return clientHost;
});
}
@ -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(

View file

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

View file

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

View file

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

View file

@ -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>();
@ -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();
@ -285,7 +290,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
}
}
get onTsServerStarted(): Event<void> {
get onTsServerStarted(): Event<API> {
return this._onTsServerStarted.event;
}
@ -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);
});

View file

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

View file

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

View file

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