refactor.ts 7.2 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

M
Matt Bierner 已提交
6
import * as vscode from 'vscode';
7
import * as fs from 'fs';
8 9

import * as Proto from '../protocol';
10
import { ITypeScriptServiceClient } from '../typescriptService';
11
import * as typeConverters from '../utils/typeConverters';
M
Matt Bierner 已提交
12
import FormattingOptionsManager from './fileConfigurationManager';
M
Matt Bierner 已提交
13
import { CommandManager, Command } from '../utils/commandManager';
14
import { VersionDependentRegistration } from '../utils/dependentRegistration';
M
Matt Bierner 已提交
15
import API from '../utils/api';
16

M
Matt Bierner 已提交
17 18 19
class ApplyRefactoringCommand implements Command {
	public static readonly ID = '_typescript.applyRefactoring';
	public readonly id = ApplyRefactoringCommand.ID;
20 21

	constructor(
22
		private readonly client: ITypeScriptServiceClient
M
Matt Bierner 已提交
23 24 25 26 27 28 29 30 31 32
	) { }

	public async execute(
		document: vscode.TextDocument,
		file: string,
		refactor: string,
		action: string,
		range: vscode.Range
	): Promise<boolean> {
		const args: Proto.GetEditsForRefactorRequestArgs = {
33
			...typeConverters.Range.toFileRangeRequestArgs(file, range),
M
Matt Bierner 已提交
34 35 36 37 38 39 40 41
			refactor,
			action
		};
		const response = await this.client.execute('getEditsForRefactor', args);
		if (!response || !response.body || !response.body.edits.length) {
			return false;
		}

42 43 44 45 46 47 48 49 50 51 52 53 54 55
		for (const edit of response.body.edits) {
			try {
				await vscode.workspace.openTextDocument(edit.fileName);
			} catch {
				try {
					if (!fs.existsSync(edit.fileName)) {
						fs.writeFileSync(edit.fileName, '');
					}
				} catch {
					// noop
				}
			}
		}

56
		const edit = typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, response.body.edits);
M
Matt Bierner 已提交
57 58 59 60 61 62
		if (!(await vscode.workspace.applyEdit(edit))) {
			return false;
		}

		const renameLocation = response.body.renameLocation;
		if (renameLocation) {
63 64 65 66
			await vscode.commands.executeCommand('editor.action.rename', [
				document.uri,
				typeConverters.Position.fromLocation(renameLocation)
			]);
M
Matt Bierner 已提交
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
		}
		return true;
	}
}

class SelectRefactorCommand implements Command {
	public static readonly ID = '_typescript.selectRefactoring';
	public readonly id = SelectRefactorCommand.ID;

	constructor(
		private readonly doRefactoring: ApplyRefactoringCommand
	) { }

	public async execute(
		document: vscode.TextDocument,
		file: string,
		info: Proto.ApplicableRefactorInfo,
		range: vscode.Range
	): Promise<boolean> {
		const selected = await vscode.window.showQuickPick(info.actions.map((action): vscode.QuickPickItem => ({
			label: action.name,
			description: action.description
		})));
		if (!selected) {
			return false;
		}
		return this.doRefactoring.execute(document, file, info.name, selected.label, range);
	}
}

97
class TypeScriptRefactorProvider implements vscode.CodeActionProvider {
98 99
	private static readonly extractFunctionKind = vscode.CodeActionKind.RefactorExtract.append('function');
	private static readonly extractConstantKind = vscode.CodeActionKind.RefactorExtract.append('constant');
100
	private static readonly moveKind = vscode.CodeActionKind.Refactor.append('move');
101

M
Matt Bierner 已提交
102 103
	constructor(
		private readonly client: ITypeScriptServiceClient,
104
		private readonly formattingOptionsManager: FormattingOptionsManager,
105
		commandManager: CommandManager
106
	) {
107
		const doRefactoringCommand = commandManager.register(new ApplyRefactoringCommand(this.client));
M
Matt Bierner 已提交
108
		commandManager.register(new SelectRefactorCommand(doRefactoringCommand));
109 110
	}

111
	public static readonly metadata: vscode.CodeActionProviderMetadata = {
112 113 114
		providedCodeActionKinds: [vscode.CodeActionKind.Refactor]
	};

115
	public async provideCodeActions(
M
Matt Bierner 已提交
116
		document: vscode.TextDocument,
117
		rangeOrSelection: vscode.Range | vscode.Selection,
M
Matt Bierner 已提交
118
		context: vscode.CodeActionContext,
M
Matt Bierner 已提交
119
		token: vscode.CancellationToken
M
Matt Bierner 已提交
120 121 122
	): Promise<vscode.CodeAction[] | undefined> {
		if (!this.shouldTrigger(rangeOrSelection, context)) {
			return undefined;
123 124
		}

125
		const file = this.client.normalizePath(document.uri);
126
		if (!file) {
M
Matt Bierner 已提交
127
			return undefined;
128 129
		}

130 131
		await this.formattingOptionsManager.ensureConfigurationForDocument(document, undefined);

132
		const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs(file, rangeOrSelection);
M
Matt Bierner 已提交
133
		let response: Proto.GetApplicableRefactorsResponse;
134
		try {
M
Matt Bierner 已提交
135
			response = await this.client.execute('getApplicableRefactors', args, token);
136
			if (!response || !response.body) {
M
Matt Bierner 已提交
137
				return undefined;
138
			}
M
Matt Bierner 已提交
139
		} catch {
M
Matt Bierner 已提交
140
			return undefined;
M
Matt Bierner 已提交
141
		}
142

M
Matt Bierner 已提交
143 144 145 146 147 148 149 150 151
		return this.convertApplicableRefactors(response.body, document, file, rangeOrSelection);
	}

	private convertApplicableRefactors(
		body: Proto.ApplicableRefactorInfo[],
		document: vscode.TextDocument,
		file: string,
		rangeOrSelection: vscode.Range | vscode.Selection
	) {
M
Matt Bierner 已提交
152
		const actions: vscode.CodeAction[] = [];
M
Matt Bierner 已提交
153
		for (const info of body) {
M
Matt Bierner 已提交
154 155 156 157 158 159 160 161 162 163
			if (info.inlineable === false) {
				const codeAction = new vscode.CodeAction(info.description, vscode.CodeActionKind.Refactor);
				codeAction.command = {
					title: info.description,
					command: SelectRefactorCommand.ID,
					arguments: [document, file, info, rangeOrSelection]
				};
				actions.push(codeAction);
			} else {
				for (const action of info.actions) {
M
Matt Bierner 已提交
164
					actions.push(this.refactorActionToCodeAction(action, document, file, info, rangeOrSelection));
165 166 167
				}
			}
		}
M
Matt Bierner 已提交
168
		return actions;
169
	}
M
Matt Bierner 已提交
170

M
Matt Bierner 已提交
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
	private refactorActionToCodeAction(
		action: Proto.RefactorActionInfo,
		document: vscode.TextDocument,
		file: string,
		info: Proto.ApplicableRefactorInfo,
		rangeOrSelection: vscode.Range | vscode.Selection
	) {
		const codeAction = new vscode.CodeAction(action.description, TypeScriptRefactorProvider.getKind(action));
		codeAction.command = {
			title: action.description,
			command: ApplyRefactoringCommand.ID,
			arguments: [document, file, info.name, action.name, rangeOrSelection],
		};
		return codeAction;
	}

	private shouldTrigger(rangeOrSelection: vscode.Range | vscode.Selection, context: vscode.CodeActionContext) {
		if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) {
			return false;
		}

		return rangeOrSelection instanceof vscode.Selection && (!rangeOrSelection.isEmpty || context.triggerKind === vscode.CodeActionTrigger.Manual);
	}

M
Matt Bierner 已提交
195 196
	private static getKind(refactor: Proto.RefactorActionInfo) {
		if (refactor.name.startsWith('function_')) {
197 198 199
			return TypeScriptRefactorProvider.extractFunctionKind;
		} else if (refactor.name.startsWith('constant_')) {
			return TypeScriptRefactorProvider.extractConstantKind;
200 201
		} else if (refactor.name.startsWith('Move')) {
			return TypeScriptRefactorProvider.moveKind;
M
Matt Bierner 已提交
202 203 204
		}
		return vscode.CodeActionKind.Refactor;
	}
205 206 207 208 209 210 211 212
}

export function register(
	selector: vscode.DocumentSelector,
	client: ITypeScriptServiceClient,
	formattingOptionsManager: FormattingOptionsManager,
	commandManager: CommandManager,
) {
213 214
	return new VersionDependentRegistration(client, {
		isSupportedVersion(api) {
M
Matt Bierner 已提交
215
			return api.gte(API.v240);
216 217 218 219 220 221 222 223
		},
		register() {
			return vscode.languages.registerCodeActionsProvider(selector,
				new TypeScriptRefactorProvider(client, formattingOptionsManager, commandManager),
				TypeScriptRefactorProvider.metadata);
		}
	});
}