Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
56 changes: 52 additions & 4 deletions src/languageServerApi/languageServerApiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class LanguageServerApiManager {
private extensionApi: any;

private isServerReady: boolean = false;
private isServerRunning: boolean = false;

public async ready(): Promise<boolean> {
if (this.isServerReady) {
Expand All @@ -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;
Comment on lines +32 to +38
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.
// 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);
});
Comment on lines +43 to +46
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.
}
return true;
}
if (this.isServerRunning) {
return true;
}
Comment on lines +50 to +52
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.

// Fallback for older API versions: wait for full server readiness
await this.extensionApi.serverReady();
this.isServerReady = true;
return true;
Expand All @@ -51,16 +75,32 @@ class LanguageServerApiManager {
this.extensionApi = extensionApi;
if (extensionApi.onDidClasspathUpdate) {
const onDidClasspathUpdate: Event<Uri> = 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);
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.
} 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<Uri[]> = 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());
}));
}
Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions src/views/dependencyDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
* `null` means no node is pending.
*/
private pendingRefreshElement: ExplorerNode | undefined | null;
/** Resolved when the first batch of progressive items arrives. */
private _progressiveItemsReady: Promise<void> | 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);
}));
Expand Down Expand Up @@ -117,10 +122,35 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
}

public async getChildren(element?: ExplorerNode): Promise<ExplorerNode[] | undefined | null> {
// 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<void>((resolve) => {
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.
}
return this._rootItems || [];
}

const children = (!this._rootItems || !element) ?
await this.getRootNodes() : await element.getChildren();

Comment on lines 154 to 156
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.
Expand Down Expand Up @@ -173,6 +203,56 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
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);
Comment on lines +211 to +240
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.
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<ExplorerNode[]> {
try {
await explorerLock.acquireAsync();
Expand Down
Loading