diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index a2f25e5..e81613a 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java @@ -39,11 +39,18 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleBuildTarget(arguments); case "bazel-jdt.setActiveDebugProject": return handleSetActiveDebugProject(arguments); + case "bazel-jdt.clearActiveDebugProject": + return handleClearActiveDebugProject(); default: return null; } } + private Object handleClearActiveDebugProject() { + BazelRuntimeClasspathEntryResolver.clearActiveDebugProject(); + return null; + } + private Object handleImportProject(List arguments) { try { BazelBridge bridge = BazelBridge.getInstance(); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java index 4bf0e54..d380f4d 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java @@ -64,22 +64,72 @@ static String resolveFallbackJar(String missingPath, String workspacePath) { if (outputBase == null) return null; String repoName = extractRepoName(path); - if (repoName == null) return null; - - File repoDir = new File(outputBase, "external/" + repoName); - if (!repoDir.isDirectory()) { - repoDir = findBzlmodRepoDir(outputBase, repoName); + if (repoName != null) { + File repoDir = new File(outputBase, "external/" + repoName); + if (!repoDir.isDirectory()) { + repoDir = findBzlmodRepoDir(outputBase, repoName); + } + if (repoDir != null && repoDir.isDirectory()) { + String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); + if (found != null) { + LOG.info("Fallback JAR resolved: " + path + " -> " + found); + return found; + } + } + return null; } - if (repoDir == null || !repoDir.isDirectory()) return null; - String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); - if (found != null) { - LOG.info("Fallback JAR resolved: " + path + " -> " + found); - } - return found; + return resolveBuildOutputJar(path, outputBase); }); } + static String resolveBuildOutputJar(String missingPath, String outputBase) { + String artifactName = extractArtifactNameFromLibJar(missingPath); + if (artifactName == null) return null; + + File repoDir = findCandidateRepoDir(outputBase, artifactName); + if (repoDir == null) return null; + + String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); + if (found != null) { + LOG.info("Build-output JAR resolved: " + missingPath + " -> " + found); + } + return found; + } + + static String extractArtifactNameFromLibJar(String path) { + String fileName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash >= 0) { + fileName = path.substring(lastSlash + 1); + } + if (!fileName.startsWith("lib") || !fileName.endsWith(".jar")) return null; + String name = fileName.substring(3, fileName.length() - 4); + if (name.isEmpty()) return null; + return name; + } + + static File findCandidateRepoDir(String outputBase, String artifactName) { + File externalRoot = new File(outputBase, "external"); + if (!externalRoot.isDirectory()) return null; + + File exact1 = new File(externalRoot, artifactName + "_" + artifactName); + if (exact1.isDirectory()) return exact1; + + File exact2 = new File(externalRoot, artifactName); + if (exact2.isDirectory()) return exact2; + + File[] prefixMatches = externalRoot.listFiles((dir, name) -> + name.startsWith(artifactName + "_")); + if (prefixMatches != null && prefixMatches.length > 0) { + for (File candidate : prefixMatches) { + if (candidate.isDirectory()) return candidate; + } + } + + return findBzlmodRepoDir(outputBase, artifactName); + } + static String extractRepoName(String path) { int idx = path.indexOf("/external/"); if (idx < 0) return null; diff --git a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml index 59de6c0..e2dd134 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -26,6 +26,7 @@ + diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java index ede62b7..3f61b7c 100644 --- a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java +++ b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java @@ -126,12 +126,171 @@ public void resolveFallbackJarReturnsNullWhenNoJarExists() throws IOException { } @Test - public void resolveFallbackJarReturnsNullForNonExternalPath() { + public void resolveFallbackJarResolvesLibJarViaBuildOutput() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/junit_junit/jar"); + assertTrue(repoDir.mkdirs()); + File jar = new File(repoDir, "junit-4.13.2.jar"); + assertTrue(jar.createNewFile()); + + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, outputBase); + + String result = BazelExternalRepoResolver.resolveFallbackJar( + "/workspace/bazel-out/darwin_arm64-fastbuild/bin/3rdparty/libjunit.jar", wsPath); + assertNotNull("Should resolve lib.jar via build-output fallback", result); + assertEquals(jar.getAbsolutePath(), result); + } + + @Test + public void resolveFallbackJarReturnsNullForNonExternalNonLibPath() { String wsPath = tempDir.resolve("workspace").toString(); BazelExternalRepoResolver.setOutputBaseForTest(wsPath, tempDir.toString()); String result = BazelExternalRepoResolver.resolveFallbackJar( - "/workspace/bazel-out/bin/3rdparty/libjunit.jar", wsPath); + "/workspace/bazel-out/bin/3rdparty/junit.jar", wsPath); + assertNull(result); + } + + // --- extractArtifactNameFromLibJar tests --- + + @Test + public void extractArtifactNameFromValidLibJar() { + assertEquals("junit", + BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "/workspace/bazel-out/bin/3rdparty/libjunit.jar")); + } + + @Test + public void extractArtifactNameFromLibJarWithUnderscore() { + assertEquals("hamcrest_core", + BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/libhamcrest_core.jar")); + } + + @Test + public void extractArtifactNameReturnsNullForNonLibPrefix() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/junit.jar")); + } + + @Test + public void extractArtifactNameReturnsNullForNonJarExtension() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/libfoo.txt")); + } + + @Test + public void extractArtifactNameReturnsNullForEmptyName() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/lib.jar")); + } + + @Test + public void extractArtifactNameFromFileNameOnly() { + assertEquals("guava", + BazelExternalRepoResolver.extractArtifactNameFromLibJar("libguava.jar")); + } + + // --- findCandidateRepoDir tests --- + + @Test + public void findCandidateRepoDirExactDoubleMatch() throws IOException { + String outputBase = tempDir.toString(); + File exact = new File(outputBase, "external/junit_junit"); + assertTrue(exact.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(exact.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirExactSingleMatch() throws IOException { + String outputBase = tempDir.toString(); + File exact = new File(outputBase, "external/guava"); + assertTrue(exact.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "guava"); + assertNotNull(result); + assertEquals(exact.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirPrefersDoubleOverSingle() throws IOException { + String outputBase = tempDir.toString(); + File single = new File(outputBase, "external/junit"); + assertTrue(single.mkdirs()); + File double_ = new File(outputBase, "external/junit_junit"); + assertTrue(double_.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(double_.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirPrefixMatch() throws IOException { + String outputBase = tempDir.toString(); + File prefixed = new File(outputBase, "external/guava_guava_jre"); + assertTrue(prefixed.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "guava"); + assertNotNull(result); + assertEquals(prefixed.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirBzlmodMatch() throws IOException { + String outputBase = tempDir.toString(); + File bzlmod = new File(outputBase, "external/rules_jvm_external~~maven~junit"); + assertTrue(bzlmod.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(bzlmod.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirNoMatch() throws IOException { + String outputBase = tempDir.toString(); + File externalDir = new File(outputBase, "external"); + assertTrue(externalDir.mkdirs()); + + assertNull(BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "nonexistent")); + } + + // --- resolveBuildOutputJar end-to-end tests --- + + @Test + public void resolveBuildOutputJarEndToEnd() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/hamcrest_core/jar"); + assertTrue(repoDir.mkdirs()); + File jar = new File(repoDir, "hamcrest-core-1.3.jar"); + assertTrue(jar.createNewFile()); + + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/k8-fastbuild/bin/3rdparty/libhamcrest_core.jar", outputBase); + assertNotNull(result); + assertEquals(jar.getAbsolutePath(), result); + } + + @Test + public void resolveBuildOutputJarReturnsNullForNonLibJar() { + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/bin/3rdparty/junit.jar", tempDir.toString()); + assertNull(result); + } + + @Test + public void resolveBuildOutputJarReturnsNullWhenNoRepoDir() throws IOException { + String outputBase = tempDir.toString(); + File externalDir = new File(outputBase, "external"); + assertTrue(externalDir.mkdirs()); + + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/bin/3rdparty/libunknown.jar", outputBase); assertNull(result); } diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index 14237c3..d53d63c 100644 --- a/bazel-jdt-bridge/vscode-extension/src/extension.ts +++ b/bazel-jdt-bridge/vscode-extension/src/extension.ts @@ -40,6 +40,21 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { ) ); + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession(async (session) => { + if (session.type === 'java') { + try { + await vscode.commands.executeCommand( + 'java.execute.workspaceCommand', + 'bazel-jdt.clearActiveDebugProject' + ); + } catch { + // LSP connection may be closed — safe to ignore + } + } + }) + ); + let dependencyPackageCache: string[] = []; const bazelprojectPattern = new vscode.RelativePattern(workspaceRoot, '.bazelproject'); @@ -89,6 +104,7 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { } } + refreshTestDiscovery(); vscode.window.showInformationMessage('Bazel project re-imported (scope changed)'); } catch { // Silently ignore — re-import is best-effort @@ -103,6 +119,13 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { statusBarItem, ); + // Trigger test discovery after JDT.LS finishes initial import + const javaExt = vscode.extensions.getExtension('redhat.java'); + const javaApi = javaExt?.exports; + if (javaApi?.serverReady) { + javaApi.serverReady().then(() => refreshTestDiscovery()).catch(() => {}); + } + // On-demand dependency source loading: monitor opened Java files context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(async (doc) => { @@ -156,6 +179,19 @@ function setupCreationOnlyWatcher(context: vscode.ExtensionContext, workspaceRoo context.subscriptions.push(watcher); } +async function refreshTestDiscovery(): Promise { + try { + const testExtension = vscode.extensions.getExtension('vscjava.vscode-java-test'); + if (!testExtension) return; + if (!testExtension.isActive) { + await testExtension.activate(); + } + await vscode.commands.executeCommand('testing.refreshTests'); + } catch { + // Test discovery is best-effort — never block import + } +} + export async function deactivate() { try { await vscode.commands.executeCommand(