@mrclrchtr/supi-code-intelligence 0.1.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 (146) hide show
  1. package/README.md +212 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
  52. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
  66. package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
  69. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
  70. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
  73. package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
  74. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
  127. package/package.json +47 -0
  128. package/src/actions/affected-action.ts +310 -0
  129. package/src/actions/brief-action.ts +242 -0
  130. package/src/actions/callees-action.ts +134 -0
  131. package/src/actions/callers-action.ts +215 -0
  132. package/src/actions/implementations-action.ts +190 -0
  133. package/src/actions/index-action.ts +187 -0
  134. package/src/actions/pattern-action.ts +232 -0
  135. package/src/architecture.ts +367 -0
  136. package/src/brief-focused.ts +383 -0
  137. package/src/brief.ts +228 -0
  138. package/src/code-intelligence.ts +122 -0
  139. package/src/git-context.ts +65 -0
  140. package/src/guidance.ts +39 -0
  141. package/src/index.ts +28 -0
  142. package/src/resolve-target.ts +104 -0
  143. package/src/search-helpers.ts +283 -0
  144. package/src/target-resolution.ts +368 -0
  145. package/src/tool-actions.ts +109 -0
  146. package/src/types.ts +57 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@mrclrchtr/supi-lsp",
3
+ "version": "0.1.0",
4
+ "description": "SuPi LSP extension — Language Server Protocol integration for pi",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mrclrchtr/supi.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi",
16
+ "pi-coding-agent"
17
+ ],
18
+ "files": [
19
+ "src/**/*.ts",
20
+ "src/**/*.json",
21
+ "!__tests__"
22
+ ],
23
+ "dependencies": {
24
+ "@mrclrchtr/supi-core": "workspace:*"
25
+ },
26
+ "bundledDependencies": [
27
+ "@mrclrchtr/supi-core"
28
+ ],
29
+ "peerDependencies": {
30
+ "@earendil-works/pi-ai": "*",
31
+ "@earendil-works/pi-coding-agent": "*",
32
+ "@earendil-works/pi-tui": "*",
33
+ "typebox": "*"
34
+ },
35
+ "devDependencies": {
36
+ "vitest": "^4.1.4",
37
+ "@mrclrchtr/supi-test-utils": "workspace:*"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./src/lsp.ts"
42
+ ]
43
+ },
44
+ "main": "src/index.ts"
45
+ }
@@ -0,0 +1,62 @@
1
+ // LSP client capabilities — declares what we support to servers.
2
+
3
+ import type { ClientCapabilities } from "./types.ts";
4
+
5
+ export const CLIENT_CAPABILITIES: ClientCapabilities = {
6
+ textDocument: {
7
+ synchronization: {
8
+ didSave: true,
9
+ dynamicRegistration: false,
10
+ },
11
+ hover: {
12
+ contentFormat: ["markdown", "plaintext"],
13
+ dynamicRegistration: false,
14
+ },
15
+ definition: {
16
+ dynamicRegistration: false,
17
+ linkSupport: true,
18
+ },
19
+ references: {
20
+ dynamicRegistration: false,
21
+ },
22
+ documentSymbol: {
23
+ dynamicRegistration: false,
24
+ hierarchicalDocumentSymbolSupport: true,
25
+ },
26
+ rename: {
27
+ dynamicRegistration: false,
28
+ prepareSupport: true,
29
+ },
30
+ codeAction: {
31
+ dynamicRegistration: false,
32
+ codeActionLiteralSupport: {
33
+ codeActionKind: {
34
+ valueSet: [
35
+ "quickfix",
36
+ "refactor",
37
+ "refactor.extract",
38
+ "refactor.inline",
39
+ "refactor.rewrite",
40
+ "source",
41
+ "source.organizeImports",
42
+ "source.fixAll",
43
+ ],
44
+ },
45
+ },
46
+ },
47
+ publishDiagnostics: {
48
+ relatedInformation: true,
49
+ versionSupport: true,
50
+ },
51
+ diagnostic: {
52
+ dynamicRegistration: false,
53
+ relatedDocumentSupport: true,
54
+ },
55
+ },
56
+ workspace: {
57
+ workspaceFolders: false,
58
+ diagnostics: {
59
+ refreshSupport: false,
60
+ },
61
+ },
62
+ };
@@ -0,0 +1,229 @@
1
+ // LSP Client diagnostic refresh — pull diagnostics and push settle logic.
2
+ // Extracted from client.ts to keep file sizes manageable.
3
+
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import type { DocumentDiagnosticReport, TextDocumentIdentifier } from "../types.ts";
6
+ import { uriToFile } from "../utils.ts";
7
+ import type { DiagnosticCacheEntry, LspClient } from "./client.ts";
8
+ import type { JsonRpcClient } from "./transport.ts";
9
+
10
+ interface ClientAccess {
11
+ openDocs: Map<string, { version: number; languageId: string }>;
12
+ rpc: JsonRpcClient | null;
13
+ diagnosticStore: Map<string, DiagnosticCacheEntry>;
14
+ releaseDiagnosticWaiters(uri: string): void;
15
+ }
16
+
17
+ function accessClient(client: LspClient): ClientAccess {
18
+ return client as unknown as ClientAccess;
19
+ }
20
+
21
+ /**
22
+ * Re-read and re-sync all currently open, existing documents for a client.
23
+ * Uses pull diagnostics when the server supports them, otherwise falls back
24
+ * to push-diagnostic settling. Never throws.
25
+ */
26
+ export async function refreshClientOpenDiagnostics(
27
+ client: LspClient,
28
+ options: { maxWaitMs?: number; quietMs?: number } = {},
29
+ ): Promise<void> {
30
+ const maxWaitMs = options.maxWaitMs ?? 3_000;
31
+ const quietMs = options.quietMs ?? 200;
32
+
33
+ if (client.status !== "running") return;
34
+
35
+ const syncStart = Date.now();
36
+
37
+ // Re-sync all open documents that still exist on disk
38
+ const openDocs = accessClient(client).openDocs as Map<
39
+ string,
40
+ { version: number; languageId: string }
41
+ >;
42
+
43
+ for (const [uri, doc] of openDocs) {
44
+ const filePath = uriToFile(uri);
45
+ try {
46
+ if (!existsSync(filePath)) {
47
+ clearFileState(client, uri);
48
+ sendNotification(client, "textDocument/didClose", {
49
+ textDocument: { uri } satisfies TextDocumentIdentifier,
50
+ });
51
+ continue;
52
+ }
53
+ const content = readFileSync(filePath, "utf-8");
54
+ doc.version++;
55
+ sendDidChange(client, uri, doc.version, content);
56
+ } catch {
57
+ // Read error — skip this file, keep it open
58
+ }
59
+ }
60
+
61
+ if (openDocs.size === 0) return;
62
+
63
+ // Try pull diagnostics if server supports them
64
+ if (client.hasDiagnosticProvider) {
65
+ try {
66
+ await pullDiagnosticsForOpenDocs(client, syncStart, maxWaitMs);
67
+ return;
68
+ } catch {
69
+ // Pull diagnostics failed — fall back to push settle
70
+ }
71
+ }
72
+
73
+ // Fall back to push-diagnostic settling
74
+ await waitForDiagnosticSettle(client, syncStart, maxWaitMs, quietMs);
75
+ }
76
+
77
+ /** Send a didChange notification through the client's RPC. */
78
+ function sendDidChange(client: LspClient, uri: string, version: number, content: string): void {
79
+ sendNotification(client, "textDocument/didChange", {
80
+ textDocument: { uri, version },
81
+ contentChanges: [{ text: content }],
82
+ });
83
+ }
84
+
85
+ /** Send an RPC notification through the client. */
86
+ function sendNotification(client: LspClient, method: string, params: unknown): void {
87
+ const rpc = accessClient(client).rpc;
88
+ if (rpc) rpc.sendNotification(method, params);
89
+ }
90
+
91
+ /** Clear open doc and diagnostic state for a URI. */
92
+ function clearFileState(client: LspClient, uri: string): void {
93
+ accessClient(client).openDocs.delete(uri);
94
+ accessClient(client).diagnosticStore.delete(uri);
95
+ accessClient(client).releaseDiagnosticWaiters(uri);
96
+ }
97
+
98
+ /**
99
+ * Request pull diagnostics for all open documents.
100
+ * Throws if no diagnostics were successfully retrieved.
101
+ */
102
+ async function pullDiagnosticsForOpenDocs(
103
+ client: LspClient,
104
+ syncStart: number,
105
+ maxWaitMs: number,
106
+ ): Promise<void> {
107
+ const deadline = syncStart + maxWaitMs;
108
+ const uris = Array.from((accessClient(client).openDocs as Map<string, unknown>).keys());
109
+ const results = await Promise.allSettled(
110
+ uris.map(async (uri) => {
111
+ const remaining = deadline - Date.now();
112
+ if (remaining <= 0) throw new Error("pull diagnostic timeout");
113
+
114
+ return pullDiagnosticsForUri(client, uri, remaining);
115
+ }),
116
+ );
117
+
118
+ const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
119
+ const hadFailure = results.some((result) => result.status === "rejected");
120
+
121
+ if ((hadFailure || !anySuccess) && uris.length > 0) {
122
+ throw new Error("pull diagnostics incomplete");
123
+ }
124
+ }
125
+
126
+ /** Pull diagnostics for a single URI and apply the report to the cache. */
127
+ export async function pullDiagnosticsForUri(
128
+ client: LspClient,
129
+ uri: string,
130
+ timeoutMs: number,
131
+ ): Promise<boolean> {
132
+ const report = await pullDocumentDiagnostics(client, uri, timeoutMs);
133
+ if (!report) return false;
134
+ applyPullReport(client, uri, report);
135
+ return true;
136
+ }
137
+
138
+ /** Pull diagnostics for a single document with timeout. */
139
+ async function pullDocumentDiagnostics(
140
+ client: LspClient,
141
+ uri: string,
142
+ timeoutMs: number,
143
+ ): Promise<DocumentDiagnosticReport | null> {
144
+ const rpc = accessClient(client).rpc;
145
+ if (!rpc || client.status !== "running") {
146
+ throw new Error("client not running");
147
+ }
148
+
149
+ const previousResultId = accessClient(client).diagnosticStore.get(uri)?.resultId;
150
+ return rpc.sendRequest(
151
+ "textDocument/diagnostic",
152
+ {
153
+ textDocument: { uri },
154
+ previousResultId,
155
+ },
156
+ { timeoutMs },
157
+ ) as Promise<DocumentDiagnosticReport>;
158
+ }
159
+
160
+ /** Apply a pull diagnostic report to the cache, including related documents. */
161
+ function applyPullReport(client: LspClient, uri: string, report: DocumentDiagnosticReport): void {
162
+ if (report.kind === "full") {
163
+ accessClient(client).diagnosticStore.set(uri, {
164
+ diagnostics: report.items,
165
+ receivedAt: Date.now(),
166
+ resultId: report.resultId,
167
+ });
168
+ } else if (report.kind === "unchanged" && report.resultId) {
169
+ const current = accessClient(client).diagnosticStore.get(uri);
170
+ if (current) current.resultId = report.resultId;
171
+ }
172
+
173
+ applyRelatedDocuments(client, report);
174
+ }
175
+
176
+ /** Extract and store related document diagnostics from a pull report. */
177
+ function applyRelatedDocuments(client: LspClient, report: DocumentDiagnosticReport): void {
178
+ const related = (report as unknown as Record<string, unknown>).relatedDocuments;
179
+ if (!related || typeof related !== "object") return;
180
+
181
+ for (const [relatedUri, relatedReport] of Object.entries(
182
+ related as Record<string, DocumentDiagnosticReport>,
183
+ )) {
184
+ if (relatedReport.kind === "full" && relatedReport.items) {
185
+ accessClient(client).diagnosticStore.set(relatedUri, {
186
+ diagnostics: relatedReport.items,
187
+ receivedAt: Date.now(),
188
+ resultId: relatedReport.resultId,
189
+ });
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Wait until no publishDiagnostics notifications arrive for quietMs
196
+ * after syncStart, or until maxWaitMs elapses.
197
+ */
198
+ async function waitForDiagnosticSettle(
199
+ client: LspClient,
200
+ syncStart: number,
201
+ maxWaitMs: number,
202
+ quietMs: number,
203
+ ): Promise<void> {
204
+ const deadline = syncStart + maxWaitMs;
205
+
206
+ while (Date.now() < deadline) {
207
+ const lastReceived = lastDiagnosticReceivedTimeAfter(client, syncStart) || syncStart;
208
+ const elapsed = Date.now() - lastReceived;
209
+ if (elapsed >= quietMs) {
210
+ return;
211
+ }
212
+
213
+ await new Promise((resolve) =>
214
+ setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
215
+ );
216
+ }
217
+ }
218
+
219
+ /** Get the most recent receivedAt timestamp after a given time. */
220
+ function lastDiagnosticReceivedTimeAfter(client: LspClient, afterTime: number): number {
221
+ let latest = 0;
222
+ const store = accessClient(client).diagnosticStore as Map<string, { receivedAt: number }>;
223
+ for (const entry of store.values()) {
224
+ if (entry.receivedAt > afterTime && entry.receivedAt > latest) {
225
+ latest = entry.receivedAt;
226
+ }
227
+ }
228
+ return latest;
229
+ }