diff --git a/src/extension.ts b/src/extension.ts index c003cfd9b..75266d701 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -80,6 +80,7 @@ export function activate(context: ExtensionContext): Promise { generateConstructorsPromptSupport: true, generateDelegateMethodsPromptSupport: true, advancedExtractRefactoringSupport: true, + moveRefactoringSupport: true, }, triggerFiles: getTriggerFiles() }, diff --git a/src/protocol.ts b/src/protocol.ts index aad0512cb..01850e976 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -288,6 +288,7 @@ export interface RenamePosition { export interface RefactorWorkspaceEdit { edit: WorkspaceEdit; command?: Command; + errorMessage?: string; } export interface GetRefactorEditParams { @@ -300,3 +301,32 @@ export interface GetRefactorEditParams { export namespace GetRefactorEditRequest { export const type = new RequestType('java/getRefactorEdit'); } + +export interface PackageNode { + displayName: string; + uri: string; + path: string; + project: string; + isDefaultPackage: boolean; + isParentOfSelectedFile: boolean; +} + +export interface MoveParams { + moveKind: string; + sourceUris: string[]; + params: CodeActionParams; + destination?: any; + updateReferences?: boolean; +} + +export interface MoveDestinationsResponse { + destinations: any[]; +} + +export namespace GetMoveDestinationsRequest { + export const type = new RequestType('java/getMoveDestinations'); +} + +export namespace MoveRequest { + export const type = new RequestType('java/move'); +} diff --git a/src/refactorAction.ts b/src/refactorAction.ts index 913b829ae..dadc8c2ef 100644 --- a/src/refactorAction.ts +++ b/src/refactorAction.ts @@ -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 { GetRefactorEditRequest, MoveRequest, RefactorWorkspaceEdit, RenamePosition, GetMoveDestinationsRequest } from './protocol'; export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) { registerApplyRefactorCommand(languageClient, context); @@ -73,22 +75,172 @@ 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 from different directories are not supported. Please make sure they are from the same directory."); + return; + } + + const moveDestinations = await languageClient.sendRequest(GetMoveDestinationsRequest.type, { + moveKind: 'moveResource', + sourceUris: fileUris.map(uri => uri.toString()) + }); + if (!moveDestinations || !moveDestinations.destinations || !moveDestinations.destinations.length) { + window.showErrorMessage("Cannot find available Java packages to move the selected files to."); + return; + } + + const packageNodeItems = moveDestinations.destinations.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(MoveRequest.type, { + moveKind: 'moveResource', + sourceUris: fileUris.map(uri => uri.toString()), + params: null, + destination: selectPackageNodeItem.packageNode, + 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 = new Set(); + 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 = ( change).kind; + if (kind === 'rename') { + if (touchedFiles.has(( change).oldUri)) { + touchedFiles.delete(( change).oldUri); + touchedFiles.add(( change).newUri); } + } else if (kind === 'delete') { + if (touchedFiles.has(( change).uri)) { + touchedFiles.delete(( change).uri); + } + } else if (!kind) { + touchedFiles.add(( 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(); + } }