Files
volar-docs/docs/workspace-diagnostics.md
2025-11-09 22:22:52 -06:00

6.6 KiB
Raw Blame History

Workspace Diagnostics Deep Dive

Docs IndexRepo READMEPerformance GuideTesting & CI

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/diagnostic clients (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 send kind: 'unchanged', you must supply the previous resultId.

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

  1. Document opened/changed remove cached entry and recompute on next workspace request.
  2. Configuration change (e.g., schema update) bump all resultIds to ensure clients refresh.
  3. 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 resultId management.
  • 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

  1. Version numbers include the documents version (or null if unknown). Helps clients detect stale reports.
  2. Respect cancellation workspace diagnostics can be expensive; break when the token is canceled.
  3. 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.
  4. Surface progress pair workspace diagnostics with a work-done progress notification so users see progress for large workspaces.
  5. Log durations record how long workspace diagnostics take; emit telemetry (workspaceDiagnostics.run) to spot regressions.
  6. 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 watch workspace/diagnostic traffic.
  • Verify resultId usage: when the client provides previous IDs, ensure you respond with kind: 'unchanged' if nothing changed.
  • Check that collectDiagnostics returns the same results as textDocument/publishDiagnostics; any divergence indicates a caching or mapping issue.

By understanding the workspace diagnostics protocol and building on Volars primitives, you can provide lightning-fast, up-to-date diagnostic summaries for any workspace size, both in editors and in automated tooling.