@mrclrchtr/supi-code-intelligence 4.10.0 → 6.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 +4 -3
- 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 +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +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 -6
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +111 -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 +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +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 -6
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- 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 +22 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +245 -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 +171 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +413 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -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 +690 -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 +97 -16
- 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 +448 -298
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +70 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +231 -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 +529 -119
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +12 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +42 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
- 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 +17 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +350 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +586 -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 +42 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +92 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +165 -118
- 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 +17 -7
- 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 +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +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 -6
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- 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/package.json +6 -3
- 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 +221 -77
- package/src/analysis/health/file-scope.ts +184 -0
- package/src/analysis/health/recovery.ts +14 -3
- package/src/analysis/provider.ts +58 -0
- package/src/analysis/readiness.ts +34 -9
- package/src/analysis/refactor/apply.ts +45 -1
- package/src/analysis/refactor/mutation-authority.ts +143 -0
- 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 -2
- package/src/analysis/search/ast-scan.ts +5 -2
- package/src/analysis/search/deadline.ts +2 -2
- package/src/analysis/search/pattern-analysis.ts +54 -33
- package/src/analysis/search/pattern.ts +18 -3
- 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/config.ts +35 -1
- package/src/diagnostics/evidence.ts +51 -0
- package/src/extension.ts +57 -14
- package/src/overview/overview-data.ts +9 -2
- package/src/overview/overview.ts +49 -30
- package/src/overview/types.ts +2 -0
- package/src/session/capability-adapter.ts +12 -3
- package/src/session/find-types.ts +2 -2
- package/src/session/find-workflow.ts +20 -9
- package/src/session/graph/collect.ts +18 -2
- package/src/session/graph-workflow.ts +19 -12
- package/src/session/health-refresh.ts +175 -0
- package/src/session/health-types.ts +43 -15
- package/src/session/health-workflow.ts +69 -54
- package/src/session/input/workflows.ts +2 -2
- package/src/session/inspect/collect.ts +20 -3
- package/src/session/inspect-workflow.ts +12 -6
- package/src/session/orientation/collect.ts +12 -3
- 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 +4 -1
- package/src/session/orientation-workflow.ts +23 -10
- package/src/session/refactor-plans.ts +2 -0
- package/src/session/refactor-workflow.ts +61 -15
- package/src/session/session.ts +29 -2
- 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 +284 -28
- package/src/substrate/lsp/recovery.ts +11 -5
- package/src/substrate/lsp/state.ts +2 -2
- package/src/substrate/workspace-provider-host.ts +25 -3
- package/src/tool/{find → code_find}/execute.ts +2 -16
- package/src/tool/code_find/guidance.ts +8 -0
- package/src/tool/{find → code_find}/render.ts +21 -4
- package/src/tool/{result/find.ts → code_find/result.ts} +78 -9
- package/src/tool/code_find/spec.ts +37 -0
- package/src/tool/code_find/tui.ts +41 -0
- package/src/tool/code_graph/execute.ts +24 -0
- package/src/tool/code_graph/guidance.ts +6 -0
- package/src/tool/{graph → code_graph}/markdown.ts +1 -1
- package/src/tool/{result/graph.ts → code_graph/result.ts} +111 -3
- package/src/tool/code_graph/spec.ts +39 -0
- package/src/tool/code_graph/tui.ts +117 -0
- package/src/tool/{health → code_health}/execute.ts +2 -13
- package/src/tool/code_health/guidance.ts +8 -0
- package/src/tool/{health → code_health}/markdown.ts +119 -27
- package/src/tool/code_health/refresh-status.ts +105 -0
- package/src/tool/{result/health.ts → code_health/result.ts} +80 -3
- package/src/tool/code_health/spec.ts +41 -0
- package/src/tool/{health → code_health}/tui.ts +95 -98
- package/src/tool/{inspect → code_inspect}/execute.ts +2 -16
- package/src/tool/code_inspect/guidance.ts +6 -0
- package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
- package/src/tool/{result/inspect.ts → code_inspect/result.ts} +143 -2
- package/src/tool/code_inspect/spec.ts +25 -0
- package/src/tool/code_inspect/tui.ts +44 -0
- package/src/tool/code_orientation/execute.ts +25 -0
- package/src/tool/code_orientation/guidance.ts +9 -0
- package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
- package/src/tool/code_orientation/result.ts +248 -0
- package/src/tool/code_orientation/spec.ts +25 -0
- package/src/tool/{orientation → code_orientation}/tui.ts +51 -35
- package/src/tool/code_refactor_apply/execute.ts +17 -0
- package/src/tool/code_refactor_apply/guidance.ts +5 -0
- package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +8 -11
- package/src/tool/code_refactor_apply/spec.ts +26 -0
- package/src/tool/code_refactor_apply/tui.ts +73 -0
- package/src/tool/code_refactor_plan/execute.ts +27 -0
- package/src/tool/code_refactor_plan/guidance.ts +8 -0
- package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +13 -22
- package/src/tool/code_refactor_plan/spec.ts +25 -0
- package/src/tool/code_refactor_plan/tui.ts +54 -0
- package/src/tool/code_resolve/execute.ts +19 -0
- package/src/tool/code_resolve/guidance.ts +8 -0
- package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
- package/src/tool/{result/resolve.ts → code_resolve/result.ts} +79 -29
- package/src/tool/code_resolve/spec.ts +25 -0
- package/src/tool/code_resolve/tui.ts +60 -0
- package/src/tool/guidance.ts +71 -52
- package/src/tool/infra/truncate.ts +40 -0
- package/src/tool/infra/workflow-control.ts +8 -0
- package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
- package/src/tool/register.ts +105 -88
- package/src/tool/result/display.ts +107 -0
- package/src/tool/result/errors.ts +20 -17
- package/src/tool/result/refactor.ts +37 -8
- package/src/tool/result/types.ts +20 -0
- package/src/tool/schemas.ts +26 -157
- package/src/tool/specs.ts +40 -126
- 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 -351
- package/src/tool/find/tui.ts +0 -39
- package/src/tool/graph/execute.ts +0 -62
- package/src/tool/graph/tui.ts +0 -104
- package/src/tool/inspect/tui.ts +0 -36
- package/src/tool/orientation/execute.ts +0 -62
- package/src/tool/refactor-apply/tui.ts +0 -55
- package/src/tool/refactor-plan/tui.ts +0 -41
- package/src/tool/resolve/execute.ts +0 -36
- package/src/tool/resolve/tui.ts +0 -42
- package/src/tool/result/orientation.ts +0 -132
- /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
- /package/src/tool/{find → code_find}/markdown.ts +0 -0
- /package/src/tool/{find → code_find}/modes.ts +0 -0
- /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
|
@@ -6,11 +6,14 @@ import { type ChildProcess, execSync, spawn } from "node:child_process";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
type CodeQueryResult,
|
|
9
|
+
type CodeRequestControl,
|
|
9
10
|
completedCodeQuery,
|
|
11
|
+
isCodeRequestInterruption,
|
|
12
|
+
throwIfCodeRequestInterrupted,
|
|
10
13
|
unavailableCodeQuery,
|
|
11
14
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
12
15
|
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
13
|
-
import type
|
|
16
|
+
import { type ProgressToken, TextDocumentSyncKind } from "vscode-languageserver-protocol";
|
|
14
17
|
import { CLIENT_CAPABILITIES } from "../config/capabilities.ts";
|
|
15
18
|
import type {
|
|
16
19
|
CodeAction,
|
|
@@ -25,7 +28,6 @@ import type {
|
|
|
25
28
|
Location,
|
|
26
29
|
LocationLink,
|
|
27
30
|
Position,
|
|
28
|
-
PublishDiagnosticsParams,
|
|
29
31
|
Range,
|
|
30
32
|
ServerCapabilities,
|
|
31
33
|
ServerConfig,
|
|
@@ -33,14 +35,35 @@ import type {
|
|
|
33
35
|
WorkspaceEdit,
|
|
34
36
|
WorkspaceSymbol,
|
|
35
37
|
} from "../config/types.ts";
|
|
38
|
+
import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
|
|
39
|
+
import type { DiagnosticEvidenceSummary } from "../diagnostics/evidence.ts";
|
|
40
|
+
import { raceRequestControl } from "../session/readiness.ts";
|
|
36
41
|
import { fileToUri } from "../utils.ts";
|
|
37
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ClientDynamicRegistrations,
|
|
44
|
+
DOCUMENT_DIAGNOSTIC_METHOD,
|
|
45
|
+
isValidDiagnosticOptions,
|
|
46
|
+
} from "./client-diagnostic-capabilities.ts";
|
|
47
|
+
import { ClientDiagnostics } from "./client-diagnostics.ts";
|
|
48
|
+
import type { ClientDiagnosticSnapshot, DiagnosticEntry } from "./client-document-state.ts";
|
|
38
49
|
import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
|
|
39
50
|
|
|
40
51
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
41
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Fixed bound after which a running client that never became ready is
|
|
55
|
+
* considered readiness-stalled and eligible for a recovery restart.
|
|
56
|
+
*/
|
|
57
|
+
export const RECOVERY_CLIENT_STARTUP_BOUND_MS = 5_000;
|
|
58
|
+
|
|
59
|
+
/** Repeated protocol-stall failures that justify a recovery restart. */
|
|
60
|
+
export const RECOVERY_PROTOCOL_FAILURE_THRESHOLD = 3;
|
|
61
|
+
|
|
62
|
+
/** Stall signals that justify replacing a client's server process. */
|
|
63
|
+
export type RecoveryRestartReason = "readiness-stall" | "protocol-errors";
|
|
64
|
+
|
|
42
65
|
/** Race an operation against a timeout without retaining the timer after settlement. */
|
|
43
|
-
async function withTimeout<T>(
|
|
66
|
+
export async function withTimeout<T>(
|
|
44
67
|
operation: Promise<T>,
|
|
45
68
|
timeoutMs: number,
|
|
46
69
|
message: string,
|
|
@@ -58,6 +81,41 @@ async function withTimeout<T>(
|
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read and validate the `registrations` array of a `client/registerCapability`
|
|
90
|
+
* request. Each entry must be a record with string `id` and `method`; the
|
|
91
|
+
* optional `registerOptions` stay unvalidated here (method-specific checks
|
|
92
|
+
* happen in the handler). Malformed values reject the request.
|
|
93
|
+
*/
|
|
94
|
+
function readRegistrations(
|
|
95
|
+
params: unknown,
|
|
96
|
+
requestName: string,
|
|
97
|
+
): Array<{ id: string; method: string; registerOptions?: unknown }> {
|
|
98
|
+
if (!isRecord(params) || !Array.isArray(params.registrations)) {
|
|
99
|
+
throw new JsonRpcRequestError(-32602, `Malformed ${requestName} params.`);
|
|
100
|
+
}
|
|
101
|
+
const registrations: Array<{ id: string; method: string; registerOptions?: unknown }> = [];
|
|
102
|
+
for (const registration of params.registrations) {
|
|
103
|
+
if (
|
|
104
|
+
!isRecord(registration) ||
|
|
105
|
+
typeof registration.id !== "string" ||
|
|
106
|
+
typeof registration.method !== "string"
|
|
107
|
+
) {
|
|
108
|
+
throw new JsonRpcRequestError(-32602, `Malformed ${requestName} registration.`);
|
|
109
|
+
}
|
|
110
|
+
registrations.push({
|
|
111
|
+
id: registration.id,
|
|
112
|
+
method: registration.method,
|
|
113
|
+
registerOptions: registration.registerOptions,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return registrations;
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
// ── Process-tree cleanup ──────────────────────────────────────────────
|
|
62
120
|
|
|
63
121
|
/**
|
|
@@ -86,6 +144,17 @@ function killProcessTree(pid: number): void {
|
|
|
86
144
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
87
145
|
export type ClientStatus = "initializing" | "running" | "error" | "shutdown";
|
|
88
146
|
|
|
147
|
+
/** Package-internal facts that the manager projects into workspace lifecycle transitions. */
|
|
148
|
+
export type LspClientLifecycleTransitionKind =
|
|
149
|
+
| "startup"
|
|
150
|
+
| "readiness"
|
|
151
|
+
| "crash"
|
|
152
|
+
| "shutdown"
|
|
153
|
+
| "tracked-files";
|
|
154
|
+
|
|
155
|
+
/** Observer for one concrete client's lifecycle facts. */
|
|
156
|
+
export type LspClientLifecycleListener = (kind: LspClientLifecycleTransitionKind) => void;
|
|
157
|
+
|
|
89
158
|
// ── LspClient ─────────────────────────────────────────────────────────
|
|
90
159
|
export class LspClient {
|
|
91
160
|
readonly name: string;
|
|
@@ -96,37 +165,62 @@ export class LspClient {
|
|
|
96
165
|
private _status: ClientStatus = "initializing";
|
|
97
166
|
private capabilities: ServerCapabilities | null = null;
|
|
98
167
|
private readonly diagnostics: ClientDiagnostics;
|
|
168
|
+
/** Dynamic capability registrations for this client instance only. */
|
|
169
|
+
private readonly dynamicRegistrations = new ClientDynamicRegistrations();
|
|
99
170
|
|
|
100
171
|
// ── Readiness (work-done-progress) ──────────────────────────────────
|
|
101
|
-
private trackedTokens = new Map<ProgressToken, "
|
|
172
|
+
private trackedTokens = new Map<ProgressToken, "created" | "active" | "ended">();
|
|
173
|
+
private tokenCreatedAt = new Map<ProgressToken, number>();
|
|
102
174
|
private _readyPromise: Promise<void> | null = null;
|
|
103
175
|
private _readyResolve: (() => void) | undefined;
|
|
104
176
|
private _readyReject: ((err: Error) => void) | undefined;
|
|
105
177
|
private _isReady = false;
|
|
178
|
+
/** Whether this client generation ever reached concrete readiness. */
|
|
179
|
+
private everReady = false;
|
|
106
180
|
private noProgressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
107
181
|
private tokenTimeouts = new Map<ProgressToken, ReturnType<typeof setTimeout>>();
|
|
182
|
+
/** Wall-clock start of the current client generation, for stall detection. */
|
|
183
|
+
private startedAt = 0;
|
|
108
184
|
|
|
185
|
+
// biome-ignore lint/complexity/useMaxParams: internal constructor keeps positional identity for test call sites
|
|
109
186
|
constructor(
|
|
110
187
|
name: string,
|
|
111
188
|
private readonly config: ServerConfig,
|
|
112
189
|
root: string,
|
|
190
|
+
private readonly onLifecycleTransition?: LspClientLifecycleListener,
|
|
191
|
+
/** Absolute workspace root for debug-telemetry identity. */
|
|
192
|
+
readonly cwd?: string,
|
|
113
193
|
) {
|
|
114
194
|
this.name = name;
|
|
115
195
|
this.root = root;
|
|
116
196
|
this.diagnostics = new ClientDiagnostics({
|
|
197
|
+
server: name,
|
|
198
|
+
cwd: cwd,
|
|
117
199
|
isOperational: () => this.rpc !== null && this._status === "running",
|
|
118
200
|
supportsPullDiagnostics: () => this.hasDiagnosticProvider,
|
|
201
|
+
usesIncrementalDocumentSync: () => this.usesIncrementalDocumentSync,
|
|
119
202
|
sendNotification: (method, params) => {
|
|
120
203
|
if (this.rpc) void this.rpc.sendNotification(method, params);
|
|
121
204
|
},
|
|
122
|
-
pullDocumentDiagnostics: async (
|
|
205
|
+
pullDocumentDiagnostics: async (request) => {
|
|
123
206
|
const rpc = this.rpc;
|
|
124
207
|
if (!rpc || this._status !== "running") throw new Error("client not running");
|
|
125
|
-
await this.getReady(
|
|
208
|
+
await this.getReady({
|
|
209
|
+
signal: request.signal,
|
|
210
|
+
deadline: request.deadline,
|
|
211
|
+
});
|
|
126
212
|
return rpc.sendRequest(
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
213
|
+
DOCUMENT_DIAGNOSTIC_METHOD,
|
|
214
|
+
{
|
|
215
|
+
textDocument: { uri: request.uri },
|
|
216
|
+
previousResultId: request.previousResultId,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
timeoutMs: request.timeoutMs,
|
|
220
|
+
signal: request.signal,
|
|
221
|
+
deadline: request.deadline,
|
|
222
|
+
operationId: request.operationId,
|
|
223
|
+
},
|
|
130
224
|
) as Promise<DocumentDiagnosticReport>;
|
|
131
225
|
},
|
|
132
226
|
});
|
|
@@ -144,44 +238,66 @@ export class LspClient {
|
|
|
144
238
|
return this.capabilities;
|
|
145
239
|
}
|
|
146
240
|
|
|
241
|
+
/** Whether the server requires range-based document changes. */
|
|
242
|
+
get usesIncrementalDocumentSync(): boolean {
|
|
243
|
+
const sync = this.capabilities?.textDocumentSync;
|
|
244
|
+
const change = typeof sync === "number" ? sync : sync?.change;
|
|
245
|
+
return change === TextDocumentSyncKind.Incremental;
|
|
246
|
+
}
|
|
247
|
+
|
|
147
248
|
/** Whether the server is currently not indexing and ready to serve queries. */
|
|
148
249
|
get ready(): boolean {
|
|
149
250
|
return this._isReady;
|
|
150
251
|
}
|
|
151
252
|
|
|
253
|
+
/** Publish one client fact without letting an observer disrupt the client. */
|
|
254
|
+
private publishLifecycle(kind: LspClientLifecycleTransitionKind): void {
|
|
255
|
+
try {
|
|
256
|
+
this.onLifecycleTransition?.(kind);
|
|
257
|
+
} catch {
|
|
258
|
+
// Lifecycle observers must not alter protocol behavior.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
152
262
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
153
263
|
/** Spawn the server process and perform the initialize handshake. */
|
|
154
264
|
async start(): Promise<void> {
|
|
155
265
|
const cmd = this.config.command;
|
|
156
266
|
const args = this.config.args ?? [];
|
|
267
|
+
this.startedAt = Date.now();
|
|
157
268
|
|
|
158
269
|
try {
|
|
159
270
|
this.process = spawn(cmd, args, {
|
|
160
271
|
cwd: this.root,
|
|
161
272
|
stdio: ["pipe", "pipe", "pipe"],
|
|
162
|
-
env: { ...process.env },
|
|
273
|
+
env: { ...process.env, ...this.config.env },
|
|
163
274
|
// Run the server in its own process group so we can atomically
|
|
164
275
|
// kill the entire tree (server + subprocesses like tsserver)
|
|
165
276
|
// with `process.kill(-pid, signal)` on Unix or `taskkill /T` on Windows.
|
|
166
277
|
detached: true,
|
|
167
278
|
});
|
|
168
279
|
} catch (err) {
|
|
169
|
-
|
|
170
|
-
|
|
280
|
+
const failure = new Error(`Failed to spawn ${cmd}: ${err}`, { cause: err });
|
|
281
|
+
this.handleProcessFailure(failure);
|
|
282
|
+
throw failure;
|
|
171
283
|
}
|
|
172
284
|
|
|
173
285
|
if (!this.process.stdin || !this.process.stdout) {
|
|
174
|
-
|
|
286
|
+
const failure = new Error(`${cmd}: missing stdin/stdout`);
|
|
287
|
+
this.handleProcessFailure(failure);
|
|
175
288
|
this.process.kill();
|
|
176
|
-
throw
|
|
289
|
+
throw failure;
|
|
177
290
|
}
|
|
178
291
|
|
|
179
|
-
this.rpc = new JsonRpcClient(this.process.stdout, this.process.stdin
|
|
292
|
+
this.rpc = new JsonRpcClient(this.process.stdout, this.process.stdin, {
|
|
293
|
+
server: this.name,
|
|
294
|
+
cwd: this.cwd,
|
|
295
|
+
});
|
|
180
296
|
|
|
181
297
|
// Handle notifications
|
|
182
298
|
this.rpc.onNotification((method, params) => {
|
|
183
299
|
if (method === "textDocument/publishDiagnostics") {
|
|
184
|
-
this.handlePublishDiagnostics(params
|
|
300
|
+
this.handlePublishDiagnostics(params);
|
|
185
301
|
} else if (method === "$/progress") {
|
|
186
302
|
this.handleProgress(params as { token: ProgressToken; value: { kind: string } });
|
|
187
303
|
}
|
|
@@ -190,19 +306,11 @@ export class LspClient {
|
|
|
190
306
|
|
|
191
307
|
// Handle crashes
|
|
192
308
|
this.process.on("exit", (_code) => {
|
|
193
|
-
|
|
194
|
-
this._status = "error";
|
|
195
|
-
this.cancelNoProgressTimer();
|
|
196
|
-
this.rejectReady(new Error("Client crashed"));
|
|
197
|
-
}
|
|
198
|
-
this.rpc?.dispose();
|
|
309
|
+
this.handleProcessFailure(new Error("Client crashed"));
|
|
199
310
|
});
|
|
200
311
|
|
|
201
312
|
this.process.on("error", (_err) => {
|
|
202
|
-
|
|
203
|
-
this._status = "error";
|
|
204
|
-
this.rejectReady(new Error("Client process error"));
|
|
205
|
-
}
|
|
313
|
+
this.handleProcessFailure(new Error("Client process error"));
|
|
206
314
|
});
|
|
207
315
|
|
|
208
316
|
// Suppress stderr to avoid noise in the agent
|
|
@@ -213,19 +321,31 @@ export class LspClient {
|
|
|
213
321
|
const result = (await this.rpc.sendRequest("initialize", {
|
|
214
322
|
processId: process.pid,
|
|
215
323
|
rootUri: fileToUri(this.root),
|
|
324
|
+
workspaceFolders: [
|
|
325
|
+
{
|
|
326
|
+
uri: fileToUri(this.root),
|
|
327
|
+
name: path.basename(this.root) || this.root,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
216
330
|
capabilities: CLIENT_CAPABILITIES,
|
|
217
331
|
initializationOptions: this.config.initializationOptions,
|
|
218
332
|
})) as InitializeResult;
|
|
219
333
|
|
|
334
|
+
const positionEncoding = result.capabilities.positionEncoding ?? "utf-16";
|
|
335
|
+
if (positionEncoding !== "utf-16") {
|
|
336
|
+
throw new Error(`Server selected unsupported position encoding "${positionEncoding}".`);
|
|
337
|
+
}
|
|
220
338
|
this.capabilities = result.capabilities;
|
|
221
|
-
|
|
339
|
+
await this.rpc.sendNotification("initialized", {});
|
|
222
340
|
this._status = "running";
|
|
341
|
+
this.publishLifecycle("startup");
|
|
223
342
|
|
|
224
343
|
this.armNoProgressTimer();
|
|
225
344
|
} catch (err) {
|
|
226
|
-
this.
|
|
345
|
+
const failure = new Error(`${this.name}: initialize failed: ${err}`, { cause: err });
|
|
346
|
+
this.handleProcessFailure(failure);
|
|
227
347
|
this.process.kill();
|
|
228
|
-
throw
|
|
348
|
+
throw failure;
|
|
229
349
|
}
|
|
230
350
|
}
|
|
231
351
|
|
|
@@ -233,6 +353,11 @@ export class LspClient {
|
|
|
233
353
|
async shutdown(): Promise<void> {
|
|
234
354
|
if (this._status === "shutdown") return;
|
|
235
355
|
this._status = "shutdown";
|
|
356
|
+
this.diagnostics.clear();
|
|
357
|
+
this.dynamicRegistrations.clear();
|
|
358
|
+
this.cancelNoProgressTimer();
|
|
359
|
+
this.rejectReady(new Error("Client shutdown"));
|
|
360
|
+
this.publishLifecycle("shutdown");
|
|
236
361
|
|
|
237
362
|
if (!this.rpc || !this.process) return;
|
|
238
363
|
|
|
@@ -271,38 +396,123 @@ export class LspClient {
|
|
|
271
396
|
});
|
|
272
397
|
}
|
|
273
398
|
}
|
|
399
|
+
}
|
|
274
400
|
|
|
275
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Terminate the process tree without a protocol handshake.
|
|
403
|
+
*
|
|
404
|
+
* Used when a replacement startup exceeds its recovery bound, so the
|
|
405
|
+
* orphaned server process cannot outlive the failed restart.
|
|
406
|
+
*/
|
|
407
|
+
async forceKill(): Promise<void> {
|
|
408
|
+
const pid = this.process?.pid;
|
|
409
|
+
this.rpc?.dispose();
|
|
410
|
+
if (pid && this.process?.exitCode === null) {
|
|
411
|
+
killProcessTree(pid);
|
|
412
|
+
if (process.platform !== "win32") {
|
|
413
|
+
// Escalate to SIGKILL after a brief grace period on Unix, mirroring
|
|
414
|
+
// the graceful shutdown path for servers that ignore SIGTERM.
|
|
415
|
+
await new Promise<void>((resolve) => {
|
|
416
|
+
setTimeout(() => {
|
|
417
|
+
try {
|
|
418
|
+
process.kill(-pid, "SIGKILL");
|
|
419
|
+
} catch {
|
|
420
|
+
// Already dead — ignore.
|
|
421
|
+
}
|
|
422
|
+
resolve();
|
|
423
|
+
}, 500);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.handleProcessFailure(new Error("Client start bound exceeded"));
|
|
428
|
+
}
|
|
276
429
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
430
|
+
private handleProcessFailure(reason: Error): void {
|
|
431
|
+
const didCrash = this._status !== "shutdown" && this._status !== "error";
|
|
432
|
+
if (didCrash) {
|
|
433
|
+
this._status = "error";
|
|
434
|
+
this.cancelNoProgressTimer();
|
|
435
|
+
this.rejectReady(reason);
|
|
281
436
|
}
|
|
282
|
-
|
|
283
|
-
this.
|
|
284
|
-
this.
|
|
437
|
+
this.diagnostics.clear({ preserveFailedDocuments: didCrash || this._status === "error" });
|
|
438
|
+
this.dynamicRegistrations.clear();
|
|
439
|
+
this.rpc?.dispose();
|
|
440
|
+
if (didCrash) this.publishLifecycle("crash");
|
|
285
441
|
}
|
|
286
442
|
|
|
287
443
|
// ── Document Synchronization and Diagnostics ────────────────────────
|
|
288
444
|
/** Open a document, or update it when it is already open. */
|
|
289
445
|
didOpen(filePath: string, content: string): void {
|
|
446
|
+
const trackedCount = this.openFiles.length;
|
|
290
447
|
this.diagnostics.didOpen(filePath, content);
|
|
448
|
+
this.publishTrackedFileChange(trackedCount);
|
|
291
449
|
}
|
|
292
450
|
|
|
293
451
|
/** Update a document, or open it when it is not tracked yet. */
|
|
294
452
|
didChange(filePath: string, content: string): void {
|
|
453
|
+
const trackedCount = this.openFiles.length;
|
|
295
454
|
this.diagnostics.didChange(filePath, content);
|
|
455
|
+
this.publishTrackedFileChange(trackedCount);
|
|
296
456
|
}
|
|
297
457
|
|
|
298
458
|
/** Close a document and remove its cached diagnostic state. */
|
|
299
459
|
didClose(filePath: string): void {
|
|
460
|
+
const trackedCount = this.openFiles.length;
|
|
300
461
|
this.diagnostics.didClose(filePath);
|
|
462
|
+
this.publishTrackedFileChange(trackedCount);
|
|
301
463
|
}
|
|
302
464
|
|
|
303
465
|
/** Remove missing document and diagnostic state, and return the removed paths. */
|
|
304
466
|
pruneMissingFiles(): string[] {
|
|
305
|
-
|
|
467
|
+
const trackedCount = this.openFiles.length;
|
|
468
|
+
const removed = this.diagnostics.pruneMissingFiles();
|
|
469
|
+
this.publishTrackedFileChange(trackedCount);
|
|
470
|
+
return removed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private publishTrackedFileChange(previousCount: number): void {
|
|
474
|
+
if (this.openFiles.length !== previousCount) this.publishLifecycle("tracked-files");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Retain a failed document outcome when a replacement cannot reopen it. */
|
|
478
|
+
markFailedFile(filePath: string): void {
|
|
479
|
+
this.diagnostics.markFailedFile(filePath);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Return the stall signal that justifies replacing this client's process,
|
|
484
|
+
* or null when the client is healthy. Recovery restarts clients only on
|
|
485
|
+
* these signals, never on unconfirmed evidence alone (ADR 0020).
|
|
486
|
+
*/
|
|
487
|
+
getRecoveryStallSignal(): RecoveryRestartReason | null {
|
|
488
|
+
if (this._status !== "running") return null;
|
|
489
|
+
// The startup bound applies only before the client ever became ready: a
|
|
490
|
+
// later readiness loss (a normal progress begin during indexing) is not
|
|
491
|
+
// a startup stall.
|
|
492
|
+
const pastStartupBound =
|
|
493
|
+
this.startedAt > 0 && Date.now() - this.startedAt >= RECOVERY_CLIENT_STARTUP_BOUND_MS;
|
|
494
|
+
if (!this.everReady && pastStartupBound) return "readiness-stall";
|
|
495
|
+
if (this.hasUnbegunCreatedToken()) return "readiness-stall";
|
|
496
|
+
if ((this.rpc?.getProtocolFailureCount() ?? 0) >= RECOVERY_PROTOCOL_FAILURE_THRESHOLD) {
|
|
497
|
+
return "protocol-errors";
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Test whether a created progress token never began within its per-token bound. */
|
|
503
|
+
private hasUnbegunCreatedToken(): boolean {
|
|
504
|
+
const boundMs = this.config.readinessTimeoutMs ?? 10_000;
|
|
505
|
+
for (const [token, createdAt] of this.tokenCreatedAt) {
|
|
506
|
+
if (this.trackedTokens.get(token) === "created" && Date.now() - createdAt >= boundMs) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Return the current client version, or null when the document is not open. */
|
|
514
|
+
getOpenDocumentVersion(filePath: string): number | null {
|
|
515
|
+
return this.diagnostics.getOpenDocumentVersion(filePath);
|
|
306
516
|
}
|
|
307
517
|
|
|
308
518
|
/** Return stored diagnostics for one file. */
|
|
@@ -311,8 +521,12 @@ export class LspClient {
|
|
|
311
521
|
}
|
|
312
522
|
|
|
313
523
|
/** Return non-empty diagnostics for files that still exist. */
|
|
524
|
+
getDiagnosticSnapshot(): ClientDiagnosticSnapshot {
|
|
525
|
+
return this.diagnostics.getDiagnosticSnapshot();
|
|
526
|
+
}
|
|
527
|
+
|
|
314
528
|
getAllDiagnostics(): DiagnosticEntry[] {
|
|
315
|
-
return this.
|
|
529
|
+
return this.getDiagnosticSnapshot().entries;
|
|
316
530
|
}
|
|
317
531
|
|
|
318
532
|
/** Force the next pull refresh to request complete diagnostic reports. */
|
|
@@ -322,12 +536,20 @@ export class LspClient {
|
|
|
322
536
|
|
|
323
537
|
/** Check if server supports pull diagnostics. */
|
|
324
538
|
get hasDiagnosticProvider(): boolean {
|
|
325
|
-
|
|
539
|
+
// Static state: a valid `diagnosticProvider` in the initialize result.
|
|
540
|
+
// Dynamic state: an active registration for the diagnostic method. A
|
|
541
|
+
// malformed static shape or an empty dynamic set fails closed, so an
|
|
542
|
+
// unsupported server never gets pull requests.
|
|
543
|
+
return (
|
|
544
|
+
isValidDiagnosticOptions(this.capabilities?.diagnosticProvider) ||
|
|
545
|
+
this.dynamicRegistrations.has(DOCUMENT_DIAGNOSTIC_METHOD)
|
|
546
|
+
);
|
|
326
547
|
}
|
|
327
548
|
|
|
328
549
|
/** Notify the server that watched workspace files changed. */
|
|
329
550
|
notifyWorkspaceFileChanges(changes: FileEvent[]): void {
|
|
330
551
|
if (!this.rpc || this._status !== "running" || changes.length === 0) return;
|
|
552
|
+
this.diagnostics.invalidateCachedEvidence();
|
|
331
553
|
void this.rpc.sendNotification("workspace/didChangeWatchedFiles", {
|
|
332
554
|
changes,
|
|
333
555
|
} satisfies DidChangeWatchedFilesParams);
|
|
@@ -335,118 +557,157 @@ export class LspClient {
|
|
|
335
557
|
|
|
336
558
|
/** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
|
|
337
559
|
async refreshOpenDiagnostics(
|
|
338
|
-
options: { maxWaitMs?: number; quietMs?: number } = {},
|
|
339
|
-
): Promise<
|
|
560
|
+
options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl = {},
|
|
561
|
+
): Promise<DiagnosticEvidenceSummary> {
|
|
340
562
|
return this.diagnostics.refreshOpenDiagnostics(options);
|
|
341
563
|
}
|
|
342
564
|
|
|
343
|
-
/** Sync one file and return
|
|
344
|
-
async syncAndWaitForDiagnostics(
|
|
345
|
-
|
|
565
|
+
/** Sync one file and return diagnostics with explicit evidence availability. */
|
|
566
|
+
async syncAndWaitForDiagnostics(
|
|
567
|
+
filePath: string,
|
|
568
|
+
content: string,
|
|
569
|
+
control?: CodeRequestControl,
|
|
570
|
+
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
571
|
+
return this.diagnostics.syncAndWaitForDiagnostics(filePath, content, control);
|
|
346
572
|
}
|
|
347
573
|
|
|
348
574
|
// ── LSP Requests ───────────────────────────────────────────────────
|
|
349
|
-
async hover(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
575
|
+
async hover(
|
|
576
|
+
filePath: string,
|
|
577
|
+
position: Position,
|
|
578
|
+
control?: CodeRequestControl,
|
|
579
|
+
): Promise<CodeQueryResult<Hover | null>> {
|
|
580
|
+
return this.query(
|
|
581
|
+
"textDocument/hover",
|
|
582
|
+
{ textDocument: { uri: fileToUri(filePath) }, position },
|
|
583
|
+
control,
|
|
584
|
+
);
|
|
354
585
|
}
|
|
355
586
|
|
|
356
587
|
async definition(
|
|
357
588
|
filePath: string,
|
|
358
589
|
position: Position,
|
|
590
|
+
control?: CodeRequestControl,
|
|
359
591
|
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
|
|
360
|
-
return this.query(
|
|
361
|
-
textDocument
|
|
362
|
-
position,
|
|
363
|
-
|
|
592
|
+
return this.query(
|
|
593
|
+
"textDocument/definition",
|
|
594
|
+
{ textDocument: { uri: fileToUri(filePath) }, position },
|
|
595
|
+
control,
|
|
596
|
+
);
|
|
364
597
|
}
|
|
365
598
|
|
|
366
599
|
async references(
|
|
367
600
|
filePath: string,
|
|
368
601
|
position: Position,
|
|
602
|
+
control?: CodeRequestControl,
|
|
369
603
|
): Promise<CodeQueryResult<Location[] | null>> {
|
|
370
|
-
return this.query(
|
|
371
|
-
textDocument
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
604
|
+
return this.query(
|
|
605
|
+
"textDocument/references",
|
|
606
|
+
{
|
|
607
|
+
textDocument: { uri: fileToUri(filePath) },
|
|
608
|
+
position,
|
|
609
|
+
context: { includeDeclaration: true },
|
|
610
|
+
},
|
|
611
|
+
control,
|
|
612
|
+
);
|
|
375
613
|
}
|
|
376
614
|
|
|
377
615
|
async documentSymbols(
|
|
378
616
|
filePath: string,
|
|
617
|
+
control?: CodeRequestControl,
|
|
379
618
|
): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[] | null>> {
|
|
380
|
-
return this.query(
|
|
381
|
-
textDocument
|
|
382
|
-
|
|
619
|
+
return this.query(
|
|
620
|
+
"textDocument/documentSymbol",
|
|
621
|
+
{ textDocument: { uri: fileToUri(filePath) } },
|
|
622
|
+
control,
|
|
623
|
+
);
|
|
383
624
|
}
|
|
384
625
|
|
|
385
626
|
async workspaceSymbol(
|
|
386
627
|
query: string,
|
|
628
|
+
control?: CodeRequestControl,
|
|
387
629
|
): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[] | null>> {
|
|
388
630
|
if (!this.capabilities?.workspaceSymbolProvider) {
|
|
389
631
|
return unavailableCodeQuery("Workspace-symbol requests are not supported by this server.");
|
|
390
632
|
}
|
|
391
|
-
return this.query("workspace/symbol", { query });
|
|
633
|
+
return this.query("workspace/symbol", { query }, control);
|
|
392
634
|
}
|
|
393
635
|
|
|
394
636
|
async rename(
|
|
395
637
|
filePath: string,
|
|
396
638
|
position: Position,
|
|
397
639
|
newName: string,
|
|
640
|
+
control?: CodeRequestControl,
|
|
398
641
|
): Promise<WorkspaceEdit | null> {
|
|
399
|
-
return this.request(
|
|
400
|
-
textDocument
|
|
401
|
-
position,
|
|
402
|
-
|
|
403
|
-
|
|
642
|
+
return this.request(
|
|
643
|
+
"textDocument/rename",
|
|
644
|
+
{ textDocument: { uri: fileToUri(filePath) }, position, newName },
|
|
645
|
+
control,
|
|
646
|
+
);
|
|
404
647
|
}
|
|
405
648
|
|
|
406
649
|
async codeActions(
|
|
407
650
|
filePath: string,
|
|
408
651
|
range: Range,
|
|
409
652
|
context: CodeActionContext,
|
|
653
|
+
control?: CodeRequestControl,
|
|
410
654
|
): Promise<CodeAction[] | null> {
|
|
411
|
-
return this.request(
|
|
412
|
-
textDocument
|
|
413
|
-
range,
|
|
414
|
-
|
|
415
|
-
|
|
655
|
+
return this.request(
|
|
656
|
+
"textDocument/codeAction",
|
|
657
|
+
{ textDocument: { uri: fileToUri(filePath) }, range, context },
|
|
658
|
+
control,
|
|
659
|
+
);
|
|
416
660
|
}
|
|
417
661
|
|
|
418
662
|
async implementation(
|
|
419
663
|
filePath: string,
|
|
420
664
|
position: Position,
|
|
665
|
+
control?: CodeRequestControl,
|
|
421
666
|
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
|
|
422
667
|
if (!this.capabilities?.implementationProvider) {
|
|
423
668
|
return unavailableCodeQuery("Implementation requests are not supported by this server.");
|
|
424
669
|
}
|
|
425
|
-
return this.query(
|
|
426
|
-
textDocument
|
|
427
|
-
position,
|
|
428
|
-
|
|
670
|
+
return this.query(
|
|
671
|
+
"textDocument/implementation",
|
|
672
|
+
{ textDocument: { uri: fileToUri(filePath) }, position },
|
|
673
|
+
control,
|
|
674
|
+
);
|
|
429
675
|
}
|
|
430
676
|
|
|
431
677
|
// ── Private ─────────────────────────────────────────────────────────
|
|
432
|
-
private async query<T>(
|
|
678
|
+
private async query<T>(
|
|
679
|
+
method: string,
|
|
680
|
+
params: unknown,
|
|
681
|
+
control?: CodeRequestControl,
|
|
682
|
+
): Promise<CodeQueryResult<T | null>> {
|
|
683
|
+
// An already-cancelled caller gets the interruption, not an unavailable
|
|
684
|
+
// outcome that could mask the cancellation.
|
|
685
|
+
throwIfCodeRequestInterrupted(control);
|
|
433
686
|
if (!this.rpc || this._status !== "running") {
|
|
434
687
|
return unavailableCodeQuery(
|
|
435
688
|
`LSP request ${method} is unavailable because the client is not running.`,
|
|
436
689
|
);
|
|
437
690
|
}
|
|
438
691
|
try {
|
|
439
|
-
await this.getReady();
|
|
440
|
-
const data = (await this.rpc.sendRequest(method, params)) as T | null | undefined;
|
|
692
|
+
await this.getReady(control);
|
|
693
|
+
const data = (await this.rpc.sendRequest(method, params, control)) as T | null | undefined;
|
|
441
694
|
return completedCodeQuery(data ?? null);
|
|
442
695
|
} catch (error) {
|
|
696
|
+
// Cancellation and absolute-deadline expiry propagate as interruptions:
|
|
697
|
+
// the caller no longer awaits a result, so no unavailable outcome may
|
|
698
|
+
// mask the cancellation.
|
|
699
|
+
if (isCodeRequestInterruption(error, control)) throw error;
|
|
443
700
|
const detail = error instanceof Error ? error.message : String(error);
|
|
444
701
|
return unavailableCodeQuery(`LSP request ${method} failed: ${detail}`);
|
|
445
702
|
}
|
|
446
703
|
}
|
|
447
704
|
|
|
448
|
-
private async request<T>(
|
|
449
|
-
|
|
705
|
+
private async request<T>(
|
|
706
|
+
method: string,
|
|
707
|
+
params: unknown,
|
|
708
|
+
control?: CodeRequestControl,
|
|
709
|
+
): Promise<T | null> {
|
|
710
|
+
const result = await this.query<T>(method, params, control);
|
|
450
711
|
return result.kind === "unavailable" ? null : result.data;
|
|
451
712
|
}
|
|
452
713
|
|
|
@@ -457,23 +718,21 @@ export class LspClient {
|
|
|
457
718
|
case "workspace/workspaceFolders":
|
|
458
719
|
return [{ uri: fileToUri(this.root), name: path.basename(this.root) || this.root }];
|
|
459
720
|
case "client/registerCapability":
|
|
721
|
+
return this.handleRegisterCapability(params);
|
|
460
722
|
case "client/unregisterCapability":
|
|
461
|
-
return
|
|
723
|
+
return this.handleUnregisterCapability(params);
|
|
724
|
+
case "workspace/diagnostic/refresh":
|
|
725
|
+
// Refresh the tracked diagnostic set without blocking the server's
|
|
726
|
+
// request. The LSP response is always null; the pass records its
|
|
727
|
+
// terminal result asynchronously for local protocol diagnosis.
|
|
728
|
+
return this.handleServerDiagnosticRefreshRequest();
|
|
462
729
|
case "window/workDoneProgress/create": {
|
|
730
|
+
// A create reserves a token; it does not prove active work. The
|
|
731
|
+
// token stays pending and readiness is untouched until a begin
|
|
732
|
+
// arrives, so an unused token never causes false readiness loss.
|
|
463
733
|
const token = (params as { token: ProgressToken }).token;
|
|
464
|
-
this.trackedTokens.set(token, "
|
|
465
|
-
this.
|
|
466
|
-
this._isReady = false;
|
|
467
|
-
if (!this._readyPromise) {
|
|
468
|
-
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
469
|
-
this._readyResolve = resolve;
|
|
470
|
-
this._readyReject = reject;
|
|
471
|
-
});
|
|
472
|
-
// Prevent unhandled rejection when rejectReady fires before any
|
|
473
|
-
// consumer is actively awaiting this promise (e.g. during shutdown).
|
|
474
|
-
this._readyPromise.catch(() => {});
|
|
475
|
-
}
|
|
476
|
-
this.startTokenTimeout(token);
|
|
734
|
+
this.trackedTokens.set(token, "created");
|
|
735
|
+
this.tokenCreatedAt.set(token, Date.now());
|
|
477
736
|
return null;
|
|
478
737
|
}
|
|
479
738
|
default:
|
|
@@ -481,6 +740,51 @@ export class LspClient {
|
|
|
481
740
|
}
|
|
482
741
|
}
|
|
483
742
|
|
|
743
|
+
/** Run a server-requested refresh through the forced diagnostic path. */
|
|
744
|
+
private refreshForServerRequest(): Promise<DiagnosticEvidenceSummary> {
|
|
745
|
+
return this.diagnostics.refreshForServerRequest();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private handleServerDiagnosticRefreshRequest(): null {
|
|
749
|
+
// Defer the pass before invoking it. Its setup rereads and resynchronizes
|
|
750
|
+
// tracked documents, so even an async method can otherwise delay the null
|
|
751
|
+
// response on the JSON-RPC request stack.
|
|
752
|
+
void Promise.resolve()
|
|
753
|
+
.then(() => this.refreshForServerRequest())
|
|
754
|
+
.then(
|
|
755
|
+
(evidence) => this.recordDiagnosticRefreshRequest("completed", evidence),
|
|
756
|
+
() => this.recordDiagnosticRefreshRequest("failed"),
|
|
757
|
+
)
|
|
758
|
+
// Consume failures from the telemetry callback as well as the pass.
|
|
759
|
+
.catch(() => {});
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private recordDiagnosticRefreshRequest(
|
|
764
|
+
outcome: "completed" | "failed",
|
|
765
|
+
evidence?: DiagnosticEvidenceSummary,
|
|
766
|
+
): void {
|
|
767
|
+
recordDebugEvent({
|
|
768
|
+
source: "lsp",
|
|
769
|
+
level: "debug",
|
|
770
|
+
category: "diagnostics.refresh-request",
|
|
771
|
+
message: `LSP diagnostic refresh request ${outcome}`,
|
|
772
|
+
cwd: boundCwd(this.cwd),
|
|
773
|
+
data:
|
|
774
|
+
outcome === "completed" && evidence
|
|
775
|
+
? {
|
|
776
|
+
outcome,
|
|
777
|
+
server: truncateIdentity(this.name),
|
|
778
|
+
requested: evidence.requested,
|
|
779
|
+
confirmed: evidence.confirmed,
|
|
780
|
+
unconfirmed: evidence.unconfirmed,
|
|
781
|
+
failed: evidence.failed,
|
|
782
|
+
removed: evidence.removed,
|
|
783
|
+
}
|
|
784
|
+
: { outcome, server: truncateIdentity(this.name) },
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
484
788
|
private buildWorkspaceConfigurationResult(params: unknown): unknown[] {
|
|
485
789
|
if (!params || typeof params !== "object") return [];
|
|
486
790
|
const items = (params as { items?: unknown }).items;
|
|
@@ -488,8 +792,52 @@ export class LspClient {
|
|
|
488
792
|
return items.map(() => null);
|
|
489
793
|
}
|
|
490
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Apply a dynamic registration for `textDocument/diagnostic`.
|
|
797
|
+
*
|
|
798
|
+
* Registrations for other methods are ignored (status quo). Malformed
|
|
799
|
+
* params or malformed diagnostic registration options reject the request
|
|
800
|
+
* without enabling pull, so a server never gets pull requests it did not
|
|
801
|
+
* validly register for.
|
|
802
|
+
*/
|
|
803
|
+
private handleRegisterCapability(params: unknown): null {
|
|
804
|
+
const registrations = readRegistrations(params, "client/registerCapability");
|
|
805
|
+
for (const registration of registrations) {
|
|
806
|
+
if (registration.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
|
|
807
|
+
if (!isValidDiagnosticOptions(registration.registerOptions)) {
|
|
808
|
+
throw new JsonRpcRequestError(
|
|
809
|
+
-32602,
|
|
810
|
+
"Malformed textDocument/diagnostic registration options.",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
this.dynamicRegistrations.register(registration.method, registration.id);
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Remove dynamic registrations for `textDocument/diagnostic`.
|
|
820
|
+
*
|
|
821
|
+
* The params key is the LSP specification's documented compatibility typo
|
|
822
|
+
* `unregisterations` (renamed to `unregistrations` only in a future 4.x).
|
|
823
|
+
* Capability loss disables pull as soon as the last id is removed.
|
|
824
|
+
*/
|
|
825
|
+
private handleUnregisterCapability(params: unknown): null {
|
|
826
|
+
if (!isRecord(params) || !Array.isArray(params.unregisterations)) {
|
|
827
|
+
throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability params.");
|
|
828
|
+
}
|
|
829
|
+
for (const entry of params.unregisterations) {
|
|
830
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || typeof entry.method !== "string") {
|
|
831
|
+
throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability entry.");
|
|
832
|
+
}
|
|
833
|
+
if (entry.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
|
|
834
|
+
this.dynamicRegistrations.unregister(entry.method, entry.id);
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
|
|
491
839
|
/** Apply a diagnostic publication received from the LSP transport. */
|
|
492
|
-
handlePublishDiagnostics(params:
|
|
840
|
+
handlePublishDiagnostics(params: unknown): void {
|
|
493
841
|
this.diagnostics.handlePublishDiagnostics(params);
|
|
494
842
|
}
|
|
495
843
|
|
|
@@ -499,9 +847,17 @@ export class LspClient {
|
|
|
499
847
|
* Wait for the server to be ready to serve queries.
|
|
500
848
|
* Returns immediately if already ready; returns the ongoing promise
|
|
501
849
|
* if one is pending; creates and returns a new one otherwise.
|
|
850
|
+
* With request control, the caller's wait stops promptly on abort or
|
|
851
|
+
* deadline while the shared readiness state keeps its own lifecycle.
|
|
502
852
|
*/
|
|
503
|
-
async getReady(): Promise<void> {
|
|
504
|
-
|
|
853
|
+
async getReady(control?: CodeRequestControl): Promise<void> {
|
|
854
|
+
const pending = this.pendingReady();
|
|
855
|
+
if (!control) return pending;
|
|
856
|
+
return raceRequestControl(pending, control);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private pendingReady(): Promise<void> {
|
|
860
|
+
if (this._isReady) return Promise.resolve();
|
|
505
861
|
if (this._readyPromise !== null) return this._readyPromise;
|
|
506
862
|
// If no progress timer was ever armed and no tokens are tracked,
|
|
507
863
|
// the server was either never started with a real process (test scenario)
|
|
@@ -509,8 +865,11 @@ export class LspClient {
|
|
|
509
865
|
// only when the client is still running — a crash or shutdown clears
|
|
510
866
|
// both fields but must not report the client as ready.
|
|
511
867
|
if (this.noProgressTimer === null && this.trackedTokens.size === 0) {
|
|
512
|
-
if (this._status === "running")
|
|
513
|
-
|
|
868
|
+
if (this._status === "running") {
|
|
869
|
+
this._isReady = true;
|
|
870
|
+
this.everReady = true;
|
|
871
|
+
}
|
|
872
|
+
return Promise.resolve();
|
|
514
873
|
}
|
|
515
874
|
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
516
875
|
this._readyResolve = resolve;
|
|
@@ -533,14 +892,20 @@ export class LspClient {
|
|
|
533
892
|
source: "lsp",
|
|
534
893
|
level: "debug",
|
|
535
894
|
category: "readiness.progress-begin",
|
|
536
|
-
message: `Readiness progress begin for
|
|
537
|
-
|
|
895
|
+
message: `Readiness progress begin for ${this.name}`,
|
|
896
|
+
cwd: boundCwd(this.cwd),
|
|
897
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
|
|
538
898
|
});
|
|
539
|
-
//
|
|
540
|
-
//
|
|
899
|
+
// begin is the only transition that proves active work: it cancels
|
|
900
|
+
// the no-progress grace timer, blocks readiness, and arms the
|
|
901
|
+
// bounded per-token timeout. A server that sends begin without a
|
|
902
|
+
// prior create is spec-deviant but valid.
|
|
541
903
|
this.cancelNoProgressTimer();
|
|
542
|
-
this.trackedTokens.set(token, "
|
|
904
|
+
this.trackedTokens.set(token, "active");
|
|
905
|
+
this.tokenCreatedAt.delete(token);
|
|
906
|
+
const wasReady = this._isReady;
|
|
543
907
|
this._isReady = false;
|
|
908
|
+
if (wasReady) this.publishLifecycle("readiness");
|
|
544
909
|
// Re-arm readiness promise if not already pending
|
|
545
910
|
if (!this._readyPromise) {
|
|
546
911
|
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
@@ -557,22 +922,36 @@ export class LspClient {
|
|
|
557
922
|
source: "lsp",
|
|
558
923
|
level: "debug",
|
|
559
924
|
category: "readiness.progress-end",
|
|
560
|
-
message: `Readiness progress end for
|
|
561
|
-
|
|
925
|
+
message: `Readiness progress end for ${this.name}`,
|
|
926
|
+
cwd: boundCwd(this.cwd),
|
|
927
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
|
|
562
928
|
});
|
|
929
|
+
const state = this.trackedTokens.get(token);
|
|
930
|
+
if (state === undefined) return; // Unknown token: ignore fail-closed.
|
|
931
|
+
this.tokenCreatedAt.delete(token);
|
|
932
|
+
if (state === "created") {
|
|
933
|
+
// A pending token never blocked readiness; its end removes it.
|
|
934
|
+
this.trackedTokens.delete(token);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
563
937
|
this.trackedTokens.set(token, "ended");
|
|
564
938
|
this.clearTokenTimeout(token);
|
|
565
939
|
this.checkAllTokensEnded();
|
|
566
940
|
}
|
|
567
|
-
// kind: "report" — intentionally no-op
|
|
941
|
+
// kind: "report" — intentionally no-op; active state is retained.
|
|
568
942
|
}
|
|
569
943
|
|
|
570
|
-
/**
|
|
571
|
-
private
|
|
572
|
-
if (this.trackedTokens.size === 0) return;
|
|
944
|
+
/** Test whether any token has active (begun) work. */
|
|
945
|
+
private hasActiveTokens(): boolean {
|
|
573
946
|
for (const state of this.trackedTokens.values()) {
|
|
574
|
-
if (state
|
|
947
|
+
if (state === "active") return true;
|
|
575
948
|
}
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Resolve readiness when no token is active; pending tokens do not block. */
|
|
953
|
+
private checkAllTokensEnded(): void {
|
|
954
|
+
if (this.hasActiveTokens()) return;
|
|
576
955
|
this.trackedTokens.clear();
|
|
577
956
|
this.resolveReady();
|
|
578
957
|
}
|
|
@@ -585,15 +964,35 @@ export class LspClient {
|
|
|
585
964
|
this._readyResolve = undefined;
|
|
586
965
|
this._readyReject = undefined;
|
|
587
966
|
}
|
|
967
|
+
// A rejected or disposed client must never be marked ready again,
|
|
968
|
+
// even if a stray progress end arrives after the rejection.
|
|
969
|
+
if (this._status !== "running") return;
|
|
970
|
+
const becameReady = !this._isReady;
|
|
588
971
|
this._isReady = true;
|
|
972
|
+
if (becameReady) this.everReady = true;
|
|
973
|
+
if (becameReady) this.publishLifecycle("readiness");
|
|
589
974
|
recordDebugEvent({
|
|
590
975
|
source: "lsp",
|
|
591
976
|
level: "info",
|
|
592
977
|
category: "readiness.resolved",
|
|
593
|
-
message: `LSP client ${this.name} is ready
|
|
978
|
+
message: `LSP client ${this.name} is ready`,
|
|
979
|
+
cwd: boundCwd(this.cwd),
|
|
980
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
|
|
594
981
|
});
|
|
595
982
|
}
|
|
596
983
|
|
|
984
|
+
/**
|
|
985
|
+
* Tear down readiness state without a protocol shutdown.
|
|
986
|
+
*
|
|
987
|
+
* Clears pending and active progress tokens and rejects any pending
|
|
988
|
+
* readiness. Called when a client is discarded or its observer is
|
|
989
|
+
* disposed so token state cannot outlive the client.
|
|
990
|
+
*/
|
|
991
|
+
dispose(): void {
|
|
992
|
+
this.cancelNoProgressTimer();
|
|
993
|
+
this.dynamicRegistrations.clear();
|
|
994
|
+
this.rejectReady(new Error("Client disposed"));
|
|
995
|
+
}
|
|
597
996
|
/**
|
|
598
997
|
* Reject the current readiness promise (if any) and mark the client
|
|
599
998
|
* not ready. Called on shutdown, crash, or restart.
|
|
@@ -607,6 +1006,7 @@ export class LspClient {
|
|
|
607
1006
|
}
|
|
608
1007
|
this._isReady = false;
|
|
609
1008
|
this.trackedTokens.clear();
|
|
1009
|
+
this.tokenCreatedAt.clear();
|
|
610
1010
|
for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
|
|
611
1011
|
this.tokenTimeouts.clear();
|
|
612
1012
|
recordDebugEvent({
|
|
@@ -614,7 +1014,12 @@ export class LspClient {
|
|
|
614
1014
|
level: this._status === "shutdown" ? "debug" : "warning",
|
|
615
1015
|
category: "readiness.rejected",
|
|
616
1016
|
message: `LSP client ${this.name} readiness rejected: ${reason.message}`,
|
|
617
|
-
|
|
1017
|
+
cwd: boundCwd(this.cwd),
|
|
1018
|
+
data: {
|
|
1019
|
+
server: truncateIdentity(this.name),
|
|
1020
|
+
root: truncateIdentity(this.root),
|
|
1021
|
+
status: this._status,
|
|
1022
|
+
},
|
|
618
1023
|
});
|
|
619
1024
|
}
|
|
620
1025
|
|
|
@@ -634,8 +1039,9 @@ export class LspClient {
|
|
|
634
1039
|
source: "lsp",
|
|
635
1040
|
level: "debug",
|
|
636
1041
|
category: "readiness.token-timeout",
|
|
637
|
-
message: `Readiness per-token timeout fired for
|
|
638
|
-
|
|
1042
|
+
message: `Readiness per-token timeout fired for ${this.name} after ${timeoutMs}ms`,
|
|
1043
|
+
cwd: boundCwd(this.cwd),
|
|
1044
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root), timeoutMs },
|
|
639
1045
|
});
|
|
640
1046
|
this.checkAllTokensEnded();
|
|
641
1047
|
}, timeoutMs);
|
|
@@ -661,6 +1067,8 @@ export class LspClient {
|
|
|
661
1067
|
level: "debug",
|
|
662
1068
|
category: "readiness.no-progress-cancelled",
|
|
663
1069
|
message: `No-progress grace timer cancelled for ${this.name}`,
|
|
1070
|
+
cwd: boundCwd(this.cwd),
|
|
1071
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
|
|
664
1072
|
});
|
|
665
1073
|
}
|
|
666
1074
|
}
|
|
@@ -676,12 +1084,14 @@ export class LspClient {
|
|
|
676
1084
|
*/
|
|
677
1085
|
private armNoProgressTimer(): void {
|
|
678
1086
|
this.noProgressTimer = setTimeout(() => {
|
|
679
|
-
if (this.
|
|
1087
|
+
if (!this.hasActiveTokens() && this._status === "running") {
|
|
680
1088
|
recordDebugEvent({
|
|
681
1089
|
source: "lsp",
|
|
682
1090
|
level: "debug",
|
|
683
1091
|
category: "readiness.no-progress-resolved",
|
|
684
1092
|
message: `No-progress grace timer resolved for ${this.name}`,
|
|
1093
|
+
cwd: boundCwd(this.cwd),
|
|
1094
|
+
data: { server: truncateIdentity(this.name), root: truncateIdentity(this.root) },
|
|
685
1095
|
});
|
|
686
1096
|
this.resolveReady();
|
|
687
1097
|
}
|