Skip to content

Commit

Permalink
feat: Support creating new files/folders in Java Project explorer (#742)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdneo authored Apr 12, 2023
1 parent dc2908e commit 169e166
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 34 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%",
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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)/)"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "永久删除",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "永久刪除",
Expand Down
4 changes: 4 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
103 changes: 103 additions & 0 deletions src/explorerCommands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down Expand Up @@ -278,3 +279,105 @@ interface ISourcePath {
projectName: string;
projectType: string;
}

export async function newFile(node: DataNode): Promise<void> {
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<string> => {
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<void> {
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<string> => {
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<string> {
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;
}
}
25 changes: 0 additions & 25 deletions src/views/PrimaryTypeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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 = <IPackageRootNodeData>rootNodeData;
if (data.entryKind === PackageRootKind.K_SOURCE) {
return true;
}

return false;
}

/**
* @returns ProjectNode if the current node is under an unmanaged folder,
* otherwise undefined.
Expand Down
8 changes: 7 additions & 1 deletion src/views/dependencyExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions test/suite/contextValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 169e166

Please sign in to comment.