diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestSearchUtils.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestSearchUtils.java index 71ec7032..92220a71 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestSearchUtils.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestSearchUtils.java @@ -62,6 +62,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -76,12 +77,14 @@ public static List findJavaProjects(List arguments, IProgr return Collections.emptyList(); } final String workspaceFolderUri = (String) arguments.get(0); - final IPath workspaceFolderPath = ResourceUtils.filePathFromURI(workspaceFolderUri); + final IPath workspaceFolderPath = ResourceUtils.canonicalFilePathFromURI(workspaceFolderUri); if (workspaceFolderPath == null) { JUnitPlugin.logError("Failed to parse workspace folder path from uri: " + workspaceFolderUri); // todo: handle non-file scheme return Collections.emptyList(); } + + final String invisibleProjectName = ProjectUtils.getWorkspaceInvisibleProjectName(workspaceFolderPath); final List resultList = new LinkedList<>(); for (final IJavaProject project : ProjectUtils.getJavaProjects()) { if (monitor != null && monitor.isCanceled()) { @@ -92,6 +95,15 @@ public static List findJavaProjects(List arguments, IProgr continue; } + // Ignore all the projects that's not contained in the workspace folder, except + // for the invisible project. This is to make sure in a multi-roots workspace, an + // out-of-date invisible project won't be listed in the result. + if ((!ResourceUtils.isContainedIn(project.getProject().getLocation(), + Collections.singletonList(workspaceFolderPath)) && !Objects.equals(project.getProject().getName(), + invisibleProjectName))) { + continue; + } + try { resultList.add(TestItemUtils.constructJavaTestItem(project, TestLevel.PROJECT, TestKind.None)); } catch (JavaModelException e) { diff --git a/src/controller/testController.ts b/src/controller/testController.ts index c99a2d23..c973c454 100644 --- a/src/controller/testController.ts +++ b/src/controller/testController.ts @@ -6,7 +6,7 @@ import { CancellationToken, DebugConfiguration, Disposable, FileSystemWatcher, R import { instrumentOperation, sendError, sendInfo } from 'vscode-extension-telemetry-wrapper'; import { INVOCATION_PREFIX } from '../constants'; import { IProgressReporter } from '../debugger.api'; -import { extensionContext, isStandardServerReady, progressProvider } from '../extension'; +import { isStandardServerReady, progressProvider } from '../extension'; import { testSourceProvider } from '../provider/testSourceProvider'; import { IExecutionConfig } from '../runConfigs'; import { BaseRunner } from '../runners/baseRunner/BaseRunner'; @@ -19,6 +19,7 @@ import { dataCache, ITestItemData } from './testItemDataCache'; import { findDirectTestChildrenForClass, findTestPackagesAndTypes, findTestTypesAndMethods, loadJavaProjects, resolvePath, synchronizeItemsRecursively, updateItemForDocumentWithDebounce } from './utils'; export let testController: TestController | undefined; +export const watchers: Disposable[] = []; export function createTestController(): void { if (!isStandardServerReady()) { @@ -67,11 +68,15 @@ async function startWatchingWorkspace(): Promise { return; } + for (const disposable of watchers) { + disposable.dispose(); + } + for (const workspaceFolder of workspace.workspaceFolders) { const patterns: RelativePattern[] = await testSourceProvider.getTestSourcePattern(workspaceFolder); for (const pattern of patterns) { const watcher: FileSystemWatcher = workspace.createFileSystemWatcher(pattern); - extensionContext.subscriptions.push( + watchers.push( watcher, watcher.onDidCreate(async (uri: Uri) => { const testTypes: IJavaTestItem[] = await findTestTypesAndMethods(uri.toString()); diff --git a/src/extension.ts b/src/extension.ts index ed85fcc1..c2a372a4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,14 +2,14 @@ // Licensed under the MIT license. import * as path from 'path'; -import { commands, DebugConfiguration, Event, Extension, ExtensionContext, extensions, TestItem, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, workspace } from 'vscode'; +import { commands, DebugConfiguration, Event, Extension, ExtensionContext, extensions, TestItem, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand } from 'vscode-extension-telemetry-wrapper'; import { generateTests, registerAdvanceAskForChoice, registerAskForChoiceCommand, registerAskForInputCommand } from './commands/generationCommands'; import { runTestsFromJavaProjectExplorer } from './commands/projectExplorerCommands'; import { refresh, runTestsFromTestExplorer } from './commands/testExplorerCommands'; import { openStackTrace } from './commands/testReportCommands'; import { Context, ExtensionName, JavaTestRunnerCommands, VSCodeCommands } from './constants'; -import { createTestController, testController } from './controller/testController'; +import { createTestController, testController, watchers } from './controller/testController'; import { updateItemForDocument, updateItemForDocumentWithDebounce } from './controller/utils'; import { IProgressProvider } from './debugger.api'; import { initExpService } from './experimentationService'; @@ -30,6 +30,9 @@ export async function deactivate(): Promise { disposeCodeActionProvider(); await disposeTelemetryWrapper(); testController?.dispose(); + for (const disposable of watchers) { + disposable.dispose(); + } } async function doActivate(_operationId: string, context: ExtensionContext): Promise { @@ -116,6 +119,17 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom } await updateItemForDocumentWithDebounce(e.document.uri); }), + workspace.onDidChangeWorkspaceFolders(async (e: WorkspaceFoldersChangeEvent) => { + for (const deletedFolder of e.removed) { + testSourceProvider.delete(deletedFolder.uri); + } + // workaround to wait for Java Language Server to accept the workspace folder change event, + // otherwise we cannot find the projects in the new workspace folder. + // TODO: this event should be notified by onDidProjectsImport, we need to fix upstream + setTimeout(() => { + createTestController(); + }, 1000); + }), ); if (isStandardServerReady()) { diff --git a/src/provider/testSourceProvider.ts b/src/provider/testSourceProvider.ts index b9dafcbc..d9fab1e4 100644 --- a/src/provider/testSourceProvider.ts +++ b/src/provider/testSourceProvider.ts @@ -51,6 +51,10 @@ class TestSourcePathProvider { this.testSourceMapping.clear(); } + public delete(workspaceUri: Uri): boolean { + return this.testSourceMapping.delete(workspaceUri); + } + private async getTestPaths(workspaceFolder: WorkspaceFolder): Promise { let testPaths: ITestSourcePath[] | undefined = this.testSourceMapping.get(workspaceFolder.uri); if (!testPaths) {