@mrclrchtr/supi-code-intelligence 1.3.1 → 1.4.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.
- package/README.md +70 -32
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/{supi-lsp/node_modules/@mrclrchtr/supi-core/src → supi-core/src/context}/context-provider-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/{supi-lsp/node_modules/@mrclrchtr/supi-core/src → supi-core/src/settings}/settings-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/README.md +58 -39
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/context}/context-provider-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings}/settings-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +27 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +61 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +244 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/{types.ts → config/types.ts} +4 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/coordinates.ts +11 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +5 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-context.ts +115 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +3 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/{workspace-sentinels.ts → diagnostics/workspace-sentinels.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +2 -23
- package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +18 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +72 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +4 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +10 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +158 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +202 -43
- package/node_modules/@mrclrchtr/supi-lsp/src/{lsp-state.ts → session/lsp-state.ts} +22 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/{scanner.ts → session/scanner.ts} +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/{service-registry.ts → session/service-registry.ts} +104 -12
- package/node_modules/@mrclrchtr/supi-lsp/src/{settings-registration.ts → session/settings-registration.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/tree-persist.ts +75 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/tool/guidance.ts +138 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tool/names.ts +19 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/{overrides.ts → tool/overrides.ts} +55 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/tool/register-tools.ts +224 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tool/service-actions.ts +258 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/{ui.ts → ui/ui.ts} +4 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +11 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +46 -39
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{runtime.ts → session/runtime.ts} +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{session.ts → session/session.ts} +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{callees.ts → tool/callees.ts} +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{exports.ts → tool/exports.ts} +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{formatting.ts → tool/formatting.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/guidance.ts +22 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{imports.ts → tool/imports.ts} +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{node-at.ts → tool/node-at.ts} +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/{outline.ts → tool/outline.ts} +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +6 -29
- package/package.json +4 -4
- package/src/actions/affected-action.ts +4 -4
- package/src/actions/brief-action.ts +12 -13
- package/src/actions/callees-action.ts +14 -10
- package/src/actions/callers-action.ts +4 -4
- package/src/actions/implementations-action.ts +4 -4
- package/src/code-intelligence.ts +1 -1
- package/src/pattern-structured.ts +20 -22
- package/src/providers/semantic-provider.ts +34 -0
- package/src/providers/structural-provider.ts +14 -0
- package/src/target-resolution.ts +26 -35
- package/src/tool/guidance.ts +21 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +0 -163
- package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +0 -98
- package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +0 -430
- package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +0 -48
- package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +0 -156
- package/src/guidance.ts +0 -42
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/src/{capabilities.ts → config/capabilities.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/src/{defaults.json → config/defaults.json} +0 -0
- /package/node_modules/@mrclrchtr/supi-lsp/src/{renderer.ts → ui/renderer.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-tree-sitter/src/{structure.ts → tool/structure.ts} +0 -0
|
@@ -4,7 +4,16 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as projectRoots from "@mrclrchtr/supi-core/api";
|
|
6
6
|
import { LspClient } from "../client/client.ts";
|
|
7
|
-
import { getServerForFile } from "../config.ts";
|
|
7
|
+
import { getServerForFile } from "../config/config.ts";
|
|
8
|
+
import type {
|
|
9
|
+
DetectedProjectServer,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
FileEvent,
|
|
12
|
+
LspConfig,
|
|
13
|
+
ProjectServerInfo,
|
|
14
|
+
SymbolInformation,
|
|
15
|
+
WorkspaceSymbol,
|
|
16
|
+
} from "../config/types.ts";
|
|
8
17
|
import {
|
|
9
18
|
accumulateOutstandingDiagnostics,
|
|
10
19
|
collectDiagnosticSummaryCounts,
|
|
@@ -19,14 +28,7 @@ import {
|
|
|
19
28
|
normalizeRelevantPaths,
|
|
20
29
|
shouldIgnoreLspPath,
|
|
21
30
|
} from "../summary.ts";
|
|
22
|
-
import
|
|
23
|
-
DetectedProjectServer,
|
|
24
|
-
Diagnostic,
|
|
25
|
-
FileEvent,
|
|
26
|
-
LspConfig,
|
|
27
|
-
ProjectServerInfo,
|
|
28
|
-
} from "../types.ts";
|
|
29
|
-
import { commandExists } from "../utils.ts";
|
|
31
|
+
import { commandExists, resolveSessionPath } from "../utils.ts";
|
|
30
32
|
import {
|
|
31
33
|
closeFileAcrossClients,
|
|
32
34
|
pruneMissingFilesFromClients,
|
|
@@ -53,12 +55,15 @@ import type {
|
|
|
53
55
|
ServerStatus,
|
|
54
56
|
} from "./manager-types.ts";
|
|
55
57
|
import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
|
|
58
|
+
|
|
59
|
+
type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
60
|
+
|
|
56
61
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
57
62
|
export class LspManager {
|
|
58
63
|
/** Active clients keyed by "serverName:root" */
|
|
59
64
|
private clients = new Map<string, LspClient>();
|
|
60
|
-
/**
|
|
61
|
-
private unavailable = new
|
|
65
|
+
/** Per-root startup failures keyed by "serverName:root" */
|
|
66
|
+
private unavailable = new Map<string, UnavailableReason>();
|
|
62
67
|
/** Memoized per-command availability of LSP server binaries on PATH */
|
|
63
68
|
private commandAvailability = new Map<string, boolean>();
|
|
64
69
|
/** Guards against concurrent client creation for the same server:root key */
|
|
@@ -67,6 +72,8 @@ export class LspManager {
|
|
|
67
72
|
private knownRoots = new Map<string, string[]>();
|
|
68
73
|
/** User-configured gitignore-style exclude patterns */
|
|
69
74
|
private excludePatterns: string[] = [];
|
|
75
|
+
/** Project roots already warmed for workspace-symbol queries. */
|
|
76
|
+
private warmedWorkspaceSymbolProjects = new Set<string>();
|
|
70
77
|
constructor(
|
|
71
78
|
private readonly config: LspConfig,
|
|
72
79
|
private readonly cwd: string,
|
|
@@ -80,32 +87,43 @@ export class LspManager {
|
|
|
80
87
|
|
|
81
88
|
/** Check whether any configured language server handles the given file's extension. */
|
|
82
89
|
hasServerForExtension(filePath: string): boolean {
|
|
83
|
-
return getServerForFile(this.config, filePath) !== null;
|
|
90
|
+
return getServerForFile(this.config, resolveSessionPath(this.cwd, filePath)) !== null;
|
|
84
91
|
}
|
|
85
92
|
// ── Public API ────────────────────────────────────────────────────
|
|
86
93
|
registerDetectedServers(detected: DetectedProjectServer[]): void {
|
|
87
94
|
this.knownRoots = projectRoots.buildKnownRootsMap(detected);
|
|
88
95
|
}
|
|
89
|
-
/** Check whether a file path has an available LSP server. */
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
96
|
+
/** Check whether a file path has an available LSP server for explicit semantic operations. */
|
|
97
|
+
canServeFile(filePath: string): boolean {
|
|
98
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
99
|
+
const match = getServerForFile(this.config, resolvedPath);
|
|
97
100
|
if (!match) return false;
|
|
98
101
|
const [serverName, serverConfig] = match;
|
|
99
102
|
// Mirror getClientForFile's root resolution so the unavailable check stays
|
|
100
103
|
// root-specific. A failed startup in one workspace must not suppress
|
|
101
104
|
// activation for unrelated roots served by the same language server.
|
|
102
|
-
const root = resolveRootForFile(
|
|
105
|
+
const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
|
|
103
106
|
knownRoots: this.knownRoots,
|
|
104
107
|
cwd: this.cwd,
|
|
105
108
|
});
|
|
106
|
-
|
|
109
|
+
const key = clientKey(serverName, root);
|
|
110
|
+
if (this.getUnavailableReason(key, serverConfig.command)) return false;
|
|
107
111
|
return this.isServerCommandAvailable(serverConfig.command);
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check whether a file should participate in runtime guidance and diagnostics.
|
|
116
|
+
* This is stricter than {@link canServeFile} and intentionally filters dependency
|
|
117
|
+
* and tsconfig-excluded paths from UI/context behavior.
|
|
118
|
+
*/
|
|
119
|
+
isSupportedSourceFile(filePath: string): boolean {
|
|
120
|
+
// Dependency directories are intentionally excluded from recent-path
|
|
121
|
+
// tracking and diagnostic summaries (shouldIgnoreLspPath). Keep runtime
|
|
122
|
+
// guidance activation consistent: reading or editing a file under
|
|
123
|
+
// node_modules / .pnpm must not arm LSP guidance for dependency sources.
|
|
124
|
+
if (shouldIgnoreLspPath(filePath, this.cwd)) return false;
|
|
125
|
+
return this.canServeFile(filePath);
|
|
126
|
+
}
|
|
109
127
|
private isServerCommandAvailable(command: string): boolean {
|
|
110
128
|
// Only memoize positive lookups. A negative result may become stale if the
|
|
111
129
|
// user installs the binary mid-session (e.g. `mise install`), and
|
|
@@ -117,12 +135,25 @@ export class LspManager {
|
|
|
117
135
|
if (available) this.commandAvailability.set(command, true);
|
|
118
136
|
return available;
|
|
119
137
|
}
|
|
138
|
+
|
|
139
|
+
private getUnavailableReason(key: string, command?: string): UnavailableReason | null {
|
|
140
|
+
const reason = this.unavailable.get(key);
|
|
141
|
+
if (!reason) return null;
|
|
142
|
+
|
|
143
|
+
if (reason === "missing-command" && command && this.isServerCommandAvailable(command)) {
|
|
144
|
+
this.unavailable.delete(key);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return reason;
|
|
149
|
+
}
|
|
120
150
|
/** Get or create an LSP client for the given file. */
|
|
121
151
|
async getClientForFile(filePath: string): Promise<LspClient | null> {
|
|
122
|
-
const
|
|
152
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
153
|
+
const match = getServerForFile(this.config, resolvedPath);
|
|
123
154
|
if (!match) return null;
|
|
124
155
|
const [serverName, serverConfig] = match;
|
|
125
|
-
const root = resolveRootForFile(
|
|
156
|
+
const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
|
|
126
157
|
knownRoots: this.knownRoots,
|
|
127
158
|
cwd: this.cwd,
|
|
128
159
|
});
|
|
@@ -132,7 +163,7 @@ export class LspManager {
|
|
|
132
163
|
const serverConfig = this.config.servers[serverName];
|
|
133
164
|
if (!serverConfig) return null;
|
|
134
165
|
const key = clientKey(serverName, root);
|
|
135
|
-
if (this.
|
|
166
|
+
if (this.getUnavailableReason(key, serverConfig.command)) return null;
|
|
136
167
|
|
|
137
168
|
// Return existing client
|
|
138
169
|
const existing = this.clients.get(key);
|
|
@@ -141,7 +172,8 @@ export class LspManager {
|
|
|
141
172
|
// If existing client errored, remove it
|
|
142
173
|
if (existing && existing.status === "error") {
|
|
143
174
|
this.clients.delete(key);
|
|
144
|
-
this.unavailable.
|
|
175
|
+
this.unavailable.set(key, "runtime-error");
|
|
176
|
+
this.clearWarmedWorkspaceSymbolProjects(existing.name, existing.root);
|
|
145
177
|
return null;
|
|
146
178
|
}
|
|
147
179
|
|
|
@@ -168,25 +200,27 @@ export class LspManager {
|
|
|
168
200
|
*/
|
|
169
201
|
private async performStart(
|
|
170
202
|
serverName: string,
|
|
171
|
-
serverConfig: import("../types.ts").ServerConfig,
|
|
203
|
+
serverConfig: import("../config/types.ts").ServerConfig,
|
|
172
204
|
root: string,
|
|
173
205
|
key: string,
|
|
174
206
|
): Promise<LspClient | null> {
|
|
175
207
|
// Validate command exists
|
|
176
208
|
if (!commandExists(serverConfig.command)) {
|
|
177
|
-
this.unavailable.
|
|
209
|
+
this.unavailable.set(key, "missing-command");
|
|
178
210
|
return null;
|
|
179
211
|
}
|
|
180
212
|
|
|
181
213
|
// Spawn new client
|
|
182
214
|
const client = new LspClient(serverName, serverConfig, root);
|
|
215
|
+
this.clearWarmedWorkspaceSymbolProjects(serverName, root);
|
|
183
216
|
this.clients.set(key, client);
|
|
184
217
|
rememberKnownRoot(this.knownRoots, serverName, root);
|
|
185
218
|
try {
|
|
186
219
|
await client.start();
|
|
220
|
+
this.unavailable.delete(key);
|
|
187
221
|
return client;
|
|
188
222
|
} catch {
|
|
189
|
-
this.unavailable.
|
|
223
|
+
this.unavailable.set(key, "start-failed");
|
|
190
224
|
this.clients.delete(key);
|
|
191
225
|
return null;
|
|
192
226
|
}
|
|
@@ -194,10 +228,11 @@ export class LspManager {
|
|
|
194
228
|
|
|
195
229
|
/** Find an already-started client for a file without spawning a new server. */
|
|
196
230
|
private getExistingClientForFile(filePath: string): LspClient | null {
|
|
197
|
-
const
|
|
231
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
232
|
+
const match = getServerForFile(this.config, resolvedPath);
|
|
198
233
|
if (!match) return null;
|
|
199
234
|
const [serverName, serverConfig] = match;
|
|
200
|
-
const root = resolveRootForFile(
|
|
235
|
+
const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
|
|
201
236
|
knownRoots: this.knownRoots,
|
|
202
237
|
cwd: this.cwd,
|
|
203
238
|
});
|
|
@@ -210,7 +245,7 @@ export class LspManager {
|
|
|
210
245
|
const seen = new Set<string>();
|
|
211
246
|
|
|
212
247
|
for (const filePath of filePaths) {
|
|
213
|
-
const resolvedPath =
|
|
248
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
214
249
|
const client = this.getExistingClientForFile(resolvedPath);
|
|
215
250
|
if (!client) continue;
|
|
216
251
|
|
|
@@ -240,6 +275,7 @@ export class LspManager {
|
|
|
240
275
|
|
|
241
276
|
this.clients.delete(key);
|
|
242
277
|
this.unavailable.delete(key);
|
|
278
|
+
this.clearWarmedWorkspaceSymbolProjects(client.name, client.root);
|
|
243
279
|
|
|
244
280
|
const replacement = new LspClient(client.name, serverConfig, client.root);
|
|
245
281
|
this.clients.set(key, replacement);
|
|
@@ -258,7 +294,7 @@ export class LspManager {
|
|
|
258
294
|
return true;
|
|
259
295
|
} catch {
|
|
260
296
|
this.clients.delete(key);
|
|
261
|
-
this.unavailable.
|
|
297
|
+
this.unavailable.set(key, "start-failed");
|
|
262
298
|
return false;
|
|
263
299
|
}
|
|
264
300
|
}
|
|
@@ -271,7 +307,8 @@ export class LspManager {
|
|
|
271
307
|
root,
|
|
272
308
|
fileTypes,
|
|
273
309
|
client: this.clients.get(key),
|
|
274
|
-
|
|
310
|
+
unavailableReason:
|
|
311
|
+
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
275
312
|
},
|
|
276
313
|
this.cwd,
|
|
277
314
|
);
|
|
@@ -303,7 +340,7 @@ export class LspManager {
|
|
|
303
340
|
filePath: string,
|
|
304
341
|
maxSeverity: number = 1,
|
|
305
342
|
): Promise<Diagnostic[]> {
|
|
306
|
-
const resolvedPath =
|
|
343
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
307
344
|
return (
|
|
308
345
|
(await this.syncFileAndGetCascadingDiagnostics(resolvedPath, maxSeverity)).find(
|
|
309
346
|
(entry) => entry.file === resolvedPath,
|
|
@@ -314,9 +351,9 @@ export class LspManager {
|
|
|
314
351
|
filePath: string,
|
|
315
352
|
maxSeverity: number = 1,
|
|
316
353
|
): Promise<Array<{ file: string; diagnostics: Diagnostic[] }>> {
|
|
317
|
-
const
|
|
354
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
355
|
+
const client = await this.getClientForFile(resolvedPath);
|
|
318
356
|
if (!client) return [];
|
|
319
|
-
const resolvedPath = path.resolve(filePath);
|
|
320
357
|
try {
|
|
321
358
|
const { primary, cascade } = await syncClientFileAndGetCascadingDiagnostics(
|
|
322
359
|
client,
|
|
@@ -334,7 +371,7 @@ export class LspManager {
|
|
|
334
371
|
}
|
|
335
372
|
/** Close a file across any active LSP clients and clear its cached diagnostics. */
|
|
336
373
|
closeFile(filePath: string): void {
|
|
337
|
-
closeFileAcrossClients(this.clients.values(), filePath);
|
|
374
|
+
closeFileAcrossClients(this.clients.values(), resolveSessionPath(this.cwd, filePath));
|
|
338
375
|
}
|
|
339
376
|
/** Remove any missing files from open-document and diagnostic state. */
|
|
340
377
|
pruneMissingFiles(): string[] {
|
|
@@ -384,6 +421,7 @@ export class LspManager {
|
|
|
384
421
|
this.clients.clear();
|
|
385
422
|
this.unavailable.clear();
|
|
386
423
|
this.knownRoots.clear();
|
|
424
|
+
this.warmedWorkspaceSymbolProjects.clear();
|
|
387
425
|
}
|
|
388
426
|
/** Get status of all servers. */
|
|
389
427
|
getStatus(): ManagerStatus {
|
|
@@ -529,15 +567,30 @@ export class LspManager {
|
|
|
529
567
|
maxSeverity,
|
|
530
568
|
);
|
|
531
569
|
}
|
|
532
|
-
async workspaceSymbol(query: string) {
|
|
533
|
-
|
|
534
|
-
|
|
570
|
+
async workspaceSymbol(query: string): Promise<(SymbolInformation | WorkspaceSymbol)[] | null> {
|
|
571
|
+
const helper = await import("./manager-workspace-symbol.ts");
|
|
572
|
+
const initial = await helper.collectWorkspaceSymbols(this.clients.values(), query);
|
|
573
|
+
if (!initial.hasSupport) return null;
|
|
574
|
+
if (initial.results.length > 0) return initial.results;
|
|
575
|
+
|
|
576
|
+
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult(
|
|
577
|
+
helper.findWorkspaceSymbolWarmTargets,
|
|
578
|
+
helper.getWorkspaceSymbolWarmPosition,
|
|
579
|
+
helper.collectWorkspaceSymbols,
|
|
535
580
|
query,
|
|
536
581
|
);
|
|
582
|
+
if (warmed.results) return warmed.results;
|
|
583
|
+
if (!warmed.warmedAny) return initial.results;
|
|
584
|
+
|
|
585
|
+
return this.retryWorkspaceSymbolAfterWarmup(
|
|
586
|
+
helper.collectWorkspaceSymbols,
|
|
587
|
+
query,
|
|
588
|
+
initial.results,
|
|
589
|
+
);
|
|
537
590
|
}
|
|
538
591
|
async ensureFileOpen(filePath: string): Promise<LspClient | null> {
|
|
539
|
-
const
|
|
540
|
-
const
|
|
592
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
593
|
+
const client = await this.getClientForFile(resolvedPath);
|
|
541
594
|
if (!client) return null;
|
|
542
595
|
try {
|
|
543
596
|
client.didOpen(resolvedPath, fs.readFileSync(resolvedPath, "utf-8"));
|
|
@@ -547,4 +600,110 @@ export class LspManager {
|
|
|
547
600
|
return null;
|
|
548
601
|
}
|
|
549
602
|
}
|
|
603
|
+
|
|
604
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: warm-up coordinates client/project iteration, targeted semantic nudges, and early-return queries in one place.
|
|
605
|
+
private async warmWorkspaceSymbolProjectsUntilResult(
|
|
606
|
+
findWarmTargets: (
|
|
607
|
+
root: string,
|
|
608
|
+
rootMarkers: string[],
|
|
609
|
+
fileTypes: string[],
|
|
610
|
+
) => Array<{ projectRoot: string; file: string }>,
|
|
611
|
+
getWarmPosition: (
|
|
612
|
+
symbols: Awaited<ReturnType<LspClient["documentSymbols"]>>,
|
|
613
|
+
) => import("../config/types.ts").Position | null,
|
|
614
|
+
collect: (
|
|
615
|
+
clients: Iterable<LspClient>,
|
|
616
|
+
query: string,
|
|
617
|
+
) => Promise<{ results: (SymbolInformation | WorkspaceSymbol)[]; hasSupport: boolean }>,
|
|
618
|
+
query: string,
|
|
619
|
+
): Promise<{ warmedAny: boolean; results: (SymbolInformation | WorkspaceSymbol)[] | null }> {
|
|
620
|
+
let warmedAny = false;
|
|
621
|
+
|
|
622
|
+
for (const client of Array.from(this.clients.values())) {
|
|
623
|
+
if (client.status !== "running") continue;
|
|
624
|
+
if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
|
|
625
|
+
|
|
626
|
+
const serverConfig = this.config.servers[client.name];
|
|
627
|
+
if (!serverConfig) continue;
|
|
628
|
+
|
|
629
|
+
const warmTargets = findWarmTargets(
|
|
630
|
+
client.root,
|
|
631
|
+
serverConfig.rootMarkers,
|
|
632
|
+
serverConfig.fileTypes,
|
|
633
|
+
).slice(0, 24);
|
|
634
|
+
|
|
635
|
+
for (const target of warmTargets) {
|
|
636
|
+
const projectKey = this.workspaceSymbolProjectKey(client.name, target.projectRoot);
|
|
637
|
+
if (this.warmedWorkspaceSymbolProjects.has(projectKey)) continue;
|
|
638
|
+
if (this.hasOpenFileInProject(client, target.projectRoot)) {
|
|
639
|
+
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const openedClient = await this.ensureFileOpen(target.file);
|
|
644
|
+
if (!openedClient) continue;
|
|
645
|
+
|
|
646
|
+
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
647
|
+
warmedAny = true;
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const symbols = await openedClient.documentSymbols(target.file);
|
|
651
|
+
const hoverPosition = getWarmPosition(symbols);
|
|
652
|
+
if (hoverPosition) {
|
|
653
|
+
await openedClient.hover(target.file, hoverPosition);
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
// Best-effort warm-up only.
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const collected = await collect(this.clients.values(), query);
|
|
660
|
+
if (collected.hasSupport && collected.results.length > 0) {
|
|
661
|
+
return { warmedAny, results: collected.results };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return { warmedAny, results: null };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private async retryWorkspaceSymbolAfterWarmup(
|
|
670
|
+
collect: (
|
|
671
|
+
clients: Iterable<LspClient>,
|
|
672
|
+
query: string,
|
|
673
|
+
) => Promise<{ results: (SymbolInformation | WorkspaceSymbol)[]; hasSupport: boolean }>,
|
|
674
|
+
query: string,
|
|
675
|
+
fallbackResults: (SymbolInformation | WorkspaceSymbol)[],
|
|
676
|
+
): Promise<(SymbolInformation | WorkspaceSymbol)[]> {
|
|
677
|
+
const attempts = 5;
|
|
678
|
+
const delayMs = 50;
|
|
679
|
+
|
|
680
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
681
|
+
if (attempt > 0) {
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
683
|
+
}
|
|
684
|
+
const retried = await collect(this.clients.values(), query);
|
|
685
|
+
if (!retried.hasSupport || retried.results.length > 0) {
|
|
686
|
+
return retried.hasSupport ? retried.results : fallbackResults;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return fallbackResults;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
|
|
694
|
+
return client.openFiles.some((openFile) => projectRoots.isWithinOrEqual(projectRoot, openFile));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private workspaceSymbolProjectKey(serverName: string, projectRoot: string): string {
|
|
698
|
+
return `${serverName}:${path.resolve(projectRoot)}`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private clearWarmedWorkspaceSymbolProjects(serverName: string, root: string): void {
|
|
702
|
+
const prefix = `${serverName}:${path.resolve(root)}`;
|
|
703
|
+
for (const key of Array.from(this.warmedWorkspaceSymbolProjects)) {
|
|
704
|
+
if (key === prefix || key.startsWith(`${prefix}${path.sep}`)) {
|
|
705
|
+
this.warmedWorkspaceSymbolProjects.delete(key);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
550
709
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Extracted from lsp.ts to keep file sizes within Biome limits.
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type {
|
|
5
|
+
import type { DetectedProjectServer, ProjectServerInfo } from "../config/types.ts";
|
|
6
|
+
import type { LspManager } from "../manager/manager.ts";
|
|
7
|
+
import { LSP_TOOL_NAMES } from "../tool/names.ts";
|
|
8
|
+
import type { LspInspectorState } from "../ui/ui.ts";
|
|
6
9
|
import { introspectCapabilities } from "./scanner.ts";
|
|
7
10
|
import { clearSessionLspService } from "./service-registry.ts";
|
|
8
|
-
import type { DetectedProjectServer, ProjectServerInfo } from "./types.ts";
|
|
9
|
-
import type { LspInspectorState } from "./ui.ts";
|
|
10
11
|
|
|
11
12
|
export interface LspRuntimeState {
|
|
12
13
|
manager: LspManager | null;
|
|
@@ -49,7 +50,12 @@ export function refreshProjectServers(state: LspRuntimeState): void {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function isLspAwareTool(toolName: string): boolean {
|
|
52
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
LSP_TOOL_NAMES.includes(toolName as (typeof LSP_TOOL_NAMES)[number]) ||
|
|
55
|
+
toolName === "read" ||
|
|
56
|
+
toolName === "write" ||
|
|
57
|
+
toolName === "edit"
|
|
58
|
+
);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export function disableLspState(pi: ExtensionAPI, state: LspRuntimeState): void {
|
|
@@ -66,17 +72,22 @@ export function disableLspState(pi: ExtensionAPI, state: LspRuntimeState): void
|
|
|
66
72
|
state.lastWorkspaceChangeAt = 0;
|
|
67
73
|
state.sentinelSnapshot = new Map();
|
|
68
74
|
state.lspActive = false;
|
|
69
|
-
|
|
75
|
+
removeLspTools(pi);
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
export function
|
|
78
|
+
export function removeLspTools(pi: ExtensionAPI): void {
|
|
73
79
|
const activeTools = pi.getActiveTools();
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
const nextTools = activeTools.filter(
|
|
81
|
+
(toolName: string) => !LSP_TOOL_NAMES.includes(toolName as (typeof LSP_TOOL_NAMES)[number]),
|
|
82
|
+
);
|
|
83
|
+
if (nextTools.length !== activeTools.length) {
|
|
84
|
+
pi.setActiveTools(nextTools);
|
|
85
|
+
}
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
export function
|
|
88
|
+
export function ensureLspToolsActive(pi: ExtensionAPI): void {
|
|
79
89
|
const activeTools = pi.getActiveTools();
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
const missing = LSP_TOOL_NAMES.filter((toolName) => !activeTools.includes(toolName));
|
|
91
|
+
if (missing.length === 0) return;
|
|
92
|
+
pi.setActiveTools([...activeTools, ...missing]);
|
|
82
93
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { dedupeTopmostRoots, walkProject } from "@mrclrchtr/supi-core/api";
|
|
4
|
-
import type { LspManager } from "./manager/manager.ts";
|
|
5
4
|
import type {
|
|
6
5
|
DetectedProjectServer,
|
|
7
6
|
LspConfig,
|
|
8
7
|
MissingServer,
|
|
9
8
|
ProjectServerInfo,
|
|
10
9
|
ServerConfig,
|
|
11
|
-
} from "
|
|
12
|
-
import {
|
|
10
|
+
} from "../config/types.ts";
|
|
11
|
+
import type { LspManager } from "../manager/manager.ts";
|
|
12
|
+
import { commandExists } from "../utils.ts";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MAX_DEPTH = 3;
|
|
15
15
|
|
package/node_modules/@mrclrchtr/supi-lsp/src/{service-registry.ts → session/service-registry.ts}
RENAMED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// to reuse the active LSP runtime without starting duplicate servers.
|
|
4
4
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import type { LspManager } from "./manager/manager.ts";
|
|
7
6
|
import type {
|
|
7
|
+
CodeAction,
|
|
8
8
|
Diagnostic,
|
|
9
9
|
DocumentSymbol,
|
|
10
10
|
Hover,
|
|
@@ -13,11 +13,43 @@ import type {
|
|
|
13
13
|
Position,
|
|
14
14
|
ProjectServerInfo,
|
|
15
15
|
SymbolInformation,
|
|
16
|
+
WorkspaceEdit,
|
|
16
17
|
WorkspaceSymbol,
|
|
17
|
-
} from "
|
|
18
|
+
} from "../config/types.ts";
|
|
19
|
+
import type { LspManager } from "../manager/manager.ts";
|
|
20
|
+
import { resolveSessionPath } from "../utils.ts";
|
|
21
|
+
|
|
22
|
+
/** Workspace diagnostic summary grouped by file. */
|
|
23
|
+
export interface WorkspaceDiagnosticSummaryEntry {
|
|
24
|
+
file: string;
|
|
25
|
+
errors: number;
|
|
26
|
+
warnings: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Outstanding diagnostics grouped by file, including info and hint counts. */
|
|
30
|
+
export interface OutstandingDiagnosticSummaryEntry {
|
|
31
|
+
file: string;
|
|
32
|
+
total: number;
|
|
33
|
+
errors: number;
|
|
34
|
+
warnings: number;
|
|
35
|
+
information: number;
|
|
36
|
+
hints: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Result from a workspace diagnostic recovery pass. */
|
|
40
|
+
export interface RecoverDiagnosticsResult {
|
|
41
|
+
refreshedClients: number;
|
|
42
|
+
restartedClients: number;
|
|
43
|
+
staleAssessment: {
|
|
44
|
+
suspected: boolean;
|
|
45
|
+
matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
|
|
46
|
+
warning: string | null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
18
49
|
|
|
19
50
|
export type SessionLspServiceState =
|
|
20
51
|
| { kind: "ready"; service: SessionLspService }
|
|
52
|
+
| { kind: "inactive"; service: SessionLspService }
|
|
21
53
|
| { kind: "pending" }
|
|
22
54
|
| { kind: "disabled" }
|
|
23
55
|
| { kind: "unavailable"; reason: string };
|
|
@@ -25,7 +57,9 @@ export type SessionLspServiceState =
|
|
|
25
57
|
/**
|
|
26
58
|
* Public wrapper around {@link LspManager} that exposes stable semantic operations.
|
|
27
59
|
* File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
|
|
28
|
-
* to match pi's built-in path-tool convention.
|
|
60
|
+
* to match pi's built-in path-tool convention. Position arguments use raw 0-based LSP
|
|
61
|
+
* coordinates; use `toLspPosition()` from `@mrclrchtr/supi-lsp/api` when starting from
|
|
62
|
+
* user-facing 1-based line and character values.
|
|
29
63
|
*/
|
|
30
64
|
export class SessionLspService {
|
|
31
65
|
constructor(private readonly manager: LspManager) {}
|
|
@@ -77,44 +111,86 @@ export class SessionLspService {
|
|
|
77
111
|
return this.manager.workspaceSymbol(query);
|
|
78
112
|
}
|
|
79
113
|
|
|
114
|
+
async rename(
|
|
115
|
+
filePath: string,
|
|
116
|
+
position: Position,
|
|
117
|
+
newName: string,
|
|
118
|
+
): Promise<WorkspaceEdit | null> {
|
|
119
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
120
|
+
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
121
|
+
if (!client) return null;
|
|
122
|
+
return client.rename(resolvedPath, position, newName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async codeActions(filePath: string, position: Position): Promise<CodeAction[] | null> {
|
|
126
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
127
|
+
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
128
|
+
if (!client) return null;
|
|
129
|
+
|
|
130
|
+
const range = { start: position, end: position };
|
|
131
|
+
const diagnostics = client
|
|
132
|
+
.getDiagnostics(resolvedPath)
|
|
133
|
+
.filter((diagnostic) => diagnostic.range.start.line <= position.line)
|
|
134
|
+
.filter((diagnostic) => diagnostic.range.end.line >= position.line);
|
|
135
|
+
|
|
136
|
+
return client.codeActions(resolvedPath, range, { diagnostics });
|
|
137
|
+
}
|
|
138
|
+
|
|
80
139
|
// ── Project / runtime awareness ─────────────────────────────────────
|
|
81
140
|
|
|
82
141
|
getProjectServers(): ProjectServerInfo[] {
|
|
83
142
|
return this.manager.getKnownProjectServers([]);
|
|
84
143
|
}
|
|
85
144
|
|
|
145
|
+
/** Check whether the file can be served semantically for explicit LSP operations. */
|
|
86
146
|
isSupportedSourceFile(filePath: string): boolean {
|
|
87
|
-
return this.manager.
|
|
147
|
+
return this.manager.canServeFile(this.resolveFilePath(filePath));
|
|
88
148
|
}
|
|
89
149
|
|
|
90
150
|
// ── Diagnostics ─────────────────────────────────────────────────────
|
|
91
151
|
|
|
152
|
+
/** Sync a file through LSP and return diagnostics up to the supplied severity threshold. */
|
|
153
|
+
async fileDiagnostics(filePath: string, maxSeverity: number = 4): Promise<Diagnostic[] | null> {
|
|
154
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
155
|
+
if (!this.manager.canServeFile(resolvedPath)) return null;
|
|
156
|
+
return this.manager.syncFileAndGetDiagnostics(resolvedPath, maxSeverity);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get a lightweight workspace diagnostic summary for all tracked files. */
|
|
160
|
+
getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[] {
|
|
161
|
+
return this.manager.getDiagnosticSummary();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Get outstanding diagnostics grouped by file at or above the supplied severity threshold. */
|
|
92
165
|
getOutstandingDiagnostics(
|
|
93
166
|
maxSeverity: number = 1,
|
|
94
167
|
): Array<{ file: string; diagnostics: Diagnostic[] }> {
|
|
95
168
|
return this.manager.getOutstandingDiagnostics(maxSeverity);
|
|
96
169
|
}
|
|
97
170
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
): import("./manager/manager-types.ts").OutstandingDiagnosticSummaryEntry[] {
|
|
171
|
+
/** Get outstanding diagnostic counts grouped by file. */
|
|
172
|
+
getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
|
|
101
173
|
return this.manager.getOutstandingDiagnosticSummary(maxSeverity);
|
|
102
174
|
}
|
|
103
175
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
/** Trigger a workspace-wide diagnostics refresh and stale-state recovery pass. */
|
|
177
|
+
async recoverDiagnostics(options?: {
|
|
178
|
+
restartIfStillStale?: boolean;
|
|
179
|
+
maxWaitMs?: number;
|
|
180
|
+
quietMs?: number;
|
|
181
|
+
}): Promise<RecoverDiagnosticsResult> {
|
|
182
|
+
return this.manager.recoverWorkspaceDiagnostics(options);
|
|
107
183
|
}
|
|
108
184
|
|
|
109
185
|
private resolveFilePath(filePath: string): string {
|
|
110
|
-
|
|
111
|
-
return path.resolve(this.manager.getCwd(), normalizedPath);
|
|
186
|
+
return resolveSessionPath(this.manager.getCwd(), filePath);
|
|
112
187
|
}
|
|
113
188
|
}
|
|
114
189
|
|
|
115
190
|
// ── Registry ──────────────────────────────────────────────────────────
|
|
116
191
|
|
|
117
192
|
const REGISTRY_KEY = Symbol.for("@mrclrchtr/supi-lsp/session-registry");
|
|
193
|
+
const WAIT_INTERVAL_MS = 25;
|
|
118
194
|
|
|
119
195
|
function getRegistry(): Map<string, SessionLspServiceState> {
|
|
120
196
|
const globalScope = globalThis as typeof globalThis & Record<symbol, unknown>;
|
|
@@ -147,6 +223,22 @@ export function getSessionLspService(cwd: string): SessionLspServiceState {
|
|
|
147
223
|
);
|
|
148
224
|
}
|
|
149
225
|
|
|
226
|
+
/** Wait briefly for a pending session-scoped LSP service to become ready. */
|
|
227
|
+
export async function waitForSessionLspService(
|
|
228
|
+
cwd: string,
|
|
229
|
+
timeoutMs: number = 250,
|
|
230
|
+
): Promise<SessionLspServiceState> {
|
|
231
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
232
|
+
let state = getSessionLspService(cwd);
|
|
233
|
+
|
|
234
|
+
while (state.kind === "pending" && Date.now() < deadline) {
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
|
|
236
|
+
state = getSessionLspService(cwd);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return state;
|
|
240
|
+
}
|
|
241
|
+
|
|
150
242
|
/** Remove the LSP service state for a session cwd. */
|
|
151
243
|
export function clearSessionLspService(cwd: string): void {
|
|
152
244
|
registry.delete(normalizeCwd(cwd));
|