@mrclrchtr/supi-code-intelligence 4.9.0 → 5.0.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 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +215 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +370 -266
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +238 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +26 -6
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +236 -24
- package/src/analysis/health/recovery.ts +6 -2
- package/src/analysis/provider.ts +58 -0
- package/src/analysis/readiness.ts +30 -9
- package/src/analysis/refactor/apply.ts +59 -20
- package/src/analysis/refactor/mutation-authority.ts +143 -0
- package/src/analysis/refactor/position.ts +47 -0
- package/src/analysis/refactor/safety.ts +18 -10
- package/src/analysis/relations/callees.ts +9 -1
- package/src/analysis/relations/types.ts +5 -2
- package/src/analysis/search/ast-scan-timing.ts +9 -4
- package/src/analysis/search/ast-scan.ts +1 -1
- package/src/analysis/search/deadline.ts +2 -2
- package/src/analysis/search/pattern-analysis.ts +46 -32
- package/src/analysis/search/pattern.ts +12 -2
- package/src/analysis/target/anchored.ts +20 -11
- package/src/analysis/target/file.ts +18 -11
- package/src/analysis/target/identity.ts +19 -3
- package/src/analysis/target/symbol.ts +36 -15
- package/src/api.ts +4 -0
- package/src/diagnostics/evidence.ts +51 -0
- package/src/session/capability-adapter.ts +12 -3
- package/src/session/find-workflow.ts +17 -7
- package/src/session/graph/collect.ts +18 -2
- package/src/session/graph-workflow.ts +19 -12
- package/src/session/health-refresh.ts +148 -0
- package/src/session/health-types.ts +37 -15
- package/src/session/health-workflow.ts +37 -39
- package/src/session/inspect/collect.ts +20 -3
- package/src/session/inspect-workflow.ts +10 -5
- package/src/session/orientation/collect.ts +10 -1
- package/src/session/orientation/context-facts.ts +3 -1
- package/src/session/orientation/context-sections.ts +20 -7
- package/src/session/orientation/gather.ts +17 -11
- package/src/session/orientation-types.ts +2 -1
- package/src/session/orientation-workflow.ts +15 -8
- package/src/session/refactor-plans.ts +2 -0
- package/src/session/refactor-workflow.ts +59 -14
- package/src/session/session.ts +4 -1
- package/src/session/target-workflow.ts +54 -23
- package/src/session/workflow-control.ts +7 -3
- package/src/substrate/lsp/lifecycle.ts +26 -46
- package/src/substrate/lsp/maintenance.ts +199 -25
- package/src/substrate/lsp/state.ts +2 -2
- package/src/substrate/workspace-provider-host.ts +25 -3
- package/src/tool/find/execute.ts +7 -1
- package/src/tool/find/render.ts +20 -3
- package/src/tool/find/tui.ts +23 -21
- package/src/tool/graph/execute.ts +23 -3
- package/src/tool/graph/tui.ts +59 -46
- package/src/tool/health/execute.ts +7 -2
- package/src/tool/health/markdown.ts +56 -14
- package/src/tool/health/refresh-status.ts +105 -0
- package/src/tool/health/tui.ts +95 -98
- package/src/tool/infra/workflow-control.ts +8 -0
- package/src/tool/inspect/execute.ts +7 -1
- package/src/tool/inspect/tui.ts +26 -18
- package/src/tool/orientation/execute.ts +21 -3
- package/src/tool/orientation/tui.ts +51 -35
- package/src/tool/refactor-apply/execute.ts +3 -0
- package/src/tool/refactor-apply/tui.ts +43 -25
- package/src/tool/refactor-plan/execute.ts +4 -0
- package/src/tool/refactor-plan/tui.ts +33 -20
- package/src/tool/register.ts +127 -22
- package/src/tool/resolve/execute.ts +18 -1
- package/src/tool/resolve/tui.ts +42 -24
- package/src/tool/result/display.ts +107 -0
- package/src/tool/result/errors.ts +20 -17
- package/src/tool/result/find.ts +52 -8
- package/src/tool/result/graph.ts +44 -1
- package/src/tool/result/health.ts +53 -1
- package/src/tool/result/inspect.ts +113 -1
- package/src/tool/result/orientation.ts +66 -14
- package/src/tool/result/refactor.ts +36 -4
- package/src/tool/result/resolve.ts +44 -7
- package/src/tool/result/types.ts +20 -0
- package/src/types/execution.ts +18 -5
- package/src/types/index.ts +8 -1
- package/src/ui/status-command.ts +5 -3
- package/src/ui/status-overlay.ts +4 -0
- package/src/ui/tui/common.ts +108 -10
- package/src/ui/tui/display.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -247
|
@@ -2,9 +2,23 @@
|
|
|
2
2
|
// biome-ignore-all lint/style/noExcessiveLinesPerFile: LspManager stays cohesive; recovery and sync helpers are split into manager-*.ts modules.
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CodeQueryResult,
|
|
7
|
+
type CodeRequestControl,
|
|
8
|
+
isCodeRequestInterruption,
|
|
9
|
+
throwIfCodeRequestInterrupted,
|
|
10
|
+
unavailableCodeQuery,
|
|
11
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
6
12
|
import * as projectRoots from "@mrclrchtr/supi-core/project";
|
|
7
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
LspClient,
|
|
15
|
+
type LspClientLifecycleTransitionKind,
|
|
16
|
+
RECOVERY_CLIENT_STARTUP_BOUND_MS,
|
|
17
|
+
withTimeout,
|
|
18
|
+
} from "../client/client.ts";
|
|
19
|
+
|
|
20
|
+
export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
|
|
21
|
+
|
|
8
22
|
import { getServerForFile } from "../config/config.ts";
|
|
9
23
|
import type {
|
|
10
24
|
DetectedProjectServer,
|
|
@@ -22,6 +36,12 @@ import {
|
|
|
22
36
|
createOutstandingDiagnosticSummary,
|
|
23
37
|
relativeFilePathFromUri,
|
|
24
38
|
} from "../diagnostics/diagnostic-summary.ts";
|
|
39
|
+
import {
|
|
40
|
+
type DiagnosticEvidenceDocument,
|
|
41
|
+
type DiagnosticEvidenceSummary,
|
|
42
|
+
summarizeDiagnosticEvidence,
|
|
43
|
+
} from "../diagnostics/evidence.ts";
|
|
44
|
+
import { raceRequestControl } from "../session/readiness.ts";
|
|
25
45
|
import {
|
|
26
46
|
displayRelativeFilePath,
|
|
27
47
|
formatCoverageSummaryText,
|
|
@@ -30,7 +50,7 @@ import {
|
|
|
30
50
|
normalizeRelevantPaths,
|
|
31
51
|
shouldIgnoreLspPath,
|
|
32
52
|
} from "../summary.ts";
|
|
33
|
-
import { commandExists, resolveSessionPath } from "../utils.ts";
|
|
53
|
+
import { commandExists, resolveSessionPath, uriToFile } from "../utils.ts";
|
|
34
54
|
import {
|
|
35
55
|
closeFileAcrossClients,
|
|
36
56
|
pruneMissingFilesFromClients,
|
|
@@ -55,7 +75,10 @@ import type {
|
|
|
55
75
|
OutstandingDiagnosticSummaryEntry,
|
|
56
76
|
ServerStatus,
|
|
57
77
|
} from "./manager-types.ts";
|
|
58
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl,
|
|
80
|
+
type WorkspaceRecoveryResult,
|
|
81
|
+
} from "./manager-workspace-recovery.ts";
|
|
59
82
|
import {
|
|
60
83
|
collectWorkspaceSymbols,
|
|
61
84
|
findWorkspaceSymbolWarmTargets,
|
|
@@ -73,6 +96,24 @@ type FileRoute = {
|
|
|
73
96
|
key: string;
|
|
74
97
|
};
|
|
75
98
|
|
|
99
|
+
type ClientRestartResult = {
|
|
100
|
+
key: string;
|
|
101
|
+
serverName: string;
|
|
102
|
+
files: string[];
|
|
103
|
+
restarted: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type ManagerLifecycleTransitionKind = LspClientLifecycleTransitionKind | "recovery";
|
|
107
|
+
|
|
108
|
+
/** Package-internal aggregate lifecycle snapshot for the runtime controller. */
|
|
109
|
+
export interface ManagerLifecycleTransition {
|
|
110
|
+
readonly kind: ManagerLifecycleTransitionKind;
|
|
111
|
+
readonly semanticReady: boolean;
|
|
112
|
+
readonly projectServers: readonly ProjectServerInfo[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
|
|
116
|
+
|
|
76
117
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
77
118
|
export class LspManager {
|
|
78
119
|
/** Active clients keyed by "serverName:root" */
|
|
@@ -83,10 +124,16 @@ export class LspManager {
|
|
|
83
124
|
private commandAvailability = new Map<string, boolean>();
|
|
84
125
|
/** Guards against concurrent client creation for the same server:root key */
|
|
85
126
|
private pendingStarts = new Map<string, Promise<LspClient | null>>();
|
|
127
|
+
/** Monotonic ownership generation for each replaceable client route. */
|
|
128
|
+
private clientGenerations = new Map<string, number>();
|
|
86
129
|
/** Preferred project roots discovered by proactive scan or lazy startup */
|
|
87
130
|
private knownRoots = new Map<string, string[]>();
|
|
88
131
|
/** User-configured gitignore-style exclude patterns */
|
|
89
132
|
private excludePatterns: string[] = [];
|
|
133
|
+
/** Monotonic workspace invalidation generation advanced by each invalidation event. */
|
|
134
|
+
private invalidationEpoch = 0;
|
|
135
|
+
/** Invalidation generation in which each route last received a recovery restart. */
|
|
136
|
+
private recoveryRestartEpochs = new Map<string, number>();
|
|
90
137
|
/** Project roots already warmed for workspace-symbol queries. */
|
|
91
138
|
private warmedWorkspaceSymbolProjects = new Set<string>();
|
|
92
139
|
/** Project roots whose semantic state was warmed with a readiness probe. */
|
|
@@ -96,6 +143,7 @@ export class LspManager {
|
|
|
96
143
|
constructor(
|
|
97
144
|
private readonly config: LspConfig,
|
|
98
145
|
private readonly cwd: string,
|
|
146
|
+
private readonly onLifecycleTransition?: ManagerLifecycleListener,
|
|
99
147
|
) {}
|
|
100
148
|
getCwd(): string {
|
|
101
149
|
return this.cwd;
|
|
@@ -152,6 +200,15 @@ export class LspManager {
|
|
|
152
200
|
|
|
153
201
|
return reason;
|
|
154
202
|
}
|
|
203
|
+
|
|
204
|
+
/** Return the open document version without starting a server. */
|
|
205
|
+
getOpenDocumentVersion(filePath: string): number | null {
|
|
206
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
207
|
+
return (
|
|
208
|
+
this.getExistingClientForFile(resolvedPath)?.getOpenDocumentVersion(resolvedPath) ?? null
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
155
212
|
/** Get or create an LSP client for the given file. */
|
|
156
213
|
async getClientForFile(filePath: string): Promise<LspClient | null> {
|
|
157
214
|
const route = this.resolveFileRoute(filePath);
|
|
@@ -194,6 +251,53 @@ export class LspManager {
|
|
|
194
251
|
}
|
|
195
252
|
}
|
|
196
253
|
|
|
254
|
+
private createClient(
|
|
255
|
+
serverName: string,
|
|
256
|
+
serverConfig: ServerConfig,
|
|
257
|
+
root: string,
|
|
258
|
+
key: string,
|
|
259
|
+
): LspClient {
|
|
260
|
+
const generation = (this.clientGenerations.get(key) ?? 0) + 1;
|
|
261
|
+
this.clientGenerations.set(key, generation);
|
|
262
|
+
let client: LspClient;
|
|
263
|
+
client = new LspClient(
|
|
264
|
+
serverName,
|
|
265
|
+
serverConfig,
|
|
266
|
+
root,
|
|
267
|
+
(kind) => {
|
|
268
|
+
this.handleClientLifecycle(key, generation, client, kind);
|
|
269
|
+
},
|
|
270
|
+
this.cwd,
|
|
271
|
+
);
|
|
272
|
+
return client;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private handleClientLifecycle(
|
|
276
|
+
key: string,
|
|
277
|
+
generation: number,
|
|
278
|
+
client: LspClient,
|
|
279
|
+
kind: LspClientLifecycleTransitionKind,
|
|
280
|
+
): void {
|
|
281
|
+
if (this.clientGenerations.get(key) !== generation) return;
|
|
282
|
+
if (this.clients.get(key) !== client) return;
|
|
283
|
+
const aggregateKind =
|
|
284
|
+
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
285
|
+
this.publishLifecycle(aggregateKind);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private publishLifecycle(kind: ManagerLifecycleTransitionKind): void {
|
|
289
|
+
if (!this.onLifecycleTransition) return;
|
|
290
|
+
const projectServers = this.getKnownProjectServers([]);
|
|
291
|
+
const semanticReady = projectServers.some(
|
|
292
|
+
(server) => server.status === "running" && server.ready,
|
|
293
|
+
);
|
|
294
|
+
try {
|
|
295
|
+
this.onLifecycleTransition({ kind, semanticReady, projectServers });
|
|
296
|
+
} catch {
|
|
297
|
+
// Runtime lifecycle consumers must not alter manager behavior.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
197
301
|
/**
|
|
198
302
|
* Perform the actual server start — extracted so the public method can
|
|
199
303
|
* deduplicate via pendingStarts without wrapping the entire body.
|
|
@@ -211,7 +315,7 @@ export class LspManager {
|
|
|
211
315
|
}
|
|
212
316
|
|
|
213
317
|
// Spawn new client
|
|
214
|
-
const client =
|
|
318
|
+
const client = this.createClient(serverName, serverConfig, root, key);
|
|
215
319
|
this.clearWarmedWorkspaceSymbolProjects(serverName, root);
|
|
216
320
|
this.clearWarmedSemanticProjects(serverName, root);
|
|
217
321
|
this.clearPendingWarmProbes(serverName, root);
|
|
@@ -247,33 +351,51 @@ export class LspManager {
|
|
|
247
351
|
return route ? (this.clients.get(route.key) ?? null) : null;
|
|
248
352
|
}
|
|
249
353
|
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Restart the clients that own the supplied file paths, if any are active.
|
|
356
|
+
*
|
|
357
|
+
* Each route restarts at most once per invalidation generation. Pass
|
|
358
|
+
* `pushOnly` to restrict restarts to clients without pull diagnostics.
|
|
359
|
+
* The loop observes request cancellation between route restarts.
|
|
360
|
+
*/
|
|
361
|
+
async restartClientsForFiles(
|
|
362
|
+
filePaths: string[],
|
|
363
|
+
options?: { pushOnly?: boolean; control?: CodeRequestControl },
|
|
364
|
+
): Promise<ClientRestartResult[]> {
|
|
365
|
+
const restarted: ClientRestartResult[] = [];
|
|
253
366
|
const seen = new Set<string>();
|
|
254
367
|
|
|
255
368
|
for (const filePath of filePaths) {
|
|
256
369
|
const client = this.getExistingClientForFile(filePath);
|
|
257
370
|
if (!client) continue;
|
|
371
|
+
if (options?.pushOnly && client.hasDiagnosticProvider) continue;
|
|
258
372
|
|
|
259
373
|
const key = clientKey(client.name, client.root);
|
|
260
374
|
if (seen.has(key)) continue;
|
|
261
375
|
seen.add(key);
|
|
376
|
+
if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
|
|
262
377
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
378
|
+
// Observe cancellation between route restarts so a cancelled pass does
|
|
379
|
+
// not keep restarting further clients.
|
|
380
|
+
throwIfCodeRequestInterrupted(options?.control);
|
|
381
|
+
const result = await this.restartClient(client);
|
|
382
|
+
this.recoveryRestartEpochs.set(key, this.invalidationEpoch);
|
|
383
|
+
restarted.push({ key, serverName: client.name, ...result });
|
|
266
384
|
}
|
|
267
385
|
|
|
268
386
|
return restarted;
|
|
269
387
|
}
|
|
270
388
|
|
|
271
|
-
private async restartClient(client: LspClient): Promise<boolean> {
|
|
389
|
+
private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
|
|
272
390
|
const key = clientKey(client.name, client.root);
|
|
273
391
|
const serverConfig = this.config.servers[client.name];
|
|
274
|
-
if (!serverConfig) return false;
|
|
392
|
+
if (!serverConfig) return { files: [], restarted: false };
|
|
275
393
|
|
|
276
394
|
const openFiles = client.openFiles;
|
|
395
|
+
const trackedDiagnosticFiles = client
|
|
396
|
+
.getDiagnosticSnapshot()
|
|
397
|
+
.documents.map((document) => uriToFile(document.uri));
|
|
398
|
+
const files = [...new Set([...openFiles, ...trackedDiagnosticFiles])];
|
|
277
399
|
try {
|
|
278
400
|
await client.shutdown();
|
|
279
401
|
} catch {
|
|
@@ -286,25 +408,37 @@ export class LspManager {
|
|
|
286
408
|
this.clearWarmedSemanticProjects(client.name, client.root);
|
|
287
409
|
this.clearPendingWarmProbes(client.name, client.root);
|
|
288
410
|
|
|
289
|
-
const replacement =
|
|
411
|
+
const replacement = this.createClient(client.name, serverConfig, client.root, key);
|
|
290
412
|
this.clients.set(key, replacement);
|
|
291
413
|
rememberKnownRoot(this.knownRoots, client.name, client.root);
|
|
292
414
|
|
|
293
415
|
try {
|
|
294
|
-
await
|
|
295
|
-
|
|
296
|
-
|
|
416
|
+
await withTimeout(
|
|
417
|
+
replacement.start(),
|
|
418
|
+
RECOVERY_CLIENT_STARTUP_BOUND_MS,
|
|
419
|
+
"replacement client startup bound exceeded",
|
|
420
|
+
);
|
|
421
|
+
const failedFiles: string[] = [];
|
|
422
|
+
for (const filePath of files) {
|
|
423
|
+
if (!fs.existsSync(filePath)) {
|
|
424
|
+
failedFiles.push(filePath);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
297
427
|
try {
|
|
298
428
|
replacement.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
299
429
|
} catch {
|
|
300
|
-
|
|
430
|
+
failedFiles.push(filePath);
|
|
301
431
|
}
|
|
302
432
|
}
|
|
303
|
-
|
|
433
|
+
for (const filePath of failedFiles) replacement.markFailedFile(filePath);
|
|
434
|
+
return { files, restarted: true };
|
|
304
435
|
} catch {
|
|
305
436
|
this.clients.delete(key);
|
|
306
437
|
this.unavailable.set(key, "start-failed");
|
|
307
|
-
|
|
438
|
+
// A bound-exceeded start may have spawned a live process; terminate
|
|
439
|
+
// the tree so the orphan cannot outlive the failed restart.
|
|
440
|
+
void replacement.forceKill().catch(() => {});
|
|
441
|
+
return { files, restarted: false };
|
|
308
442
|
}
|
|
309
443
|
}
|
|
310
444
|
|
|
@@ -347,41 +481,68 @@ export class LspManager {
|
|
|
347
481
|
}
|
|
348
482
|
|
|
349
483
|
/** Wait until the client that owns a file is query-ready, then run a light warm-up probe. */
|
|
350
|
-
async waitUntilFileReady(
|
|
484
|
+
async waitUntilFileReady(
|
|
485
|
+
filePath: string,
|
|
486
|
+
control?: CodeRequestControl,
|
|
487
|
+
): Promise<LspClient | null> {
|
|
488
|
+
throwIfCodeRequestInterrupted(control);
|
|
351
489
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
352
490
|
const client = await this.getClientForFile(resolvedPath);
|
|
353
491
|
if (!client) return null;
|
|
354
|
-
|
|
355
|
-
|
|
492
|
+
// The caller's wait stops on cancellation even though the shared
|
|
493
|
+
// readiness state keeps its own lifecycle.
|
|
494
|
+
await client.getReady(control);
|
|
495
|
+
await this.warmSemanticProject(client, resolvedPath, true, control);
|
|
496
|
+
throwIfCodeRequestInterrupted(control);
|
|
356
497
|
return client;
|
|
357
498
|
}
|
|
358
499
|
|
|
359
500
|
/**
|
|
360
|
-
* Wait until
|
|
361
|
-
* Returns the number of concrete clients that
|
|
501
|
+
* Wait until one started client is query-ready, then warm its project.
|
|
502
|
+
* Returns the number of concrete clients that are ready after the warm-up.
|
|
362
503
|
*/
|
|
363
|
-
async waitUntilWorkspaceReady(): Promise<number> {
|
|
504
|
+
async waitUntilWorkspaceReady(control?: CodeRequestControl): Promise<number> {
|
|
505
|
+
throwIfCodeRequestInterrupted(control);
|
|
364
506
|
const activeClients = Array.from(this.clients.values()).filter(
|
|
365
507
|
(client) => client.status === "running",
|
|
366
508
|
);
|
|
367
|
-
|
|
509
|
+
if (activeClients.length === 0) return 0;
|
|
368
510
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
511
|
+
const alreadyReady = activeClients.find((client) => client.ready);
|
|
512
|
+
const firstReady =
|
|
513
|
+
alreadyReady ??
|
|
514
|
+
(await Promise.any(
|
|
515
|
+
activeClients.map(async (client) => {
|
|
516
|
+
await client.getReady(control);
|
|
517
|
+
if (client.status !== "running" || !client.ready) {
|
|
518
|
+
throw new Error("LSP client did not reach concrete readiness.");
|
|
519
|
+
}
|
|
520
|
+
return client;
|
|
521
|
+
}),
|
|
522
|
+
).catch(() => null));
|
|
523
|
+
// Promise.any swallows individual rejections, so an interruption that
|
|
524
|
+
// raced the readiness wait must be re-raised before it degrades into a
|
|
525
|
+
// zero-ready outcome.
|
|
526
|
+
throwIfCodeRequestInterrupted(control);
|
|
527
|
+
if (!firstReady) return 0;
|
|
528
|
+
|
|
529
|
+
const serverConfig = this.config.servers[firstReady.name];
|
|
530
|
+
if (serverConfig) {
|
|
372
531
|
const target = findWorkspaceSymbolWarmTargets(
|
|
373
|
-
|
|
532
|
+
firstReady.root,
|
|
374
533
|
serverConfig.rootMarkers,
|
|
375
534
|
serverConfig.fileTypes,
|
|
376
535
|
)[0];
|
|
377
|
-
if (
|
|
378
|
-
await this.warmSemanticProject(client, target.file);
|
|
536
|
+
if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
|
|
379
537
|
}
|
|
380
|
-
|
|
538
|
+
|
|
539
|
+
throwIfCodeRequestInterrupted(control);
|
|
540
|
+
return activeClients.filter((client) => client.status === "running" && client.ready).length;
|
|
381
541
|
}
|
|
382
542
|
async syncFileAndGetDiagnostics(
|
|
383
543
|
filePath: string,
|
|
384
544
|
maxSeverity: number = 1,
|
|
545
|
+
control?: CodeRequestControl,
|
|
385
546
|
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
386
547
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
387
548
|
const client = await this.getClientForFile(resolvedPath);
|
|
@@ -389,11 +550,11 @@ export class LspManager {
|
|
|
389
550
|
return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
|
|
390
551
|
}
|
|
391
552
|
try {
|
|
392
|
-
return
|
|
393
|
-
kind: "completed",
|
|
394
|
-
data: await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity),
|
|
395
|
-
};
|
|
553
|
+
return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, control);
|
|
396
554
|
} catch (error) {
|
|
555
|
+
// Cancellation must propagate; a cancelled caller no longer awaits a
|
|
556
|
+
// diagnostic result or the file cleanup side effects.
|
|
557
|
+
if (isCodeRequestInterruption(error, control)) throw error;
|
|
397
558
|
this.closeFile(resolvedPath);
|
|
398
559
|
const detail = error instanceof Error ? error.message : String(error);
|
|
399
560
|
return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
|
|
@@ -407,9 +568,12 @@ export class LspManager {
|
|
|
407
568
|
pruneMissingFiles(): string[] {
|
|
408
569
|
return pruneMissingFilesFromClients(this.clients.values());
|
|
409
570
|
}
|
|
410
|
-
/** Re-sync all open documents across active clients and
|
|
411
|
-
async refreshOpenDiagnostics(
|
|
412
|
-
|
|
571
|
+
/** Re-sync all open documents across active clients and return exact evidence coverage. */
|
|
572
|
+
async refreshOpenDiagnostics(
|
|
573
|
+
options?: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl,
|
|
574
|
+
): Promise<DiagnosticEvidenceSummary> {
|
|
575
|
+
const summary = await refreshOpenDiagnosticsForClients(this.clients.values(), options);
|
|
576
|
+
return this.toWorkspaceDiagnosticEvidence(summary.documents);
|
|
413
577
|
}
|
|
414
578
|
|
|
415
579
|
/** Clear cached pull-diagnostic result IDs across all clients. */
|
|
@@ -426,21 +590,57 @@ export class LspManager {
|
|
|
426
590
|
}
|
|
427
591
|
}
|
|
428
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Record one workspace invalidation event and forward it to clients.
|
|
595
|
+
*
|
|
596
|
+
* Every call with a non-empty change batch advances the invalidation
|
|
597
|
+
* generation, so a repeated edit to the same file opens a fresh
|
|
598
|
+
* generation. Recovery passes forward changes through
|
|
599
|
+
* {@link notifyWorkspaceFileChanges} and never advance the generation.
|
|
600
|
+
*/
|
|
601
|
+
noteWorkspaceChanges(changes: FileEvent[]): void {
|
|
602
|
+
if (changes.length > 0) this.invalidationEpoch++;
|
|
603
|
+
this.notifyWorkspaceFileChanges(changes);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Return per-route diagnostic evidence capability for recovery targeting. */
|
|
607
|
+
getClientDiagnosticRoutes(): Array<{
|
|
608
|
+
key: string;
|
|
609
|
+
supportsPull: boolean;
|
|
610
|
+
unconfirmedFiles: string[];
|
|
611
|
+
stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
|
|
612
|
+
}> {
|
|
613
|
+
const routes: Array<{
|
|
614
|
+
key: string;
|
|
615
|
+
supportsPull: boolean;
|
|
616
|
+
unconfirmedFiles: string[];
|
|
617
|
+
stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
|
|
618
|
+
}> = [];
|
|
619
|
+
for (const [key, client] of this.clients) {
|
|
620
|
+
if (client.status !== "running") continue;
|
|
621
|
+
const unconfirmedFiles = client
|
|
622
|
+
.getDiagnosticSnapshot()
|
|
623
|
+
.documents.filter((document) => document.status === "unconfirmed")
|
|
624
|
+
.map((document) => uriToFile(document.uri))
|
|
625
|
+
.filter((file) => this.isDiagnosticFile(file));
|
|
626
|
+
routes.push({
|
|
627
|
+
key,
|
|
628
|
+
supportsPull: client.hasDiagnosticProvider,
|
|
629
|
+
unconfirmedFiles,
|
|
630
|
+
stallSignal: client.getRecoveryStallSignal(),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return routes;
|
|
634
|
+
}
|
|
635
|
+
|
|
429
636
|
/** Force a workspace-wide diagnostic recovery pass. */
|
|
430
637
|
async recoverWorkspaceDiagnostics(options?: {
|
|
431
638
|
changes?: FileEvent[];
|
|
432
639
|
restartIfStillStale?: boolean;
|
|
433
640
|
maxWaitMs?: number;
|
|
434
641
|
quietMs?: number;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
restartedClients: number;
|
|
438
|
-
staleAssessment: {
|
|
439
|
-
suspected: boolean;
|
|
440
|
-
matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
|
|
441
|
-
warning: string | null;
|
|
442
|
-
};
|
|
443
|
-
}> {
|
|
642
|
+
control?: CodeRequestControl;
|
|
643
|
+
}): Promise<WorkspaceRecoveryResult> {
|
|
444
644
|
return recoverWorkspaceDiagnosticsImpl(this, options);
|
|
445
645
|
}
|
|
446
646
|
|
|
@@ -448,7 +648,12 @@ export class LspManager {
|
|
|
448
648
|
async shutdownAll(): Promise<void> {
|
|
449
649
|
const shutdowns = Array.from(this.clients.values()).map((c) => c.shutdown().catch(() => {}));
|
|
450
650
|
await Promise.all(shutdowns);
|
|
651
|
+
// Dispose every client so pending or active progress tokens and
|
|
652
|
+
// readiness promises cannot outlive the manager.
|
|
653
|
+
for (const client of this.clients.values()) client.dispose();
|
|
451
654
|
this.clients.clear();
|
|
655
|
+
this.clientGenerations.clear();
|
|
656
|
+
this.recoveryRestartEpochs.clear();
|
|
452
657
|
this.unavailable.clear();
|
|
453
658
|
this.knownRoots.clear();
|
|
454
659
|
this.warmedWorkspaceSymbolProjects.clear();
|
|
@@ -456,6 +661,30 @@ export class LspManager {
|
|
|
456
661
|
this.pendingWarmProbes.clear();
|
|
457
662
|
}
|
|
458
663
|
/** Get status of all servers. */
|
|
664
|
+
getRunningClientCount(): number {
|
|
665
|
+
return Array.from(this.clients.values()).filter((client) => client.status === "running").length;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Get the server names of all running clients, for recovery telemetry. */
|
|
669
|
+
getRunningClientNames(): string[] {
|
|
670
|
+
return Array.from(this.clients.values())
|
|
671
|
+
.filter((client) => client.status === "running")
|
|
672
|
+
.map((client) => client.name);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Check whether a path belongs to the configured diagnostic evidence scope. */
|
|
676
|
+
isDiagnosticFile(filePath: string): boolean {
|
|
677
|
+
const relative = path.relative(this.cwd, path.resolve(this.cwd, filePath));
|
|
678
|
+
return (
|
|
679
|
+
!shouldIgnoreLspPath(relative, this.cwd) &&
|
|
680
|
+
!isExcludedByPattern(relative, this.excludePatterns)
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
getDiagnosticEvidence(): DiagnosticEvidenceSummary {
|
|
685
|
+
return this.getDiagnosticSnapshot().evidence;
|
|
686
|
+
}
|
|
687
|
+
|
|
459
688
|
getStatus(): ManagerStatus {
|
|
460
689
|
this.pruneMissingFiles();
|
|
461
690
|
const servers: ServerStatus[] = [];
|
|
@@ -540,39 +769,65 @@ export class LspManager {
|
|
|
540
769
|
}
|
|
541
770
|
/** Get a diagnostic summary across all servers and files. */
|
|
542
771
|
getDiagnosticSummary(): DiagnosticSummary[] {
|
|
772
|
+
return this.getDiagnosticSnapshot().entries;
|
|
773
|
+
}
|
|
774
|
+
getDiagnosticSnapshot(): {
|
|
775
|
+
entries: DiagnosticSummary[];
|
|
776
|
+
current: boolean;
|
|
777
|
+
evidence: DiagnosticEvidenceSummary;
|
|
778
|
+
} {
|
|
543
779
|
this.pruneMissingFiles();
|
|
544
780
|
const fileDiags = new Map<string, { errors: number; warnings: number }>();
|
|
781
|
+
const evidenceDocuments: DiagnosticEvidenceDocument[] = [];
|
|
782
|
+
let clientCurrent = true;
|
|
545
783
|
for (const client of this.clients.values()) {
|
|
546
|
-
|
|
784
|
+
const snapshot = client.getDiagnosticSnapshot();
|
|
785
|
+
clientCurrent &&= snapshot.current;
|
|
786
|
+
for (const document of snapshot.documents) {
|
|
787
|
+
evidenceDocuments.push({
|
|
788
|
+
file: relativeFilePathFromUri(document.uri, this.cwd),
|
|
789
|
+
status: document.status,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
for (const entry of snapshot.entries) {
|
|
547
793
|
collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
|
|
548
794
|
}
|
|
549
795
|
}
|
|
550
|
-
|
|
796
|
+
const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
|
|
797
|
+
return {
|
|
798
|
+
entries: Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts })),
|
|
799
|
+
current:
|
|
800
|
+
clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
|
|
801
|
+
evidence,
|
|
802
|
+
};
|
|
551
803
|
}
|
|
552
804
|
/** Get outstanding diagnostics at or above the configured inline threshold. */
|
|
553
805
|
getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
|
|
806
|
+
return this.getOutstandingDiagnosticSummarySnapshot(maxSeverity).entries;
|
|
807
|
+
}
|
|
808
|
+
getOutstandingDiagnosticSummarySnapshot(maxSeverity: number = 1): {
|
|
809
|
+
entries: OutstandingDiagnosticSummaryEntry[];
|
|
810
|
+
current: boolean;
|
|
811
|
+
evidence: DiagnosticEvidenceSummary;
|
|
812
|
+
} {
|
|
554
813
|
this.pruneMissingFiles();
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (isExcludedByPattern(file, this.excludePatterns)) continue;
|
|
561
|
-
const current = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
|
|
562
|
-
const next = accumulateOutstandingDiagnostics(current, entry.diagnostics, maxSeverity);
|
|
563
|
-
if (next.total > 0) {
|
|
564
|
-
fileDiags.set(file, next);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return Array.from(fileDiags.values()).sort(
|
|
569
|
-
(a, b) =>
|
|
570
|
-
b.errors - a.errors ||
|
|
571
|
-
b.warnings - a.warnings ||
|
|
572
|
-
b.information - a.information ||
|
|
573
|
-
b.hints - a.hints ||
|
|
574
|
-
a.file.localeCompare(b.file),
|
|
814
|
+
const evidence = this.toWorkspaceDiagnosticEvidence(
|
|
815
|
+
collectClientDiagnosticEvidenceDocuments(this.clients.values(), this.cwd),
|
|
816
|
+
);
|
|
817
|
+
const clientCurrent = Array.from(this.clients.values()).every(
|
|
818
|
+
(client) => client.getDiagnosticSnapshot().current,
|
|
575
819
|
);
|
|
820
|
+
return {
|
|
821
|
+
entries: collectOutstandingDiagnosticSummaryEntries(
|
|
822
|
+
this.clients.values(),
|
|
823
|
+
this.cwd,
|
|
824
|
+
this.excludePatterns,
|
|
825
|
+
maxSeverity,
|
|
826
|
+
),
|
|
827
|
+
current:
|
|
828
|
+
clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
|
|
829
|
+
evidence,
|
|
830
|
+
};
|
|
576
831
|
}
|
|
577
832
|
getRelevantOutstandingDiagnosticsSummaryText(
|
|
578
833
|
relevantPaths: string[],
|
|
@@ -591,28 +846,71 @@ export class LspManager {
|
|
|
591
846
|
getOutstandingDiagnostics(
|
|
592
847
|
maxSeverity: number = 1,
|
|
593
848
|
): Array<{ file: string; diagnostics: Diagnostic[] }> {
|
|
849
|
+
return this.getOutstandingDiagnosticsSnapshot(maxSeverity).entries;
|
|
850
|
+
}
|
|
851
|
+
getOutstandingDiagnosticsSnapshot(maxSeverity: number = 1): {
|
|
852
|
+
entries: Array<{ file: string; diagnostics: Diagnostic[] }>;
|
|
853
|
+
current: boolean;
|
|
854
|
+
evidence: DiagnosticEvidenceSummary;
|
|
855
|
+
} {
|
|
594
856
|
this.pruneMissingFiles();
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
857
|
+
const clients = Array.from(this.clients.values());
|
|
858
|
+
let clientCurrent = true;
|
|
859
|
+
const evidenceDocuments = clients.flatMap((client) => {
|
|
860
|
+
const snapshot = client.getDiagnosticSnapshot();
|
|
861
|
+
clientCurrent &&= snapshot.current;
|
|
862
|
+
return snapshot.documents.map((document) => ({
|
|
863
|
+
file: relativeFilePathFromUri(document.uri, this.cwd),
|
|
864
|
+
status: document.status,
|
|
865
|
+
}));
|
|
866
|
+
});
|
|
867
|
+
const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
|
|
868
|
+
return {
|
|
869
|
+
entries: collectOutstandingDiagnosticsDetailed(
|
|
870
|
+
clients,
|
|
871
|
+
this.cwd,
|
|
872
|
+
this.excludePatterns,
|
|
873
|
+
maxSeverity,
|
|
874
|
+
),
|
|
875
|
+
current:
|
|
876
|
+
clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
|
|
877
|
+
evidence,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
private toWorkspaceDiagnosticEvidence(
|
|
881
|
+
documents: readonly DiagnosticEvidenceDocument[],
|
|
882
|
+
): DiagnosticEvidenceSummary {
|
|
883
|
+
const byFile = new Map<string, DiagnosticEvidenceDocument>();
|
|
884
|
+
for (const document of documents) {
|
|
885
|
+
const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
|
|
886
|
+
if (shouldIgnoreLspPath(file, this.cwd)) continue;
|
|
887
|
+
if (isExcludedByPattern(file, this.excludePatterns)) continue;
|
|
888
|
+
const existing = byFile.get(file);
|
|
889
|
+
if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
|
|
890
|
+
byFile.set(file, { file, status: document.status });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return summarizeDiagnosticEvidence(
|
|
894
|
+
Array.from(byFile.values()).sort((a, b) => a.file.localeCompare(b.file)),
|
|
600
895
|
);
|
|
601
896
|
}
|
|
897
|
+
|
|
602
898
|
async workspaceSymbol(
|
|
603
899
|
query: string,
|
|
900
|
+
control?: CodeRequestControl,
|
|
604
901
|
): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
|
|
605
|
-
const initial = await collectWorkspaceSymbols(this.clients.values(), query);
|
|
902
|
+
const initial = await collectWorkspaceSymbols(this.clients.values(), query, control);
|
|
606
903
|
if (!initial.hasSupport || initial.results.length > 0) {
|
|
607
904
|
return workspaceSymbolCollectionResult(initial);
|
|
608
905
|
}
|
|
609
906
|
|
|
610
|
-
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult(
|
|
611
|
-
findWorkspaceSymbolWarmTargets,
|
|
612
|
-
getWorkspaceSymbolWarmPosition,
|
|
613
|
-
collectWorkspaceSymbols,
|
|
907
|
+
const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
|
|
908
|
+
findWarmTargets: findWorkspaceSymbolWarmTargets,
|
|
909
|
+
getWarmPosition: getWorkspaceSymbolWarmPosition,
|
|
910
|
+
collect: (clients, value) => collectWorkspaceSymbols(clients, value, control),
|
|
614
911
|
query,
|
|
615
|
-
|
|
912
|
+
control,
|
|
913
|
+
});
|
|
616
914
|
return workspaceSymbolCollectionResult(warmed.collection ?? initial);
|
|
617
915
|
}
|
|
618
916
|
async ensureFileOpen(filePath: string): Promise<LspClient | null> {
|
|
@@ -629,18 +927,20 @@ export class LspManager {
|
|
|
629
927
|
}
|
|
630
928
|
|
|
631
929
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: warm-up coordinates client/project iteration, targeted semantic nudges, and early-return queries in one place.
|
|
632
|
-
private async warmWorkspaceSymbolProjectsUntilResult(
|
|
930
|
+
private async warmWorkspaceSymbolProjectsUntilResult(options: {
|
|
633
931
|
findWarmTargets: (
|
|
634
932
|
root: string,
|
|
635
933
|
rootMarkers: string[],
|
|
636
934
|
fileTypes: string[],
|
|
637
|
-
) => Array<{ projectRoot: string; file: string }
|
|
935
|
+
) => Array<{ projectRoot: string; file: string }>;
|
|
638
936
|
getWarmPosition: (
|
|
639
937
|
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
640
|
-
) => import("../config/types.ts").Position | null
|
|
641
|
-
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection
|
|
642
|
-
query: string
|
|
643
|
-
|
|
938
|
+
) => import("../config/types.ts").Position | null;
|
|
939
|
+
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
|
|
940
|
+
query: string;
|
|
941
|
+
control?: CodeRequestControl;
|
|
942
|
+
}): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
|
|
943
|
+
const { findWarmTargets, getWarmPosition, collect, query, control } = options;
|
|
644
944
|
let warmedAny = false;
|
|
645
945
|
|
|
646
946
|
for (const client of Array.from(this.clients.values())) {
|
|
@@ -667,13 +967,20 @@ export class LspManager {
|
|
|
667
967
|
const openedClient = await this.ensureFileOpen(target.file);
|
|
668
968
|
if (!openedClient) continue;
|
|
669
969
|
|
|
970
|
+
// A cancelled pass must not mark projects warmed on its way out.
|
|
971
|
+
throwIfCodeRequestInterrupted(control);
|
|
670
972
|
this.warmedWorkspaceSymbolProjects.add(projectKey);
|
|
671
973
|
warmedAny = true;
|
|
672
974
|
|
|
673
|
-
const symbols =
|
|
975
|
+
const symbols = control
|
|
976
|
+
? await openedClient.documentSymbols(target.file, control)
|
|
977
|
+
: await openedClient.documentSymbols(target.file);
|
|
674
978
|
if (symbols.kind !== "unavailable") {
|
|
675
979
|
const hoverPosition = getWarmPosition(symbols.data);
|
|
676
|
-
if (hoverPosition)
|
|
980
|
+
if (hoverPosition) {
|
|
981
|
+
if (control) await openedClient.hover(target.file, hoverPosition, control);
|
|
982
|
+
else await openedClient.hover(target.file, hoverPosition);
|
|
983
|
+
}
|
|
677
984
|
}
|
|
678
985
|
|
|
679
986
|
const collected = await collect(this.clients.values(), query);
|
|
@@ -690,6 +997,7 @@ export class LspManager {
|
|
|
690
997
|
client: LspClient,
|
|
691
998
|
filePath: string,
|
|
692
999
|
preferExactFile: boolean = false,
|
|
1000
|
+
control?: CodeRequestControl,
|
|
693
1001
|
): Promise<void> {
|
|
694
1002
|
const serverConfig = this.config.servers[client.name];
|
|
695
1003
|
if (!serverConfig) return;
|
|
@@ -706,14 +1014,16 @@ export class LspManager {
|
|
|
706
1014
|
|
|
707
1015
|
const pending = this.pendingWarmProbes.get(projectKey);
|
|
708
1016
|
if (pending) {
|
|
709
|
-
|
|
1017
|
+
// The shared probe keeps running for other consumers; only this
|
|
1018
|
+
// caller's wait stops when it cancels.
|
|
1019
|
+
await raceRequestControl(pending, control);
|
|
710
1020
|
return;
|
|
711
1021
|
}
|
|
712
1022
|
|
|
713
|
-
const probe = this.performWarmProbe(client, resolvedFile, projectKey);
|
|
1023
|
+
const probe = this.performWarmProbe(client, resolvedFile, projectKey, control);
|
|
714
1024
|
this.pendingWarmProbes.set(projectKey, probe);
|
|
715
1025
|
try {
|
|
716
|
-
await probe;
|
|
1026
|
+
await raceRequestControl(probe, control);
|
|
717
1027
|
} finally {
|
|
718
1028
|
this.pendingWarmProbes.delete(projectKey);
|
|
719
1029
|
}
|
|
@@ -723,16 +1033,23 @@ export class LspManager {
|
|
|
723
1033
|
_client: LspClient,
|
|
724
1034
|
resolvedFile: string,
|
|
725
1035
|
projectKey: string,
|
|
1036
|
+
control?: CodeRequestControl,
|
|
726
1037
|
): Promise<void> {
|
|
727
1038
|
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
728
1039
|
if (!openedClient) return;
|
|
729
1040
|
|
|
730
1041
|
this.warmedSemanticProjects.add(projectKey);
|
|
731
1042
|
|
|
732
|
-
|
|
1043
|
+
// The probe is shared state for concurrent callers. One caller's
|
|
1044
|
+
// cancellation must not cancel the shared in-flight queries, so only the
|
|
1045
|
+
// Debug Operation ID is forwarded; each caller races its own wait above.
|
|
1046
|
+
const sharedControl = { operationId: control?.operationId };
|
|
1047
|
+
const symbols = await openedClient.documentSymbols(resolvedFile, sharedControl);
|
|
733
1048
|
if (symbols.kind === "unavailable") return;
|
|
734
1049
|
const hoverPosition = getWorkspaceSymbolWarmPosition(symbols.data);
|
|
735
|
-
if (hoverPosition)
|
|
1050
|
+
if (hoverPosition) {
|
|
1051
|
+
await openedClient.hover(resolvedFile, hoverPosition, sharedControl);
|
|
1052
|
+
}
|
|
736
1053
|
}
|
|
737
1054
|
|
|
738
1055
|
private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
|
|
@@ -770,3 +1087,54 @@ export class LspManager {
|
|
|
770
1087
|
}
|
|
771
1088
|
}
|
|
772
1089
|
}
|
|
1090
|
+
|
|
1091
|
+
function collectClientDiagnosticEvidenceDocuments(
|
|
1092
|
+
clients: Iterable<LspClient>,
|
|
1093
|
+
cwd: string,
|
|
1094
|
+
): DiagnosticEvidenceDocument[] {
|
|
1095
|
+
return Array.from(clients).flatMap((client) =>
|
|
1096
|
+
client.getDiagnosticSnapshot().documents.map((document) => ({
|
|
1097
|
+
file: relativeFilePathFromUri(document.uri, cwd),
|
|
1098
|
+
status: document.status,
|
|
1099
|
+
})),
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function collectOutstandingDiagnosticSummaryEntries(
|
|
1104
|
+
clients: Iterable<LspClient>,
|
|
1105
|
+
cwd: string,
|
|
1106
|
+
excludePatterns: string[],
|
|
1107
|
+
maxSeverity: number,
|
|
1108
|
+
): OutstandingDiagnosticSummaryEntry[] {
|
|
1109
|
+
const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
|
|
1110
|
+
for (const client of clients) {
|
|
1111
|
+
for (const entry of client.getDiagnosticSnapshot().entries) {
|
|
1112
|
+
const file = relativeFilePathFromUri(entry.uri, cwd);
|
|
1113
|
+
if (shouldIgnoreLspPath(file, cwd) || isExcludedByPattern(file, excludePatterns)) continue;
|
|
1114
|
+
const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
|
|
1115
|
+
const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
|
|
1116
|
+
if (next.total > 0) fileDiags.set(file, next);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return Array.from(fileDiags.values()).sort(
|
|
1120
|
+
(a, b) =>
|
|
1121
|
+
b.errors - a.errors ||
|
|
1122
|
+
b.warnings - a.warnings ||
|
|
1123
|
+
b.information - a.information ||
|
|
1124
|
+
b.hints - a.hints ||
|
|
1125
|
+
a.file.localeCompare(b.file),
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function evidenceStatusRank(status: DiagnosticEvidenceDocument["status"]): number {
|
|
1130
|
+
switch (status) {
|
|
1131
|
+
case "confirmed":
|
|
1132
|
+
return 1;
|
|
1133
|
+
case "unconfirmed":
|
|
1134
|
+
return 2;
|
|
1135
|
+
case "failed":
|
|
1136
|
+
return 3;
|
|
1137
|
+
case "removed":
|
|
1138
|
+
return 4;
|
|
1139
|
+
}
|
|
1140
|
+
}
|