diff --git a/package.json b/package.json index a7ce21fa..291b67bd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "explorer" ], "engines": { - "vscode": "^1.69.0" + "vscode": "^1.75.0" }, "repository": { "type": "git", @@ -39,10 +39,7 @@ "workspaceContains:settings.gradle.kts", "workspaceContains:*/settings.gradle.kts", "workspaceContains:.classpath", - "workspaceContains:*/.classpath", - "onCommand:_java.project.open", - "onCommand:java.project.create", - "onCommand:java.view.package.newJavaClass" + "workspaceContains:*/.classpath" ], "license": "MIT", "main": "./main.js", @@ -166,6 +163,12 @@ "title": "%contributes.commands.java.view.package.copyRelativeFilePath%", "category": "Java" }, + { + "command": "java.view.menus.file.newJavaClass", + "title": "%contributes.commands.java.view.menus.file.newJavaClass%", + "category": "Java", + "icon": "$(add)" + }, { "command": "java.view.package.newJavaClass", "title": "%contributes.commands.java.view.package.newJavaClass%", @@ -290,7 +293,7 @@ "menus": { "file/newFile": [ { - "command": "java.view.package.newJavaClass" + "command": "java.view.menus.file.newJavaClass" } ], "commandPalette": [ @@ -350,6 +353,10 @@ "command": "java.project.refreshLibraries", "when": "false" }, + { + "command": "java.view.package.newJavaClass", + "when": "false" + }, { "command": "java.view.package.newPackage", "when": "false" @@ -522,8 +529,8 @@ "group": "8_execution@6" }, { - "command": "java.view.package.newJavaClass", - "when": "view == javaProjectExplorer && viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)(?=.*?\\b\\+uri\\b)/", + "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)/)", "group": "1_new@10" }, { @@ -531,26 +538,11 @@ "when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/", "group": "inline@add_0" }, - { - "command": "java.view.package.newJavaClass", - "when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/", - "group": "1_new@10" - }, { "command": "java.view.package.newJavaClass", "when": "view == javaProjectExplorer && viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)(?=.*?\\b\\+uri\\b)/", "group": "inline@add_0" }, - { - "command": "java.view.package.newPackage", - "when": "view == javaProjectExplorer && viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)(?=.*?\\b\\+uri\\b)/", - "group": "1_new@20" - }, - { - "command": "java.view.package.newPackage", - "when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/", - "group": "1_new@20" - }, { "command": "java.project.addLibraries", "alt": "java.project.addLibraryFolders", @@ -594,6 +586,16 @@ "command": "java.project.update", "group": "gradle@10" } + ], + "javaProject.new": [ + { + "command": "java.view.package.newJavaClass", + "group": "new@10" + }, + { + "command": "java.view.package.newPackage", + "group": "new@40" + } ] }, "submenus": [ @@ -604,6 +606,10 @@ { "id": "javaProject.gradle", "label": "Gradle" + }, + { + "id": "javaProject.new", + "label": "%contributes.submenus.javaProject.new%" } ], "views": { @@ -813,7 +819,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^16.18.11", "@types/semver": "^7.3.13", - "@types/vscode": "1.69.0", + "@types/vscode": "1.75.0", "@vscode/test-electron": "^2.2.2", "copy-webpack-plugin": "^11.0.0", "glob": "^7.2.3", diff --git a/package.nls.json b/package.nls.json index 9873b339..507e2eaf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -19,11 +19,13 @@ "contributes.commands.java.view.package.exportJar": "Export Jar...", "contributes.commands.java.view.package.copyFilePath": "Copy Path", "contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path", - "contributes.commands.java.view.package.newJavaClass": "New Java Class", - "contributes.commands.java.view.package.newPackage": "New Package", + "contributes.commands.java.view.package.newJavaClass": "Java Class", + "contributes.commands.java.view.package.newPackage": "Package", "contributes.commands.java.view.package.renameFile": "Rename", "contributes.commands.java.view.package.moveFileToTrash": "Delete", "contributes.commands.java.view.package.deleteFilePermanently": "Delete Permanently", + "contributes.submenus.javaProject.new": "New", + "contributes.commands.java.view.menus.file.newJavaClass": "New Java Class", "configuration.java.dependency.showMembers": "Show the members in the explorer", "configuration.java.dependency.syncWithFolderExplorer": "Synchronize Java Projects explorer selection with folder explorer", "configuration.java.dependency.autoRefresh": "Synchronize Java Projects explorer with changes", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 99fa7448..75a45a6d 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -19,11 +19,13 @@ "contributes.commands.java.view.package.exportJar": "导出到 Jar 文件...", "contributes.commands.java.view.package.copyFilePath": "复制路径", "contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径", - "contributes.commands.java.view.package.newJavaClass": "创建 Java 类", - "contributes.commands.java.view.package.newPackage": "创建包", + "contributes.commands.java.view.package.newJavaClass": "Java 类", + "contributes.commands.java.view.package.newPackage": "包", "contributes.commands.java.view.package.renameFile": "重命名", "contributes.commands.java.view.package.moveFileToTrash": "删除", "contributes.commands.java.view.package.deleteFilePermanently": "永久删除", + "contributes.submenus.javaProject.new": "创建", + "contributes.commands.java.view.menus.file.newJavaClass": "创建 Java 类", "configuration.java.dependency.showMembers": "在 Java 项目管理器中显示成员", "configuration.java.dependency.syncWithFolderExplorer": "在 Java 项目管理器中同步关联当前打开的文件", "configuration.java.dependency.autoRefresh": "在 Java 项目管理器中自动同步修改", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 67f8d6c4..461bfa00 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -19,11 +19,13 @@ "contributes.commands.java.view.package.exportJar": "匯出成 Jar 檔案...", "contributes.commands.java.view.package.copyFilePath": "複製路徑", "contributes.commands.java.view.package.copyRelativeFilePath": "複製相對路徑", - "contributes.commands.java.view.package.newJavaClass": "建立 Java 類別", - "contributes.commands.java.view.package.newPackage": "建立套件", + "contributes.commands.java.view.package.newJavaClass": "Java 類別", + "contributes.commands.java.view.package.newPackage": "套件", "contributes.commands.java.view.package.renameFile": "重新命名", "contributes.commands.java.view.package.moveFileToTrash": "刪除", "contributes.commands.java.view.package.deleteFilePermanently": "永久刪除", + "contributes.submenus.javaProject.new": "建立", + "contributes.commands.java.view.menus.file.newJavaClass": "建立 Java 類別", "configuration.java.dependency.showMembers": "在 Java 專案管理員中顯示成員", "configuration.java.dependency.syncWithFolderExplorer": "在 Java 專案管理員中同步關聯當前開啟的檔案", "configuration.java.dependency.autoRefresh": "在 Java 專案管理員中自動同步修改", diff --git a/src/commands.ts b/src/commands.ts index c8b8d641..0542cda1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -46,6 +46,8 @@ export namespace Commands { export const VIEW_PACKAGE_REVEAL_IN_PROJECT_EXPLORER = "java.view.package.revealInProjectExplorer"; + export const VIEW_MENUS_FILE_NEW_JAVA_CLASS = "java.view.menus.file.newJavaClass"; + export const JAVA_PROJECT_OPEN = "_java.project.open"; export const JAVA_PROJECT_CREATE = "java.project.create"; diff --git a/src/explorerCommands/new.ts b/src/explorerCommands/new.ts index ba93aeb4..2e17bd62 100644 --- a/src/explorerCommands/new.ts +++ b/src/explorerCommands/new.ts @@ -5,26 +5,19 @@ import * as fse from "fs-extra"; import * as path from "path"; import { commands, Extension, extensions, languages, QuickPickItem, SnippetString, TextEditor, Uri, window, workspace, WorkspaceEdit, WorkspaceFolder } from "vscode"; -import { sendInfo } from "vscode-extension-telemetry-wrapper"; -import { Commands } from "../../extension.bundle"; +import { Commands, PrimaryTypeNode } from "../../extension.bundle"; import { ExtensionName } from "../constants"; import { NodeKind } from "../java/nodeData"; import { DataNode } from "../views/dataNode"; import { resourceRoots } from "../views/packageRootNode"; import { checkJavaQualifiedName } from "./utility"; +// TODO: separate to two function to handle creation from menu bar and explorer. export async function newJavaClass(node?: DataNode): Promise { let packageFsPath: string | undefined; if (!node) { - // from the new file menu entry - sendInfo("", { - triggerNewFileFrom: "menuBar", - }); packageFsPath = await inferPackageFsPath(); } else { - sendInfo("", { - triggerNewFileFrom: "projectExplorer", - }); if (!node?.uri || !canCreateClass(node)) { return; } @@ -209,6 +202,19 @@ export async function newPackage(node?: DataNode): Promise { } else if (nodeKind === NodeKind.Package) { defaultValue = node.nodeData.name + "."; packageRootPath = getPackageRootPath(Uri.parse(node.uri).fsPath, node.nodeData.name); + } else if (nodeKind === NodeKind.PrimaryType) { + const primaryTypeNode = node; + packageRootPath = primaryTypeNode.getPackageRootPath(); + if (packageRootPath === "") { + window.showErrorMessage("Failed to get the package root path."); + return; + } + const packagePath = await getPackageFsPath(node); + if (!packagePath) { + window.showErrorMessage("Failed to get the package path."); + return; + } + defaultValue = path.relative(packageRootPath, packagePath).replace(/[\\\/]/g, ".") + "."; } else { return; } @@ -239,14 +245,12 @@ export async function newPackage(node?: DataNode): Promise { await fse.ensureDir(getNewPackagePath(packageRootPath, packageName)); } +/** + * Check if the create package command is available for the given node. + * Currently the check logic is the same as the create class command. + */ function canCreatePackage(node: DataNode): boolean { - if (node.nodeData.kind === NodeKind.Project || - node.nodeData.kind === NodeKind.PackageRoot || - node.nodeData.kind === NodeKind.Package) { - return true; - } - - return false; + return canCreateClass(node); } function getPackageRootPath(packageFsPath: string, packageName: string): string { diff --git a/src/extension.ts b/src/extension.ts index 35eb624e..4f4ad709 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import { DependencyExplorer } from "./views/dependencyExplorer"; import { DiagnosticProvider } from "./tasks/buildArtifact/migration/DiagnosticProvider"; import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buildArtifact/migration/utils"; import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; +import { newJavaClass } from "./explorerCommands/new"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -46,7 +47,7 @@ async function activateExtension(_operationId: string, context: ExtensionContext context.subscriptions.push(tasks.registerTaskProvider(DeprecatedExportJarTaskProvider.type, new DeprecatedExportJarTaskProvider())); context.subscriptions.push(tasks.registerTaskProvider(BuildArtifactTaskProvider.exportJarType, new BuildArtifactTaskProvider())); context.subscriptions.push(tasks.registerTaskProvider(BuildTaskProvider.type, new BuildTaskProvider())); - + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_MENUS_FILE_NEW_JAVA_CLASS, newJavaClass)); context.subscriptions.push(window.onDidChangeActiveTextEditor((e: TextEditor | undefined) => { setContextForReloadProject(e?.document); })); diff --git a/src/views/PrimaryTypeNode.ts b/src/views/PrimaryTypeNode.ts index 0f0e6693..e7182337 100644 --- a/src/views/PrimaryTypeNode.ts +++ b/src/views/PrimaryTypeNode.ts @@ -11,6 +11,8 @@ import { isTest } from "../utility"; 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 { @@ -20,6 +22,19 @@ export class PrimaryTypeNode extends DataNode { super(nodeData, parent); } + public getPackageRootPath(): string { + if (this._rootNode?.uri) { + return Uri.parse(this._rootNode.uri).fsPath; + } + + const unmanagedFolder = this.getUnmanagedFolderAncestor(); + if (unmanagedFolder?.uri) { + return Uri.parse(unmanagedFolder.uri).fsPath; + } + + return ""; + } + protected async loadData(): Promise { if (!this.hasChildren() || !this.nodeData.uri) { return undefined; @@ -107,6 +122,46 @@ 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. + */ + private getUnmanagedFolderAncestor(): ProjectNode | undefined { + let ancestor = this.getParent(); + while (ancestor && !(ancestor instanceof ProjectNode)) { + ancestor = ancestor.getParent(); + } + if (ancestor?.isUnmanagedFolder()) { + return ancestor; + } + + return undefined; + } } diff --git a/test/suite/contextValue.test.ts b/test/suite/contextValue.test.ts index 9fa1e1d5..061dde48 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\+uri\b)/.test((await classType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+source\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\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+class\b)(?=.*?\b\+test\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await testClassType.getTreeItem()).contextValue || "")); }); test("test enum type node", async function() { - assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+enum\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await enumType.getTreeItem()).contextValue || "")); }); test("test interface type node", async function() { - assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || "")); + assert.ok(/java:type(?=.*?\b\+interface\b)(?=.*?\b\+source\b)(?=.*?\b\+uri\b)/.test((await interfaceType.getTreeItem()).contextValue || "")); }); test("test folder node", async function() {