Skip to content

Commit 0783074

Browse files
testforstephenfbricon
authored andcommitted
Enable 'Move...' in Refactor menu (#979)
Signed-off-by: Jinbo Wang <jinbwan@microsoft.com>
1 parent 1f57f69 commit 0783074

3 files changed

Lines changed: 196 additions & 13 deletions

File tree

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
8080
generateConstructorsPromptSupport: true,
8181
generateDelegateMethodsPromptSupport: true,
8282
advancedExtractRefactoringSupport: true,
83+
moveRefactoringSupport: true,
8384
},
8485
triggerFiles: getTriggerFiles()
8586
},

src/protocol.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ export interface RenamePosition {
288288
export interface RefactorWorkspaceEdit {
289289
edit: WorkspaceEdit;
290290
command?: Command;
291+
errorMessage?: string;
291292
}
292293

293294
export interface GetRefactorEditParams {
@@ -300,3 +301,32 @@ export interface GetRefactorEditParams {
300301
export namespace GetRefactorEditRequest {
301302
export const type = new RequestType<GetRefactorEditParams, RefactorWorkspaceEdit, void, void>('java/getRefactorEdit');
302303
}
304+
305+
export interface PackageNode {
306+
displayName: string;
307+
uri: string;
308+
path: string;
309+
project: string;
310+
isDefaultPackage: boolean;
311+
isParentOfSelectedFile: boolean;
312+
}
313+
314+
export interface MoveParams {
315+
moveKind: string;
316+
sourceUris: string[];
317+
params: CodeActionParams;
318+
destination?: any;
319+
updateReferences?: boolean;
320+
}
321+
322+
export interface MoveDestinationsResponse {
323+
destinations: any[];
324+
}
325+
326+
export namespace GetMoveDestinationsRequest {
327+
export const type = new RequestType<MoveParams, MoveDestinationsResponse, void, void>('java/getMoveDestinations');
328+
}
329+
330+
export namespace MoveRequest {
331+
export const type = new RequestType<MoveParams, RefactorWorkspaceEdit, void, void>('java/move');
332+
}

src/refactorAction.ts

Lines changed: 165 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use strict';
22

3-
import { commands, window, ExtensionContext, workspace, Position, Uri, TextDocument } from 'vscode';
4-
import { LanguageClient, FormattingOptions } from 'vscode-languageclient';
3+
import { existsSync } from 'fs';
4+
import * as path from 'path';
5+
import { commands, ExtensionContext, Position, TextDocument, Uri, window, workspace } from 'vscode';
6+
import { FormattingOptions, LanguageClient, WorkspaceEdit, CreateFile, RenameFile, DeleteFile, TextDocumentEdit } from 'vscode-languageclient';
57
import { Commands as javaCommands } from './commands';
6-
import { GetRefactorEditRequest, RefactorWorkspaceEdit, RenamePosition } from './protocol';
8+
import { GetRefactorEditRequest, MoveRequest, RefactorWorkspaceEdit, RenamePosition, GetMoveDestinationsRequest } from './protocol';
79

810
export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) {
911
registerApplyRefactorCommand(languageClient, context);
@@ -73,22 +75,172 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
7375
commandArguments,
7476
});
7577

76-
if (!result || !result.edit) {
78+
await applyRefactorEdit(languageClient, result);
79+
} else if (command === 'moveFile') {
80+
if (!commandInfo || !commandInfo.uri) {
7781
return;
7882
}
7983

80-
const edit = languageClient.protocol2CodeConverter.asWorkspaceEdit(result.edit);
81-
if (edit) {
82-
await workspace.applyEdit(edit);
84+
await moveFile(languageClient, [Uri.parse(commandInfo.uri)]);
85+
}
86+
}));
87+
}
88+
89+
async function applyRefactorEdit(languageClient: LanguageClient, refactorEdit: RefactorWorkspaceEdit) {
90+
if (!refactorEdit) {
91+
return;
92+
}
93+
94+
if (refactorEdit.errorMessage) {
95+
window.showErrorMessage(refactorEdit.errorMessage);
96+
return;
97+
}
98+
99+
if (refactorEdit.edit) {
100+
const edit = languageClient.protocol2CodeConverter.asWorkspaceEdit(refactorEdit.edit);
101+
if (edit) {
102+
await workspace.applyEdit(edit);
103+
}
104+
}
105+
106+
if (refactorEdit.command) {
107+
if (refactorEdit.command.arguments) {
108+
await commands.executeCommand(refactorEdit.command.command, ...refactorEdit.command.arguments);
109+
} else {
110+
await commands.executeCommand(refactorEdit.command.command);
111+
}
112+
}
113+
}
114+
115+
async function moveFile(languageClient: LanguageClient, fileUris: Uri[]) {
116+
if (!hasCommonParent(fileUris)) {
117+
window.showErrorMessage("Moving files from different directories are not supported. Please make sure they are from the same directory.");
118+
return;
119+
}
120+
121+
const moveDestinations = await languageClient.sendRequest(GetMoveDestinationsRequest.type, {
122+
moveKind: 'moveResource',
123+
sourceUris: fileUris.map(uri => uri.toString())
124+
});
125+
if (!moveDestinations || !moveDestinations.destinations || !moveDestinations.destinations.length) {
126+
window.showErrorMessage("Cannot find available Java packages to move the selected files to.");
127+
return;
128+
}
129+
130+
const packageNodeItems = moveDestinations.destinations.map((packageNode) => {
131+
const packageUri: Uri = packageNode.uri ? Uri.parse(packageNode.uri) : null;
132+
const displayPath: string = packageUri ? workspace.asRelativePath(packageUri, true) : packageNode.path;
133+
return {
134+
label: (packageNode.isParentOfSelectedFile ? '* ' : '') + packageNode.displayName,
135+
description: displayPath,
136+
packageNode,
137+
}
138+
});
139+
140+
let placeHolder = (fileUris.length === 1) ? `Choose the target package for ${getFileNameFromUri(fileUris[0])}.`
141+
: `Choose the target package for ${fileUris.length} selected files.`;
142+
let selectPackageNodeItem = await window.showQuickPick(packageNodeItems, {
143+
placeHolder,
144+
});
145+
if (!selectPackageNodeItem) {
146+
return;
147+
}
148+
149+
const packageUri: Uri = selectPackageNodeItem.packageNode.uri ? Uri.parse(selectPackageNodeItem.packageNode.uri) : null;
150+
if (packageUri && packageUri.fsPath) {
151+
const duplicatedFiles: string[] = [];
152+
const moveUris: Uri[] = [];
153+
for (const uri of fileUris) {
154+
const fileName: string = getFileNameFromUri(uri);
155+
if (existsSync(path.join(packageUri.fsPath, fileName))) {
156+
duplicatedFiles.push(fileName);
157+
} else {
158+
moveUris.push(uri);
83159
}
160+
}
161+
162+
if (duplicatedFiles.length) {
163+
window.showWarningMessage(`The files '${duplicatedFiles.join(',')}' already exist in the package '${selectPackageNodeItem.packageNode.displayName}'. The move operation will ignore them.`);
164+
}
165+
166+
if (!moveUris.length) {
167+
return;
168+
}
169+
170+
fileUris = moveUris;
171+
}
172+
173+
const refactorEdit: RefactorWorkspaceEdit = await languageClient.sendRequest(MoveRequest.type, {
174+
moveKind: 'moveResource',
175+
sourceUris: fileUris.map(uri => uri.toString()),
176+
params: null,
177+
destination: selectPackageNodeItem.packageNode,
178+
updateReferences: true,
179+
});
180+
181+
await applyRefactorEdit(languageClient, refactorEdit);
182+
if (refactorEdit && refactorEdit.edit) {
183+
await saveEdit(refactorEdit.edit);
184+
}
185+
}
186+
187+
function getFileNameFromUri(uri: Uri): string {
188+
return uri.fsPath.replace(/^.*[\\\/]/, '');
189+
}
190+
191+
function hasCommonParent(uris: Uri[]): boolean {
192+
if (uris == null || uris.length <= 1) {
193+
return true;
194+
}
195+
196+
const firstParent: string = path.dirname(uris[0].fsPath);
197+
for (let i = 1; i < uris.length; i++) {
198+
const parent = path.dirname(uris[i].fsPath);
199+
if (path.relative(firstParent, parent) !== '.') {
200+
return false;
201+
}
202+
}
84203

85-
if (result.command) {
86-
if (result.command.arguments) {
87-
await commands.executeCommand(result.command.command, ...result.command.arguments);
88-
} else {
89-
await commands.executeCommand(result.command.command);
204+
return true;
205+
}
206+
207+
async function saveEdit(edit: WorkspaceEdit) {
208+
if (!edit) {
209+
return;
210+
}
211+
212+
const touchedFiles: Set<string> = new Set<string>();
213+
if (edit.changes) {
214+
for (const uri of Object.keys(edit.changes)) {
215+
touchedFiles.add(uri);
216+
}
217+
}
218+
219+
if (edit.documentChanges) {
220+
for (const change of edit.documentChanges) {
221+
const kind = (<any> change).kind;
222+
if (kind === 'rename') {
223+
if (touchedFiles.has((<RenameFile> change).oldUri)) {
224+
touchedFiles.delete((<RenameFile> change).oldUri);
225+
touchedFiles.add((<RenameFile> change).newUri);
90226
}
227+
} else if (kind === 'delete') {
228+
if (touchedFiles.has((<DeleteFile> change).uri)) {
229+
touchedFiles.delete((<DeleteFile> change).uri);
230+
}
231+
} else if (!kind) {
232+
touchedFiles.add((<TextDocumentEdit> change).textDocument.uri);
91233
}
92234
}
93-
}));
235+
}
236+
237+
for (const fileUri of touchedFiles) {
238+
const uri: Uri = Uri.parse(fileUri);
239+
const document: TextDocument = await workspace.openTextDocument(uri);
240+
if (document == null) {
241+
continue;
242+
}
243+
244+
await document.save();
245+
}
94246
}

0 commit comments

Comments
 (0)