From fa90a3f9239a7c6b2ef9b4f1f2bf655d164a392b Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Fri, 27 Mar 2026 14:36:36 +0800 Subject: [PATCH 1/7] feat: progressive project tree view during import Show Java project names in the tree view progressively as they are imported, instead of waiting for the entire import to complete. Key changes: - Use serverRunning() API (v0.14) instead of serverReady() so the tree view can start rendering before import finishes - Add addProgressiveProjects() to create ProjectNode items directly from ProjectsImported notification URIs without querying the server - Guard getChildren() from entering getRootNodes() during progressive loading to avoid blocking on server queries - Keep TreeView progress spinner visible until first items arrive - After import completes, trigger full refresh to replace placeholder items with complete data from the server This reduces perceived loading time for large projects (e.g., 436 Gradle subprojects) from ~7 minutes to ~1 minute. --- package-lock.json | 2 +- src/commands.ts | 2 + .../languageServerApiManager.ts | 56 ++++++++++++- src/views/dependencyDataProvider.ts | 80 +++++++++++++++++++ 4 files changed, 135 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4078800b..c64f668b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "vscode-java-dependency", - "version": "0.27.0", + "version": "0.27.1", "license": "MIT", "dependencies": { "@github/copilot-language-server": "^1.388.0", diff --git a/src/commands.ts b/src/commands.ts index ec7934cc..572a81c7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -26,6 +26,8 @@ export namespace Commands { export const VIEW_PACKAGE_INTERNAL_REFRESH = "_java.view.package.internal.refresh"; + export const VIEW_PACKAGE_INTERNAL_ADD_PROJECTS = "_java.view.package.internal.addProjects"; + export const VIEW_PACKAGE_OUTLINE = "java.view.package.outline"; export const VIEW_PACKAGE_REVEAL_FILE_OS = "java.view.package.revealFileInOS"; diff --git a/src/languageServerApi/languageServerApiManager.ts b/src/languageServerApi/languageServerApiManager.ts index 494c08a4..48bb129f 100644 --- a/src/languageServerApi/languageServerApiManager.ts +++ b/src/languageServerApi/languageServerApiManager.ts @@ -13,6 +13,7 @@ class LanguageServerApiManager { private extensionApi: any; private isServerReady: boolean = false; + private isServerRunning: boolean = false; public async ready(): Promise { if (this.isServerReady) { @@ -28,6 +29,29 @@ class LanguageServerApiManager { return false; } + // Use serverRunning() if available (API >= 0.14) for progressive loading. + // This resolves when the server process is alive and can handle requests, + // even if project imports haven't completed yet. This enables the tree view + // to show projects incrementally as they are imported. + if (!this.isServerRunning && this.extensionApi.serverRunning) { + await this.extensionApi.serverRunning(); + this.isServerRunning = true; + // Start background wait for full server readiness (import complete). + // When the server finishes importing, trigger a full refresh to replace + // progressive placeholder items with proper data from the server. + if (this.extensionApi.serverReady) { + this.extensionApi.serverReady().then(() => { + this.isServerReady = true; + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); + }); + } + return true; + } + if (this.isServerRunning) { + return true; + } + + // Fallback for older API versions: wait for full server readiness await this.extensionApi.serverReady(); this.isServerReady = true; return true; @@ -51,16 +75,32 @@ class LanguageServerApiManager { this.extensionApi = extensionApi; if (extensionApi.onDidClasspathUpdate) { const onDidClasspathUpdate: Event = extensionApi.onDidClasspathUpdate; - contextManager.context.subscriptions.push(onDidClasspathUpdate(() => { - commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true); + contextManager.context.subscriptions.push(onDidClasspathUpdate((uri: Uri) => { + if (this.isServerReady) { + // Server is fully ready — do a normal refresh to get full project data. + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); + } else { + // During import, the server is blocked and can't respond to queries. + // Don't clear progressive items. Try to add the project if not + // already present (typically a no-op since ProjectsImported fires first). + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, [uri.toString()]); + } syncHandler.updateFileWatcher(Settings.autoRefresh()); })); } if (extensionApi.onDidProjectsImport) { const onDidProjectsImport: Event = extensionApi.onDidProjectsImport; - contextManager.context.subscriptions.push(onDidProjectsImport(() => { - commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true); + contextManager.context.subscriptions.push(onDidProjectsImport((uris: Uri[]) => { + // Server is sending project data, so it's definitely running. + // Mark as running so ready() returns immediately on subsequent calls. + this.isServerRunning = true; + // During import, the JDTLS server is blocked by Eclipse workspace + // operations and cannot respond to queries. Instead of triggering + // a refresh (which queries the server), directly add projects to + // the tree view from the notification data. + const projectUris = uris.map(u => u.toString()); + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, projectUris); syncHandler.updateFileWatcher(Settings.autoRefresh()); })); } @@ -91,6 +131,14 @@ class LanguageServerApiManager { return this.extensionApi !== undefined; } + /** + * Returns true if the server has fully completed initialization (import finished). + * During progressive loading, this returns false even though ready() has resolved. + */ + public isFullyReady(): boolean { + return this.isServerReady; + } + /** * Check if the language server is ready in the given timeout. * @param timeout the timeout in milliseconds to wait diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 12001be2..d7d1e4b1 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -37,11 +37,16 @@ export class DependencyDataProvider implements TreeDataProvider { * `null` means no node is pending. */ private pendingRefreshElement: ExplorerNode | undefined | null; + /** Resolved when the first batch of progressive items arrives. */ + private _progressiveItemsReady: Promise | undefined; + private _resolveProgressiveItems: (() => void) | undefined; constructor(public readonly context: ExtensionContext) { // commands that do not send back telemetry context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, (debounce?: boolean, element?: ExplorerNode) => this.refresh(debounce, element))); + context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, (projectUris: string[]) => + this.addProgressiveProjects(projectUris))); context.subscriptions.push(commands.registerCommand(Commands.EXPORT_JAR_REPORT, (terminalId: string, message: string) => { appendOutput(terminalId, message); })); @@ -117,10 +122,35 @@ export class DependencyDataProvider implements TreeDataProvider { } public async getChildren(element?: ExplorerNode): Promise { + // Fast path: if root items are already populated by progressive loading + // (addProgressiveProjects), return them directly without querying the + // server, which may be blocked during long-running imports. + if (!element && this._rootItems && this._rootItems.length > 0) { + explorerNodeCache.saveNodes(this._rootItems); + return this._rootItems; + } + if (!await languageServerApiManager.ready()) { return []; } + // During progressive loading (server running but not fully ready after + // a clean workspace), don't enter getRootNodes() — its server queries + // will block for the entire import duration. Instead, keep the TreeView + // progress spinner visible by awaiting until the first progressive + // notification delivers items. + if (!element && !languageServerApiManager.isFullyReady()) { + if (!this._rootItems || this._rootItems.length === 0) { + if (!this._progressiveItemsReady) { + this._progressiveItemsReady = new Promise((resolve) => { + this._resolveProgressiveItems = resolve; + }); + } + await this._progressiveItemsReady; + } + return this._rootItems || []; + } + const children = (!this._rootItems || !element) ? await this.getRootNodes() : await element.getChildren(); @@ -173,6 +203,56 @@ export class DependencyDataProvider implements TreeDataProvider { this.pendingRefreshElement = null; } + /** + * Add projects progressively from ProjectsImported notifications. + * This directly creates ProjectNode items from URIs without querying + * the JDTLS server, which may be blocked during long-running imports. + */ + public addProgressiveProjects(projectUris: string[]): void { + const folders = workspace.workspaceFolders; + if (!folders || !folders.length) { + return; + } + + if (!this._rootItems) { + this._rootItems = []; + } + + const existingUris = new Set( + this._rootItems + .filter((n): n is ProjectNode => n instanceof ProjectNode) + .map((n) => n.uri) + ); + + let added = false; + for (const uriStr of projectUris) { + if (existingUris.has(uriStr)) { + continue; + } + // Extract project name from URI (last non-empty path segment) + const name = uriStr.replace(/\/+$/, "").split("/").pop() || "unknown"; + const nodeData: INodeData = { + name, + uri: uriStr, + kind: NodeKind.Project, + }; + this._rootItems.push(new ProjectNode(nodeData, undefined)); + existingUris.add(uriStr); + added = true; + } + + if (added) { + // Resolve the pending getChildren() promise so the TreeView + // spinner stops and items appear. + if (this._resolveProgressiveItems) { + this._resolveProgressiveItems(); + this._resolveProgressiveItems = undefined; + this._progressiveItemsReady = undefined; + } + this._onDidChangeTreeData.fire(undefined); + } + } + private async getRootNodes(): Promise { try { await explorerLock.acquireAsync(); From 334772bcd1b785d4c33af84ee1614bdd5a0fbe78 Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Fri, 27 Mar 2026 14:47:28 +0800 Subject: [PATCH 2/7] Downgrade version from 0.27.1 to 0.27.0 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index c64f668b..ba470f19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-java-dependency", - "version": "0.27.1", + "version": "0.27.0", "lockfileVersion": 2, "requires": true, "packages": { From 48df6e3ff0e9607e89e18f5e45ab9a82808f93ef Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Fri, 27 Mar 2026 14:50:01 +0800 Subject: [PATCH 3/7] Update version to 0.27.1 in package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ba470f19..c64f668b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-java-dependency", - "version": "0.27.0", + "version": "0.27.1", "lockfileVersion": 2, "requires": true, "packages": { From e0b08233b51aa484879a1adc060c67dfd8e339de Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 30 Mar 2026 10:45:00 +0800 Subject: [PATCH 4/7] fix: address review feedback on progressive loading - Extract startServerReadyWait() and call it unconditionally in initializeJavaLanguageServerApis() so isServerReady is set correctly even if onDidProjectsImport fires before ready() (fixes #6) - Restore ready() to not start serverReady() wait inline, keeping its semantics consistent for other callers like syncHandler, upgradeManager, BuildArtifactTaskProvider (fixes #4) - Add .catch() on serverReady().then() to avoid unhandled promise rejections if the server fails to start (fixes #5) - Restore debounce=true on onDidClasspathUpdate when server is ready to avoid burst refreshes (fixes #7) - Add 30s timeout on _progressiveItemsReady await to prevent getChildren() from hanging indefinitely (fixes #2) - Resolve _progressiveItemsReady in doRefresh() to prevent stale getChildren() calls from hanging --- .../languageServerApiManager.ts | 40 ++++++++++++++----- src/views/dependencyDataProvider.ts | 12 +++++- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/languageServerApi/languageServerApiManager.ts b/src/languageServerApi/languageServerApiManager.ts index 48bb129f..54eb7f0d 100644 --- a/src/languageServerApi/languageServerApiManager.ts +++ b/src/languageServerApi/languageServerApiManager.ts @@ -14,6 +14,7 @@ class LanguageServerApiManager { private isServerReady: boolean = false; private isServerRunning: boolean = false; + private serverReadyWaitStarted: boolean = false; public async ready(): Promise { if (this.isServerReady) { @@ -36,15 +37,6 @@ class LanguageServerApiManager { if (!this.isServerRunning && this.extensionApi.serverRunning) { await this.extensionApi.serverRunning(); this.isServerRunning = true; - // Start background wait for full server readiness (import complete). - // When the server finishes importing, trigger a full refresh to replace - // progressive placeholder items with proper data from the server. - if (this.extensionApi.serverReady) { - this.extensionApi.serverReady().then(() => { - this.isServerReady = true; - commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); - }); - } return true; } if (this.isServerRunning) { @@ -57,6 +49,29 @@ class LanguageServerApiManager { return true; } + /** + * Start a background wait for full server readiness (import complete). + * When the server finishes importing, trigger a full refresh to replace + * progressive placeholder items with proper data from the server. + * Guarded so it only starts once regardless of call order. + */ + private startServerReadyWait(): void { + if (this.serverReadyWaitStarted || this.isServerReady) { + return; + } + if (this.extensionApi?.serverReady) { + this.serverReadyWaitStarted = true; + this.extensionApi.serverReady() + .then(() => { + this.isServerReady = true; + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); + }) + .catch((error: unknown) => { + console.error("Java language server failed to become ready:", error); + }); + } + } + public async initializeJavaLanguageServerApis(): Promise { if (this.isApiInitialized()) { return; @@ -73,12 +88,17 @@ class LanguageServerApiManager { } this.extensionApi = extensionApi; + // Start background wait for full server readiness unconditionally. + // This ensures isServerReady is set and final refresh fires even + // if onDidProjectsImport sets isServerRunning before ready() runs. + this.startServerReadyWait(); + if (extensionApi.onDidClasspathUpdate) { const onDidClasspathUpdate: Event = extensionApi.onDidClasspathUpdate; contextManager.context.subscriptions.push(onDidClasspathUpdate((uri: Uri) => { if (this.isServerReady) { // Server is fully ready — do a normal refresh to get full project data. - commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true); } else { // During import, the server is blocked and can't respond to queries. // Don't clear progressive items. Try to add the project if not diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index d7d1e4b1..db5f0e34 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -146,7 +146,11 @@ export class DependencyDataProvider implements TreeDataProvider { this._resolveProgressiveItems = resolve; }); } - await this._progressiveItemsReady; + // Race with a timeout to prevent hanging indefinitely if no + // progressive notifications arrive (e.g., small project or + // server failure). After timeout, fall through to normal path. + const timeout = new Promise((resolve) => setTimeout(resolve, 30_000)); + await Promise.race([this._progressiveItemsReady, timeout]); } return this._rootItems || []; } @@ -197,6 +201,12 @@ export class DependencyDataProvider implements TreeDataProvider { private doRefresh(element?: ExplorerNode): void { if (!element) { this._rootItems = undefined; + // Resolve any pending progressive await so getChildren() doesn't hang + if (this._resolveProgressiveItems) { + this._resolveProgressiveItems(); + this._resolveProgressiveItems = undefined; + this._progressiveItemsReady = undefined; + } } explorerNodeCache.removeNodeChildren(element); this._onDidChangeTreeData.fire(element); From b864584f8ba39c9bed0a4c093ad8c11d6ca960aa Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 30 Mar 2026 11:29:51 +0800 Subject: [PATCH 5/7] fix: remove console.error to satisfy no-console tslint rule --- src/languageServerApi/languageServerApiManager.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/languageServerApi/languageServerApiManager.ts b/src/languageServerApi/languageServerApiManager.ts index 54eb7f0d..7affb88c 100644 --- a/src/languageServerApi/languageServerApiManager.ts +++ b/src/languageServerApi/languageServerApiManager.ts @@ -66,8 +66,9 @@ class LanguageServerApiManager { this.isServerReady = true; commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); }) - .catch((error: unknown) => { - console.error("Java language server failed to become ready:", error); + .catch((_error: unknown) => { + // Server failed to become ready (e.g., startup failure). + // Leave isServerReady as false; progressive items remain as-is. }); } } From 25a5942823a61fc9750e76ff7bb0e6108357dd2f Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 30 Mar 2026 15:16:08 +0800 Subject: [PATCH 6/7] Fix: fall through to getRootNodes() when no progressive items arrive After the 30s timeout, if no progressive notifications were received (e.g., eclipse.jdt.ls progressive notifications not yet available), fall through to the normal getRootNodes() path instead of returning an empty array. This ensures the worst case matches today's behavior. Addresses review comment from wenytang-ms. --- src/views/dependencyDataProvider.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index db5f0e34..03555827 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -152,7 +152,13 @@ export class DependencyDataProvider implements TreeDataProvider { const timeout = new Promise((resolve) => setTimeout(resolve, 30_000)); await Promise.race([this._progressiveItemsReady, timeout]); } - return this._rootItems || []; + // If progressive items arrived, return them. Otherwise fall + // through to the normal getRootNodes() path so the tree is + // never left blank (e.g., when the JDTLS progressive + // notifications are not available yet). + if (this._rootItems && this._rootItems.length > 0) { + return this._rootItems; + } } const children = (!this._rootItems || !element) ? From 9b2ec92b2d183ad0f3342a91c38f01955fd3d4ab Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Mon, 30 Mar 2026 16:57:14 +0800 Subject: [PATCH 7/7] Simplify progressive loading: remove timeout fallback and return root items directly --- src/views/dependencyDataProvider.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 03555827..0a1ec956 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -146,19 +146,9 @@ export class DependencyDataProvider implements TreeDataProvider { this._resolveProgressiveItems = resolve; }); } - // Race with a timeout to prevent hanging indefinitely if no - // progressive notifications arrive (e.g., small project or - // server failure). After timeout, fall through to normal path. - const timeout = new Promise((resolve) => setTimeout(resolve, 30_000)); - await Promise.race([this._progressiveItemsReady, timeout]); - } - // If progressive items arrived, return them. Otherwise fall - // through to the normal getRootNodes() path so the tree is - // never left blank (e.g., when the JDTLS progressive - // notifications are not available yet). - if (this._rootItems && this._rootItems.length > 0) { - return this._rootItems; + await this._progressiveItemsReady; } + return this._rootItems || []; } const children = (!this._rootItems || !element) ?