@mrclrchtr/supi-code-intelligence 6.1.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- 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 +23 -3
- 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/@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/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 +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -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 +17 -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 +0 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -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-process-crash-report.ts +106 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
- 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 +577 -152
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
- 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-diagnostics.ts +54 -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 +74 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -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 +29 -3
- 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 +296 -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-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/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 -6
- 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/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/search/ast-scan-timing.ts +4 -2
- package/src/analysis/search/ast-scan.ts +1 -1
- package/src/api.ts +1 -0
- package/src/extension.ts +4 -2
- package/src/session/health-refresh.ts +26 -2
- package/src/session/health-types.ts +19 -0
- 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/substrate/lsp/lifecycle.ts +0 -3
- package/src/substrate/lsp/maintenance.ts +4 -5
- package/src/substrate/lsp/settings.ts +7 -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/markdown.ts +66 -66
- package/src/tool/code_health/refresh-outcome.ts +76 -0
- package/src/tool/code_health/refresh-status.ts +212 -23
- package/src/tool/code_health/result.ts +1 -0
- package/src/tool/code_health/tui.ts +18 -5
- 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/schemas.ts +21 -11
- 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,9 +21,18 @@ 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 {
|
|
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";
|
|
25
29
|
import { getServerForFile } from "../config/config.ts";
|
|
26
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getFileScopeDecision,
|
|
32
|
+
invalidateTsconfigCacheForConfig,
|
|
33
|
+
invalidateTsconfigCacheForConfigDir,
|
|
34
|
+
isProjectConfigFileName,
|
|
35
|
+
} from "../config/tsconfig-scope.ts";
|
|
27
36
|
import type {
|
|
28
37
|
DetectedProjectServer,
|
|
29
38
|
Diagnostic,
|
|
@@ -35,7 +44,8 @@ import type {
|
|
|
35
44
|
SymbolInformation,
|
|
36
45
|
WorkspaceSymbol,
|
|
37
46
|
} from "../config/types.ts";
|
|
38
|
-
import {
|
|
47
|
+
import { FileChangeType } from "../config/types.ts";
|
|
48
|
+
import { boundCwd } from "../debug-telemetry.ts";
|
|
39
49
|
import {
|
|
40
50
|
accumulateOutstandingDiagnostics,
|
|
41
51
|
collectDiagnosticSummaryCounts,
|
|
@@ -47,11 +57,19 @@ import {
|
|
|
47
57
|
type DiagnosticEvidenceSummary,
|
|
48
58
|
summarizeDiagnosticEvidence,
|
|
49
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";
|
|
50
66
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
51
|
-
import
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
import {
|
|
68
|
+
emptyProcessCrashRecoveryReport,
|
|
69
|
+
type ProcessCrashRecoveryReport,
|
|
70
|
+
type ScopeDecisionEntry,
|
|
71
|
+
type ScopeDecisionSummary,
|
|
72
|
+
type WorkspaceDiagnosticReport,
|
|
55
73
|
} from "../session/runtime-diagnostics.ts";
|
|
56
74
|
import {
|
|
57
75
|
displayRelativeFilePath,
|
|
@@ -61,7 +79,11 @@ import {
|
|
|
61
79
|
normalizeRelevantPaths,
|
|
62
80
|
shouldIgnoreLspPath,
|
|
63
81
|
} from "../summary.ts";
|
|
64
|
-
import { commandExists
|
|
82
|
+
import { commandExists } from "../utils.ts";
|
|
83
|
+
import {
|
|
84
|
+
type AutomaticLspPathPolicy,
|
|
85
|
+
createDefaultAutomaticLspPathPolicy,
|
|
86
|
+
} from "../workspace-path-policy.ts";
|
|
65
87
|
import {
|
|
66
88
|
closeFileAcrossClients,
|
|
67
89
|
pruneMissingFilesFromClients,
|
|
@@ -71,12 +93,12 @@ import {
|
|
|
71
93
|
collectOutstandingDiagnosticsDetailed,
|
|
72
94
|
syncClientFileAndGetDiagnostics,
|
|
73
95
|
} from "./manager-diagnostics.ts";
|
|
96
|
+
import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-helpers.ts";
|
|
74
97
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} from "./manager-helpers.ts";
|
|
98
|
+
boundProcessCrashFailureMessage,
|
|
99
|
+
buildProcessCrashRecoveryReport,
|
|
100
|
+
type ProcessCrashRecoveryRouteResult,
|
|
101
|
+
} from "./manager-process-crash-report.ts";
|
|
80
102
|
import { buildProjectServerInfo } from "./manager-project-info.ts";
|
|
81
103
|
import type {
|
|
82
104
|
ActiveCoverageSummaryEntry,
|
|
@@ -161,6 +183,11 @@ type FileClientRequestOptions = {
|
|
|
161
183
|
control?: CodeRequestControl;
|
|
162
184
|
};
|
|
163
185
|
|
|
186
|
+
interface FileReadinessResult {
|
|
187
|
+
client: LspClient | null;
|
|
188
|
+
processCrashRecovery: ProcessCrashRecoveryReport;
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
interface ProcessCrashRecoveryState {
|
|
165
192
|
/** The one automatic attempt is consumed when a replacement starts. */
|
|
166
193
|
attemptConsumed: boolean;
|
|
@@ -174,25 +201,57 @@ interface ProcessCrashRecoveryState {
|
|
|
174
201
|
pending: Promise<LspClient | null> | null;
|
|
175
202
|
/** Start time for the current replacement attempt, when one exists. */
|
|
176
203
|
attemptStartedAt?: number;
|
|
204
|
+
/** Bounded caught Error.message from a failed replacement startup. */
|
|
205
|
+
failureMessage?: string;
|
|
177
206
|
}
|
|
178
207
|
|
|
179
|
-
type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhausted" | "cancelled";
|
|
180
|
-
|
|
181
208
|
type ProcessCrashDemandResult = {
|
|
182
|
-
/** Whether at least one
|
|
209
|
+
/** Whether at least one crashed route supports the operation. */
|
|
183
210
|
hasSupport: boolean;
|
|
211
|
+
/** Whether replacement work changed the route set and needs a fresh pass. */
|
|
212
|
+
requiresFreshEvidence: boolean;
|
|
213
|
+
/** Separate route-level result for this explicit process-crash demand. */
|
|
214
|
+
processCrashRecovery: ProcessCrashRecoveryReport;
|
|
184
215
|
/** Required routes that remain unavailable after shared recovery settles. */
|
|
185
216
|
failures: string[];
|
|
186
217
|
/** In-scope tracked files from required routes that remain unavailable. */
|
|
187
218
|
failedFiles: string[];
|
|
188
219
|
};
|
|
189
220
|
|
|
221
|
+
type ProcessCrashRecoverySnapshot = {
|
|
222
|
+
readonly key: string;
|
|
223
|
+
/** Mutable state handle used only to await the shared replacement. */
|
|
224
|
+
readonly recovery: ProcessCrashRecoveryState;
|
|
225
|
+
readonly failedClient: LspClient;
|
|
226
|
+
readonly name: string;
|
|
227
|
+
readonly root: string;
|
|
228
|
+
readonly statusReason: NonNullable<ProcessCrashRecoveryState["statusReason"]>;
|
|
229
|
+
readonly files: readonly string[];
|
|
230
|
+
};
|
|
231
|
+
|
|
190
232
|
type RequiredProcessCrashDemand = {
|
|
191
|
-
|
|
192
|
-
|
|
233
|
+
snapshot: ProcessCrashRecoverySnapshot;
|
|
234
|
+
failedFiles: readonly string[];
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
type ImmediateProcessCrashDemand = {
|
|
238
|
+
route: ProcessCrashRecoveryRouteResult;
|
|
193
239
|
failedFiles: readonly string[];
|
|
240
|
+
reportOnly: boolean;
|
|
194
241
|
};
|
|
195
242
|
|
|
243
|
+
type ProcessCrashDemandSelection = {
|
|
244
|
+
immediate: ImmediateProcessCrashDemand[];
|
|
245
|
+
required: RequiredProcessCrashDemand[];
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
type ProcessCrashRecoveryTelemetryOutcome =
|
|
249
|
+
| "attempt"
|
|
250
|
+
| "success"
|
|
251
|
+
| "failure"
|
|
252
|
+
| "exhausted"
|
|
253
|
+
| "cancelled";
|
|
254
|
+
|
|
196
255
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
197
256
|
export class LspManager {
|
|
198
257
|
/** Active clients keyed by "serverName:root" */
|
|
@@ -201,14 +260,14 @@ export class LspManager {
|
|
|
201
260
|
private unavailable = new Map<string, UnavailableReason>();
|
|
202
261
|
/** Memoized per-command availability of LSP server binaries on PATH */
|
|
203
262
|
private commandAvailability = new Map<string, boolean>();
|
|
204
|
-
/** Guards against concurrent client creation for the same server:root key */
|
|
263
|
+
/** Guards against concurrent client creation for the same server:root key. */
|
|
205
264
|
private pendingStarts = new Map<string, Promise<LspClient | null>>();
|
|
265
|
+
/** Shared diagnostic-recovery restart for each replaceable client route. */
|
|
266
|
+
private pendingRestarts = new Map<string, Promise<{ files: string[]; restarted: boolean }>>();
|
|
206
267
|
/** Monotonic ownership generation for each replaceable client route. */
|
|
207
268
|
private clientGenerations = new Map<string, number>();
|
|
208
269
|
/** Preferred project roots discovered by proactive scan or lazy startup */
|
|
209
270
|
private knownRoots = new Map<string, string[]>();
|
|
210
|
-
/** User-configured gitignore-style exclude patterns */
|
|
211
|
-
private excludePatterns: string[] = [];
|
|
212
271
|
/** Monotonic workspace invalidation generation advanced by each invalidation event. */
|
|
213
272
|
private invalidationEpoch = 0;
|
|
214
273
|
/** Invalidation generation in which each route last received a recovery restart. */
|
|
@@ -231,17 +290,19 @@ export class LspManager {
|
|
|
231
290
|
private readonly config: LspConfig,
|
|
232
291
|
private readonly cwd: string,
|
|
233
292
|
private readonly onLifecycleTransition?: ManagerLifecycleListener,
|
|
293
|
+
private readonly automaticPathPolicy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(
|
|
294
|
+
cwd,
|
|
295
|
+
),
|
|
234
296
|
) {}
|
|
235
297
|
getCwd(): string {
|
|
236
298
|
return this.cwd;
|
|
237
299
|
}
|
|
238
|
-
setExcludePatterns(patterns: string[]): void {
|
|
239
|
-
this.excludePatterns = patterns;
|
|
240
|
-
}
|
|
241
300
|
|
|
242
301
|
// ── Public API ────────────────────────────────────────────────────
|
|
243
302
|
registerDetectedServers(detected: DetectedProjectServer[]): void {
|
|
244
|
-
this.knownRoots = projectRoots.buildKnownRootsMap(
|
|
303
|
+
this.knownRoots = projectRoots.buildKnownRootsMap(
|
|
304
|
+
detected.filter((entry) => this.automaticPathPolicy.isEligible(entry.root, "directory")),
|
|
305
|
+
);
|
|
245
306
|
}
|
|
246
307
|
/** Check whether a file path has an available LSP server for explicit semantic operations. */
|
|
247
308
|
canServeFile(filePath: string): boolean {
|
|
@@ -252,18 +313,60 @@ export class LspManager {
|
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
/**
|
|
255
|
-
*
|
|
256
|
-
* This is
|
|
257
|
-
*
|
|
316
|
+
* Return an exhausted process-crash outcome for a file readiness result.
|
|
317
|
+
* This is a passive read and never starts recovery. A crashed or pending
|
|
318
|
+
* route has no final result until an explicit operation observes it.
|
|
319
|
+
*/
|
|
320
|
+
getProcessCrashRecoveryReportForFile(filePath: string): ProcessCrashRecoveryReport | null {
|
|
321
|
+
const route = this.resolveFileRoute(resolveSessionPath(this.cwd, filePath));
|
|
322
|
+
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
323
|
+
if (!route || recovery?.statusReason !== "process-crash-recovery-exhausted") return null;
|
|
324
|
+
return buildProcessCrashRecoveryReport(
|
|
325
|
+
[
|
|
326
|
+
{
|
|
327
|
+
name: route.serverName,
|
|
328
|
+
root: route.root,
|
|
329
|
+
outcome: "recovery-exhausted",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
this.cwd,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check whether a file should participate in automatic LSP work.
|
|
338
|
+
* This is stricter than {@link canServeFile} and applies the workspace path
|
|
339
|
+
* policy plus tsconfig diagnostic suppression.
|
|
258
340
|
*/
|
|
259
341
|
isSupportedSourceFile(filePath: string): boolean {
|
|
260
|
-
|
|
261
|
-
// tracking and diagnostic summaries (shouldIgnoreLspPath). Keep runtime
|
|
262
|
-
// guidance activation consistent: reading or editing a file under
|
|
263
|
-
// node_modules / .pnpm must not arm LSP guidance for dependency sources.
|
|
264
|
-
if (shouldIgnoreLspPath(filePath, this.cwd)) return false;
|
|
342
|
+
if (!this.isAutomaticScopePath(filePath)) return false;
|
|
265
343
|
return this.canServeFile(filePath);
|
|
266
344
|
}
|
|
345
|
+
|
|
346
|
+
/** Inventory automatic workspace sentinel and source paths with the runtime policy. */
|
|
347
|
+
scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
|
|
348
|
+
return scanAutomaticWorkspaceSentinels(this.cwd, {
|
|
349
|
+
...options,
|
|
350
|
+
policy: this.automaticPathPolicy,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Refresh the automatic workspace inventory with the runtime policy. */
|
|
355
|
+
syncWorkspaceSentinelSnapshot(
|
|
356
|
+
previous: Map<string, number>,
|
|
357
|
+
options: WorkspaceSentinelScanOptions = {},
|
|
358
|
+
): WorkspaceSentinelSyncResult {
|
|
359
|
+
return syncAutomaticWorkspaceSentinelSnapshot(this.cwd, previous, {
|
|
360
|
+
...options,
|
|
361
|
+
policy: this.automaticPathPolicy,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Apply automatic path exclusions plus diagnostic tsconfig suppression. */
|
|
366
|
+
private isAutomaticScopePath(filePath: string): boolean {
|
|
367
|
+
return !shouldIgnoreLspPath(filePath, this.cwd, this.automaticPathPolicy);
|
|
368
|
+
}
|
|
369
|
+
|
|
267
370
|
private isServerCommandAvailable(command: string): boolean {
|
|
268
371
|
// Only memoize positive lookups. A negative result may become stale if the
|
|
269
372
|
// user installs the binary mid-session (e.g. `mise install`), and
|
|
@@ -431,6 +534,7 @@ export class LspManager {
|
|
|
431
534
|
failedClient: client,
|
|
432
535
|
files: this.trackedFilesForClient(client),
|
|
433
536
|
pending: null,
|
|
537
|
+
failureMessage: undefined,
|
|
434
538
|
});
|
|
435
539
|
}
|
|
436
540
|
|
|
@@ -469,54 +573,192 @@ export class LspManager {
|
|
|
469
573
|
/**
|
|
470
574
|
* Recover all crashed routes selected by one evidence-producing operation.
|
|
471
575
|
* Each route keeps its independent attempt budget and shared replacement.
|
|
576
|
+
* Selection uses one immutable route-fact snapshot so scope and retention
|
|
577
|
+
* facts cannot change halfway through the demand.
|
|
472
578
|
*/
|
|
473
579
|
private async recoverProcessCrashDemand(
|
|
474
|
-
selectFiles: (
|
|
580
|
+
selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
|
|
475
581
|
control?: CodeRequestControl,
|
|
582
|
+
options: { skipWhenNoRetainedFile?: boolean } = {},
|
|
476
583
|
): Promise<ProcessCrashDemandResult> {
|
|
477
|
-
const required =
|
|
478
|
-
(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
|
|
482
|
-
},
|
|
584
|
+
const { immediate, required } = this.selectProcessCrashDemandRoutes(
|
|
585
|
+
this.snapshotProcessCrashRoutes(),
|
|
586
|
+
selectFiles,
|
|
587
|
+
options,
|
|
483
588
|
);
|
|
484
|
-
|
|
589
|
+
const hasSupport = immediate.length > 0 || required.length > 0;
|
|
590
|
+
if (required.length === 0) {
|
|
591
|
+
const failures = immediate
|
|
592
|
+
.filter((entry) => !entry.reportOnly)
|
|
593
|
+
.map((entry) => this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome));
|
|
594
|
+
return {
|
|
595
|
+
hasSupport,
|
|
596
|
+
requiresFreshEvidence: false,
|
|
597
|
+
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
598
|
+
immediate.map(({ route }) => route),
|
|
599
|
+
this.cwd,
|
|
600
|
+
),
|
|
601
|
+
failures,
|
|
602
|
+
failedFiles: Array.from(
|
|
603
|
+
new Set(
|
|
604
|
+
immediate
|
|
605
|
+
.filter((entry) => !entry.reportOnly)
|
|
606
|
+
.flatMap(({ failedFiles }) => failedFiles),
|
|
607
|
+
),
|
|
608
|
+
),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
485
611
|
|
|
486
612
|
await Promise.all(
|
|
487
|
-
required.map(async ({
|
|
488
|
-
const serverConfig = this.config.servers[
|
|
613
|
+
required.map(async ({ snapshot }) => {
|
|
614
|
+
const serverConfig = this.config.servers[snapshot.name];
|
|
489
615
|
if (!serverConfig) return;
|
|
490
616
|
await this.acquireProcessCrashReplacement(
|
|
491
|
-
recovery,
|
|
617
|
+
snapshot.recovery,
|
|
492
618
|
{
|
|
493
|
-
serverName:
|
|
619
|
+
serverName: snapshot.name,
|
|
494
620
|
serverConfig,
|
|
495
|
-
root:
|
|
496
|
-
key,
|
|
621
|
+
root: snapshot.root,
|
|
622
|
+
key: snapshot.key,
|
|
497
623
|
},
|
|
498
624
|
control,
|
|
499
625
|
);
|
|
500
626
|
}),
|
|
501
627
|
);
|
|
502
628
|
|
|
503
|
-
const
|
|
629
|
+
const observedRequired = required.filter(
|
|
630
|
+
({ snapshot }) => !this.isProcessCrashDemandCancelled(snapshot),
|
|
631
|
+
);
|
|
632
|
+
const failedDemands = observedRequired.filter(
|
|
633
|
+
({ snapshot }) => snapshot.recovery.statusReason !== undefined,
|
|
634
|
+
);
|
|
635
|
+
const recoveredRoutes = observedRequired.map(
|
|
636
|
+
({ snapshot }): ProcessCrashRecoveryRouteResult => ({
|
|
637
|
+
name: snapshot.name,
|
|
638
|
+
root: snapshot.root,
|
|
639
|
+
outcome: snapshot.recovery.statusReason === undefined ? "recovered" : "recovery-failed",
|
|
640
|
+
...(snapshot.recovery.statusReason === undefined || !snapshot.recovery.failureMessage
|
|
641
|
+
? {}
|
|
642
|
+
: { failureMessage: snapshot.recovery.failureMessage }),
|
|
643
|
+
}),
|
|
644
|
+
);
|
|
645
|
+
const failedImmediate = immediate.filter((entry) => !entry.reportOnly);
|
|
504
646
|
return {
|
|
505
|
-
hasSupport
|
|
506
|
-
|
|
507
|
-
|
|
647
|
+
hasSupport,
|
|
648
|
+
requiresFreshEvidence: observedRequired.length > 0,
|
|
649
|
+
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
650
|
+
[...immediate.map(({ route }) => route), ...recoveredRoutes],
|
|
651
|
+
this.cwd,
|
|
652
|
+
),
|
|
653
|
+
failures: [
|
|
654
|
+
...failedImmediate.map((entry) =>
|
|
655
|
+
this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome),
|
|
656
|
+
),
|
|
657
|
+
...failedDemands.map(({ snapshot }) =>
|
|
658
|
+
this.formatProcessCrashDemandFailure(snapshot.recovery, "recovery-failed"),
|
|
659
|
+
),
|
|
660
|
+
],
|
|
661
|
+
failedFiles: Array.from(
|
|
662
|
+
new Set([
|
|
663
|
+
...failedImmediate.flatMap(({ failedFiles }) => failedFiles),
|
|
664
|
+
...failedDemands.flatMap(({ failedFiles }) => failedFiles),
|
|
665
|
+
]),
|
|
666
|
+
),
|
|
508
667
|
};
|
|
509
668
|
}
|
|
510
669
|
|
|
511
|
-
private
|
|
512
|
-
|
|
670
|
+
private isProcessCrashDemandCancelled(snapshot: ProcessCrashRecoverySnapshot): boolean {
|
|
671
|
+
return (
|
|
672
|
+
snapshot.recovery.statusReason !== undefined &&
|
|
673
|
+
(this.shuttingDown || this.processCrashRecoveries.get(snapshot.key) !== snapshot.recovery)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private selectProcessCrashDemandRoutes(
|
|
678
|
+
snapshots: readonly ProcessCrashRecoverySnapshot[],
|
|
679
|
+
selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
|
|
680
|
+
options: { skipWhenNoRetainedFile?: boolean },
|
|
681
|
+
): ProcessCrashDemandSelection {
|
|
682
|
+
const immediate: ImmediateProcessCrashDemand[] = [];
|
|
683
|
+
const required: RequiredProcessCrashDemand[] = [];
|
|
684
|
+
|
|
685
|
+
for (const snapshot of snapshots) {
|
|
686
|
+
const failedFiles = selectFiles(snapshot);
|
|
687
|
+
if (failedFiles === null) continue;
|
|
688
|
+
|
|
689
|
+
const exhausted = snapshot.statusReason === "process-crash-recovery-exhausted";
|
|
690
|
+
const noRetainedFile = failedFiles.length === 0;
|
|
691
|
+
if (exhausted) {
|
|
692
|
+
immediate.push({
|
|
693
|
+
route: {
|
|
694
|
+
name: snapshot.name,
|
|
695
|
+
root: snapshot.root,
|
|
696
|
+
outcome: "recovery-exhausted",
|
|
697
|
+
},
|
|
698
|
+
failedFiles,
|
|
699
|
+
reportOnly: options.skipWhenNoRetainedFile === true && noRetainedFile,
|
|
700
|
+
});
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (
|
|
704
|
+
options.skipWhenNoRetainedFile === true &&
|
|
705
|
+
noRetainedFile &&
|
|
706
|
+
snapshot.statusReason === "process-crashed"
|
|
707
|
+
) {
|
|
708
|
+
immediate.push({
|
|
709
|
+
route: {
|
|
710
|
+
name: snapshot.name,
|
|
711
|
+
root: snapshot.root,
|
|
712
|
+
outcome: "skipped-no-retained-file",
|
|
713
|
+
},
|
|
714
|
+
failedFiles,
|
|
715
|
+
reportOnly: true,
|
|
716
|
+
});
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
required.push({ snapshot, failedFiles });
|
|
720
|
+
}
|
|
721
|
+
return { immediate, required };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private snapshotProcessCrashRoutes(): ProcessCrashRecoverySnapshot[] {
|
|
725
|
+
return Array.from(this.processCrashRecoveries.entries()).flatMap(([key, recovery]) => {
|
|
726
|
+
if (recovery.statusReason === undefined) return [];
|
|
727
|
+
return [
|
|
728
|
+
{
|
|
729
|
+
key,
|
|
730
|
+
recovery,
|
|
731
|
+
failedClient: recovery.failedClient,
|
|
732
|
+
name: recovery.failedClient.name,
|
|
733
|
+
root: recovery.failedClient.root,
|
|
734
|
+
statusReason: recovery.statusReason,
|
|
735
|
+
files: [...recovery.files],
|
|
736
|
+
},
|
|
737
|
+
];
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private formatProcessCrashDemandFailure(
|
|
742
|
+
recovery: ProcessCrashRecoveryState | ProcessCrashRecoveryRouteResult,
|
|
743
|
+
outcome?: ProcessCrashRecoveryRouteResult["outcome"],
|
|
744
|
+
): string {
|
|
745
|
+
const isRouteResult = "outcome" in recovery;
|
|
746
|
+
const name = isRouteResult ? recovery.name : recovery.failedClient.name;
|
|
747
|
+
const root = isRouteResult
|
|
748
|
+
? recovery.root
|
|
749
|
+
: (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
|
|
750
|
+
const routeOutcome =
|
|
751
|
+
outcome ??
|
|
752
|
+
(isRouteResult
|
|
753
|
+
? recovery.outcome
|
|
754
|
+
: recovery.statusReason === "process-crash-recovery-exhausted"
|
|
755
|
+
? "recovery-exhausted"
|
|
756
|
+
: "recovery-failed");
|
|
513
757
|
const reason =
|
|
514
|
-
|
|
758
|
+
routeOutcome === "recovery-exhausted"
|
|
515
759
|
? "process recovery exhausted; reload required"
|
|
516
|
-
:
|
|
517
|
-
|
|
518
|
-
: "process crashed";
|
|
519
|
-
return `${recovery.failedClient.name} @ ${root} is unavailable — ${reason}.`;
|
|
760
|
+
: "process recovery failed";
|
|
761
|
+
return `${name} @ ${root} is unavailable — ${reason}.`;
|
|
520
762
|
}
|
|
521
763
|
|
|
522
764
|
private startProcessCrashRecovery(
|
|
@@ -580,6 +822,7 @@ export class LspManager {
|
|
|
580
822
|
|
|
581
823
|
this.unavailable.delete(key);
|
|
582
824
|
recovery.statusReason = undefined;
|
|
825
|
+
recovery.failureMessage = undefined;
|
|
583
826
|
this.publishLifecycle("recovery");
|
|
584
827
|
this.restoreTrackedDocuments(started, recovery.files);
|
|
585
828
|
this.recordProcessCrashRecoveryEvent(
|
|
@@ -626,7 +869,7 @@ export class LspManager {
|
|
|
626
869
|
// LspClient's initialize request already has the 30-second transport
|
|
627
870
|
// bound. Do not apply the 5-second diagnostic-restart bound here.
|
|
628
871
|
await replacement.start();
|
|
629
|
-
} catch {
|
|
872
|
+
} catch (error) {
|
|
630
873
|
if (this.isProcessCrashRecoveryCancelled(recovery, route.key)) {
|
|
631
874
|
await replacement.forceKill().catch(() => {});
|
|
632
875
|
this.recordProcessCrashRecoveryEvent(
|
|
@@ -638,7 +881,7 @@ export class LspManager {
|
|
|
638
881
|
return null;
|
|
639
882
|
}
|
|
640
883
|
if (recovery.statusReason !== "process-crash-recovery-exhausted") {
|
|
641
|
-
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement);
|
|
884
|
+
this.markProcessCrashRecoveryFailed(recovery, route.key, replacement, error);
|
|
642
885
|
this.recordProcessCrashRecoveryEvent(
|
|
643
886
|
route.serverName,
|
|
644
887
|
route.root,
|
|
@@ -686,8 +929,13 @@ export class LspManager {
|
|
|
686
929
|
recovery: ProcessCrashRecoveryState,
|
|
687
930
|
key: string,
|
|
688
931
|
replacement: LspClient,
|
|
932
|
+
error: unknown,
|
|
689
933
|
): void {
|
|
690
934
|
recovery.statusReason = "process-crash-recovery-exhausted";
|
|
935
|
+
recovery.failureMessage =
|
|
936
|
+
error instanceof Error && error.message
|
|
937
|
+
? boundProcessCrashFailureMessage(error.message)
|
|
938
|
+
: undefined;
|
|
691
939
|
this.unavailable.set(key, "start-failed");
|
|
692
940
|
if (this.clients.get(key) === replacement) this.clients.delete(key);
|
|
693
941
|
// A failed initialize may still leave a child process alive. The
|
|
@@ -699,7 +947,7 @@ export class LspManager {
|
|
|
699
947
|
private recordProcessCrashRecoveryEvent(
|
|
700
948
|
serverName: string,
|
|
701
949
|
root: string,
|
|
702
|
-
outcome:
|
|
950
|
+
outcome: ProcessCrashRecoveryTelemetryOutcome,
|
|
703
951
|
startedAt?: number,
|
|
704
952
|
): void {
|
|
705
953
|
try {
|
|
@@ -808,32 +1056,60 @@ export class LspManager {
|
|
|
808
1056
|
const seen = new Set<string>();
|
|
809
1057
|
|
|
810
1058
|
for (const filePath of filePaths) {
|
|
811
|
-
const
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const key = clientKey(client.name, client.root);
|
|
816
|
-
const processRecovery = this.processCrashRecoveries.get(key);
|
|
817
|
-
if (processRecovery?.pending) continue;
|
|
818
|
-
if (seen.has(key)) continue;
|
|
819
|
-
seen.add(key);
|
|
820
|
-
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
|
|
1059
|
+
const candidate = this.getRestartCandidate(filePath, options?.pushOnly === true, seen);
|
|
1060
|
+
if (!candidate) continue;
|
|
1061
|
+
const { client, key } = candidate;
|
|
821
1062
|
|
|
822
1063
|
// Observe cancellation between route restarts so a cancelled pass does
|
|
823
1064
|
// not keep restarting further clients.
|
|
824
1065
|
throwIfCodeRequestInterrupted(options?.control);
|
|
825
|
-
const
|
|
826
|
-
|
|
1066
|
+
const restartPromise = this.pendingRestarts.get(key) ?? this.startSharedRestart(client, key);
|
|
1067
|
+
const result = await raceRequestControl(restartPromise, options?.control);
|
|
827
1068
|
restarted.push({ key, serverName: client.name, ...result });
|
|
828
1069
|
}
|
|
829
1070
|
|
|
830
1071
|
return restarted;
|
|
831
1072
|
}
|
|
832
1073
|
|
|
1074
|
+
private startSharedRestart(
|
|
1075
|
+
client: LspClient,
|
|
1076
|
+
key: string,
|
|
1077
|
+
): Promise<{ files: string[]; restarted: boolean }> {
|
|
1078
|
+
const restartEpoch = this.invalidationEpoch;
|
|
1079
|
+
const restartPromise = this.restartClient(client).then((result) => {
|
|
1080
|
+
this.recoveryRestartEpochs.set(key, restartEpoch);
|
|
1081
|
+
return result;
|
|
1082
|
+
});
|
|
1083
|
+
this.pendingRestarts.set(key, restartPromise);
|
|
1084
|
+
void restartPromise
|
|
1085
|
+
.finally(() => {
|
|
1086
|
+
if (this.pendingRestarts.get(key) === restartPromise) this.pendingRestarts.delete(key);
|
|
1087
|
+
})
|
|
1088
|
+
.catch(() => {});
|
|
1089
|
+
return restartPromise;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private getRestartCandidate(
|
|
1093
|
+
filePath: string,
|
|
1094
|
+
pushOnly: boolean,
|
|
1095
|
+
seen: Set<string>,
|
|
1096
|
+
): { client: LspClient; key: string } | null {
|
|
1097
|
+
const client = this.getExistingClientForFile(filePath);
|
|
1098
|
+
if (!client || (pushOnly && client.hasDiagnosticProvider)) return null;
|
|
1099
|
+
|
|
1100
|
+
const key = clientKey(client.name, client.root);
|
|
1101
|
+
if (this.processCrashRecoveries.get(key)?.pending || seen.has(key)) return null;
|
|
1102
|
+
seen.add(key);
|
|
1103
|
+
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) return null;
|
|
1104
|
+
return { client, key };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
833
1107
|
private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
|
|
834
1108
|
const key = clientKey(client.name, client.root);
|
|
835
1109
|
const serverConfig = this.config.servers[client.name];
|
|
836
|
-
if (!serverConfig
|
|
1110
|
+
if (!serverConfig || this.clients.get(key) !== client) {
|
|
1111
|
+
return { files: [], restarted: false };
|
|
1112
|
+
}
|
|
837
1113
|
|
|
838
1114
|
const openFiles = client.openFiles;
|
|
839
1115
|
const trackedDiagnosticFiles = client
|
|
@@ -846,6 +1122,7 @@ export class LspManager {
|
|
|
846
1122
|
// Ignore shutdown failures when forcing a restart.
|
|
847
1123
|
}
|
|
848
1124
|
|
|
1125
|
+
if (this.clients.get(key) !== client) return { files, restarted: false };
|
|
849
1126
|
this.clients.delete(key);
|
|
850
1127
|
this.unavailable.delete(key);
|
|
851
1128
|
this.clearWarmedWorkspaceSymbolProjects(client.name, client.root);
|
|
@@ -862,12 +1139,18 @@ export class LspManager {
|
|
|
862
1139
|
RECOVERY_CLIENT_STARTUP_BOUND_MS,
|
|
863
1140
|
"replacement client startup bound exceeded",
|
|
864
1141
|
);
|
|
1142
|
+
if (this.clients.get(key) !== replacement || this.shuttingDown) {
|
|
1143
|
+
void replacement.forceKill().catch(() => {});
|
|
1144
|
+
return { files, restarted: false };
|
|
1145
|
+
}
|
|
865
1146
|
this.restoreTrackedDocuments(replacement, files);
|
|
866
1147
|
this.clearUnconsumedProcessCrashState(key, client);
|
|
867
1148
|
return { files, restarted: true };
|
|
868
1149
|
} catch {
|
|
869
|
-
this.clients.
|
|
870
|
-
|
|
1150
|
+
if (this.clients.get(key) === replacement) {
|
|
1151
|
+
this.clients.delete(key);
|
|
1152
|
+
this.unavailable.set(key, "start-failed");
|
|
1153
|
+
}
|
|
871
1154
|
// A bound-exceeded start may have spawned a live process; terminate
|
|
872
1155
|
// the tree so the orphan cannot outlive the failed restart.
|
|
873
1156
|
void replacement.forceKill().catch(() => {});
|
|
@@ -893,6 +1176,7 @@ export class LspManager {
|
|
|
893
1176
|
unavailableReason:
|
|
894
1177
|
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
895
1178
|
statusReason: this.processCrashRecoveries.get(key)?.statusReason,
|
|
1179
|
+
includeOpenFile: (file) => this.isAutomaticScopePath(file),
|
|
896
1180
|
},
|
|
897
1181
|
this.cwd,
|
|
898
1182
|
);
|
|
@@ -900,6 +1184,7 @@ export class LspManager {
|
|
|
900
1184
|
getKnownProjectServers(detected: DetectedProjectServer[]): ProjectServerInfo[] {
|
|
901
1185
|
const known = new Map<string, DetectedProjectServer>();
|
|
902
1186
|
for (const entry of detected) {
|
|
1187
|
+
if (!this.automaticPathPolicy.isEligible(entry.root, "directory")) continue;
|
|
903
1188
|
known.set(clientKey(entry.name, entry.root), entry);
|
|
904
1189
|
}
|
|
905
1190
|
for (const client of this.clients.values()) {
|
|
@@ -938,20 +1223,101 @@ export class LspManager {
|
|
|
938
1223
|
async waitUntilFileReady(
|
|
939
1224
|
filePath: string,
|
|
940
1225
|
control?: CodeRequestControl,
|
|
941
|
-
|
|
1226
|
+
onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void,
|
|
1227
|
+
): Promise<FileReadinessResult> {
|
|
942
1228
|
throwIfCodeRequestInterrupted(control);
|
|
943
1229
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1230
|
+
const route = this.resolveFileRoute(resolvedPath);
|
|
1231
|
+
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
1232
|
+
const statusAtStart = recovery?.statusReason;
|
|
1233
|
+
const processCrashRecoveryRequested = statusAtStart !== undefined;
|
|
944
1234
|
const client = await this.getClientForFile(resolvedPath, {
|
|
945
1235
|
recoverProcessCrash: true,
|
|
946
1236
|
control,
|
|
947
1237
|
});
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1238
|
+
const processCrashRecovery = this.buildFileProcessCrashRecoveryReport(
|
|
1239
|
+
route,
|
|
1240
|
+
recovery,
|
|
1241
|
+
statusAtStart,
|
|
1242
|
+
client,
|
|
1243
|
+
);
|
|
1244
|
+
if (processCrashRecoveryRequested) onProcessCrashRecovery?.(processCrashRecovery);
|
|
1245
|
+
if (!client) return { client: null, processCrashRecovery };
|
|
1246
|
+
try {
|
|
1247
|
+
await client.getReady(control);
|
|
1248
|
+
await this.warmSemanticProject(client, resolvedPath, true, control);
|
|
1249
|
+
throwIfCodeRequestInterrupted(control);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
const failedRecovery = this.buildFileProcessCrashFailureReport(
|
|
1252
|
+
route,
|
|
1253
|
+
recovery,
|
|
1254
|
+
statusAtStart,
|
|
1255
|
+
error,
|
|
1256
|
+
);
|
|
1257
|
+
if (failedRecovery) {
|
|
1258
|
+
onProcessCrashRecovery?.(failedRecovery);
|
|
1259
|
+
return { client: null, processCrashRecovery: failedRecovery };
|
|
1260
|
+
}
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
return { client, processCrashRecovery };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private buildFileProcessCrashRecoveryReport(
|
|
1267
|
+
route: FileRoute | null,
|
|
1268
|
+
recovery: ProcessCrashRecoveryState | undefined,
|
|
1269
|
+
statusAtStart: ProcessCrashRecoveryState["statusReason"],
|
|
1270
|
+
client: LspClient | null,
|
|
1271
|
+
): ProcessCrashRecoveryReport {
|
|
1272
|
+
if (statusAtStart === undefined) return emptyProcessCrashRecoveryReport();
|
|
1273
|
+
return buildProcessCrashRecoveryReport(
|
|
1274
|
+
[
|
|
1275
|
+
{
|
|
1276
|
+
name: route?.serverName ?? "unknown",
|
|
1277
|
+
root: route?.root ?? this.cwd,
|
|
1278
|
+
outcome:
|
|
1279
|
+
statusAtStart === "process-crash-recovery-exhausted"
|
|
1280
|
+
? "recovery-exhausted"
|
|
1281
|
+
: client
|
|
1282
|
+
? "recovered"
|
|
1283
|
+
: "recovery-failed",
|
|
1284
|
+
...(client || !recovery?.failureMessage
|
|
1285
|
+
? {}
|
|
1286
|
+
: { failureMessage: recovery.failureMessage }),
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
this.cwd,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private buildFileProcessCrashFailureReport(
|
|
1294
|
+
route: FileRoute | null,
|
|
1295
|
+
recovery: ProcessCrashRecoveryState | undefined,
|
|
1296
|
+
statusAtStart: ProcessCrashRecoveryState["statusReason"],
|
|
1297
|
+
error: unknown,
|
|
1298
|
+
): ProcessCrashRecoveryReport | null {
|
|
1299
|
+
if (
|
|
1300
|
+
statusAtStart === undefined ||
|
|
1301
|
+
statusAtStart === "process-crash-recovery-exhausted" ||
|
|
1302
|
+
recovery?.statusReason !== "process-crash-recovery-exhausted"
|
|
1303
|
+
) {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
return buildProcessCrashRecoveryReport(
|
|
1307
|
+
[
|
|
1308
|
+
{
|
|
1309
|
+
name: route?.serverName ?? "unknown",
|
|
1310
|
+
root: route?.root ?? this.cwd,
|
|
1311
|
+
outcome: "recovery-failed",
|
|
1312
|
+
...(error instanceof Error && error.message
|
|
1313
|
+
? { failureMessage: boundProcessCrashFailureMessage(error.message) }
|
|
1314
|
+
: recovery.failureMessage
|
|
1315
|
+
? { failureMessage: recovery.failureMessage }
|
|
1316
|
+
: {}),
|
|
1317
|
+
},
|
|
1318
|
+
],
|
|
1319
|
+
this.cwd,
|
|
1320
|
+
);
|
|
955
1321
|
}
|
|
956
1322
|
|
|
957
1323
|
/**
|
|
@@ -989,6 +1355,7 @@ export class LspManager {
|
|
|
989
1355
|
firstReady.root,
|
|
990
1356
|
serverConfig.rootMarkers,
|
|
991
1357
|
serverConfig.fileTypes,
|
|
1358
|
+
{ policy: this.automaticPathPolicy },
|
|
992
1359
|
)[0];
|
|
993
1360
|
if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
|
|
994
1361
|
}
|
|
@@ -1060,6 +1427,7 @@ export class LspManager {
|
|
|
1060
1427
|
*/
|
|
1061
1428
|
noteWorkspaceChanges(changes: FileEvent[]): void {
|
|
1062
1429
|
if (changes.length > 0) this.invalidationEpoch++;
|
|
1430
|
+
invalidateProjectConfigCaches(changes);
|
|
1063
1431
|
this.notifyWorkspaceFileChanges(changes);
|
|
1064
1432
|
}
|
|
1065
1433
|
|
|
@@ -1105,34 +1473,63 @@ export class LspManager {
|
|
|
1105
1473
|
processCrashDemand?: { scopes?: readonly string[] };
|
|
1106
1474
|
control?: CodeRequestControl;
|
|
1107
1475
|
}): Promise<WorkspaceRecoveryResult> {
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1476
|
+
const processCrashDemand = options?.processCrashDemand;
|
|
1477
|
+
const demand = processCrashDemand
|
|
1478
|
+
? await this.recoverProcessCrashDemand(
|
|
1479
|
+
(snapshot) => {
|
|
1480
|
+
if (!this.processCrashDiagnosticRouteInScope(snapshot, processCrashDemand)) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
return this.processCrashDiagnosticFiles(snapshot, processCrashDemand);
|
|
1484
|
+
},
|
|
1485
|
+
options?.control,
|
|
1486
|
+
{ skipWhenNoRetainedFile: true },
|
|
1487
|
+
)
|
|
1488
|
+
: {
|
|
1489
|
+
hasSupport: false,
|
|
1490
|
+
requiresFreshEvidence: false,
|
|
1491
|
+
processCrashRecovery: emptyProcessCrashRecoveryReport(),
|
|
1492
|
+
failures: [],
|
|
1493
|
+
failedFiles: [],
|
|
1494
|
+
};
|
|
1495
|
+
const recoveryOptions = demand.requiresFreshEvidence
|
|
1115
1496
|
? { ...options, initialEvidence: undefined }
|
|
1116
1497
|
: options;
|
|
1117
1498
|
const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
|
|
1118
|
-
return this.addProcessCrashDiagnosticEvidence(
|
|
1499
|
+
return this.addProcessCrashDiagnosticEvidence(
|
|
1500
|
+
{ ...result, processCrashRecovery: demand.processCrashRecovery },
|
|
1501
|
+
demand,
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
private processCrashDiagnosticRouteInScope(
|
|
1506
|
+
snapshot: ProcessCrashRecoverySnapshot,
|
|
1507
|
+
demand: { scopes?: readonly string[] },
|
|
1508
|
+
): boolean {
|
|
1509
|
+
return this.processCrashDiagnosticScopes(demand).some(
|
|
1510
|
+
(scope) =>
|
|
1511
|
+
projectRoots.isWithinOrEqual(scope, snapshot.root) ||
|
|
1512
|
+
projectRoots.isWithinOrEqual(snapshot.root, scope),
|
|
1513
|
+
);
|
|
1119
1514
|
}
|
|
1120
1515
|
|
|
1121
1516
|
private processCrashDiagnosticFiles(
|
|
1122
|
-
|
|
1123
|
-
demand: { scopes?: readonly string[] }
|
|
1517
|
+
snapshot: ProcessCrashRecoverySnapshot,
|
|
1518
|
+
demand: { scopes?: readonly string[] },
|
|
1124
1519
|
): string[] {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
return recovery.files.filter(
|
|
1520
|
+
const scopes = this.processCrashDiagnosticScopes(demand);
|
|
1521
|
+
return snapshot.files.filter(
|
|
1128
1522
|
(file) =>
|
|
1129
1523
|
this.isDiagnosticFile(file) &&
|
|
1130
|
-
(
|
|
1131
|
-
scopes.length === 0 ||
|
|
1132
|
-
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file))),
|
|
1524
|
+
scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file)),
|
|
1133
1525
|
);
|
|
1134
1526
|
}
|
|
1135
1527
|
|
|
1528
|
+
private processCrashDiagnosticScopes(demand: { scopes?: readonly string[] }): string[] {
|
|
1529
|
+
const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
|
|
1530
|
+
return scopes && scopes.length > 0 ? scopes : [this.cwd];
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1136
1533
|
private addProcessCrashDiagnosticEvidence(
|
|
1137
1534
|
result: WorkspaceRecoveryResult,
|
|
1138
1535
|
demand: ProcessCrashDemandResult,
|
|
@@ -1184,6 +1581,7 @@ export class LspManager {
|
|
|
1184
1581
|
this.warmedWorkspaceSymbolProjects.clear();
|
|
1185
1582
|
this.warmedSemanticProjects.clear();
|
|
1186
1583
|
this.pendingWarmProbes.clear();
|
|
1584
|
+
this.pendingRestarts.clear();
|
|
1187
1585
|
}
|
|
1188
1586
|
/** Get status of all servers. */
|
|
1189
1587
|
getRunningClientCount(): number {
|
|
@@ -1197,13 +1595,9 @@ export class LspManager {
|
|
|
1197
1595
|
.map((client) => client.name);
|
|
1198
1596
|
}
|
|
1199
1597
|
|
|
1200
|
-
/** Check whether a path belongs to the
|
|
1598
|
+
/** Check whether a path belongs to the automatic diagnostic evidence scope. */
|
|
1201
1599
|
isDiagnosticFile(filePath: string): boolean {
|
|
1202
|
-
|
|
1203
|
-
return (
|
|
1204
|
-
!shouldIgnoreLspPath(relative, this.cwd) &&
|
|
1205
|
-
!isExcludedByPattern(relative, this.excludePatterns)
|
|
1206
|
-
);
|
|
1600
|
+
return this.isAutomaticScopePath(filePath);
|
|
1207
1601
|
}
|
|
1208
1602
|
|
|
1209
1603
|
getDiagnosticEvidence(): DiagnosticEvidenceSummary {
|
|
@@ -1218,7 +1612,7 @@ export class LspManager {
|
|
|
1218
1612
|
name: client.name,
|
|
1219
1613
|
status: client.status === "running" ? "running" : "error",
|
|
1220
1614
|
root: client.root,
|
|
1221
|
-
openFiles: client.openFiles,
|
|
1615
|
+
openFiles: client.openFiles.filter((file) => this.isAutomaticScopePath(file)),
|
|
1222
1616
|
});
|
|
1223
1617
|
}
|
|
1224
1618
|
return { servers };
|
|
@@ -1259,8 +1653,7 @@ export class LspManager {
|
|
|
1259
1653
|
const openFiles = activeServers.get(server.name) ?? new Set<string>();
|
|
1260
1654
|
for (const file of server.openFiles) {
|
|
1261
1655
|
const relativeFile = displayRelativeFilePath(file, this.cwd);
|
|
1262
|
-
if (
|
|
1263
|
-
if (isExcludedByPattern(relativeFile, this.excludePatterns)) continue;
|
|
1656
|
+
if (!this.isAutomaticScopePath(relativeFile)) continue;
|
|
1264
1657
|
openFiles.add(relativeFile);
|
|
1265
1658
|
}
|
|
1266
1659
|
activeServers.set(server.name, openFiles);
|
|
@@ -1286,7 +1679,7 @@ export class LspManager {
|
|
|
1286
1679
|
.map((entry) => ({
|
|
1287
1680
|
...entry,
|
|
1288
1681
|
openFiles: entry.openFiles.filter((file) =>
|
|
1289
|
-
isPathRelevant(file, normalizedPaths, this.cwd),
|
|
1682
|
+
isPathRelevant(file, normalizedPaths, this.cwd, this.automaticPathPolicy),
|
|
1290
1683
|
),
|
|
1291
1684
|
}))
|
|
1292
1685
|
.filter((entry) => entry.openFiles.length > 0);
|
|
@@ -1318,7 +1711,9 @@ export class LspManager {
|
|
|
1318
1711
|
const fileDiags = new Map<string, { errors: number; warnings: number }>();
|
|
1319
1712
|
for (const snapshot of snapshots) {
|
|
1320
1713
|
for (const entry of snapshot.entries) {
|
|
1321
|
-
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd,
|
|
1714
|
+
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
|
|
1715
|
+
this.isAutomaticScopePath(file),
|
|
1716
|
+
);
|
|
1322
1717
|
}
|
|
1323
1718
|
}
|
|
1324
1719
|
|
|
@@ -1332,7 +1727,7 @@ export class LspManager {
|
|
|
1332
1727
|
entries: collectOutstandingDiagnosticsDetailed(
|
|
1333
1728
|
snapshots.map((snapshot) => snapshot.entries),
|
|
1334
1729
|
this.cwd,
|
|
1335
|
-
this.
|
|
1730
|
+
(file) => this.isAutomaticScopePath(file),
|
|
1336
1731
|
maxSeverity,
|
|
1337
1732
|
),
|
|
1338
1733
|
current,
|
|
@@ -1364,7 +1759,9 @@ export class LspManager {
|
|
|
1364
1759
|
});
|
|
1365
1760
|
}
|
|
1366
1761
|
for (const entry of snapshot.entries) {
|
|
1367
|
-
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd,
|
|
1762
|
+
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
|
|
1763
|
+
this.isAutomaticScopePath(file),
|
|
1764
|
+
);
|
|
1368
1765
|
}
|
|
1369
1766
|
}
|
|
1370
1767
|
const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
|
|
@@ -1396,7 +1793,7 @@ export class LspManager {
|
|
|
1396
1793
|
entries: collectOutstandingDiagnosticSummaryEntries(
|
|
1397
1794
|
snapshots,
|
|
1398
1795
|
this.cwd,
|
|
1399
|
-
this.
|
|
1796
|
+
(file) => this.isAutomaticScopePath(file),
|
|
1400
1797
|
maxSeverity,
|
|
1401
1798
|
),
|
|
1402
1799
|
current:
|
|
@@ -1413,7 +1810,7 @@ export class LspManager {
|
|
|
1413
1810
|
if (normalizedPaths.length === 0) return null;
|
|
1414
1811
|
return formatOutstandingDiagnosticsSummaryText(
|
|
1415
1812
|
this.getOutstandingDiagnosticSummary(maxSeverity).filter((entry) =>
|
|
1416
|
-
isPathRelevant(entry.file, normalizedPaths, this.cwd),
|
|
1813
|
+
isPathRelevant(entry.file, normalizedPaths, this.cwd, this.automaticPathPolicy),
|
|
1417
1814
|
),
|
|
1418
1815
|
maxFiles,
|
|
1419
1816
|
);
|
|
@@ -1443,7 +1840,7 @@ export class LspManager {
|
|
|
1443
1840
|
entries: collectOutstandingDiagnosticsDetailed(
|
|
1444
1841
|
snapshots.map((snapshot) => snapshot.entries),
|
|
1445
1842
|
this.cwd,
|
|
1446
|
-
this.
|
|
1843
|
+
(file) => this.isAutomaticScopePath(file),
|
|
1447
1844
|
maxSeverity,
|
|
1448
1845
|
),
|
|
1449
1846
|
current:
|
|
@@ -1472,6 +1869,7 @@ export class LspManager {
|
|
|
1472
1869
|
for (const client of this.clients.values()) {
|
|
1473
1870
|
for (const document of client.getDiagnosticSnapshot().documents) {
|
|
1474
1871
|
const file = relativeFilePathFromUri(document.uri, this.cwd);
|
|
1872
|
+
if (!this.isAutomaticScopePath(file)) continue;
|
|
1475
1873
|
totalFiles++;
|
|
1476
1874
|
accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
|
|
1477
1875
|
}
|
|
@@ -1491,8 +1889,7 @@ export class LspManager {
|
|
|
1491
1889
|
const byFile = new Map<string, DiagnosticEvidenceDocument>();
|
|
1492
1890
|
for (const document of documents) {
|
|
1493
1891
|
const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
|
|
1494
|
-
if (
|
|
1495
|
-
if (isExcludedByPattern(file, this.excludePatterns)) continue;
|
|
1892
|
+
if (!this.isAutomaticScopePath(file)) continue;
|
|
1496
1893
|
const existing = byFile.get(file);
|
|
1497
1894
|
if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
|
|
1498
1895
|
byFile.set(file, { file, status: document.status });
|
|
@@ -1517,9 +1914,9 @@ export class LspManager {
|
|
|
1517
1914
|
);
|
|
1518
1915
|
}
|
|
1519
1916
|
const demand = await this.recoverProcessCrashDemand(
|
|
1520
|
-
(
|
|
1521
|
-
|
|
1522
|
-
routeMatches(
|
|
1917
|
+
(snapshot) =>
|
|
1918
|
+
snapshot.failedClient.serverCapabilities?.workspaceSymbolProvider &&
|
|
1919
|
+
routeMatches(snapshot.failedClient)
|
|
1523
1920
|
? []
|
|
1524
1921
|
: null,
|
|
1525
1922
|
control,
|
|
@@ -1539,7 +1936,10 @@ export class LspManager {
|
|
|
1539
1936
|
}
|
|
1540
1937
|
|
|
1541
1938
|
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
|
|
1542
|
-
findWarmTargets:
|
|
1939
|
+
findWarmTargets: (root, rootMarkers, fileTypes) =>
|
|
1940
|
+
findWorkspaceSymbolWarmTargets(root, rootMarkers, fileTypes, {
|
|
1941
|
+
policy: this.automaticPathPolicy,
|
|
1942
|
+
}),
|
|
1543
1943
|
getWarmPosition: getWorkspaceSymbolWarmPosition,
|
|
1544
1944
|
collect,
|
|
1545
1945
|
routeMatches,
|
|
@@ -1637,28 +2037,21 @@ export class LspManager {
|
|
|
1637
2037
|
for (const target of warmTargets) {
|
|
1638
2038
|
const projectKey = this.workspaceSymbolProjectKey(client.name, target.projectRoot);
|
|
1639
2039
|
if (this.warmedWorkspaceSymbolProjects.has(projectKey)) continue;
|
|
1640
|
-
if (this.hasOpenFileInProject(client, target.projectRoot)) {
|
|
1641
|
-
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
2040
|
|
|
1645
2041
|
const openedClient = await this.ensureFileOpen(target.file);
|
|
1646
2042
|
if (!openedClient) continue;
|
|
1647
2043
|
|
|
1648
2044
|
// A cancelled pass must not mark projects warmed on its way out.
|
|
1649
2045
|
throwIfCodeRequestInterrupted(control);
|
|
1650
|
-
this.
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
if (
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
if (control) await openedClient.hover(target.file, hoverPosition, control);
|
|
1660
|
-
else await openedClient.hover(target.file, hoverPosition);
|
|
1661
|
-
}
|
|
2046
|
+
const available = await this.probeDocumentSymbols(
|
|
2047
|
+
openedClient,
|
|
2048
|
+
target.file,
|
|
2049
|
+
getWarmPosition,
|
|
2050
|
+
control,
|
|
2051
|
+
);
|
|
2052
|
+
if (available) {
|
|
2053
|
+
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
2054
|
+
warmedAny = true;
|
|
1662
2055
|
}
|
|
1663
2056
|
|
|
1664
2057
|
const collected = await collect(this.clients.values(), query);
|
|
@@ -1703,35 +2096,56 @@ export class LspManager {
|
|
|
1703
2096
|
try {
|
|
1704
2097
|
await raceRequestControl(probe, control);
|
|
1705
2098
|
} finally {
|
|
1706
|
-
this.pendingWarmProbes.
|
|
2099
|
+
if (this.pendingWarmProbes.get(projectKey) === probe) {
|
|
2100
|
+
this.pendingWarmProbes.delete(projectKey);
|
|
2101
|
+
}
|
|
1707
2102
|
}
|
|
1708
2103
|
}
|
|
1709
2104
|
|
|
1710
2105
|
private async performWarmProbe(
|
|
1711
|
-
|
|
2106
|
+
client: LspClient,
|
|
1712
2107
|
resolvedFile: string,
|
|
1713
2108
|
projectKey: string,
|
|
1714
2109
|
control?: CodeRequestControl,
|
|
1715
2110
|
): Promise<void> {
|
|
2111
|
+
const key = clientKey(client.name, client.root);
|
|
2112
|
+
if (this.clients.get(key) !== client) return;
|
|
1716
2113
|
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
1717
|
-
if (!openedClient) return;
|
|
1718
|
-
|
|
1719
|
-
this.warmedSemanticProjects.add(projectKey);
|
|
2114
|
+
if (!openedClient || openedClient !== client || this.clients.get(key) !== client) return;
|
|
1720
2115
|
|
|
1721
2116
|
// The probe is shared state for concurrent callers. One caller's
|
|
1722
2117
|
// cancellation must not cancel the shared in-flight queries, so only the
|
|
1723
2118
|
// Debug Operation ID is forwarded; each caller races its own wait above.
|
|
1724
2119
|
const sharedControl = { operationId: control?.operationId };
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2120
|
+
const available = await this.probeDocumentSymbols(
|
|
2121
|
+
openedClient,
|
|
2122
|
+
resolvedFile,
|
|
2123
|
+
getWorkspaceSymbolWarmPosition,
|
|
2124
|
+
sharedControl,
|
|
2125
|
+
);
|
|
2126
|
+
if (available && this.clients.get(key) === client) {
|
|
2127
|
+
this.warmedSemanticProjects.add(projectKey);
|
|
1730
2128
|
}
|
|
1731
2129
|
}
|
|
1732
2130
|
|
|
1733
|
-
private
|
|
1734
|
-
|
|
2131
|
+
private async probeDocumentSymbols(
|
|
2132
|
+
client: LspClient,
|
|
2133
|
+
file: string,
|
|
2134
|
+
getWarmPosition: (
|
|
2135
|
+
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
2136
|
+
) => import("../config/types.ts").Position | null,
|
|
2137
|
+
control?: CodeRequestControl,
|
|
2138
|
+
): Promise<boolean> {
|
|
2139
|
+
const symbols = control
|
|
2140
|
+
? await client.documentSymbols(file, control)
|
|
2141
|
+
: await client.documentSymbols(file);
|
|
2142
|
+
if (symbols.kind === "unavailable") return false;
|
|
2143
|
+
const hoverPosition = getWarmPosition(symbols.data);
|
|
2144
|
+
if (hoverPosition) {
|
|
2145
|
+
if (control) await client.hover(file, hoverPosition, control);
|
|
2146
|
+
else await client.hover(file, hoverPosition);
|
|
2147
|
+
}
|
|
2148
|
+
return true;
|
|
1735
2149
|
}
|
|
1736
2150
|
|
|
1737
2151
|
private workspaceSymbolProjectKey(serverName: string, projectRoot: string): string {
|
|
@@ -1766,6 +2180,17 @@ export class LspManager {
|
|
|
1766
2180
|
}
|
|
1767
2181
|
}
|
|
1768
2182
|
|
|
2183
|
+
function invalidateProjectConfigCaches(changes: readonly FileEvent[]): void {
|
|
2184
|
+
for (const change of changes) {
|
|
2185
|
+
const configPath = uriToFile(change.uri);
|
|
2186
|
+
if (!isProjectConfigFileName(path.basename(configPath))) continue;
|
|
2187
|
+
invalidateTsconfigCacheForConfig(configPath);
|
|
2188
|
+
if (change.type === FileChangeType.Created) {
|
|
2189
|
+
invalidateTsconfigCacheForConfigDir(path.dirname(configPath));
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1769
2194
|
function collectClientDiagnosticEvidenceDocuments(
|
|
1770
2195
|
snapshots: Iterable<ClientDiagnosticSnapshot>,
|
|
1771
2196
|
cwd: string,
|
|
@@ -1781,14 +2206,14 @@ function collectClientDiagnosticEvidenceDocuments(
|
|
|
1781
2206
|
function collectOutstandingDiagnosticSummaryEntries(
|
|
1782
2207
|
snapshots: Iterable<ClientDiagnosticSnapshot>,
|
|
1783
2208
|
cwd: string,
|
|
1784
|
-
|
|
2209
|
+
includeFile: (file: string) => boolean,
|
|
1785
2210
|
maxSeverity: number,
|
|
1786
2211
|
): OutstandingDiagnosticSummaryEntry[] {
|
|
1787
2212
|
const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
|
|
1788
2213
|
for (const snapshot of snapshots) {
|
|
1789
2214
|
for (const entry of snapshot.entries) {
|
|
1790
2215
|
const file = relativeFilePathFromUri(entry.uri, cwd);
|
|
1791
|
-
if (
|
|
2216
|
+
if (!includeFile(file)) continue;
|
|
1792
2217
|
const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
|
|
1793
2218
|
const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
|
|
1794
2219
|
if (next.total > 0) fileDiags.set(file, next);
|