diff --git a/CHANGELOG.md b/CHANGELOG.md index b9416af7..6dffbd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Display non-Java files in Java Projects explorer. [#145](https://github.com/microsoft/vscode-java-dependency/issues/145) - Apply file decorators to project level. [#481](https://github.com/microsoft/vscode-java-dependency/issues/481) - Give more hints about the project import status. [#580](https://github.com/microsoft/vscode-java-dependency/issues/580) +- Support creating files and folders in Java Projects explorer. [#598](https://github.com/microsoft/vscode-java-dependency/issues/598) ### Fixed - Apply `files.exclude` to Java Projects explorer. [#214](https://github.com/microsoft/vscode-java-dependency/issues/214) diff --git a/package.json b/package.json index 291b67bd..b77b08ae 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,18 @@ "title": "%contributes.commands.java.view.package.newPackage%", "category": "Java" }, + { + "command": "java.view.package.newFile", + "title": "%contributes.commands.java.view.package.newFile%", + "category": "Java", + "icon": "$(new-file)" + }, + { + "command": "java.view.package.newFolder", + "title": "%contributes.commands.java.view.package.newFolder%", + "category": "Java", + "icon": "$(new-folder)" + }, { "command": "java.view.package.moveFileToTrash", "title": "%contributes.commands.java.view.package.moveFileToTrash%", @@ -361,6 +373,14 @@ "command": "java.view.package.newPackage", "when": "false" }, + { + "command": "java.view.package.newFile", + "when": "false" + }, + { + "command": "java.view.package.newFolder", + "when": "false" + }, { "command": "java.view.package.renameFile", "when": "false" @@ -530,7 +550,7 @@ }, { "submenu": "javaProject.new", - "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)/)", + "when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/", "group": "1_new@10" }, { @@ -590,11 +610,22 @@ "javaProject.new": [ { "command": "java.view.package.newJavaClass", - "group": "new@10" + "group": "new@10", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" }, { "command": "java.view.package.newPackage", - "group": "new@40" + "group": "new@20", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" + }, + { + "command": "java.view.package.newFile", + "group": "new@30" + }, + { + "command": "java.view.package.newFolder", + "group": "new@40", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(file|folder|project)/ || viewItem =~ /java:(packageRoot)(?=.*?\\b\\+resource\\b)/)" } ] }, diff --git a/package.nls.json b/package.nls.json index 507e2eaf..75eae2c5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -21,6 +21,8 @@ "contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path", "contributes.commands.java.view.package.newJavaClass": "Java Class", "contributes.commands.java.view.package.newPackage": "Package", + "contributes.commands.java.view.package.newFile": "File", + "contributes.commands.java.view.package.newFolder": "Folder", "contributes.commands.java.view.package.renameFile": "Rename", "contributes.commands.java.view.package.moveFileToTrash": "Delete", "contributes.commands.java.view.package.deleteFilePermanently": "Delete Permanently", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 75a45a6d..3898a11b 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -21,6 +21,8 @@ "contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径", "contributes.commands.java.view.package.newJavaClass": "Java 类", "contributes.commands.java.view.package.newPackage": "包", + "contributes.commands.java.view.package.newFile": "文件", + "contributes.commands.java.view.package.newFolder": "文件夹", "contributes.commands.java.view.package.renameFile": "重命名", "contributes.commands.java.view.package.moveFileToTrash": "删除", "contributes.commands.java.view.package.deleteFilePermanently": "永久删除", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 461bfa00..a8151127 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -21,6 +21,8 @@ "contributes.commands.java.view.package.copyRelativeFilePath": "複製相對路徑", "contributes.commands.java.view.package.newJavaClass": "Java 類別", "contributes.commands.java.view.package.newPackage": "套件", + "contributes.commands.java.view.package.newFile": "檔案", + "contributes.commands.java.view.package.newFolder": "資料夾", "contributes.commands.java.view.package.renameFile": "重新命名", "contributes.commands.java.view.package.moveFileToTrash": "刪除", "contributes.commands.java.view.package.deleteFilePermanently": "永久刪除", diff --git a/src/commands.ts b/src/commands.ts index 0542cda1..baf3151d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -46,6 +46,10 @@ export namespace Commands { export const VIEW_PACKAGE_REVEAL_IN_PROJECT_EXPLORER = "java.view.package.revealInProjectExplorer"; + export const VIEW_PACKAGE_NEW_FILE = "java.view.package.newFile"; + + export const VIEW_PACKAGE_NEW_FOLDER = "java.view.package.newFolder"; + export const VIEW_MENUS_FILE_NEW_JAVA_CLASS = "java.view.menus.file.newJavaClass"; export const JAVA_PROJECT_OPEN = "_java.project.open"; diff --git a/src/explorerCommands/new.ts b/src/explorerCommands/new.ts index 2e17bd62..45837ce0 100644 --- a/src/explorerCommands/new.ts +++ b/src/explorerCommands/new.ts @@ -11,6 +11,7 @@ import { NodeKind } from "../java/nodeData"; import { DataNode } from "../views/dataNode"; import { resourceRoots } from "../views/packageRootNode"; import { checkJavaQualifiedName } from "./utility"; +import { sendError, setUserError } from "vscode-extension-telemetry-wrapper"; // TODO: separate to two function to handle creation from menu bar and explorer. export async function newJavaClass(node?: DataNode): Promise { @@ -278,3 +279,105 @@ interface ISourcePath { projectName: string; projectType: string; } + +export async function newFile(node: DataNode): Promise { + const basePath = getBasePath(node); + if (!basePath) { + window.showErrorMessage("The selected node is invalid."); + return; + } + + const fileName: string | undefined = await window.showInputBox({ + placeHolder: "Input the file name", + ignoreFocusOut: true, + validateInput: async (value: string): Promise => { + return validateNewFileFolder(basePath, value); + }, + }); + + if (!fileName) { + return; + } + + // any continues separator will be deduplicated. + const relativePath = fileName.replace(/[/\\]+/g, path.sep); + const newFilePath = path.join(basePath, relativePath); + await createFile(newFilePath); +} + +async function createFile(newFilePath: string) { + fse.createFile(newFilePath, async (err: Error) => { + if (err) { + setUserError(err); + sendError(err); + const choice = await window.showErrorMessage( + err.message || "Failed to create file: " + path.basename(newFilePath), + "Retry" + ); + if (choice === "Retry") { + await createFile(newFilePath); + } + } else { + window.showTextDocument(Uri.file(newFilePath)); + } + }); +} + +export async function newFolder(node: DataNode): Promise { + const basePath = getBasePath(node); + if (!basePath) { + window.showErrorMessage("The selected node is invalid."); + return; + } + + const folderName: string | undefined = await window.showInputBox({ + placeHolder: "Input the folder name", + ignoreFocusOut: true, + validateInput: async (value: string): Promise => { + return validateNewFileFolder(basePath, value); + }, + }); + + if (!folderName) { + return; + } + + // any continues separator will be deduplicated. + const relativePath = folderName.replace(/[/\\]+/g, path.sep); + const newFolderPath = path.join(basePath, relativePath); + fse.mkdirs(newFolderPath); +} + +async function validateNewFileFolder(basePath: string, relativePath: string): Promise { + relativePath = relativePath.replace(/[/\\]+/g, path.sep); + if (await fse.pathExists(path.join(basePath, relativePath))) { + return "A file or folder already exists in the target location."; + } + + return ""; +} + +function getBasePath(node: DataNode): string | undefined { + if (!node.uri) { + return undefined; + } + + const uri: Uri = Uri.parse(node.uri); + if (uri.scheme !== "file") { + return undefined; + } + + const nodeKind = node.nodeData.kind; + switch (nodeKind) { + case NodeKind.Project: + case NodeKind.PackageRoot: + case NodeKind.Package: + case NodeKind.Folder: + return Uri.parse(node.uri!).fsPath; + case NodeKind.PrimaryType: + case NodeKind.File: + return path.dirname(Uri.parse(node.uri).fsPath); + default: + return undefined; + } +} diff --git a/src/views/PrimaryTypeNode.ts b/src/views/PrimaryTypeNode.ts index e7182337..3b5540ca 100644 --- a/src/views/PrimaryTypeNode.ts +++ b/src/views/PrimaryTypeNode.ts @@ -12,7 +12,6 @@ import { DataNode } from "./dataNode"; import { DocumentSymbolNode } from "./documentSymbolNode"; import { ExplorerNode } from "./explorerNode"; import { ProjectNode } from "./projectNode"; -import { IPackageRootNodeData, PackageRootKind } from "../java/packageRootNodeData"; export class PrimaryTypeNode extends DataNode { @@ -122,33 +121,9 @@ export class PrimaryTypeNode extends DataNode { contextValue += "+test"; } - if (this.belongsToSourceRoot() || this.getUnmanagedFolderAncestor()) { - contextValue += "+source"; - } - return contextValue; } - /** - * Check if the type belongs to a source root. Following conditions can cause the - * result to be false: - * - The type belongs to a jar package - * - The type belongs to an unmanaged folder with '.' as its source root. - */ - private belongsToSourceRoot(): boolean { - const rootNodeData = this._rootNode?.nodeData; - if (!rootNodeData) { - return false; - } - - const data = rootNodeData; - if (data.entryKind === PackageRootKind.K_SOURCE) { - return true; - } - - return false; - } - /** * @returns ProjectNode if the current node is under an unmanaged folder, * otherwise undefined. diff --git a/src/views/dependencyExplorer.ts b/src/views/dependencyExplorer.ts index 117fe031..3e401df3 100644 --- a/src/views/dependencyExplorer.ts +++ b/src/views/dependencyExplorer.ts @@ -12,7 +12,7 @@ import { import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; import { Commands } from "../commands"; import { deleteFiles } from "../explorerCommands/delete"; -import { newJavaClass, newPackage } from "../explorerCommands/new"; +import { newFile, newFolder, newJavaClass, newPackage } from "../explorerCommands/new"; import { renameFile } from "../explorerCommands/rename"; import { getCmdNode } from "../explorerCommands/utility"; import { Jdtls } from "../java/jdtls"; @@ -110,6 +110,12 @@ export class DependencyExplorer implements Disposable { instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_CLASS, async (node?: DataNode) => { newJavaClass(node); }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_FILE, async (node: DataNode) => { + newFile(node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_FOLDER, async (node: DataNode) => { + newFolder(node); + }), instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_PACKAGE, async (node?: DataNode) => { let cmdNode = getCmdNode(this._dependencyViewer.selection, node); if (!cmdNode) { diff --git a/test/suite/contextValue.test.ts b/test/suite/contextValue.test.ts index 061dde48..9fa1e1d5 100644 --- a/test/suite/contextValue.test.ts +++ b/test/suite/contextValue.test.ts @@ -89,19 +89,19 @@ suite("Context Value Tests", () => { }); test("test class type node", async function() { - assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await classType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+uri\b)/.test((await classType.getTreeItem()).contextValue || "")); }); test("test test-class type node", async function() { - assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+test\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+test\b)(?=.*?\b\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || "")); }); test("test enum type node", async function() { - assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || "")); }); test("test interface type node", async function() { - assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || "")); }); test("test folder node", async function() {