Skip to content

Commit a6315cb

Browse files
vadimLuzyaninqwencoder
authored andcommitted
fix(core): add LSP diagnostics caching and document refresh fallback
Add publishDiagnostics notification handler in LspServerManager that caches diagnostics and supports pending diagnostic promises. When textDocument/diagnostic pull fails in NativeLspService, fall back to force-refreshing the document (didClose + didOpen) and reading from the cached diagnostics. Also fix review findings: - Clear cache/pending maps on server restart in resetHandle() - Clean up pending diagnostics entries on timeout - Re-track URI in openedDocuments on refresh failure - Filter workspaceDiagnostics fallback by workspace root URI Fixes #3029 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent f208801 commit a6315cb

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

packages/core/src/lsp/LspServerManager.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
LspServerStatus,
2929
LspSocketOptions,
3030
} from './types.js';
31+
import { isPublishDiagnosticsParams } from './types.js';
3132
import { createDebugLogger } from '../utils/debugLogger.js';
3233

3334
const debugLogger = createDebugLogger('LSP');
@@ -58,6 +59,8 @@ export class LspServerManager {
5859
this.serverHandles.set(config.name, {
5960
config,
6061
status: 'NOT_STARTED',
62+
cachedDiagnostics: new Map(),
63+
pendingDiagnostics: new Map(),
6164
});
6265
}
6366
}
@@ -264,6 +267,21 @@ export class LspServerManager {
264267
handle.connection = connection.connection;
265268
handle.process = connection.process;
266269

270+
handle.connection.onNotification((msg) => {
271+
if (
272+
msg &&
273+
msg.method === 'textDocument/publishDiagnostics' &&
274+
isPublishDiagnosticsParams(msg.params)
275+
) {
276+
handle.cachedDiagnostics.set(msg.params.uri, msg.params.diagnostics);
277+
const pending = handle.pendingDiagnostics.get(msg.params.uri);
278+
if (pending) {
279+
handle.pendingDiagnostics.delete(msg.params.uri);
280+
pending.resolve();
281+
}
282+
}
283+
});
284+
267285
// Initialize LSP server
268286
await this.initializeLspServer(connection, handle.config);
269287

@@ -370,6 +388,8 @@ export class LspServerManager {
370388
handle.error = undefined;
371389
handle.warmedUp = false;
372390
handle.stopRequested = false;
391+
handle.cachedDiagnostics?.clear();
392+
handle.pendingDiagnostics?.clear();
373393
}
374394

375395
private buildProcessEnv(
@@ -527,6 +547,8 @@ export class LspServerManager {
527547
references: { dynamicRegistration: true },
528548
documentSymbol: { dynamicRegistration: true },
529549
codeAction: { dynamicRegistration: true },
550+
// Signal acceptance of publishDiagnostics notifications from server
551+
publishDiagnostics: {},
530552
},
531553
workspace: {
532554
workspaceFolders: true,

packages/core/src/lsp/NativeLspService.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,85 @@ export class NativeLspService {
10641064
`LSP textDocument/diagnostic failed for ${name}:`,
10651065
error,
10661066
);
1067+
1068+
// Force-refresh the document: send didClose + didOpen to trigger fresh analysis
1069+
const openedForServer = this.openedDocuments.get(name);
1070+
if (openedForServer?.has(uri)) {
1071+
openedForServer.delete(uri);
1072+
try {
1073+
const filePath = fileURLToPath(uri);
1074+
const text = fs.readFileSync(filePath, 'utf-8');
1075+
const languageId =
1076+
this.resolveLanguageId(filePath, handle) ?? 'plaintext';
1077+
handle.connection.send({
1078+
jsonrpc: '2.0',
1079+
method: 'textDocument/didClose',
1080+
params: { textDocument: { uri } },
1081+
});
1082+
handle.connection.send({
1083+
jsonrpc: '2.0',
1084+
method: 'textDocument/didOpen',
1085+
params: {
1086+
textDocument: {
1087+
uri,
1088+
languageId,
1089+
version: Date.now(),
1090+
text,
1091+
},
1092+
},
1093+
});
1094+
await this.delay(DEFAULT_LSP_DOCUMENT_OPEN_DELAY_MS * 5);
1095+
} catch (err) {
1096+
debugLogger.warn(`Failed to refresh document:`, err);
1097+
openedForServer.add(uri);
1098+
}
1099+
}
1100+
// Check push diagnostics cache (freshly updated after didOpen)
1101+
const cache = handle.cachedDiagnostics;
1102+
if (cache) {
1103+
const cached = cache.get(uri);
1104+
if (cached && cached.length > 0) {
1105+
for (const item of cached) {
1106+
const normalized2 = this.normalizer.normalizeDiagnostic(
1107+
item,
1108+
name,
1109+
);
1110+
if (normalized2) {
1111+
allDiagnostics.push(normalized2);
1112+
}
1113+
}
1114+
return allDiagnostics;
1115+
}
1116+
}
1117+
// Await push diagnostics via pub/sub (Promise.race with 5s timeout)
1118+
if (handle.pendingDiagnostics) {
1119+
await Promise.race([
1120+
new Promise<void>((resolve) => {
1121+
handle.pendingDiagnostics!.set(uri, { resolve });
1122+
}),
1123+
new Promise<void>((resolve) => {
1124+
setTimeout(() => {
1125+
handle.pendingDiagnostics!.delete(uri);
1126+
resolve();
1127+
}, 5000);
1128+
}),
1129+
]);
1130+
}
1131+
// Read diagnostics after notification arrives or timeout
1132+
if (cache) {
1133+
const cached = cache.get(uri);
1134+
if (cached) {
1135+
for (const item of cached) {
1136+
const normalized = this.normalizer.normalizeDiagnostic(
1137+
item,
1138+
name,
1139+
);
1140+
if (normalized) {
1141+
allDiagnostics.push(normalized);
1142+
}
1143+
}
1144+
}
1145+
}
10671146
}
10681147
}
10691148

@@ -1112,6 +1191,28 @@ export class NativeLspService {
11121191
}
11131192
} catch (error) {
11141193
debugLogger.warn(`LSP workspace/diagnostic failed for ${name}:`, error);
1194+
1195+
if (handle.cachedDiagnostics) {
1196+
const workspaceRootUri = pathToFileURL(this.workspaceRoot).toString();
1197+
for (const [uri, diagnostics] of handle.cachedDiagnostics) {
1198+
if (!uri.startsWith(workspaceRootUri)) continue;
1199+
if (results.length >= limit) break;
1200+
if (diagnostics && diagnostics.length > 0) {
1201+
const normalizedDiagnostics = [];
1202+
for (const diag of diagnostics) {
1203+
const n = this.normalizer.normalizeDiagnostic(diag, name);
1204+
if (n) normalizedDiagnostics.push(n);
1205+
}
1206+
if (normalizedDiagnostics.length > 0) {
1207+
results.push({
1208+
uri,
1209+
diagnostics: normalizedDiagnostics,
1210+
serverName: name,
1211+
});
1212+
}
1213+
}
1214+
}
1215+
}
11151216
}
11161217

11171218
if (results.length >= limit) {

packages/core/src/lsp/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,10 @@ export interface LspServerHandle {
494494
restartAttempts?: number;
495495
/** Lock to prevent concurrent startup attempts */
496496
startingPromise?: Promise<void>;
497+
/** Cache of diagnostics keyed by document URI */
498+
cachedDiagnostics: Map<string, Array<Record<string, unknown>>>;
499+
/** Pending diagnostic notification resolvers keyed by document URI */
500+
pendingDiagnostics: Map<string, { resolve: () => void }>;
497501
}
498502

499503
/**
@@ -521,3 +525,19 @@ export interface LspConnectionResult {
521525
/** Send initialize request */
522526
initialize: (params: unknown) => Promise<unknown>;
523527
}
528+
529+
/**
530+
* Type guard for publishDiagnostics notification params.
531+
*/
532+
export function isPublishDiagnosticsParams(
533+
value: unknown,
534+
): value is { uri: string; diagnostics: Array<Record<string, unknown>> } {
535+
return (
536+
typeof value === 'object' &&
537+
value !== null &&
538+
'uri' in value &&
539+
typeof value['uri'] === 'string' &&
540+
'diagnostics' in value &&
541+
Array.isArray(value['diagnostics'])
542+
);
543+
}

0 commit comments

Comments
 (0)