Skip to content

Commit dbd132f

Browse files
authored
feat: Support new class & package from explorer (#299)
1 parent 73bc880 commit dbd132f

File tree

11 files changed

+185
-21
lines changed

11 files changed

+185
-21
lines changed

jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/PackageCommand.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ private static Object[] getPackageFragmentRootContent(IPackageFragmentRoot root,
440440
result.add(child);
441441
} else if (fragment.getNonJavaResources().length > 0) { // some package has non-java files
442442
result.add(fragment);
443+
} else if (!fragment.hasSubpackages()) {
444+
result.add(fragment);
443445
}
444446
}
445447
Object[] nonJavaResources = root.getNonJavaResources();

package.json

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@
120120
"command": "java.view.package.copyRelativeFilePath",
121121
"title": "%contributes.commands.java.view.package.copyRelativeFilePath%",
122122
"category": "Java"
123+
},
124+
{
125+
"command": "java.view.package.newJavaClass",
126+
"title": "%contributes.commands.java.view.package.newJavaClass%",
127+
"category": "Java"
128+
},
129+
{
130+
"command": "java.view.package.newPackage",
131+
"title": "%contributes.commands.java.view.package.newPackage%",
132+
"category": "Java"
123133
}
124134
],
125135
"configuration": {
@@ -210,6 +220,14 @@
210220
{
211221
"command": "java.project.refreshLibraries",
212222
"when": "never"
223+
},
224+
{
225+
"command": "java.view.package.newJavaClass",
226+
"when": "never"
227+
},
228+
{
229+
"command": "java.view.package.newPackage",
230+
"when": "never"
213231
}
214232
],
215233
"view/title": [
@@ -253,17 +271,27 @@
253271
{
254272
"command": "java.view.package.revealFileInOS",
255273
"when": "view == javaProjectExplorer && viewItem =~ /java:.*?\\+uri/",
256-
"group": "@1"
274+
"group": "path@10"
257275
},
258276
{
259277
"command": "java.view.package.copyFilePath",
260278
"when": "view == javaProjectExplorer && viewItem =~ /java:.*?\\+uri/",
261-
"group": "@2"
279+
"group": "path@20"
262280
},
263281
{
264282
"command": "java.view.package.copyRelativeFilePath",
265283
"when": "view == javaProjectExplorer && viewItem =~ /java:.*?\\+uri/",
266-
"group": "@2"
284+
"group": "path@25"
285+
},
286+
{
287+
"command": "java.view.package.newJavaClass",
288+
"when": "view == javaProjectExplorer && viewItem =~ /java:(package|packageRoot).*\\+uri/",
289+
"group": "new@10"
290+
},
291+
{
292+
"command": "java.view.package.newPackage",
293+
"when": "view == javaProjectExplorer && viewItem =~ /java:(package|packageRoot).*\\+uri/",
294+
"group": "new@20"
267295
},
268296
{
269297
"command": "java.project.addLibraries",

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"contributes.commands.java.view.package.exportJar": "Export Jar...",
1515
"contributes.commands.java.view.package.copyFilePath": "Copy Path",
1616
"contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path",
17+
"contributes.commands.java.view.package.newJavaClass": "New Java Class",
18+
"contributes.commands.java.view.package.newPackage": "New Package",
1719
"configuration.java.dependency.showMembers": "Show the members in the explorer",
1820
"configuration.java.dependency.syncWithFolderExplorer": "Synchronize Java Projects explorer selection with folder explorer",
1921
"configuration.java.dependency.autoRefresh": "Synchronize Java Projects explorer with changes",

package.nls.zh.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"contributes.commands.java.view.package.exportJar": "导出到 Jar 文件...",
1515
"contributes.commands.java.view.package.copyFilePath": "复制路径",
1616
"contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径",
17+
"contributes.commands.java.view.package.newJavaClass": "创建 Java 类",
18+
"contributes.commands.java.view.package.newPackage": "创建包",
1719
"configuration.java.dependency.showMembers": "在 Java 项目管理器中显示成员",
1820
"configuration.java.dependency.syncWithFolderExplorer": "在 Java 项目管理器中同步关联当前打开的文件",
1921
"configuration.java.dependency.autoRefresh": "在 Java 项目管理器中自动同步修改",

src/commands.ts

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

3333
export const VIEW_PACKAGE_EXPORT_JAR = "java.view.package.exportJar";
3434

35+
export const VIEW_PACKAGE_NEW_JAVA_CLASS = "java.view.package.newJavaClass";
36+
37+
export const VIEW_PACKAGE_NEW_JAVA_PACKAGE = "java.view.package.newPackage";
38+
3539
export const JAVA_PROJECT_CREATE = "java.project.create";
3640

3741
export const JAVA_PROJECT_ADD_LIBRARIES = "java.project.addLibraries";

src/explorerCommands/new.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as fse from "fs-extra";
5+
import * as path from "path";
6+
import { Uri, window, workspace, WorkspaceEdit } from "vscode";
7+
import { NodeKind } from "../java/nodeData";
8+
import { isJavaIdentifier, isKeyword } from "../utility";
9+
import { DataNode } from "../views/dataNode";
10+
11+
export async function newJavaClass(node: DataNode): Promise<void> {
12+
const packageFsPath: string = Uri.parse(node.uri).fsPath;
13+
const className: string | undefined = await window.showInputBox({
14+
placeHolder: "Input the class name",
15+
ignoreFocusOut: true,
16+
validateInput: async (value: string): Promise<string> => {
17+
const checkMessage: string = checkJavaQualifiedName(value);
18+
if (checkMessage) {
19+
return checkMessage;
20+
}
21+
22+
if (await fse.pathExists(getNewFilePath(packageFsPath, value))) {
23+
return "Class already exists.";
24+
}
25+
26+
return "";
27+
},
28+
});
29+
30+
if (!className) {
31+
return;
32+
}
33+
34+
// `workspace.applyEdit()` will trigger a workspace file event, and let the
35+
// vscode-java extension to handle the type: class, interface or enum.
36+
const workspaceEdit: WorkspaceEdit = new WorkspaceEdit();
37+
const fsPath: string = getNewFilePath(packageFsPath, className);
38+
workspaceEdit.createFile(Uri.file(fsPath));
39+
workspace.applyEdit(workspaceEdit);
40+
}
41+
42+
function getNewFilePath(basePath: string, className: string): string {
43+
if (className.endsWith(".java")) {
44+
className = className.substr(0, className.length - ".java".length);
45+
}
46+
return path.join(basePath, ...className.split(".")) + ".java";
47+
}
48+
49+
export async function newPackage(node: DataNode): Promise<void> {
50+
let defaultValue: string;
51+
let packageRootPath: string;
52+
if (node.nodeData.kind === NodeKind.PackageRoot) {
53+
defaultValue = "";
54+
packageRootPath = Uri.parse(node.uri).fsPath;
55+
} else if (node.nodeData.kind === NodeKind.Package) {
56+
defaultValue = node.nodeData.name + ".";
57+
const numberOfSegment: number = node.nodeData.name.split(".").length;
58+
packageRootPath = path.join(Uri.parse(node.uri).fsPath, ...Array(numberOfSegment).fill(".."));
59+
} else {
60+
return;
61+
}
62+
63+
const packageName: string | undefined = await window.showInputBox({
64+
value: defaultValue,
65+
placeHolder: "Input the package name",
66+
valueSelection: [defaultValue.length, defaultValue.length],
67+
ignoreFocusOut: true,
68+
validateInput: async (value: string): Promise<string> => {
69+
const checkMessage: string = checkJavaQualifiedName(value);
70+
if (checkMessage) {
71+
return checkMessage;
72+
}
73+
74+
if (await fse.pathExists(getNewPackagePath(packageRootPath, value))) {
75+
return "Package already exists.";
76+
}
77+
78+
return "";
79+
},
80+
});
81+
82+
if (!packageName) {
83+
return;
84+
}
85+
86+
await fse.ensureDir(getNewPackagePath(packageRootPath, packageName));
87+
}
88+
89+
function getNewPackagePath(packageRootPath: string, packageName: string): string {
90+
return path.join(packageRootPath, ...packageName.split("."));
91+
}
92+
93+
function checkJavaQualifiedName(value: string): string {
94+
if (!value || !value.trim()) {
95+
return "Input cannot be empty.";
96+
}
97+
98+
for (const part of value.split(".")) {
99+
if (isKeyword(part)) {
100+
return `Keyword '${part}' cannot be used.`;
101+
}
102+
103+
if (!isJavaIdentifier(part)) {
104+
return `Invalid Java qualified name.`;
105+
}
106+
}
107+
108+
return "";
109+
}

src/java/typeRootNodeData.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,6 @@
44
import { SymbolInformation } from "vscode";
55
import { INodeData } from "./nodeData";
66

7-
export enum TypeRootKind {
8-
/**
9-
* Kind constant for a source path root. Indicates this root
10-
* only contains source files.
11-
*/
12-
K_SOURCE = 1,
13-
/**
14-
* Kind constant for a binary path root. Indicates this
15-
* root only contains binary files.
16-
*/
17-
K_BINARY = 2,
18-
}
19-
207
export interface ITypeRootNodeData extends INodeData {
21-
entryKind: TypeRootKind;
22-
238
symbolTree?: Map<string, SymbolInformation[]>;
249
}

src/utility.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,19 @@ export enum Type {
5353
USAGEERROR = "usageError",
5454
ACTIVATEEXTENSION = "activateExtension", // TODO: Activation belongs to usage data, remove this category.
5555
}
56+
57+
const keywords: Set<string> = new Set([
58+
"abstract", "default", "if", "private", "this", "boolean", "do", "implements", "protected", "throw", "break", "double", "import",
59+
"public", "throws", "byte", "else", "instanceof", "return", "transient", "case", "extends", "int", "short", "try", "catch", "final",
60+
"interface", "static", "void", "char", "finally", "long", "strictfp", "volatile", "class", "float", "native", "super", "while",
61+
"const", "for", "new", "switch", "continue", "goto", "package", "synchronized", "true", "false", "null", "assert", "enum",
62+
]);
63+
64+
export function isKeyword(identifier: string): boolean {
65+
return keywords.has(identifier);
66+
}
67+
68+
const identifierRegExp: RegExp = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/;
69+
export function isJavaIdentifier(identifier: string): boolean {
70+
return identifierRegExp.test(identifier);
71+
}

src/views/dependencyDataProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "vscode";
99
import { instrumentOperation, instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper";
1010
import { Commands } from "../commands";
11+
import { newJavaClass, newPackage } from "../explorerCommands/new";
1112
import { createJarFile } from "../exportJarFileCommand";
1213
import { isStandardServerReady, isSwitchingServer } from "../extension";
1314
import { Jdtls } from "../java/jdtls";
@@ -34,7 +35,9 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
3435
context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_REFRESH, (debounce?: boolean, element?: ExplorerNode) =>
3536
this.refreshWithLog(debounce, element)));
3637
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_EXPORT_JAR, (node: INodeData) => createJarFile(node)));
37-
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_REVEAL_FILE_OS, (node: INodeData) =>
38+
context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_NEW_JAVA_CLASS, (node: DataNode) => newJavaClass(node)));
39+
context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_NEW_JAVA_PACKAGE, (node: DataNode) => newPackage(node)));
40+
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_REVEAL_FILE_OS, (node?: INodeData) =>
3841
commands.executeCommand("revealFileInOS", Uri.parse(node.uri))));
3942
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_COPY_FILE_PATH, (node: INodeData) =>
4043
commands.executeCommand("copyFilePath", Uri.parse(node.uri))));

src/views/packageNode.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { ThemeIcon } from "vscode";
55
import { Jdtls } from "../java/jdtls";
66
import { INodeData, NodeKind } from "../java/nodeData";
7+
import { IPackageRootNodeData, PackageRootKind } from "../java/packageRootNodeData";
78
import { DataNode } from "./dataNode";
89
import { ExplorerNode } from "./explorerNode";
910
import { FileNode } from "./fileNode";
@@ -43,4 +44,11 @@ export class PackageNode extends DataNode {
4344
protected get iconPath(): ThemeIcon {
4445
return new ThemeIcon("symbol-package");
4546
}
47+
48+
protected get contextValue(): string {
49+
const parentData = <IPackageRootNodeData> this._rootNode.nodeData;
50+
if (parentData.entryKind === PackageRootKind.K_SOURCE) {
51+
return `package/${this.name}`;
52+
}
53+
}
4654
}

0 commit comments

Comments
 (0)