Skip to content

Commit fa90a3f

Browse files
committed
feat: progressive project tree view during import
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.
1 parent adf5f07 commit fa90a3f

File tree

4 files changed

+135
-5
lines changed

4 files changed

+135
-5
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export namespace Commands {
2626

2727
export const VIEW_PACKAGE_INTERNAL_REFRESH = "_java.view.package.internal.refresh";
2828

29+
export const VIEW_PACKAGE_INTERNAL_ADD_PROJECTS = "_java.view.package.internal.addProjects";
30+
2931
export const VIEW_PACKAGE_OUTLINE = "java.view.package.outline";
3032

3133
export const VIEW_PACKAGE_REVEAL_FILE_OS = "java.view.package.revealFileInOS";

src/languageServerApi/languageServerApiManager.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class LanguageServerApiManager {
1313
private extensionApi: any;
1414

1515
private isServerReady: boolean = false;
16+
private isServerRunning: boolean = false;
1617

1718
public async ready(): Promise<boolean> {
1819
if (this.isServerReady) {
@@ -28,6 +29,29 @@ class LanguageServerApiManager {
2829
return false;
2930
}
3031

32+
// Use serverRunning() if available (API >= 0.14) for progressive loading.
33+
// This resolves when the server process is alive and can handle requests,
34+
// even if project imports haven't completed yet. This enables the tree view
35+
// to show projects incrementally as they are imported.
36+
if (!this.isServerRunning && this.extensionApi.serverRunning) {
37+
await this.extensionApi.serverRunning();
38+
this.isServerRunning = true;
39+
// Start background wait for full server readiness (import complete).
40+
// When the server finishes importing, trigger a full refresh to replace
41+
// progressive placeholder items with proper data from the server.
42+
if (this.extensionApi.serverReady) {
43+
this.extensionApi.serverReady().then(() => {
44+
this.isServerReady = true;
45+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false);
46+
});
47+
}
48+
return true;
49+
}
50+
if (this.isServerRunning) {
51+
return true;
52+
}
53+
54+
// Fallback for older API versions: wait for full server readiness
3155
await this.extensionApi.serverReady();
3256
this.isServerReady = true;
3357
return true;
@@ -51,16 +75,32 @@ class LanguageServerApiManager {
5175
this.extensionApi = extensionApi;
5276
if (extensionApi.onDidClasspathUpdate) {
5377
const onDidClasspathUpdate: Event<Uri> = extensionApi.onDidClasspathUpdate;
54-
contextManager.context.subscriptions.push(onDidClasspathUpdate(() => {
55-
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);
78+
contextManager.context.subscriptions.push(onDidClasspathUpdate((uri: Uri) => {
79+
if (this.isServerReady) {
80+
// Server is fully ready — do a normal refresh to get full project data.
81+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false);
82+
} else {
83+
// During import, the server is blocked and can't respond to queries.
84+
// Don't clear progressive items. Try to add the project if not
85+
// already present (typically a no-op since ProjectsImported fires first).
86+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, [uri.toString()]);
87+
}
5688
syncHandler.updateFileWatcher(Settings.autoRefresh());
5789
}));
5890
}
5991

6092
if (extensionApi.onDidProjectsImport) {
6193
const onDidProjectsImport: Event<Uri[]> = extensionApi.onDidProjectsImport;
62-
contextManager.context.subscriptions.push(onDidProjectsImport(() => {
63-
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);
94+
contextManager.context.subscriptions.push(onDidProjectsImport((uris: Uri[]) => {
95+
// Server is sending project data, so it's definitely running.
96+
// Mark as running so ready() returns immediately on subsequent calls.
97+
this.isServerRunning = true;
98+
// During import, the JDTLS server is blocked by Eclipse workspace
99+
// operations and cannot respond to queries. Instead of triggering
100+
// a refresh (which queries the server), directly add projects to
101+
// the tree view from the notification data.
102+
const projectUris = uris.map(u => u.toString());
103+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, projectUris);
64104
syncHandler.updateFileWatcher(Settings.autoRefresh());
65105
}));
66106
}
@@ -91,6 +131,14 @@ class LanguageServerApiManager {
91131
return this.extensionApi !== undefined;
92132
}
93133

134+
/**
135+
* Returns true if the server has fully completed initialization (import finished).
136+
* During progressive loading, this returns false even though ready() has resolved.
137+
*/
138+
public isFullyReady(): boolean {
139+
return this.isServerReady;
140+
}
141+
94142
/**
95143
* Check if the language server is ready in the given timeout.
96144
* @param timeout the timeout in milliseconds to wait

src/views/dependencyDataProvider.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
3737
* `null` means no node is pending.
3838
*/
3939
private pendingRefreshElement: ExplorerNode | undefined | null;
40+
/** Resolved when the first batch of progressive items arrives. */
41+
private _progressiveItemsReady: Promise<void> | undefined;
42+
private _resolveProgressiveItems: (() => void) | undefined;
4043

4144
constructor(public readonly context: ExtensionContext) {
4245
// commands that do not send back telemetry
4346
context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, (debounce?: boolean, element?: ExplorerNode) =>
4447
this.refresh(debounce, element)));
48+
context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, (projectUris: string[]) =>
49+
this.addProgressiveProjects(projectUris)));
4550
context.subscriptions.push(commands.registerCommand(Commands.EXPORT_JAR_REPORT, (terminalId: string, message: string) => {
4651
appendOutput(terminalId, message);
4752
}));
@@ -117,10 +122,35 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
117122
}
118123

119124
public async getChildren(element?: ExplorerNode): Promise<ExplorerNode[] | undefined | null> {
125+
// Fast path: if root items are already populated by progressive loading
126+
// (addProgressiveProjects), return them directly without querying the
127+
// server, which may be blocked during long-running imports.
128+
if (!element && this._rootItems && this._rootItems.length > 0) {
129+
explorerNodeCache.saveNodes(this._rootItems);
130+
return this._rootItems;
131+
}
132+
120133
if (!await languageServerApiManager.ready()) {
121134
return [];
122135
}
123136

137+
// During progressive loading (server running but not fully ready after
138+
// a clean workspace), don't enter getRootNodes() — its server queries
139+
// will block for the entire import duration. Instead, keep the TreeView
140+
// progress spinner visible by awaiting until the first progressive
141+
// notification delivers items.
142+
if (!element && !languageServerApiManager.isFullyReady()) {
143+
if (!this._rootItems || this._rootItems.length === 0) {
144+
if (!this._progressiveItemsReady) {
145+
this._progressiveItemsReady = new Promise<void>((resolve) => {
146+
this._resolveProgressiveItems = resolve;
147+
});
148+
}
149+
await this._progressiveItemsReady;
150+
}
151+
return this._rootItems || [];
152+
}
153+
124154
const children = (!this._rootItems || !element) ?
125155
await this.getRootNodes() : await element.getChildren();
126156

@@ -173,6 +203,56 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
173203
this.pendingRefreshElement = null;
174204
}
175205

206+
/**
207+
* Add projects progressively from ProjectsImported notifications.
208+
* This directly creates ProjectNode items from URIs without querying
209+
* the JDTLS server, which may be blocked during long-running imports.
210+
*/
211+
public addProgressiveProjects(projectUris: string[]): void {
212+
const folders = workspace.workspaceFolders;
213+
if (!folders || !folders.length) {
214+
return;
215+
}
216+
217+
if (!this._rootItems) {
218+
this._rootItems = [];
219+
}
220+
221+
const existingUris = new Set(
222+
this._rootItems
223+
.filter((n): n is ProjectNode => n instanceof ProjectNode)
224+
.map((n) => n.uri)
225+
);
226+
227+
let added = false;
228+
for (const uriStr of projectUris) {
229+
if (existingUris.has(uriStr)) {
230+
continue;
231+
}
232+
// Extract project name from URI (last non-empty path segment)
233+
const name = uriStr.replace(/\/+$/, "").split("/").pop() || "unknown";
234+
const nodeData: INodeData = {
235+
name,
236+
uri: uriStr,
237+
kind: NodeKind.Project,
238+
};
239+
this._rootItems.push(new ProjectNode(nodeData, undefined));
240+
existingUris.add(uriStr);
241+
added = true;
242+
}
243+
244+
if (added) {
245+
// Resolve the pending getChildren() promise so the TreeView
246+
// spinner stops and items appear.
247+
if (this._resolveProgressiveItems) {
248+
this._resolveProgressiveItems();
249+
this._resolveProgressiveItems = undefined;
250+
this._progressiveItemsReady = undefined;
251+
}
252+
this._onDidChangeTreeData.fire(undefined);
253+
}
254+
}
255+
176256
private async getRootNodes(): Promise<ExplorerNode[]> {
177257
try {
178258
await explorerLock.acquireAsync();

0 commit comments

Comments
 (0)