From a0dcf41bca18d9019796a5d6bb42bbebc269ba44 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 29 Nov 2023 16:57:08 +0800 Subject: [PATCH] feat: Improve the user experience of new Java file. (#801) --- ThirdPartyNotices.txt | 13 + .../jdtls/ext/core/model/PackageNode.java | 55 ++-- package-lock.json | 17 ++ package.json | 84 +++++- package.nls.json | 9 +- package.nls.zh-cn.json | 9 +- package.nls.zh-tw.json | 4 +- src/commands.ts | 15 + src/explorerCommands/new.ts | 261 ++++++++++++++++-- src/extension.ts | 4 +- src/views/PrimaryTypeNode.ts | 5 + src/views/dependencyExplorer.ts | 19 +- src/views/packageNode.ts | 3 + src/views/packageRootNode.ts | 3 + src/views/projectNode.ts | 3 + test/ui/command.test.ts | 4 +- 16 files changed, 455 insertions(+), 53 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 86dfa178..de26a4ca 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -13,6 +13,7 @@ This project incorporates components from the projects listed below. The origina 6. jprichardson/node-fs-extra (https://github.com/jprichardson/node-fs-extra) 7. lodash/lodash (https://github.com/lodash/lodash) 8. sindresorhus/globby (https://github.com/sindresorhus/globby) +9. tcort/fmtr (https://github.com/tcort/fmtr) %% Apache Commons Lang NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -571,3 +572,15 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF sindresorhus/globby NOTICES AND INFORMATION + +%% tcort/fmtr NOTICES AND INFORMATION BEGIN HERE +========================================= +ISC License + +Copyright (c) 2015-2021 Thomas Cort linuxgeek@gmail.com + +Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF tcort/fmtr NOTICES AND INFORMATION diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/PackageNode.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/PackageNode.java index 618b391d..f945bfd0 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/PackageNode.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/PackageNode.java @@ -36,6 +36,8 @@ import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.core.JrtPackageFragmentRoot; import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.ProjectUtils; @@ -68,9 +70,11 @@ public class PackageNode { public static final String REFERENCED_LIBRARIES_PATH = "REFERENCED_LIBRARIES_PATH"; private static final String REFERENCED_LIBRARIES_CONTAINER_NAME = "Referenced Libraries"; private static final String IMMUTABLE_REFERENCED_LIBRARIES_CONTAINER_NAME = "Referenced Libraries (Read-only)"; - public static final ContainerNode REFERENCED_LIBRARIES_CONTAINER = new ContainerNode(REFERENCED_LIBRARIES_CONTAINER_NAME, REFERENCED_LIBRARIES_PATH, + public static final ContainerNode REFERENCED_LIBRARIES_CONTAINER = new ContainerNode( + REFERENCED_LIBRARIES_CONTAINER_NAME, REFERENCED_LIBRARIES_PATH, NodeKind.CONTAINER, IClasspathEntry.CPE_CONTAINER); - public static final ContainerNode IMMUTABLE_REFERENCED_LIBRARIES_CONTAINER = new ContainerNode(IMMUTABLE_REFERENCED_LIBRARIES_CONTAINER_NAME, + public static final ContainerNode IMMUTABLE_REFERENCED_LIBRARIES_CONTAINER = new ContainerNode( + IMMUTABLE_REFERENCED_LIBRARIES_CONTAINER_NAME, REFERENCED_LIBRARIES_PATH, NodeKind.CONTAINER, IClasspathEntry.CPE_CONTAINER); /** @@ -85,6 +89,8 @@ public class PackageNode { */ private static final String UNMANAGED_FOLDER_NATURE_ID = "org.eclipse.jdt.ls.core.unmanagedFolder"; + private static final String MAX_SOURCE_VERSION = "MaxSourceVersion"; + /** * The name of the PackageNode. */ @@ -164,7 +170,8 @@ public static PackageNode createNodeForProject(IJavaElement javaElement) { return null; } IProject proj = javaElement.getJavaProject().getProject(); - PackageNode projectNode = new PackageNode(proj.getName(), proj.getFullPath().toPortableString(), NodeKind.PROJECT); + PackageNode projectNode = new PackageNode(proj.getName(), proj.getFullPath().toPortableString(), + NodeKind.PROJECT); projectNode.setUri(ProjectUtils.getProjectRealFolder(proj).toFile().toURI().toString()); try { List natureIds = new ArrayList<>(Arrays.asList(proj.getDescription().getNatureIds())); @@ -173,6 +180,10 @@ public static PackageNode createNodeForProject(IJavaElement javaElement) { projectNode.setMetaDataValue(UNMANAGED_FOLDER_INNER_PATH, proj.getLocationURI().toString()); } projectNode.setMetaDataValue(NATURE_ID, natureIds); + String sourceVersion = javaElement.getJavaProject().getOption(JavaCore.COMPILER_SOURCE, true); + int jdkLevel = (int) (CompilerOptions.versionToJdkLevel(sourceVersion, true) >>> 16); + int majorVersion = Math.max(0, jdkLevel - ClassFileConstants.MAJOR_VERSION_0); + projectNode.setMetaDataValue(MAX_SOURCE_VERSION, majorVersion); } catch (CoreException e) { // do nothing } @@ -201,7 +212,8 @@ public static PackageNode createNodeForResource(IResource resource) { } public static PackageNode createNodeForPackageFragment(IPackageFragment packageFragment) { - PackageNode fragmentNode = new PackageNode(packageFragment.getElementName(), packageFragment.getPath().toPortableString(), NodeKind.PACKAGE); + PackageNode fragmentNode = new PackageNode(packageFragment.getElementName(), + packageFragment.getPath().toPortableString(), NodeKind.PACKAGE); fragmentNode.setHandlerIdentifier(packageFragment.getHandleIdentifier()); if (packageFragment.getResource() != null) { fragmentNode.setUri(packageFragment.getResource().getLocationURI().toString()); @@ -215,16 +227,19 @@ public static PackageNode createNodeForVirtualContainer(IPackageFragmentRoot pkg IClasspathEntry entry = pkgRoot.getRawClasspathEntry(); IClasspathContainer container = JavaCore.getClasspathContainer(entry.getPath(), pkgRoot.getJavaProject()); PackageNode containerNode = null; - if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY || entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY + || entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { containerNode = REFERENCED_LIBRARIES_CONTAINER; } else { - containerNode = new ContainerNode(container.getDescription(), container.getPath().toPortableString(), NodeKind.CONTAINER, entry.getEntryKind()); + containerNode = new ContainerNode(container.getDescription(), container.getPath().toPortableString(), + NodeKind.CONTAINER, entry.getEntryKind()); } return containerNode; } - public static PackageRootNode createNodeForPackageFragmentRoot(IPackageFragmentRoot pkgRoot) throws JavaModelException { + public static PackageRootNode createNodeForPackageFragmentRoot(IPackageFragmentRoot pkgRoot) + throws JavaModelException { PackageRootNode node; String displayName = pkgRoot.getElementName(); boolean isSourcePath = pkgRoot.getKind() == IPackageFragmentRoot.K_SOURCE; @@ -271,14 +286,16 @@ public static PackageRootNode createNodeForPackageFragmentRoot(IPackageFragmentR * Get the correspond node of classpath, it may be container or a package root. * * @param classpathEntry - * classpath entry + * classpath entry * @param javaProject - * correspond java project + * correspond java project * @param nodeKind - * could be CONTAINER or PACKAGEROOT(for referenced libraries) + * could be CONTAINER or PACKAGEROOT(for referenced + * libraries) * @return correspond PackageNode of classpath entry */ - public static PackageNode createNodeForClasspathEntry(IClasspathEntry classpathEntry, IJavaProject javaProject, NodeKind nodeKind) { + public static PackageNode createNodeForClasspathEntry(IClasspathEntry classpathEntry, IJavaProject javaProject, + NodeKind nodeKind) { try { IClasspathEntry entry = JavaCore.getResolvedClasspathEntry(classpathEntry); IClasspathContainer container = JavaCore.getClasspathContainer(entry.getPath(), javaProject); @@ -289,17 +306,18 @@ public static PackageNode createNodeForClasspathEntry(IClasspathEntry classpathE if (container != null) { PackageNode node = null; if (nodeKind == NodeKind.CONTAINER) { - node = new ContainerNode(container.getDescription(), container.getPath().toPortableString(), nodeKind, entry.getEntryKind()); + node = new ContainerNode(container.getDescription(), container.getPath().toPortableString(), + nodeKind, entry.getEntryKind()); final URI containerURI = ExtUtils.getContainerURI(javaProject, container); node.setUri(containerURI != null ? containerURI.toString() : null); } else if (nodeKind == NodeKind.PACKAGEROOT) { // ClasspathEntry for referenced jar files // Use package name as package root name String[] pathSegments = container.getPath().segments(); node = new PackageRootNode( - pathSegments[pathSegments.length - 1], - container.getPath().toPortableString(), - container.getPath().toFile().toURI().toString(), - nodeKind, IPackageFragmentRoot.K_BINARY); + pathSegments[pathSegments.length - 1], + container.getPath().toPortableString(), + container.getPath().toFile().toURI().toString(), + nodeKind, IPackageFragmentRoot.K_BINARY); } return node; } @@ -310,7 +328,8 @@ public static PackageNode createNodeForClasspathEntry(IClasspathEntry classpathE } public static PackageNode createNodeForPrimaryType(IType type) { - PackageNode primaryTypeNode = new PackageNode(type.getElementName(), type.getPath().toPortableString(), NodeKind.PRIMARYTYPE); + PackageNode primaryTypeNode = new PackageNode(type.getElementName(), type.getPath().toPortableString(), + NodeKind.PRIMARYTYPE); try { if (type.isEnum()) { @@ -332,7 +351,7 @@ public static PackageNode createNodeForPrimaryType(IType type) { * Get correspond node of referenced variable. * * @param classpathEntry - * referenced variable's classpath entry + * referenced variable's classpath entry * @return correspond package node */ public static PackageRootNode createNodeForClasspathVariable(IClasspathEntry classpathEntry) { diff --git a/package-lock.json b/package-lock.json index ea42e4d9..64d6a4da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "await-lock": "^2.2.2", + "fmtr": "^1.1.4", "fs-extra": "^10.1.0", "globby": "^13.1.3", "lodash": "^4.17.21", @@ -2052,6 +2053,14 @@ "flat": "cli.js" } }, + "node_modules/fmtr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fmtr/-/fmtr-1.1.4.tgz", + "integrity": "sha512-5qCv7eLlkR7LUDrcQdx0vGFo8tLgwFtO0JHkX9xfQm+DNnXpk9zQNSIldKlZ2VPZBwxTM9GB1esyeQuxhCVvmQ==", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", @@ -6879,6 +6888,14 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, + "fmtr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fmtr/-/fmtr-1.1.4.tgz", + "integrity": "sha512-5qCv7eLlkR7LUDrcQdx0vGFo8tLgwFtO0JHkX9xfQm+DNnXpk9zQNSIldKlZ2VPZBwxTM9GB1esyeQuxhCVvmQ==", + "requires": { + "lodash": "^4.17.21" + } + }, "follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", diff --git a/package.json b/package.json index aba8da47..0770e28e 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,36 @@ "category": "Java", "icon": "$(add)" }, + { + "command": "java.view.package.newJavaInterface", + "title": "%contributes.commands.java.view.package.newJavaInterface%", + "category": "Java", + "icon": "$(add)" + }, + { + "command": "java.view.package.newJavaEnum", + "title": "%contributes.commands.java.view.package.newJavaEnum%", + "category": "Java", + "icon": "$(add)" + }, + { + "command": "java.view.package.newJavaRecord", + "title": "%contributes.commands.java.view.package.newJavaRecord%", + "category": "Java", + "icon": "$(add)" + }, + { + "command": "java.view.package.newJavaAnnotation", + "title": "%contributes.commands.java.view.package.newJavaAnnotation%", + "category": "Java", + "icon": "$(add)" + }, + { + "command": "java.view.package.newJavaAbstractClass", + "title": "%contributes.commands.java.view.package.newJavaAbstractClass%", + "category": "Java", + "icon": "$(add)" + }, { "command": "java.view.package.newPackage", "title": "%contributes.commands.java.view.package.newPackage%", @@ -395,6 +425,26 @@ "command": "java.view.package.newJavaClass", "when": "false" }, + { + "command": "java.view.package.newJavaInterface", + "when": "false" + }, + { + "command": "java.view.package.newJavaEnum", + "when": "false" + }, + { + "command": "java.view.package.newJavaRecord", + "when": "false" + }, + { + "command": "java.view.package.newJavaAnnotation", + "when": "false" + }, + { + "command": "java.view.package.newJavaAbstractClass", + "when": "false" + }, { "command": "java.view.package.newPackage", "when": "false" @@ -641,21 +691,46 @@ "javaProject.new": [ { "command": "java.view.package.newJavaClass", - "group": "new@10", + "group": "new1@10", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" + }, + { + "command": "java.view.package.newJavaInterface", + "group": "new1@11", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" + }, + { + "command": "java.view.package.newJavaEnum", + "group": "new1@12", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" + }, + { + "command": "java.view.package.newJavaRecord", + "group": "new1@13", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/) && viewItem =~ /java:.*\\+allowRecord\\b/" + }, + { + "command": "java.view.package.newJavaAnnotation", + "group": "new1@14", + "when": "view == javaProjectExplorer && (viewItem =~ /java:(package|packageRoot)(?=.*?\\b\\+source\\b)/ || viewItem =~ /java:project(?=.*?\\b\\+java\\b)/ || viewItem =~ /java:type/)" + }, + { + "command": "java.view.package.newJavaAbstractClass", + "group": "new1@15", "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@20", + "group": "new1@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" + "group": "new2@30" }, { "command": "java.view.package.newFolder", - "group": "new@40", + "group": "new2@40", "when": "view == javaProjectExplorer && (viewItem =~ /java:(file|folder|project)/ || viewItem =~ /java:(packageRoot)(?=.*?\\b\\+resource\\b)/)" } ] @@ -895,6 +970,7 @@ }, "dependencies": { "await-lock": "^2.2.2", + "fmtr": "^1.1.4", "fs-extra": "^10.1.0", "globby": "^13.1.3", "lodash": "^4.17.21", diff --git a/package.nls.json b/package.nls.json index c876d1f8..1cf98d01 100644 --- a/package.nls.json +++ b/package.nls.json @@ -22,10 +22,15 @@ "contributes.commands.java.view.package.copyFilePath": "Copy Path", "contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path", "contributes.commands.java.view.package.new": "New...", - "contributes.commands.java.view.package.newJavaClass": "Java Class...", + "contributes.commands.java.view.package.newJavaClass": "Class...", + "contributes.commands.java.view.package.newJavaInterface": "Interface...", + "contributes.commands.java.view.package.newJavaEnum": "Enum...", + "contributes.commands.java.view.package.newJavaRecord": "Record...", + "contributes.commands.java.view.package.newJavaAnnotation": "Annotation...", + "contributes.commands.java.view.package.newJavaAbstractClass": "Abstract 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.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 ed5f3403..f9253872 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -22,10 +22,15 @@ "contributes.commands.java.view.package.copyFilePath": "复制路径", "contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径", "contributes.commands.java.view.package.new": "创建...", - "contributes.commands.java.view.package.newJavaClass": "Java 类...", + "contributes.commands.java.view.package.newJavaClass": "类...", + "contributes.commands.java.view.package.newJavaInterface": "接口类型...", + "contributes.commands.java.view.package.newJavaEnum": "枚举类型...", + "contributes.commands.java.view.package.newJavaRecord": "记录类型...", + "contributes.commands.java.view.package.newJavaAnnotation": "注解类型...", + "contributes.commands.java.view.package.newJavaAbstractClass": "抽象类型...", "contributes.commands.java.view.package.newPackage": "包...", "contributes.commands.java.view.package.newFile": "文件...", - "contributes.commands.java.view.package.newFolder": "文件夹", + "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 94ac2499..c40519b6 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -20,10 +20,10 @@ "contributes.commands.java.view.package.copyFilePath": "複製路徑", "contributes.commands.java.view.package.copyRelativeFilePath": "複製相對路徑", "contributes.commands.java.view.package.new": "建立...", - "contributes.commands.java.view.package.newJavaClass": "Java 類別...", + "contributes.commands.java.view.package.newJavaClass": "類別...", "contributes.commands.java.view.package.newPackage": "套件...", "contributes.commands.java.view.package.newFile": "檔案...", - "contributes.commands.java.view.package.newFolder": "資料夾", + "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 c64f0f37..2e449862 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -42,6 +42,16 @@ export namespace Commands { export const VIEW_PACKAGE_NEW_JAVA_CLASS = "java.view.package.newJavaClass"; + export const VIEW_PACKAGE_NEW_JAVA_INTERFACE = "java.view.package.newJavaInterface"; + + export const VIEW_PACKAGE_NEW_JAVA_ENUM = "java.view.package.newJavaEnum"; + + export const VIEW_PACKAGE_NEW_JAVA_RECORD = "java.view.package.newJavaRecord"; + + export const VIEW_PACKAGE_NEW_JAVA_ANNOTATION = "java.view.package.newJavaAnnotation"; + + export const VIEW_PACKAGE_NEW_JAVA_ABSTRACT_CLASS = "java.view.package.newJavaAbstractClass"; + export const VIEW_PACKAGE_NEW_JAVA_PACKAGE = "java.view.package.newPackage"; export const VIEW_PACKAGE_RENAME_FILE = "java.view.package.renameFile"; @@ -133,6 +143,11 @@ export namespace Commands { export const GET_ALL_PROJECTS = "java.project.getAll"; export const BUILD_PROJECT = "java.project.build"; + + /** + * Get the project settings + */ + export const GET_PROJECT_SETTINGS = 'java.project.getSettings'; } export function executeJavaLanguageServerCommand(...rest: any[]) { diff --git a/src/explorerCommands/new.ts b/src/explorerCommands/new.ts index bc835034..248f57e1 100644 --- a/src/explorerCommands/new.ts +++ b/src/explorerCommands/new.ts @@ -2,8 +2,9 @@ // Licensed under the MIT license. import * as fse from "fs-extra"; +import { userInfo } from "os"; import * as path from "path"; -import { commands, Extension, extensions, languages, QuickPickItem, SnippetString, TextEditor, Uri, +import { commands, Extension, extensions, languages, Position, QuickPickItem, QuickPickItemKind, SnippetString, TextEditor, Uri, window, workspace, WorkspaceEdit, WorkspaceFolder } from "vscode"; import { Commands, PrimaryTypeNode } from "../../extension.bundle"; import { ExtensionName } from "../constants"; @@ -13,26 +14,87 @@ import { resourceRoots } from "../views/packageRootNode"; import { checkJavaQualifiedName } from "./utility"; import { sendError, setUserError } from "vscode-extension-telemetry-wrapper"; +// tslint:disable no-var-requires +const stringInterpolate = require("fmtr"); + +export class JavaType { + public static readonly CLASS: JavaType = new JavaType("Class", "class", "$(symbol-class)"); + public static readonly INTERFACE: JavaType = new JavaType("Interface", "interface", "$(symbol-interface)"); + public static readonly ENUM: JavaType = new JavaType("Enum", "enum", "$(symbol-enum)"); + public static readonly RECORD: JavaType = new JavaType("Record", "record", "$(symbol-class)"); + public static readonly ANNOTATION: JavaType = new JavaType("Annotation", "@interface", "$(symbol-interface)"); + public static readonly ABSTRACT_CLASS: JavaType = new JavaType("Abstract Class", "abstract class", "$(symbol-class)"); + + public static readonly ALL: JavaType[] = [ + JavaType.CLASS, + JavaType.INTERFACE, + JavaType.ENUM, + JavaType.RECORD, + JavaType.ANNOTATION, + JavaType.ABSTRACT_CLASS, + ]; + + public static fromDisplayName(label: string): JavaType | undefined { + if (label?.startsWith("$")) { + return JavaType.ALL.find((javaType) => `${javaType.icon} ${javaType.label}` === label); + } + + return JavaType.ALL.find((javaType) => javaType.label === label); + } + + public static getDisplayNames(includeIcon: boolean, includeRecord?: boolean): string[] { + return JavaType.ALL + .filter((javaType) => includeRecord || javaType !== JavaType.RECORD) + .map((javaType) => { + if (includeIcon) { + return `${javaType.icon} ${javaType.label}`; + } else { + return javaType.label; + } + }); + } + + private constructor(public readonly label: string, public readonly keyword: string, + public readonly icon: string) { + } +} + export async function newResource(node: DataNode): Promise { - const availableTypes: string[] = []; + const availableTypes: QuickPickItem[] = []; // add options for Java nodes if (node.nodeData.kind === NodeKind.Project || (node.nodeData.kind === NodeKind.PackageRoot && !resourceRoots.includes(node.nodeData.name)) || node.nodeData.kind === NodeKind.Package || node.nodeData.kind === NodeKind.PrimaryType || node.nodeData.kind === NodeKind.CompilationUnit) { - availableTypes.push("$(symbol-class) Java Class", "$(symbol-namespace) Package"); + const allowRecord = node.computeContextValue()?.includes("+allowRecord"); + availableTypes.push(...JavaType.getDisplayNames(true, allowRecord).map((label) => { + return { + label, + }; + })); + availableTypes.push({ + label: "$(symbol-namespace) Package", + }); } + availableTypes.push({ + label: "", + kind: QuickPickItemKind.Separator, + }); // add new file option - availableTypes.push("$(file) File"); + availableTypes.push({ + label: "$(file) File", + }); // add new folder option if (node.nodeData.kind === NodeKind.Project || (node.nodeData.kind === NodeKind.PackageRoot && resourceRoots.includes(node.nodeData.name)) || node.nodeData.kind === NodeKind.Folder || node.nodeData.kind === NodeKind.File) { - availableTypes.push("$(folder) Folder"); + availableTypes.push({ + label: "$(folder) Folder", + }); } const type = await window.showQuickPick( @@ -43,10 +105,7 @@ export async function newResource(node: DataNode): Promise { } ); - switch (type) { - case "$(symbol-class) Java Class": - await newJavaClass(node); - break; + switch (type?.label) { case "$(symbol-namespace) Package": await newPackage(node); break; @@ -57,12 +116,40 @@ export async function newResource(node: DataNode): Promise { await newFolder(node); break; default: + const javaType = JavaType.fromDisplayName(type?.label || ""); + if (javaType) { + await newJavaFileWithSpecificType(javaType, node); + } break; } } -// TODO: separate to two function to handle creation from menu bar and explorer. -export async function newJavaClass(node?: DataNode): Promise { +// Create a new Java file from the menu bar. +export async function newJavaFile(): Promise { + const packageFsPath: string | undefined = await inferPackageFsPath(); + if (packageFsPath === undefined) { + // User canceled + return; + } else if (packageFsPath.length === 0) { + return newUntitledJavaFile(); + } + + const includeRecord = !(await isVersionLessThan(Uri.file(packageFsPath).toString(), 16)); + const supportedTypes: string[] = JavaType.getDisplayNames(true, includeRecord); + const typeName: string | undefined = await window.showQuickPick(supportedTypes, + { + placeHolder: "Select the Java type you want to create", + ignoreFocusOut: true, + }); + if (!typeName) { + return; + } + + newJavaFile0(packageFsPath, JavaType.fromDisplayName(typeName)); +} + +// Create a new Java file from the context menu of Java Projects view. +export async function newJavaFileWithSpecificType(javaType: JavaType, node?: DataNode): Promise { let packageFsPath: string | undefined; if (!node) { packageFsPath = await inferPackageFsPath(); @@ -70,10 +157,8 @@ export async function newJavaClass(node?: DataNode): Promise { if (!node?.uri || !canCreateClass(node)) { return; } - packageFsPath = await getPackageFsPath(node); } - if (packageFsPath === undefined) { // User canceled return; @@ -81,8 +166,16 @@ export async function newJavaClass(node?: DataNode): Promise { return newUntitledJavaFile(); } + newJavaFile0(packageFsPath, javaType); +} + +async function newJavaFile0(packageFsPath: string, javaType: JavaType | undefined) { + if (!javaType) { + return; + } + const className: string | undefined = await window.showInputBox({ - placeHolder: "Enter the Java file name for class/interface/enum/record/@interface", + placeHolder: `Input the ${javaType.label.toLowerCase()} name`, ignoreFocusOut: true, validateInput: async (value: string): Promise => { const checkMessage: string = checkJavaQualifiedName(value); @@ -102,12 +195,86 @@ export async function newJavaClass(node?: DataNode): Promise { return; } - // `workspace.applyEdit()` will trigger a workspace file event, and let the - // vscode-java extension to handle the type: class, interface or enum. - const workspaceEdit: WorkspaceEdit = new WorkspaceEdit(); const fsPath: string = getNewFilePath(packageFsPath, className); - workspaceEdit.createFile(Uri.file(fsPath)); - workspace.applyEdit(workspaceEdit); + const packageName = await resolvePackageName(fsPath); + await newJavaFileWithContents(fsPath, javaType, packageName); +} + +// New File implementation is copied from +// https://github.com/redhat-developer/vscode-java/blob/86bf3ae02f4f457184e6cc217f20240f9882dde9/src/fileEventHandler.ts#L66 +async function newJavaFileWithContents(fsPath: string, javaType: JavaType, packageName: string) { + const snippets: string[] = []; + const formatNumber = (num: number) => num > 9 ? String(num) : `0${num}`; + const typeName: string = resolveTypeName(fsPath); + const isPackageInfo = typeName === 'package-info'; + const isModuleInfo = typeName === 'module-info'; + const date = new Date(); + const context: any = { + fileName: path.basename(fsPath), + packageName: "", + typeName, + user: userInfo().username, + date: date.toLocaleDateString(undefined, {month: "short", day: "2-digit", year: "numeric"}), + time: date.toLocaleTimeString(), + year: date.getFullYear(), + month: formatNumber(date.getMonth() + 1), + shortmonth: date.toLocaleDateString(undefined, {month: "short"}), + day: formatNumber(date.getDate()), + hour: formatNumber(date.getHours()), + minute: formatNumber(date.getMinutes()), + }; + + if (!isModuleInfo) { + context.packageName = packageName; + } + + const fileHeader = workspace.getConfiguration('java').get("templates.fileHeader"); + if (fileHeader && fileHeader.length) { + for (const template of fileHeader) { + snippets.push(stringInterpolate(template, context)); + } + } + + if (!isModuleInfo) { + if (context.packageName) { + snippets.push(`package ${context.packageName};`); + snippets.push(""); + } + } + + if (!isPackageInfo) { + const typeComment = workspace.getConfiguration('java').get("templates.typeComment"); + if (typeComment && typeComment.length) { + for (const template of typeComment) { + snippets.push(stringInterpolate(template, context)); + } + } + + if (isModuleInfo) { + snippets.push(`module {`); + } else { + snippets.push(`public ${javaType.keyword} ${typeName}${javaType === JavaType.RECORD ? "()" : ""} {`); + } + snippets.push(""); + snippets.push("}"); + snippets.push(""); + } + + const workspaceEdit: WorkspaceEdit = new WorkspaceEdit(); + const fsUri: Uri = Uri.file(fsPath); + workspaceEdit.createFile(fsUri); + workspaceEdit.insert(fsUri, new Position(0, 0), snippets.join("\n")); + await workspace.applyEdit(workspaceEdit); + const editor = await window.showTextDocument(fsUri); + if (editor) { + editor.document.save(); + } +} + +function resolveTypeName(filePath: string): string { + const fileName: string = path.basename(filePath); + const extName: string = path.extname(fileName); + return fileName.substring(0, fileName.length - extName.length); } async function newUntitledJavaFile(): Promise { @@ -186,6 +353,62 @@ function canCreateClass(node: DataNode): boolean { return false; } +const COMPLIANCE = "org.eclipse.jdt.core.compiler.compliance"; +async function isVersionLessThan(fileUri: string, targetVersion: number): Promise { + let projectSettings: any = {}; + try { + projectSettings = await commands.executeCommand( + Commands.EXECUTE_WORKSPACE_COMMAND, Commands.GET_PROJECT_SETTINGS, fileUri, [ COMPLIANCE ]); + } catch (err) { + // do nothing. + } + + let javaVersion = 0; + let complianceVersion = projectSettings[COMPLIANCE]; + if (complianceVersion) { + // Ignore '1.' prefix for legacy Java versions + if (complianceVersion.startsWith('1.')) { + complianceVersion = complianceVersion.substring(2); + } + + // look into the interesting bits now + const regexp = /\d+/g; + const match = regexp.exec(complianceVersion); + if (match) { + javaVersion = parseInt(match[0], 10); + } + } + + return javaVersion < targetVersion; +} + +async function resolvePackageName(filePath: string): Promise { + let sourcePaths: string[] = []; + const result: IListCommandResult = + await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.LIST_SOURCEPATHS); + if (result && result.data && result.data.length) { + sourcePaths = result.data.map((sourcePath) => sourcePath.path).sort((a, b) => b.length - a.length); + } + + if (!sourcePaths || !sourcePaths.length) { + return ""; + } + + for (const sourcePath of sourcePaths) { + if (isPrefix(sourcePath, filePath)) { + const relative = path.relative(sourcePath, path.dirname(filePath)); + return relative.replace(/[/\\]/g, "."); + } + } + + return ""; +} + +function isPrefix(parentPath: string, filePath: string): boolean { + const relative = path.relative(parentPath, filePath); + return !relative || (!relative.startsWith('..') && !path.isAbsolute(relative)); +} + async function getPackageFsPath(node: DataNode): Promise { if (node.nodeData.kind === NodeKind.Project) { const childrenNodes: DataNode[] = await node.getChildren() as DataNode[]; diff --git a/src/extension.ts b/src/extension.ts index 4f4ad709..b9b1d0d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +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"; +import { newJavaFile } from "./explorerCommands/new"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -47,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(instrumentOperationAsVsCodeCommand(Commands.VIEW_MENUS_FILE_NEW_JAVA_CLASS, newJavaFile)); context.subscriptions.push(window.onDidChangeActiveTextEditor((e: TextEditor | undefined) => { setContextForReloadProject(e?.document); })); diff --git a/src/views/PrimaryTypeNode.ts b/src/views/PrimaryTypeNode.ts index 3b5540ca..8ee95223 100644 --- a/src/views/PrimaryTypeNode.ts +++ b/src/views/PrimaryTypeNode.ts @@ -121,6 +121,11 @@ export class PrimaryTypeNode extends DataNode { contextValue += "+test"; } + if (this._rootNode?.getParent() instanceof ProjectNode + && (this._rootNode.getParent() as ProjectNode).nodeData?.metaData?.MaxSourceVersion >= 16) { + contextValue += "+allowRecord"; + } + return contextValue; } diff --git a/src/views/dependencyExplorer.ts b/src/views/dependencyExplorer.ts index 90384d14..af248add 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 { newFile, newFolder, newJavaClass, newPackage, newResource } from "../explorerCommands/new"; +import { JavaType, newFile, newFolder, newJavaFileWithSpecificType, newPackage, newResource } from "../explorerCommands/new"; import { renameFile } from "../explorerCommands/rename"; import { getCmdNode } from "../explorerCommands/utility"; import { Jdtls } from "../java/jdtls"; @@ -117,7 +117,22 @@ export class DependencyExplorer implements Disposable { newResource(node); }), instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_CLASS, async (node?: DataNode) => { - newJavaClass(node); + newJavaFileWithSpecificType(JavaType.CLASS, node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_INTERFACE, async (node?: DataNode) => { + newJavaFileWithSpecificType(JavaType.INTERFACE, node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_ENUM, async (node?: DataNode) => { + newJavaFileWithSpecificType(JavaType.ENUM, node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_RECORD, async (node?: DataNode) => { + newJavaFileWithSpecificType(JavaType.RECORD, node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_ANNOTATION, async (node?: DataNode) => { + newJavaFileWithSpecificType(JavaType.ANNOTATION, node); + }), + instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_JAVA_ABSTRACT_CLASS, async (node?: DataNode) => { + newJavaFileWithSpecificType(JavaType.ABSTRACT_CLASS, node); }), instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_NEW_FILE, async (node: DataNode) => { newFile(node); diff --git a/src/views/packageNode.ts b/src/views/packageNode.ts index 501b369b..a6151435 100644 --- a/src/views/packageNode.ts +++ b/src/views/packageNode.ts @@ -50,6 +50,9 @@ export class PackageNode extends DataNode { let contextValue: string = Explorer.ContextValueType.Package; if (parentData.entryKind === PackageRootKind.K_SOURCE || parentData.kind === NodeKind.Project) { contextValue += "+source"; + if (this._project.nodeData.metaData?.MaxSourceVersion >= 16) { + contextValue += "+allowRecord"; + } } else if (parentData.entryKind === PackageRootKind.K_BINARY) { contextValue += "+binary"; } diff --git a/src/views/packageRootNode.ts b/src/views/packageRootNode.ts index 8ca4ca19..a4951a28 100644 --- a/src/views/packageRootNode.ts +++ b/src/views/packageRootNode.ts @@ -76,6 +76,9 @@ export class PackageRootNode extends DataNode { } else { contextValue += "+source"; } + if (this._project.nodeData.metaData?.MaxSourceVersion >= 16) { + contextValue += "+allowRecord"; + } return contextValue; } } diff --git a/src/views/projectNode.ts b/src/views/projectNode.ts index 2d127fb0..f48ef75a 100644 --- a/src/views/projectNode.ts +++ b/src/views/projectNode.ts @@ -96,6 +96,9 @@ export class ProjectNode extends DataNode { const attributeString: string = getProjectTypeAttributes(natureIds); contextValue += attributeString; } + if (this.nodeData.metaData?.MaxSourceVersion >= 16) { + contextValue += "+allowRecord"; + } return contextValue; } } diff --git a/test/ui/command.test.ts b/test/ui/command.test.ts index 7894907a..37efdaee 100644 --- a/test/ui/command.test.ts +++ b/test/ui/command.test.ts @@ -114,14 +114,14 @@ describe("Command Tests", function() { it("Test java.view.package.newJavaClass", async function() { let inputBox = await createJavaResource(); - const javaClassQuickPick = await inputBox.findQuickPick("Java Class"); + const javaClassQuickPick = await inputBox.findQuickPick(0); await javaClassQuickPick!.click(); assert.ok(await inputBox.getPlaceHolder() === "Choose a source folder", `InputBox "Choose a source folder" should appear`); const quickPick = await inputBox.findQuickPick("src/main/java"); assert.ok(quickPick, `Quickpick item "src/main/java" should be found`); await quickPick!.click(); inputBox = await InputBox.create(); - assert.ok(await inputBox.getPlaceHolder() === "Enter the Java file name for class/interface/enum/record/@interface", `InputBox "Enter the Java file name" should appear`); + assert.ok(await inputBox.getPlaceHolder() === "Input the class name", `InputBox "Input the class name" should appear`); await inputBox.setText("App2"); await inputBox.confirm(); await sleep(1000);