@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,550 @@
1
+ // LSP Manager — server pool with lazy spawning and diagnostic collection.
2
+ // biome-ignore-all lint/nursery/noExcessiveLinesPerFile: LspManager stays cohesive; recovery and sync helpers are split into manager-*.ts modules.
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import * as projectRoots from "@mrclrchtr/supi-core";
6
+ import { LspClient } from "../client/client.ts";
7
+ import { getServerForFile } from "../config.ts";
8
+ import {
9
+ accumulateOutstandingDiagnostics,
10
+ collectDiagnosticSummaryCounts,
11
+ createOutstandingDiagnosticSummary,
12
+ relativeFilePathFromUri,
13
+ } from "../diagnostics/diagnostic-summary.ts";
14
+ import {
15
+ displayRelativeFilePath,
16
+ formatCoverageSummaryText,
17
+ formatOutstandingDiagnosticsSummaryText,
18
+ isPathRelevant,
19
+ normalizeRelevantPaths,
20
+ shouldIgnoreLspPath,
21
+ } from "../summary.ts";
22
+ import type {
23
+ DetectedProjectServer,
24
+ Diagnostic,
25
+ FileEvent,
26
+ LspConfig,
27
+ ProjectServerInfo,
28
+ } from "../types.ts";
29
+ import { commandExists } from "../utils.ts";
30
+ import {
31
+ closeFileAcrossClients,
32
+ pruneMissingFilesFromClients,
33
+ refreshOpenDiagnosticsForClients,
34
+ } from "./manager-client-state.ts";
35
+ import {
36
+ collectOutstandingDiagnosticsDetailed,
37
+ mapCascadeDiagnosticsToFiles,
38
+ syncClientFileAndGetCascadingDiagnostics,
39
+ } from "./manager-diagnostics.ts";
40
+ import {
41
+ clientKey,
42
+ isExcludedByPattern,
43
+ rememberKnownRoot,
44
+ resolveRootForFile,
45
+ } from "./manager-helpers.ts";
46
+ import { buildProjectServerInfo } from "./manager-project-info.ts";
47
+ import type {
48
+ ActiveCoverageSummaryEntry,
49
+ CoverageSummaryEntry,
50
+ DiagnosticSummary,
51
+ ManagerStatus,
52
+ OutstandingDiagnosticSummaryEntry,
53
+ ServerStatus,
54
+ } from "./manager-types.ts";
55
+ import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
56
+ // ── LspManager ────────────────────────────────────────────────────────
57
+ export class LspManager {
58
+ /** Active clients keyed by "serverName:root" */
59
+ private clients = new Map<string, LspClient>();
60
+ /** Servers we've already tried and failed to start */
61
+ private unavailable = new Set<string>();
62
+ /** Memoized per-command availability of LSP server binaries on PATH */
63
+ private commandAvailability = new Map<string, boolean>();
64
+ /** Guards against concurrent client creation for the same server:root key */
65
+ private pendingStarts = new Map<string, Promise<LspClient | null>>();
66
+ /** Preferred project roots discovered by proactive scan or lazy startup */
67
+ private knownRoots = new Map<string, string[]>();
68
+ /** User-configured gitignore-style exclude patterns */
69
+ private excludePatterns: string[] = [];
70
+ constructor(
71
+ private readonly config: LspConfig,
72
+ private readonly cwd: string,
73
+ ) {}
74
+ getCwd(): string {
75
+ return this.cwd;
76
+ }
77
+ setExcludePatterns(patterns: string[]): void {
78
+ this.excludePatterns = patterns;
79
+ }
80
+
81
+ /** Check whether any configured language server handles the given file's extension. */
82
+ hasServerForExtension(filePath: string): boolean {
83
+ return getServerForFile(this.config, filePath) !== null;
84
+ }
85
+ // ── Public API ────────────────────────────────────────────────────
86
+ registerDetectedServers(detected: DetectedProjectServer[]): void {
87
+ this.knownRoots = projectRoots.buildKnownRootsMap(detected);
88
+ }
89
+ /** Check whether a file path has an available LSP server. */
90
+ isSupportedSourceFile(filePath: string): boolean {
91
+ // Dependency directories are intentionally excluded from recent-path
92
+ // tracking and diagnostic summaries (shouldIgnoreLspPath). Keep runtime
93
+ // guidance activation consistent: reading or editing a file under
94
+ // node_modules / .pnpm must not arm LSP guidance for dependency sources.
95
+ if (shouldIgnoreLspPath(filePath, this.cwd)) return false;
96
+ const match = getServerForFile(this.config, filePath);
97
+ if (!match) return false;
98
+ const [serverName, serverConfig] = match;
99
+ // Mirror getClientForFile's root resolution so the unavailable check stays
100
+ // root-specific. A failed startup in one workspace must not suppress
101
+ // activation for unrelated roots served by the same language server.
102
+ const root = resolveRootForFile(filePath, serverName, serverConfig.rootMarkers, {
103
+ knownRoots: this.knownRoots,
104
+ cwd: this.cwd,
105
+ });
106
+ if (this.unavailable.has(`${serverName}:${root}`)) return false;
107
+ return this.isServerCommandAvailable(serverConfig.command);
108
+ }
109
+ private isServerCommandAvailable(command: string): boolean {
110
+ // Only memoize positive lookups. A negative result may become stale if the
111
+ // user installs the binary mid-session (e.g. `mise install`), and
112
+ // getClientForFile calls commandExists directly — caching false here would
113
+ // leave runtime guidance permanently dormant while client spawning can
114
+ // still succeed.
115
+ if (this.commandAvailability.get(command) === true) return true;
116
+ const available = commandExists(command);
117
+ if (available) this.commandAvailability.set(command, true);
118
+ return available;
119
+ }
120
+ /** Get or create an LSP client for the given file. */
121
+ async getClientForFile(filePath: string): Promise<LspClient | null> {
122
+ const match = getServerForFile(this.config, filePath);
123
+ if (!match) return null;
124
+ const [serverName, serverConfig] = match;
125
+ const root = resolveRootForFile(filePath, serverName, serverConfig.rootMarkers, {
126
+ knownRoots: this.knownRoots,
127
+ cwd: this.cwd,
128
+ });
129
+ return this.startServerForRoot(serverName, root);
130
+ }
131
+ async startServerForRoot(serverName: string, root: string): Promise<LspClient | null> {
132
+ const serverConfig = this.config.servers[serverName];
133
+ if (!serverConfig) return null;
134
+ const key = clientKey(serverName, root);
135
+ if (this.unavailable.has(key)) return null;
136
+
137
+ // Return existing client
138
+ const existing = this.clients.get(key);
139
+ if (existing && existing.status === "running") return existing;
140
+
141
+ // If existing client errored, remove it
142
+ if (existing && existing.status === "error") {
143
+ this.clients.delete(key);
144
+ this.unavailable.add(key);
145
+ return null;
146
+ }
147
+
148
+ // Deduplicate concurrent starts for the same server:root pair.
149
+ // This prevents spawning duplicate server processes when two
150
+ // callers race through getClientForFile before either await yields.
151
+ const pending = this.pendingStarts.get(key);
152
+ if (pending) return pending;
153
+
154
+ const startPromise = this.performStart(serverName, serverConfig, root, key);
155
+ this.pendingStarts.set(key, startPromise);
156
+ try {
157
+ return await startPromise;
158
+ } finally {
159
+ if (this.pendingStarts.get(key) === startPromise) {
160
+ this.pendingStarts.delete(key);
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Perform the actual server start — extracted so the public method can
167
+ * deduplicate via pendingStarts without wrapping the entire body.
168
+ */
169
+ private async performStart(
170
+ serverName: string,
171
+ serverConfig: import("../types.ts").ServerConfig,
172
+ root: string,
173
+ key: string,
174
+ ): Promise<LspClient | null> {
175
+ // Validate command exists
176
+ if (!commandExists(serverConfig.command)) {
177
+ this.unavailable.add(key);
178
+ return null;
179
+ }
180
+
181
+ // Spawn new client
182
+ const client = new LspClient(serverName, serverConfig, root);
183
+ this.clients.set(key, client);
184
+ rememberKnownRoot(this.knownRoots, serverName, root);
185
+ try {
186
+ await client.start();
187
+ return client;
188
+ } catch {
189
+ this.unavailable.add(key);
190
+ this.clients.delete(key);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /** Find an already-started client for a file without spawning a new server. */
196
+ private getExistingClientForFile(filePath: string): LspClient | null {
197
+ const match = getServerForFile(this.config, filePath);
198
+ if (!match) return null;
199
+ const [serverName, serverConfig] = match;
200
+ const root = resolveRootForFile(filePath, serverName, serverConfig.rootMarkers, {
201
+ knownRoots: this.knownRoots,
202
+ cwd: this.cwd,
203
+ });
204
+ return this.clients.get(clientKey(serverName, root)) ?? null;
205
+ }
206
+
207
+ /** Restart the clients that own the supplied file paths, if any are active. */
208
+ async restartClientsForFiles(filePaths: string[]): Promise<string[]> {
209
+ const restarted: string[] = [];
210
+ const seen = new Set<string>();
211
+
212
+ for (const filePath of filePaths) {
213
+ const resolvedPath = path.resolve(this.cwd, filePath);
214
+ const client = this.getExistingClientForFile(resolvedPath);
215
+ if (!client) continue;
216
+
217
+ const key = clientKey(client.name, client.root);
218
+ if (seen.has(key)) continue;
219
+ seen.add(key);
220
+
221
+ if (await this.restartClient(client)) {
222
+ restarted.push(key);
223
+ }
224
+ }
225
+
226
+ return restarted;
227
+ }
228
+
229
+ private async restartClient(client: LspClient): Promise<boolean> {
230
+ const key = clientKey(client.name, client.root);
231
+ const serverConfig = this.config.servers[client.name];
232
+ if (!serverConfig) return false;
233
+
234
+ const openFiles = client.openFiles;
235
+ try {
236
+ await client.shutdown();
237
+ } catch {
238
+ // Ignore shutdown failures when forcing a restart.
239
+ }
240
+
241
+ this.clients.delete(key);
242
+ this.unavailable.delete(key);
243
+
244
+ const replacement = new LspClient(client.name, serverConfig, client.root);
245
+ this.clients.set(key, replacement);
246
+ rememberKnownRoot(this.knownRoots, client.name, client.root);
247
+
248
+ try {
249
+ await replacement.start();
250
+ for (const filePath of openFiles) {
251
+ if (!fs.existsSync(filePath)) continue;
252
+ try {
253
+ replacement.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
254
+ } catch {
255
+ // Skip unreadable files on restart.
256
+ }
257
+ }
258
+ return true;
259
+ } catch {
260
+ this.clients.delete(key);
261
+ this.unavailable.add(key);
262
+ return false;
263
+ }
264
+ }
265
+
266
+ getProjectServerInfo(serverName: string, root: string, fileTypes: string[]): ProjectServerInfo {
267
+ const key = clientKey(serverName, root);
268
+ return buildProjectServerInfo(
269
+ {
270
+ serverName,
271
+ root,
272
+ fileTypes,
273
+ client: this.clients.get(key),
274
+ unavailable: this.unavailable.has(key),
275
+ },
276
+ this.cwd,
277
+ );
278
+ }
279
+ getKnownProjectServers(detected: DetectedProjectServer[]): ProjectServerInfo[] {
280
+ const known = new Map<string, DetectedProjectServer>();
281
+ for (const entry of detected) {
282
+ known.set(clientKey(entry.name, entry.root), entry);
283
+ }
284
+ for (const client of this.clients.values()) {
285
+ const key = clientKey(client.name, client.root);
286
+ if (known.has(key)) continue;
287
+ known.set(key, {
288
+ name: client.name,
289
+ root: client.root,
290
+ fileTypes: [...(this.config.servers[client.name]?.fileTypes ?? [])],
291
+ });
292
+ }
293
+ return Array.from(known.values())
294
+ .map((entry) => this.getProjectServerInfo(entry.name, entry.root, entry.fileTypes))
295
+ .sort(
296
+ (a, b) =>
297
+ a.root.localeCompare(b.root) ||
298
+ a.name.localeCompare(b.name) ||
299
+ a.status.localeCompare(b.status),
300
+ );
301
+ }
302
+ async syncFileAndGetDiagnostics(
303
+ filePath: string,
304
+ maxSeverity: number = 1,
305
+ ): Promise<Diagnostic[]> {
306
+ const resolvedPath = path.resolve(filePath);
307
+ return (
308
+ (await this.syncFileAndGetCascadingDiagnostics(resolvedPath, maxSeverity)).find(
309
+ (entry) => entry.file === resolvedPath,
310
+ )?.diagnostics ?? []
311
+ );
312
+ }
313
+ async syncFileAndGetCascadingDiagnostics(
314
+ filePath: string,
315
+ maxSeverity: number = 1,
316
+ ): Promise<Array<{ file: string; diagnostics: Diagnostic[] }>> {
317
+ const client = await this.getClientForFile(filePath);
318
+ if (!client) return [];
319
+ const resolvedPath = path.resolve(filePath);
320
+ try {
321
+ const { primary, cascade } = await syncClientFileAndGetCascadingDiagnostics(
322
+ client,
323
+ resolvedPath,
324
+ maxSeverity,
325
+ );
326
+ return [
327
+ ...(primary.length > 0 ? [{ file: resolvedPath, diagnostics: primary }] : []),
328
+ ...mapCascadeDiagnosticsToFiles(cascade),
329
+ ];
330
+ } catch {
331
+ this.closeFile(resolvedPath);
332
+ return [];
333
+ }
334
+ }
335
+ /** Close a file across any active LSP clients and clear its cached diagnostics. */
336
+ closeFile(filePath: string): void {
337
+ closeFileAcrossClients(this.clients.values(), filePath);
338
+ }
339
+ /** Remove any missing files from open-document and diagnostic state. */
340
+ pruneMissingFiles(): string[] {
341
+ return pruneMissingFilesFromClients(this.clients.values());
342
+ }
343
+ /** Re-sync all open documents across active clients and wait for diagnostics to settle. */
344
+ async refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void> {
345
+ await refreshOpenDiagnosticsForClients(this.clients.values(), options);
346
+ }
347
+
348
+ /** Clear cached pull-diagnostic result IDs across all clients. */
349
+ clearAllPullResultIds(): void {
350
+ for (const client of this.clients.values()) {
351
+ client.clearPullResultIds();
352
+ }
353
+ }
354
+
355
+ /** Notify running clients about watched workspace file changes. */
356
+ notifyWorkspaceFileChanges(changes: FileEvent[]): void {
357
+ for (const client of this.clients.values()) {
358
+ client.notifyWorkspaceFileChanges(changes);
359
+ }
360
+ }
361
+
362
+ /** Force a workspace-wide diagnostic recovery pass. */
363
+ async recoverWorkspaceDiagnostics(options?: {
364
+ changes?: FileEvent[];
365
+ restartIfStillStale?: boolean;
366
+ maxWaitMs?: number;
367
+ quietMs?: number;
368
+ }): Promise<{
369
+ refreshedClients: number;
370
+ restartedClients: number;
371
+ staleAssessment: {
372
+ suspected: boolean;
373
+ matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
374
+ warning: string | null;
375
+ };
376
+ }> {
377
+ return recoverWorkspaceDiagnosticsImpl(this, options);
378
+ }
379
+
380
+ /** Shut down all running LSP servers. */
381
+ async shutdownAll(): Promise<void> {
382
+ const shutdowns = Array.from(this.clients.values()).map((c) => c.shutdown().catch(() => {}));
383
+ await Promise.all(shutdowns);
384
+ this.clients.clear();
385
+ this.unavailable.clear();
386
+ this.knownRoots.clear();
387
+ }
388
+ /** Get status of all servers. */
389
+ getStatus(): ManagerStatus {
390
+ this.pruneMissingFiles();
391
+ const servers: ServerStatus[] = [];
392
+ for (const [_key, client] of this.clients) {
393
+ servers.push({
394
+ name: client.name,
395
+ status: client.status === "running" ? "running" : "error",
396
+ root: client.root,
397
+ openFiles: client.openFiles,
398
+ });
399
+ }
400
+ return { servers };
401
+ }
402
+ /** Get configured and active LSP coverage for the current project. */
403
+ getCoverageSummary(): CoverageSummaryEntry[] {
404
+ this.pruneMissingFiles();
405
+ const activeServers = new Map<string, { active: boolean; openFiles: number }>();
406
+ for (const server of this.getStatus().servers) {
407
+ const current = activeServers.get(server.name) ?? { active: false, openFiles: 0 };
408
+ current.active = current.active || server.status === "running";
409
+ current.openFiles += server.openFiles.length;
410
+ activeServers.set(server.name, current);
411
+ }
412
+ return Object.entries(this.config.servers)
413
+ .map(([name, server]) => {
414
+ const activity = activeServers.get(name);
415
+ return {
416
+ name,
417
+ fileTypes: server.fileTypes,
418
+ active: activity?.active ?? false,
419
+ openFiles: activity?.openFiles ?? 0,
420
+ } satisfies CoverageSummaryEntry;
421
+ })
422
+ .sort(
423
+ (a, b) =>
424
+ Number(b.active) - Number(a.active) ||
425
+ b.openFiles - a.openFiles ||
426
+ a.name.localeCompare(b.name),
427
+ );
428
+ }
429
+ /** Get active LSP coverage summarized by running servers with open files. */
430
+ getActiveCoverageSummary(): ActiveCoverageSummaryEntry[] {
431
+ this.pruneMissingFiles();
432
+ const activeServers = new Map<string, Set<string>>();
433
+ for (const server of this.getStatus().servers) {
434
+ if (server.status !== "running" || server.openFiles.length === 0) continue;
435
+ const openFiles = activeServers.get(server.name) ?? new Set<string>();
436
+ for (const file of server.openFiles) {
437
+ const relativeFile = displayRelativeFilePath(file, this.cwd);
438
+ if (shouldIgnoreLspPath(relativeFile, this.cwd)) continue;
439
+ if (isExcludedByPattern(relativeFile, this.excludePatterns)) continue;
440
+ openFiles.add(relativeFile);
441
+ }
442
+ activeServers.set(server.name, openFiles);
443
+ }
444
+ return Array.from(activeServers.entries())
445
+ .map(([name, openFiles]) => ({
446
+ name,
447
+ openFiles: Array.from(openFiles).sort(),
448
+ }))
449
+ .sort((a, b) => b.openFiles.length - a.openFiles.length || a.name.localeCompare(b.name));
450
+ }
451
+ getCoverageSummaryText(maxServers: number = 2, maxFiles: number = 2): string | null {
452
+ return formatCoverageSummaryText(this.getActiveCoverageSummary(), maxServers, maxFiles);
453
+ }
454
+ getRelevantCoverageSummaryText(
455
+ relevantPaths: string[],
456
+ maxServers: number = 2,
457
+ maxFiles: number = 2,
458
+ ): string | null {
459
+ const normalizedPaths = normalizeRelevantPaths(relevantPaths);
460
+ if (normalizedPaths.length === 0) return null;
461
+ const relevantEntries = this.getActiveCoverageSummary()
462
+ .map((entry) => ({
463
+ ...entry,
464
+ openFiles: entry.openFiles.filter((file) =>
465
+ isPathRelevant(file, normalizedPaths, this.cwd),
466
+ ),
467
+ }))
468
+ .filter((entry) => entry.openFiles.length > 0);
469
+ return formatCoverageSummaryText(relevantEntries, maxServers, maxFiles);
470
+ }
471
+ /** Get a diagnostic summary across all servers and files. */
472
+ getDiagnosticSummary(): DiagnosticSummary[] {
473
+ this.pruneMissingFiles();
474
+ const fileDiags = new Map<string, { errors: number; warnings: number }>();
475
+ for (const client of this.clients.values()) {
476
+ for (const entry of client.getAllDiagnostics()) {
477
+ collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
478
+ }
479
+ }
480
+ return Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts }));
481
+ }
482
+ /** Get outstanding diagnostics at or above the configured inline threshold. */
483
+ getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
484
+ this.pruneMissingFiles();
485
+ const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
486
+ for (const client of this.clients.values()) {
487
+ for (const entry of client.getAllDiagnostics()) {
488
+ const file = relativeFilePathFromUri(entry.uri, this.cwd);
489
+ if (shouldIgnoreLspPath(file, this.cwd)) continue;
490
+ if (isExcludedByPattern(file, this.excludePatterns)) continue;
491
+ const current = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
492
+ const next = accumulateOutstandingDiagnostics(current, entry.diagnostics, maxSeverity);
493
+ if (next.total > 0) {
494
+ fileDiags.set(file, next);
495
+ }
496
+ }
497
+ }
498
+ return Array.from(fileDiags.values()).sort(
499
+ (a, b) =>
500
+ b.errors - a.errors ||
501
+ b.warnings - a.warnings ||
502
+ b.information - a.information ||
503
+ b.hints - a.hints ||
504
+ a.file.localeCompare(b.file),
505
+ );
506
+ }
507
+ getRelevantOutstandingDiagnosticsSummaryText(
508
+ relevantPaths: string[],
509
+ maxSeverity: number = 1,
510
+ maxFiles: number = 3,
511
+ ): string | null {
512
+ const normalizedPaths = normalizeRelevantPaths(relevantPaths);
513
+ if (normalizedPaths.length === 0) return null;
514
+ return formatOutstandingDiagnosticsSummaryText(
515
+ this.getOutstandingDiagnosticSummary(maxSeverity).filter((entry) =>
516
+ isPathRelevant(entry.file, normalizedPaths, this.cwd),
517
+ ),
518
+ maxFiles,
519
+ );
520
+ }
521
+ getOutstandingDiagnostics(
522
+ maxSeverity: number = 1,
523
+ ): Array<{ file: string; diagnostics: Diagnostic[] }> {
524
+ this.pruneMissingFiles();
525
+ return collectOutstandingDiagnosticsDetailed(
526
+ this.clients.values(),
527
+ this.cwd,
528
+ this.excludePatterns,
529
+ maxSeverity,
530
+ );
531
+ }
532
+ async workspaceSymbol(query: string) {
533
+ return (await import("./manager-workspace-symbol.ts")).managerWorkspaceSymbol(
534
+ this.clients.values(),
535
+ query,
536
+ );
537
+ }
538
+ async ensureFileOpen(filePath: string): Promise<LspClient | null> {
539
+ const client = await this.getClientForFile(filePath);
540
+ const resolvedPath = path.resolve(filePath);
541
+ if (!client) return null;
542
+ try {
543
+ client.didOpen(resolvedPath, fs.readFileSync(resolvedPath, "utf-8"));
544
+ return client;
545
+ } catch {
546
+ this.closeFile(resolvedPath);
547
+ return null;
548
+ }
549
+ }
550
+ }