diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index efac422b8ad..570be95fa33 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -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" } ] }, diff --git a/extensions/typescript/package.nls.json b/extensions/typescript/package.nls.json index c8b37dfddd5..9e2c885647d 100644 --- a/extensions/typescript/package.nls.json +++ b/extensions/typescript/package.nls.json @@ -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" } \ No newline at end of file diff --git a/extensions/typescript/src/extension.ts b/extensions/typescript/src/extension.ts index 73604a21c3f..ac33918d1c2 100644 --- a/extensions/typescript/src/extension.ts +++ b/extensions/typescript/src/extension.ts @@ -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(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( diff --git a/extensions/typescript/src/features/organizeImports.ts b/extensions/typescript/src/features/organizeImports.ts new file mode 100644 index 00000000000..3f53aa29f1a --- /dev/null +++ b/extensions/typescript/src/features/organizeImports.ts @@ -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 + ) { } + + public async execute(): Promise { + // 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; + } +} diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 2728e6a5baf..17cb2d66f2f 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -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 { diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index b60dbd8c5e5..399c9ce3856 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -16,7 +16,7 @@ export interface ITypeScriptServiceClient { asUrl(filepath: string): Uri; getWorkspaceRootForResource(resource: Uri): string | undefined; - onTsServerStarted: Event; + onTsServerStarted: Event; onProjectLanguageServiceStateChanged: Event; onDidBeginInstallTypings: Event; onDidEndInstallTypings: Event; @@ -58,5 +58,6 @@ export interface ITypeScriptServiceClient { execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise; execute(command: 'getEditsForRefactor', args: Proto.GetEditsForRefactorRequestArgs, token?: CancellationToken): Promise; execute(command: 'applyCodeActionCommand', args: Proto.ApplyCodeActionCommandRequestArgs, token?: CancellationToken): Promise; + execute(command: 'organizeImports', args: Proto.OrganizeImportsRequestArgs, token?: CancellationToken): Promise; execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise; } \ No newline at end of file diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index f41adaff666..61cfe79967d 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -168,7 +168,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private requestQueue: RequestQueue; private callbacks: CallbackMap; - private readonly _onTsServerStarted = new EventEmitter(); + private readonly _onTsServerStarted = new EventEmitter(); private readonly _onProjectLanguageServiceStateChanged = new EventEmitter(); private readonly _onDidBeginInstallTypings = new EventEmitter(); private readonly _onDidEndInstallTypings = new EventEmitter(); @@ -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 { + get onTsServerStarted(): Event { 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); }); diff --git a/extensions/typescript/src/utils/api.ts b/extensions/typescript/src/utils/api.ts index e4103a29231..7dadfd840f1 100644 --- a/extensions/typescript/src/utils/api.ts +++ b/extensions/typescript/src/utils/api.ts @@ -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'); + } } \ No newline at end of file diff --git a/extensions/typescript/src/utils/languageModeIds.ts b/extensions/typescript/src/utils/languageModeIds.ts index 65f8cb0e9ff..5fe187e13db 100644 --- a/extensions/typescript/src/utils/languageModeIds.ts +++ b/extensions/typescript/src/utils/languageModeIds.ts @@ -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'; \ No newline at end of file +export const jsxTags = 'jsx-tags'; + + +export function isSupportedLanguageMode(doc: vscode.TextDocument) { + return vscode.languages.match([typescript, typescriptreact, javascript, javascriptreact], doc) > 0; +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/managedFileContext.ts b/extensions/typescript/src/utils/managedFileContext.ts index baf4fd704e6..b16f4524061 100644 --- a/extensions/typescript/src/utils/managedFileContext.ts +++ b/extensions/typescript/src/utils/managedFileContext.ts @@ -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; -}