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
60 changes: 58 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -40,7 +40,63 @@ export async function activate(context: ExtensionContext): Promise<void> {
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<void> {
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<void> {
Expand Down
7 changes: 5 additions & 2 deletions test/e2e-plans/java-dep-file-operations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ async function main(): Promise<void> {
],
});

// 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) {
Expand Down
41 changes: 41 additions & 0 deletions test/non-java-gradle-suite/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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);
}
});
});
}
80 changes: 80 additions & 0 deletions test/non-java-gradle-suite/projectExplorerActivation.test.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>(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<boolean>(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<boolean>(PROJECT_MANAGER_ACTIVATED) !== true
&& Date.now() < deadline) {
await sleep(200);
}

assert.strictEqual(
contextManager.getContextValue<boolean>(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));
});
});
10 changes: 10 additions & 0 deletions test/non-java-gradle/build.gradle
Original file line number Diff line number Diff line change
@@ -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()
}
5 changes: 5 additions & 0 deletions test/non-java-gradle/src/main/groovy/Hello.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Hello {
static void main(String[] args) {
println 'Hello from Groovy!'
}
}
12 changes: 12 additions & 0 deletions test/suite/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<boolean>("java:projectManagerActivated"),
true,
);
});
});
Loading