@mrclrchtr/supi-code-intelligence 6.0.0 → 6.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.
- package/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-jsonrpc/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.d.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.d.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.d.ts +9 -9
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.d.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.js +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.d.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.js +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +582 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +2 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +36 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
- package/package.json +5 -5
- package/src/analysis/health/recovery.ts +4 -0
- package/src/analysis/health/server-status.ts +64 -0
- package/src/analysis/health/signals.ts +1 -0
- package/src/analysis/provider.ts +2 -1
- package/src/analysis/target/symbol.ts +28 -16
- package/src/analysis/target/types.ts +1 -0
- package/src/session/find-workflow.ts +5 -12
- package/src/session/health-refresh.ts +6 -2
- package/src/session/health-types.ts +8 -2
- package/src/session/health-workflow.ts +5 -1
- package/src/session/semantic-demand.ts +36 -0
- package/src/session/target-workflow.ts +9 -12
- package/src/tool/code_health/guidance.ts +2 -2
- package/src/tool/code_health/markdown.ts +15 -3
- package/src/tool/code_health/result.ts +14 -5
- package/src/tool/code_health/spec.ts +2 -1
- package/src/tool/code_health/tui.ts +24 -3
- package/src/tool/result/errors.ts +1 -0
- package/src/tool/result/types.ts +6 -0
- package/src/ui/footer.ts +7 -5
- package/src/ui/status-command.ts +31 -0
- package/src/ui/status-overlay.ts +69 -23
|
@@ -21,6 +21,7 @@ import type { ClientDiagnosticSnapshot } from "../client/client-document-state.t
|
|
|
21
21
|
|
|
22
22
|
export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
|
|
23
23
|
|
|
24
|
+
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
24
25
|
import { getServerForFile } from "../config/config.ts";
|
|
25
26
|
import { getFileScopeDecision } from "../config/tsconfig-scope.ts";
|
|
26
27
|
import type {
|
|
@@ -29,10 +30,12 @@ import type {
|
|
|
29
30
|
FileEvent,
|
|
30
31
|
LspConfig,
|
|
31
32
|
ProjectServerInfo,
|
|
33
|
+
ProjectServerStatusReason,
|
|
32
34
|
ServerConfig,
|
|
33
35
|
SymbolInformation,
|
|
34
36
|
WorkspaceSymbol,
|
|
35
37
|
} from "../config/types.ts";
|
|
38
|
+
import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
|
|
36
39
|
import {
|
|
37
40
|
accumulateOutstandingDiagnostics,
|
|
38
41
|
collectDiagnosticSummaryCounts,
|
|
@@ -151,6 +154,45 @@ export interface ManagerLifecycleTransition {
|
|
|
151
154
|
|
|
152
155
|
type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
|
|
153
156
|
|
|
157
|
+
type FileClientRequestOptions = {
|
|
158
|
+
/** Allow this request to start the route's one process-crash recovery. */
|
|
159
|
+
recoverProcessCrash?: boolean;
|
|
160
|
+
/** Control only this caller's wait for a shared replacement. */
|
|
161
|
+
control?: CodeRequestControl;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
interface ProcessCrashRecoveryState {
|
|
165
|
+
/** The one automatic attempt is consumed when a replacement starts. */
|
|
166
|
+
attemptConsumed: boolean;
|
|
167
|
+
/** Current user-facing reason, or undefined after successful recovery. */
|
|
168
|
+
statusReason: ProjectServerStatusReason | undefined;
|
|
169
|
+
/** The failed generation whose tracked files must be restored. */
|
|
170
|
+
failedClient: LspClient;
|
|
171
|
+
/** Files tracked by the failed route at the time of the crash. */
|
|
172
|
+
files: string[];
|
|
173
|
+
/** Shared replacement startup and document-restoration operation. */
|
|
174
|
+
pending: Promise<LspClient | null> | null;
|
|
175
|
+
/** Start time for the current replacement attempt, when one exists. */
|
|
176
|
+
attemptStartedAt?: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhausted" | "cancelled";
|
|
180
|
+
|
|
181
|
+
type ProcessCrashDemandResult = {
|
|
182
|
+
/** Whether at least one required crashed route supports the operation. */
|
|
183
|
+
hasSupport: boolean;
|
|
184
|
+
/** Required routes that remain unavailable after shared recovery settles. */
|
|
185
|
+
failures: string[];
|
|
186
|
+
/** In-scope tracked files from required routes that remain unavailable. */
|
|
187
|
+
failedFiles: string[];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
type RequiredProcessCrashDemand = {
|
|
191
|
+
key: string;
|
|
192
|
+
recovery: ProcessCrashRecoveryState;
|
|
193
|
+
failedFiles: readonly string[];
|
|
194
|
+
};
|
|
195
|
+
|
|
154
196
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
155
197
|
export class LspManager {
|
|
156
198
|
/** Active clients keyed by "serverName:root" */
|
|
@@ -177,6 +219,14 @@ export class LspManager {
|
|
|
177
219
|
private warmedSemanticProjects = new Set<string>();
|
|
178
220
|
/** In-flight warm-up probes keyed by project key so concurrent callers share one probe. */
|
|
179
221
|
private pendingWarmProbes = new Map<string, Promise<void>>();
|
|
222
|
+
/** One process-crash recovery budget and shared replacement per route. */
|
|
223
|
+
private processCrashRecoveries = new Map<string, ProcessCrashRecoveryState>();
|
|
224
|
+
/** Client generations that completed the initialize handshake. */
|
|
225
|
+
private initializedClientGenerations = new Map<string, number>();
|
|
226
|
+
/** Client generations whose process-failure fact was already handled. */
|
|
227
|
+
private handledCrashGenerations = new Map<string, number>();
|
|
228
|
+
/** Prevent late startup and lifecycle callbacks from republishing after shutdown. */
|
|
229
|
+
private shuttingDown = false;
|
|
180
230
|
constructor(
|
|
181
231
|
private readonly config: LspConfig,
|
|
182
232
|
private readonly cwd: string,
|
|
@@ -247,11 +297,27 @@ export class LspManager {
|
|
|
247
297
|
}
|
|
248
298
|
|
|
249
299
|
/** Get or create an LSP client for the given file. */
|
|
250
|
-
async getClientForFile(
|
|
300
|
+
async getClientForFile(
|
|
301
|
+
filePath: string,
|
|
302
|
+
options: FileClientRequestOptions = {},
|
|
303
|
+
): Promise<LspClient | null> {
|
|
304
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
305
|
+
if (this.shuttingDown) return null;
|
|
251
306
|
const route = this.resolveFileRoute(filePath);
|
|
252
|
-
|
|
307
|
+
if (!route) return null;
|
|
308
|
+
|
|
309
|
+
const recovery = this.processCrashRecoveries.get(route.key);
|
|
310
|
+
if (recovery && (recovery.pending || recovery.statusReason)) {
|
|
311
|
+
if (!options.recoverProcessCrash) return null;
|
|
312
|
+
return this.acquireProcessCrashReplacement(recovery, route, options.control);
|
|
313
|
+
}
|
|
314
|
+
// A recovered route keeps its consumed budget but has no active reason.
|
|
315
|
+
// Passive callers can use its running client normally.
|
|
316
|
+
|
|
317
|
+
return this.startServerForRoot(route.serverName, route.root);
|
|
253
318
|
}
|
|
254
319
|
async startServerForRoot(serverName: string, root: string): Promise<LspClient | null> {
|
|
320
|
+
if (this.shuttingDown) return null;
|
|
255
321
|
const serverConfig = this.config.servers[serverName];
|
|
256
322
|
if (!serverConfig) return null;
|
|
257
323
|
const key = clientKey(serverName, root);
|
|
@@ -261,6 +327,12 @@ export class LspManager {
|
|
|
261
327
|
const existing = this.clients.get(key);
|
|
262
328
|
if (existing && existing.status === "running") return existing;
|
|
263
329
|
|
|
330
|
+
// A crashed route is retained until evidence demand starts its shared
|
|
331
|
+
// replacement. Proactive starts must not consume that budget.
|
|
332
|
+
if (existing && existing.status === "error" && this.processCrashRecoveries.has(key)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
264
336
|
// If existing client errored, remove it
|
|
265
337
|
if (existing && existing.status === "error") {
|
|
266
338
|
this.clients.delete(key);
|
|
@@ -315,13 +387,341 @@ export class LspManager {
|
|
|
315
387
|
client: LspClient,
|
|
316
388
|
kind: LspClientLifecycleTransitionKind,
|
|
317
389
|
): void {
|
|
390
|
+
if (this.shuttingDown) return;
|
|
318
391
|
if (this.clientGenerations.get(key) !== generation) return;
|
|
319
392
|
if (this.clients.get(key) !== client) return;
|
|
393
|
+
|
|
394
|
+
if (kind === "startup") {
|
|
395
|
+
this.initializedClientGenerations.set(key, generation);
|
|
396
|
+
}
|
|
397
|
+
if (kind === "crash") {
|
|
398
|
+
if (this.handledCrashGenerations.get(key) === generation) return;
|
|
399
|
+
this.handledCrashGenerations.set(key, generation);
|
|
400
|
+
if (this.initializedClientGenerations.get(key) === generation) {
|
|
401
|
+
this.handleInitializedClientCrash(key, client);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
320
405
|
const aggregateKind =
|
|
321
406
|
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
322
407
|
this.publishLifecycle(aggregateKind);
|
|
323
408
|
}
|
|
324
409
|
|
|
410
|
+
private handleInitializedClientCrash(key: string, client: LspClient): void {
|
|
411
|
+
const existing = this.processCrashRecoveries.get(key);
|
|
412
|
+
if (existing?.attemptConsumed) {
|
|
413
|
+
existing.failedClient = client;
|
|
414
|
+
existing.files = this.trackedFilesForClient(client);
|
|
415
|
+
this.unavailable.set(key, "runtime-error");
|
|
416
|
+
if (existing.statusReason !== "process-crash-recovery-exhausted") {
|
|
417
|
+
existing.statusReason = "process-crash-recovery-exhausted";
|
|
418
|
+
this.recordProcessCrashRecoveryEvent(
|
|
419
|
+
client.name,
|
|
420
|
+
client.root,
|
|
421
|
+
"exhausted",
|
|
422
|
+
existing.pending ? existing.attemptStartedAt : Date.now(),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.processCrashRecoveries.set(key, {
|
|
429
|
+
attemptConsumed: false,
|
|
430
|
+
statusReason: "process-crashed",
|
|
431
|
+
failedClient: client,
|
|
432
|
+
files: this.trackedFilesForClient(client),
|
|
433
|
+
pending: null,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private trackedFilesForClient(client: LspClient): string[] {
|
|
438
|
+
const files = new Set(client.openFiles);
|
|
439
|
+
for (const document of client.getDiagnosticSnapshot().documents) {
|
|
440
|
+
files.add(uriToFile(document.uri));
|
|
441
|
+
}
|
|
442
|
+
return Array.from(files);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private waitForProcessCrashRecovery(
|
|
446
|
+
recovery: ProcessCrashRecoveryState,
|
|
447
|
+
control?: CodeRequestControl,
|
|
448
|
+
): Promise<LspClient | null> {
|
|
449
|
+
if (!recovery.pending) return Promise.resolve(null);
|
|
450
|
+
return raceRequestControl(recovery.pending, control);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private acquireProcessCrashReplacement(
|
|
454
|
+
recovery: ProcessCrashRecoveryState,
|
|
455
|
+
route: FileRoute,
|
|
456
|
+
control?: CodeRequestControl,
|
|
457
|
+
): Promise<LspClient | null> {
|
|
458
|
+
if (recovery.pending) return this.waitForProcessCrashRecovery(recovery, control);
|
|
459
|
+
if (
|
|
460
|
+
recovery.statusReason === "process-crashed" &&
|
|
461
|
+
!recovery.attemptConsumed &&
|
|
462
|
+
!this.shuttingDown
|
|
463
|
+
) {
|
|
464
|
+
return this.startProcessCrashRecovery(recovery, route, control);
|
|
465
|
+
}
|
|
466
|
+
return Promise.resolve(null);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Recover all crashed routes selected by one evidence-producing operation.
|
|
471
|
+
* Each route keeps its independent attempt budget and shared replacement.
|
|
472
|
+
*/
|
|
473
|
+
private async recoverProcessCrashDemand(
|
|
474
|
+
selectFiles: (recovery: ProcessCrashRecoveryState) => readonly string[] | null,
|
|
475
|
+
control?: CodeRequestControl,
|
|
476
|
+
): Promise<ProcessCrashDemandResult> {
|
|
477
|
+
const required = Array.from(this.processCrashRecoveries.entries()).flatMap(
|
|
478
|
+
([key, recovery]): RequiredProcessCrashDemand[] => {
|
|
479
|
+
if (recovery.statusReason === undefined) return [];
|
|
480
|
+
const failedFiles = selectFiles(recovery);
|
|
481
|
+
return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
if (required.length === 0) return { hasSupport: false, failures: [], failedFiles: [] };
|
|
485
|
+
|
|
486
|
+
await Promise.all(
|
|
487
|
+
required.map(async ({ key, recovery }) => {
|
|
488
|
+
const serverConfig = this.config.servers[recovery.failedClient.name];
|
|
489
|
+
if (!serverConfig) return;
|
|
490
|
+
await this.acquireProcessCrashReplacement(
|
|
491
|
+
recovery,
|
|
492
|
+
{
|
|
493
|
+
serverName: recovery.failedClient.name,
|
|
494
|
+
serverConfig,
|
|
495
|
+
root: recovery.failedClient.root,
|
|
496
|
+
key,
|
|
497
|
+
},
|
|
498
|
+
control,
|
|
499
|
+
);
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const failedDemands = required.filter(({ recovery }) => recovery.statusReason !== undefined);
|
|
504
|
+
return {
|
|
505
|
+
hasSupport: true,
|
|
506
|
+
failures: failedDemands.map(({ recovery }) => this.formatProcessCrashDemandFailure(recovery)),
|
|
507
|
+
failedFiles: Array.from(new Set(failedDemands.flatMap(({ failedFiles }) => failedFiles))),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private formatProcessCrashDemandFailure(recovery: ProcessCrashRecoveryState): string {
|
|
512
|
+
const root = (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
|
|
513
|
+
const reason =
|
|
514
|
+
recovery.statusReason === "process-crash-recovery-exhausted"
|
|
515
|
+
? "process recovery exhausted; reload required"
|
|
516
|
+
: recovery.statusReason === "process-crash-recovery-pending"
|
|
517
|
+
? "process recovery in progress"
|
|
518
|
+
: "process crashed";
|
|
519
|
+
return `${recovery.failedClient.name} @ ${root} is unavailable — ${reason}.`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private startProcessCrashRecovery(
|
|
523
|
+
recovery: ProcessCrashRecoveryState,
|
|
524
|
+
route: FileRoute,
|
|
525
|
+
control?: CodeRequestControl,
|
|
526
|
+
): Promise<LspClient | null> {
|
|
527
|
+
throwIfCodeRequestInterrupted(control);
|
|
528
|
+
recovery.attemptConsumed = true;
|
|
529
|
+
recovery.statusReason = "process-crash-recovery-pending";
|
|
530
|
+
recovery.attemptStartedAt = Date.now();
|
|
531
|
+
const pending = this.performProcessCrashRecovery(recovery, route);
|
|
532
|
+
recovery.pending = pending;
|
|
533
|
+
// A cancelled caller may be the only current waiter. Keep the shared
|
|
534
|
+
// replacement promise observed until it settles without adopting that
|
|
535
|
+
// caller's cancellation or deadline.
|
|
536
|
+
pending.catch(() => {});
|
|
537
|
+
this.recordProcessCrashRecoveryEvent(
|
|
538
|
+
route.serverName,
|
|
539
|
+
route.root,
|
|
540
|
+
"attempt",
|
|
541
|
+
recovery.attemptStartedAt,
|
|
542
|
+
);
|
|
543
|
+
this.publishLifecycle("recovery");
|
|
544
|
+
return raceRequestControl(pending, control);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private async performProcessCrashRecovery(
|
|
548
|
+
recovery: ProcessCrashRecoveryState,
|
|
549
|
+
route: FileRoute,
|
|
550
|
+
): Promise<LspClient | null> {
|
|
551
|
+
const key = route.key;
|
|
552
|
+
const attemptStartedAt = recovery.attemptStartedAt;
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
// A process-error event can leave the child alive. Stop the old tree
|
|
556
|
+
// before installing its replacement, but retain its captured evidence.
|
|
557
|
+
await recovery.failedClient.forceKill().catch(() => {});
|
|
558
|
+
if (this.isProcessCrashRecoveryCancelled(recovery, key)) {
|
|
559
|
+
this.recordProcessCrashRecoveryEvent(
|
|
560
|
+
route.serverName,
|
|
561
|
+
route.root,
|
|
562
|
+
"cancelled",
|
|
563
|
+
attemptStartedAt,
|
|
564
|
+
);
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const replacement = this.installProcessCrashReplacement(recovery, route);
|
|
569
|
+
const started = await this.startProcessCrashReplacement(recovery, route, replacement);
|
|
570
|
+
if (!started) return null;
|
|
571
|
+
|
|
572
|
+
// A replacement can crash after initialize and before this operation
|
|
573
|
+
// resumes. Its lifecycle callback has already exhausted the route.
|
|
574
|
+
if (
|
|
575
|
+
started.status !== "running" ||
|
|
576
|
+
recovery.statusReason === "process-crash-recovery-exhausted"
|
|
577
|
+
) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.unavailable.delete(key);
|
|
582
|
+
recovery.statusReason = undefined;
|
|
583
|
+
this.publishLifecycle("recovery");
|
|
584
|
+
this.restoreTrackedDocuments(started, recovery.files);
|
|
585
|
+
this.recordProcessCrashRecoveryEvent(
|
|
586
|
+
route.serverName,
|
|
587
|
+
route.root,
|
|
588
|
+
"success",
|
|
589
|
+
attemptStartedAt,
|
|
590
|
+
);
|
|
591
|
+
return started;
|
|
592
|
+
} finally {
|
|
593
|
+
if (this.processCrashRecoveries.get(key) === recovery) recovery.pending = null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private isProcessCrashRecoveryCancelled(
|
|
598
|
+
recovery: ProcessCrashRecoveryState,
|
|
599
|
+
key: string,
|
|
600
|
+
): boolean {
|
|
601
|
+
return this.shuttingDown || this.processCrashRecoveries.get(key) !== recovery;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private installProcessCrashReplacement(
|
|
605
|
+
recovery: ProcessCrashRecoveryState,
|
|
606
|
+
route: FileRoute,
|
|
607
|
+
): LspClient {
|
|
608
|
+
const key = route.key;
|
|
609
|
+
if (this.clients.get(key) === recovery.failedClient) this.clients.delete(key);
|
|
610
|
+
this.clearWarmedWorkspaceSymbolProjects(route.serverName, route.root);
|
|
611
|
+
this.clearWarmedSemanticProjects(route.serverName, route.root);
|
|
612
|
+
this.clearPendingWarmProbes(route.serverName, route.root);
|
|
613
|
+
|
|
614
|
+
const replacement = this.createClient(route.serverName, route.serverConfig, route.root, key);
|
|
615
|
+
this.clients.set(key, replacement);
|
|
616
|
+
rememberKnownRoot(this.knownRoots, route.serverName, route.root);
|
|
617
|
+
return replacement;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async startProcessCrashReplacement(
|
|
621
|
+
recovery: ProcessCrashRecoveryState,
|
|
622
|
+
route: FileRoute,
|
|
623
|
+
replacement: LspClient,
|
|
624
|
+
): Promise<LspClient | null> {
|
|
625
|
+
try {
|
|
626
|
+
// LspClient's initialize request already has the 30-second transport
|
|
627
|
+
// bound. Do not apply the 5-second diagnostic-restart bound here.
|
|
628
|
+
await replacement.start();
|
|
629
|
+
} catch {
|
|
630
|
+
if (this.isProcessCrashRecoveryCancelled(recovery, route.key)) {
|
|
631
|
+
await replacement.forceKill().catch(() => {});
|
|
632
|
+
this.recordProcessCrashRecoveryEvent(
|
|
633
|
+
route.serverName,
|
|
634
|
+
route.root,
|
|
635
|
+
"cancelled",
|
|
636
|
+
recovery.attemptStartedAt,
|
|
637
|
+
);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
if (recovery.statusReason !== "process-crash-recovery-exhausted") {
|
|
641
|
+
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement);
|
|
642
|
+
this.recordProcessCrashRecoveryEvent(
|
|
643
|
+
route.serverName,
|
|
644
|
+
route.root,
|
|
645
|
+
"failure",
|
|
646
|
+
recovery.attemptStartedAt,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (
|
|
653
|
+
this.isProcessCrashRecoveryCancelled(recovery, route.key) ||
|
|
654
|
+
this.clients.get(route.key) !== replacement
|
|
655
|
+
) {
|
|
656
|
+
await replacement.forceKill().catch(() => {});
|
|
657
|
+
this.recordProcessCrashRecoveryEvent(
|
|
658
|
+
route.serverName,
|
|
659
|
+
route.root,
|
|
660
|
+
"cancelled",
|
|
661
|
+
recovery.attemptStartedAt,
|
|
662
|
+
);
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return replacement;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Restore route documents from disk and retain failed-document evidence. */
|
|
669
|
+
private restoreTrackedDocuments(client: LspClient, files: readonly string[]): void {
|
|
670
|
+
const failedFiles: string[] = [];
|
|
671
|
+
for (const filePath of files) {
|
|
672
|
+
if (!fs.existsSync(filePath)) {
|
|
673
|
+
failedFiles.push(filePath);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
client.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
678
|
+
} catch {
|
|
679
|
+
failedFiles.push(filePath);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (const filePath of failedFiles) client.markFailedFile(filePath);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private markProcessCrashRecoveryFailed(
|
|
686
|
+
recovery: ProcessCrashRecoveryState,
|
|
687
|
+
key: string,
|
|
688
|
+
replacement: LspClient,
|
|
689
|
+
): void {
|
|
690
|
+
recovery.statusReason = "process-crash-recovery-exhausted";
|
|
691
|
+
this.unavailable.set(key, "start-failed");
|
|
692
|
+
if (this.clients.get(key) === replacement) this.clients.delete(key);
|
|
693
|
+
// A failed initialize may still leave a child process alive. The
|
|
694
|
+
// replacement is no longer owned by the manager before it is terminated.
|
|
695
|
+
void replacement.forceKill().catch(() => {});
|
|
696
|
+
this.publishLifecycle("recovery");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private recordProcessCrashRecoveryEvent(
|
|
700
|
+
serverName: string,
|
|
701
|
+
root: string,
|
|
702
|
+
outcome: ProcessCrashRecoveryOutcome,
|
|
703
|
+
startedAt?: number,
|
|
704
|
+
): void {
|
|
705
|
+
try {
|
|
706
|
+
recordDebugEvent({
|
|
707
|
+
source: "lsp",
|
|
708
|
+
level: outcome === "failure" || outcome === "exhausted" ? "warning" : "debug",
|
|
709
|
+
category: "runtime.recovery",
|
|
710
|
+
message: `LSP process-crash recovery ${outcome}`,
|
|
711
|
+
cwd: boundCwd(this.cwd),
|
|
712
|
+
data: {
|
|
713
|
+
reason: "process-crash",
|
|
714
|
+
outcome,
|
|
715
|
+
server: truncateIdentity(serverName),
|
|
716
|
+
root: truncateIdentity(root),
|
|
717
|
+
elapsedMs: startedAt === undefined ? 0 : Math.max(0, Date.now() - startedAt),
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
} catch {
|
|
721
|
+
// Recovery telemetry must never change route behavior.
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
325
725
|
private publishLifecycle(kind: ManagerLifecycleTransitionKind): void {
|
|
326
726
|
if (!this.onLifecycleTransition) return;
|
|
327
727
|
const projectServers = this.getKnownProjectServers([]);
|
|
@@ -360,11 +760,16 @@ export class LspManager {
|
|
|
360
760
|
rememberKnownRoot(this.knownRoots, serverName, root);
|
|
361
761
|
try {
|
|
362
762
|
await client.start();
|
|
763
|
+
if (this.shuttingDown) {
|
|
764
|
+
await client.forceKill().catch(() => {});
|
|
765
|
+
if (this.clients.get(key) === client) this.clients.delete(key);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
363
768
|
this.unavailable.delete(key);
|
|
364
769
|
return client;
|
|
365
770
|
} catch {
|
|
366
771
|
this.unavailable.set(key, "start-failed");
|
|
367
|
-
this.clients.delete(key);
|
|
772
|
+
if (this.clients.get(key) === client) this.clients.delete(key);
|
|
368
773
|
return null;
|
|
369
774
|
}
|
|
370
775
|
}
|
|
@@ -408,6 +813,8 @@ export class LspManager {
|
|
|
408
813
|
if (options?.pushOnly && client.hasDiagnosticProvider) continue;
|
|
409
814
|
|
|
410
815
|
const key = clientKey(client.name, client.root);
|
|
816
|
+
const processRecovery = this.processCrashRecoveries.get(key);
|
|
817
|
+
if (processRecovery?.pending) continue;
|
|
411
818
|
if (seen.has(key)) continue;
|
|
412
819
|
seen.add(key);
|
|
413
820
|
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
|
|
@@ -455,19 +862,8 @@ export class LspManager {
|
|
|
455
862
|
RECOVERY_CLIENT_STARTUP_BOUND_MS,
|
|
456
863
|
"replacement client startup bound exceeded",
|
|
457
864
|
);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (!fs.existsSync(filePath)) {
|
|
461
|
-
failedFiles.push(filePath);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
replacement.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
466
|
-
} catch {
|
|
467
|
-
failedFiles.push(filePath);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
for (const filePath of failedFiles) replacement.markFailedFile(filePath);
|
|
865
|
+
this.restoreTrackedDocuments(replacement, files);
|
|
866
|
+
this.clearUnconsumedProcessCrashState(key, client);
|
|
471
867
|
return { files, restarted: true };
|
|
472
868
|
} catch {
|
|
473
869
|
this.clients.delete(key);
|
|
@@ -479,6 +875,13 @@ export class LspManager {
|
|
|
479
875
|
}
|
|
480
876
|
}
|
|
481
877
|
|
|
878
|
+
private clearUnconsumedProcessCrashState(key: string, failedClient: LspClient): void {
|
|
879
|
+
const recovery = this.processCrashRecoveries.get(key);
|
|
880
|
+
if (recovery?.failedClient !== failedClient || recovery.attemptConsumed) return;
|
|
881
|
+
this.processCrashRecoveries.delete(key);
|
|
882
|
+
this.publishLifecycle("recovery");
|
|
883
|
+
}
|
|
884
|
+
|
|
482
885
|
getProjectServerInfo(serverName: string, root: string, fileTypes: string[]): ProjectServerInfo {
|
|
483
886
|
const key = clientKey(serverName, root);
|
|
484
887
|
return buildProjectServerInfo(
|
|
@@ -489,6 +892,7 @@ export class LspManager {
|
|
|
489
892
|
client: this.clients.get(key),
|
|
490
893
|
unavailableReason:
|
|
491
894
|
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
895
|
+
statusReason: this.processCrashRecoveries.get(key)?.statusReason,
|
|
492
896
|
},
|
|
493
897
|
this.cwd,
|
|
494
898
|
);
|
|
@@ -507,6 +911,19 @@ export class LspManager {
|
|
|
507
911
|
fileTypes: [...(this.config.servers[client.name]?.fileTypes ?? [])],
|
|
508
912
|
});
|
|
509
913
|
}
|
|
914
|
+
// Keep an exhausted recovery route visible after its failed replacement
|
|
915
|
+
// is removed from the active client pool.
|
|
916
|
+
for (const recovery of this.processCrashRecoveries.values()) {
|
|
917
|
+
const name = recovery.failedClient.name;
|
|
918
|
+
const root = recovery.failedClient.root;
|
|
919
|
+
const key = clientKey(name, root);
|
|
920
|
+
if (known.has(key)) continue;
|
|
921
|
+
known.set(key, {
|
|
922
|
+
name,
|
|
923
|
+
root,
|
|
924
|
+
fileTypes: [...(this.config.servers[name]?.fileTypes ?? [])],
|
|
925
|
+
});
|
|
926
|
+
}
|
|
510
927
|
return Array.from(known.values())
|
|
511
928
|
.map((entry) => this.getProjectServerInfo(entry.name, entry.root, entry.fileTypes))
|
|
512
929
|
.sort(
|
|
@@ -524,7 +941,10 @@ export class LspManager {
|
|
|
524
941
|
): Promise<LspClient | null> {
|
|
525
942
|
throwIfCodeRequestInterrupted(control);
|
|
526
943
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
527
|
-
const client = await this.getClientForFile(resolvedPath
|
|
944
|
+
const client = await this.getClientForFile(resolvedPath, {
|
|
945
|
+
recoverProcessCrash: true,
|
|
946
|
+
control,
|
|
947
|
+
});
|
|
528
948
|
if (!client) return null;
|
|
529
949
|
// The caller's wait stops on cancellation even though the shared
|
|
530
950
|
// readiness state keeps its own lifecycle.
|
|
@@ -582,7 +1002,10 @@ export class LspManager {
|
|
|
582
1002
|
control?: CodeRequestControl,
|
|
583
1003
|
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
584
1004
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
585
|
-
const client = await this.getClientForFile(resolvedPath
|
|
1005
|
+
const client = await this.getClientForFile(resolvedPath, {
|
|
1006
|
+
recoverProcessCrash: true,
|
|
1007
|
+
control,
|
|
1008
|
+
});
|
|
586
1009
|
if (!client) {
|
|
587
1010
|
return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
|
|
588
1011
|
}
|
|
@@ -678,20 +1101,83 @@ export class LspManager {
|
|
|
678
1101
|
quietMs?: number;
|
|
679
1102
|
/** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
|
|
680
1103
|
initialEvidence?: DiagnosticEvidenceSummary;
|
|
1104
|
+
/** Explicit demand to recover crashed routes with tracked files in scope. */
|
|
1105
|
+
processCrashDemand?: { scopes?: readonly string[] };
|
|
681
1106
|
control?: CodeRequestControl;
|
|
682
1107
|
}): Promise<WorkspaceRecoveryResult> {
|
|
683
|
-
|
|
1108
|
+
const demand = options?.processCrashDemand
|
|
1109
|
+
? await this.recoverProcessCrashDemand((recovery) => {
|
|
1110
|
+
const files = this.processCrashDiagnosticFiles(recovery, options.processCrashDemand);
|
|
1111
|
+
return files.length > 0 ? files : null;
|
|
1112
|
+
}, options.control)
|
|
1113
|
+
: { hasSupport: false, failures: [], failedFiles: [] };
|
|
1114
|
+
const recoveryOptions = demand.hasSupport
|
|
1115
|
+
? { ...options, initialEvidence: undefined }
|
|
1116
|
+
: options;
|
|
1117
|
+
const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
|
|
1118
|
+
return this.addProcessCrashDiagnosticEvidence(result, demand);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private processCrashDiagnosticFiles(
|
|
1122
|
+
recovery: ProcessCrashRecoveryState,
|
|
1123
|
+
demand: { scopes?: readonly string[] } | undefined,
|
|
1124
|
+
): string[] {
|
|
1125
|
+
if (!demand) return [];
|
|
1126
|
+
const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1127
|
+
return recovery.files.filter(
|
|
1128
|
+
(file) =>
|
|
1129
|
+
this.isDiagnosticFile(file) &&
|
|
1130
|
+
(!scopes ||
|
|
1131
|
+
scopes.length === 0 ||
|
|
1132
|
+
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file))),
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private addProcessCrashDiagnosticEvidence(
|
|
1137
|
+
result: WorkspaceRecoveryResult,
|
|
1138
|
+
demand: ProcessCrashDemandResult,
|
|
1139
|
+
): WorkspaceRecoveryResult {
|
|
1140
|
+
if (demand.failures.length === 0) return result;
|
|
1141
|
+
const byFile = new Map(
|
|
1142
|
+
result.diagnosticEvidence.documents.map((document) => [document.file, document] as const),
|
|
1143
|
+
);
|
|
1144
|
+
for (const file of demand.failedFiles) {
|
|
1145
|
+
const relativeFile = path.relative(this.cwd, file);
|
|
1146
|
+
byFile.set(relativeFile, { file: relativeFile, status: "failed" });
|
|
1147
|
+
}
|
|
1148
|
+
const diagnosticEvidence = summarizeDiagnosticEvidence(Array.from(byFile.values()));
|
|
1149
|
+
const diagnosticReport = {
|
|
1150
|
+
summary: { ...result.diagnosticReport.summary, current: false, evidence: diagnosticEvidence },
|
|
1151
|
+
outstanding: {
|
|
1152
|
+
...result.diagnosticReport.outstanding,
|
|
1153
|
+
current: false,
|
|
1154
|
+
evidence: diagnosticEvidence,
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
return {
|
|
1158
|
+
...result,
|
|
1159
|
+
diagnosticEvidence,
|
|
1160
|
+
diagnosticReport,
|
|
1161
|
+
refreshFailureReason: [result.refreshFailureReason, ...demand.failures]
|
|
1162
|
+
.filter(Boolean)
|
|
1163
|
+
.join("; "),
|
|
1164
|
+
};
|
|
684
1165
|
}
|
|
685
1166
|
|
|
686
1167
|
/** Shut down all running LSP servers. */
|
|
687
1168
|
async shutdownAll(): Promise<void> {
|
|
688
|
-
|
|
1169
|
+
this.shuttingDown = true;
|
|
1170
|
+
const clients = Array.from(this.clients.values());
|
|
1171
|
+
const shutdowns = clients.map((client) => client.shutdown().catch(() => {}));
|
|
689
1172
|
await Promise.all(shutdowns);
|
|
690
1173
|
// Dispose every client so pending or active progress tokens and
|
|
691
1174
|
// readiness promises cannot outlive the manager.
|
|
692
|
-
for (const client of
|
|
1175
|
+
for (const client of clients) client.dispose();
|
|
693
1176
|
this.clients.clear();
|
|
694
1177
|
this.clientGenerations.clear();
|
|
1178
|
+
this.initializedClientGenerations.clear();
|
|
1179
|
+
this.handledCrashGenerations.clear();
|
|
1180
|
+
this.processCrashRecoveries.clear();
|
|
695
1181
|
this.recoveryRestartEpochs.clear();
|
|
696
1182
|
this.unavailable.clear();
|
|
697
1183
|
this.knownRoots.clear();
|
|
@@ -1020,8 +1506,34 @@ export class LspManager {
|
|
|
1020
1506
|
async workspaceSymbol(
|
|
1021
1507
|
query: string,
|
|
1022
1508
|
control?: CodeRequestControl,
|
|
1509
|
+
scopes?: readonly string[],
|
|
1023
1510
|
): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
|
|
1024
|
-
const
|
|
1511
|
+
const resolvedScopes = scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1512
|
+
const routeMatches = (client: LspClient) =>
|
|
1513
|
+
this.workspaceSymbolRouteMatchesScopes(client, resolvedScopes);
|
|
1514
|
+
if (resolvedScopes && !this.hasWorkspaceSymbolRouteForScopes(resolvedScopes)) {
|
|
1515
|
+
return unavailableCodeQuery(
|
|
1516
|
+
"No known LSP route intersects the requested workspace-symbol scope.",
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const demand = await this.recoverProcessCrashDemand(
|
|
1520
|
+
(recovery) =>
|
|
1521
|
+
recovery.failedClient.serverCapabilities?.workspaceSymbolProvider &&
|
|
1522
|
+
routeMatches(recovery.failedClient)
|
|
1523
|
+
? []
|
|
1524
|
+
: null,
|
|
1525
|
+
control,
|
|
1526
|
+
);
|
|
1527
|
+
const collect = async (clients: Iterable<LspClient>, value: string) =>
|
|
1528
|
+
this.addWorkspaceSymbolDemandEvidence(
|
|
1529
|
+
await collectWorkspaceSymbols(
|
|
1530
|
+
Array.from(clients).filter((client) => routeMatches(client)),
|
|
1531
|
+
value,
|
|
1532
|
+
control,
|
|
1533
|
+
),
|
|
1534
|
+
demand,
|
|
1535
|
+
);
|
|
1536
|
+
const initial = await collect(this.clients.values(), query);
|
|
1025
1537
|
if (!initial.hasSupport || initial.results.length > 0) {
|
|
1026
1538
|
return workspaceSymbolCollectionResult(initial);
|
|
1027
1539
|
}
|
|
@@ -1029,15 +1541,57 @@ export class LspManager {
|
|
|
1029
1541
|
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
|
|
1030
1542
|
findWarmTargets: findWorkspaceSymbolWarmTargets,
|
|
1031
1543
|
getWarmPosition: getWorkspaceSymbolWarmPosition,
|
|
1032
|
-
collect
|
|
1544
|
+
collect,
|
|
1545
|
+
routeMatches,
|
|
1033
1546
|
query,
|
|
1034
1547
|
control,
|
|
1035
1548
|
});
|
|
1036
1549
|
return workspaceSymbolCollectionResult(warmed.collection ?? initial);
|
|
1037
1550
|
}
|
|
1038
|
-
|
|
1551
|
+
|
|
1552
|
+
private addWorkspaceSymbolDemandEvidence(
|
|
1553
|
+
collection: WorkspaceSymbolCollection,
|
|
1554
|
+
demand: ProcessCrashDemandResult,
|
|
1555
|
+
): WorkspaceSymbolCollection {
|
|
1556
|
+
return {
|
|
1557
|
+
...collection,
|
|
1558
|
+
hasSupport: collection.hasSupport || demand.hasSupport,
|
|
1559
|
+
failures: [...collection.failures, ...demand.failures],
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
private hasWorkspaceSymbolRouteForScopes(scopes: readonly string[]): boolean {
|
|
1564
|
+
const clients = [
|
|
1565
|
+
...this.clients.values(),
|
|
1566
|
+
...Array.from(this.processCrashRecoveries.values(), (recovery) => recovery.failedClient),
|
|
1567
|
+
];
|
|
1568
|
+
return clients.some((client) => this.workspaceSymbolRouteMatchesScopes(client, scopes));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private workspaceSymbolRouteMatchesScopes(
|
|
1572
|
+
client: LspClient,
|
|
1573
|
+
scopes: readonly string[] | undefined,
|
|
1574
|
+
): boolean {
|
|
1575
|
+
if (!scopes || scopes.length === 0) return true;
|
|
1576
|
+
const key = clientKey(client.name, client.root);
|
|
1577
|
+
return scopes.some((scope) => {
|
|
1578
|
+
try {
|
|
1579
|
+
if (fs.statSync(scope).isFile()) return this.resolveFileRoute(scope)?.key === key;
|
|
1580
|
+
} catch {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
return (
|
|
1584
|
+
projectRoots.isWithinOrEqual(client.root, scope) ||
|
|
1585
|
+
projectRoots.isWithinOrEqual(scope, client.root)
|
|
1586
|
+
);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async ensureFileOpen(
|
|
1590
|
+
filePath: string,
|
|
1591
|
+
options: FileClientRequestOptions = {},
|
|
1592
|
+
): Promise<LspClient | null> {
|
|
1039
1593
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1040
|
-
const client = await this.getClientForFile(resolvedPath);
|
|
1594
|
+
const client = await this.getClientForFile(resolvedPath, options);
|
|
1041
1595
|
if (!client) return null;
|
|
1042
1596
|
try {
|
|
1043
1597
|
client.didOpen(resolvedPath, fs.readFileSync(resolvedPath, "utf-8"));
|
|
@@ -1059,13 +1613,15 @@ export class LspManager {
|
|
|
1059
1613
|
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
1060
1614
|
) => import("../config/types.ts").Position | null;
|
|
1061
1615
|
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
|
|
1616
|
+
routeMatches?: (client: LspClient) => boolean;
|
|
1062
1617
|
query: string;
|
|
1063
1618
|
control?: CodeRequestControl;
|
|
1064
1619
|
}): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
|
|
1065
|
-
const { findWarmTargets, getWarmPosition, collect, query, control } = options;
|
|
1620
|
+
const { findWarmTargets, getWarmPosition, collect, routeMatches, query, control } = options;
|
|
1066
1621
|
let warmedAny = false;
|
|
1067
1622
|
|
|
1068
1623
|
for (const client of Array.from(this.clients.values())) {
|
|
1624
|
+
if (routeMatches && !routeMatches(client)) continue;
|
|
1069
1625
|
if (client.status !== "running") continue;
|
|
1070
1626
|
if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
|
|
1071
1627
|
|