@mrclrchtr/supi-code-intelligence 6.0.1 → 6.2.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 -2
- 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/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +26 -4
- 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/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- 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 +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +14 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +23 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
- package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
- package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -29
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +12 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +849 -117
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +6 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +118 -27
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +7 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +33 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
- 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/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
- 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/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
- package/package.json +5 -5
- package/src/analysis/capability/capability-warnings.ts +4 -22
- 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/search/ast-scan-timing.ts +4 -2
- package/src/analysis/search/ast-scan.ts +1 -1
- package/src/analysis/target/symbol.ts +28 -16
- package/src/analysis/target/types.ts +1 -0
- package/src/api.ts +1 -0
- package/src/extension.ts +4 -2
- package/src/session/find-workflow.ts +5 -12
- package/src/session/health-refresh.ts +30 -4
- package/src/session/health-types.ts +27 -2
- package/src/session/health-workflow.ts +5 -1
- package/src/session/input/health-refactor.ts +16 -14
- package/src/session/refactor-types.ts +14 -1
- package/src/session/refactor-workflow.ts +21 -16
- package/src/session/semantic-demand.ts +36 -0
- package/src/session/target-workflow.ts +9 -12
- package/src/substrate/lsp/lifecycle.ts +0 -3
- package/src/substrate/lsp/maintenance.ts +4 -5
- package/src/substrate/lsp/settings.ts +6 -9
- package/src/substrate/lsp/state.ts +0 -2
- package/src/substrate/workspace-provider-host.ts +2 -2
- package/src/tool/code_health/file-scope-markdown.ts +43 -0
- package/src/tool/code_health/guidance.ts +2 -2
- package/src/tool/code_health/markdown.ts +81 -69
- package/src/tool/code_health/refresh-outcome.ts +46 -0
- package/src/tool/code_health/refresh-status.ts +166 -23
- package/src/tool/code_health/result.ts +15 -5
- package/src/tool/code_health/spec.ts +2 -1
- package/src/tool/code_health/tui.ts +42 -8
- package/src/tool/code_refactor_apply/guidance.ts +3 -2
- package/src/tool/code_refactor_plan/guidance.ts +1 -1
- package/src/tool/register.ts +5 -2
- package/src/tool/result/errors.ts +1 -0
- package/src/tool/result/types.ts +6 -0
- package/src/tool/schemas.ts +21 -11
- package/src/ui/footer.ts +7 -5
- package/src/ui/status-command.ts +31 -0
- package/src/ui/status-overlay.ts +69 -23
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
- package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
|
@@ -21,18 +21,31 @@ 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 {
|
|
25
|
+
recordDebugEvent,
|
|
26
|
+
truncateDebugIdentity as truncateIdentity,
|
|
27
|
+
} from "@mrclrchtr/supi-core/debug";
|
|
28
|
+
import { resolveToolPath as resolveSessionPath, uriToFile } from "@mrclrchtr/supi-core/path";
|
|
24
29
|
import { getServerForFile } from "../config/config.ts";
|
|
25
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getFileScopeDecision,
|
|
32
|
+
invalidateTsconfigCacheForConfig,
|
|
33
|
+
invalidateTsconfigCacheForConfigDir,
|
|
34
|
+
isProjectConfigFileName,
|
|
35
|
+
} from "../config/tsconfig-scope.ts";
|
|
26
36
|
import type {
|
|
27
37
|
DetectedProjectServer,
|
|
28
38
|
Diagnostic,
|
|
29
39
|
FileEvent,
|
|
30
40
|
LspConfig,
|
|
31
41
|
ProjectServerInfo,
|
|
42
|
+
ProjectServerStatusReason,
|
|
32
43
|
ServerConfig,
|
|
33
44
|
SymbolInformation,
|
|
34
45
|
WorkspaceSymbol,
|
|
35
46
|
} from "../config/types.ts";
|
|
47
|
+
import { FileChangeType } from "../config/types.ts";
|
|
48
|
+
import { boundCwd } from "../debug-telemetry.ts";
|
|
36
49
|
import {
|
|
37
50
|
accumulateOutstandingDiagnostics,
|
|
38
51
|
collectDiagnosticSummaryCounts,
|
|
@@ -44,11 +57,19 @@ import {
|
|
|
44
57
|
type DiagnosticEvidenceSummary,
|
|
45
58
|
summarizeDiagnosticEvidence,
|
|
46
59
|
} from "../diagnostics/evidence.ts";
|
|
60
|
+
import {
|
|
61
|
+
scanWorkspaceSentinels as scanAutomaticWorkspaceSentinels,
|
|
62
|
+
syncWorkspaceSentinelSnapshot as syncAutomaticWorkspaceSentinelSnapshot,
|
|
63
|
+
type WorkspaceSentinelScanOptions,
|
|
64
|
+
type WorkspaceSentinelSyncResult,
|
|
65
|
+
} from "../diagnostics/workspace-sentinels.ts";
|
|
47
66
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
48
|
-
import
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
import {
|
|
68
|
+
emptyProcessCrashRecoverySummary,
|
|
69
|
+
type ProcessCrashRecoverySummary,
|
|
70
|
+
type ScopeDecisionEntry,
|
|
71
|
+
type ScopeDecisionSummary,
|
|
72
|
+
type WorkspaceDiagnosticReport,
|
|
52
73
|
} from "../session/runtime-diagnostics.ts";
|
|
53
74
|
import {
|
|
54
75
|
displayRelativeFilePath,
|
|
@@ -58,7 +79,11 @@ import {
|
|
|
58
79
|
normalizeRelevantPaths,
|
|
59
80
|
shouldIgnoreLspPath,
|
|
60
81
|
} from "../summary.ts";
|
|
61
|
-
import { commandExists
|
|
82
|
+
import { commandExists } from "../utils.ts";
|
|
83
|
+
import {
|
|
84
|
+
type AutomaticLspPathPolicy,
|
|
85
|
+
createDefaultAutomaticLspPathPolicy,
|
|
86
|
+
} from "../workspace-path-policy.ts";
|
|
62
87
|
import {
|
|
63
88
|
closeFileAcrossClients,
|
|
64
89
|
pruneMissingFilesFromClients,
|
|
@@ -68,12 +93,7 @@ import {
|
|
|
68
93
|
collectOutstandingDiagnosticsDetailed,
|
|
69
94
|
syncClientFileAndGetDiagnostics,
|
|
70
95
|
} from "./manager-diagnostics.ts";
|
|
71
|
-
import {
|
|
72
|
-
clientKey,
|
|
73
|
-
isExcludedByPattern,
|
|
74
|
-
rememberKnownRoot,
|
|
75
|
-
resolveRootForFile,
|
|
76
|
-
} from "./manager-helpers.ts";
|
|
96
|
+
import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-helpers.ts";
|
|
77
97
|
import { buildProjectServerInfo } from "./manager-project-info.ts";
|
|
78
98
|
import type {
|
|
79
99
|
ActiveCoverageSummaryEntry,
|
|
@@ -151,6 +171,66 @@ export interface ManagerLifecycleTransition {
|
|
|
151
171
|
|
|
152
172
|
type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
|
|
153
173
|
|
|
174
|
+
type FileClientRequestOptions = {
|
|
175
|
+
/** Allow this request to start the route's one process-crash recovery. */
|
|
176
|
+
recoverProcessCrash?: boolean;
|
|
177
|
+
/** Control only this caller's wait for a shared replacement. */
|
|
178
|
+
control?: CodeRequestControl;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
interface FileReadinessResult {
|
|
182
|
+
client: LspClient | null;
|
|
183
|
+
processCrashRecovery: ProcessCrashRecoverySummary;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ProcessCrashRecoveryState {
|
|
187
|
+
/** The one automatic attempt is consumed when a replacement starts. */
|
|
188
|
+
attemptConsumed: boolean;
|
|
189
|
+
/** Current user-facing reason, or undefined after successful recovery. */
|
|
190
|
+
statusReason: ProjectServerStatusReason | undefined;
|
|
191
|
+
/** The failed generation whose tracked files must be restored. */
|
|
192
|
+
failedClient: LspClient;
|
|
193
|
+
/** Files tracked by the failed route at the time of the crash. */
|
|
194
|
+
files: string[];
|
|
195
|
+
/** Shared replacement startup and document-restoration operation. */
|
|
196
|
+
pending: Promise<LspClient | null> | null;
|
|
197
|
+
/** Start time for the current replacement attempt, when one exists. */
|
|
198
|
+
attemptStartedAt?: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhausted" | "cancelled";
|
|
202
|
+
|
|
203
|
+
type ProcessCrashDemandResult = {
|
|
204
|
+
/** Whether at least one required crashed route supports the operation. */
|
|
205
|
+
hasSupport: boolean;
|
|
206
|
+
/** Separate route-level result for this explicit process-crash demand. */
|
|
207
|
+
processCrashRecovery: ProcessCrashRecoverySummary;
|
|
208
|
+
/** Required routes that remain unavailable after shared recovery settles. */
|
|
209
|
+
failures: string[];
|
|
210
|
+
/** In-scope tracked files from required routes that remain unavailable. */
|
|
211
|
+
failedFiles: string[];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type RequiredProcessCrashDemand = {
|
|
215
|
+
key: string;
|
|
216
|
+
recovery: ProcessCrashRecoveryState;
|
|
217
|
+
failedFiles: readonly string[];
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type ProcessCrashRecoveryRouteOutcome = "pending" | "recovered" | "failed";
|
|
221
|
+
|
|
222
|
+
function summarizeProcessCrashRecoveryRoutes(
|
|
223
|
+
outcomes: Iterable<ProcessCrashRecoveryRouteOutcome>,
|
|
224
|
+
): ProcessCrashRecoverySummary {
|
|
225
|
+
const summary = emptyProcessCrashRecoverySummary();
|
|
226
|
+
for (const outcome of outcomes) {
|
|
227
|
+
summary.attemptedRoutes++;
|
|
228
|
+
if (outcome === "recovered") summary.recoveredRoutes++;
|
|
229
|
+
if (outcome === "failed") summary.failedRoutes++;
|
|
230
|
+
}
|
|
231
|
+
return summary;
|
|
232
|
+
}
|
|
233
|
+
|
|
154
234
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
155
235
|
export class LspManager {
|
|
156
236
|
/** Active clients keyed by "serverName:root" */
|
|
@@ -159,14 +239,14 @@ export class LspManager {
|
|
|
159
239
|
private unavailable = new Map<string, UnavailableReason>();
|
|
160
240
|
/** Memoized per-command availability of LSP server binaries on PATH */
|
|
161
241
|
private commandAvailability = new Map<string, boolean>();
|
|
162
|
-
/** Guards against concurrent client creation for the same server:root key */
|
|
242
|
+
/** Guards against concurrent client creation for the same server:root key. */
|
|
163
243
|
private pendingStarts = new Map<string, Promise<LspClient | null>>();
|
|
244
|
+
/** Shared diagnostic-recovery restart for each replaceable client route. */
|
|
245
|
+
private pendingRestarts = new Map<string, Promise<{ files: string[]; restarted: boolean }>>();
|
|
164
246
|
/** Monotonic ownership generation for each replaceable client route. */
|
|
165
247
|
private clientGenerations = new Map<string, number>();
|
|
166
248
|
/** Preferred project roots discovered by proactive scan or lazy startup */
|
|
167
249
|
private knownRoots = new Map<string, string[]>();
|
|
168
|
-
/** User-configured gitignore-style exclude patterns */
|
|
169
|
-
private excludePatterns: string[] = [];
|
|
170
250
|
/** Monotonic workspace invalidation generation advanced by each invalidation event. */
|
|
171
251
|
private invalidationEpoch = 0;
|
|
172
252
|
/** Invalidation generation in which each route last received a recovery restart. */
|
|
@@ -177,21 +257,31 @@ export class LspManager {
|
|
|
177
257
|
private warmedSemanticProjects = new Set<string>();
|
|
178
258
|
/** In-flight warm-up probes keyed by project key so concurrent callers share one probe. */
|
|
179
259
|
private pendingWarmProbes = new Map<string, Promise<void>>();
|
|
260
|
+
/** One process-crash recovery budget and shared replacement per route. */
|
|
261
|
+
private processCrashRecoveries = new Map<string, ProcessCrashRecoveryState>();
|
|
262
|
+
/** Client generations that completed the initialize handshake. */
|
|
263
|
+
private initializedClientGenerations = new Map<string, number>();
|
|
264
|
+
/** Client generations whose process-failure fact was already handled. */
|
|
265
|
+
private handledCrashGenerations = new Map<string, number>();
|
|
266
|
+
/** Prevent late startup and lifecycle callbacks from republishing after shutdown. */
|
|
267
|
+
private shuttingDown = false;
|
|
180
268
|
constructor(
|
|
181
269
|
private readonly config: LspConfig,
|
|
182
270
|
private readonly cwd: string,
|
|
183
271
|
private readonly onLifecycleTransition?: ManagerLifecycleListener,
|
|
272
|
+
private readonly automaticPathPolicy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(
|
|
273
|
+
cwd,
|
|
274
|
+
),
|
|
184
275
|
) {}
|
|
185
276
|
getCwd(): string {
|
|
186
277
|
return this.cwd;
|
|
187
278
|
}
|
|
188
|
-
setExcludePatterns(patterns: string[]): void {
|
|
189
|
-
this.excludePatterns = patterns;
|
|
190
|
-
}
|
|
191
279
|
|
|
192
280
|
// ── Public API ────────────────────────────────────────────────────
|
|
193
281
|
registerDetectedServers(detected: DetectedProjectServer[]): void {
|
|
194
|
-
this.knownRoots = projectRoots.buildKnownRootsMap(
|
|
282
|
+
this.knownRoots = projectRoots.buildKnownRootsMap(
|
|
283
|
+
detected.filter((entry) => this.automaticPathPolicy.isEligible(entry.root, "directory")),
|
|
284
|
+
);
|
|
195
285
|
}
|
|
196
286
|
/** Check whether a file path has an available LSP server for explicit semantic operations. */
|
|
197
287
|
canServeFile(filePath: string): boolean {
|
|
@@ -202,18 +292,56 @@ export class LspManager {
|
|
|
202
292
|
}
|
|
203
293
|
|
|
204
294
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* and
|
|
295
|
+
* Return the consumed process-crash outcome for a file readiness result.
|
|
296
|
+
* The result supports unavailable and non-resolved waits; it is null before
|
|
297
|
+
* an attempt is consumed and after successful recovery, so old success is
|
|
298
|
+
* not reported as a new outcome. This is a passive read and never starts
|
|
299
|
+
* recovery.
|
|
300
|
+
*/
|
|
301
|
+
getProcessCrashRecoverySummaryForFile(filePath: string): ProcessCrashRecoverySummary | null {
|
|
302
|
+
const route = this.resolveFileRoute(resolveSessionPath(this.cwd, filePath));
|
|
303
|
+
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
304
|
+
if (!recovery?.attemptConsumed || recovery.statusReason === undefined) return null;
|
|
305
|
+
|
|
306
|
+
const outcome: ProcessCrashRecoveryRouteOutcome =
|
|
307
|
+
recovery.statusReason === "process-crash-recovery-exhausted" ? "failed" : "pending";
|
|
308
|
+
return summarizeProcessCrashRecoveryRoutes([outcome]);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check whether a file should participate in automatic LSP work.
|
|
313
|
+
* This is stricter than {@link canServeFile} and applies the workspace path
|
|
314
|
+
* policy plus tsconfig diagnostic suppression.
|
|
208
315
|
*/
|
|
209
316
|
isSupportedSourceFile(filePath: string): boolean {
|
|
210
|
-
|
|
211
|
-
// tracking and diagnostic summaries (shouldIgnoreLspPath). Keep runtime
|
|
212
|
-
// guidance activation consistent: reading or editing a file under
|
|
213
|
-
// node_modules / .pnpm must not arm LSP guidance for dependency sources.
|
|
214
|
-
if (shouldIgnoreLspPath(filePath, this.cwd)) return false;
|
|
317
|
+
if (!this.isAutomaticScopePath(filePath)) return false;
|
|
215
318
|
return this.canServeFile(filePath);
|
|
216
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Inventory automatic workspace sentinel and source paths with the runtime policy. */
|
|
322
|
+
scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
|
|
323
|
+
return scanAutomaticWorkspaceSentinels(this.cwd, {
|
|
324
|
+
...options,
|
|
325
|
+
policy: this.automaticPathPolicy,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Refresh the automatic workspace inventory with the runtime policy. */
|
|
330
|
+
syncWorkspaceSentinelSnapshot(
|
|
331
|
+
previous: Map<string, number>,
|
|
332
|
+
options: WorkspaceSentinelScanOptions = {},
|
|
333
|
+
): WorkspaceSentinelSyncResult {
|
|
334
|
+
return syncAutomaticWorkspaceSentinelSnapshot(this.cwd, previous, {
|
|
335
|
+
...options,
|
|
336
|
+
policy: this.automaticPathPolicy,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Apply automatic path exclusions plus diagnostic tsconfig suppression. */
|
|
341
|
+
private isAutomaticScopePath(filePath: string): boolean {
|
|
342
|
+
return !shouldIgnoreLspPath(filePath, this.cwd, this.automaticPathPolicy);
|
|
343
|
+
}
|
|
344
|
+
|
|
217
345
|
private isServerCommandAvailable(command: string): boolean {
|
|
218
346
|
// Only memoize positive lookups. A negative result may become stale if the
|
|
219
347
|
// user installs the binary mid-session (e.g. `mise install`), and
|
|
@@ -247,11 +375,27 @@ export class LspManager {
|
|
|
247
375
|
}
|
|
248
376
|
|
|
249
377
|
/** Get or create an LSP client for the given file. */
|
|
250
|
-
async getClientForFile(
|
|
378
|
+
async getClientForFile(
|
|
379
|
+
filePath: string,
|
|
380
|
+
options: FileClientRequestOptions = {},
|
|
381
|
+
): Promise<LspClient | null> {
|
|
382
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
383
|
+
if (this.shuttingDown) return null;
|
|
251
384
|
const route = this.resolveFileRoute(filePath);
|
|
252
|
-
|
|
385
|
+
if (!route) return null;
|
|
386
|
+
|
|
387
|
+
const recovery = this.processCrashRecoveries.get(route.key);
|
|
388
|
+
if (recovery && (recovery.pending || recovery.statusReason)) {
|
|
389
|
+
if (!options.recoverProcessCrash) return null;
|
|
390
|
+
return this.acquireProcessCrashReplacement(recovery, route, options.control);
|
|
391
|
+
}
|
|
392
|
+
// A recovered route keeps its consumed budget but has no active reason.
|
|
393
|
+
// Passive callers can use its running client normally.
|
|
394
|
+
|
|
395
|
+
return this.startServerForRoot(route.serverName, route.root);
|
|
253
396
|
}
|
|
254
397
|
async startServerForRoot(serverName: string, root: string): Promise<LspClient | null> {
|
|
398
|
+
if (this.shuttingDown) return null;
|
|
255
399
|
const serverConfig = this.config.servers[serverName];
|
|
256
400
|
if (!serverConfig) return null;
|
|
257
401
|
const key = clientKey(serverName, root);
|
|
@@ -261,6 +405,12 @@ export class LspManager {
|
|
|
261
405
|
const existing = this.clients.get(key);
|
|
262
406
|
if (existing && existing.status === "running") return existing;
|
|
263
407
|
|
|
408
|
+
// A crashed route is retained until evidence demand starts its shared
|
|
409
|
+
// replacement. Proactive starts must not consume that budget.
|
|
410
|
+
if (existing && existing.status === "error" && this.processCrashRecoveries.has(key)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
264
414
|
// If existing client errored, remove it
|
|
265
415
|
if (existing && existing.status === "error") {
|
|
266
416
|
this.clients.delete(key);
|
|
@@ -315,13 +465,353 @@ export class LspManager {
|
|
|
315
465
|
client: LspClient,
|
|
316
466
|
kind: LspClientLifecycleTransitionKind,
|
|
317
467
|
): void {
|
|
468
|
+
if (this.shuttingDown) return;
|
|
318
469
|
if (this.clientGenerations.get(key) !== generation) return;
|
|
319
470
|
if (this.clients.get(key) !== client) return;
|
|
471
|
+
|
|
472
|
+
if (kind === "startup") {
|
|
473
|
+
this.initializedClientGenerations.set(key, generation);
|
|
474
|
+
}
|
|
475
|
+
if (kind === "crash") {
|
|
476
|
+
if (this.handledCrashGenerations.get(key) === generation) return;
|
|
477
|
+
this.handledCrashGenerations.set(key, generation);
|
|
478
|
+
if (this.initializedClientGenerations.get(key) === generation) {
|
|
479
|
+
this.handleInitializedClientCrash(key, client);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
320
483
|
const aggregateKind =
|
|
321
484
|
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
322
485
|
this.publishLifecycle(aggregateKind);
|
|
323
486
|
}
|
|
324
487
|
|
|
488
|
+
private handleInitializedClientCrash(key: string, client: LspClient): void {
|
|
489
|
+
const existing = this.processCrashRecoveries.get(key);
|
|
490
|
+
if (existing?.attemptConsumed) {
|
|
491
|
+
existing.failedClient = client;
|
|
492
|
+
existing.files = this.trackedFilesForClient(client);
|
|
493
|
+
this.unavailable.set(key, "runtime-error");
|
|
494
|
+
if (existing.statusReason !== "process-crash-recovery-exhausted") {
|
|
495
|
+
existing.statusReason = "process-crash-recovery-exhausted";
|
|
496
|
+
this.recordProcessCrashRecoveryEvent(
|
|
497
|
+
client.name,
|
|
498
|
+
client.root,
|
|
499
|
+
"exhausted",
|
|
500
|
+
existing.pending ? existing.attemptStartedAt : Date.now(),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.processCrashRecoveries.set(key, {
|
|
507
|
+
attemptConsumed: false,
|
|
508
|
+
statusReason: "process-crashed",
|
|
509
|
+
failedClient: client,
|
|
510
|
+
files: this.trackedFilesForClient(client),
|
|
511
|
+
pending: null,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private trackedFilesForClient(client: LspClient): string[] {
|
|
516
|
+
const files = new Set(client.openFiles);
|
|
517
|
+
for (const document of client.getDiagnosticSnapshot().documents) {
|
|
518
|
+
files.add(uriToFile(document.uri));
|
|
519
|
+
}
|
|
520
|
+
return Array.from(files);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private waitForProcessCrashRecovery(
|
|
524
|
+
recovery: ProcessCrashRecoveryState,
|
|
525
|
+
control?: CodeRequestControl,
|
|
526
|
+
): Promise<LspClient | null> {
|
|
527
|
+
if (!recovery.pending) return Promise.resolve(null);
|
|
528
|
+
return raceRequestControl(recovery.pending, control);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private acquireProcessCrashReplacement(
|
|
532
|
+
recovery: ProcessCrashRecoveryState,
|
|
533
|
+
route: FileRoute,
|
|
534
|
+
control?: CodeRequestControl,
|
|
535
|
+
): Promise<LspClient | null> {
|
|
536
|
+
if (recovery.pending) return this.waitForProcessCrashRecovery(recovery, control);
|
|
537
|
+
if (
|
|
538
|
+
recovery.statusReason === "process-crashed" &&
|
|
539
|
+
!recovery.attemptConsumed &&
|
|
540
|
+
!this.shuttingDown
|
|
541
|
+
) {
|
|
542
|
+
return this.startProcessCrashRecovery(recovery, route, control);
|
|
543
|
+
}
|
|
544
|
+
return Promise.resolve(null);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Recover all crashed routes selected by one evidence-producing operation.
|
|
549
|
+
* Each route keeps its independent attempt budget and shared replacement.
|
|
550
|
+
*/
|
|
551
|
+
private async recoverProcessCrashDemand(
|
|
552
|
+
selectFiles: (recovery: ProcessCrashRecoveryState) => readonly string[] | null,
|
|
553
|
+
control?: CodeRequestControl,
|
|
554
|
+
): Promise<ProcessCrashDemandResult> {
|
|
555
|
+
const required = Array.from(this.processCrashRecoveries.entries()).flatMap(
|
|
556
|
+
([key, recovery]): RequiredProcessCrashDemand[] => {
|
|
557
|
+
if (recovery.statusReason === undefined) return [];
|
|
558
|
+
const failedFiles = selectFiles(recovery);
|
|
559
|
+
return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
if (required.length === 0) {
|
|
563
|
+
return {
|
|
564
|
+
hasSupport: false,
|
|
565
|
+
processCrashRecovery: emptyProcessCrashRecoverySummary(),
|
|
566
|
+
failures: [],
|
|
567
|
+
failedFiles: [],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
await Promise.all(
|
|
572
|
+
required.map(async ({ key, recovery }) => {
|
|
573
|
+
const serverConfig = this.config.servers[recovery.failedClient.name];
|
|
574
|
+
if (!serverConfig) return;
|
|
575
|
+
await this.acquireProcessCrashReplacement(
|
|
576
|
+
recovery,
|
|
577
|
+
{
|
|
578
|
+
serverName: recovery.failedClient.name,
|
|
579
|
+
serverConfig,
|
|
580
|
+
root: recovery.failedClient.root,
|
|
581
|
+
key,
|
|
582
|
+
},
|
|
583
|
+
control,
|
|
584
|
+
);
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const failedDemands = required.filter(({ recovery }) => recovery.statusReason !== undefined);
|
|
589
|
+
const outcomes = required.map(
|
|
590
|
+
({ recovery }): ProcessCrashRecoveryRouteOutcome =>
|
|
591
|
+
recovery.statusReason === undefined ? "recovered" : "failed",
|
|
592
|
+
);
|
|
593
|
+
return {
|
|
594
|
+
hasSupport: true,
|
|
595
|
+
processCrashRecovery: summarizeProcessCrashRecoveryRoutes(outcomes),
|
|
596
|
+
failures: failedDemands.map(({ recovery }) => this.formatProcessCrashDemandFailure(recovery)),
|
|
597
|
+
failedFiles: Array.from(new Set(failedDemands.flatMap(({ failedFiles }) => failedFiles))),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private formatProcessCrashDemandFailure(recovery: ProcessCrashRecoveryState): string {
|
|
602
|
+
const root = (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
|
|
603
|
+
const reason =
|
|
604
|
+
recovery.statusReason === "process-crash-recovery-exhausted"
|
|
605
|
+
? "process recovery exhausted; reload required"
|
|
606
|
+
: recovery.statusReason === "process-crash-recovery-pending"
|
|
607
|
+
? "process recovery in progress"
|
|
608
|
+
: "process crashed";
|
|
609
|
+
return `${recovery.failedClient.name} @ ${root} is unavailable — ${reason}.`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private startProcessCrashRecovery(
|
|
613
|
+
recovery: ProcessCrashRecoveryState,
|
|
614
|
+
route: FileRoute,
|
|
615
|
+
control?: CodeRequestControl,
|
|
616
|
+
): Promise<LspClient | null> {
|
|
617
|
+
throwIfCodeRequestInterrupted(control);
|
|
618
|
+
recovery.attemptConsumed = true;
|
|
619
|
+
recovery.statusReason = "process-crash-recovery-pending";
|
|
620
|
+
recovery.attemptStartedAt = Date.now();
|
|
621
|
+
const pending = this.performProcessCrashRecovery(recovery, route);
|
|
622
|
+
recovery.pending = pending;
|
|
623
|
+
// A cancelled caller may be the only current waiter. Keep the shared
|
|
624
|
+
// replacement promise observed until it settles without adopting that
|
|
625
|
+
// caller's cancellation or deadline.
|
|
626
|
+
pending.catch(() => {});
|
|
627
|
+
this.recordProcessCrashRecoveryEvent(
|
|
628
|
+
route.serverName,
|
|
629
|
+
route.root,
|
|
630
|
+
"attempt",
|
|
631
|
+
recovery.attemptStartedAt,
|
|
632
|
+
);
|
|
633
|
+
this.publishLifecycle("recovery");
|
|
634
|
+
return raceRequestControl(pending, control);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private async performProcessCrashRecovery(
|
|
638
|
+
recovery: ProcessCrashRecoveryState,
|
|
639
|
+
route: FileRoute,
|
|
640
|
+
): Promise<LspClient | null> {
|
|
641
|
+
const key = route.key;
|
|
642
|
+
const attemptStartedAt = recovery.attemptStartedAt;
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
// A process-error event can leave the child alive. Stop the old tree
|
|
646
|
+
// before installing its replacement, but retain its captured evidence.
|
|
647
|
+
await recovery.failedClient.forceKill().catch(() => {});
|
|
648
|
+
if (this.isProcessCrashRecoveryCancelled(recovery, key)) {
|
|
649
|
+
this.recordProcessCrashRecoveryEvent(
|
|
650
|
+
route.serverName,
|
|
651
|
+
route.root,
|
|
652
|
+
"cancelled",
|
|
653
|
+
attemptStartedAt,
|
|
654
|
+
);
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const replacement = this.installProcessCrashReplacement(recovery, route);
|
|
659
|
+
const started = await this.startProcessCrashReplacement(recovery, route, replacement);
|
|
660
|
+
if (!started) return null;
|
|
661
|
+
|
|
662
|
+
// A replacement can crash after initialize and before this operation
|
|
663
|
+
// resumes. Its lifecycle callback has already exhausted the route.
|
|
664
|
+
if (
|
|
665
|
+
started.status !== "running" ||
|
|
666
|
+
recovery.statusReason === "process-crash-recovery-exhausted"
|
|
667
|
+
) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
this.unavailable.delete(key);
|
|
672
|
+
recovery.statusReason = undefined;
|
|
673
|
+
this.publishLifecycle("recovery");
|
|
674
|
+
this.restoreTrackedDocuments(started, recovery.files);
|
|
675
|
+
this.recordProcessCrashRecoveryEvent(
|
|
676
|
+
route.serverName,
|
|
677
|
+
route.root,
|
|
678
|
+
"success",
|
|
679
|
+
attemptStartedAt,
|
|
680
|
+
);
|
|
681
|
+
return started;
|
|
682
|
+
} finally {
|
|
683
|
+
if (this.processCrashRecoveries.get(key) === recovery) recovery.pending = null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private isProcessCrashRecoveryCancelled(
|
|
688
|
+
recovery: ProcessCrashRecoveryState,
|
|
689
|
+
key: string,
|
|
690
|
+
): boolean {
|
|
691
|
+
return this.shuttingDown || this.processCrashRecoveries.get(key) !== recovery;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private installProcessCrashReplacement(
|
|
695
|
+
recovery: ProcessCrashRecoveryState,
|
|
696
|
+
route: FileRoute,
|
|
697
|
+
): LspClient {
|
|
698
|
+
const key = route.key;
|
|
699
|
+
if (this.clients.get(key) === recovery.failedClient) this.clients.delete(key);
|
|
700
|
+
this.clearWarmedWorkspaceSymbolProjects(route.serverName, route.root);
|
|
701
|
+
this.clearWarmedSemanticProjects(route.serverName, route.root);
|
|
702
|
+
this.clearPendingWarmProbes(route.serverName, route.root);
|
|
703
|
+
|
|
704
|
+
const replacement = this.createClient(route.serverName, route.serverConfig, route.root, key);
|
|
705
|
+
this.clients.set(key, replacement);
|
|
706
|
+
rememberKnownRoot(this.knownRoots, route.serverName, route.root);
|
|
707
|
+
return replacement;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async startProcessCrashReplacement(
|
|
711
|
+
recovery: ProcessCrashRecoveryState,
|
|
712
|
+
route: FileRoute,
|
|
713
|
+
replacement: LspClient,
|
|
714
|
+
): Promise<LspClient | null> {
|
|
715
|
+
try {
|
|
716
|
+
// LspClient's initialize request already has the 30-second transport
|
|
717
|
+
// bound. Do not apply the 5-second diagnostic-restart bound here.
|
|
718
|
+
await replacement.start();
|
|
719
|
+
} catch {
|
|
720
|
+
if (this.isProcessCrashRecoveryCancelled(recovery, route.key)) {
|
|
721
|
+
await replacement.forceKill().catch(() => {});
|
|
722
|
+
this.recordProcessCrashRecoveryEvent(
|
|
723
|
+
route.serverName,
|
|
724
|
+
route.root,
|
|
725
|
+
"cancelled",
|
|
726
|
+
recovery.attemptStartedAt,
|
|
727
|
+
);
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
if (recovery.statusReason !== "process-crash-recovery-exhausted") {
|
|
731
|
+
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement);
|
|
732
|
+
this.recordProcessCrashRecoveryEvent(
|
|
733
|
+
route.serverName,
|
|
734
|
+
route.root,
|
|
735
|
+
"failure",
|
|
736
|
+
recovery.attemptStartedAt,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (
|
|
743
|
+
this.isProcessCrashRecoveryCancelled(recovery, route.key) ||
|
|
744
|
+
this.clients.get(route.key) !== replacement
|
|
745
|
+
) {
|
|
746
|
+
await replacement.forceKill().catch(() => {});
|
|
747
|
+
this.recordProcessCrashRecoveryEvent(
|
|
748
|
+
route.serverName,
|
|
749
|
+
route.root,
|
|
750
|
+
"cancelled",
|
|
751
|
+
recovery.attemptStartedAt,
|
|
752
|
+
);
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
return replacement;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Restore route documents from disk and retain failed-document evidence. */
|
|
759
|
+
private restoreTrackedDocuments(client: LspClient, files: readonly string[]): void {
|
|
760
|
+
const failedFiles: string[] = [];
|
|
761
|
+
for (const filePath of files) {
|
|
762
|
+
if (!fs.existsSync(filePath)) {
|
|
763
|
+
failedFiles.push(filePath);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
client.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
768
|
+
} catch {
|
|
769
|
+
failedFiles.push(filePath);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const filePath of failedFiles) client.markFailedFile(filePath);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private markProcessCrashRecoveryFailed(
|
|
776
|
+
recovery: ProcessCrashRecoveryState,
|
|
777
|
+
key: string,
|
|
778
|
+
replacement: LspClient,
|
|
779
|
+
): void {
|
|
780
|
+
recovery.statusReason = "process-crash-recovery-exhausted";
|
|
781
|
+
this.unavailable.set(key, "start-failed");
|
|
782
|
+
if (this.clients.get(key) === replacement) this.clients.delete(key);
|
|
783
|
+
// A failed initialize may still leave a child process alive. The
|
|
784
|
+
// replacement is no longer owned by the manager before it is terminated.
|
|
785
|
+
void replacement.forceKill().catch(() => {});
|
|
786
|
+
this.publishLifecycle("recovery");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private recordProcessCrashRecoveryEvent(
|
|
790
|
+
serverName: string,
|
|
791
|
+
root: string,
|
|
792
|
+
outcome: ProcessCrashRecoveryOutcome,
|
|
793
|
+
startedAt?: number,
|
|
794
|
+
): void {
|
|
795
|
+
try {
|
|
796
|
+
recordDebugEvent({
|
|
797
|
+
source: "lsp",
|
|
798
|
+
level: outcome === "failure" || outcome === "exhausted" ? "warning" : "debug",
|
|
799
|
+
category: "runtime.recovery",
|
|
800
|
+
message: `LSP process-crash recovery ${outcome}`,
|
|
801
|
+
cwd: boundCwd(this.cwd),
|
|
802
|
+
data: {
|
|
803
|
+
reason: "process-crash",
|
|
804
|
+
outcome,
|
|
805
|
+
server: truncateIdentity(serverName),
|
|
806
|
+
root: truncateIdentity(root),
|
|
807
|
+
elapsedMs: startedAt === undefined ? 0 : Math.max(0, Date.now() - startedAt),
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
} catch {
|
|
811
|
+
// Recovery telemetry must never change route behavior.
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
325
815
|
private publishLifecycle(kind: ManagerLifecycleTransitionKind): void {
|
|
326
816
|
if (!this.onLifecycleTransition) return;
|
|
327
817
|
const projectServers = this.getKnownProjectServers([]);
|
|
@@ -360,11 +850,16 @@ export class LspManager {
|
|
|
360
850
|
rememberKnownRoot(this.knownRoots, serverName, root);
|
|
361
851
|
try {
|
|
362
852
|
await client.start();
|
|
853
|
+
if (this.shuttingDown) {
|
|
854
|
+
await client.forceKill().catch(() => {});
|
|
855
|
+
if (this.clients.get(key) === client) this.clients.delete(key);
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
363
858
|
this.unavailable.delete(key);
|
|
364
859
|
return client;
|
|
365
860
|
} catch {
|
|
366
861
|
this.unavailable.set(key, "start-failed");
|
|
367
|
-
this.clients.delete(key);
|
|
862
|
+
if (this.clients.get(key) === client) this.clients.delete(key);
|
|
368
863
|
return null;
|
|
369
864
|
}
|
|
370
865
|
}
|
|
@@ -403,30 +898,60 @@ export class LspManager {
|
|
|
403
898
|
const seen = new Set<string>();
|
|
404
899
|
|
|
405
900
|
for (const filePath of filePaths) {
|
|
406
|
-
const
|
|
407
|
-
if (!
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const key = clientKey(client.name, client.root);
|
|
411
|
-
if (seen.has(key)) continue;
|
|
412
|
-
seen.add(key);
|
|
413
|
-
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
|
|
901
|
+
const candidate = this.getRestartCandidate(filePath, options?.pushOnly === true, seen);
|
|
902
|
+
if (!candidate) continue;
|
|
903
|
+
const { client, key } = candidate;
|
|
414
904
|
|
|
415
905
|
// Observe cancellation between route restarts so a cancelled pass does
|
|
416
906
|
// not keep restarting further clients.
|
|
417
907
|
throwIfCodeRequestInterrupted(options?.control);
|
|
418
|
-
const
|
|
419
|
-
|
|
908
|
+
const restartPromise = this.pendingRestarts.get(key) ?? this.startSharedRestart(client, key);
|
|
909
|
+
const result = await raceRequestControl(restartPromise, options?.control);
|
|
420
910
|
restarted.push({ key, serverName: client.name, ...result });
|
|
421
911
|
}
|
|
422
912
|
|
|
423
913
|
return restarted;
|
|
424
914
|
}
|
|
425
915
|
|
|
916
|
+
private startSharedRestart(
|
|
917
|
+
client: LspClient,
|
|
918
|
+
key: string,
|
|
919
|
+
): Promise<{ files: string[]; restarted: boolean }> {
|
|
920
|
+
const restartEpoch = this.invalidationEpoch;
|
|
921
|
+
const restartPromise = this.restartClient(client).then((result) => {
|
|
922
|
+
this.recoveryRestartEpochs.set(key, restartEpoch);
|
|
923
|
+
return result;
|
|
924
|
+
});
|
|
925
|
+
this.pendingRestarts.set(key, restartPromise);
|
|
926
|
+
void restartPromise
|
|
927
|
+
.finally(() => {
|
|
928
|
+
if (this.pendingRestarts.get(key) === restartPromise) this.pendingRestarts.delete(key);
|
|
929
|
+
})
|
|
930
|
+
.catch(() => {});
|
|
931
|
+
return restartPromise;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private getRestartCandidate(
|
|
935
|
+
filePath: string,
|
|
936
|
+
pushOnly: boolean,
|
|
937
|
+
seen: Set<string>,
|
|
938
|
+
): { client: LspClient; key: string } | null {
|
|
939
|
+
const client = this.getExistingClientForFile(filePath);
|
|
940
|
+
if (!client || (pushOnly && client.hasDiagnosticProvider)) return null;
|
|
941
|
+
|
|
942
|
+
const key = clientKey(client.name, client.root);
|
|
943
|
+
if (this.processCrashRecoveries.get(key)?.pending || seen.has(key)) return null;
|
|
944
|
+
seen.add(key);
|
|
945
|
+
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) return null;
|
|
946
|
+
return { client, key };
|
|
947
|
+
}
|
|
948
|
+
|
|
426
949
|
private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
|
|
427
950
|
const key = clientKey(client.name, client.root);
|
|
428
951
|
const serverConfig = this.config.servers[client.name];
|
|
429
|
-
if (!serverConfig
|
|
952
|
+
if (!serverConfig || this.clients.get(key) !== client) {
|
|
953
|
+
return { files: [], restarted: false };
|
|
954
|
+
}
|
|
430
955
|
|
|
431
956
|
const openFiles = client.openFiles;
|
|
432
957
|
const trackedDiagnosticFiles = client
|
|
@@ -439,6 +964,7 @@ export class LspManager {
|
|
|
439
964
|
// Ignore shutdown failures when forcing a restart.
|
|
440
965
|
}
|
|
441
966
|
|
|
967
|
+
if (this.clients.get(key) !== client) return { files, restarted: false };
|
|
442
968
|
this.clients.delete(key);
|
|
443
969
|
this.unavailable.delete(key);
|
|
444
970
|
this.clearWarmedWorkspaceSymbolProjects(client.name, client.root);
|
|
@@ -455,23 +981,18 @@ export class LspManager {
|
|
|
455
981
|
RECOVERY_CLIENT_STARTUP_BOUND_MS,
|
|
456
982
|
"replacement client startup bound exceeded",
|
|
457
983
|
);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
}
|
|
984
|
+
if (this.clients.get(key) !== replacement || this.shuttingDown) {
|
|
985
|
+
void replacement.forceKill().catch(() => {});
|
|
986
|
+
return { files, restarted: false };
|
|
469
987
|
}
|
|
470
|
-
|
|
988
|
+
this.restoreTrackedDocuments(replacement, files);
|
|
989
|
+
this.clearUnconsumedProcessCrashState(key, client);
|
|
471
990
|
return { files, restarted: true };
|
|
472
991
|
} catch {
|
|
473
|
-
this.clients.
|
|
474
|
-
|
|
992
|
+
if (this.clients.get(key) === replacement) {
|
|
993
|
+
this.clients.delete(key);
|
|
994
|
+
this.unavailable.set(key, "start-failed");
|
|
995
|
+
}
|
|
475
996
|
// A bound-exceeded start may have spawned a live process; terminate
|
|
476
997
|
// the tree so the orphan cannot outlive the failed restart.
|
|
477
998
|
void replacement.forceKill().catch(() => {});
|
|
@@ -479,6 +1000,13 @@ export class LspManager {
|
|
|
479
1000
|
}
|
|
480
1001
|
}
|
|
481
1002
|
|
|
1003
|
+
private clearUnconsumedProcessCrashState(key: string, failedClient: LspClient): void {
|
|
1004
|
+
const recovery = this.processCrashRecoveries.get(key);
|
|
1005
|
+
if (recovery?.failedClient !== failedClient || recovery.attemptConsumed) return;
|
|
1006
|
+
this.processCrashRecoveries.delete(key);
|
|
1007
|
+
this.publishLifecycle("recovery");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
482
1010
|
getProjectServerInfo(serverName: string, root: string, fileTypes: string[]): ProjectServerInfo {
|
|
483
1011
|
const key = clientKey(serverName, root);
|
|
484
1012
|
return buildProjectServerInfo(
|
|
@@ -489,6 +1017,8 @@ export class LspManager {
|
|
|
489
1017
|
client: this.clients.get(key),
|
|
490
1018
|
unavailableReason:
|
|
491
1019
|
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
1020
|
+
statusReason: this.processCrashRecoveries.get(key)?.statusReason,
|
|
1021
|
+
includeOpenFile: (file) => this.isAutomaticScopePath(file),
|
|
492
1022
|
},
|
|
493
1023
|
this.cwd,
|
|
494
1024
|
);
|
|
@@ -496,6 +1026,7 @@ export class LspManager {
|
|
|
496
1026
|
getKnownProjectServers(detected: DetectedProjectServer[]): ProjectServerInfo[] {
|
|
497
1027
|
const known = new Map<string, DetectedProjectServer>();
|
|
498
1028
|
for (const entry of detected) {
|
|
1029
|
+
if (!this.automaticPathPolicy.isEligible(entry.root, "directory")) continue;
|
|
499
1030
|
known.set(clientKey(entry.name, entry.root), entry);
|
|
500
1031
|
}
|
|
501
1032
|
for (const client of this.clients.values()) {
|
|
@@ -507,6 +1038,19 @@ export class LspManager {
|
|
|
507
1038
|
fileTypes: [...(this.config.servers[client.name]?.fileTypes ?? [])],
|
|
508
1039
|
});
|
|
509
1040
|
}
|
|
1041
|
+
// Keep an exhausted recovery route visible after its failed replacement
|
|
1042
|
+
// is removed from the active client pool.
|
|
1043
|
+
for (const recovery of this.processCrashRecoveries.values()) {
|
|
1044
|
+
const name = recovery.failedClient.name;
|
|
1045
|
+
const root = recovery.failedClient.root;
|
|
1046
|
+
const key = clientKey(name, root);
|
|
1047
|
+
if (known.has(key)) continue;
|
|
1048
|
+
known.set(key, {
|
|
1049
|
+
name,
|
|
1050
|
+
root,
|
|
1051
|
+
fileTypes: [...(this.config.servers[name]?.fileTypes ?? [])],
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
510
1054
|
return Array.from(known.values())
|
|
511
1055
|
.map((entry) => this.getProjectServerInfo(entry.name, entry.root, entry.fileTypes))
|
|
512
1056
|
.sort(
|
|
@@ -521,17 +1065,31 @@ export class LspManager {
|
|
|
521
1065
|
async waitUntilFileReady(
|
|
522
1066
|
filePath: string,
|
|
523
1067
|
control?: CodeRequestControl,
|
|
524
|
-
|
|
1068
|
+
onProcessCrashRecovery?: (summary: ProcessCrashRecoverySummary) => void,
|
|
1069
|
+
): Promise<FileReadinessResult> {
|
|
525
1070
|
throwIfCodeRequestInterrupted(control);
|
|
526
1071
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
527
|
-
const
|
|
528
|
-
|
|
1072
|
+
const route = this.resolveFileRoute(resolvedPath);
|
|
1073
|
+
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
1074
|
+
const processCrashRecoveryRequested =
|
|
1075
|
+
recovery !== undefined && (recovery.pending !== null || recovery.statusReason !== undefined);
|
|
1076
|
+
if (processCrashRecoveryRequested) {
|
|
1077
|
+
onProcessCrashRecovery?.(summarizeProcessCrashRecoveryRoutes(["pending"]));
|
|
1078
|
+
}
|
|
1079
|
+
const client = await this.getClientForFile(resolvedPath, {
|
|
1080
|
+
recoverProcessCrash: true,
|
|
1081
|
+
control,
|
|
1082
|
+
});
|
|
1083
|
+
const processCrashRecovery = processCrashRecoveryRequested
|
|
1084
|
+
? summarizeProcessCrashRecoveryRoutes([client ? "recovered" : "failed"])
|
|
1085
|
+
: emptyProcessCrashRecoverySummary();
|
|
1086
|
+
if (!client) return { client: null, processCrashRecovery };
|
|
529
1087
|
// The caller's wait stops on cancellation even though the shared
|
|
530
1088
|
// readiness state keeps its own lifecycle.
|
|
531
1089
|
await client.getReady(control);
|
|
532
1090
|
await this.warmSemanticProject(client, resolvedPath, true, control);
|
|
533
1091
|
throwIfCodeRequestInterrupted(control);
|
|
534
|
-
return client;
|
|
1092
|
+
return { client, processCrashRecovery };
|
|
535
1093
|
}
|
|
536
1094
|
|
|
537
1095
|
/**
|
|
@@ -569,6 +1127,7 @@ export class LspManager {
|
|
|
569
1127
|
firstReady.root,
|
|
570
1128
|
serverConfig.rootMarkers,
|
|
571
1129
|
serverConfig.fileTypes,
|
|
1130
|
+
{ policy: this.automaticPathPolicy },
|
|
572
1131
|
)[0];
|
|
573
1132
|
if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
|
|
574
1133
|
}
|
|
@@ -582,7 +1141,10 @@ export class LspManager {
|
|
|
582
1141
|
control?: CodeRequestControl,
|
|
583
1142
|
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
584
1143
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
585
|
-
const client = await this.getClientForFile(resolvedPath
|
|
1144
|
+
const client = await this.getClientForFile(resolvedPath, {
|
|
1145
|
+
recoverProcessCrash: true,
|
|
1146
|
+
control,
|
|
1147
|
+
});
|
|
586
1148
|
if (!client) {
|
|
587
1149
|
return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
|
|
588
1150
|
}
|
|
@@ -637,6 +1199,7 @@ export class LspManager {
|
|
|
637
1199
|
*/
|
|
638
1200
|
noteWorkspaceChanges(changes: FileEvent[]): void {
|
|
639
1201
|
if (changes.length > 0) this.invalidationEpoch++;
|
|
1202
|
+
invalidateProjectConfigCaches(changes);
|
|
640
1203
|
this.notifyWorkspaceFileChanges(changes);
|
|
641
1204
|
}
|
|
642
1205
|
|
|
@@ -678,26 +1241,98 @@ export class LspManager {
|
|
|
678
1241
|
quietMs?: number;
|
|
679
1242
|
/** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
|
|
680
1243
|
initialEvidence?: DiagnosticEvidenceSummary;
|
|
1244
|
+
/** Explicit demand to recover crashed routes with tracked files in scope. */
|
|
1245
|
+
processCrashDemand?: { scopes?: readonly string[] };
|
|
681
1246
|
control?: CodeRequestControl;
|
|
682
1247
|
}): Promise<WorkspaceRecoveryResult> {
|
|
683
|
-
|
|
1248
|
+
const demand = options?.processCrashDemand
|
|
1249
|
+
? await this.recoverProcessCrashDemand((recovery) => {
|
|
1250
|
+
const files = this.processCrashDiagnosticFiles(recovery, options.processCrashDemand);
|
|
1251
|
+
return files.length > 0 ? files : null;
|
|
1252
|
+
}, options.control)
|
|
1253
|
+
: {
|
|
1254
|
+
hasSupport: false,
|
|
1255
|
+
processCrashRecovery: emptyProcessCrashRecoverySummary(),
|
|
1256
|
+
failures: [],
|
|
1257
|
+
failedFiles: [],
|
|
1258
|
+
};
|
|
1259
|
+
const recoveryOptions = demand.hasSupport
|
|
1260
|
+
? { ...options, initialEvidence: undefined }
|
|
1261
|
+
: options;
|
|
1262
|
+
const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
|
|
1263
|
+
return this.addProcessCrashDiagnosticEvidence(
|
|
1264
|
+
{ ...result, processCrashRecovery: demand.processCrashRecovery },
|
|
1265
|
+
demand,
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
private processCrashDiagnosticFiles(
|
|
1270
|
+
recovery: ProcessCrashRecoveryState,
|
|
1271
|
+
demand: { scopes?: readonly string[] } | undefined,
|
|
1272
|
+
): string[] {
|
|
1273
|
+
if (!demand) return [];
|
|
1274
|
+
const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1275
|
+
return recovery.files.filter(
|
|
1276
|
+
(file) =>
|
|
1277
|
+
this.isDiagnosticFile(file) &&
|
|
1278
|
+
(!scopes ||
|
|
1279
|
+
scopes.length === 0 ||
|
|
1280
|
+
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file))),
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
private addProcessCrashDiagnosticEvidence(
|
|
1285
|
+
result: WorkspaceRecoveryResult,
|
|
1286
|
+
demand: ProcessCrashDemandResult,
|
|
1287
|
+
): WorkspaceRecoveryResult {
|
|
1288
|
+
if (demand.failures.length === 0) return result;
|
|
1289
|
+
const byFile = new Map(
|
|
1290
|
+
result.diagnosticEvidence.documents.map((document) => [document.file, document] as const),
|
|
1291
|
+
);
|
|
1292
|
+
for (const file of demand.failedFiles) {
|
|
1293
|
+
const relativeFile = path.relative(this.cwd, file);
|
|
1294
|
+
byFile.set(relativeFile, { file: relativeFile, status: "failed" });
|
|
1295
|
+
}
|
|
1296
|
+
const diagnosticEvidence = summarizeDiagnosticEvidence(Array.from(byFile.values()));
|
|
1297
|
+
const diagnosticReport = {
|
|
1298
|
+
summary: { ...result.diagnosticReport.summary, current: false, evidence: diagnosticEvidence },
|
|
1299
|
+
outstanding: {
|
|
1300
|
+
...result.diagnosticReport.outstanding,
|
|
1301
|
+
current: false,
|
|
1302
|
+
evidence: diagnosticEvidence,
|
|
1303
|
+
},
|
|
1304
|
+
};
|
|
1305
|
+
return {
|
|
1306
|
+
...result,
|
|
1307
|
+
diagnosticEvidence,
|
|
1308
|
+
diagnosticReport,
|
|
1309
|
+
refreshFailureReason: [result.refreshFailureReason, ...demand.failures]
|
|
1310
|
+
.filter(Boolean)
|
|
1311
|
+
.join("; "),
|
|
1312
|
+
};
|
|
684
1313
|
}
|
|
685
1314
|
|
|
686
1315
|
/** Shut down all running LSP servers. */
|
|
687
1316
|
async shutdownAll(): Promise<void> {
|
|
688
|
-
|
|
1317
|
+
this.shuttingDown = true;
|
|
1318
|
+
const clients = Array.from(this.clients.values());
|
|
1319
|
+
const shutdowns = clients.map((client) => client.shutdown().catch(() => {}));
|
|
689
1320
|
await Promise.all(shutdowns);
|
|
690
1321
|
// Dispose every client so pending or active progress tokens and
|
|
691
1322
|
// readiness promises cannot outlive the manager.
|
|
692
|
-
for (const client of
|
|
1323
|
+
for (const client of clients) client.dispose();
|
|
693
1324
|
this.clients.clear();
|
|
694
1325
|
this.clientGenerations.clear();
|
|
1326
|
+
this.initializedClientGenerations.clear();
|
|
1327
|
+
this.handledCrashGenerations.clear();
|
|
1328
|
+
this.processCrashRecoveries.clear();
|
|
695
1329
|
this.recoveryRestartEpochs.clear();
|
|
696
1330
|
this.unavailable.clear();
|
|
697
1331
|
this.knownRoots.clear();
|
|
698
1332
|
this.warmedWorkspaceSymbolProjects.clear();
|
|
699
1333
|
this.warmedSemanticProjects.clear();
|
|
700
1334
|
this.pendingWarmProbes.clear();
|
|
1335
|
+
this.pendingRestarts.clear();
|
|
701
1336
|
}
|
|
702
1337
|
/** Get status of all servers. */
|
|
703
1338
|
getRunningClientCount(): number {
|
|
@@ -711,13 +1346,9 @@ export class LspManager {
|
|
|
711
1346
|
.map((client) => client.name);
|
|
712
1347
|
}
|
|
713
1348
|
|
|
714
|
-
/** Check whether a path belongs to the
|
|
1349
|
+
/** Check whether a path belongs to the automatic diagnostic evidence scope. */
|
|
715
1350
|
isDiagnosticFile(filePath: string): boolean {
|
|
716
|
-
|
|
717
|
-
return (
|
|
718
|
-
!shouldIgnoreLspPath(relative, this.cwd) &&
|
|
719
|
-
!isExcludedByPattern(relative, this.excludePatterns)
|
|
720
|
-
);
|
|
1351
|
+
return this.isAutomaticScopePath(filePath);
|
|
721
1352
|
}
|
|
722
1353
|
|
|
723
1354
|
getDiagnosticEvidence(): DiagnosticEvidenceSummary {
|
|
@@ -732,7 +1363,7 @@ export class LspManager {
|
|
|
732
1363
|
name: client.name,
|
|
733
1364
|
status: client.status === "running" ? "running" : "error",
|
|
734
1365
|
root: client.root,
|
|
735
|
-
openFiles: client.openFiles,
|
|
1366
|
+
openFiles: client.openFiles.filter((file) => this.isAutomaticScopePath(file)),
|
|
736
1367
|
});
|
|
737
1368
|
}
|
|
738
1369
|
return { servers };
|
|
@@ -773,8 +1404,7 @@ export class LspManager {
|
|
|
773
1404
|
const openFiles = activeServers.get(server.name) ?? new Set<string>();
|
|
774
1405
|
for (const file of server.openFiles) {
|
|
775
1406
|
const relativeFile = displayRelativeFilePath(file, this.cwd);
|
|
776
|
-
if (
|
|
777
|
-
if (isExcludedByPattern(relativeFile, this.excludePatterns)) continue;
|
|
1407
|
+
if (!this.isAutomaticScopePath(relativeFile)) continue;
|
|
778
1408
|
openFiles.add(relativeFile);
|
|
779
1409
|
}
|
|
780
1410
|
activeServers.set(server.name, openFiles);
|
|
@@ -800,7 +1430,7 @@ export class LspManager {
|
|
|
800
1430
|
.map((entry) => ({
|
|
801
1431
|
...entry,
|
|
802
1432
|
openFiles: entry.openFiles.filter((file) =>
|
|
803
|
-
isPathRelevant(file, normalizedPaths, this.cwd),
|
|
1433
|
+
isPathRelevant(file, normalizedPaths, this.cwd, this.automaticPathPolicy),
|
|
804
1434
|
),
|
|
805
1435
|
}))
|
|
806
1436
|
.filter((entry) => entry.openFiles.length > 0);
|
|
@@ -832,7 +1462,9 @@ export class LspManager {
|
|
|
832
1462
|
const fileDiags = new Map<string, { errors: number; warnings: number }>();
|
|
833
1463
|
for (const snapshot of snapshots) {
|
|
834
1464
|
for (const entry of snapshot.entries) {
|
|
835
|
-
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd,
|
|
1465
|
+
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
|
|
1466
|
+
this.isAutomaticScopePath(file),
|
|
1467
|
+
);
|
|
836
1468
|
}
|
|
837
1469
|
}
|
|
838
1470
|
|
|
@@ -846,7 +1478,7 @@ export class LspManager {
|
|
|
846
1478
|
entries: collectOutstandingDiagnosticsDetailed(
|
|
847
1479
|
snapshots.map((snapshot) => snapshot.entries),
|
|
848
1480
|
this.cwd,
|
|
849
|
-
this.
|
|
1481
|
+
(file) => this.isAutomaticScopePath(file),
|
|
850
1482
|
maxSeverity,
|
|
851
1483
|
),
|
|
852
1484
|
current,
|
|
@@ -878,7 +1510,9 @@ export class LspManager {
|
|
|
878
1510
|
});
|
|
879
1511
|
}
|
|
880
1512
|
for (const entry of snapshot.entries) {
|
|
881
|
-
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd,
|
|
1513
|
+
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
|
|
1514
|
+
this.isAutomaticScopePath(file),
|
|
1515
|
+
);
|
|
882
1516
|
}
|
|
883
1517
|
}
|
|
884
1518
|
const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
|
|
@@ -910,7 +1544,7 @@ export class LspManager {
|
|
|
910
1544
|
entries: collectOutstandingDiagnosticSummaryEntries(
|
|
911
1545
|
snapshots,
|
|
912
1546
|
this.cwd,
|
|
913
|
-
this.
|
|
1547
|
+
(file) => this.isAutomaticScopePath(file),
|
|
914
1548
|
maxSeverity,
|
|
915
1549
|
),
|
|
916
1550
|
current:
|
|
@@ -927,7 +1561,7 @@ export class LspManager {
|
|
|
927
1561
|
if (normalizedPaths.length === 0) return null;
|
|
928
1562
|
return formatOutstandingDiagnosticsSummaryText(
|
|
929
1563
|
this.getOutstandingDiagnosticSummary(maxSeverity).filter((entry) =>
|
|
930
|
-
isPathRelevant(entry.file, normalizedPaths, this.cwd),
|
|
1564
|
+
isPathRelevant(entry.file, normalizedPaths, this.cwd, this.automaticPathPolicy),
|
|
931
1565
|
),
|
|
932
1566
|
maxFiles,
|
|
933
1567
|
);
|
|
@@ -957,7 +1591,7 @@ export class LspManager {
|
|
|
957
1591
|
entries: collectOutstandingDiagnosticsDetailed(
|
|
958
1592
|
snapshots.map((snapshot) => snapshot.entries),
|
|
959
1593
|
this.cwd,
|
|
960
|
-
this.
|
|
1594
|
+
(file) => this.isAutomaticScopePath(file),
|
|
961
1595
|
maxSeverity,
|
|
962
1596
|
),
|
|
963
1597
|
current:
|
|
@@ -986,6 +1620,7 @@ export class LspManager {
|
|
|
986
1620
|
for (const client of this.clients.values()) {
|
|
987
1621
|
for (const document of client.getDiagnosticSnapshot().documents) {
|
|
988
1622
|
const file = relativeFilePathFromUri(document.uri, this.cwd);
|
|
1623
|
+
if (!this.isAutomaticScopePath(file)) continue;
|
|
989
1624
|
totalFiles++;
|
|
990
1625
|
accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
|
|
991
1626
|
}
|
|
@@ -1005,8 +1640,7 @@ export class LspManager {
|
|
|
1005
1640
|
const byFile = new Map<string, DiagnosticEvidenceDocument>();
|
|
1006
1641
|
for (const document of documents) {
|
|
1007
1642
|
const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
|
|
1008
|
-
if (
|
|
1009
|
-
if (isExcludedByPattern(file, this.excludePatterns)) continue;
|
|
1643
|
+
if (!this.isAutomaticScopePath(file)) continue;
|
|
1010
1644
|
const existing = byFile.get(file);
|
|
1011
1645
|
if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
|
|
1012
1646
|
byFile.set(file, { file, status: document.status });
|
|
@@ -1020,24 +1654,95 @@ export class LspManager {
|
|
|
1020
1654
|
async workspaceSymbol(
|
|
1021
1655
|
query: string,
|
|
1022
1656
|
control?: CodeRequestControl,
|
|
1657
|
+
scopes?: readonly string[],
|
|
1023
1658
|
): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
|
|
1024
|
-
const
|
|
1659
|
+
const resolvedScopes = scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1660
|
+
const routeMatches = (client: LspClient) =>
|
|
1661
|
+
this.workspaceSymbolRouteMatchesScopes(client, resolvedScopes);
|
|
1662
|
+
if (resolvedScopes && !this.hasWorkspaceSymbolRouteForScopes(resolvedScopes)) {
|
|
1663
|
+
return unavailableCodeQuery(
|
|
1664
|
+
"No known LSP route intersects the requested workspace-symbol scope.",
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const demand = await this.recoverProcessCrashDemand(
|
|
1668
|
+
(recovery) =>
|
|
1669
|
+
recovery.failedClient.serverCapabilities?.workspaceSymbolProvider &&
|
|
1670
|
+
routeMatches(recovery.failedClient)
|
|
1671
|
+
? []
|
|
1672
|
+
: null,
|
|
1673
|
+
control,
|
|
1674
|
+
);
|
|
1675
|
+
const collect = async (clients: Iterable<LspClient>, value: string) =>
|
|
1676
|
+
this.addWorkspaceSymbolDemandEvidence(
|
|
1677
|
+
await collectWorkspaceSymbols(
|
|
1678
|
+
Array.from(clients).filter((client) => routeMatches(client)),
|
|
1679
|
+
value,
|
|
1680
|
+
control,
|
|
1681
|
+
),
|
|
1682
|
+
demand,
|
|
1683
|
+
);
|
|
1684
|
+
const initial = await collect(this.clients.values(), query);
|
|
1025
1685
|
if (!initial.hasSupport || initial.results.length > 0) {
|
|
1026
1686
|
return workspaceSymbolCollectionResult(initial);
|
|
1027
1687
|
}
|
|
1028
1688
|
|
|
1029
1689
|
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
|
|
1030
|
-
findWarmTargets:
|
|
1690
|
+
findWarmTargets: (root, rootMarkers, fileTypes) =>
|
|
1691
|
+
findWorkspaceSymbolWarmTargets(root, rootMarkers, fileTypes, {
|
|
1692
|
+
policy: this.automaticPathPolicy,
|
|
1693
|
+
}),
|
|
1031
1694
|
getWarmPosition: getWorkspaceSymbolWarmPosition,
|
|
1032
|
-
collect
|
|
1695
|
+
collect,
|
|
1696
|
+
routeMatches,
|
|
1033
1697
|
query,
|
|
1034
1698
|
control,
|
|
1035
1699
|
});
|
|
1036
1700
|
return workspaceSymbolCollectionResult(warmed.collection ?? initial);
|
|
1037
1701
|
}
|
|
1038
|
-
|
|
1702
|
+
|
|
1703
|
+
private addWorkspaceSymbolDemandEvidence(
|
|
1704
|
+
collection: WorkspaceSymbolCollection,
|
|
1705
|
+
demand: ProcessCrashDemandResult,
|
|
1706
|
+
): WorkspaceSymbolCollection {
|
|
1707
|
+
return {
|
|
1708
|
+
...collection,
|
|
1709
|
+
hasSupport: collection.hasSupport || demand.hasSupport,
|
|
1710
|
+
failures: [...collection.failures, ...demand.failures],
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
private hasWorkspaceSymbolRouteForScopes(scopes: readonly string[]): boolean {
|
|
1715
|
+
const clients = [
|
|
1716
|
+
...this.clients.values(),
|
|
1717
|
+
...Array.from(this.processCrashRecoveries.values(), (recovery) => recovery.failedClient),
|
|
1718
|
+
];
|
|
1719
|
+
return clients.some((client) => this.workspaceSymbolRouteMatchesScopes(client, scopes));
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private workspaceSymbolRouteMatchesScopes(
|
|
1723
|
+
client: LspClient,
|
|
1724
|
+
scopes: readonly string[] | undefined,
|
|
1725
|
+
): boolean {
|
|
1726
|
+
if (!scopes || scopes.length === 0) return true;
|
|
1727
|
+
const key = clientKey(client.name, client.root);
|
|
1728
|
+
return scopes.some((scope) => {
|
|
1729
|
+
try {
|
|
1730
|
+
if (fs.statSync(scope).isFile()) return this.resolveFileRoute(scope)?.key === key;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
return (
|
|
1735
|
+
projectRoots.isWithinOrEqual(client.root, scope) ||
|
|
1736
|
+
projectRoots.isWithinOrEqual(scope, client.root)
|
|
1737
|
+
);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
async ensureFileOpen(
|
|
1741
|
+
filePath: string,
|
|
1742
|
+
options: FileClientRequestOptions = {},
|
|
1743
|
+
): Promise<LspClient | null> {
|
|
1039
1744
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1040
|
-
const client = await this.getClientForFile(resolvedPath);
|
|
1745
|
+
const client = await this.getClientForFile(resolvedPath, options);
|
|
1041
1746
|
if (!client) return null;
|
|
1042
1747
|
try {
|
|
1043
1748
|
client.didOpen(resolvedPath, fs.readFileSync(resolvedPath, "utf-8"));
|
|
@@ -1059,13 +1764,15 @@ export class LspManager {
|
|
|
1059
1764
|
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
1060
1765
|
) => import("../config/types.ts").Position | null;
|
|
1061
1766
|
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
|
|
1767
|
+
routeMatches?: (client: LspClient) => boolean;
|
|
1062
1768
|
query: string;
|
|
1063
1769
|
control?: CodeRequestControl;
|
|
1064
1770
|
}): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
|
|
1065
|
-
const { findWarmTargets, getWarmPosition, collect, query, control } = options;
|
|
1771
|
+
const { findWarmTargets, getWarmPosition, collect, routeMatches, query, control } = options;
|
|
1066
1772
|
let warmedAny = false;
|
|
1067
1773
|
|
|
1068
1774
|
for (const client of Array.from(this.clients.values())) {
|
|
1775
|
+
if (routeMatches && !routeMatches(client)) continue;
|
|
1069
1776
|
if (client.status !== "running") continue;
|
|
1070
1777
|
if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
|
|
1071
1778
|
|
|
@@ -1081,28 +1788,21 @@ export class LspManager {
|
|
|
1081
1788
|
for (const target of warmTargets) {
|
|
1082
1789
|
const projectKey = this.workspaceSymbolProjectKey(client.name, target.projectRoot);
|
|
1083
1790
|
if (this.warmedWorkspaceSymbolProjects.has(projectKey)) continue;
|
|
1084
|
-
if (this.hasOpenFileInProject(client, target.projectRoot)) {
|
|
1085
|
-
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1088
1791
|
|
|
1089
1792
|
const openedClient = await this.ensureFileOpen(target.file);
|
|
1090
1793
|
if (!openedClient) continue;
|
|
1091
1794
|
|
|
1092
1795
|
// A cancelled pass must not mark projects warmed on its way out.
|
|
1093
1796
|
throwIfCodeRequestInterrupted(control);
|
|
1094
|
-
this.
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (control) await openedClient.hover(target.file, hoverPosition, control);
|
|
1104
|
-
else await openedClient.hover(target.file, hoverPosition);
|
|
1105
|
-
}
|
|
1797
|
+
const available = await this.probeDocumentSymbols(
|
|
1798
|
+
openedClient,
|
|
1799
|
+
target.file,
|
|
1800
|
+
getWarmPosition,
|
|
1801
|
+
control,
|
|
1802
|
+
);
|
|
1803
|
+
if (available) {
|
|
1804
|
+
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
1805
|
+
warmedAny = true;
|
|
1106
1806
|
}
|
|
1107
1807
|
|
|
1108
1808
|
const collected = await collect(this.clients.values(), query);
|
|
@@ -1147,35 +1847,56 @@ export class LspManager {
|
|
|
1147
1847
|
try {
|
|
1148
1848
|
await raceRequestControl(probe, control);
|
|
1149
1849
|
} finally {
|
|
1150
|
-
this.pendingWarmProbes.
|
|
1850
|
+
if (this.pendingWarmProbes.get(projectKey) === probe) {
|
|
1851
|
+
this.pendingWarmProbes.delete(projectKey);
|
|
1852
|
+
}
|
|
1151
1853
|
}
|
|
1152
1854
|
}
|
|
1153
1855
|
|
|
1154
1856
|
private async performWarmProbe(
|
|
1155
|
-
|
|
1857
|
+
client: LspClient,
|
|
1156
1858
|
resolvedFile: string,
|
|
1157
1859
|
projectKey: string,
|
|
1158
1860
|
control?: CodeRequestControl,
|
|
1159
1861
|
): Promise<void> {
|
|
1862
|
+
const key = clientKey(client.name, client.root);
|
|
1863
|
+
if (this.clients.get(key) !== client) return;
|
|
1160
1864
|
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
1161
|
-
if (!openedClient) return;
|
|
1162
|
-
|
|
1163
|
-
this.warmedSemanticProjects.add(projectKey);
|
|
1865
|
+
if (!openedClient || openedClient !== client || this.clients.get(key) !== client) return;
|
|
1164
1866
|
|
|
1165
1867
|
// The probe is shared state for concurrent callers. One caller's
|
|
1166
1868
|
// cancellation must not cancel the shared in-flight queries, so only the
|
|
1167
1869
|
// Debug Operation ID is forwarded; each caller races its own wait above.
|
|
1168
1870
|
const sharedControl = { operationId: control?.operationId };
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1871
|
+
const available = await this.probeDocumentSymbols(
|
|
1872
|
+
openedClient,
|
|
1873
|
+
resolvedFile,
|
|
1874
|
+
getWorkspaceSymbolWarmPosition,
|
|
1875
|
+
sharedControl,
|
|
1876
|
+
);
|
|
1877
|
+
if (available && this.clients.get(key) === client) {
|
|
1878
|
+
this.warmedSemanticProjects.add(projectKey);
|
|
1174
1879
|
}
|
|
1175
1880
|
}
|
|
1176
1881
|
|
|
1177
|
-
private
|
|
1178
|
-
|
|
1882
|
+
private async probeDocumentSymbols(
|
|
1883
|
+
client: LspClient,
|
|
1884
|
+
file: string,
|
|
1885
|
+
getWarmPosition: (
|
|
1886
|
+
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
1887
|
+
) => import("../config/types.ts").Position | null,
|
|
1888
|
+
control?: CodeRequestControl,
|
|
1889
|
+
): Promise<boolean> {
|
|
1890
|
+
const symbols = control
|
|
1891
|
+
? await client.documentSymbols(file, control)
|
|
1892
|
+
: await client.documentSymbols(file);
|
|
1893
|
+
if (symbols.kind === "unavailable") return false;
|
|
1894
|
+
const hoverPosition = getWarmPosition(symbols.data);
|
|
1895
|
+
if (hoverPosition) {
|
|
1896
|
+
if (control) await client.hover(file, hoverPosition, control);
|
|
1897
|
+
else await client.hover(file, hoverPosition);
|
|
1898
|
+
}
|
|
1899
|
+
return true;
|
|
1179
1900
|
}
|
|
1180
1901
|
|
|
1181
1902
|
private workspaceSymbolProjectKey(serverName: string, projectRoot: string): string {
|
|
@@ -1210,6 +1931,17 @@ export class LspManager {
|
|
|
1210
1931
|
}
|
|
1211
1932
|
}
|
|
1212
1933
|
|
|
1934
|
+
function invalidateProjectConfigCaches(changes: readonly FileEvent[]): void {
|
|
1935
|
+
for (const change of changes) {
|
|
1936
|
+
const configPath = uriToFile(change.uri);
|
|
1937
|
+
if (!isProjectConfigFileName(path.basename(configPath))) continue;
|
|
1938
|
+
invalidateTsconfigCacheForConfig(configPath);
|
|
1939
|
+
if (change.type === FileChangeType.Created) {
|
|
1940
|
+
invalidateTsconfigCacheForConfigDir(path.dirname(configPath));
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1213
1945
|
function collectClientDiagnosticEvidenceDocuments(
|
|
1214
1946
|
snapshots: Iterable<ClientDiagnosticSnapshot>,
|
|
1215
1947
|
cwd: string,
|
|
@@ -1225,14 +1957,14 @@ function collectClientDiagnosticEvidenceDocuments(
|
|
|
1225
1957
|
function collectOutstandingDiagnosticSummaryEntries(
|
|
1226
1958
|
snapshots: Iterable<ClientDiagnosticSnapshot>,
|
|
1227
1959
|
cwd: string,
|
|
1228
|
-
|
|
1960
|
+
includeFile: (file: string) => boolean,
|
|
1229
1961
|
maxSeverity: number,
|
|
1230
1962
|
): OutstandingDiagnosticSummaryEntry[] {
|
|
1231
1963
|
const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
|
|
1232
1964
|
for (const snapshot of snapshots) {
|
|
1233
1965
|
for (const entry of snapshot.entries) {
|
|
1234
1966
|
const file = relativeFilePathFromUri(entry.uri, cwd);
|
|
1235
|
-
if (
|
|
1967
|
+
if (!includeFile(file)) continue;
|
|
1236
1968
|
const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
|
|
1237
1969
|
const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
|
|
1238
1970
|
if (next.total > 0) fileDiags.set(file, next);
|