Skip to content
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
77 changes: 73 additions & 4 deletions src/languageServerApi/languageServerApiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class LanguageServerApiManager {
private extensionApi: any;

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

public async ready(): Promise<boolean> {
if (this.isServerReady) {
Expand All @@ -28,11 +30,49 @@ 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;
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;
}

/**
* 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) => {
// Server failed to become ready (e.g., startup failure).
// Leave isServerReady as false; progressive items remain as-is.
});
}
}

public async initializeJavaLanguageServerApis(): Promise<void> {
if (this.isApiInitialized()) {
return;
Expand All @@ -49,18 +89,39 @@ 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<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 = */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
// 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 +152,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
86 changes: 86 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;
}
return this._rootItems || [];
}

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

Expand Down Expand Up @@ -167,12 +197,68 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
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);
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<ExplorerNode[]> {
try {
await explorerLock.acquireAsync();
Expand Down
Loading