From 89bbfba51b8a8557e32b4cc1b49ee2b46b40265f Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 7 May 2026 10:29:28 +0800 Subject: [PATCH 1/3] perf: narrow to java project to show the explorer --- src/extension.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4c5c9637..86d6e44a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as path from "path"; import { - commands, Diagnostic, Extension, ExtensionContext, extensions, languages, + commands, Diagnostic, Disposable, Extension, ExtensionContext, extensions, languages, Range, tasks, TextDocument, TextEditor, Uri, window, workspace } from "vscode"; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; @@ -40,7 +40,63 @@ export async function activate(context: ExtensionContext): Promise { contextManager.setContextValue(Context.WORKSPACE_CONTAINS_BUILD_FILES, true); } }); - contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); + await activateJavaProjectExplorerWhenJavaContentExists(context); +} + +/** + * The extension is activated by `workspaceContains:*.gradle*` as well, which fires for any + * Gradle workspace regardless of language (Groovy/Grails/Kotlin/etc.). Showing the + * "Java Projects" view in such workspaces is annoying for non-Java users. To avoid that, + * we only flip the `java:projectManagerActivated` context (which controls the view's + * visibility) when we are confident the workspace actually contains Java content: + * 1. The active editor is a Java file (typical when activated via `onLanguage:java`). + * 2. The workspace contains Maven/Eclipse Java metadata (`pom.xml` / `.classpath`). + * 3. The workspace contains at least one `*.java` source file. + * For Gradle-only workspaces without Java sources we install a watcher so the view will + * appear automatically once a Java file is added later. + */ +async function activateJavaProjectExplorerWhenJavaContentExists(context: ExtensionContext): Promise { + let activated = false; + const setActivated = () => { + if (activated) { + return; + } + activated = true; + contextManager.setContextValue(Context.EXTENSION_ACTIVATED, true); + }; + + // Any already-loaded Java document (active or not) is a strong signal. This also covers + // the case where the extension is activated by `onLanguage:java` but `activeTextEditor` + // has not yet been populated. + if (workspace.textDocuments.some((doc) => doc.languageId === "java") + || window.activeTextEditor?.document.languageId === "java") { + setActivated(); + return; + } + + const [javaProjectMetadata, javaSources] = await Promise.all([ + workspace.findFiles("{**/pom.xml,**/.classpath}", undefined, 1), + workspace.findFiles("**/*.java", undefined, 1), + ]); + if (javaProjectMetadata.length > 0 || javaSources.length > 0) { + setActivated(); + return; + } + + // No Java content detected yet. Listen for it to appear via any of these channels: + // - A `*.java` source file being created in the workspace (FileSystemWatcher). + // - A Java document being opened later (e.g. a single file from outside the workspace). + const javaFileWatcher = workspace.createFileSystemWatcher("**/*.java"); + const disposables: Disposable[] = [ + javaFileWatcher, + javaFileWatcher.onDidCreate(setActivated), + workspace.onDidOpenTextDocument((doc) => { + if (doc.languageId === "java") { + setActivated(); + } + }), + ]; + context.subscriptions.push(...disposables); } async function activateExtension(_operationId: string, context: ExtensionContext): Promise { From a269e3d8ce457bed4768373c51c2a24ddd7c7b26 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 7 May 2026 10:46:15 +0800 Subject: [PATCH 2/3] test: add test cases --- test/index.ts | 11 +++ test/non-java-gradle-suite/index.ts | 41 ++++++++++ .../projectExplorerActivation.test.ts | 80 +++++++++++++++++++ test/non-java-gradle/build.gradle | 10 +++ .../src/main/groovy/Hello.groovy | 5 ++ test/suite/extension.test.ts | 12 +++ 6 files changed, 159 insertions(+) create mode 100644 test/non-java-gradle-suite/index.ts create mode 100644 test/non-java-gradle-suite/projectExplorerActivation.test.ts create mode 100644 test/non-java-gradle/build.gradle create mode 100644 test/non-java-gradle/src/main/groovy/Hello.groovy diff --git a/test/index.ts b/test/index.ts index c89db539..f9db1d22 100644 --- a/test/index.ts +++ b/test/index.ts @@ -98,6 +98,17 @@ async function main(): Promise { ], }); + // Run test for non-Java Gradle project (regression test for #921) + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath, + extensionTestsPath: path.resolve(__dirname, "./non-java-gradle-suite"), + launchArgs: [ + path.join(__dirname, "..", "..", "test", "non-java-gradle"), + `--user-data-dir=${userDir}`, + ], + }); + process.exit(0); } catch (err) { diff --git a/test/non-java-gradle-suite/index.ts b/test/non-java-gradle-suite/index.ts new file mode 100644 index 00000000..3e503ad8 --- /dev/null +++ b/test/non-java-gradle-suite/index.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as glob from "glob"; +import * as Mocha from "mocha"; +import * as path from "path"; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + color: true, + timeout: 1 * 60 * 1000, + }); + + const testsRoot = __dirname; + + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} diff --git a/test/non-java-gradle-suite/projectExplorerActivation.test.ts b/test/non-java-gradle-suite/projectExplorerActivation.test.ts new file mode 100644 index 00000000..6161fc0e --- /dev/null +++ b/test/non-java-gradle-suite/projectExplorerActivation.test.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; +import * as fse from "fs-extra"; +import * as path from "path"; +import { extensions, Uri, workspace } from "vscode"; +import { contextManager } from "../../extension.bundle"; +import { sleep } from "../util"; + +const PROJECT_MANAGER_ACTIVATED = "java:projectManagerActivated"; + +// tslint:disable: only-arrow-functions +/** + * Regression tests for https://github.com/microsoft/vscode-java-dependency/issues/921 + * + * The "Java Projects" explorer view's visibility is gated by the `java:projectManagerActivated` + * context. For non-Java Gradle workspaces (e.g. Groovy/Grails) the view used to appear + * unconditionally, which annoyed users that never write Java. The activation logic now + * defers setting that context until actual Java content is detected, and reacts when a + * Java file is added later. + */ +suite("Non-Java Gradle Workspace Activation Tests", () => { + + const workspaceRoot = workspace.workspaceFolders![0].uri.fsPath; + const generatedJavaFile = path.join(workspaceRoot, "Generated.java"); + + suiteSetup(async () => { + // Make sure no leftover from a previous failed run pollutes the workspace. + await fse.remove(generatedJavaFile); + // Activation is auto-triggered by `workspaceContains:build.gradle`, but await it + // explicitly so the test does not race with the activation function. + await extensions.getExtension("vscjava.vscode-java-dependency")!.activate(); + }); + + suiteTeardown(async () => { + await fse.remove(generatedJavaFile); + }); + + test("Should not flip projectManagerActivated when the workspace has no Java content", function() { + const activated = contextManager.getContextValue(PROJECT_MANAGER_ACTIVATED); + assert.notStrictEqual( + activated, + true, + "Java Projects view should stay hidden in a non-Java Gradle workspace (issue #921)", + ); + }); + + test("Should flip projectManagerActivated when a Java source file appears later", async function() { + this.timeout(20 * 1000); + + // Sanity check: still inactive before the file is created. + assert.notStrictEqual( + contextManager.getContextValue(PROJECT_MANAGER_ACTIVATED), + true, + ); + + await fse.outputFile( + generatedJavaFile, + "public class Generated { public static void main(String[] args) {} }\n", + ); + + // Wait for the FileSystemWatcher's onDidCreate event to propagate. + const deadline = Date.now() + 10 * 1000; + while (contextManager.getContextValue(PROJECT_MANAGER_ACTIVATED) !== true + && Date.now() < deadline) { + await sleep(200); + } + + assert.strictEqual( + contextManager.getContextValue(PROJECT_MANAGER_ACTIVATED), + true, + "Java Projects view should become visible after a *.java file is created", + ); + + // Sanity: file actually lives where we expect, in case the watcher is reacting to + // some other event source. + assert.ok(await fse.pathExists(Uri.file(generatedJavaFile).fsPath)); + }); +}); diff --git a/test/non-java-gradle/build.gradle b/test/non-java-gradle/build.gradle new file mode 100644 index 00000000..4311cf43 --- /dev/null +++ b/test/non-java-gradle/build.gradle @@ -0,0 +1,10 @@ +// A Gradle build file used to simulate a non-Java workspace (e.g. Groovy/Grails) +// that should NOT trigger the "Java Projects" explorer view. +// See: https://github.com/microsoft/vscode-java-dependency/issues/921 +plugins { + id 'groovy' +} + +repositories { + mavenCentral() +} diff --git a/test/non-java-gradle/src/main/groovy/Hello.groovy b/test/non-java-gradle/src/main/groovy/Hello.groovy new file mode 100644 index 00000000..4b6bebec --- /dev/null +++ b/test/non-java-gradle/src/main/groovy/Hello.groovy @@ -0,0 +1,5 @@ +class Hello { + static void main(String[] args) { + println 'Hello from Groovy!' + } +} diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 8b6e2864..625b9957 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -3,6 +3,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; +import { contextManager } from "../../extension.bundle"; // tslint:disable: only-arrow-functions // Defines a Mocha test suite to group tests of similar kind together @@ -16,4 +17,15 @@ suite("Extension Tests", () => { await vscode.extensions.getExtension("vscjava.vscode-java-dependency")!.activate(); assert.ok(true); }); + + test("Should flip projectManagerActivated when the workspace contains Java content", async function() { + await vscode.extensions.getExtension("vscjava.vscode-java-dependency")!.activate(); + // The general suite runs against `test/java9`, which contains *.java sources, so the + // explorer-visibility context must be set. Guards against regressions of issue #921 in + // the opposite direction (i.e. the view erroneously hidden for real Java workspaces). + assert.strictEqual( + contextManager.getContextValue("java:projectManagerActivated"), + true, + ); + }); }); From a1334d490d769653cbfec38b4268a04d9f824788 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 7 May 2026 12:24:41 +0800 Subject: [PATCH 3/3] test(e2e): use expectConfirmDialog for delete confirm The original windowsUI failure on this PR (run 25473266463) showed delete-context-menu silently passing while the Delete command never fired, only surfacing 30s later via verify-deleted. Root cause: the lenient confirmDialog swallowed the missing dialog and the substring-only contextMenu matcher did not validate the click landed. vscjava/vscode-autotest 0.6.7 fixes both: contextMenuOnTreeItem now pre-selects, smart-matches the menu item, and verifies the menu dismisses; expectConfirmDialog throws when the dialog is absent. This change opts the delete confirm step into the strict variant so a missing dialog fails fast at the action site instead of being masked. The lenient confirmDialog is kept for the rename flow (handle-rename-dialog) where the dialog is optional depending on user settings. The CI Setup autotest step continues to install @latest from npm; this PR is expected to land after autotest 0.6.7 is published so the new verb is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/e2e-plans/java-dep-file-operations.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e-plans/java-dep-file-operations.yaml b/test/e2e-plans/java-dep-file-operations.yaml index 59032d51..7b89d837 100644 --- a/test/e2e-plans/java-dep-file-operations.yaml +++ b/test/e2e-plans/java-dep-file-operations.yaml @@ -196,9 +196,12 @@ steps: action: "contextMenu AppToDelete Delete" verify: "Delete confirmation triggered" - # VSCode shows a platform-specific confirmation dialog for delete + # VSCode shows a platform-specific confirmation dialog for delete. + # Use the strict variant: throw if the dialog is not present, so a + # silently-failed delete-context-menu surfaces here rather than 30s later + # at verify-deleted. Requires @vscjava/vscode-autotest >= 0.6.7. - id: "confirm-delete" - action: "confirmDialog" + action: "expectConfirmDialog" - id: "wait-delete" action: "wait 3 seconds"