@mrclrchtr/supi-code-intelligence 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +2 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  8. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  14. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  15. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  30. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +215 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +370 -266
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  46. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +238 -11
  47. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  49. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  59. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  66. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +26 -6
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -3
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  127. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  128. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  129. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  130. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  131. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  132. package/package.json +5 -5
  133. package/src/analysis/health/diagnostics.ts +236 -24
  134. package/src/analysis/health/recovery.ts +6 -2
  135. package/src/analysis/provider.ts +58 -0
  136. package/src/analysis/readiness.ts +30 -9
  137. package/src/analysis/refactor/apply.ts +59 -20
  138. package/src/analysis/refactor/mutation-authority.ts +143 -0
  139. package/src/analysis/refactor/position.ts +47 -0
  140. package/src/analysis/refactor/safety.ts +18 -10
  141. package/src/analysis/relations/callees.ts +9 -1
  142. package/src/analysis/relations/types.ts +5 -2
  143. package/src/analysis/search/ast-scan-timing.ts +9 -4
  144. package/src/analysis/search/ast-scan.ts +1 -1
  145. package/src/analysis/search/deadline.ts +2 -2
  146. package/src/analysis/search/pattern-analysis.ts +46 -32
  147. package/src/analysis/search/pattern.ts +12 -2
  148. package/src/analysis/target/anchored.ts +20 -11
  149. package/src/analysis/target/file.ts +18 -11
  150. package/src/analysis/target/identity.ts +19 -3
  151. package/src/analysis/target/symbol.ts +36 -15
  152. package/src/api.ts +4 -0
  153. package/src/diagnostics/evidence.ts +51 -0
  154. package/src/session/capability-adapter.ts +12 -3
  155. package/src/session/find-workflow.ts +17 -7
  156. package/src/session/graph/collect.ts +18 -2
  157. package/src/session/graph-workflow.ts +19 -12
  158. package/src/session/health-refresh.ts +148 -0
  159. package/src/session/health-types.ts +37 -15
  160. package/src/session/health-workflow.ts +37 -39
  161. package/src/session/inspect/collect.ts +20 -3
  162. package/src/session/inspect-workflow.ts +10 -5
  163. package/src/session/orientation/collect.ts +10 -1
  164. package/src/session/orientation/context-facts.ts +3 -1
  165. package/src/session/orientation/context-sections.ts +20 -7
  166. package/src/session/orientation/gather.ts +17 -11
  167. package/src/session/orientation-types.ts +2 -1
  168. package/src/session/orientation-workflow.ts +15 -8
  169. package/src/session/refactor-plans.ts +2 -0
  170. package/src/session/refactor-workflow.ts +59 -14
  171. package/src/session/session.ts +4 -1
  172. package/src/session/target-workflow.ts +54 -23
  173. package/src/session/workflow-control.ts +7 -3
  174. package/src/substrate/lsp/lifecycle.ts +26 -46
  175. package/src/substrate/lsp/maintenance.ts +199 -25
  176. package/src/substrate/lsp/state.ts +2 -2
  177. package/src/substrate/workspace-provider-host.ts +25 -3
  178. package/src/tool/find/execute.ts +7 -1
  179. package/src/tool/find/render.ts +20 -3
  180. package/src/tool/find/tui.ts +23 -21
  181. package/src/tool/graph/execute.ts +23 -3
  182. package/src/tool/graph/tui.ts +59 -46
  183. package/src/tool/health/execute.ts +7 -2
  184. package/src/tool/health/markdown.ts +56 -14
  185. package/src/tool/health/refresh-status.ts +105 -0
  186. package/src/tool/health/tui.ts +95 -98
  187. package/src/tool/infra/workflow-control.ts +8 -0
  188. package/src/tool/inspect/execute.ts +7 -1
  189. package/src/tool/inspect/tui.ts +26 -18
  190. package/src/tool/orientation/execute.ts +21 -3
  191. package/src/tool/orientation/tui.ts +51 -35
  192. package/src/tool/refactor-apply/execute.ts +3 -0
  193. package/src/tool/refactor-apply/tui.ts +43 -25
  194. package/src/tool/refactor-plan/execute.ts +4 -0
  195. package/src/tool/refactor-plan/tui.ts +33 -20
  196. package/src/tool/register.ts +127 -22
  197. package/src/tool/resolve/execute.ts +18 -1
  198. package/src/tool/resolve/tui.ts +42 -24
  199. package/src/tool/result/display.ts +107 -0
  200. package/src/tool/result/errors.ts +20 -17
  201. package/src/tool/result/find.ts +52 -8
  202. package/src/tool/result/graph.ts +44 -1
  203. package/src/tool/result/health.ts +53 -1
  204. package/src/tool/result/inspect.ts +113 -1
  205. package/src/tool/result/orientation.ts +66 -14
  206. package/src/tool/result/refactor.ts +36 -4
  207. package/src/tool/result/resolve.ts +44 -7
  208. package/src/tool/result/types.ts +20 -0
  209. package/src/types/execution.ts +18 -5
  210. package/src/types/index.ts +8 -1
  211. package/src/ui/status-command.ts +5 -3
  212. package/src/ui/status-overlay.ts +4 -0
  213. package/src/ui/tui/common.ts +108 -10
  214. package/src/ui/tui/display.ts +48 -0
  215. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -247
@@ -1,54 +1,63 @@
1
- // LSP client document synchronization and diagnostic state.
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import type {
5
- Diagnostic,
6
- DocumentDiagnosticReport,
7
- PublishDiagnosticsParams,
8
- TextDocumentIdentifier,
9
- TextDocumentItem,
10
- VersionedTextDocumentIdentifier,
11
- } from "../config/types.ts";
1
+ // biome-ignore-all lint/style/noExcessiveLinesPerFile: one client's document sync, diagnostics, refresh, and sync-file flow stay in one cohesive class.
2
+ import * as path from "node:path";
3
+ import {
4
+ type CodeQueryResult,
5
+ type CodeRequestControl,
6
+ completedCodeQuery,
7
+ throwIfCodeRequestInterrupted,
8
+ unavailableCodeQuery,
9
+ } from "@mrclrchtr/supi-code-runtime/api";
10
+ import type { Diagnostic, TextDocumentItem } from "../config/types.ts";
11
+ import type { DiagnosticEvidenceSummary } from "../diagnostics/evidence.ts";
12
12
  import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
13
+ import { applyPushDiagnostics, buildClientDiagnosticSnapshot } from "./client-diagnostic-cache.ts";
14
+ import { collectSynchronizedFileDiagnostics } from "./client-diagnostic-collection.ts";
15
+ import {
16
+ type DiagnosticCacheEntry,
17
+ type DiagnosticSynchronization,
18
+ hasFreshPush,
19
+ isCurrentSynchronization,
20
+ isValidPublishDiagnosticsParams,
21
+ nextDocumentVersion,
22
+ } from "./client-diagnostic-evidence.ts";
23
+ import type { ClientDiagnosticsHost } from "./client-diagnostic-host.ts";
24
+ import {
25
+ pullClientDiagnosticEvidenceFromHost,
26
+ refreshClientOpenDiagnostics,
27
+ sendDidCloseNotification,
28
+ } from "./client-diagnostic-refresh.ts";
29
+ import { DiagnosticObserver, type DiagnosticPushWaitOutcome } from "./client-diagnostic-timing.ts";
30
+ import { DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
31
+ import {
32
+ type ClientDiagnosticSnapshot,
33
+ type DiagnosticEntry,
34
+ fingerprintDocumentContent,
35
+ hasCurrentDiagnosticEvidence,
36
+ type OpenDocumentState,
37
+ } from "./client-document-state.ts";
38
+ import {
39
+ clearTrackedDocumentState,
40
+ reopenDocument,
41
+ synchronizeTrackedDocument,
42
+ } from "./client-document-sync.ts";
43
+ import { getDiagnosticFileState } from "./client-file-state.ts";
13
44
 
14
45
  const DIAGNOSTIC_WAIT_MS = 3_000;
15
-
16
- type OpenDocument = { version: number };
17
-
18
- type DiagnosticCacheEntry = {
19
- diagnostics: Diagnostic[];
20
- receivedAt: number;
21
- version?: number;
22
- resultId?: string;
23
- };
24
-
25
- /** One file's stored diagnostics as consumed by manager-level collection. */
26
- export interface DiagnosticEntry {
27
- uri: string;
28
- diagnostics: Diagnostic[];
29
- }
30
-
31
- interface ClientDiagnosticsHost {
32
- isOperational(): boolean;
33
- supportsPullDiagnostics(): boolean;
34
- sendNotification(method: string, params: unknown): void;
35
- pullDocumentDiagnostics(
36
- uri: string,
37
- previousResultId: string | undefined,
38
- timeoutMs: number,
39
- ): Promise<DocumentDiagnosticReport | null>;
40
- }
41
-
42
- /**
43
- * Owns one LSP client's open documents, diagnostic cache, waiters, and refresh policy.
44
- *
45
- * The host supplies transport and readiness operations. Callers use `LspClient` as
46
- * the external seam and do not access this state directly.
47
- */
46
+ /** Bounded push wait after a reopen-resync fallback, in milliseconds. */
47
+ const REOPEN_EVIDENCE_WAIT_MS = 1_000;
48
+ /** Own one client's document and diagnostic evidence; revisions prevent stale reuse. */
48
49
  export class ClientDiagnostics {
49
- readonly #openDocs = new Map<string, OpenDocument>();
50
+ readonly #openDocs = new Map<string, OpenDocumentState>();
50
51
  readonly #diagnosticStore = new Map<string, DiagnosticCacheEntry>();
51
- readonly #diagnosticWaiters = new Map<string, Array<() => void>>();
52
+ readonly #waiters = new DiagnosticWaitRegistry();
53
+ readonly #versionHistory = new Map<string, number>();
54
+ readonly #failedUris = new Set<string>();
55
+ /** Client-side sync moment per URI: unversioned pushes before it stay rejected. */
56
+ readonly #unversionedPushSyncMoments = new Map<string, number>();
57
+ /** URIs closed by a lifecycle operation: versioned pushes stay fail-closed. */
58
+ readonly #closedVersionedBarrier = new Set<string>();
59
+ #evidenceRevision = 0;
60
+ #nextSynchronizationId = 0;
52
61
 
53
62
  constructor(private readonly host: ClientDiagnosticsHost) {}
54
63
 
@@ -56,36 +65,58 @@ export class ClientDiagnostics {
56
65
  return Array.from(this.#openDocs.keys()).map(uriToFile);
57
66
  }
58
67
 
59
- /** Release all waiters and remove all document and diagnostic state. */
60
- clear(): void {
68
+ clear(options: { preserveFailedDocuments?: boolean } = {}): void {
69
+ if (options.preserveFailedDocuments) {
70
+ for (const uri of [...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]) {
71
+ this.#failedUris.add(uri);
72
+ this.#closedVersionedBarrier.add(uri);
73
+ }
74
+ } else {
75
+ this.#failedUris.clear();
76
+ this.#closedVersionedBarrier.clear();
77
+ }
61
78
  this.#openDocs.clear();
62
79
  this.#diagnosticStore.clear();
63
- this.#releaseAllDiagnosticWaiters();
80
+ this.#versionHistory.clear();
81
+ this.#evidenceRevision++;
82
+ this.#unversionedPushSyncMoments.clear();
83
+ this.#waiters.releaseAll();
84
+ this.#waiters.cancelSettle();
64
85
  }
65
86
 
66
- /** Open a document, or update it when it is already open. */
67
87
  didOpen(filePath: string, content: string): void {
68
88
  if (!this.host.isOperational()) return;
69
89
 
70
90
  const uri = fileToUri(filePath);
91
+ this.#failedUris.delete(uri);
71
92
  if (this.#openDocs.has(uri)) {
72
93
  this.didChange(filePath, content);
73
94
  return;
74
95
  }
75
96
 
76
97
  const languageId = detectLanguageId(filePath);
77
- this.#openDocs.set(uri, { version: 1 });
98
+ this.#waiters.cancelSettle();
99
+ // A didOpen is a synchronization: pushes the server sends after it are
100
+ // responses to the open and can become fresh evidence (ADR 0020).
101
+ this.#unversionedPushSyncMoments.set(uri, Date.now());
102
+ this.#closedVersionedBarrier.delete(uri);
103
+ const version = nextDocumentVersion(this.#versionHistory, uri);
104
+ this.#openDocs.set(uri, {
105
+ version,
106
+ synchronizationId: ++this.#nextSynchronizationId,
107
+ evidenceRevision: this.#evidenceRevision,
108
+ contentFingerprint: fingerprintDocumentContent(content),
109
+ });
78
110
  this.host.sendNotification("textDocument/didOpen", {
79
111
  textDocument: {
80
112
  uri,
81
113
  languageId,
82
- version: 1,
114
+ version,
83
115
  text: content,
84
116
  } satisfies TextDocumentItem,
85
117
  });
86
118
  }
87
119
 
88
- /** Update a document, or open it when it is not tracked yet. */
89
120
  didChange(filePath: string, content: string): void {
90
121
  if (!this.host.isOperational()) return;
91
122
 
@@ -95,267 +126,340 @@ export class ClientDiagnostics {
95
126
  this.didOpen(filePath, content);
96
127
  return;
97
128
  }
98
-
99
- doc.version++;
100
- this.#sendDidChange(uri, doc.version, content);
129
+ const nextFingerprint = fingerprintDocumentContent(content);
130
+ if (doc.contentFingerprint === nextFingerprint) return;
131
+ synchronizeTrackedDocument({
132
+ uri,
133
+ content,
134
+ document: doc,
135
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
136
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
137
+ evidenceRevision: this.#evidenceRevision,
138
+ waiters: this.#waiters,
139
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
140
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
141
+ clearFailedFile: () => this.#failedUris.delete(uri),
142
+ open: () => this.didOpen(filePath, content),
143
+ });
101
144
  }
102
145
 
103
- /** Close a document and remove its cached diagnostic state. */
104
146
  didClose(filePath: string): void {
105
147
  const uri = fileToUri(filePath);
106
148
  const wasOpen = this.#openDocs.has(uri);
107
- this.#clearFileState(uri);
149
+ this.#failedUris.delete(uri);
150
+ this.#unversionedPushSyncMoments.delete(uri);
151
+ this.#closedVersionedBarrier.add(uri);
152
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
108
153
 
109
154
  if (wasOpen && this.host.isOperational()) {
110
- this.#sendDidClose(uri);
155
+ sendDidCloseNotification(this.host, uri);
111
156
  }
112
157
  }
113
158
 
114
- /** Remove missing open documents and diagnostics, and return their file paths. */
115
159
  pruneMissingFiles(): string[] {
116
160
  const uris = new Set([...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]);
117
161
  const removedFiles: string[] = [];
118
162
 
119
163
  for (const uri of uris) {
120
164
  const filePath = uriToFile(uri);
121
- if (existsSync(filePath)) continue;
165
+ if (getDiagnosticFileState(filePath) !== "removed") continue;
122
166
 
123
167
  const wasOpen = this.#openDocs.has(uri);
124
- this.#clearFileState(uri);
168
+ this.#failedUris.add(uri);
169
+ this.#unversionedPushSyncMoments.delete(uri);
170
+ this.#closedVersionedBarrier.add(uri);
171
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
125
172
  removedFiles.push(filePath);
126
- if (wasOpen && this.host.isOperational()) this.#sendDidClose(uri);
173
+ if (wasOpen && this.host.isOperational()) sendDidCloseNotification(this.host, uri);
127
174
  }
128
175
 
129
176
  return removedFiles;
130
177
  }
178
+ /** Retain a failed document outcome when a replacement cannot reopen it. */
179
+ markFailedFile(filePath: string): void {
180
+ const uri = fileToUri(filePath);
181
+ this.#failedUris.add(uri);
182
+ this.#unversionedPushSyncMoments.delete(uri);
183
+ this.#closedVersionedBarrier.add(uri);
184
+ }
131
185
 
186
+ getOpenDocumentVersion(filePath: string): number | null {
187
+ return this.#openDocs.get(fileToUri(filePath))?.version ?? null;
188
+ }
132
189
  getDiagnostics(filePath: string): Diagnostic[] {
133
190
  return this.#diagnosticStore.get(fileToUri(filePath))?.diagnostics ?? [];
134
191
  }
135
-
136
- /** Return non-empty diagnostics for files that still exist. */
192
+ getDiagnosticSnapshot(): ClientDiagnosticSnapshot {
193
+ return buildClientDiagnosticSnapshot({
194
+ store: this.#diagnosticStore,
195
+ openDocuments: this.#openDocs,
196
+ failedDocuments: this.#failedUris,
197
+ evidenceRevision: this.#evidenceRevision,
198
+ });
199
+ }
137
200
  getAllDiagnostics(): DiagnosticEntry[] {
138
- const result: DiagnosticEntry[] = [];
139
- for (const [uri, entry] of this.#diagnosticStore) {
140
- if (entry.diagnostics.length === 0 || !existsSync(uriToFile(uri))) continue;
141
- result.push({ uri, diagnostics: entry.diagnostics });
142
- }
143
- return result;
201
+ return this.getDiagnosticSnapshot().entries;
144
202
  }
145
-
146
- /** Force the next pull refresh to request complete diagnostic reports. */
147
203
  clearPullResultIds(): void {
148
204
  for (const entry of this.#diagnosticStore.values()) delete entry.resultId;
149
205
  }
150
-
151
- /** Apply diagnostics received through `textDocument/publishDiagnostics`. */
152
- handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
153
- if (params.version !== undefined && params.version !== null) {
154
- const openDoc = this.#openDocs.get(params.uri);
155
- if (openDoc && params.version < openDoc.version) return;
206
+ /** Invalidate cache proof while retaining its data as partial fallback. */
207
+ invalidateCachedEvidence(): void {
208
+ this.#evidenceRevision++;
209
+ const knownUris = new Set([
210
+ ...this.#openDocs.keys(),
211
+ ...this.#diagnosticStore.keys(),
212
+ ...this.#versionHistory.keys(),
213
+ ...this.#failedUris,
214
+ ]);
215
+ // A watched-file notification is sent immediately after this call, so the
216
+ // recorded moment gates unversioned pushes around the invalidation: only
217
+ // pushes the server sends after the change can become fresh evidence.
218
+ const moment = Date.now();
219
+ for (const uri of knownUris) {
220
+ this.#unversionedPushSyncMoments.set(uri, moment);
221
+ this.#closedVersionedBarrier.add(uri);
156
222
  }
157
-
158
- this.#diagnosticStore.set(params.uri, {
159
- diagnostics: params.diagnostics,
160
- receivedAt: Date.now(),
161
- version: params.version ?? undefined,
162
- });
163
- this.#releaseDiagnosticWaiters(params.uri);
164
223
  }
165
224
 
166
- /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
167
- async refreshOpenDiagnostics(
168
- options: { maxWaitMs?: number; quietMs?: number } = {},
169
- ): Promise<void> {
170
- if (!this.host.isOperational()) return;
171
-
172
- const maxWaitMs = options.maxWaitMs ?? 3_000;
173
- const quietMs = options.quietMs ?? 200;
174
- const syncStart = Date.now();
175
-
176
- this.#resyncOpenDocuments();
177
- if (this.#openDocs.size === 0) return;
178
-
179
- if (this.host.supportsPullDiagnostics()) {
180
- try {
181
- await this.#pullDiagnosticsForOpenDocuments(syncStart, maxWaitMs);
182
- return;
183
- } catch {
184
- // Pull diagnostics failed. Wait for push diagnostics instead.
185
- }
225
+ handlePublishDiagnostics(params: unknown): void {
226
+ if (!isValidPublishDiagnosticsParams(params)) return;
227
+ if (
228
+ !applyPushDiagnostics({
229
+ store: this.#diagnosticStore,
230
+ openDocuments: this.#openDocs,
231
+ params,
232
+ evidenceRevision: this.#evidenceRevision,
233
+ unversionedSyncMoment: this.#unversionedPushSyncMoments.get(params.uri),
234
+ closedVersionedBarrier: this.#closedVersionedBarrier.has(params.uri),
235
+ })
236
+ ) {
237
+ return;
186
238
  }
187
-
188
- await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
239
+ this.#waiters.releaseFile(params.uri, "published");
240
+ this.#waiters.notifySettle();
241
+ }
242
+ async refreshOpenDiagnostics(
243
+ options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl = {},
244
+ ): Promise<DiagnosticEvidenceSummary> {
245
+ const requestedFiles = Array.from(
246
+ new Set([...this.#openDocs.keys(), ...this.#diagnosticStore.keys(), ...this.#failedUris]),
247
+ ).map(uriToFile);
248
+ return refreshClientOpenDiagnostics({
249
+ host: this.host,
250
+ openDocuments: this.#openDocs,
251
+ diagnosticStore: this.#diagnosticStore,
252
+ waiters: this.#waiters,
253
+ versionHistory: this.#versionHistory,
254
+ requestedFiles,
255
+ evidenceRevision: () => this.#evidenceRevision,
256
+ failedFiles: () => new Set(Array.from(this.#failedUris).map(uriToFile)),
257
+ isRelatedUriTracked: (uri) => this.#openDocs.has(uri) || this.#versionHistory.has(uri),
258
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
259
+ invalidateEvidence: (uri) => {
260
+ this.#failedUris.add(uri);
261
+ const document = this.#openDocs.get(uri);
262
+ if (document) document.evidenceRevision = -1;
263
+ },
264
+ clearFile: (uri) => {
265
+ this.#failedUris.add(uri);
266
+ this.#unversionedPushSyncMoments.delete(uri);
267
+ this.#closedVersionedBarrier.add(uri);
268
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
269
+ },
270
+ markUnversionedSyncMoment: (uri) => this.#unversionedPushSyncMoments.set(uri, Date.now()),
271
+ clearFailedFile: (uri) => this.#failedUris.delete(uri),
272
+ options,
273
+ });
189
274
  }
190
275
 
191
- /** Sync one file and return its diagnostics after pull or push collection. */
192
- async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
276
+ async syncAndWaitForDiagnostics(
277
+ filePath: string,
278
+ content: string,
279
+ control?: CodeRequestControl,
280
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
281
+ // Reject immediately when the request was already cancelled: no document
282
+ // synchronization or protocol traffic may start for a caller that no
283
+ // longer awaits a result.
284
+ throwIfCodeRequestInterrupted(control);
285
+ const supportsPull = this.host.supportsPullDiagnostics();
286
+ const observer = new DiagnosticObserver("sync-file", supportsPull, control, {
287
+ server: this.host.server,
288
+ cwd: this.host.cwd,
289
+ file: relativeDiagnosticFile(this.host.cwd, filePath),
290
+ });
193
291
  const uri = fileToUri(filePath);
292
+ const cached = this.#diagnosticStore.get(uri);
293
+ const cachedDiagnostics = cached ? [...cached.diagnostics] : null;
194
294
  const syncStart = Date.now();
195
- this.didChange(filePath, content);
196
-
197
- if (this.host.supportsPullDiagnostics()) {
198
- const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
199
- if (remaining > 0) {
200
- try {
201
- const pulled = await this.#pullDiagnosticsForUri(uri, remaining);
202
- if (pulled) return this.getDiagnostics(filePath);
203
- } catch {
204
- // Pull diagnostics failed. Wait for push diagnostics instead.
205
- }
206
- }
207
- }
208
-
209
- await this.#waitForDiagnostics(uri, Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)));
210
- return this.getDiagnostics(filePath);
211
- }
212
-
213
- #resyncOpenDocuments(): void {
214
- for (const [uri, doc] of this.#openDocs) {
215
- const filePath = uriToFile(uri);
216
- try {
217
- if (!existsSync(filePath)) {
218
- this.#clearFileState(uri);
219
- this.#sendDidClose(uri);
220
- continue;
221
- }
222
- const content = readFileSync(filePath, "utf-8");
223
- doc.version++;
224
- this.#sendDidChange(uri, doc.version, content);
225
- } catch {
226
- // Keep the document open when a transient read fails.
227
- }
228
- }
229
- }
230
-
231
- async #pullDiagnosticsForOpenDocuments(syncStart: number, maxWaitMs: number): Promise<void> {
232
- const deadline = syncStart + maxWaitMs;
233
- const uris = Array.from(this.#openDocs.keys());
234
- const results = await Promise.allSettled(
235
- uris.map(async (uri) => {
236
- const remaining = deadline - Date.now();
237
- if (remaining <= 0) throw new Error("pull diagnostic timeout");
238
- return this.#pullDiagnosticsForUri(uri, remaining);
239
- }),
240
- );
241
-
242
- const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
243
- const hadFailure = results.some((result) => result.status === "rejected");
244
- if ((hadFailure || !anySuccess) && uris.length > 0) {
245
- throw new Error("pull diagnostics incomplete");
246
- }
247
- }
248
-
249
- async #pullDiagnosticsForUri(uri: string, timeoutMs: number): Promise<boolean> {
250
- const previousResultId = this.#diagnosticStore.get(uri)?.resultId;
251
- const report = await this.host.pullDocumentDiagnostics(uri, previousResultId, timeoutMs);
252
- if (!report) return false;
253
- this.#applyPullReport(uri, report);
254
- return true;
255
- }
256
-
257
- #applyPullReport(uri: string, report: DocumentDiagnosticReport): void {
258
- if (report.kind === "full") {
259
- this.#diagnosticStore.set(uri, {
260
- diagnostics: report.items,
261
- receivedAt: Date.now(),
262
- resultId: report.resultId,
263
- });
264
- } else {
265
- const current = this.#diagnosticStore.get(uri);
266
- if (current) current.resultId = report.resultId;
267
- }
268
-
269
- for (const [relatedUri, relatedReport] of Object.entries(report.relatedDocuments ?? {})) {
270
- if (relatedReport.kind !== "full") continue;
271
- this.#diagnosticStore.set(relatedUri, {
272
- diagnostics: relatedReport.items,
273
- receivedAt: Date.now(),
274
- resultId: relatedReport.resultId,
295
+ const openDocument = this.#openDocs.get(uri);
296
+ const contentUnchanged =
297
+ openDocument?.contentFingerprint === fingerprintDocumentContent(content);
298
+ const synchronizationCurrent = openDocument?.evidenceRevision === this.#evidenceRevision;
299
+ const cacheCurrent = hasCurrentDiagnosticEvidence(openDocument, cached, this.#evidenceRevision);
300
+ if (!contentUnchanged || !synchronizationCurrent || (cached !== undefined && !cacheCurrent)) {
301
+ synchronizeTrackedDocument({
302
+ uri,
303
+ content,
304
+ document: this.#openDocs.get(uri),
305
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
306
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
307
+ evidenceRevision: this.#evidenceRevision,
308
+ waiters: this.#waiters,
309
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
310
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
311
+ clearFailedFile: () => this.#failedUris.delete(uri),
312
+ open: () => this.didOpen(filePath, content),
275
313
  });
276
314
  }
277
- }
278
-
279
- async #waitForDiagnosticSettle(
280
- syncStart: number,
281
- maxWaitMs: number,
282
- quietMs: number,
283
- ): Promise<void> {
284
- const deadline = syncStart + maxWaitMs;
285
- while (Date.now() < deadline) {
286
- const lastReceived = this.#lastDiagnosticReceivedAfter(syncStart) || syncStart;
287
- const elapsed = Date.now() - lastReceived;
288
- if (elapsed >= quietMs) return;
289
- await new Promise((resolve) =>
290
- setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
291
- );
315
+ const synchronization = this.#openDocs.get(uri);
316
+ observer.synchronized();
317
+ if (!synchronization) {
318
+ return unavailableCodeQuery("The document could not be synchronized for diagnostics.");
292
319
  }
293
- }
294
-
295
- #lastDiagnosticReceivedAfter(afterTime: number): number {
296
- let latest = 0;
297
- for (const entry of this.#diagnosticStore.values()) {
298
- if (entry.receivedAt > afterTime && entry.receivedAt > latest) latest = entry.receivedAt;
320
+ if (
321
+ contentUnchanged &&
322
+ hasCurrentDiagnosticEvidence(synchronization, cached, this.#evidenceRevision)
323
+ ) {
324
+ observer.cacheReused(1);
325
+ return completedCodeQuery(cachedDiagnostics ?? []);
299
326
  }
300
- return latest;
301
- }
302
-
303
- #sendDidChange(uri: string, version: number, content: string): void {
304
- this.host.sendNotification("textDocument/didChange", {
305
- textDocument: { uri, version } satisfies VersionedTextDocumentIdentifier,
306
- contentChanges: [{ text: content }],
307
- });
308
- }
309
-
310
- #sendDidClose(uri: string): void {
311
- this.host.sendNotification("textDocument/didClose", {
312
- textDocument: { uri } satisfies TextDocumentIdentifier,
313
- });
314
- }
315
-
316
- #clearFileState(uri: string): void {
317
- this.#openDocs.delete(uri);
318
- this.#diagnosticStore.delete(uri);
319
- this.#releaseDiagnosticWaiters(uri);
327
+ const request = {
328
+ uri,
329
+ synchronizationId: synchronization.synchronizationId,
330
+ evidenceRevision: synchronization.evidenceRevision,
331
+ };
332
+ return this.#collectFileDiagnosticsWithReopenRetry(
333
+ {
334
+ filePath,
335
+ content,
336
+ uri,
337
+ supportsPull,
338
+ syncStart,
339
+ request,
340
+ cachedDiagnostics,
341
+ observer,
342
+ },
343
+ control,
344
+ );
320
345
  }
321
346
 
322
- #waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
323
- if (timeoutMs <= 0) return Promise.resolve();
324
-
325
- return new Promise<void>((resolve) => {
326
- const waiter = () => {
327
- clearTimeout(timer);
328
- this.#removeDiagnosticWaiter(uri, waiter);
329
- resolve();
347
+ /**
348
+ * Collect diagnostics for one synchronized document, retrying once through
349
+ * a reopen-resync on push-only routes when the first wait times out.
350
+ */
351
+ async #collectFileDiagnosticsWithReopenRetry(
352
+ options: {
353
+ filePath: string;
354
+ content: string;
355
+ uri: string;
356
+ supportsPull: boolean;
357
+ syncStart: number;
358
+ request: DiagnosticSynchronization;
359
+ cachedDiagnostics: Diagnostic[] | null;
360
+ observer: DiagnosticObserver;
361
+ },
362
+ control?: CodeRequestControl,
363
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
364
+ const {
365
+ filePath,
366
+ content,
367
+ uri,
368
+ supportsPull,
369
+ syncStart,
370
+ request,
371
+ cachedDiagnostics,
372
+ observer,
373
+ } = options;
374
+ let pushOutcome: DiagnosticPushWaitOutcome | undefined;
375
+ let attemptRequest = request;
376
+ let attemptSyncStart = syncStart;
377
+ let attemptMaxWaitMs = DIAGNOSTIC_WAIT_MS;
378
+ let reopenedOnce = false;
379
+ let result: CodeQueryResult<Diagnostic[]>;
380
+ for (;;) {
381
+ result = await collectSynchronizedFileDiagnostics(
382
+ {
383
+ supportsPull,
384
+ syncStart: attemptSyncStart,
385
+ maxWaitMs: attemptMaxWaitMs,
386
+ request: attemptRequest,
387
+ cachedDiagnostics,
388
+ observer,
389
+ waiters: this.#waiters,
390
+ current: () => isCurrentSynchronization(this.#openDocs, attemptRequest),
391
+ freshPush: () =>
392
+ hasFreshPush(this.#diagnosticStore, attemptRequest, this.#evidenceRevision),
393
+ diagnostics: () => this.getDiagnostics(filePath),
394
+ pullDiagnostics: (timeoutMs, signal) =>
395
+ pullClientDiagnosticEvidenceFromHost({
396
+ host: this.host,
397
+ store: this.#diagnosticStore,
398
+ openDocuments: this.#openDocs,
399
+ currentEvidenceRevision: () => this.#evidenceRevision,
400
+ isRelatedUriTracked: (relatedUri) =>
401
+ this.#openDocs.has(relatedUri) || this.#versionHistory.has(relatedUri),
402
+ request: {
403
+ uri,
404
+ timeoutMs,
405
+ synchronizationId: attemptRequest.synchronizationId,
406
+ evidenceRevision: attemptRequest.evidenceRevision ?? this.#evidenceRevision,
407
+ signal,
408
+ deadline: control?.deadline,
409
+ operationId: control?.operationId,
410
+ },
411
+ }),
412
+ onPushWait: (outcome) => {
413
+ pushOutcome = outcome;
414
+ },
415
+ },
416
+ control,
417
+ );
418
+ // Reopen-resync fallback (R2): on push-only routes a document that
419
+ // timed out with no push stays unconfirmed — a clean file gets no
420
+ // push on didChange at all. Close and reopen it over the protocol so
421
+ // the server publishes on didOpen, then wait once more with a bounded
422
+ // budget. The cache entry and version history survive the reopen.
423
+ const document = this.#openDocs.get(uri);
424
+ if (
425
+ supportsPull ||
426
+ reopenedOnce ||
427
+ result.kind === "completed" ||
428
+ pushOutcome !== "timed-out" ||
429
+ !document ||
430
+ !isCurrentSynchronization(this.#openDocs, attemptRequest)
431
+ ) {
432
+ break;
433
+ }
434
+ reopenDocument({
435
+ uri,
436
+ content,
437
+ document,
438
+ languageId: detectLanguageId(filePath),
439
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
440
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
441
+ evidenceRevision: this.#evidenceRevision,
442
+ waiters: this.#waiters,
443
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
444
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
445
+ });
446
+ observer.reopened(1);
447
+ reopenedOnce = true;
448
+ attemptRequest = {
449
+ uri,
450
+ synchronizationId: document.synchronizationId,
451
+ evidenceRevision: document.evidenceRevision,
330
452
  };
331
- const timer = setTimeout(() => {
332
- this.#removeDiagnosticWaiter(uri, waiter);
333
- resolve();
334
- }, timeoutMs);
335
- const waiters = this.#diagnosticWaiters.get(uri) ?? [];
336
- waiters.push(waiter);
337
- this.#diagnosticWaiters.set(uri, waiters);
338
- });
339
- }
340
-
341
- #removeDiagnosticWaiter(uri: string, waiter: () => void): void {
342
- const waiters = this.#diagnosticWaiters.get(uri);
343
- if (!waiters) return;
344
- const next = waiters.filter((entry) => entry !== waiter);
345
- if (next.length > 0) this.#diagnosticWaiters.set(uri, next);
346
- else this.#diagnosticWaiters.delete(uri);
347
- }
348
-
349
- #releaseAllDiagnosticWaiters(): void {
350
- for (const uri of Array.from(this.#diagnosticWaiters.keys())) {
351
- this.#releaseDiagnosticWaiters(uri);
453
+ attemptSyncStart = Date.now();
454
+ attemptMaxWaitMs = REOPEN_EVIDENCE_WAIT_MS;
352
455
  }
456
+ observer.pushWaitCompleted(1, pushOutcome ?? "timed-out");
457
+ return result;
353
458
  }
459
+ }
354
460
 
355
- #releaseDiagnosticWaiters(uri: string): void {
356
- const waiters = this.#diagnosticWaiters.get(uri);
357
- if (!waiters) return;
358
- this.#diagnosticWaiters.delete(uri);
359
- for (const waiter of waiters) waiter();
360
- }
461
+ /** Return a workspace-relative diagnostic file path for telemetry identity. */
462
+ function relativeDiagnosticFile(cwd: string | undefined, filePath: string): string | undefined {
463
+ if (cwd === undefined) return undefined;
464
+ return path.relative(cwd, path.resolve(cwd, filePath));
361
465
  }