Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ You can turn these reminders on or off at any time with the `java.dependency.ena
| `java.dependency.packagePresentation` | Specify how to display the package. Supported values are: `flat`, `hierarchical`.| `flat` |
| `java.dependency.enableDependencyCheckup` | Show reminders when your Java runtimes or dependencies need an upgrade. | `true` |
| `java.project.exportJar.targetPath` | The output path of export jar. When this setting is **empty** , a file explorer will pop up to let the user select the output location.| `${workspaceFolder}/${workspaceFolderBasename}.jar` |
| `java.project.explorer.showNonJavaResources` | When enabled, the explorer shows non-Java resources. When disabled, workspace folders without Java projects are hidden. | `true` |
| `java.project.explorer.showNonJavaResources` | When enabled, the explorer shows non-Java resources and lets you browse workspace folders without Java projects. When disabled, workspace folders without Java projects are hidden. | `true` |

## Contribution

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -835,12 +835,12 @@
},
{
"submenu": "javaProject.new",
"when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/",
"when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!:workspaceResource)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/",
"group": "1_new@10"
},
{
"command": "java.view.package.new",
"when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/",
"when": "view == javaProjectExplorer && viewItem =~ /java(?!:container)(?!:jar)(?!:workspaceResource)(?!.*?\\b\\+binary\\b)(?=.*?\\b\\+uri\\b)/",
"group": "inline@add_0"
},
{
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"configuration.java.dependency.refreshDelay": "The delay time (ms) the auto refresh is invoked when changes are detected",
"configuration.java.dependency.packagePresentation": "Package presentation mode: flat or hierarchical",
"configuration.java.dependency.enableDependencyCheckup": "Show reminders when your Java runtimes or dependencies need an upgrade.",
"configuration.java.project.explorer.showNonJavaResources": "When enabled, the explorer shows non-Java resources. When disabled, workspace folders without Java projects are hidden.",
"configuration.java.project.explorer.showNonJavaResources": "When enabled, the explorer shows non-Java resources and lets you browse workspace folders without Java projects. When disabled, workspace folders without Java projects are hidden.",
"configuration.java.project.exportJar.targetPath.customization": "The output path of the exported jar. Leave it empty if you want to manually pick the output location.",
"configuration.java.project.exportJar.targetPath.workspaceFolder": "Export the jar file into the workspace folder. Its name is the same as the folder's.",
"configuration.java.project.exportJar.targetPath.select": "Select output location manually when exporting the jar file.",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"configuration.java.dependency.autoRefresh": "在 Java 项目管理器中自动同步修改",
"configuration.java.dependency.refreshDelay": "控制 Java 项目管理器刷新的延迟时间 (毫秒)",
"configuration.java.dependency.packagePresentation": "Java 包显示方式: 平行显示或者分层显示",
"configuration.java.project.explorer.showNonJavaResources": "启用时,Java 项目管理器将显示非 Java 资源;禁用时,将隐藏不包含 Java 项目的工作区文件夹。",
"configuration.java.project.explorer.showNonJavaResources": "启用时,Java 项目管理器将显示非 Java 资源,并可浏览不包含 Java 项目的工作区文件夹;禁用时,将隐藏不包含 Java 项目的工作区文件夹。",
"configuration.java.project.exportJar.targetPath.customization": "导出 Jar 文件的路径。您可以将此选项置为空串来手动选择 jar 文件的导出路径。",
"configuration.java.project.exportJar.targetPath.workspaceFolder": "导出 Jar 文件到工作空间文件夹下。Jar 文件的名称和工作空间文件夹的名称相同。",
"configuration.java.project.exportJar.targetPath.select": "在导出 Jar 文件时手动选择输出目录。",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"configuration.java.dependency.autoRefresh": "在 Java 專案管理員中自動同步修改",
"configuration.java.dependency.refreshDelay": "控制 Java 專案管理員重新整理的延遲時間 (毫秒)",
"configuration.java.dependency.packagePresentation": "Java 套件顯示方式: 平行顯示或者分層顯示",
"configuration.java.project.explorer.showNonJavaResources": "啟用時,Java 專案管理員會顯示非 Java 資源;停用時,將隱藏不包含 Java 專案的工作區資料夾。",
"configuration.java.project.explorer.showNonJavaResources": "啟用時,Java 專案管理員會顯示非 Java 資源,並可瀏覽不包含 Java 專案的工作區資料夾;停用時,將隱藏不包含 Java 專案的工作區資料夾。",
"configuration.java.project.exportJar.targetPath.customization": "匯出 Jar 檔案的路徑。如果您想要手動選擇輸出路徑,可以不填。",
"configuration.java.project.exportJar.targetPath.workspaceFolder": "匯出 Jar 檔案到工作區資料夾下。Jar 檔案的名稱和工作區資料夾的名稱相同。",
"configuration.java.project.exportJar.targetPath.select": "在匯出 Jar 檔案時手動選擇輸出位置。",
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export namespace Explorer {
File = "file",
Type = "type",
Folder = "folder",
WorkspaceResourceFile = "workspaceResourceFile",
WorkspaceResourceFolder = "workspaceResourceFolder",
Symbol = "symbol",
}

Expand Down
3 changes: 2 additions & 1 deletion src/views/DragAndDropController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { PackageRootNode } from "./packageRootNode";
import { PrimaryTypeNode } from "./PrimaryTypeNode";
import { ProjectNode } from "./projectNode";
import { WorkspaceNode } from "./workspaceNode";
import { isWorkspaceResourceNode } from "./workspaceResourceFolderNode";
import { addLibraryGlobs } from "../controllers/libraryController";
import { sendError, sendInfo } from "vscode-extension-telemetry-wrapper";
import { DocumentSymbolNode } from "./documentSymbolNode";
Expand Down Expand Up @@ -241,7 +242,7 @@ export class DragAndDropController implements TreeDragAndDropController<Explorer
}
if (node instanceof WorkspaceNode || node instanceof ProjectNode
|| node instanceof PackageRootNode || node instanceof ContainerNode
|| node instanceof DocumentSymbolNode) {
|| node instanceof DocumentSymbolNode || isWorkspaceResourceNode(node)) {
return false;
}

Expand Down
8 changes: 4 additions & 4 deletions src/views/dependencyDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,16 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
return project?.revealPaths(paths);
}

public async getRootProjects(): Promise<ExplorerNode[]> {
public async getRootProjects(): Promise<ProjectNode[]> {
const rootElements = await this.getRootNodes();
if (rootElements[0] instanceof ProjectNode) {
return rootElements;
return rootElements.filter((node): node is ProjectNode => node instanceof ProjectNode);
} else {
let result: ExplorerNode[] = [];
let result: ProjectNode[] = [];
for (const rootWorkspace of rootElements) {
const projects = await rootWorkspace.getChildren();
if (projects) {
result = result.concat(projects);
result = result.concat(projects.filter((node): node is ProjectNode => node instanceof ProjectNode));
}
}
return result;
Expand Down
21 changes: 17 additions & 4 deletions src/views/workspaceNode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { ThemeIcon } from "vscode";
import { ThemeIcon, Uri } from "vscode";
import { Explorer } from "../constants";
import { Jdtls } from "../java/jdtls";
import { INodeData } from "../java/nodeData";
import { INodeData, NodeKind } from "../java/nodeData";
import { Settings } from "../settings";
import { DataNode } from "./dataNode";
import { ExplorerNode } from "./explorerNode";
import { NodeFactory } from "./nodeFactory";
import { createWorkspaceResourceNode, getWorkspaceResourceData } from "./workspaceResourceFolderNode";

export class WorkspaceNode extends DataNode {
constructor(nodeData: INodeData, parent?: DataNode) {
Expand All @@ -18,14 +20,25 @@ export class WorkspaceNode extends DataNode {
if (!this.nodeData.uri) {
return undefined;
}
return Jdtls.getProjects(this.nodeData.uri);
const projects = await Jdtls.getProjects(this.nodeData.uri);
if (projects.length || Settings.nonJavaResourcesFiltered()) {
return projects;
}

const workspaceUri = Uri.parse(this.nodeData.uri);
return getWorkspaceResourceData(workspaceUri, workspaceUri);
}

protected createChildNodeList(): ExplorerNode[] {
const result: (ExplorerNode | undefined)[] = [];
const workspaceUri = this.uri ? Uri.parse(this.uri) : undefined;
if (this.nodeData.children && this.nodeData.children.length) {
this.nodeData.children.forEach((nodeData) => {
result.push(NodeFactory.createNode(nodeData, this));
if (workspaceUri && (nodeData.kind === NodeKind.Folder || nodeData.kind === NodeKind.File)) {
result.push(createWorkspaceResourceNode(nodeData, this, workspaceUri));
} else {
result.push(NodeFactory.createNode(nodeData, this));
}
});
}
return result.filter(<T>(n?: T): n is T => Boolean(n));
Expand Down
119 changes: 119 additions & 0 deletions src/views/workspaceResourceFolderNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as minimatch from "minimatch";
import * as path from "path";
import { FileType, ThemeIcon, Uri, workspace } from "vscode";
import { Explorer } from "../constants";
import { INodeData, NodeKind } from "../java/nodeData";
import { DataNode } from "./dataNode";
import { ExplorerNode } from "./explorerNode";
import { FileNode } from "./fileNode";

interface ISiblingClause {
when: string;
}

interface IFilesExclude {
[pattern: string]: boolean | ISiblingClause;
}

export async function getWorkspaceResourceData(directoryUri: Uri, workspaceFolderUri: Uri): Promise<INodeData[]> {
const entries = await workspace.fs.readDirectory(directoryUri);
const excludePatterns = workspace.getConfiguration("files", workspaceFolderUri)
.get<IFilesExclude>("exclude", {});
const siblingNames = new Set(entries.map(([name]) => name));

return entries
.map(([name, fileType]) => {
const uri = Uri.joinPath(directoryUri, name);
const relativePath = path.posix.relative(workspaceFolderUri.path, uri.path);
// VS Code's FileType is a bit mask, so symbolic-link directories can include both flags.
// tslint:disable-next-line:no-bitwise
const kind = (fileType & FileType.Directory) !== 0 ? NodeKind.Folder : NodeKind.File;
const nodeData: INodeData = {
name,
path: relativePath,
uri: uri.toString(),
kind,
};
return {
name,
nodeData,
relativePath,
};
})
.filter(({ name, relativePath }) => !isExcluded(name, relativePath, siblingNames, excludePatterns))
.map(({ nodeData }) => nodeData);
}

export function createWorkspaceResourceNode(
nodeData: INodeData,
parent: DataNode,
workspaceFolderUri: Uri,
): ExplorerNode {
switch (nodeData.kind) {
case NodeKind.Folder:
return new WorkspaceResourceFolderNode(nodeData, parent, workspaceFolderUri);
case NodeKind.File:
return new WorkspaceResourceFileNode(nodeData, parent);
default:
throw new Error(`Unsupported workspace resource kind: ${nodeData.kind}`);
}
}

export function isWorkspaceResourceNode(node: ExplorerNode): boolean {
return node instanceof WorkspaceResourceFolderNode || node instanceof WorkspaceResourceFileNode;
}

class WorkspaceResourceFileNode extends FileNode {
protected get contextValue(): string {
return Explorer.ContextValueType.WorkspaceResourceFile;
}
}

class WorkspaceResourceFolderNode extends DataNode {
constructor(nodeData: INodeData, parent: DataNode, private readonly workspaceFolderUri: Uri) {
super(nodeData, parent);
}

protected async loadData(): Promise<INodeData[]> {
if (!this.uri) {
return [];
}
return getWorkspaceResourceData(Uri.parse(this.uri), this.workspaceFolderUri);
}

protected createChildNodeList(): ExplorerNode[] {
return (this.nodeData.children || []).map((nodeData) =>
createWorkspaceResourceNode(nodeData, this, this.workspaceFolderUri));
}

protected get iconPath(): ThemeIcon {
return ThemeIcon.Folder;
}

protected get contextValue(): string {
return Explorer.ContextValueType.WorkspaceResourceFolder;
}
}

function isExcluded(
name: string,
relativePath: string,
siblingNames: Set<string>,
excludePatterns: IFilesExclude,
): boolean {
return Object.entries(excludePatterns).some(([pattern, value]) => {
if (!value || minimatch.match([relativePath], pattern, { dot: true }).length === 0) {
return false;
}
if (typeof value === "boolean") {
return value;
}

const extension = path.posix.extname(name);
const basename = name.substring(0, name.length - extension.length);
return siblingNames.has(value.when.replace("$(basename)", basename));
});
}
18 changes: 16 additions & 2 deletions test/e2e-plans/java-dep-project-explorer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
# - revealInProjectExplorer reveals file from File Explorer context menu
# - hideNonJavaResources removes non-Java workspace roots while preserving
# Java roots across refreshes and newly imported projects
# - showNonJavaResources restores and expands filesystem-only workspace roots
#
# Usage:
# npx autotest run test/e2e-plans/java-dep-project-explorer.yaml --vsix <path-to-vsix>

name: "Java Dependency — Project Explorer"
description: |
Tests the Java Projects explorer view: focus, link/unlink with editor,
reveal in project explorer, and filtering non-Java workspace roots without
hiding Java roots during refresh or project import.
reveal in project explorer, and hiding or expanding non-Java workspace roots
without disrupting Java roots during refresh or project import.

setup:
extension: "redhat.java"
Expand Down Expand Up @@ -314,3 +315,16 @@ steps:
level: 1
inView: "Java Projects"
timeout: 30

- id: "expand-restored-non-java-workspace-root"
action: "expandTreeItem non-java"

- id: "verify-non-java-workspace-resource"
action: "wait 1 seconds"
verifyTreeItem:
name: "package.json"
exact: true
count: 1
level: 2
inView: "Java Projects"
timeout: 15
53 changes: 51 additions & 2 deletions test/multiple-suite/projectView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as assert from "assert";
import * as vscode from "vscode";
import {
Commands, contextManager, DependencyExplorer, Jdtls, ProjectNode, WorkspaceNode,
Commands, contextManager, DependencyExplorer, FileNode, Jdtls, ProjectNode, WorkspaceNode,
} from "../../extension.bundle";
import { setupTestEnv } from "../shared";

Expand All @@ -30,9 +30,18 @@ suite("Multiple Project View Tests", () => {
const nonJavaRoot = roots?.find(root =>
root instanceof WorkspaceNode && root.name === "non-java") as WorkspaceNode | undefined;
assert.ok(nonJavaRoot, "The non-Java workspace folder should have a root node");
assert.equal((await nonJavaRoot!.getChildren()).length, 0, "The non-Java root should not contain Java projects");
const nonJavaChildren = await nonJavaRoot.getChildren();
const packageJson = nonJavaChildren.find((node) => node instanceof FileNode && node.name === "package.json");
assert.ok(packageJson, "The non-Java workspace folder should expose its filesystem resources");
assert.match(
(await packageJson.getTreeItem()).contextValue || "",
/java:workspaceResourceFile(?=.*?\b\+uri\b)/,
"Filesystem-backed resources should use a browse-only context",
);

const projects = await explorer.dataProvider.getRootProjects();
assert.ok(projects.every((node) => node instanceof ProjectNode),
"Filesystem resources should not be returned as root projects");
const project = projects.find((node): node is ProjectNode =>
node instanceof ProjectNode && Boolean(node.uri));
assert.ok(project?.uri, "At least one Java project should be available");
Expand Down Expand Up @@ -81,6 +90,46 @@ suite("Multiple Project View Tests", () => {
}
});

test("Applies boolean and conditional files.exclude to non-Java workspace folders", async function() {
const configuration = vscode.workspace.getConfiguration("files");
const originalWorkspaceValue = configuration.inspect<{
[pattern: string]: boolean | { when: string };
}>("exclude")?.workspaceValue;
const nonJavaFolder = vscode.workspace.workspaceFolders?.find((folder) => folder.name === "non-java");
assert.ok(nonJavaFolder, "The non-Java workspace folder should exist");
const standaloneJs = vscode.Uri.joinPath(nonJavaFolder.uri, "standalone.js");
const pairedJs = vscode.Uri.joinPath(nonJavaFolder.uri, "paired.js");
const pairedTs = vscode.Uri.joinPath(nonJavaFolder.uri, "paired.ts");
const temporaryFiles = [standaloneJs, pairedJs, pairedTs];

await Promise.all(temporaryFiles.map((uri) => vscode.workspace.fs.writeFile(uri, new Uint8Array())));
try {
await configuration.update("exclude", {
"package.json": true,
"**/*.js": { when: "$(basename).ts" },
}, vscode.ConfigurationTarget.Workspace);
await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH);

const explorer = DependencyExplorer.getInstance(contextManager.context);
const roots = await explorer.dataProvider.getChildren();
const nonJavaRoot = roots?.find(root =>
root instanceof WorkspaceNode && root.name === "non-java") as WorkspaceNode | undefined;
assert.ok(nonJavaRoot, "The non-Java workspace folder should have a root node");
const childNames = (await nonJavaRoot.getChildren()).map((node) => node.getDisplayName());
assert.ok(!childNames.includes("package.json"),
"Excluded filesystem resources should not appear in the Java Projects explorer");
assert.ok(childNames.includes("standalone.js"),
"A conditional exclusion should keep files whose sibling does not exist");
assert.ok(!childNames.includes("paired.js"),
"A conditional exclusion should hide files whose configured sibling exists");
assert.ok(childNames.includes("paired.ts"), "The matching sibling should remain visible");
} finally {
await configuration.update("exclude", originalWorkspaceValue, vscode.ConfigurationTarget.Workspace);
await Promise.all(temporaryFiles.map((uri) => vscode.workspace.fs.delete(uri)));
await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH);
}
});

test("Does not add project roots while cached multi-root roots are stale", async function() {
const explorer = DependencyExplorer.getInstance(contextManager.context);
const roots = await explorer.dataProvider.getChildren();
Expand Down