diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ResourceSet.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ResourceSet.java index dde7eec5..55bfeb89 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ResourceSet.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ResourceSet.java @@ -16,6 +16,7 @@ import java.util.Objects; import org.eclipse.core.internal.utils.FileUtil; +import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; @@ -109,7 +110,9 @@ public void accept(ResourceVisitor visitor) { visitor.visit((IFile) resource); } } else if (resource instanceof IFolder) { - if (shouldVisit((IFolder) resource)) { + if (shouldVisit((IFolder) resource) + && (!containsSourceClasspathEntry((IFolder) resource) + || hasVisibleNonJavaResources((IFolder) resource))) { visitor.visit((IFolder) resource); } } else if (resource instanceof IJarEntryResource) { @@ -152,4 +155,49 @@ private boolean shouldVisit(IResource resource) { return JavaCore.create(resource) == null; } + + private boolean containsSourceClasspathEntry(IContainer container) { + try { + IJavaProject javaProject = JavaCore.create(container.getProject()); + if (javaProject == null) { + return false; + } + IPath containerPath = container.getFullPath(); + if (containerPath.equals(javaProject.getOutputLocation())) { + return false; + } + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE + && containerPath.isPrefixOf(entry.getPath())) { + return true; + } + } + } catch (CoreException e) { + JdtlsExtActivator.logException("Failed to inspect Java source entries", e); + } + return false; + } + + private boolean hasVisibleNonJavaResources(IContainer container) { + try { + for (IResource member : container.members()) { + if (JavaCore.create(member) != null) { + continue; + } + if (member instanceof IFile) { + return true; + } + if (member instanceof IContainer) { + IContainer child = (IContainer) member; + if (!containsSourceClasspathEntry(child) || hasVisibleNonJavaResources(child)) { + return true; + } + } + } + } catch (CoreException e) { + JdtlsExtActivator.logException("Failed to inspect non-Java resources", e); + return true; + } + return false; + } } diff --git a/test/e2e-plans/java-dep-view-modes.yaml b/test/e2e-plans/java-dep-view-modes.yaml index 51f4f258..85ff1ee5 100644 --- a/test/e2e-plans/java-dep-view-modes.yaml +++ b/test/e2e-plans/java-dep-view-modes.yaml @@ -222,3 +222,44 @@ steps: exact: true inView: "Java Projects" timeout: 30 + + # ── Test 6: do not show empty physical ancestors of package roots (#1062) ── + # src/main/java and src/main/resources are represented as Java package roots. + # The physical src → main hierarchy must not also appear as an empty normal + # folder tree when non-Java resources are shown. + - id: "reset-tree-for-non-java-ancestor-check" + action: "executeVSCodeCommand workbench.actions.treeView.javaProjectExplorer.collapseAll" + + - id: "expand-project-for-non-java-ancestor-check" + action: "expandTreeItem my-app" + waitBefore: 2 + + - id: "verify-resources-root-visible" + action: "wait 1 seconds" + verifyTreeItem: + name: "src/main/resources" + exact: true + level: 2 + inView: "Java Projects" + timeout: 15 + + - id: "expand-resources-root" + action: "expandTreeItem src/main/resources" + + - id: "verify-resource-file-visible" + action: "wait 1 seconds" + verifyTreeItem: + name: "application.yml" + exact: true + inView: "Java Projects" + timeout: 15 + + - id: "verify-empty-physical-src-hidden" + action: "wait 1 seconds" + verifyTreeItem: + name: "src" + exact: true + level: 2 + visible: false + inView: "Java Projects" + timeout: 15 diff --git a/test/maven-suite/projectView.test.ts b/test/maven-suite/projectView.test.ts index f5c545f4..ff3c5db5 100644 --- a/test/maven-suite/projectView.test.ts +++ b/test/maven-suite/projectView.test.ts @@ -3,7 +3,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import { Commands, ContainerNode, contextManager, DataNode, DependencyExplorer, FileNode, +import { Commands, ContainerNode, contextManager, DataNode, DependencyExplorer, FileNode, FolderNode, INodeData, Jdtls, languageServerApiManager, NodeKind, PackageNode, PackageRootNode, PrimaryTypeNode, ProjectNode } from "../../extension.bundle"; import { fsPath, printNodes, setupTestEnv, Uris } from "../shared"; @@ -247,6 +247,61 @@ suite("Maven Project View Tests", () => { assert.ok(!projectChildren.find((node: DataNode) => node.nodeData.name === ".hidden")); }); + test("Does not display empty physical ancestors of Java package roots", async function() { + const explorer = DependencyExplorer.getInstance(contextManager.context); + const projectNode = (await explorer.dataProvider.getChildren())![0] as ProjectNode; + const projectChildren = await projectNode.getChildren(); + + assert.ok(!projectChildren.find((node: DataNode) => node.nodeData.name === "src"), + "The physical src folder should not duplicate Java package roots"); + + const resourcesRoot = projectChildren.find((node: DataNode) => + node.nodeData.name === "src/main/resources") as PackageRootNode; + assert.ok(resourcesRoot, "The Maven resources root should remain visible"); + const resourceChildren = await resourcesRoot.getChildren(); + assert.ok(resourceChildren.find((node: DataNode) => node.nodeData.name === "application.yml"), + "Non-Java files under the resources root should remain visible"); + }); + + test("Displays empty non-Java folders next to Java package roots", async function() { + const explorer = DependencyExplorer.getInstance(contextManager.context); + const projectNode = (await explorer.dataProvider.getChildren())![0] as ProjectNode; + const initialChildren = await projectNode.getChildren(); + const mainSourceRoot = initialChildren.find((node: DataNode) => + node.nodeData.name === "src/main/java") as PackageRootNode; + const srcPath = mainSourceRoot.nodeData.path!.replace(/\/main\/java$/, ""); + const docsUri = vscode.Uri.joinPath(vscode.Uri.file(Uris.MAVEN_PROJECT_NODE), "src", "docs"); + await vscode.workspace.fs.createDirectory(docsUri); + + try { + await vscode.commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, + Commands.JAVA_GETPACKAGEDATA, { + kind: NodeKind.Folder, + projectUri: projectNode.uri, + path: srcPath, + }); + await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH); + const refreshedProjectNode = (await explorer.dataProvider.getChildren())![0] as ProjectNode; + const projectChildren = await refreshedProjectNode.getChildren(); + const srcFolder = projectChildren.find((node: DataNode) => + node.nodeData.name === "src") as FolderNode; + + assert.ok(srcFolder, "The physical src folder should remain visible when it contains an empty non-Java folder"); + const srcChildren = await srcFolder.getChildren(); + assert.ok(srcChildren.find((node: DataNode) => node.nodeData.name === "docs"), + "The empty non-Java folder should remain visible"); + } finally { + await vscode.workspace.fs.delete(docsUri, { recursive: true }); + await vscode.commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, + Commands.JAVA_GETPACKAGEDATA, { + kind: NodeKind.Folder, + projectUri: projectNode.uri, + path: srcPath, + }); + await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH); + } + }); + test("Can apply 'java.project.explorer.showNonJavaResources'", async function() { await vscode.workspace.getConfiguration("java.project.explorer").update( "showNonJavaResources", diff --git a/test/maven/src/main/resources/application.yml b/test/maven/src/main/resources/application.yml new file mode 100644 index 00000000..b6c6235b --- /dev/null +++ b/test/maven/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + application: + name: my-app diff --git a/test/simple-suite/projectView.test.ts b/test/simple-suite/projectView.test.ts index 29e39b97..eef03ce9 100644 --- a/test/simple-suite/projectView.test.ts +++ b/test/simple-suite/projectView.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as assert from "assert"; -import { ContainerNode, contextManager, DependencyExplorer, +import { ContainerNode, contextManager, DataNode, DependencyExplorer, PackageRootNode, PrimaryTypeNode, ProjectNode } from "../../extension.bundle"; import { fsPath, setupTestEnv, Uris } from "../shared"; @@ -22,7 +22,10 @@ suite("Simple Project View Tests", () => { // validate package root/dependency nodes const projectChildren = await projectNode.getChildren(); - assert.equal(projectChildren.length, 6, "Number of children nodes should be 6"); + assert.equal(projectChildren.length, 5, + `Number of children nodes should be 5: ${projectChildren.map((node: DataNode) => node.name).join(", ")}`); + assert.ok(!projectChildren.find((node: DataNode) => node.name === "src"), + "The empty physical source folder should not be visible"); const mainPackage = projectChildren[0] as PackageRootNode; assert.equal(mainPackage.name, "src/main/java", "Package name should be \"src/main/java\""); const systemLibrary = projectChildren[1] as ContainerNode;