From c376656456a3906cf11cf30334135237fd48a43b Mon Sep 17 00:00:00 2001 From: sheche Date: Mon, 13 Jun 2022 14:51:03 +0800 Subject: [PATCH 1/2] feat: Support drag and drop inside Java Project explorer Signed-off-by: sheche --- src/views/DragAndDropController.ts | 181 ++++++++++++++++++++++++++++- src/views/containerNode.ts | 35 +++--- src/views/explorerNode.ts | 5 +- src/views/packageRootNode.ts | 4 + src/views/projectNode.ts | 10 ++ 5 files changed, 215 insertions(+), 20 deletions(-) diff --git a/src/views/DragAndDropController.ts b/src/views/DragAndDropController.ts index 8a630ebc..7917ff15 100644 --- a/src/views/DragAndDropController.ts +++ b/src/views/DragAndDropController.ts @@ -1,13 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { DataTransfer, DataTransferItem, TreeDragAndDropController } from "vscode"; +import * as path from "path"; +import { commands, DataTransfer, DataTransferItem, TreeDragAndDropController, Uri, window, workspace, WorkspaceEdit } from "vscode"; +import { Commands } from "../commands"; import { Explorer } from "../constants"; import { BaseSymbolNode } from "./baseSymbolNode"; +import { ContainerNode, ContainerType } from "./containerNode"; import { DataNode } from "./dataNode"; import { ExplorerNode } from "./explorerNode"; import { FileNode } from "./fileNode"; +import { FolderNode } from "./folderNode"; +import { explorerNodeCache } from "./nodeCache/explorerNodeCache"; +import { PackageNode } from "./packageNode"; +import { PackageRootNode } from "./packageRootNode"; import { PrimaryTypeNode } from "./PrimaryTypeNode"; +import { ProjectNode } from "./projectNode"; +import { WorkspaceNode } from "./workspaceNode"; export class DragAndDropController implements TreeDragAndDropController { @@ -16,17 +25,31 @@ export class DragAndDropController implements TreeDragAndDropController { + const data = dataTransfer.get(Explorer.Mime.JavaProjectExplorer); + if (data) { + await this.dropFromJavaProjectExplorer(target, data.value); + return; + } } + /** + * Add data transfer that is used when node is dropped to the editor. + * @param node node being dragged. + * @param treeDataTransfer A map containing a mapping of the mime type of the corresponding transferred data. + */ private addDragToEditorDataTransfer(node: ExplorerNode, treeDataTransfer: DataTransfer) { - if ((node instanceof PrimaryTypeNode || node instanceof FileNode) && (node as DataNode).uri) { - treeDataTransfer.set(Explorer.Mime.TextUriList, new DataTransferItem((node as DataNode).uri)); + if ((node instanceof PrimaryTypeNode || node instanceof FileNode) && node.uri) { + treeDataTransfer.set(Explorer.Mime.TextUriList, new DataTransferItem(node.uri)); } else if ((node instanceof BaseSymbolNode)) { const parent = (node.getParent() as PrimaryTypeNode); if (parent.uri) { @@ -37,4 +60,154 @@ export class DragAndDropController implements TreeDragAndDropController { + const source: DataNode | undefined = explorerNodeCache.getDataNode(Uri.parse(uri)); + if (!this.isDraggableNode(source)) { + return; + } + + if (!this.isDroppableNode(target)) { + return; + } + + // check if the target node is source node itself or its parent. + if (target?.isItselfOrAncestorOf(source, 1 /*levelToCheck*/)) { + return; + } + + if (target instanceof ContainerNode) { + if (target.getContainerType() !== ContainerType.ReferencedLibrary) { + return; + } + + if (!(target.getParent() as ProjectNode).isUnmanagedFolder()) { + return; + } + + // TODO: referenced library + } else if (target instanceof PackageRootNode || target instanceof PackageNode + || target instanceof FolderNode) { + await this.move(source!, target); + } + } + + /** + * Check whether the dragged node is draggable. + * @param node the dragged node. + */ + private isDraggableNode(node: DataNode | undefined): boolean { + if (!node?.uri) { + return false; + } + if (node instanceof WorkspaceNode || node instanceof ProjectNode + || node instanceof PackageRootNode || node instanceof ContainerNode + || node instanceof BaseSymbolNode) { + return false; + } + + return this.isUnderSourceRoot(node); + } + + /** + * Check whether the node is under source root. + * + * Note: There is one exception: The primary type directly under an unmanaged folder project, + * in that case, `true` is returned. + * @param node DataNode + */ + private isUnderSourceRoot(node: DataNode): boolean { + let parent = node.getParent(); + while (parent) { + if (parent instanceof ContainerNode) { + return false; + } + + if (parent instanceof PackageRootNode) { + return parent.isSourceRoot(); + } + parent = parent.getParent(); + } + return true; + } + + /** + * Check whether the node is able to be dropped. + */ + private isDroppableNode(node: ExplorerNode | undefined): boolean { + // drop to root is not supported yet + if (!node) { + return false; + } + + if (node instanceof DataNode && !node.uri) { + return false; + } + + if (node instanceof WorkspaceNode || node instanceof ProjectNode + || node instanceof BaseSymbolNode) { + return false; + } + + let parent: ExplorerNode | undefined = node; + while (parent) { + if (parent instanceof ProjectNode) { + return false; + } else if (parent instanceof PackageRootNode) { + return parent.isSourceRoot(); + } else if (parent instanceof ContainerNode) { + if (parent.getContainerType() === ContainerType.ReferencedLibrary) { + return (parent.getParent() as ProjectNode).isUnmanagedFolder(); + } + return false; + } + parent = parent.getParent(); + } + return false; + } + + /** + * Trigger a workspace edit that move the source node into the target node. + */ + private async move(source: DataNode, target: DataNode): Promise { + const sourceUri = Uri.parse(source.uri!); + const targetUri = Uri.parse(target.uri!); + if (sourceUri === targetUri) { + return; + } + + const newPath = path.join(targetUri.fsPath, path.basename(sourceUri.fsPath)); + const choice = await window.showInformationMessage( + `Are you sure you want to move '${path.basename(sourceUri.fsPath)}' into '${path.basename(targetUri.fsPath)}'?`, + { modal: true }, + "Move", + ); + + if (choice === "Move") { + const edit = new WorkspaceEdit(); + edit.renameFile(sourceUri, Uri.file(newPath)); + await workspace.applyEdit(edit); + commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH, /* debounce = */true); + } + } } diff --git a/src/views/containerNode.ts b/src/views/containerNode.ts index 664feb7b..c85c98d9 100644 --- a/src/views/containerNode.ts +++ b/src/views/containerNode.ts @@ -20,6 +20,20 @@ export class ContainerNode extends DataNode { return this._project.uri && Uri.parse(this._project.uri).fsPath; } + public getContainerType(): string { + const containerPath: string = this._nodeData.path || ""; + if (containerPath.startsWith(ContainerPath.JRE)) { + return ContainerType.JRE; + } else if (containerPath.startsWith(ContainerPath.Maven)) { + return ContainerType.Maven; + } else if (containerPath.startsWith(ContainerPath.Gradle)) { + return ContainerType.Gradle; + } else if (containerPath.startsWith(ContainerPath.ReferencedLibrary)) { + return ContainerType.ReferencedLibrary; + } + return ContainerType.Unknown; + } + protected async loadData(): Promise { return Jdtls.getPackageData({ kind: NodeKind.Container, projectUri: this._project.uri, path: this.path }); } @@ -36,7 +50,7 @@ export class ContainerNode extends DataNode { protected get contextValue(): string { let contextValue: string = Explorer.ContextValueType.Container; - const containerType: string = getContainerType(this._nodeData.path); + const containerType: string = this.getContainerType(); if (containerType) { contextValue += `+${containerType}`; } @@ -48,19 +62,12 @@ export class ContainerNode extends DataNode { } } -function getContainerType(containerPath: string | undefined): string { - if (!containerPath) { - return ""; - } else if (containerPath.startsWith(ContainerPath.JRE)) { - return "jre"; - } else if (containerPath.startsWith(ContainerPath.Maven)) { - return "maven"; - } else if (containerPath.startsWith(ContainerPath.Gradle)) { - return "gradle"; - } else if (containerPath.startsWith(ContainerPath.ReferencedLibrary)) { - return "referencedLibrary"; - } - return ""; +export enum ContainerType { + JRE = "jre", + Maven = "maven", + Gradle = "gradle", + ReferencedLibrary = "referencedLibrary", + Unknown = "", } const enum ContainerPath { diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts index 18c0e79e..c892a1d6 100644 --- a/src/views/explorerNode.ts +++ b/src/views/explorerNode.ts @@ -12,12 +12,13 @@ export abstract class ExplorerNode { return this._parent; } - public isItselfOrAncestorOf(node: ExplorerNode | undefined | null) { - while (node) { + public isItselfOrAncestorOf(node: ExplorerNode | undefined | null, levelToCheck: number = Number.MAX_VALUE) { + while (node && levelToCheck >= 0) { if (this === node) { return true; } node = node.getParent(); + levelToCheck--; } return false; diff --git a/src/views/packageRootNode.ts b/src/views/packageRootNode.ts index ef98f72e..90a6bd29 100644 --- a/src/views/packageRootNode.ts +++ b/src/views/packageRootNode.ts @@ -23,6 +23,10 @@ export class PackageRootNode extends DataNode { super(nodeData, parent); } + public isSourceRoot(): boolean { + return (this.nodeData).entryKind === PackageRootKind.K_SOURCE; + } + protected async loadData(): Promise { return Jdtls.getPackageData({ kind: NodeKind.PackageRoot, diff --git a/src/views/projectNode.ts b/src/views/projectNode.ts index 52fd5b2c..d77c9d80 100644 --- a/src/views/projectNode.ts +++ b/src/views/projectNode.ts @@ -52,6 +52,16 @@ export class ProjectNode extends DataNode { return (childNode && paths.length > 0) ? childNode.revealPaths(paths) : childNode; } + public isUnmanagedFolder(): boolean { + const natureIds: string[] = this.nodeData.metaData?.["NatureId"] || []; + for (const natureId of natureIds) { + if (natureId === NatureId.UnmanagedFolder) { + return true; + } + } + return false; + } + protected async loadData(): Promise { let result: INodeData[] = []; return Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: this.nodeData.uri }).then((res) => { From b61231f4223383e4d6230f83cc2a16871597aee5 Mon Sep 17 00:00:00 2001 From: sheche Date: Tue, 14 Jun 2022 11:32:10 +0800 Subject: [PATCH 2/2] Fix linting errors Signed-off-by: sheche --- src/views/DragAndDropController.ts | 4 ++-- src/views/projectNode.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/DragAndDropController.ts b/src/views/DragAndDropController.ts index 7917ff15..fca6ffc1 100644 --- a/src/views/DragAndDropController.ts +++ b/src/views/DragAndDropController.ts @@ -29,7 +29,7 @@ export class DragAndDropController implements TreeDragAndDropController