diff --git a/src/commands/testExplorerCommands.ts b/src/commands/testExplorerCommands.ts index b35f66f6..6a7c21f2 100644 --- a/src/commands/testExplorerCommands.ts +++ b/src/commands/testExplorerCommands.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { DebugConfiguration, TestItem, TestRunRequest } from 'vscode'; +import { DebugConfiguration, TestItem, TestRunRequest, Uri } from 'vscode'; import { sendInfo } from 'vscode-extension-telemetry-wrapper'; -import { runTests, testController } from '../controller/testController'; +import { loadChildren, runTests, testController } from '../controller/testController'; import { loadJavaProjects } from '../controller/utils'; import { showTestItemsInCurrentFile } from '../extension'; @@ -39,10 +39,46 @@ export async function runTestsFromTestExplorer(testItem: TestItem, launchConfigu export async function refreshExplorer(): Promise { sendInfo('', { name: 'refreshTests' }); + + await loadJavaProjects(); + + // Force re-resolution of all existing project roots + const loadPromises: Promise[] = []; testController?.items.forEach((root: TestItem) => { - testController?.items.delete(root.id); + loadPromises.push(loadChildren(root)); }); + await Promise.all(loadPromises); - await loadJavaProjects(); + await showTestItemsInCurrentFile(); +} + +/** + * Refresh only the project subtree that matches the given classpath-change URI. + * Falls back to a full (incremental) refresh if no matching project is found. + */ +export async function refreshProject(classpathUri: Uri): Promise { + sendInfo('', { name: 'refreshProject' }); + const uriString: string = classpathUri.toString(); + + // Find the project root with the longest matching URI prefix (most specific match) + let matchedProject: TestItem | undefined; + let matchedUriLength: number = 0; + testController?.items.forEach((root: TestItem) => { + if (root.uri) { + const rootUriString: string = root.uri.toString(); + if (uriString.startsWith(rootUriString) && rootUriString.length > matchedUriLength) { + matchedProject = root; + matchedUriLength = rootUriString.length; + } + } + }); + + if (matchedProject) { + // Re-resolve only the matched project's children + await loadChildren(matchedProject); + } else { + // No matching project found – do incremental full refresh + await loadJavaProjects(); + } await showTestItemsInCurrentFile(); } diff --git a/src/controller/utils.ts b/src/controller/utils.ts index af54caeb..946d4c97 100644 --- a/src/controller/utils.ts +++ b/src/controller/utils.ts @@ -34,6 +34,14 @@ export async function loadJavaProjects(): Promise { return project.testKind !== TestKind.None; }); + // Remove projects that no longer exist + const projectIds: Set = new Set(testProjects.map((p: IJavaTestItem) => p.id)); + testController?.items.forEach((root: TestItem) => { + if (!projectIds.has(root.id)) { + testController?.items.delete(root.id); + } + }); + for (const project of testProjects) { if (testController?.items.get(project.id)) { continue; diff --git a/src/extension.ts b/src/extension.ts index 24051eb2..18a786c7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,7 @@ import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentO import { navigateToTestOrTarget } from './commands/navigation/navigationCommands'; import { generateTests } from './commands/generationCommands'; import { runTestsFromJavaProjectExplorer } from './commands/projectExplorerCommands'; -import { refreshExplorer, runTestsFromTestExplorer } from './commands/testExplorerCommands'; +import { refreshExplorer, refreshProject, runTestsFromTestExplorer } from './commands/testExplorerCommands'; import { openStackTrace } from './commands/testReportCommands'; import { Context, ExtensionName, JavaTestRunnerCommands, VSCodeCommands } from './constants'; import { createTestController, testController, watchers } from './controller/testController'; @@ -76,11 +76,11 @@ async function doActivate(_operationId: string, context: ExtensionContext): Prom if (extensionApi.onDidClasspathUpdate) { const onDidClasspathUpdate: Event = extensionApi.onDidClasspathUpdate; - context.subscriptions.push(onDidClasspathUpdate(async () => { + context.subscriptions.push(onDidClasspathUpdate(async (uri: Uri) => { // workaround: wait more time to make sure Language Server has updated all caches setTimeout(() => { testSourceProvider.clear(); - refreshExplorer(); + refreshProject(uri); }, 1000 /* ms */); })); }