From dfadffe444a7e153c0a3745eee841d9c5bf838f7 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 23 May 2017 14:47:55 -0700 Subject: [PATCH 1/4] Prototype TS/JS Refactoring Provider Fixes #25739, from https://github.com/Microsoft/TypeScript/pull/15569 Prototype of refactoring support for ts 2.4 --- .../src/features/refactorProvider.ts | 94 +++++++++++++++++++ extensions/typescript/src/typescriptMain.ts | 3 + .../typescript/src/typescriptService.ts | 5 + 3 files changed, 102 insertions(+) create mode 100644 extensions/typescript/src/features/refactorProvider.ts diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts new file mode 100644 index 0000000000000..4c5318c2cadce --- /dev/null +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit } from 'vscode'; + +import * as Proto from '../protocol'; +import { ITypescriptServiceClient } from '../typescriptService'; + + +export default class TypeScriptRefactorProvider implements CodeActionProvider { + private commandId: string; + + constructor( + private readonly client: ITypescriptServiceClient, + mode: string + ) { + this.commandId = `_typescript.applyRefactoring.${mode}`; + commands.registerCommand(this.commandId, this.onCodeAction, this); + } + + public async provideCodeActions( + document: TextDocument, + range: Range, + _context: CodeActionContext, + token: CancellationToken + ): Promise { + if (!this.client.apiVersion.has240Features()) { + return []; + } + + const file = this.client.normalizePath(document.uri); + if (!file) { + return []; + } + + const args: Proto.GetApplicableRefactorsRequestArgs = { + file: file, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }; + + const response = await this.client.execute('getApplicableRefactors', args, token); + if (!response || !response.body) { + return []; + } + + return response.body.map(action => ({ + title: action.description, + command: this.commandId, + arguments: [file, action.name, range] + })); + } + + private actionsToEdit(actions: Proto.CodeAction[]): WorkspaceEdit { + const workspaceEdit = new WorkspaceEdit(); + for (const action of actions) { + for (const change of action.changes) { + for (const textChange of change.textChanges) { + workspaceEdit.replace(this.client.asUrl(change.fileName), + new Range( + textChange.start.line - 1, textChange.start.offset - 1, + textChange.end.line - 1, textChange.end.offset - 1), + textChange.newText); + } + } + } + return workspaceEdit; + } + + private async onCodeAction(file: string, refactorName: string, range: Range): Promise { + const args: Proto.GetRefactorCodeActionsRequestArgs = { + file, + refactorName, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }; + + const response = await this.client.execute('getRefactorCodeActions', args); + if (!response || !response.body || !response.body.actions.length) { + return false; + } + + const edit = this.actionsToEdit(response.body.actions); + return workspace.applyEdit(edit); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 085e48bb40b63..ea7ee292d41aa 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -38,6 +38,8 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; +import RefactorProvider from './features/refactorProvider'; + import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider'; import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider'; @@ -263,6 +265,7 @@ class LanguageProvider { this.disposables.push(languages.registerRenameProvider(selector, new RenameProvider(client))); this.disposables.push(languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, this.description.id))); + this.disposables.push(languages.registerCodeActionsProvider(selector, new RefactorProvider(client, this.description.id))); this.registerVersionDependentProviders(); diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index 1b545fef0ace8..1001a9e8a1eea 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -68,6 +68,9 @@ export class API { public has234Features(): boolean { return semver.gte(this._version, '2.3.4'); } + public has240Features(): boolean { + return semver.gte(this._version, '2.4.0'); + } } export interface ITypescriptServiceClient { @@ -115,6 +118,8 @@ export interface ITypescriptServiceClient { execute(command: 'getCodeFixes', args: Proto.CodeFixRequestArgs, token?: CancellationToken): Promise; execute(command: 'getSupportedCodeFixes', args: null, token?: CancellationToken): Promise; execute(command: 'docCommentTemplate', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; + execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise; + execute(command: 'getRefactorCodeActions', args: Proto.GetRefactorCodeActionsRequestArgs, token?: CancellationToken): Promise; // execute(command: 'compileOnSaveAffectedFileList', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise; // execute(command: 'compileOnSaveEmitFile', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise; execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise; From d2883799bcfcfb3259b34723612380cfa619b65c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 24 May 2017 13:38:51 -0700 Subject: [PATCH 2/4] Adding error reporting --- .../src/features/refactorProvider.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 4c5318c2cadce..3dc02f65b93ad 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -45,16 +45,21 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { endOffset: range.end.character + 1 }; - const response = await this.client.execute('getApplicableRefactors', args, token); - if (!response || !response.body) { + try { + const response = await this.client.execute('getApplicableRefactors', args, token); + if (!response || !response.body) { + return []; + } + + return response.body.map(action => ({ + title: action.description, + command: this.commandId, + arguments: [file, action.name, range] + })); + } catch (err) { + this.client.error(`'getApplicableRefactors' request failed with error.`, err); return []; } - - return response.body.map(action => ({ - title: action.description, - command: this.commandId, - arguments: [file, action.name, range] - })); } private actionsToEdit(actions: Proto.CodeAction[]): WorkspaceEdit { From 561d689c07e096d109ba79ed55ea034cfb137ba4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 12 Jun 2017 11:27:21 -0700 Subject: [PATCH 3/4] Updating for new API --- .../src/features/refactorProvider.ts | 49 +++++++++---------- extensions/typescript/src/typescriptMain.ts | 4 +- .../typescript/src/typescriptService.ts | 2 +- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index 3dc02f65b93ad..dac170fd020dc 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -12,14 +12,14 @@ import { ITypescriptServiceClient } from '../typescriptService'; export default class TypeScriptRefactorProvider implements CodeActionProvider { - private commandId: string; + private doRefactorCommandId: string; constructor( private readonly client: ITypescriptServiceClient, mode: string ) { - this.commandId = `_typescript.applyRefactoring.${mode}`; - commands.registerCommand(this.commandId, this.onCodeAction, this); + this.doRefactorCommandId = `_typescript.applyRefactoring.${mode}`; + commands.registerCommand(this.doRefactorCommandId, this.onCodeAction, this); } public async provideCodeActions( @@ -51,49 +51,48 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return []; } - return response.body.map(action => ({ - title: action.description, - command: this.commandId, - arguments: [file, action.name, range] - })); + return Array.prototype.concat.apply([], response.body.map(info => + info.actions.map(action => ({ + title: action.description, + command: this.doRefactorCommandId, + arguments: [file, info.name, action.name, range] + })))); } catch (err) { - this.client.error(`'getApplicableRefactors' request failed with error.`, err); return []; } } - private actionsToEdit(actions: Proto.CodeAction[]): WorkspaceEdit { + private toWorkspaceEdit(edits: Proto.FileCodeEdits[]): WorkspaceEdit { const workspaceEdit = new WorkspaceEdit(); - for (const action of actions) { - for (const change of action.changes) { - for (const textChange of change.textChanges) { - workspaceEdit.replace(this.client.asUrl(change.fileName), - new Range( - textChange.start.line - 1, textChange.start.offset - 1, - textChange.end.line - 1, textChange.end.offset - 1), - textChange.newText); - } + for (const edit of edits) { + for (const textChange of edit.textChanges) { + workspaceEdit.replace(this.client.asUrl(edit.fileName), + new Range( + textChange.start.line - 1, textChange.start.offset - 1, + textChange.end.line - 1, textChange.end.offset - 1), + textChange.newText); } } return workspaceEdit; } - private async onCodeAction(file: string, refactorName: string, range: Range): Promise { - const args: Proto.GetRefactorCodeActionsRequestArgs = { + private async onCodeAction(file: string, refactor: string, action: string, range: Range): Promise { + const args: Proto.GetEditsForRefactorRequestArgs = { file, - refactorName, + refactor, + action, startLine: range.start.line + 1, startOffset: range.start.character + 1, endLine: range.end.line + 1, endOffset: range.end.character + 1 }; - const response = await this.client.execute('getRefactorCodeActions', args); - if (!response || !response.body || !response.body.actions.length) { + const response = await this.client.execute('getEditsForRefactor', args); + if (!response || !response.body || !response.body.edits.length) { return false; } - const edit = this.actionsToEdit(response.body.actions); + const edit = this.toWorkspaceEdit(response.body.edits); return workspace.applyEdit(edit); } } \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index ea7ee292d41aa..6faba48cf2a12 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -38,8 +38,6 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; -import RefactorProvider from './features/refactorProvider'; - import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider'; import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider'; @@ -169,6 +167,7 @@ export function activate(context: ExtensionContext): void { const validateSetting = 'validate.enable'; class LanguageProvider { + private syntaxDiagnostics: ObjectMap; private readonly currentDiagnostics: DiagnosticCollection; private readonly bufferSyncSupport: BufferSyncSupport; @@ -265,7 +264,6 @@ class LanguageProvider { this.disposables.push(languages.registerRenameProvider(selector, new RenameProvider(client))); this.disposables.push(languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, this.description.id))); - this.disposables.push(languages.registerCodeActionsProvider(selector, new RefactorProvider(client, this.description.id))); this.registerVersionDependentProviders(); diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index 1001a9e8a1eea..05b11a24f4166 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -119,7 +119,7 @@ export interface ITypescriptServiceClient { execute(command: 'getSupportedCodeFixes', args: null, token?: CancellationToken): Promise; execute(command: 'docCommentTemplate', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise; - execute(command: 'getRefactorCodeActions', args: Proto.GetRefactorCodeActionsRequestArgs, token?: CancellationToken): Promise; + execute(command: 'getEditsForRefactor', args: Proto.GetEditsForRefactorRequestArgs, token?: CancellationToken): Promise; // execute(command: 'compileOnSaveAffectedFileList', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise; // execute(command: 'compileOnSaveEmitFile', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise; execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise; From 3abf8e3a3a2f45f2d35af0b44fcc430e4863b9b4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 12 Jun 2017 13:48:05 -0700 Subject: [PATCH 4/4] show quick pick for non-inlinable refactrings --- .../src/features/refactorProvider.ts | 48 +++++++++++++++---- extensions/typescript/src/typescriptMain.ts | 4 +- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/extensions/typescript/src/features/refactorProvider.ts b/extensions/typescript/src/features/refactorProvider.ts index dac170fd020dc..4e930e09d8065 100644 --- a/extensions/typescript/src/features/refactorProvider.ts +++ b/extensions/typescript/src/features/refactorProvider.ts @@ -5,7 +5,7 @@ 'use strict'; -import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit } from 'vscode'; +import { CodeActionProvider, TextDocument, Range, CancellationToken, CodeActionContext, Command, commands, workspace, WorkspaceEdit, window, QuickPickItem } from 'vscode'; import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; @@ -13,13 +13,18 @@ import { ITypescriptServiceClient } from '../typescriptService'; export default class TypeScriptRefactorProvider implements CodeActionProvider { private doRefactorCommandId: string; + private selectRefactorCommandId: string; constructor( private readonly client: ITypescriptServiceClient, mode: string ) { this.doRefactorCommandId = `_typescript.applyRefactoring.${mode}`; - commands.registerCommand(this.doRefactorCommandId, this.onCodeAction, this); + this.selectRefactorCommandId = `_typescript.selectRefactoring.${mode}`; + + commands.registerCommand(this.doRefactorCommandId, this.doRefactoring, this); + commands.registerCommand(this.selectRefactorCommandId, this.selectRefactoring, this); + } public async provideCodeActions( @@ -51,12 +56,25 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return []; } - return Array.prototype.concat.apply([], response.body.map(info => - info.actions.map(action => ({ - title: action.description, - command: this.doRefactorCommandId, - arguments: [file, info.name, action.name, range] - })))); + const actions: Command[] = []; + for (const info of response.body) { + if (info.inlineable === false) { + actions.push({ + title: info.description, + command: this.selectRefactorCommandId, + arguments: [file, info, range] + }); + } else { + for (const action of info.actions) { + actions.push({ + title: action.description, + command: this.doRefactorCommandId, + arguments: [file, info.name, action.name, range] + }); + } + } + } + return actions; } catch (err) { return []; } @@ -76,7 +94,19 @@ export default class TypeScriptRefactorProvider implements CodeActionProvider { return workspaceEdit; } - private async onCodeAction(file: string, refactor: string, action: string, range: Range): Promise { + private async selectRefactoring(file: string, info: Proto.ApplicableRefactorInfo, range: Range): Promise { + return window.showQuickPick(info.actions.map((action): QuickPickItem => ({ + label: action.name, + description: action.description + }))).then(selected => { + if (!selected) { + return false; + } + return this.doRefactoring(file, info.name, selected.label, range); + }); + } + + private async doRefactoring(file: string, refactor: string, action: string, range: Range): Promise { const args: Proto.GetEditsForRefactorRequestArgs = { file, refactor, diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 6faba48cf2a12..48098cd735b67 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -38,11 +38,11 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; +import RefactorProvider from './features/refactorProvider'; import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider'; import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider'; import TypeScriptTaskProviderManager from './features/taskProvider'; - import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider'; import * as ProjectStatus from './utils/projectStatus'; @@ -264,7 +264,7 @@ class LanguageProvider { this.disposables.push(languages.registerRenameProvider(selector, new RenameProvider(client))); this.disposables.push(languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, this.description.id))); - + this.disposables.push(languages.registerCodeActionsProvider(selector, new RefactorProvider(client, this.description.id))); this.registerVersionDependentProviders(); this.description.modeIds.forEach(modeId => {