Skip to content

Commit 169e166

Browse files
authored
feat: Support creating new files/folders in Java Project explorer (#742)
1 parent dc2908e commit 169e166

File tree

10 files changed

+159
-33
lines changed

10 files changed

+159
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99
- Display non-Java files in Java Projects explorer. [#145](https://github.com/microsoft/vscode-java-dependency/issues/145)
1010
- Apply file decorators to project level. [#481](https://github.com/microsoft/vscode-java-dependency/issues/481)
1111
- Give more hints about the project import status. [#580](https://github.com/microsoft/vscode-java-dependency/issues/580)
12+
- Support creating files and folders in Java Projects explorer. [#598](https://github.com/microsoft/vscode-java-dependency/issues/598)
1213

1314
### Fixed
1415
- Apply `files.exclude` to Java Projects explorer. [#214](https://github.com/microsoft/vscode-java-dependency/issues/214)

package.json

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@
180180
"title": "%contributes.commands.java.view.package.newPackage%",
181181
"category": "Java"
182182
},
183+
{
184+
"command": "java.view.package.newFile",
185+
"title": "%contributes.commands.java.view.package.newFile%",
186+
"category": "Java",
187+
"icon": "$(new-file)"
188+
},
189+
{
190+
"command": "java.view.package.newFolder",
191+
"title": "%contributes.commands.java.view.package.newFolder%",
192+
"category": "Java",
193+
"icon": "$(new-folder)"
194+
},
183195
{
184196
"command": "java.view.package.moveFileToTrash",
185197
"title": "%contributes.commands.java.view.package.moveFileToTrash%",
@@ -361,6 +373,14 @@
361373
"command": "java.view.package.newPackage",
362374
"when": "false"
363375
},
376+
{
377+
"command": "java.view.package.newFile",
378+
"when": "false"
379+
},
380+
{
381+
"command": "java.view.package.newFolder",
382+
"when": "false"
383+
},
364384
{
365385
"command": "java.view.package.renameFile",
366386
"when": "false"
@@ -530,7 +550,7 @@
530550
},
531551
{
532552
"submenu": "javaProject.new",
533-
"when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)(?=.*?\\b\\+uri\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/ || viewItem =~ /java:type(?=.*?\\b\\+source\\b)(?=.*?\\b\\+uri\\b)/)",
553+
"when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/",
534554
"group": "1_new@10"
535555
},
536556
{
@@ -590,11 +610,22 @@
590610
"javaProject.new": [
591611
{
592612
"command": "java.view.package.newJavaClass",
593-
"group": "new@10"
613+
"group": "new@10",
614+
"when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)"
594615
},
595616
{
596617
"command": "java.view.package.newPackage",
597-
"group": "new@40"
618+
"group": "new@20",
619+
"when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)"
620+
},
621+
{
622+
"command": "java.view.package.newFile",
623+
"group": "new@30"
624+
},
625+
{
626+
"command": "java.view.package.newFolder",
627+
"group": "new@40",
628+
"when": "view == javaProjectExplorer && (viewItem =~ /java:(file|folder|project)/ || viewItem =~ /java:(packageRoot)(?=.*?\\b\\+resource\\b)/)"
598629
}
599630
]
600631
},

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path",
2222
"contributes.commands.java.view.package.newJavaClass": "Java Class",
2323
"contributes.commands.java.view.package.newPackage": "Package",
24+
"contributes.commands.java.view.package.newFile": "File",
25+
"contributes.commands.java.view.package.newFolder": "Folder",
2426
"contributes.commands.java.view.package.renameFile": "Rename",
2527
"contributes.commands.java.view.package.moveFileToTrash": "Delete",
2628
"contributes.commands.java.view.package.deleteFilePermanently": "Delete Permanently",

package.nls.zh-cn.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径",
2222
"contributes.commands.java.view.package.newJavaClass": "Java 类",
2323
"contributes.commands.java.view.package.newPackage": "",
24+
"contributes.commands.java.view.package.newFile": "文件",
25+
"contributes.commands.java.view.package.newFolder": "文件夹",
2426
"contributes.commands.java.view.package.renameFile": "重命名",
2527
"contributes.commands.java.view.package.moveFileToTrash": "删除",
2628
"contributes.commands.java.view.package.deleteFilePermanently": "永久删除",

package.nls.zh-tw.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"contributes.commands.java.view.package.copyRelativeFilePath": "複製相對路徑",
2222
"contributes.commands.java.view.package.newJavaClass": "Java 類別",
2323
"contributes.commands.java.view.package.newPackage": "套件",
24+
"contributes.commands.java.view.package.newFile": "檔案",
25+
"contributes.commands.java.view.package.newFolder": "資料夾",
2426
"contributes.commands.java.view.package.renameFile": "重新命名",
2527
"contributes.commands.java.view.package.moveFileToTrash": "刪除",
2628
"contributes.commands.java.view.package.deleteFilePermanently": "永久刪除",

src/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export namespace Commands {
4646

4747
export const VIEW_PACKAGE_REVEAL_IN_PROJECT_EXPLORER = "java.view.package.revealInProjectExplorer";
4848

49+
export const VIEW_PACKAGE_NEW_FILE = "java.view.package.newFile";
50+
51+
export const VIEW_PACKAGE_NEW_FOLDER = "java.view.package.newFolder";
52+
4953
export const VIEW_MENUS_FILE_NEW_JAVA_CLASS = "java.view.menus.file.newJavaClass";
5054

5155
export const JAVA_PROJECT_OPEN = "_java.project.open";

src/explorerCommands/new.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { NodeKind } from "../java/nodeData";
1111
import { DataNode } from "../views/dataNode";
1212
import { resourceRoots } from "../views/packageRootNode";
1313
import { checkJavaQualifiedName } from "./utility";
14+
import { sendError, setUserError } from "vscode-extension-telemetry-wrapper";
1415

1516
// TODO: separate to two function to handle creation from menu bar and explorer.
1617
export async function newJavaClass(node?: DataNode): Promise<void> {
@@ -278,3 +279,105 @@ interface ISourcePath {
278279
projectName: string;
279280
projectType: string;
280281
}
282+
283+
export async function newFile(node: DataNode): Promise<void> {
284+
const basePath = getBasePath(node);
285+
if (!basePath) {
286+
window.showErrorMessage("The selected node is invalid.");
287+
return;
288+
}
289+
290+
const fileName: string | undefined = await window.showInputBox({
291+
placeHolder: "Input the file name",
292+
ignoreFocusOut: true,
293+
validateInput: async (value: string): Promise<string> => {
294+
return validateNewFileFolder(basePath, value);
295+
},
296+
});
297+
298+
if (!fileName) {
299+
return;
300+
}
301+
302+
// any continues separator will be deduplicated.
303+
const relativePath = fileName.replace(/[/\\]+/g, path.sep);
304+
const newFilePath = path.join(basePath, relativePath);
305+
await createFile(newFilePath);
306+
}
307+
308+
async function createFile(newFilePath: string) {
309+
fse.createFile(newFilePath, async (err: Error) => {
310+
if (err) {
311+
setUserError(err);
312+
sendError(err);
313+
const choice = await window.showErrorMessage(
314+
err.message || "Failed to create file: " + path.basename(newFilePath),
315+
"Retry"
316+
);
317+
if (choice === "Retry") {
318+
await createFile(newFilePath);
319+
}
320+
} else {
321+
window.showTextDocument(Uri.file(newFilePath));
322+
}
323+
});
324+
}
325+
326+
export async function newFolder(node: DataNode): Promise<void> {
327+
const basePath = getBasePath(node);
328+
if (!basePath) {
329+
window.showErrorMessage("The selected node is invalid.");
330+
return;
331+
}
332+
333+
const folderName: string | undefined = await window.showInputBox({
334+
placeHolder: "Input the folder name",
335+
ignoreFocusOut: true,
336+
validateInput: async (value: string): Promise<string> => {
337+
return validateNewFileFolder(basePath, value);
338+
},
339+
});
340+
341+
if (!folderName) {
342+
return;
343+
}
344+
345+
// any continues separator will be deduplicated.
346+
const relativePath = folderName.replace(/[/\\]+/g, path.sep);
347+
const newFolderPath = path.join(basePath, relativePath);
348+
fse.mkdirs(newFolderPath);
349+
}
350+
351+
async function validateNewFileFolder(basePath: string, relativePath: string): Promise<string> {
352+
relativePath = relativePath.replace(/[/\\]+/g, path.sep);
353+
if (await fse.pathExists(path.join(basePath, relativePath))) {
354+
return "A file or folder already exists in the target location.";
355+
}
356+
357+
return "";
358+
}
359+
360+
function getBasePath(node: DataNode): string | undefined {
361+
if (!node.uri) {
362+
return undefined;
363+
}
364+
365+
const uri: Uri = Uri.parse(node.uri);
366+
if (uri.scheme !== "file") {
367+
return undefined;
368+
}
369+
370+
const nodeKind = node.nodeData.kind;
371+
switch (nodeKind) {
372+
case NodeKind.Project:
373+
case NodeKind.PackageRoot:
374+
case NodeKind.Package:
375+
case NodeKind.Folder:
376+
return Uri.parse(node.uri!).fsPath;
377+
case NodeKind.PrimaryType:
378+
case NodeKind.File:
379+
return path.dirname(Uri.parse(node.uri).fsPath);
380+
default:
381+
return undefined;
382+
}
383+
}

src/views/PrimaryTypeNode.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { DataNode } from "./dataNode";
1212
import { DocumentSymbolNode } from "./documentSymbolNode";
1313
import { ExplorerNode } from "./explorerNode";
1414
import { ProjectNode } from "./projectNode";
15-
import { IPackageRootNodeData, PackageRootKind } from "../java/packageRootNodeData";
1615

1716
export class PrimaryTypeNode extends DataNode {
1817

@@ -122,33 +121,9 @@ export class PrimaryTypeNode extends DataNode {
122121
contextValue += "+test";
123122
}
124123

125-
if (this.belongsToSourceRoot() || this.getUnmanagedFolderAncestor()) {
126-
contextValue += "+source";
127-
}
128-
129124
return contextValue;
130125
}
131126

132-
/**
133-
* Check if the type belongs to a source root. Following conditions can cause the
134-
* result to be false:
135-
* - The type belongs to a jar package
136-
* - The type belongs to an unmanaged folder with '.' as its source root.
137-
*/
138-
private belongsToSourceRoot(): boolean {
139-
const rootNodeData = this._rootNode?.nodeData;
140-
if (!rootNodeData) {
141-
return false;
142-
}
143-
144-
const data = <IPackageRootNodeData>rootNodeData;
145-
if (data.entryKind === PackageRootKind.K_SOURCE) {
146-
return true;
147-
}
148-
149-
return false;
150-
}
151-
152127
/**
153128
* @returns ProjectNode if the current node is under an unmanaged folder,
154129
* otherwise undefined.

src/views/dependencyExplorer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
1313
import { Commands } from "../commands";
1414
import { deleteFiles } from "../explorerCommands/delete";
15-
import { newJavaClass, newPackage } from "../explorerCommands/new";
15+
import { newFile, newFolder, newJavaClass, newPackage } from "../explorerCommands/new";
1616
import { renameFile } from "../explorerCommands/rename";
1717
import { getCmdNode } from "../explorerCommands/utility";
1818
import { Jdtls } from "../java/jdtls";
@@ -110,6 +110,12 @@ export class DependencyExplorer implements Disposable {
110110
instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_CLASS, async (node?: DataNode) => {
111111
newJavaClass(node);
112112
}),
113+
instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_FILE, async (node: DataNode) => {
114+
newFile(node);
115+
}),
116+
instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_FOLDER, async (node: DataNode) => {
117+
newFolder(node);
118+
}),
113119
instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_PACKAGE, async (node?: DataNode) => {
114120
let cmdNode = getCmdNode(this._dependencyViewer.selection, node);
115121
if (!cmdNode) {

test/suite/contextValue.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,19 @@ suite("Context Value Tests", () => {
8989
});
9090

9191
test("test class type node", async function() {
92-
assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await classType.getTreeItem()).contextValue || ""));
92+
assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+uri\b)/.test((await classType.getTreeItem()).contextValue || ""));
9393
});
9494

9595
test("test test-class type node", async function() {
96-
assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+test\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || ""));
96+
assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+test\b)(?=.*?\b\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || ""));
9797
});
9898

9999
test("test enum type node", async function() {
100-
assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || ""));
100+
assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || ""));
101101
});
102102

103103
test("test interface type node", async function() {
104-
assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || ""));
104+
assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || ""));
105105
});
106106

107107
test("test folder node", async function() {

0 commit comments

Comments
 (0)