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

feat: Support drag and drop inside Java Project explorer #634

Merged
merged 2 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 178 additions & 5 deletions src/views/DragAndDropController.ts
Original file line number Diff line number Diff line change
@@ -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<ExplorerNode> {

Expand All @@ -16,17 +25,31 @@ export class DragAndDropController implements TreeDragAndDropController<Explorer
];
dragMimeTypes: string[] = [
Explorer.Mime.TextUriList,
];;
];

public handleDrag(source: ExplorerNode[], treeDataTransfer: DataTransfer): void {
// select many is not supported yet
let dragItem = source[0];
const dragItem = source[0];
this.addDragToEditorDataTransfer(dragItem, treeDataTransfer);
this.addInternalDragDataTransfer(dragItem, treeDataTransfer);
}

public async handleDrop(target: ExplorerNode | undefined, dataTransfer: DataTransfer): Promise<void> {
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) {
Expand All @@ -37,4 +60,154 @@ export class DragAndDropController implements TreeDragAndDropController<Explorer
}
}
}

/**
* Add data transfer that is used when node is dropped into other Java Project Explorer node.
* @param node node being dragged.
* @param treeDataTransfer A map containing a mapping of the mime type of the corresponding transferred data.
*/
private addInternalDragDataTransfer(node: ExplorerNode, treeDataTransfer: DataTransfer): void {
// draggable node must have uri
if (!(node instanceof DataNode) || !node.uri) {
return;
}

// whether the node can be dropped will be check in handleDrop(...)
treeDataTransfer.set(Explorer.Mime.JavaProjectExplorer, new DataTransferItem(node.uri));
}

/**
* Handle the DnD event which comes from Java Project explorer itself.
* @param target the drop node.
* @param uri uri in the data transfer.
*/
public async dropFromJavaProjectExplorer(target: ExplorerNode | undefined, uri: string): Promise<void> {
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 {
jdneo marked this conversation as resolved.
Show resolved Hide resolved
// 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<void> {
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)}'?`,
jdneo marked this conversation as resolved.
Show resolved Hide resolved
{ 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);
}
}
}
35 changes: 21 additions & 14 deletions src/views/containerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<INodeData[]> {
return Jdtls.getPackageData({ kind: NodeKind.Container, projectUri: this._project.uri, path: this.path });
}
Expand All @@ -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}`;
}
Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions src/views/explorerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/views/packageRootNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class PackageRootNode extends DataNode {
super(nodeData, parent);
}

public isSourceRoot(): boolean {
return (<IPackageRootNodeData>this.nodeData).entryKind === PackageRootKind.K_SOURCE;
}

protected async loadData(): Promise<INodeData[]> {
return Jdtls.getPackageData({
kind: NodeKind.PackageRoot,
Expand Down
10 changes: 10 additions & 0 deletions src/views/projectNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?.[NATURE_ID] || [];
for (const natureId of natureIds) {
if (natureId === NatureId.UnmanagedFolder) {
return true;
}
}
return false;
}

protected async loadData(): Promise<INodeData[]> {
let result: INodeData[] = [];
return Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: this.nodeData.uri }).then((res) => {
Expand Down