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();