@mrclrchtr/supi-code-intelligence 6.2.0 → 6.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 +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/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-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/README.md +52 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +785 -129
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +660 -98
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +10 -4
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +4 -32
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +106 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +4 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +513 -113
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -15
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +40 -33
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +36 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +18 -9
- 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-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/README.md +29 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs +99 -19
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js +94 -18
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js.map +4 -4
- 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 +14 -14
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +13 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs +97 -19
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts +28 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts.map +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts +28 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts.map +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js +92 -18
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js.map +4 -4
- 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 +17 -17
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +2 -2
- package/package.json +5 -5
- package/src/session/health-refresh.ts +19 -12
- package/src/session/health-types.ts +8 -3
- package/src/session/health-workflow.ts +7 -3
- package/src/session/session.ts +12 -5
- package/src/substrate/lsp/maintenance.ts +53 -47
- package/src/substrate/lsp/settings.ts +2 -1
- package/src/substrate/lsp/source-tracking.ts +247 -0
- package/src/tool/code_find/guidance.ts +3 -3
- package/src/tool/code_graph/guidance.ts +2 -2
- package/src/tool/code_health/guidance.ts +3 -3
- package/src/tool/code_health/markdown.ts +21 -5
- package/src/tool/code_health/refresh-outcome.ts +48 -7
- package/src/tool/code_health/refresh-status.ts +83 -16
- package/src/tool/code_health/tui.ts +4 -0
- package/src/tool/code_inspect/guidance.ts +5 -3
- package/src/tool/code_orientation/guidance.ts +3 -4
- package/src/tool/code_refactor_apply/guidance.ts +1 -1
- package/src/tool/code_refactor_plan/guidance.ts +3 -5
- package/src/tool/code_resolve/guidance.ts +3 -3
- package/src/tool/guidance.ts +11 -9
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
truncateDebugIdentity as truncateIdentity,
|
|
27
27
|
} from "@mrclrchtr/supi-core/debug";
|
|
28
28
|
import { resolveToolPath as resolveSessionPath, uriToFile } from "@mrclrchtr/supi-core/path";
|
|
29
|
+
import { isMissingFileError } from "../client/client-file-state.ts";
|
|
29
30
|
import { getServerForFile } from "../config/config.ts";
|
|
30
31
|
import {
|
|
31
32
|
getFileScopeDecision,
|
|
@@ -63,14 +64,23 @@ import {
|
|
|
63
64
|
type WorkspaceSentinelScanOptions,
|
|
64
65
|
type WorkspaceSentinelSyncResult,
|
|
65
66
|
} from "../diagnostics/workspace-sentinels.ts";
|
|
67
|
+
import {
|
|
68
|
+
scanWorkspaceSources as scanAutomaticWorkspaceSources,
|
|
69
|
+
type WorkspaceSourceInventory,
|
|
70
|
+
} from "../diagnostics/workspace-sources.ts";
|
|
66
71
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
67
72
|
import {
|
|
68
|
-
|
|
69
|
-
type
|
|
73
|
+
emptyProcessCrashRecoveryReport,
|
|
74
|
+
type ProcessCrashRecoveryReport,
|
|
70
75
|
type ScopeDecisionEntry,
|
|
71
76
|
type ScopeDecisionSummary,
|
|
72
77
|
type WorkspaceDiagnosticReport,
|
|
73
78
|
} from "../session/runtime-diagnostics.ts";
|
|
79
|
+
import {
|
|
80
|
+
type BulkTrackFileOutcome,
|
|
81
|
+
type BulkTrackFilesResult,
|
|
82
|
+
MAX_BULK_TRACK_FILES,
|
|
83
|
+
} from "../session/workspace-lsp-runtime.ts";
|
|
74
84
|
import {
|
|
75
85
|
displayRelativeFilePath,
|
|
76
86
|
formatCoverageSummaryText,
|
|
@@ -94,6 +104,11 @@ import {
|
|
|
94
104
|
syncClientFileAndGetDiagnostics,
|
|
95
105
|
} from "./manager-diagnostics.ts";
|
|
96
106
|
import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-helpers.ts";
|
|
107
|
+
import {
|
|
108
|
+
boundProcessCrashFailureMessage,
|
|
109
|
+
buildProcessCrashRecoveryReport,
|
|
110
|
+
type ProcessCrashRecoveryRouteResult,
|
|
111
|
+
} from "./manager-process-crash-report.ts";
|
|
97
112
|
import { buildProjectServerInfo } from "./manager-project-info.ts";
|
|
98
113
|
import type {
|
|
99
114
|
ActiveCoverageSummaryEntry,
|
|
@@ -119,6 +134,7 @@ type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
|
119
134
|
|
|
120
135
|
/** Maximum tracked-file entries retained in one scope-decision telemetry summary. */
|
|
121
136
|
const SCOPE_DECISION_MAX_ENTRIES = 24;
|
|
137
|
+
const MAX_CONCURRENT_BULK_TRACKS = 4;
|
|
122
138
|
|
|
123
139
|
interface ScopeDecisionAccumulator {
|
|
124
140
|
entries: ScopeDecisionEntry[];
|
|
@@ -180,7 +196,7 @@ type FileClientRequestOptions = {
|
|
|
180
196
|
|
|
181
197
|
interface FileReadinessResult {
|
|
182
198
|
client: LspClient | null;
|
|
183
|
-
processCrashRecovery:
|
|
199
|
+
processCrashRecovery: ProcessCrashRecoveryReport;
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
interface ProcessCrashRecoveryState {
|
|
@@ -196,40 +212,56 @@ interface ProcessCrashRecoveryState {
|
|
|
196
212
|
pending: Promise<LspClient | null> | null;
|
|
197
213
|
/** Start time for the current replacement attempt, when one exists. */
|
|
198
214
|
attemptStartedAt?: number;
|
|
215
|
+
/** Bounded caught Error.message from a failed replacement startup. */
|
|
216
|
+
failureMessage?: string;
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhausted" | "cancelled";
|
|
202
|
-
|
|
203
219
|
type ProcessCrashDemandResult = {
|
|
204
|
-
/** Whether at least one
|
|
220
|
+
/** Whether at least one crashed route supports the operation. */
|
|
205
221
|
hasSupport: boolean;
|
|
222
|
+
/** Whether replacement work changed the route set and needs a fresh pass. */
|
|
223
|
+
requiresFreshEvidence: boolean;
|
|
206
224
|
/** Separate route-level result for this explicit process-crash demand. */
|
|
207
|
-
processCrashRecovery:
|
|
225
|
+
processCrashRecovery: ProcessCrashRecoveryReport;
|
|
208
226
|
/** Required routes that remain unavailable after shared recovery settles. */
|
|
209
227
|
failures: string[];
|
|
210
228
|
/** In-scope tracked files from required routes that remain unavailable. */
|
|
211
229
|
failedFiles: string[];
|
|
212
230
|
};
|
|
213
231
|
|
|
232
|
+
type ProcessCrashRecoverySnapshot = {
|
|
233
|
+
readonly key: string;
|
|
234
|
+
/** Mutable state handle used only to await the shared replacement. */
|
|
235
|
+
readonly recovery: ProcessCrashRecoveryState;
|
|
236
|
+
readonly failedClient: LspClient;
|
|
237
|
+
readonly name: string;
|
|
238
|
+
readonly root: string;
|
|
239
|
+
readonly statusReason: NonNullable<ProcessCrashRecoveryState["statusReason"]>;
|
|
240
|
+
readonly files: readonly string[];
|
|
241
|
+
};
|
|
242
|
+
|
|
214
243
|
type RequiredProcessCrashDemand = {
|
|
215
|
-
|
|
216
|
-
recovery: ProcessCrashRecoveryState;
|
|
244
|
+
snapshot: ProcessCrashRecoverySnapshot;
|
|
217
245
|
failedFiles: readonly string[];
|
|
218
246
|
};
|
|
219
247
|
|
|
220
|
-
type
|
|
248
|
+
type ImmediateProcessCrashDemand = {
|
|
249
|
+
route: ProcessCrashRecoveryRouteResult;
|
|
250
|
+
failedFiles: readonly string[];
|
|
251
|
+
reportOnly: boolean;
|
|
252
|
+
};
|
|
221
253
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
type ProcessCrashDemandSelection = {
|
|
255
|
+
immediate: ImmediateProcessCrashDemand[];
|
|
256
|
+
required: RequiredProcessCrashDemand[];
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
type ProcessCrashRecoveryTelemetryOutcome =
|
|
260
|
+
| "attempt"
|
|
261
|
+
| "success"
|
|
262
|
+
| "failure"
|
|
263
|
+
| "exhausted"
|
|
264
|
+
| "cancelled";
|
|
233
265
|
|
|
234
266
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
235
267
|
export class LspManager {
|
|
@@ -265,6 +297,9 @@ export class LspManager {
|
|
|
265
297
|
private handledCrashGenerations = new Map<string, number>();
|
|
266
298
|
/** Prevent late startup and lifecycle callbacks from republishing after shutdown. */
|
|
267
299
|
private shuttingDown = false;
|
|
300
|
+
/** Coalesce per-file tracking transitions into one bulk transition. */
|
|
301
|
+
private trackedFilesBatchDepth = 0;
|
|
302
|
+
private trackedFilesBatchChanged = false;
|
|
268
303
|
constructor(
|
|
269
304
|
private readonly config: LspConfig,
|
|
270
305
|
private readonly cwd: string,
|
|
@@ -292,20 +327,24 @@ export class LspManager {
|
|
|
292
327
|
}
|
|
293
328
|
|
|
294
329
|
/**
|
|
295
|
-
* Return
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* not reported as a new outcome. This is a passive read and never starts
|
|
299
|
-
* recovery.
|
|
330
|
+
* Return an exhausted process-crash outcome for a file readiness result.
|
|
331
|
+
* This is a passive read and never starts recovery. A crashed or pending
|
|
332
|
+
* route has no final result until an explicit operation observes it.
|
|
300
333
|
*/
|
|
301
|
-
|
|
334
|
+
getProcessCrashRecoveryReportForFile(filePath: string): ProcessCrashRecoveryReport | null {
|
|
302
335
|
const route = this.resolveFileRoute(resolveSessionPath(this.cwd, filePath));
|
|
303
336
|
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
304
|
-
if (!
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
337
|
+
if (!route || recovery?.statusReason !== "process-crash-recovery-exhausted") return null;
|
|
338
|
+
return buildProcessCrashRecoveryReport(
|
|
339
|
+
[
|
|
340
|
+
{
|
|
341
|
+
name: route.serverName,
|
|
342
|
+
root: route.root,
|
|
343
|
+
outcome: "recovery-exhausted",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
this.cwd,
|
|
347
|
+
);
|
|
309
348
|
}
|
|
310
349
|
|
|
311
350
|
/**
|
|
@@ -318,7 +357,7 @@ export class LspManager {
|
|
|
318
357
|
return this.canServeFile(filePath);
|
|
319
358
|
}
|
|
320
359
|
|
|
321
|
-
/** Inventory automatic workspace
|
|
360
|
+
/** Inventory automatic workspace sentinels with the runtime policy. */
|
|
322
361
|
scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
|
|
323
362
|
return scanAutomaticWorkspaceSentinels(this.cwd, {
|
|
324
363
|
...options,
|
|
@@ -326,7 +365,16 @@ export class LspManager {
|
|
|
326
365
|
});
|
|
327
366
|
}
|
|
328
367
|
|
|
329
|
-
/**
|
|
368
|
+
/** Scan configured LSP source extensions with the runtime path policy. */
|
|
369
|
+
scanWorkspaceSources(control?: CodeRequestControl): Promise<WorkspaceSourceInventory> {
|
|
370
|
+
return scanAutomaticWorkspaceSources(this.cwd, {
|
|
371
|
+
fileTypes: Object.values(this.config.servers).flatMap((server) => server.fileTypes),
|
|
372
|
+
policy: this.automaticPathPolicy,
|
|
373
|
+
control,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Refresh the automatic workspace sentinel with the runtime policy. */
|
|
330
378
|
syncWorkspaceSentinelSnapshot(
|
|
331
379
|
previous: Map<string, number>,
|
|
332
380
|
options: WorkspaceSentinelScanOptions = {},
|
|
@@ -392,9 +440,14 @@ export class LspManager {
|
|
|
392
440
|
// A recovered route keeps its consumed budget but has no active reason.
|
|
393
441
|
// Passive callers can use its running client normally.
|
|
394
442
|
|
|
395
|
-
return this.startServerForRoot(route.serverName, route.root);
|
|
443
|
+
return this.startServerForRoot(route.serverName, route.root, options.control);
|
|
396
444
|
}
|
|
397
|
-
async startServerForRoot(
|
|
445
|
+
async startServerForRoot(
|
|
446
|
+
serverName: string,
|
|
447
|
+
root: string,
|
|
448
|
+
control?: CodeRequestControl,
|
|
449
|
+
): Promise<LspClient | null> {
|
|
450
|
+
throwIfCodeRequestInterrupted(control);
|
|
398
451
|
if (this.shuttingDown) return null;
|
|
399
452
|
const serverConfig = this.config.servers[serverName];
|
|
400
453
|
if (!serverConfig) return null;
|
|
@@ -425,17 +478,18 @@ export class LspManager {
|
|
|
425
478
|
// This prevents spawning duplicate server processes when two
|
|
426
479
|
// callers race through getClientForFile before either await yields.
|
|
427
480
|
const pending = this.pendingStarts.get(key);
|
|
428
|
-
if (pending) return pending;
|
|
481
|
+
if (pending) return raceRequestControl(pending, control);
|
|
429
482
|
|
|
430
483
|
const startPromise = this.performStart(serverName, serverConfig, root, key);
|
|
431
484
|
this.pendingStarts.set(key, startPromise);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.pendingStarts.delete(key);
|
|
437
|
-
}
|
|
438
|
-
|
|
485
|
+
// Keep the shared startup registered until it settles. A cancelled
|
|
486
|
+
// caller only stops waiting; it must not allow a duplicate route start.
|
|
487
|
+
void startPromise
|
|
488
|
+
.finally(() => {
|
|
489
|
+
if (this.pendingStarts.get(key) === startPromise) this.pendingStarts.delete(key);
|
|
490
|
+
})
|
|
491
|
+
.catch(() => {});
|
|
492
|
+
return raceRequestControl(startPromise, control);
|
|
439
493
|
}
|
|
440
494
|
|
|
441
495
|
private createClient(
|
|
@@ -480,6 +534,11 @@ export class LspManager {
|
|
|
480
534
|
}
|
|
481
535
|
}
|
|
482
536
|
|
|
537
|
+
if (kind === "tracked-files" && this.trackedFilesBatchDepth > 0) {
|
|
538
|
+
this.trackedFilesBatchChanged = true;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
483
542
|
const aggregateKind =
|
|
484
543
|
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
485
544
|
this.publishLifecycle(aggregateKind);
|
|
@@ -509,6 +568,7 @@ export class LspManager {
|
|
|
509
568
|
failedClient: client,
|
|
510
569
|
files: this.trackedFilesForClient(client),
|
|
511
570
|
pending: null,
|
|
571
|
+
failureMessage: undefined,
|
|
512
572
|
});
|
|
513
573
|
}
|
|
514
574
|
|
|
@@ -547,66 +607,192 @@ export class LspManager {
|
|
|
547
607
|
/**
|
|
548
608
|
* Recover all crashed routes selected by one evidence-producing operation.
|
|
549
609
|
* Each route keeps its independent attempt budget and shared replacement.
|
|
610
|
+
* Selection uses one immutable route-fact snapshot so scope and retention
|
|
611
|
+
* facts cannot change halfway through the demand.
|
|
550
612
|
*/
|
|
551
613
|
private async recoverProcessCrashDemand(
|
|
552
|
-
selectFiles: (
|
|
614
|
+
selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
|
|
553
615
|
control?: CodeRequestControl,
|
|
616
|
+
options: { skipWhenNoRetainedFile?: boolean } = {},
|
|
554
617
|
): Promise<ProcessCrashDemandResult> {
|
|
555
|
-
const required =
|
|
556
|
-
(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
|
|
560
|
-
},
|
|
618
|
+
const { immediate, required } = this.selectProcessCrashDemandRoutes(
|
|
619
|
+
this.snapshotProcessCrashRoutes(),
|
|
620
|
+
selectFiles,
|
|
621
|
+
options,
|
|
561
622
|
);
|
|
623
|
+
const hasSupport = immediate.length > 0 || required.length > 0;
|
|
562
624
|
if (required.length === 0) {
|
|
625
|
+
const failures = immediate
|
|
626
|
+
.filter((entry) => !entry.reportOnly)
|
|
627
|
+
.map((entry) => this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome));
|
|
563
628
|
return {
|
|
564
|
-
hasSupport
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
629
|
+
hasSupport,
|
|
630
|
+
requiresFreshEvidence: false,
|
|
631
|
+
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
632
|
+
immediate.map(({ route }) => route),
|
|
633
|
+
this.cwd,
|
|
634
|
+
),
|
|
635
|
+
failures,
|
|
636
|
+
failedFiles: Array.from(
|
|
637
|
+
new Set(
|
|
638
|
+
immediate
|
|
639
|
+
.filter((entry) => !entry.reportOnly)
|
|
640
|
+
.flatMap(({ failedFiles }) => failedFiles),
|
|
641
|
+
),
|
|
642
|
+
),
|
|
568
643
|
};
|
|
569
644
|
}
|
|
570
645
|
|
|
571
646
|
await Promise.all(
|
|
572
|
-
required.map(async ({
|
|
573
|
-
const serverConfig = this.config.servers[
|
|
647
|
+
required.map(async ({ snapshot }) => {
|
|
648
|
+
const serverConfig = this.config.servers[snapshot.name];
|
|
574
649
|
if (!serverConfig) return;
|
|
575
650
|
await this.acquireProcessCrashReplacement(
|
|
576
|
-
recovery,
|
|
651
|
+
snapshot.recovery,
|
|
577
652
|
{
|
|
578
|
-
serverName:
|
|
653
|
+
serverName: snapshot.name,
|
|
579
654
|
serverConfig,
|
|
580
|
-
root:
|
|
581
|
-
key,
|
|
655
|
+
root: snapshot.root,
|
|
656
|
+
key: snapshot.key,
|
|
582
657
|
},
|
|
583
658
|
control,
|
|
584
659
|
);
|
|
585
660
|
}),
|
|
586
661
|
);
|
|
587
662
|
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
663
|
+
const observedRequired = required.filter(
|
|
664
|
+
({ snapshot }) => !this.isProcessCrashDemandCancelled(snapshot),
|
|
665
|
+
);
|
|
666
|
+
const failedDemands = observedRequired.filter(
|
|
667
|
+
({ snapshot }) => snapshot.recovery.statusReason !== undefined,
|
|
668
|
+
);
|
|
669
|
+
const recoveredRoutes = observedRequired.map(
|
|
670
|
+
({ snapshot }): ProcessCrashRecoveryRouteResult => ({
|
|
671
|
+
name: snapshot.name,
|
|
672
|
+
root: snapshot.root,
|
|
673
|
+
outcome: snapshot.recovery.statusReason === undefined ? "recovered" : "recovery-failed",
|
|
674
|
+
...(snapshot.recovery.statusReason === undefined || !snapshot.recovery.failureMessage
|
|
675
|
+
? {}
|
|
676
|
+
: { failureMessage: snapshot.recovery.failureMessage }),
|
|
677
|
+
}),
|
|
592
678
|
);
|
|
679
|
+
const failedImmediate = immediate.filter((entry) => !entry.reportOnly);
|
|
593
680
|
return {
|
|
594
|
-
hasSupport
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
681
|
+
hasSupport,
|
|
682
|
+
requiresFreshEvidence: observedRequired.length > 0,
|
|
683
|
+
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
684
|
+
[...immediate.map(({ route }) => route), ...recoveredRoutes],
|
|
685
|
+
this.cwd,
|
|
686
|
+
),
|
|
687
|
+
failures: [
|
|
688
|
+
...failedImmediate.map((entry) =>
|
|
689
|
+
this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome),
|
|
690
|
+
),
|
|
691
|
+
...failedDemands.map(({ snapshot }) =>
|
|
692
|
+
this.formatProcessCrashDemandFailure(snapshot.recovery, "recovery-failed"),
|
|
693
|
+
),
|
|
694
|
+
],
|
|
695
|
+
failedFiles: Array.from(
|
|
696
|
+
new Set([
|
|
697
|
+
...failedImmediate.flatMap(({ failedFiles }) => failedFiles),
|
|
698
|
+
...failedDemands.flatMap(({ failedFiles }) => failedFiles),
|
|
699
|
+
]),
|
|
700
|
+
),
|
|
598
701
|
};
|
|
599
702
|
}
|
|
600
703
|
|
|
601
|
-
private
|
|
602
|
-
|
|
704
|
+
private isProcessCrashDemandCancelled(snapshot: ProcessCrashRecoverySnapshot): boolean {
|
|
705
|
+
return (
|
|
706
|
+
snapshot.recovery.statusReason !== undefined &&
|
|
707
|
+
(this.shuttingDown || this.processCrashRecoveries.get(snapshot.key) !== snapshot.recovery)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private selectProcessCrashDemandRoutes(
|
|
712
|
+
snapshots: readonly ProcessCrashRecoverySnapshot[],
|
|
713
|
+
selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
|
|
714
|
+
options: { skipWhenNoRetainedFile?: boolean },
|
|
715
|
+
): ProcessCrashDemandSelection {
|
|
716
|
+
const immediate: ImmediateProcessCrashDemand[] = [];
|
|
717
|
+
const required: RequiredProcessCrashDemand[] = [];
|
|
718
|
+
|
|
719
|
+
for (const snapshot of snapshots) {
|
|
720
|
+
const failedFiles = selectFiles(snapshot);
|
|
721
|
+
if (failedFiles === null) continue;
|
|
722
|
+
|
|
723
|
+
const exhausted = snapshot.statusReason === "process-crash-recovery-exhausted";
|
|
724
|
+
const noRetainedFile = failedFiles.length === 0;
|
|
725
|
+
if (exhausted) {
|
|
726
|
+
immediate.push({
|
|
727
|
+
route: {
|
|
728
|
+
name: snapshot.name,
|
|
729
|
+
root: snapshot.root,
|
|
730
|
+
outcome: "recovery-exhausted",
|
|
731
|
+
},
|
|
732
|
+
failedFiles,
|
|
733
|
+
reportOnly: options.skipWhenNoRetainedFile === true && noRetainedFile,
|
|
734
|
+
});
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (
|
|
738
|
+
options.skipWhenNoRetainedFile === true &&
|
|
739
|
+
noRetainedFile &&
|
|
740
|
+
snapshot.statusReason === "process-crashed"
|
|
741
|
+
) {
|
|
742
|
+
immediate.push({
|
|
743
|
+
route: {
|
|
744
|
+
name: snapshot.name,
|
|
745
|
+
root: snapshot.root,
|
|
746
|
+
outcome: "skipped-no-retained-file",
|
|
747
|
+
},
|
|
748
|
+
failedFiles,
|
|
749
|
+
reportOnly: true,
|
|
750
|
+
});
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
required.push({ snapshot, failedFiles });
|
|
754
|
+
}
|
|
755
|
+
return { immediate, required };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private snapshotProcessCrashRoutes(): ProcessCrashRecoverySnapshot[] {
|
|
759
|
+
return Array.from(this.processCrashRecoveries.entries()).flatMap(([key, recovery]) => {
|
|
760
|
+
if (recovery.statusReason === undefined) return [];
|
|
761
|
+
return [
|
|
762
|
+
{
|
|
763
|
+
key,
|
|
764
|
+
recovery,
|
|
765
|
+
failedClient: recovery.failedClient,
|
|
766
|
+
name: recovery.failedClient.name,
|
|
767
|
+
root: recovery.failedClient.root,
|
|
768
|
+
statusReason: recovery.statusReason,
|
|
769
|
+
files: [...recovery.files],
|
|
770
|
+
},
|
|
771
|
+
];
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private formatProcessCrashDemandFailure(
|
|
776
|
+
recovery: ProcessCrashRecoveryState | ProcessCrashRecoveryRouteResult,
|
|
777
|
+
outcome?: ProcessCrashRecoveryRouteResult["outcome"],
|
|
778
|
+
): string {
|
|
779
|
+
const isRouteResult = "outcome" in recovery;
|
|
780
|
+
const name = isRouteResult ? recovery.name : recovery.failedClient.name;
|
|
781
|
+
const root = isRouteResult
|
|
782
|
+
? recovery.root
|
|
783
|
+
: (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
|
|
784
|
+
const routeOutcome =
|
|
785
|
+
outcome ??
|
|
786
|
+
(isRouteResult
|
|
787
|
+
? recovery.outcome
|
|
788
|
+
: recovery.statusReason === "process-crash-recovery-exhausted"
|
|
789
|
+
? "recovery-exhausted"
|
|
790
|
+
: "recovery-failed");
|
|
603
791
|
const reason =
|
|
604
|
-
|
|
792
|
+
routeOutcome === "recovery-exhausted"
|
|
605
793
|
? "process recovery exhausted; reload required"
|
|
606
|
-
:
|
|
607
|
-
|
|
608
|
-
: "process crashed";
|
|
609
|
-
return `${recovery.failedClient.name} @ ${root} is unavailable — ${reason}.`;
|
|
794
|
+
: "process recovery failed";
|
|
795
|
+
return `${name} @ ${root} is unavailable — ${reason}.`;
|
|
610
796
|
}
|
|
611
797
|
|
|
612
798
|
private startProcessCrashRecovery(
|
|
@@ -670,6 +856,7 @@ export class LspManager {
|
|
|
670
856
|
|
|
671
857
|
this.unavailable.delete(key);
|
|
672
858
|
recovery.statusReason = undefined;
|
|
859
|
+
recovery.failureMessage = undefined;
|
|
673
860
|
this.publishLifecycle("recovery");
|
|
674
861
|
this.restoreTrackedDocuments(started, recovery.files);
|
|
675
862
|
this.recordProcessCrashRecoveryEvent(
|
|
@@ -716,7 +903,7 @@ export class LspManager {
|
|
|
716
903
|
// LspClient's initialize request already has the 30-second transport
|
|
717
904
|
// bound. Do not apply the 5-second diagnostic-restart bound here.
|
|
718
905
|
await replacement.start();
|
|
719
|
-
} catch {
|
|
906
|
+
} catch (error) {
|
|
720
907
|
if (this.isProcessCrashRecoveryCancelled(recovery, route.key)) {
|
|
721
908
|
await replacement.forceKill().catch(() => {});
|
|
722
909
|
this.recordProcessCrashRecoveryEvent(
|
|
@@ -728,7 +915,7 @@ export class LspManager {
|
|
|
728
915
|
return null;
|
|
729
916
|
}
|
|
730
917
|
if (recovery.statusReason !== "process-crash-recovery-exhausted") {
|
|
731
|
-
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement);
|
|
918
|
+
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement, error);
|
|
732
919
|
this.recordProcessCrashRecoveryEvent(
|
|
733
920
|
route.serverName,
|
|
734
921
|
route.root,
|
|
@@ -776,8 +963,13 @@ export class LspManager {
|
|
|
776
963
|
recovery: ProcessCrashRecoveryState,
|
|
777
964
|
key: string,
|
|
778
965
|
replacement: LspClient,
|
|
966
|
+
error: unknown,
|
|
779
967
|
): void {
|
|
780
968
|
recovery.statusReason = "process-crash-recovery-exhausted";
|
|
969
|
+
recovery.failureMessage =
|
|
970
|
+
error instanceof Error && error.message
|
|
971
|
+
? boundProcessCrashFailureMessage(error.message)
|
|
972
|
+
: undefined;
|
|
781
973
|
this.unavailable.set(key, "start-failed");
|
|
782
974
|
if (this.clients.get(key) === replacement) this.clients.delete(key);
|
|
783
975
|
// A failed initialize may still leave a child process alive. The
|
|
@@ -789,7 +981,7 @@ export class LspManager {
|
|
|
789
981
|
private recordProcessCrashRecoveryEvent(
|
|
790
982
|
serverName: string,
|
|
791
983
|
root: string,
|
|
792
|
-
outcome:
|
|
984
|
+
outcome: ProcessCrashRecoveryTelemetryOutcome,
|
|
793
985
|
startedAt?: number,
|
|
794
986
|
): void {
|
|
795
987
|
try {
|
|
@@ -1065,33 +1257,103 @@ export class LspManager {
|
|
|
1065
1257
|
async waitUntilFileReady(
|
|
1066
1258
|
filePath: string,
|
|
1067
1259
|
control?: CodeRequestControl,
|
|
1068
|
-
onProcessCrashRecovery?: (
|
|
1260
|
+
onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void,
|
|
1069
1261
|
): Promise<FileReadinessResult> {
|
|
1070
1262
|
throwIfCodeRequestInterrupted(control);
|
|
1071
1263
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1072
1264
|
const route = this.resolveFileRoute(resolvedPath);
|
|
1073
1265
|
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
if (processCrashRecoveryRequested) {
|
|
1077
|
-
onProcessCrashRecovery?.(summarizeProcessCrashRecoveryRoutes(["pending"]));
|
|
1078
|
-
}
|
|
1266
|
+
const statusAtStart = recovery?.statusReason;
|
|
1267
|
+
const processCrashRecoveryRequested = statusAtStart !== undefined;
|
|
1079
1268
|
const client = await this.getClientForFile(resolvedPath, {
|
|
1080
1269
|
recoverProcessCrash: true,
|
|
1081
1270
|
control,
|
|
1082
1271
|
});
|
|
1083
|
-
const processCrashRecovery =
|
|
1084
|
-
|
|
1085
|
-
|
|
1272
|
+
const processCrashRecovery = this.buildFileProcessCrashRecoveryReport(
|
|
1273
|
+
route,
|
|
1274
|
+
recovery,
|
|
1275
|
+
statusAtStart,
|
|
1276
|
+
client,
|
|
1277
|
+
);
|
|
1278
|
+
if (processCrashRecoveryRequested) onProcessCrashRecovery?.(processCrashRecovery);
|
|
1086
1279
|
if (!client) return { client: null, processCrashRecovery };
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1280
|
+
try {
|
|
1281
|
+
await client.getReady(control);
|
|
1282
|
+
await this.warmSemanticProject(client, resolvedPath, true, control);
|
|
1283
|
+
throwIfCodeRequestInterrupted(control);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
const failedRecovery = this.buildFileProcessCrashFailureReport(
|
|
1286
|
+
route,
|
|
1287
|
+
recovery,
|
|
1288
|
+
statusAtStart,
|
|
1289
|
+
error,
|
|
1290
|
+
);
|
|
1291
|
+
if (failedRecovery) {
|
|
1292
|
+
onProcessCrashRecovery?.(failedRecovery);
|
|
1293
|
+
return { client: null, processCrashRecovery: failedRecovery };
|
|
1294
|
+
}
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1092
1297
|
return { client, processCrashRecovery };
|
|
1093
1298
|
}
|
|
1094
1299
|
|
|
1300
|
+
private buildFileProcessCrashRecoveryReport(
|
|
1301
|
+
route: FileRoute | null,
|
|
1302
|
+
recovery: ProcessCrashRecoveryState | undefined,
|
|
1303
|
+
statusAtStart: ProcessCrashRecoveryState["statusReason"],
|
|
1304
|
+
client: LspClient | null,
|
|
1305
|
+
): ProcessCrashRecoveryReport {
|
|
1306
|
+
if (statusAtStart === undefined) return emptyProcessCrashRecoveryReport();
|
|
1307
|
+
return buildProcessCrashRecoveryReport(
|
|
1308
|
+
[
|
|
1309
|
+
{
|
|
1310
|
+
name: route?.serverName ?? "unknown",
|
|
1311
|
+
root: route?.root ?? this.cwd,
|
|
1312
|
+
outcome:
|
|
1313
|
+
statusAtStart === "process-crash-recovery-exhausted"
|
|
1314
|
+
? "recovery-exhausted"
|
|
1315
|
+
: client
|
|
1316
|
+
? "recovered"
|
|
1317
|
+
: "recovery-failed",
|
|
1318
|
+
...(client || !recovery?.failureMessage
|
|
1319
|
+
? {}
|
|
1320
|
+
: { failureMessage: recovery.failureMessage }),
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
this.cwd,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private buildFileProcessCrashFailureReport(
|
|
1328
|
+
route: FileRoute | null,
|
|
1329
|
+
recovery: ProcessCrashRecoveryState | undefined,
|
|
1330
|
+
statusAtStart: ProcessCrashRecoveryState["statusReason"],
|
|
1331
|
+
error: unknown,
|
|
1332
|
+
): ProcessCrashRecoveryReport | null {
|
|
1333
|
+
if (
|
|
1334
|
+
statusAtStart === undefined ||
|
|
1335
|
+
statusAtStart === "process-crash-recovery-exhausted" ||
|
|
1336
|
+
recovery?.statusReason !== "process-crash-recovery-exhausted"
|
|
1337
|
+
) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
return buildProcessCrashRecoveryReport(
|
|
1341
|
+
[
|
|
1342
|
+
{
|
|
1343
|
+
name: route?.serverName ?? "unknown",
|
|
1344
|
+
root: route?.root ?? this.cwd,
|
|
1345
|
+
outcome: "recovery-failed",
|
|
1346
|
+
...(error instanceof Error && error.message
|
|
1347
|
+
? { failureMessage: boundProcessCrashFailureMessage(error.message) }
|
|
1348
|
+
: recovery.failureMessage
|
|
1349
|
+
? { failureMessage: recovery.failureMessage }
|
|
1350
|
+
: {}),
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
this.cwd,
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1095
1357
|
/**
|
|
1096
1358
|
* Wait until one started client is query-ready, then warm its project.
|
|
1097
1359
|
* Returns the number of concrete clients that are ready after the warm-up.
|
|
@@ -1159,6 +1421,118 @@ export class LspManager {
|
|
|
1159
1421
|
return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
|
|
1160
1422
|
}
|
|
1161
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Track a bounded set of automatic source files with shared route starts.
|
|
1426
|
+
* Inputs after the first 256 unique paths are ignored.
|
|
1427
|
+
*/
|
|
1428
|
+
async bulkTrackFiles(
|
|
1429
|
+
filePaths: readonly string[],
|
|
1430
|
+
control?: CodeRequestControl,
|
|
1431
|
+
): Promise<BulkTrackFilesResult> {
|
|
1432
|
+
throwIfCodeRequestInterrupted(control);
|
|
1433
|
+
const selected = Array.from(
|
|
1434
|
+
new Set(filePaths.map((filePath) => resolveSessionPath(this.cwd, filePath))),
|
|
1435
|
+
).slice(0, MAX_BULK_TRACK_FILES);
|
|
1436
|
+
if (selected.length === 0) return { outcomes: [] };
|
|
1437
|
+
|
|
1438
|
+
this.trackedFilesBatchDepth++;
|
|
1439
|
+
const outcomes = new Array<BulkTrackFileOutcome>(selected.length);
|
|
1440
|
+
const workerCount = Math.min(MAX_CONCURRENT_BULK_TRACKS, selected.length);
|
|
1441
|
+
let nextIndex = 0;
|
|
1442
|
+
const worker = async (): Promise<void> => {
|
|
1443
|
+
for (;;) {
|
|
1444
|
+
throwIfCodeRequestInterrupted(control);
|
|
1445
|
+
const index = nextIndex++;
|
|
1446
|
+
if (index >= selected.length) return;
|
|
1447
|
+
outcomes[index] = await this.trackBulkFile(selected[index], control);
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
1451
|
+
try {
|
|
1452
|
+
await Promise.all(workers);
|
|
1453
|
+
return { outcomes };
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
// Let every worker observe cancellation before the batch transition is
|
|
1456
|
+
// published. Shared route startup may continue, but no worker may open
|
|
1457
|
+
// another document after this caller stops waiting.
|
|
1458
|
+
await Promise.allSettled(workers);
|
|
1459
|
+
throw error;
|
|
1460
|
+
} finally {
|
|
1461
|
+
this.trackedFilesBatchDepth--;
|
|
1462
|
+
if (this.trackedFilesBatchDepth === 0 && this.trackedFilesBatchChanged) {
|
|
1463
|
+
this.trackedFilesBatchChanged = false;
|
|
1464
|
+
this.publishLifecycle("tracked-files");
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private async trackBulkFile(
|
|
1470
|
+
filePath: string,
|
|
1471
|
+
control?: CodeRequestControl,
|
|
1472
|
+
): Promise<BulkTrackFileOutcome> {
|
|
1473
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1474
|
+
if (!this.isConfiguredAutomaticSourceFile(resolvedPath)) {
|
|
1475
|
+
return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
|
|
1476
|
+
}
|
|
1477
|
+
const fileStatus = this.inspectSourceFile(resolvedPath);
|
|
1478
|
+
if (fileStatus !== "regular") {
|
|
1479
|
+
if (fileStatus === "missing") {
|
|
1480
|
+
return { file: resolvedPath, kind: "unsupported", reason: "missing" };
|
|
1481
|
+
}
|
|
1482
|
+
if (fileStatus === "not-automatic-source") {
|
|
1483
|
+
return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
file: resolvedPath,
|
|
1487
|
+
kind: "unavailable",
|
|
1488
|
+
reason: "The source file could not be inspected.",
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
if (this.isFileTracked(resolvedPath)) {
|
|
1492
|
+
return { file: resolvedPath, kind: "already-tracked" };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
throwIfCodeRequestInterrupted(control);
|
|
1496
|
+
// Recheck the automatic source-support rule after the inventory scan. A
|
|
1497
|
+
// config, exclusion, or route can change while a refresh is in flight.
|
|
1498
|
+
if (!this.isSupportedSourceFile(resolvedPath)) {
|
|
1499
|
+
return {
|
|
1500
|
+
file: resolvedPath,
|
|
1501
|
+
kind: "unavailable",
|
|
1502
|
+
reason: "No available LSP route can serve this source file.",
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const client = await this.ensureFileOpen(resolvedPath, { control });
|
|
1507
|
+
if (!client) {
|
|
1508
|
+
return {
|
|
1509
|
+
file: resolvedPath,
|
|
1510
|
+
kind: "unavailable",
|
|
1511
|
+
reason: "The LSP route could not open this source file.",
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
return { file: resolvedPath, kind: "tracked" };
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
private isConfiguredAutomaticSourceFile(filePath: string): boolean {
|
|
1518
|
+
return this.isAutomaticScopePath(filePath) && getServerForFile(this.config, filePath) !== null;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
private inspectSourceFile(
|
|
1522
|
+
filePath: string,
|
|
1523
|
+
): "regular" | "missing" | "not-automatic-source" | "unavailable" {
|
|
1524
|
+
try {
|
|
1525
|
+
return fs.statSync(filePath).isFile() ? "regular" : "not-automatic-source";
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
if (isMissingFileError(error)) return "missing";
|
|
1528
|
+
return "unavailable";
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private isFileTracked(filePath: string): boolean {
|
|
1533
|
+
return this.getExistingClientForFile(filePath)?.openFiles.includes(filePath) ?? false;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1162
1536
|
/** Close a file across any active LSP clients and clear its cached diagnostics. */
|
|
1163
1537
|
closeFile(filePath: string): void {
|
|
1164
1538
|
closeFileAcrossClients(this.clients.values(), resolveSessionPath(this.cwd, filePath));
|
|
@@ -1245,18 +1619,26 @@ export class LspManager {
|
|
|
1245
1619
|
processCrashDemand?: { scopes?: readonly string[] };
|
|
1246
1620
|
control?: CodeRequestControl;
|
|
1247
1621
|
}): Promise<WorkspaceRecoveryResult> {
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1622
|
+
const processCrashDemand = options?.processCrashDemand;
|
|
1623
|
+
const demand = processCrashDemand
|
|
1624
|
+
? await this.recoverProcessCrashDemand(
|
|
1625
|
+
(snapshot) => {
|
|
1626
|
+
if (!this.processCrashDiagnosticRouteInScope(snapshot, processCrashDemand)) {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
return this.processCrashDiagnosticFiles(snapshot, processCrashDemand);
|
|
1630
|
+
},
|
|
1631
|
+
options?.control,
|
|
1632
|
+
{ skipWhenNoRetainedFile: true },
|
|
1633
|
+
)
|
|
1253
1634
|
: {
|
|
1254
1635
|
hasSupport: false,
|
|
1255
|
-
|
|
1636
|
+
requiresFreshEvidence: false,
|
|
1637
|
+
processCrashRecovery: emptyProcessCrashRecoveryReport(),
|
|
1256
1638
|
failures: [],
|
|
1257
1639
|
failedFiles: [],
|
|
1258
1640
|
};
|
|
1259
|
-
const recoveryOptions = demand.
|
|
1641
|
+
const recoveryOptions = demand.requiresFreshEvidence
|
|
1260
1642
|
? { ...options, initialEvidence: undefined }
|
|
1261
1643
|
: options;
|
|
1262
1644
|
const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
|
|
@@ -1266,21 +1648,34 @@ export class LspManager {
|
|
|
1266
1648
|
);
|
|
1267
1649
|
}
|
|
1268
1650
|
|
|
1651
|
+
private processCrashDiagnosticRouteInScope(
|
|
1652
|
+
snapshot: ProcessCrashRecoverySnapshot,
|
|
1653
|
+
demand: { scopes?: readonly string[] },
|
|
1654
|
+
): boolean {
|
|
1655
|
+
return this.processCrashDiagnosticScopes(demand).some(
|
|
1656
|
+
(scope) =>
|
|
1657
|
+
projectRoots.isWithinOrEqual(scope, snapshot.root) ||
|
|
1658
|
+
projectRoots.isWithinOrEqual(snapshot.root, scope),
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1269
1662
|
private processCrashDiagnosticFiles(
|
|
1270
|
-
|
|
1271
|
-
demand: { scopes?: readonly string[] }
|
|
1663
|
+
snapshot: ProcessCrashRecoverySnapshot,
|
|
1664
|
+
demand: { scopes?: readonly string[] },
|
|
1272
1665
|
): string[] {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
return recovery.files.filter(
|
|
1666
|
+
const scopes = this.processCrashDiagnosticScopes(demand);
|
|
1667
|
+
return snapshot.files.filter(
|
|
1276
1668
|
(file) =>
|
|
1277
1669
|
this.isDiagnosticFile(file) &&
|
|
1278
|
-
(
|
|
1279
|
-
scopes.length === 0 ||
|
|
1280
|
-
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file))),
|
|
1670
|
+
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file)),
|
|
1281
1671
|
);
|
|
1282
1672
|
}
|
|
1283
1673
|
|
|
1674
|
+
private processCrashDiagnosticScopes(demand: { scopes?: readonly string[] }): string[] {
|
|
1675
|
+
const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1676
|
+
return scopes && scopes.length > 0 ? scopes : [this.cwd];
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1284
1679
|
private addProcessCrashDiagnosticEvidence(
|
|
1285
1680
|
result: WorkspaceRecoveryResult,
|
|
1286
1681
|
demand: ProcessCrashDemandResult,
|
|
@@ -1665,9 +2060,9 @@ export class LspManager {
|
|
|
1665
2060
|
);
|
|
1666
2061
|
}
|
|
1667
2062
|
const demand = await this.recoverProcessCrashDemand(
|
|
1668
|
-
(
|
|
1669
|
-
|
|
1670
|
-
routeMatches(
|
|
2063
|
+
(snapshot) =>
|
|
2064
|
+
snapshot.failedClient.serverCapabilities?.workspaceSymbolProvider &&
|
|
2065
|
+
routeMatches(snapshot.failedClient)
|
|
1671
2066
|
? []
|
|
1672
2067
|
: null,
|
|
1673
2068
|
control,
|
|
@@ -1745,9 +2140,14 @@ export class LspManager {
|
|
|
1745
2140
|
const client = await this.getClientForFile(resolvedPath, options);
|
|
1746
2141
|
if (!client) return null;
|
|
1747
2142
|
try {
|
|
1748
|
-
|
|
2143
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
2144
|
+
// Route startup can be shared. The document open belongs to this
|
|
2145
|
+
// caller, so check again after the shared wait before sending didOpen.
|
|
2146
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
2147
|
+
client.didOpen(resolvedPath, content);
|
|
1749
2148
|
return client;
|
|
1750
|
-
} catch {
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (isCodeRequestInterruption(error, options.control)) throw error;
|
|
1751
2151
|
this.closeFile(resolvedPath);
|
|
1752
2152
|
return null;
|
|
1753
2153
|
}
|