Skip to content

Commit 16d35e8

Browse files
authored
feat: Support drag and drop inside Java Project explorer (#634)
Signed-off-by: sheche <[email protected]>
1 parent 2a6c0fc commit 16d35e8

File tree

5 files changed

+216
-21
lines changed

5 files changed

+216
-21
lines changed

src/views/DragAndDropController.ts

+178-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
import { DataTransfer, DataTransferItem, TreeDragAndDropController } from "vscode";
4+
import * as path from "path";
5+
import { commands, DataTransfer, DataTransferItem, TreeDragAndDropController, Uri, window, workspace, WorkspaceEdit } from "vscode";
6+
import { Commands } from "../commands";
57
import { Explorer } from "../constants";
68
import { BaseSymbolNode } from "./baseSymbolNode";
9+
import { ContainerNode, ContainerType } from "./containerNode";
710
import { DataNode } from "./dataNode";
811
import { ExplorerNode } from "./explorerNode";
912
import { FileNode } from "./fileNode";
13+
import { FolderNode } from "./folderNode";
14+
import { explorerNodeCache } from "./nodeCache/explorerNodeCache";
15+
import { PackageNode } from "./packageNode";
16+
import { PackageRootNode } from "./packageRootNode";
1017
import { PrimaryTypeNode } from "./PrimaryTypeNode";
18+
import { ProjectNode } from "./projectNode";
19+
import { WorkspaceNode } from "./workspaceNode";
1120

1221
export class DragAndDropController implements TreeDragAndDropController<ExplorerNode> {
1322

@@ -16,17 +25,31 @@ export class DragAndDropController implements TreeDragAndDropController<Explorer
1625
];
1726
dragMimeTypes: string[] = [
1827
Explorer.Mime.TextUriList,
19-
];;
28+
];
2029

2130
public handleDrag(source: ExplorerNode[], treeDataTransfer: DataTransfer): void {
2231
// select many is not supported yet
23-
let dragItem = source[0];
32+
const dragItem = source[0];
2433
this.addDragToEditorDataTransfer(dragItem, treeDataTransfer);
34+
this.addInternalDragDataTransfer(dragItem, treeDataTransfer);
35+
}
36+
37+
public async handleDrop(target: ExplorerNode | undefined, dataTransfer: DataTransfer): Promise<void> {
38+
const data = dataTransfer.get(Explorer.Mime.JavaProjectExplorer);
39+
if (data) {
40+
await this.dropFromJavaProjectExplorer(target, data.value);
41+
return;
42+
}
2543
}
2644

45+
/**
46+
* Add data transfer that is used when node is dropped to the editor.
47+
* @param node node being dragged.
48+
* @param treeDataTransfer A map containing a mapping of the mime type of the corresponding transferred data.
49+
*/
2750
private addDragToEditorDataTransfer(node: ExplorerNode, treeDataTransfer: DataTransfer) {
28-
if ((node instanceof PrimaryTypeNode || node instanceof FileNode) && (node as DataNode).uri) {
29-
treeDataTransfer.set(Explorer.Mime.TextUriList, new DataTransferItem((node as DataNode).uri));
51+
if ((node instanceof PrimaryTypeNode || node instanceof FileNode) && node.uri) {
52+
treeDataTransfer.set(Explorer.Mime.TextUriList, new DataTransferItem(node.uri));
3053
} else if ((node instanceof BaseSymbolNode)) {
3154
const parent = (node.getParent() as PrimaryTypeNode);
3255
if (parent.uri) {
@@ -37,4 +60,154 @@ export class DragAndDropController implements TreeDragAndDropController<Explorer
3760
}
3861
}
3962
}
63+
64+
/**
65+
* Add data transfer that is used when node is dropped into other Java Project Explorer node.
66+
* @param node node being dragged.
67+
* @param treeDataTransfer A map containing a mapping of the mime type of the corresponding transferred data.
68+
*/
69+
private addInternalDragDataTransfer(node: ExplorerNode, treeDataTransfer: DataTransfer): void {
70+
// draggable node must have uri
71+
if (!(node instanceof DataNode) || !node.uri) {
72+
return;
73+
}
74+
75+
// whether the node can be dropped will be check in handleDrop(...)
76+
treeDataTransfer.set(Explorer.Mime.JavaProjectExplorer, new DataTransferItem(node.uri));
77+
}
78+
79+
/**
80+
* Handle the DnD event which comes from Java Project explorer itself.
81+
* @param target the drop node.
82+
* @param uri uri in the data transfer.
83+
*/
84+
public async dropFromJavaProjectExplorer(target: ExplorerNode | undefined, uri: string): Promise<void> {
85+
const source: DataNode | undefined = explorerNodeCache.getDataNode(Uri.parse(uri));
86+
if (!this.isDraggableNode(source)) {
87+
return;
88+
}
89+
90+
if (!this.isDroppableNode(target)) {
91+
return;
92+
}
93+
94+
// check if the target node is source node itself or its parent.
95+
if (target?.isItselfOrAncestorOf(source, 1 /*levelToCheck*/)) {
96+
return;
97+
}
98+
99+
if (target instanceof ContainerNode) {
100+
if (target.getContainerType() !== ContainerType.ReferencedLibrary) {
101+
return;
102+
}
103+
104+
if (!(target.getParent() as ProjectNode).isUnmanagedFolder()) {
105+
return;
106+
}
107+
108+
// TODO: referenced library
109+
} else if (target instanceof PackageRootNode || target instanceof PackageNode
110+
|| target instanceof FolderNode) {
111+
await this.move(source!, target);
112+
}
113+
}
114+
115+
/**
116+
* Check whether the dragged node is draggable.
117+
* @param node the dragged node.
118+
*/
119+
private isDraggableNode(node: DataNode | undefined): boolean {
120+
if (!node?.uri) {
121+
return false;
122+
}
123+
if (node instanceof WorkspaceNode || node instanceof ProjectNode
124+
|| node instanceof PackageRootNode || node instanceof ContainerNode
125+
|| node instanceof BaseSymbolNode) {
126+
return false;
127+
}
128+
129+
return this.isUnderSourceRoot(node);
130+
}
131+
132+
/**
133+
* Check whether the node is under source root.
134+
*
135+
* Note: There is one exception: The primary type directly under an unmanaged folder project,
136+
* in that case, `true` is returned.
137+
* @param node DataNode
138+
*/
139+
private isUnderSourceRoot(node: DataNode): boolean {
140+
let parent = node.getParent();
141+
while (parent) {
142+
if (parent instanceof ContainerNode) {
143+
return false;
144+
}
145+
146+
if (parent instanceof PackageRootNode) {
147+
return parent.isSourceRoot();
148+
}
149+
parent = parent.getParent();
150+
}
151+
return true;
152+
}
153+
154+
/**
155+
* Check whether the node is able to be dropped.
156+
*/
157+
private isDroppableNode(node: ExplorerNode | undefined): boolean {
158+
// drop to root is not supported yet
159+
if (!node) {
160+
return false;
161+
}
162+
163+
if (node instanceof DataNode && !node.uri) {
164+
return false;
165+
}
166+
167+
if (node instanceof WorkspaceNode || node instanceof ProjectNode
168+
|| node instanceof BaseSymbolNode) {
169+
return false;
170+
}
171+
172+
let parent: ExplorerNode | undefined = node;
173+
while (parent) {
174+
if (parent instanceof ProjectNode) {
175+
return false;
176+
} else if (parent instanceof PackageRootNode) {
177+
return parent.isSourceRoot();
178+
} else if (parent instanceof ContainerNode) {
179+
if (parent.getContainerType() === ContainerType.ReferencedLibrary) {
180+
return (parent.getParent() as ProjectNode).isUnmanagedFolder();
181+
}
182+
return false;
183+
}
184+
parent = parent.getParent();
185+
}
186+
return false;
187+
}
188+
189+
/**
190+
* Trigger a workspace edit that move the source node into the target node.
191+
*/
192+
private async move(source: DataNode, target: DataNode): Promise<void> {
193+
const sourceUri = Uri.parse(source.uri!);
194+
const targetUri = Uri.parse(target.uri!);
195+
if (sourceUri === targetUri) {
196+
return;
197+
}
198+
199+
const newPath = path.join(targetUri.fsPath, path.basename(sourceUri.fsPath));
200+
const choice = await window.showInformationMessage(
201+
`Are you sure you want to move '${path.basename(sourceUri.fsPath)}' into '${path.basename(targetUri.fsPath)}'?`,
202+
{ modal: true },
203+
"Move",
204+
);
205+
206+
if (choice === "Move") {
207+
const edit = new WorkspaceEdit();
208+
edit.renameFile(sourceUri, Uri.file(newPath));
209+
await workspace.applyEdit(edit);
210+
commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH, /* debounce = */true);
211+
}
212+
}
40213
}

src/views/containerNode.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ export class ContainerNode extends DataNode {
2020
return this._project.uri && Uri.parse(this._project.uri).fsPath;
2121
}
2222

23+
public getContainerType(): string {
24+
const containerPath: string = this._nodeData.path || "";
25+
if (containerPath.startsWith(ContainerPath.JRE)) {
26+
return ContainerType.JRE;
27+
} else if (containerPath.startsWith(ContainerPath.Maven)) {
28+
return ContainerType.Maven;
29+
} else if (containerPath.startsWith(ContainerPath.Gradle)) {
30+
return ContainerType.Gradle;
31+
} else if (containerPath.startsWith(ContainerPath.ReferencedLibrary)) {
32+
return ContainerType.ReferencedLibrary;
33+
}
34+
return ContainerType.Unknown;
35+
}
36+
2337
protected async loadData(): Promise<INodeData[]> {
2438
return Jdtls.getPackageData({ kind: NodeKind.Container, projectUri: this._project.uri, path: this.path });
2539
}
@@ -36,7 +50,7 @@ export class ContainerNode extends DataNode {
3650

3751
protected get contextValue(): string {
3852
let contextValue: string = Explorer.ContextValueType.Container;
39-
const containerType: string = getContainerType(this._nodeData.path);
53+
const containerType: string = this.getContainerType();
4054
if (containerType) {
4155
contextValue += `+${containerType}`;
4256
}
@@ -48,19 +62,12 @@ export class ContainerNode extends DataNode {
4862
}
4963
}
5064

51-
function getContainerType(containerPath: string | undefined): string {
52-
if (!containerPath) {
53-
return "";
54-
} else if (containerPath.startsWith(ContainerPath.JRE)) {
55-
return "jre";
56-
} else if (containerPath.startsWith(ContainerPath.Maven)) {
57-
return "maven";
58-
} else if (containerPath.startsWith(ContainerPath.Gradle)) {
59-
return "gradle";
60-
} else if (containerPath.startsWith(ContainerPath.ReferencedLibrary)) {
61-
return "referencedLibrary";
62-
}
63-
return "";
65+
export enum ContainerType {
66+
JRE = "jre",
67+
Maven = "maven",
68+
Gradle = "gradle",
69+
ReferencedLibrary = "referencedLibrary",
70+
Unknown = "",
6471
}
6572

6673
const enum ContainerPath {

src/views/explorerNode.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ export abstract class ExplorerNode {
1212
return this._parent;
1313
}
1414

15-
public isItselfOrAncestorOf(node: ExplorerNode | undefined | null) {
16-
while (node) {
15+
public isItselfOrAncestorOf(node: ExplorerNode | undefined | null, levelToCheck: number = Number.MAX_VALUE) {
16+
while (node && levelToCheck >= 0) {
1717
if (this === node) {
1818
return true;
1919
}
2020
node = node.getParent();
21+
levelToCheck--;
2122
}
2223

2324
return false;

src/views/packageRootNode.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class PackageRootNode extends DataNode {
2323
super(nodeData, parent);
2424
}
2525

26+
public isSourceRoot(): boolean {
27+
return (<IPackageRootNodeData>this.nodeData).entryKind === PackageRootKind.K_SOURCE;
28+
}
29+
2630
protected async loadData(): Promise<INodeData[]> {
2731
return Jdtls.getPackageData({
2832
kind: NodeKind.PackageRoot,

src/views/projectNode.ts

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ export class ProjectNode extends DataNode {
5252
return (childNode && paths.length > 0) ? childNode.revealPaths(paths) : childNode;
5353
}
5454

55+
public isUnmanagedFolder(): boolean {
56+
const natureIds: string[] = this.nodeData.metaData?.[NATURE_ID] || [];
57+
for (const natureId of natureIds) {
58+
if (natureId === NatureId.UnmanagedFolder) {
59+
return true;
60+
}
61+
}
62+
return false;
63+
}
64+
5565
protected async loadData(): Promise<INodeData[]> {
5666
let result: INodeData[] = [];
5767
return Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: this.nodeData.uri }).then((res) => {

0 commit comments

Comments
 (0)