feat: progressive project tree view during import#982
Conversation
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.
There was a problem hiding this comment.
Pull request overview
Adds progressive population of the JAVA PROJECTS tree during JDTLS import by consuming ProjectsImported notifications, so users see project names earlier instead of waiting for full import completion.
Changes:
- Introduces progressive root project nodes in
DependencyDataProviderand avoids root server queries while import is ongoing. - Updates
LanguageServerApiManager.ready()to useserverRunning()(when available) and wires import/classpath events to progressively add projects, followed by a full refresh afterserverReady(). - Adds an internal command for progressively adding projects to the tree view.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/views/dependencyDataProvider.ts | Adds progressive root node population and spinner/await logic during import. |
| src/languageServerApi/languageServerApiManager.ts | Switches readiness behavior to serverRunning() and routes JDTLS events to progressive updates + final refresh. |
| src/commands.ts | Adds internal command ID for progressive project insertion. |
| package-lock.json | Bumps package version metadata to 0.27.1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const children = (!this._rootItems || !element) ? | ||
| await this.getRootNodes() : await element.getChildren(); | ||
|
|
There was a problem hiding this comment.
During progressive loading, getChildren(element) can still call element.getChildren() for progressive ProjectNodes (because the !isFullyReady() guard only applies to the root). Since ProjectNode.loadData() queries JDTLS, expanding a progressive placeholder during import may hang on the same workspace locks this PR is trying to avoid. Consider preventing expansion until isFullyReady() (e.g., return [] for non-root nodes while not fully ready, or use a placeholder node type that reports no children until the final refresh).
| this._resolveProgressiveItems = resolve; | ||
| }); | ||
| } | ||
| await this._progressiveItemsReady; |
There was a problem hiding this comment.
await this._progressiveItemsReady; can wait indefinitely if no ProjectsImported notifications arrive (e.g., import fails, or server becomes fully ready quickly and the full refresh clears _rootItems). Also doRefresh() clears _rootItems but doesn’t resolve/reject/clear the pending promise, which can leave a getChildren() call hanging. Consider racing this await with a “server fully ready” signal/timeout, and clearing or resolving _progressiveItemsReady when a refresh occurs.
| await this._progressiveItemsReady; | |
| const timeoutPromise = new Promise<void>((resolve) => { | |
| // Prevent getChildren() from hanging indefinitely if no | |
| // progressive notifications are ever received. | |
| setTimeout(resolve, 30000); | |
| }); | |
| await Promise.race([this._progressiveItemsReady, timeoutPromise]); |
| 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); |
There was a problem hiding this comment.
addProgressiveProjects() always appends ProjectNodes directly to _rootItems. In multi-root workspaces, the normal root structure is WorkspaceNode entries (see getRootNodes()), so this will change the tree shape during import and may even mix WorkspaceNode and ProjectNode roots if progressive updates happen after an initial root load. Consider preserving the existing root model (group projects under their WorkspaceNode, or only enable progressive root project insertion in single-folder workspaces).
| // 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; |
There was a problem hiding this comment.
ready() now returns after serverRunning() (when available), which changes the meaning from “import complete” to “process running”. Several existing call sites (e.g., auto-refresh watcher setup, build artifact flow, upgrade scans, and tests) call ready() expecting server queries like Jdtls.getProjectUris() to work; those can still block during import per the PR description. Consider keeping ready() as “fully ready” and adding a separate method for “running”, or updating all ready() call sites that depend on full import completion to use a new/waitable full-ready API.
| this.extensionApi.serverReady().then(() => { | ||
| this.isServerReady = true; | ||
| commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); | ||
| }); |
There was a problem hiding this comment.
The background serverReady().then(...) handler doesn’t attach a rejection handler. If serverReady() rejects (e.g., server fails to start/import), this becomes an unhandled promise rejection and isServerReady never gets set. Please add explicit error handling (e.g., .catch(...) that logs/sets state and avoids triggering a refresh).
| this.extensionApi.serverReady().then(() => { | |
| this.isServerReady = true; | |
| commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); | |
| }); | |
| this.extensionApi.serverReady() | |
| .then(() => { | |
| this.isServerReady = true; | |
| commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); | |
| }) | |
| .catch((error: unknown) => { | |
| // Handle server readiness failure to avoid unhandled promise rejections. | |
| console.error("Java language server failed to become ready:", error); | |
| window.showErrorMessage("Java language server failed to become ready. Some features may be unavailable."); | |
| }); |
| if (this.isServerRunning) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
If onDidProjectsImport fires before the first call to ready(), it sets isServerRunning = true. In that case ready() will hit the if (this.isServerRunning) { return true; } path and will never start the background serverReady() wait/refresh, so isServerReady may remain false indefinitely. Consider starting the serverReady() wait whenever the API is initialized and serverReady exists (guarded by a single shared promise), not only inside the serverRunning() branch.
| 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); |
There was a problem hiding this comment.
onDidClasspathUpdate now triggers an immediate refresh with debounce = false when the server is ready. Classpath updates can arrive in bursts; removing debouncing can cause repeated full tree rebuilds and extra server traffic. Consider using debounce = true here (as before) unless there’s a concrete reason an immediate refresh is required.
| commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false); | |
| commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true); |
Summary
Show Java project names in the JAVA PROJECTS tree view progressively as they are imported, instead of waiting for the entire import to complete. This reduces perceived loading time for large projects (e.g., 436 Gradle subprojects) from ~7 minutes to ~1 minute.
Problem
For large Gradle projects, the JDTLS import takes 6-7 minutes. During this time, the JAVA PROJECTS tree view is completely empty because:
serverReady()doesn't resolve until import completesserverRunning(), the server is blocked by Eclipse workspace locks and can't respond to queries likejava.project.listSolution
Instead of querying the server for project data, create tree nodes directly from
ProjectsImportednotification URIs sent progressively by JDTLS during import.Key Changes
languageServerApiManager.tsserverRunning()(API >= 0.14) instead ofserverReady()so the tree view can start rendering before import finishesonDidProjectsImportevents toaddProgressiveProjects()instead of refresh (which would query the blocked server)onDidClasspathUpdateduring import toaddProgressiveProjects()(non-destructive)serverReady()), trigger full refresh to replace placeholders with real dataisFullyReady()methoddependencyDataProvider.tsaddProgressiveProjects()— createsProjectNodeitems from URIs without server queriesgetChildren()from enteringgetRootNodes()during progressive loading (prevents blocking on server queries that hang for the entire import)commands.tsVIEW_PACKAGE_INTERNAL_ADD_PROJECTScommand constantContext
This is part of a 3-repo change for progressive project loading:
serverRunning()API (v0.14)Testing
Tested with spring-boot project (436 Gradle subprojects):