Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable 'Move...' in Refactor menu #979

Merged
merged 11 commits into from
Aug 2, 2019
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
generateConstructorsPromptSupport: true,
generateDelegateMethodsPromptSupport: true,
advancedExtractRefactoringSupport: true,
moveRefactoringSupport: true,
},
triggerFiles: getTriggerFiles()
},
Expand Down
28 changes: 28 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export interface RenamePosition {
export interface RefactorWorkspaceEdit {
edit: WorkspaceEdit;
command?: Command;
errorMessage?: string;
}

export interface GetRefactorEditParams {
Expand All @@ -300,3 +301,30 @@ export interface GetRefactorEditParams {
export namespace GetRefactorEditRequest {
export const type = new RequestType<GetRefactorEditParams, RefactorWorkspaceEdit, void, void>('java/getRefactorEdit');
}

export interface PackageNode {
displayName: string;
uri: string;
path: string;
project: string;
isDefaultPackage: boolean;
isParentOfSelectedFile: boolean;
}

export interface PackageDestinationsResponse {
packageNodes: PackageNode[];
}

export namespace GetPackageDestinationsRequest {
export const type = new RequestType<string[], PackageDestinationsResponse, void, void>('java/getPackageDestinations');
}

export interface MoveFileParams {
documentUris: string[];
targetUri: string;
updateReferences: boolean;
}

export namespace MoveFileRequest {
export const type = new RequestType<MoveFileParams, RefactorWorkspaceEdit, void, void>('java/moveFile');
}
173 changes: 160 additions & 13 deletions src/refactorAction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use strict';

import { commands, window, ExtensionContext, workspace, Position, Uri, TextDocument } from 'vscode';
import { LanguageClient, FormattingOptions } from 'vscode-languageclient';
import { existsSync } from 'fs';
import * as path from 'path';
import { commands, ExtensionContext, Position, TextDocument, Uri, window, workspace } from 'vscode';
import { FormattingOptions, LanguageClient, WorkspaceEdit, CreateFile, RenameFile, DeleteFile, TextDocumentEdit } from 'vscode-languageclient';
import { Commands as javaCommands } from './commands';
import { GetRefactorEditRequest, RefactorWorkspaceEdit, RenamePosition } from './protocol';
import { GetPackageDestinationsRequest, GetRefactorEditRequest, MoveFileRequest, RefactorWorkspaceEdit, RenamePosition } from './protocol';

export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) {
registerApplyRefactorCommand(languageClient, context);
Expand Down Expand Up @@ -73,22 +75,167 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
commandArguments,
});

if (!result || !result.edit) {
await applyRefactorEdit(languageClient, result);
} else if (command === 'moveFile') {
if (!commandInfo || !commandInfo.uri) {
return;
}

const edit = languageClient.protocol2CodeConverter.asWorkspaceEdit(result.edit);
if (edit) {
await workspace.applyEdit(edit);
await moveFile(languageClient, [Uri.parse(commandInfo.uri)]);
}
}));
}

async function applyRefactorEdit(languageClient: LanguageClient, refactorEdit: RefactorWorkspaceEdit) {
if (!refactorEdit) {
return;
}

if (refactorEdit.errorMessage) {
window.showErrorMessage(refactorEdit.errorMessage);
return;
}

if (refactorEdit.edit) {
const edit = languageClient.protocol2CodeConverter.asWorkspaceEdit(refactorEdit.edit);
if (edit) {
await workspace.applyEdit(edit);
}
}

if (refactorEdit.command) {
if (refactorEdit.command.arguments) {
await commands.executeCommand(refactorEdit.command.command, ...refactorEdit.command.arguments);
} else {
await commands.executeCommand(refactorEdit.command.command);
}
}
}

async function moveFile(languageClient: LanguageClient, fileUris: Uri[]) {
if (!hasCommonParent(fileUris)) {
window.showErrorMessage("Moving files of different directories are not supported. Please make sure they are from the same directory.");
return;
}

const moveDestination = await languageClient.sendRequest(GetPackageDestinationsRequest.type, fileUris.map(uri => uri.toString()));
if (!moveDestination || !moveDestination.packageNodes || !moveDestination.packageNodes.length) {
window.showErrorMessage("Cannot find available Java packages to move the selected files to.");
return;
}

const packageNodeItems = moveDestination.packageNodes.map((packageNode) => {
const packageUri: Uri = packageNode.uri ? Uri.parse(packageNode.uri) : null;
const displayPath: string = packageUri ? workspace.asRelativePath(packageUri, true) : packageNode.path;
return {
label: (packageNode.isParentOfSelectedFile ? '* ' : '') + packageNode.displayName,
description: displayPath,
packageNode,
}
});

let placeHolder = (fileUris.length === 1) ? `Choose the target package for ${getFileNameFromUri(fileUris[0])}.`
: `Choose the target package for ${fileUris.length} selected files.`;
let selectPackageNodeItem = await window.showQuickPick(packageNodeItems, {
placeHolder,
});
if (!selectPackageNodeItem) {
return;
}

const packageUri: Uri = selectPackageNodeItem.packageNode.uri ? Uri.parse(selectPackageNodeItem.packageNode.uri) : null;
if (packageUri && packageUri.fsPath) {
const duplicatedFiles: string[] = [];
const moveUris: Uri[] = [];
for (const uri of fileUris) {
const fileName: string = getFileNameFromUri(uri);
if (existsSync(path.join(packageUri.fsPath, fileName))) {
duplicatedFiles.push(fileName);
} else {
moveUris.push(uri);
}
}

if (duplicatedFiles.length) {
window.showWarningMessage(`The files '${duplicatedFiles.join(',')}' already exist in the package '${selectPackageNodeItem.packageNode.displayName}'. The move operation will ignore them.`);
}

if (!moveUris.length) {
return;
}

fileUris = moveUris;
}

const refactorEdit: RefactorWorkspaceEdit = await languageClient.sendRequest(MoveFileRequest.type, {
documentUris: fileUris.map(uri => uri.toString()),
targetUri: selectPackageNodeItem.packageNode.uri,
updateReferences: true,
});

await applyRefactorEdit(languageClient, refactorEdit);
if (refactorEdit && refactorEdit.edit) {
await saveEdit(refactorEdit.edit);
}
}

function getFileNameFromUri(uri: Uri): string {
return uri.fsPath.replace(/^.*[\\\/]/, '');
}

function hasCommonParent(uris: Uri[]): boolean {
if (uris == null || uris.length <= 1) {
return true;
}

const firstParent: string = path.dirname(uris[0].fsPath);
for (let i = 1; i < uris.length; i++) {
const parent = path.dirname(uris[i].fsPath);
if (path.relative(firstParent, parent) !== '.') {
return false;
}
}

if (result.command) {
if (result.command.arguments) {
await commands.executeCommand(result.command.command, ...result.command.arguments);
} else {
await commands.executeCommand(result.command.command);
return true;
}

async function saveEdit(edit: WorkspaceEdit) {
if (!edit) {
return;
}

const touchedFiles: Set<string> = new Set<string>();
if (edit.changes) {
for (const uri of Object.keys(edit.changes)) {
touchedFiles.add(uri);
}
}

if (edit.documentChanges) {
for (const change of edit.documentChanges) {
const kind = (<any> change).kind;
if (kind === 'rename') {
if (touchedFiles.has((<RenameFile> change).oldUri)) {
touchedFiles.delete((<RenameFile> change).oldUri);
touchedFiles.add((<RenameFile> change).newUri);
}
} else if (kind === 'delete') {
if (touchedFiles.has((<DeleteFile> change).uri)) {
touchedFiles.delete((<DeleteFile> change).uri);
}
} else if (!kind) {
touchedFiles.add((<TextDocumentEdit> change).textDocument.uri);
}
}
}));
}

for (const fileUri of touchedFiles) {
const uri: Uri = Uri.parse(fileUri);
const document: TextDocument = await workspace.openTextDocument(uri);
if (document == null) {
continue;
}

await document.save();
}
}