6.6 KiB
Workspace Diagnostics Deep Dive
Workspace diagnostics allow clients to request a consolidated snapshot of diagnostics across every tracked document. Volar includes built-in support, but when you author custom servers or plugins you may need to understand the full protocol to extend or debug it. This guide covers request/response semantics, result IDs, incremental updates, and best practices.
Why Workspace Diagnostics?
- Provide a single “Problems” list for projects with hundreds of files without opening each file.
- Enable CLI tooling (
volar check) to return the same diagnostics as the editor. - Support
workspace/diagnosticclients (VS Code Insiders, Neovim, Sublime LSP) that fetch diagnostics on demand.
Protocol Primer
Request: workspace/diagnostic
connection.languages.diagnostics.onWorkspace(async (params, token) => {
// params.previousResultIds – known results from last run
});
Parameters:
identifier: optional string set by the client during registration; echo it back in responses if present.previousResultIds: array of{ uri: string, value: string }– tells the server what diagnostics the client already has.
Response: WorkspaceDiagnosticReport
type WorkspaceDiagnosticReport = {
items: WorkspaceDocumentDiagnosticReport[];
};
type WorkspaceDocumentDiagnosticReport =
| WorkspaceFullDocumentDiagnosticReport
| WorkspaceUnchangedDocumentDiagnosticReport;
type WorkspaceFullDocumentDiagnosticReport = {
kind: 'full';
uri: string;
version: number | null;
items: Diagnostic[];
resultId?: string;
};
type WorkspaceUnchangedDocumentDiagnosticReport = {
kind: 'unchanged';
uri: string;
version: number | null;
resultId: string;
};
resultId: opaque string used to identify a diagnostic result. If you sendkind: 'unchanged', you must supply the previousresultId.
Implementation Patterns
1. Full Refresh Every Time
Simplest approach (used in our JSON/YAML example):
connection.languages.diagnostics.onWorkspace(async (_params, token) => {
const items = [];
for (const doc of server.documents.all()) {
if (token.isCancellationRequested) break;
items.push({
uri: doc.uri,
kind: DocumentDiagnosticReportKind.Full,
version: doc.version ?? null,
items: await collectDiagnostics(doc),
});
}
return { items };
});
- Pros: Easy to implement, no result IDs to track.
- Cons: Potentially expensive for large workspaces; clients will reprocess every diagnostic each run.
2. Incremental with resultId
Track diagnostics per document:
const workspaceResults = new Map<string, { resultId: string; items: Diagnostic[] }>();
connection.languages.diagnostics.onWorkspace(async (params, token) => {
const items: WorkspaceDocumentDiagnosticReport[] = [];
for (const doc of server.documents.all()) {
if (token.isCancellationRequested) break;
const cached = workspaceResults.get(doc.uri);
const previousResultId = params.previousResultIds?.find((p) => p.uri === doc.uri)?.value;
if (cached && cached.resultId === previousResultId && !doc.versionChangedSince(cached.resultId)) {
items.push({
uri: doc.uri,
kind: 'unchanged',
version: doc.version ?? null,
resultId: cached.resultId,
});
continue;
}
const diagnostics = await collectDiagnostics(doc);
const resultId = crypto.randomUUID();
workspaceResults.set(doc.uri, { resultId, items: diagnostics });
items.push({
uri: doc.uri,
kind: 'full',
version: doc.version ?? null,
items: diagnostics,
resultId,
});
}
return { items };
});
- Pros: Clients skip reprocessing unchanged documents; faster on subsequent runs.
- Cons: Requires caching
resultId+ diagnostics and invalidating cache when files change.
When to Invalidate
- Document opened/changed – remove cached entry and recompute on next workspace request.
- Configuration change (e.g., schema update) – bump all
resultIds to ensure clients refresh. - Project reload – clear cache entirely.
Progress & Partial Results
The protocol supports work-done progress and partial results. To implement:
connection.languages.diagnostics.onWorkspace(async (params, token, _workDone, resultProgress) => {
for (const doc of server.documents.all()) {
if (token.isCancellationRequested) break;
const report = ...buildReport(doc)...;
resultProgress?.report({ items: [report] });
}
return { items: [] }; // or final aggregated array
});
Use partial results for very large workspaces so clients receive incremental updates instead of waiting for completion.
Volar Built-in Behavior
@volar/language-server already implements workspace diagnostics for TypeScript-based projects when workspaceDiagnostics capability is enabled. If you use createTypeScriptProject, you get:
- Automatic
resultIdmanagement. - Honor for
previousResultIds. - Partial result streaming.
However, if you build custom projects (e.g., JSON/YAML server) you need to implement the handler yourself, as shown above.
Best Practices
- Version numbers – include the document’s version (or
nullif unknown). Helps clients detect stale reports. - Respect cancellation – workspace diagnostics can be expensive; break when the token is canceled.
- Throttle frequency – only run workspace diagnostics on explicit requests or when the user triggers a “Refresh” command. Avoid running them on every change by default.
- Surface progress – pair workspace diagnostics with a work-done progress notification so users see progress for large workspaces.
- Log durations – record how long workspace diagnostics take; emit telemetry (
workspaceDiagnostics.run) to spot regressions. - Tie into CLI – reuse the same diagnostics pipeline for CLI tools (
volar check) to ensure consistent results.
Debugging Workspace Diagnostics
- Enable protocol tracing (
VOLAR_TRACE=protocol) and watchworkspace/diagnostictraffic. - Verify
resultIdusage: when the client provides previous IDs, ensure you respond withkind: 'unchanged'if nothing changed. - Check that
collectDiagnosticsreturns the same results astextDocument/publishDiagnostics; any divergence indicates a caching or mapping issue.
By understanding the workspace diagnostics protocol and building on Volar’s primitives, you can provide lightning-fast, up-to-date diagnostic summaries for any workspace size, both in editors and in automated tooling.