Skip to content

Commit e4e883e

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/multi-580a7c2f10
2 parents 80f9be2 + ff3671a commit e4e883e

File tree

3 files changed

+161
-4
lines changed

3 files changed

+161
-4
lines changed

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: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class LanguageServerApiManager {
1313
private extensionApi: any;
1414

1515
private isServerReady: boolean = false;
16+
private isServerRunning: boolean = false;
17+
private serverReadyWaitStarted: boolean = false;
1618

1719
public async ready(): Promise<boolean> {
1820
if (this.isServerReady) {
@@ -28,11 +30,49 @@ class LanguageServerApiManager {
2830
return false;
2931
}
3032

33+
// Use serverRunning() if available (API >= 0.14) for progressive loading.
34+
// This resolves when the server process is alive and can handle requests,
35+
// even if project imports haven't completed yet. This enables the tree view
36+
// to show projects incrementally as they are imported.
37+
if (!this.isServerRunning && this.extensionApi.serverRunning) {
38+
await this.extensionApi.serverRunning();
39+
this.isServerRunning = true;
40+
return true;
41+
}
42+
if (this.isServerRunning) {
43+
return true;
44+
}
45+
46+
// Fallback for older API versions: wait for full server readiness
3147
await this.extensionApi.serverReady();
3248
this.isServerReady = true;
3349
return true;
3450
}
3551

52+
/**
53+
* Start a background wait for full server readiness (import complete).
54+
* When the server finishes importing, trigger a full refresh to replace
55+
* progressive placeholder items with proper data from the server.
56+
* Guarded so it only starts once regardless of call order.
57+
*/
58+
private startServerReadyWait(): void {
59+
if (this.serverReadyWaitStarted || this.isServerReady) {
60+
return;
61+
}
62+
if (this.extensionApi?.serverReady) {
63+
this.serverReadyWaitStarted = true;
64+
this.extensionApi.serverReady()
65+
.then(() => {
66+
this.isServerReady = true;
67+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */false);
68+
})
69+
.catch((_error: unknown) => {
70+
// Server failed to become ready (e.g., startup failure).
71+
// Leave isServerReady as false; progressive items remain as-is.
72+
});
73+
}
74+
}
75+
3676
public async initializeJavaLanguageServerApis(): Promise<void> {
3777
if (this.isApiInitialized()) {
3878
return;
@@ -49,18 +89,39 @@ class LanguageServerApiManager {
4989
}
5090

5191
this.extensionApi = extensionApi;
92+
// Start background wait for full server readiness unconditionally.
93+
// This ensures isServerReady is set and final refresh fires even
94+
// if onDidProjectsImport sets isServerRunning before ready() runs.
95+
this.startServerReadyWait();
96+
5297
if (extensionApi.onDidClasspathUpdate) {
5398
const onDidClasspathUpdate: Event<Uri> = extensionApi.onDidClasspathUpdate;
54-
contextManager.context.subscriptions.push(onDidClasspathUpdate(() => {
55-
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);
99+
contextManager.context.subscriptions.push(onDidClasspathUpdate((uri: Uri) => {
100+
if (this.isServerReady) {
101+
// Server is fully ready — do a normal refresh to get full project data.
102+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);
103+
} else {
104+
// During import, the server is blocked and can't respond to queries.
105+
// Don't clear progressive items. Try to add the project if not
106+
// already present (typically a no-op since ProjectsImported fires first).
107+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, [uri.toString()]);
108+
}
56109
syncHandler.updateFileWatcher(Settings.autoRefresh());
57110
}));
58111
}
59112

60113
if (extensionApi.onDidProjectsImport) {
61114
const onDidProjectsImport: Event<Uri[]> = extensionApi.onDidProjectsImport;
62-
contextManager.context.subscriptions.push(onDidProjectsImport(() => {
63-
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true);
115+
contextManager.context.subscriptions.push(onDidProjectsImport((uris: Uri[]) => {
116+
// Server is sending project data, so it's definitely running.
117+
// Mark as running so ready() returns immediately on subsequent calls.
118+
this.isServerRunning = true;
119+
// During import, the JDTLS server is blocked by Eclipse workspace
120+
// operations and cannot respond to queries. Instead of triggering
121+
// a refresh (which queries the server), directly add projects to
122+
// the tree view from the notification data.
123+
const projectUris = uris.map(u => u.toString());
124+
commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, projectUris);
64125
syncHandler.updateFileWatcher(Settings.autoRefresh());
65126
}));
66127
}
@@ -91,6 +152,14 @@ class LanguageServerApiManager {
91152
return this.extensionApi !== undefined;
92153
}
93154

155+
/**
156+
* Returns true if the server has fully completed initialization (import finished).
157+
* During progressive loading, this returns false even though ready() has resolved.
158+
*/
159+
public isFullyReady(): boolean {
160+
return this.isServerReady;
161+
}
162+
94163
/**
95164
* Check if the language server is ready in the given timeout.
96165
* @param timeout the timeout in milliseconds to wait

src/views/dependencyDataProvider.ts

Lines changed: 86 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

@@ -167,12 +197,68 @@ export class DependencyDataProvider implements TreeDataProvider<ExplorerNode> {
167197
private doRefresh(element?: ExplorerNode): void {
168198
if (!element) {
169199
this._rootItems = undefined;
200+
// Resolve any pending progressive await so getChildren() doesn't hang
201+
if (this._resolveProgressiveItems) {
202+
this._resolveProgressiveItems();
203+
this._resolveProgressiveItems = undefined;
204+
this._progressiveItemsReady = undefined;
205+
}
170206
}
171207
explorerNodeCache.removeNodeChildren(element);
172208
this._onDidChangeTreeData.fire(element);
173209
this.pendingRefreshElement = null;
174210
}
175211

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

0 commit comments

Comments
 (0)