Skip to content

feat: progressive project tree view during import#982

Open
chagong wants to merge 3 commits intomainfrom
progressive-project-loading
Open

feat: progressive project tree view during import#982
chagong wants to merge 3 commits intomainfrom
progressive-project-loading

Conversation

@chagong
Copy link
Copy Markdown
Contributor

@chagong chagong commented Mar 27, 2026

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:

  1. serverReady() doesn't resolve until import completes
  2. Even if we wait for serverRunning(), the server is blocked by Eclipse workspace locks and can't respond to queries like java.project.list

Solution

Instead of querying the server for project data, create tree nodes directly from ProjectsImported notification URIs sent progressively by JDTLS during import.

Key Changes

languageServerApiManager.ts

  • Use serverRunning() (API >= 0.14) instead of serverReady() so the tree view can start rendering before import finishes
  • Route onDidProjectsImport events to addProgressiveProjects() instead of refresh (which would query the blocked server)
  • Route onDidClasspathUpdate during import to addProgressiveProjects() (non-destructive)
  • After import completes (serverReady()), trigger full refresh to replace placeholders with real data
  • Added isFullyReady() method

dependencyDataProvider.ts

  • Added addProgressiveProjects() — creates ProjectNode items from URIs without server queries
  • Guard getChildren() from entering getRootNodes() during progressive loading (prevents blocking on server queries that hang for the entire import)
  • Keep TreeView progress spinner visible until first progressive items arrive via a deferred promise

commands.ts

  • Added VIEW_PACKAGE_INTERNAL_ADD_PROJECTS command constant

Context

This is part of a 3-repo change for progressive project loading:

  1. feat: progressive project import notifications for tree view eclipse-jdtls/eclipse.jdt.ls#3744 — Server-side progressive notifications
  2. feat: add serverRunning() API (v0.14) for progressive loading redhat-developer/vscode-java#4372serverRunning() API (v0.14)
  3. This PR — Client-side progressive tree view rendering

Testing

Tested with spring-boot project (436 Gradle subprojects):

  • Before: Tree view empty for ~7 minutes until import completes
  • After: 436 project names appear within ~1 minute of opening, with progress spinner showing until items arrive
  • Works both with cached workspace (instant) and after "Clean Java Language Server Workspace" (progressive)
  • After import completes, full refresh replaces placeholder items with proper expandable project nodes

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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DependencyDataProvider and avoids root server queries while import is ongoing.
  • Updates LanguageServerApiManager.ready() to use serverRunning() (when available) and wires import/classpath events to progressively add projects, followed by a full refresh after serverReady().
  • 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.

Comment on lines 154 to 156
const children = (!this._rootItems || !element) ?
await this.getRootNodes() : await element.getChildren();

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
this._resolveProgressiveItems = resolve;
});
}
await this._progressiveItemsReady;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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]);

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +240
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);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +38
// 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;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +46
this.extensionApi.serverReady().then(() => {
this.isServerReady = true;
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false);
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.");
});

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +52
if (this.isServerRunning) {
return true;
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false);
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants