ultimate-pi 0.18.1 → 0.19.1
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/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +1 -2
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.agents/skills/web-retrieval/SKILL.md +163 -0
- package/.agents/skills/wiki-autoresearch/SKILL.md +6 -6
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +75 -123
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +0 -2
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -3
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +5 -3
- package/.pi/agents/harness/reviewing/adversary.md +0 -2
- package/.pi/agents/harness/reviewing/evaluator.md +0 -2
- package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
- package/.pi/agents/harness/running/executor.md +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/agents/harness/web-retrieval/web-answerer.md +35 -0
- package/.pi/agents/harness/web-retrieval/web-criteria-verifier.md +28 -0
- package/.pi/agents/harness/web-retrieval/web-gap-analyzer.md +31 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander-fast.md +34 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander.md +60 -0
- package/.pi/agents/harness/web-retrieval/web-summarizer.md +18 -0
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +6 -6
- package/.pi/extensions/harness-ask-user.ts +7 -7
- package/.pi/extensions/harness-debate-tools.ts +26 -42
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-plan-approval.ts +11 -11
- package/.pi/extensions/harness-run-context.ts +1070 -876
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +34 -163
- package/.pi/extensions/harness-subagents.ts +3 -3
- package/.pi/extensions/harness-telemetry.ts +2 -2
- package/.pi/extensions/harness-web-guard.ts +2 -1
- package/.pi/extensions/harness-web-tools.ts +691 -53
- package/.pi/extensions/policy-gate.ts +25 -5
- package/.pi/extensions/sentrux-rules-sync.ts +1 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/trace-recorder.ts +1 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +46 -25
- package/.pi/harness/agents.policy.yaml +309 -0
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/0050-agentic-web-retrieval-stack.md +46 -0
- package/.pi/harness/docs/adrs/README.md +5 -0
- package/.pi/harness/docs/harness-web-search.md +97 -0
- package/.pi/harness/env.harness.template +9 -1
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/examples/web-heuristic-angles.project.yaml +22 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/harness/web-heuristic-angles.json +278 -0
- package/.pi/harness/web-heuristic-angles.yaml +182 -0
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +331 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +0 -2
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
- package/.pi/lib/harness-subagent-auth.ts +81 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +74 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/lib/harness-web/artifacts.ts +200 -0
- package/.pi/lib/harness-web/cache.ts +369 -0
- package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +42 -2
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-plan.md +2 -1
- package/.pi/prompts/harness-setup.md +40 -65
- package/.pi/scripts/README.md +2 -5
- package/.pi/scripts/gen-web-heuristic-angles-json.mjs +24 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/harness-agents-manifest.mjs +60 -3
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +14 -2
- package/.pi/scripts/harness-verify.mjs +191 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +3 -3
- package/.pi/scripts/harness-web.py +218 -15
- package/.pi/scripts/harness_web/deep_search.py +55 -0
- package/.pi/scripts/harness_web/evidence_bundle.py +47 -0
- package/.pi/scripts/harness_web/find_similar.py +88 -0
- package/.pi/scripts/harness_web/heuristic_angles_shipped.py +85 -0
- package/.pi/scripts/harness_web/heuristic_config.py +251 -0
- package/.pi/scripts/harness_web/highlights.py +47 -0
- package/.pi/scripts/harness_web/multi_search.py +59 -0
- package/.pi/scripts/harness_web/output.py +24 -0
- package/.pi/scripts/harness_web/query_angles.py +116 -0
- package/.pi/scripts/harness_web/rank.py +163 -0
- package/.pi/scripts/harness_web/scrape.py +30 -0
- package/.pi/scripts/tests/test_harness_web_heuristic_config.py +132 -0
- package/.pi/scripts/tests/test_harness_web_query_angles.py +45 -0
- package/.pi/scripts/tests/test_harness_web_rank.py +56 -0
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +7 -2
- package/CHANGELOG.md +20 -0
- package/README.md +3 -12
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +17 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.agents/skills/scrapling-web/SKILL.md +0 -98
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/extensions/00-posthog-network-bootstrap.ts +0 -11
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -0,0 +1,1466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Handles JSON-RPC communication with language servers:
|
|
5
|
+
* - Initialize/shutdown lifecycle
|
|
6
|
+
* - Document synchronization (didOpen, didChange)
|
|
7
|
+
* - Diagnostics with debouncing
|
|
8
|
+
* - Request/response handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import type { MessageConnection } from "vscode-jsonrpc";
|
|
16
|
+
import {
|
|
17
|
+
createMessageConnection,
|
|
18
|
+
StreamMessageReader,
|
|
19
|
+
StreamMessageWriter,
|
|
20
|
+
} from "vscode-jsonrpc/node.js";
|
|
21
|
+
import { logLatency } from "../latency-logger.js";
|
|
22
|
+
|
|
23
|
+
import type { LSPProcess } from "./launch.js";
|
|
24
|
+
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
25
|
+
import { getStrategy } from "./server-strategies.js";
|
|
26
|
+
|
|
27
|
+
// --- Types ---
|
|
28
|
+
|
|
29
|
+
export interface LSPDiagnostic {
|
|
30
|
+
severity: 1 | 2 | 3 | 4; // Error, Warning, Info, Hint
|
|
31
|
+
message: string;
|
|
32
|
+
range: {
|
|
33
|
+
start: { line: number; character: number };
|
|
34
|
+
end: { line: number; character: number };
|
|
35
|
+
};
|
|
36
|
+
code?: string | number;
|
|
37
|
+
source?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LSPLocation {
|
|
41
|
+
uri: string;
|
|
42
|
+
range: {
|
|
43
|
+
start: { line: number; character: number };
|
|
44
|
+
end: { line: number; character: number };
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface LSPHover {
|
|
49
|
+
contents:
|
|
50
|
+
| string
|
|
51
|
+
| { kind: string; value: string }
|
|
52
|
+
| Array<string | { language: string; value: string }>;
|
|
53
|
+
range?: LSPLocation["range"];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LSPSignatureHelp {
|
|
57
|
+
signatures: Array<{
|
|
58
|
+
label: string;
|
|
59
|
+
documentation?: string | { kind: string; value: string };
|
|
60
|
+
parameters?: Array<{
|
|
61
|
+
label: string | [number, number];
|
|
62
|
+
documentation?: string | { kind: string; value: string };
|
|
63
|
+
}>;
|
|
64
|
+
}>;
|
|
65
|
+
activeSignature?: number;
|
|
66
|
+
activeParameter?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LSPCodeAction {
|
|
70
|
+
title: string;
|
|
71
|
+
kind?: string;
|
|
72
|
+
diagnostics?: LSPDiagnostic[];
|
|
73
|
+
edit?: unknown;
|
|
74
|
+
command?: unknown;
|
|
75
|
+
data?: unknown;
|
|
76
|
+
isPreferred?: boolean;
|
|
77
|
+
disabled?: { reason?: string };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LSPWorkspaceEdit {
|
|
81
|
+
changes?: Record<string, unknown[]>;
|
|
82
|
+
documentChanges?: unknown[];
|
|
83
|
+
changeAnnotations?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LSPWorkspaceDiagnosticsSupport {
|
|
87
|
+
advertised: boolean;
|
|
88
|
+
mode: "pull" | "push-only";
|
|
89
|
+
diagnosticProviderKind: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface LSPOperationSupport {
|
|
93
|
+
definition: boolean;
|
|
94
|
+
references: boolean;
|
|
95
|
+
hover: boolean;
|
|
96
|
+
signatureHelp: boolean;
|
|
97
|
+
documentSymbol: boolean;
|
|
98
|
+
workspaceSymbol: boolean;
|
|
99
|
+
codeAction: boolean;
|
|
100
|
+
rename: boolean;
|
|
101
|
+
implementation: boolean;
|
|
102
|
+
callHierarchy: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface LSPSymbol {
|
|
106
|
+
name: string;
|
|
107
|
+
kind: number;
|
|
108
|
+
location?: LSPLocation;
|
|
109
|
+
range?: LSPLocation["range"];
|
|
110
|
+
selectionRange?: LSPLocation["range"];
|
|
111
|
+
detail?: string;
|
|
112
|
+
children?: LSPSymbol[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Call Hierarchy Types ---
|
|
116
|
+
|
|
117
|
+
export interface LSPCallHierarchyItem {
|
|
118
|
+
name: string;
|
|
119
|
+
kind: number;
|
|
120
|
+
uri: string;
|
|
121
|
+
range: LSPLocation["range"];
|
|
122
|
+
selectionRange: LSPLocation["range"];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface LSPCallHierarchyIncomingCall {
|
|
126
|
+
from: LSPCallHierarchyItem;
|
|
127
|
+
fromRanges: LSPLocation["range"][];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface LSPCallHierarchyOutgoingCall {
|
|
131
|
+
to: LSPCallHierarchyItem;
|
|
132
|
+
fromRanges: LSPLocation["range"][];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface LSPClientInfo {
|
|
136
|
+
serverId: string;
|
|
137
|
+
root: string;
|
|
138
|
+
connection: MessageConnection;
|
|
139
|
+
/** Check if the connection is still alive */
|
|
140
|
+
isAlive: () => boolean;
|
|
141
|
+
/** True if the server process has exited or been killed */
|
|
142
|
+
processExited: () => boolean;
|
|
143
|
+
/** Last N lines of server stderr for diagnostics */
|
|
144
|
+
recentStderr: (lines?: number) => string;
|
|
145
|
+
/** Pre-request health check — returns error string if process is dead */
|
|
146
|
+
checkAlive: () => string | undefined;
|
|
147
|
+
notify: {
|
|
148
|
+
open(
|
|
149
|
+
filePath: string,
|
|
150
|
+
content: string,
|
|
151
|
+
languageId: string,
|
|
152
|
+
preserveDiagnostics?: boolean,
|
|
153
|
+
silent?: boolean,
|
|
154
|
+
): Promise<void>;
|
|
155
|
+
change(filePath: string, content: string): Promise<void>;
|
|
156
|
+
};
|
|
157
|
+
getDiagnostics(filePath: string): LSPDiagnostic[];
|
|
158
|
+
waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
|
|
159
|
+
/** Get all tracked diagnostics with timestamps (for cascade checking) */
|
|
160
|
+
getAllDiagnostics(): Map<string, { diags: LSPDiagnostic[]; ts: number }>;
|
|
161
|
+
pruneDiagnostics(
|
|
162
|
+
predicate: (
|
|
163
|
+
filePath: string,
|
|
164
|
+
ts: number,
|
|
165
|
+
diags: LSPDiagnostic[],
|
|
166
|
+
) => boolean,
|
|
167
|
+
): number;
|
|
168
|
+
/** Capability snapshot for workspace diagnostics support */
|
|
169
|
+
getWorkspaceDiagnosticsSupport(): LSPWorkspaceDiagnosticsSupport;
|
|
170
|
+
/** Capability snapshot for navigation/edit operations */
|
|
171
|
+
getOperationSupport(): LSPOperationSupport;
|
|
172
|
+
/** Go to definition — returns Location[] */
|
|
173
|
+
definition(
|
|
174
|
+
filePath: string,
|
|
175
|
+
line: number,
|
|
176
|
+
character: number,
|
|
177
|
+
): Promise<LSPLocation[]>;
|
|
178
|
+
/** Find all references */
|
|
179
|
+
references(
|
|
180
|
+
filePath: string,
|
|
181
|
+
line: number,
|
|
182
|
+
character: number,
|
|
183
|
+
includeDeclaration?: boolean,
|
|
184
|
+
): Promise<LSPLocation[]>;
|
|
185
|
+
/** Hover info at position */
|
|
186
|
+
hover(
|
|
187
|
+
filePath: string,
|
|
188
|
+
line: number,
|
|
189
|
+
character: number,
|
|
190
|
+
): Promise<LSPHover | null>;
|
|
191
|
+
/** Signature help at position */
|
|
192
|
+
signatureHelp(
|
|
193
|
+
filePath: string,
|
|
194
|
+
line: number,
|
|
195
|
+
character: number,
|
|
196
|
+
): Promise<LSPSignatureHelp | null>;
|
|
197
|
+
/** Symbols in a document */
|
|
198
|
+
documentSymbol(filePath: string): Promise<LSPSymbol[]>;
|
|
199
|
+
/** Workspace-wide symbol search */
|
|
200
|
+
workspaceSymbol(query: string): Promise<LSPSymbol[]>;
|
|
201
|
+
/** Available code actions at a range */
|
|
202
|
+
codeAction(
|
|
203
|
+
filePath: string,
|
|
204
|
+
line: number,
|
|
205
|
+
character: number,
|
|
206
|
+
endLine: number,
|
|
207
|
+
endCharacter: number,
|
|
208
|
+
): Promise<LSPCodeAction[]>;
|
|
209
|
+
/** Rename symbol at position */
|
|
210
|
+
rename(
|
|
211
|
+
filePath: string,
|
|
212
|
+
line: number,
|
|
213
|
+
character: number,
|
|
214
|
+
newName: string,
|
|
215
|
+
): Promise<LSPWorkspaceEdit | null>;
|
|
216
|
+
/** Go to implementation */
|
|
217
|
+
implementation(
|
|
218
|
+
filePath: string,
|
|
219
|
+
line: number,
|
|
220
|
+
character: number,
|
|
221
|
+
): Promise<LSPLocation[]>;
|
|
222
|
+
/** Prepare call hierarchy at position */
|
|
223
|
+
prepareCallHierarchy(
|
|
224
|
+
filePath: string,
|
|
225
|
+
line: number,
|
|
226
|
+
character: number,
|
|
227
|
+
): Promise<LSPCallHierarchyItem[]>;
|
|
228
|
+
/** Find incoming calls (callers) */
|
|
229
|
+
incomingCalls(
|
|
230
|
+
item: LSPCallHierarchyItem,
|
|
231
|
+
): Promise<LSPCallHierarchyIncomingCall[]>;
|
|
232
|
+
/** Find outgoing calls (callees) */
|
|
233
|
+
outgoingCalls(
|
|
234
|
+
item: LSPCallHierarchyItem,
|
|
235
|
+
): Promise<LSPCallHierarchyOutgoingCall[]>;
|
|
236
|
+
shutdown(): Promise<void>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- Constants ---
|
|
240
|
+
|
|
241
|
+
const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv(
|
|
242
|
+
"PI_LENS_LSP_INIT_TIMEOUT_MS",
|
|
243
|
+
15_000,
|
|
244
|
+
); // 15s — npx downloads are handled by ensureTool, not here
|
|
245
|
+
const NAV_REQUEST_TIMEOUT_MS = positiveIntFromEnv(
|
|
246
|
+
"PI_LENS_LSP_NAV_REQUEST_TIMEOUT_MS",
|
|
247
|
+
10_000,
|
|
248
|
+
); // 10s — per-request ceiling; prevents heavy servers (vue, svelte) from hanging
|
|
249
|
+
const DIAGNOSTICS_WAIT_TIMEOUT_MS = positiveIntFromEnv(
|
|
250
|
+
"PI_LENS_LSP_DIAGNOSTICS_WAIT_MS",
|
|
251
|
+
10_000,
|
|
252
|
+
);
|
|
253
|
+
const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv(
|
|
254
|
+
"PI_LENS_LSP_PULL_RETRY_INTERVAL_MS",
|
|
255
|
+
250,
|
|
256
|
+
);
|
|
257
|
+
const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv(
|
|
258
|
+
"PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS",
|
|
259
|
+
1000,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const LSP_CRASH_CODES = new Set([
|
|
263
|
+
"ERR_STREAM_DESTROYED",
|
|
264
|
+
"ERR_STREAM_WRITE_AFTER_END",
|
|
265
|
+
"EPIPE",
|
|
266
|
+
"ECONNRESET",
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
let crashGuardInstalled = false;
|
|
270
|
+
|
|
271
|
+
function isIgnorableLspRuntimeCrash(err: unknown): boolean {
|
|
272
|
+
if (!(err instanceof Error)) return false;
|
|
273
|
+
const code = (err as { code?: string }).code;
|
|
274
|
+
if (code && LSP_CRASH_CODES.has(code)) return true;
|
|
275
|
+
const msg = err.message.toLowerCase();
|
|
276
|
+
const stack = (err.stack ?? "").toLowerCase();
|
|
277
|
+
return (
|
|
278
|
+
msg.includes("stream") ||
|
|
279
|
+
msg.includes("write after end") ||
|
|
280
|
+
stack.includes("vscode-jsonrpc/lib/node/ril.js")
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function installCrashGuard(): void {
|
|
285
|
+
if (crashGuardInstalled) return;
|
|
286
|
+
crashGuardInstalled = true;
|
|
287
|
+
|
|
288
|
+
process.on("uncaughtException", (err) => {
|
|
289
|
+
if (isIgnorableLspRuntimeCrash(err)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
throw err;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
process.on("unhandledRejection", (reason) => {
|
|
296
|
+
if (isIgnorableLspRuntimeCrash(reason)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw reason instanceof Error ? reason : new Error(String(reason));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- Client State + Module-level helpers ---
|
|
304
|
+
|
|
305
|
+
export interface LSPClientState {
|
|
306
|
+
isConnected: boolean;
|
|
307
|
+
isDestroyed: boolean;
|
|
308
|
+
connectionDisposed: boolean;
|
|
309
|
+
lastError: Error | undefined;
|
|
310
|
+
readonly connection: MessageConnection;
|
|
311
|
+
readonly pushDiagnostics: Map<string, LSPDiagnostic[]>;
|
|
312
|
+
readonly pushDiagnosticTimestamps: Map<string, number>;
|
|
313
|
+
readonly documentPullDiagnostics: Map<string, LSPDiagnostic[]>;
|
|
314
|
+
readonly documentPullDiagnosticTimestamps: Map<string, number>;
|
|
315
|
+
readonly pendingDiagnostics: Map<string, ReturnType<typeof setTimeout>>;
|
|
316
|
+
readonly diagnosticEmitter: EventEmitter;
|
|
317
|
+
readonly documentVersions: Map<string, number>;
|
|
318
|
+
readonly openDocuments: Set<string>;
|
|
319
|
+
readonly pendingOpens: Set<string>;
|
|
320
|
+
/** Mutable: updated by applyDynamicCapabilities after registerCapability events */
|
|
321
|
+
workspaceDiagnosticsSupport: LSPWorkspaceDiagnosticsSupport;
|
|
322
|
+
/** Mutable: upgraded by applyDynamicCapabilities after registerCapability events */
|
|
323
|
+
operationSupport: LSPOperationSupport;
|
|
324
|
+
/** Baseline mode from static initResult — used to revert on unregister */
|
|
325
|
+
staticDiagnosticsMode: "pull" | "push-only";
|
|
326
|
+
/** Live dynamic registrations from client/registerCapability: id → method */
|
|
327
|
+
readonly dynamicRegistrations: Map<string, string>;
|
|
328
|
+
readonly serverId: string;
|
|
329
|
+
readonly root: string;
|
|
330
|
+
readonly lspProcess: LSPProcess;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isClientAlive(state: LSPClientState): boolean {
|
|
334
|
+
return (
|
|
335
|
+
state.isConnected && !state.isDestroyed && !state.lspProcess.process.killed
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function disposeClientConnection(state: LSPClientState): void {
|
|
340
|
+
if (state.connectionDisposed) return;
|
|
341
|
+
state.connectionDisposed = true;
|
|
342
|
+
try {
|
|
343
|
+
state.connection.dispose();
|
|
344
|
+
} catch {
|
|
345
|
+
// ignore
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function killProcessTree(
|
|
350
|
+
proc: { kill(signal?: NodeJS.Signals | number): boolean },
|
|
351
|
+
pid: number,
|
|
352
|
+
): Promise<void> {
|
|
353
|
+
if (process.platform === "win32" && pid > 0) {
|
|
354
|
+
await new Promise<void>((resolve) => {
|
|
355
|
+
try {
|
|
356
|
+
// Absolute path avoids PATH-resolution: SystemRoot is set by Windows itself.
|
|
357
|
+
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
358
|
+
const killer = nodeSpawn(taskkill, ["/F", "/T", "/PID", String(pid)], {
|
|
359
|
+
shell: false,
|
|
360
|
+
windowsHide: true,
|
|
361
|
+
});
|
|
362
|
+
killer.once("close", () => resolve());
|
|
363
|
+
killer.once("error", () => resolve());
|
|
364
|
+
} catch {
|
|
365
|
+
resolve();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
proc.kill("SIGTERM");
|
|
373
|
+
// SIGTERM → 1.5s → SIGKILL escalation.
|
|
374
|
+
// SIGTERM alone can leave zombie processes if the server hangs.
|
|
375
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
|
|
376
|
+
try {
|
|
377
|
+
if (!(proc as { killed?: boolean }).killed) {
|
|
378
|
+
proc.kill("SIGKILL");
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
// best-effort
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// ignore
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function mergeDiagnosticLists(
|
|
389
|
+
push: LSPDiagnostic[] | undefined,
|
|
390
|
+
pull: LSPDiagnostic[] | undefined,
|
|
391
|
+
): LSPDiagnostic[] {
|
|
392
|
+
const merged: LSPDiagnostic[] = [];
|
|
393
|
+
const seen = new Set<string>();
|
|
394
|
+
for (const diagnostic of [...(push ?? []), ...(pull ?? [])]) {
|
|
395
|
+
const key = [
|
|
396
|
+
diagnostic.range.start.line,
|
|
397
|
+
diagnostic.range.start.character,
|
|
398
|
+
diagnostic.range.end.line,
|
|
399
|
+
diagnostic.range.end.character,
|
|
400
|
+
diagnostic.code ?? "",
|
|
401
|
+
diagnostic.source ?? "",
|
|
402
|
+
diagnostic.message,
|
|
403
|
+
].join(":");
|
|
404
|
+
if (seen.has(key)) continue;
|
|
405
|
+
seen.add(key);
|
|
406
|
+
merged.push(diagnostic);
|
|
407
|
+
}
|
|
408
|
+
return merged;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getMergedDiagnosticsForPath(
|
|
412
|
+
state: LSPClientState,
|
|
413
|
+
normalizedPath: string,
|
|
414
|
+
): LSPDiagnostic[] {
|
|
415
|
+
const legacy = state as unknown as {
|
|
416
|
+
diagnostics?: Map<string, LSPDiagnostic[]>;
|
|
417
|
+
};
|
|
418
|
+
return mergeDiagnosticLists(
|
|
419
|
+
state.pushDiagnostics?.get(normalizedPath) ??
|
|
420
|
+
legacy.diagnostics?.get(normalizedPath),
|
|
421
|
+
state.documentPullDiagnostics?.get(normalizedPath),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function clearDiagnosticsForPath(
|
|
426
|
+
state: LSPClientState,
|
|
427
|
+
normalizedPath: string,
|
|
428
|
+
): void {
|
|
429
|
+
const legacy = state as unknown as {
|
|
430
|
+
diagnostics?: Map<string, LSPDiagnostic[]>;
|
|
431
|
+
diagnosticTimestamps?: Map<string, number>;
|
|
432
|
+
};
|
|
433
|
+
state.pushDiagnostics?.delete(normalizedPath);
|
|
434
|
+
state.pushDiagnosticTimestamps?.delete(normalizedPath);
|
|
435
|
+
state.documentPullDiagnostics?.delete(normalizedPath);
|
|
436
|
+
state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
|
|
437
|
+
legacy.diagnostics?.delete(normalizedPath);
|
|
438
|
+
legacy.diagnosticTimestamps?.delete(normalizedPath);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Methods that can be registered dynamically and map to operationSupport keys
|
|
442
|
+
const DYNAMIC_OPERATION_METHOD_MAP: Record<string, keyof LSPOperationSupport> =
|
|
443
|
+
{
|
|
444
|
+
"textDocument/definition": "definition",
|
|
445
|
+
"textDocument/references": "references",
|
|
446
|
+
"textDocument/hover": "hover",
|
|
447
|
+
"textDocument/signatureHelp": "signatureHelp",
|
|
448
|
+
"textDocument/documentSymbol": "documentSymbol",
|
|
449
|
+
"workspace/symbol": "workspaceSymbol",
|
|
450
|
+
"textDocument/codeAction": "codeAction",
|
|
451
|
+
"textDocument/rename": "rename",
|
|
452
|
+
"textDocument/implementation": "implementation",
|
|
453
|
+
"textDocument/prepareCallHierarchy": "callHierarchy",
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
export function applyDynamicCapabilities(state: LSPClientState): void {
|
|
457
|
+
const registeredMethods = new Set(state.dynamicRegistrations.values());
|
|
458
|
+
|
|
459
|
+
const hasDynamicPull =
|
|
460
|
+
registeredMethods.has("textDocument/diagnostic") ||
|
|
461
|
+
registeredMethods.has("workspace/diagnostic");
|
|
462
|
+
|
|
463
|
+
if (hasDynamicPull) {
|
|
464
|
+
state.workspaceDiagnosticsSupport = {
|
|
465
|
+
advertised: true,
|
|
466
|
+
mode: "pull",
|
|
467
|
+
diagnosticProviderKind: "dynamic",
|
|
468
|
+
};
|
|
469
|
+
} else if (
|
|
470
|
+
state.staticDiagnosticsMode === "push-only" &&
|
|
471
|
+
state.workspaceDiagnosticsSupport.diagnosticProviderKind === "dynamic"
|
|
472
|
+
) {
|
|
473
|
+
// Was only dynamically registered, now unregistered — revert to push-only
|
|
474
|
+
state.workspaceDiagnosticsSupport = {
|
|
475
|
+
advertised: false,
|
|
476
|
+
mode: "push-only",
|
|
477
|
+
diagnosticProviderKind: "none",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const [method, key] of Object.entries(DYNAMIC_OPERATION_METHOD_MAP)) {
|
|
482
|
+
if (registeredMethods.has(method)) {
|
|
483
|
+
state.operationSupport[key] = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function setupIncomingHandlers(
|
|
489
|
+
state: LSPClientState,
|
|
490
|
+
initialization: Record<string, unknown> | undefined,
|
|
491
|
+
): void {
|
|
492
|
+
state.connection.onNotification(
|
|
493
|
+
"textDocument/publishDiagnostics",
|
|
494
|
+
(params: { uri: string; diagnostics?: LSPDiagnostic[] }) => {
|
|
495
|
+
const filePath = uriToPath(params.uri);
|
|
496
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
497
|
+
const newDiags: LSPDiagnostic[] = params.diagnostics || [];
|
|
498
|
+
const strategy = getStrategy(state.serverId);
|
|
499
|
+
|
|
500
|
+
// Seed on first push for servers whose first push is known complete.
|
|
501
|
+
// Bypasses the debounce timer entirely — resolves waiting promises immediately.
|
|
502
|
+
if (
|
|
503
|
+
strategy.seedFirstPush &&
|
|
504
|
+
!state.pushDiagnostics.has(normalizedPath)
|
|
505
|
+
) {
|
|
506
|
+
state.pushDiagnostics.set(normalizedPath, newDiags);
|
|
507
|
+
state.pushDiagnosticTimestamps.set(normalizedPath, Date.now());
|
|
508
|
+
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const existingTimer = state.pendingDiagnostics.get(normalizedPath);
|
|
513
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
514
|
+
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
state.pushDiagnostics.set(normalizedPath, newDiags);
|
|
517
|
+
state.pushDiagnosticTimestamps.set(normalizedPath, Date.now());
|
|
518
|
+
state.pendingDiagnostics.delete(normalizedPath);
|
|
519
|
+
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
520
|
+
}, strategy.debounceMs);
|
|
521
|
+
|
|
522
|
+
state.pendingDiagnostics.set(normalizedPath, timer);
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
state.connection.onRequest("workspace/workspaceFolders", () => [
|
|
527
|
+
{ name: "workspace", uri: pathToFileURL(state.root).href },
|
|
528
|
+
]);
|
|
529
|
+
state.connection.onRequest(
|
|
530
|
+
"client/registerCapability",
|
|
531
|
+
async (params: {
|
|
532
|
+
registrations?: Array<{ id: string; method: string }>;
|
|
533
|
+
}) => {
|
|
534
|
+
for (const reg of params?.registrations ?? []) {
|
|
535
|
+
if (reg.id && reg.method) {
|
|
536
|
+
state.dynamicRegistrations.set(reg.id, reg.method);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
applyDynamicCapabilities(state);
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
state.connection.onRequest(
|
|
543
|
+
"client/unregisterCapability",
|
|
544
|
+
async (params: { unregisterations?: Array<{ id: string }> }) => {
|
|
545
|
+
for (const unreg of params?.unregisterations ?? []) {
|
|
546
|
+
if (unreg.id) {
|
|
547
|
+
state.dynamicRegistrations.delete(unreg.id);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
applyDynamicCapabilities(state);
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
state.connection.onRequest("workspace/configuration", async () => [
|
|
554
|
+
initialization ?? {},
|
|
555
|
+
]);
|
|
556
|
+
state.connection.onRequest("window/workDoneProgress/create", async () => {});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function setupConnectionLifecycle(state: LSPClientState): void {
|
|
560
|
+
state.connection.onError(([error]: [Error, ...unknown[]]) => {
|
|
561
|
+
state.lastError = error instanceof Error ? error : new Error(String(error));
|
|
562
|
+
state.isConnected = false;
|
|
563
|
+
state.isDestroyed = true;
|
|
564
|
+
disposeClientConnection(state);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
state.connection.onClose(() => {
|
|
568
|
+
state.isConnected = false;
|
|
569
|
+
state.isDestroyed = true;
|
|
570
|
+
disposeClientConnection(state);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
state.lspProcess.process.on("exit", (code) => {
|
|
574
|
+
const wasConnected = state.isConnected;
|
|
575
|
+
state.isConnected = false;
|
|
576
|
+
state.isDestroyed = true;
|
|
577
|
+
disposeClientConnection(state);
|
|
578
|
+
if (wasConnected) {
|
|
579
|
+
logLatency({
|
|
580
|
+
type: "phase",
|
|
581
|
+
phase: "lsp_server_unexpected_exit",
|
|
582
|
+
filePath: state.root,
|
|
583
|
+
durationMs: 0,
|
|
584
|
+
metadata: {
|
|
585
|
+
serverId: state.serverId,
|
|
586
|
+
pid: state.lspProcess.pid,
|
|
587
|
+
exitCode: code ?? null,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function clientRequestPullDiagnostics(
|
|
595
|
+
state: LSPClientState,
|
|
596
|
+
filePath: string,
|
|
597
|
+
): Promise<number> {
|
|
598
|
+
if (!isClientAlive(state)) return 0;
|
|
599
|
+
const uri = pathToFileURL(filePath).href;
|
|
600
|
+
try {
|
|
601
|
+
const report = await safeSendRequest<{
|
|
602
|
+
kind?: string;
|
|
603
|
+
items?: LSPDiagnostic[];
|
|
604
|
+
relatedDocuments?: Record<string, { items?: LSPDiagnostic[] }>;
|
|
605
|
+
}>(state.connection, "textDocument/diagnostic", { textDocument: { uri } });
|
|
606
|
+
|
|
607
|
+
if (!report) return 0;
|
|
608
|
+
|
|
609
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
610
|
+
const primaryItems = report.items ?? [];
|
|
611
|
+
const now = Date.now();
|
|
612
|
+
state.documentPullDiagnostics.set(normalizedPath, primaryItems);
|
|
613
|
+
state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
|
|
614
|
+
let totalCount = primaryItems.length;
|
|
615
|
+
|
|
616
|
+
if (report.relatedDocuments) {
|
|
617
|
+
for (const [relatedUri, related] of Object.entries(
|
|
618
|
+
report.relatedDocuments,
|
|
619
|
+
)) {
|
|
620
|
+
const relatedPath = uriToPath(relatedUri);
|
|
621
|
+
const relatedItems = related?.items ?? [];
|
|
622
|
+
state.documentPullDiagnostics.set(
|
|
623
|
+
normalizeMapKey(relatedPath),
|
|
624
|
+
relatedItems,
|
|
625
|
+
);
|
|
626
|
+
state.documentPullDiagnosticTimestamps.set(
|
|
627
|
+
normalizeMapKey(relatedPath),
|
|
628
|
+
now,
|
|
629
|
+
);
|
|
630
|
+
totalCount += relatedItems.length;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
635
|
+
return totalCount;
|
|
636
|
+
} catch {
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function clientWaitForDiagnostics(
|
|
642
|
+
state: LSPClientState,
|
|
643
|
+
filePath: string,
|
|
644
|
+
timeoutMs: number,
|
|
645
|
+
): Promise<void> {
|
|
646
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
647
|
+
|
|
648
|
+
if (state.workspaceDiagnosticsSupport.mode === "pull") {
|
|
649
|
+
const firstPullCount = await clientRequestPullDiagnostics(state, filePath);
|
|
650
|
+
if (firstPullCount > 0) return;
|
|
651
|
+
|
|
652
|
+
const strategy = getStrategy(state.serverId);
|
|
653
|
+
const retryBudgetMs =
|
|
654
|
+
strategy.pullRetryBudgetMs > 0
|
|
655
|
+
? Math.min(timeoutMs, strategy.pullRetryBudgetMs)
|
|
656
|
+
: 0;
|
|
657
|
+
const startedAt = Date.now();
|
|
658
|
+
let latestCount = firstPullCount;
|
|
659
|
+
|
|
660
|
+
while (latestCount === 0 && Date.now() - startedAt < retryBudgetMs) {
|
|
661
|
+
await new Promise((resolve) =>
|
|
662
|
+
setTimeout(resolve, PULL_DIAGNOSTICS_RETRY_INTERVAL_MS),
|
|
663
|
+
);
|
|
664
|
+
latestCount = await clientRequestPullDiagnostics(state, filePath);
|
|
665
|
+
}
|
|
666
|
+
if (latestCount > 0) return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (getMergedDiagnosticsForPath(state, normalizedPath).length > 0) return;
|
|
670
|
+
|
|
671
|
+
return new Promise<void>((resolve) => {
|
|
672
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
673
|
+
|
|
674
|
+
const onDiagnostics = (fp: string) => {
|
|
675
|
+
if (normalizeMapKey(fp) !== normalizedPath) return;
|
|
676
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
677
|
+
|
|
678
|
+
// Adaptive debounce: use time since last push to compute remaining
|
|
679
|
+
// wait instead of always waiting the full debounce window.
|
|
680
|
+
const strategy = getStrategy(state.serverId);
|
|
681
|
+
const hit = state.pushDiagnosticTimestamps.get(normalizedPath);
|
|
682
|
+
const timeSincePush = hit ? Date.now() - hit : Infinity;
|
|
683
|
+
const remaining = Math.max(0, strategy.debounceMs - timeSincePush);
|
|
684
|
+
|
|
685
|
+
debounceTimer = setTimeout(() => {
|
|
686
|
+
state.diagnosticEmitter.off("diagnostics", onDiagnostics);
|
|
687
|
+
clearTimeout(timeout);
|
|
688
|
+
resolve();
|
|
689
|
+
}, remaining);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
state.diagnosticEmitter.on("diagnostics", onDiagnostics);
|
|
693
|
+
|
|
694
|
+
const timeout = setTimeout(() => {
|
|
695
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
696
|
+
state.diagnosticEmitter.off("diagnostics", onDiagnostics);
|
|
697
|
+
resolve();
|
|
698
|
+
}, timeoutMs);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function handleNotifyOpen(
|
|
703
|
+
state: LSPClientState,
|
|
704
|
+
filePath: string,
|
|
705
|
+
content: string,
|
|
706
|
+
languageId: string,
|
|
707
|
+
preserveDiagnostics = false,
|
|
708
|
+
silent = false,
|
|
709
|
+
): Promise<void> {
|
|
710
|
+
if (!isClientAlive(state)) return;
|
|
711
|
+
const uri = pathToFileURL(filePath).href;
|
|
712
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
713
|
+
|
|
714
|
+
if (
|
|
715
|
+
state.openDocuments.has(normalizedPath) ||
|
|
716
|
+
state.pendingOpens.has(normalizedPath)
|
|
717
|
+
) {
|
|
718
|
+
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
719
|
+
state.documentVersions.set(normalizedPath, version);
|
|
720
|
+
// preserveDiagnostics: skip cache clear for format-only resyncs so
|
|
721
|
+
// waitForDiagnostics fast-paths instead of waiting up to 5s for TypeScript
|
|
722
|
+
// to re-publish what it already knows (formatting doesn't change semantics).
|
|
723
|
+
if (!preserveDiagnostics) {
|
|
724
|
+
clearDiagnosticsForPath(state, normalizedPath);
|
|
725
|
+
}
|
|
726
|
+
await safeSendNotification(state.connection, "textDocument/didChange", {
|
|
727
|
+
textDocument: { uri, version },
|
|
728
|
+
contentChanges: [{ text: content }],
|
|
729
|
+
});
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
state.pendingOpens.add(normalizedPath);
|
|
734
|
+
state.documentVersions.set(normalizedPath, 0);
|
|
735
|
+
clearDiagnosticsForPath(state, normalizedPath); // always clear for initial open
|
|
736
|
+
|
|
737
|
+
// Send workspace notification first (like opencode does).
|
|
738
|
+
// Skipped in silent mode — cascade reads a file for diagnostics,
|
|
739
|
+
// not reporting a real filesystem change. Avoids N project-wide
|
|
740
|
+
// rechecks on push-diagnostics LSPs (TypeScript, Python) per CR-1.
|
|
741
|
+
if (!silent) {
|
|
742
|
+
await safeSendNotification(
|
|
743
|
+
state.connection,
|
|
744
|
+
"workspace/didChangeWatchedFiles",
|
|
745
|
+
{ changes: [{ uri, type: existsSync(filePath) ? 2 : 1 }] },
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (!isClientAlive(state)) return;
|
|
750
|
+
|
|
751
|
+
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
752
|
+
textDocument: { uri, languageId, version: 0, text: content },
|
|
753
|
+
});
|
|
754
|
+
state.pendingOpens.delete(normalizedPath);
|
|
755
|
+
state.openDocuments.add(normalizedPath);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export async function handleNotifyChange(
|
|
759
|
+
state: LSPClientState,
|
|
760
|
+
filePath: string,
|
|
761
|
+
content: string,
|
|
762
|
+
): Promise<void> {
|
|
763
|
+
if (!isClientAlive(state)) return;
|
|
764
|
+
const uri = pathToFileURL(filePath).href;
|
|
765
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
766
|
+
|
|
767
|
+
if (!state.openDocuments.has(normalizedPath)) {
|
|
768
|
+
// Safety fallback: keep protocol ordering valid even if caller sends
|
|
769
|
+
// didChange before first didOpen for this document.
|
|
770
|
+
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
771
|
+
textDocument: { uri, languageId: "plaintext", version: 0, text: content },
|
|
772
|
+
});
|
|
773
|
+
state.documentVersions.set(normalizedPath, 0);
|
|
774
|
+
state.openDocuments.add(normalizedPath);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
779
|
+
state.documentVersions.set(normalizedPath, version);
|
|
780
|
+
// Clear stale diagnostics before sending new content so waitForDiagnostics
|
|
781
|
+
// doesn't return immediately with the previous edit's results.
|
|
782
|
+
clearDiagnosticsForPath(state, normalizedPath);
|
|
783
|
+
await safeSendNotification(state.connection, "textDocument/didChange", {
|
|
784
|
+
textDocument: { uri, version },
|
|
785
|
+
contentChanges: [{ text: content }],
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function clientShutdown(state: LSPClientState): Promise<void> {
|
|
790
|
+
state.isConnected = false;
|
|
791
|
+
state.isDestroyed = true;
|
|
792
|
+
for (const timer of state.pendingDiagnostics.values()) {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
}
|
|
795
|
+
state.pendingDiagnostics.clear();
|
|
796
|
+
state.pendingOpens.clear();
|
|
797
|
+
state.openDocuments.clear();
|
|
798
|
+
state.diagnosticEmitter.removeAllListeners();
|
|
799
|
+
try {
|
|
800
|
+
await withTimeout(
|
|
801
|
+
safeSendRequest(state.connection, "shutdown", {}),
|
|
802
|
+
SHUTDOWN_REQUEST_TIMEOUT_MS,
|
|
803
|
+
);
|
|
804
|
+
} catch {
|
|
805
|
+
/* ignore — proceed to exit/kill so shutdown cannot hang the session */
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
await safeSendNotification(state.connection, "exit", {});
|
|
809
|
+
} catch {
|
|
810
|
+
/* ignore */
|
|
811
|
+
}
|
|
812
|
+
disposeClientConnection(state);
|
|
813
|
+
const pid = state.lspProcess.pid;
|
|
814
|
+
// On Windows, killing the direct child first can orphan grandchildren before
|
|
815
|
+
// taskkill can traverse the tree. Kill the full tree first and wait briefly.
|
|
816
|
+
await killProcessTree(state.lspProcess.process, pid);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function navRequest<T>(
|
|
820
|
+
state: LSPClientState,
|
|
821
|
+
method: string,
|
|
822
|
+
params: Record<string, unknown>,
|
|
823
|
+
): Promise<T | null | undefined> {
|
|
824
|
+
if (!isClientAlive(state)) return null;
|
|
825
|
+
return withTimeout(
|
|
826
|
+
safeSendRequest<T>(state.connection, method, params),
|
|
827
|
+
NAV_REQUEST_TIMEOUT_MS,
|
|
828
|
+
).catch((err: unknown) => {
|
|
829
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
830
|
+
return undefined;
|
|
831
|
+
}
|
|
832
|
+
throw err;
|
|
833
|
+
}) as Promise<T | undefined>;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// --- Client Factory ---
|
|
837
|
+
|
|
838
|
+
export async function createLSPClient(options: {
|
|
839
|
+
serverId: string;
|
|
840
|
+
process: LSPProcess;
|
|
841
|
+
root: string;
|
|
842
|
+
initialization?: Record<string, unknown>;
|
|
843
|
+
initializeTimeoutMs?: number;
|
|
844
|
+
}): Promise<LSPClientInfo> {
|
|
845
|
+
installCrashGuard();
|
|
846
|
+
|
|
847
|
+
const {
|
|
848
|
+
serverId,
|
|
849
|
+
process: lspProcess,
|
|
850
|
+
root,
|
|
851
|
+
initialization,
|
|
852
|
+
initializeTimeoutMs = INITIALIZE_TIMEOUT_MS,
|
|
853
|
+
} = options;
|
|
854
|
+
|
|
855
|
+
const startupState: {
|
|
856
|
+
exitCode: number | null;
|
|
857
|
+
exitSignal: NodeJS.Signals | null;
|
|
858
|
+
closeCode: number | null;
|
|
859
|
+
closeSignal: NodeJS.Signals | null;
|
|
860
|
+
stderr: string;
|
|
861
|
+
} = {
|
|
862
|
+
exitCode: null,
|
|
863
|
+
exitSignal: null,
|
|
864
|
+
closeCode: null,
|
|
865
|
+
closeSignal: null,
|
|
866
|
+
stderr: "",
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// Persistent stderr ring buffer — captures last ~100 lines for diagnostics.
|
|
870
|
+
// Used in error messages to show what the server said before dying.
|
|
871
|
+
const stderrRing: string[] = [];
|
|
872
|
+
const MAX_STDERR_LINES = 100;
|
|
873
|
+
|
|
874
|
+
const onStderr = (chunk: Buffer | string): void => {
|
|
875
|
+
stderrRing.push(chunk.toString());
|
|
876
|
+
if (stderrRing.length > MAX_STDERR_LINES) stderrRing.shift();
|
|
877
|
+
// Also capture startup stderr for the initialized-failed error path
|
|
878
|
+
if (startupState.stderr.length < 4096) {
|
|
879
|
+
startupState.stderr += chunk.toString();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const recentStderr = (lines = 10): string =>
|
|
884
|
+
stderrRing.slice(-lines).join("").trim();
|
|
885
|
+
|
|
886
|
+
// Pre-request health check — returns error string if process is dead.
|
|
887
|
+
const checkProcessAlive = (): string | undefined => {
|
|
888
|
+
const exited = lspProcess.process.exitCode;
|
|
889
|
+
if (exited !== null) {
|
|
890
|
+
const tail = recentStderr(20);
|
|
891
|
+
return `LSP server ${serverId} exited with code ${exited}${tail ? `. stderr: ${tail}` : ""}`;
|
|
892
|
+
}
|
|
893
|
+
if ((lspProcess.process as { killed?: boolean }).killed) {
|
|
894
|
+
return `LSP server ${serverId} was killed`;
|
|
895
|
+
}
|
|
896
|
+
return undefined;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const onProcessExit = (
|
|
900
|
+
code: number | null,
|
|
901
|
+
signal: NodeJS.Signals | null,
|
|
902
|
+
): void => {
|
|
903
|
+
startupState.exitCode = code;
|
|
904
|
+
startupState.exitSignal = signal;
|
|
905
|
+
};
|
|
906
|
+
const onProcessClose = (
|
|
907
|
+
code: number | null,
|
|
908
|
+
signal: NodeJS.Signals | null,
|
|
909
|
+
): void => {
|
|
910
|
+
startupState.closeCode = code;
|
|
911
|
+
startupState.closeSignal = signal;
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
(lspProcess.stderr as NodeJS.ReadableStream).on("data", onStderr);
|
|
915
|
+
lspProcess.process.on("exit", onProcessExit);
|
|
916
|
+
lspProcess.process.on("close", onProcessClose);
|
|
917
|
+
|
|
918
|
+
// Attach persistent 'error' listeners to all three stdio streams.
|
|
919
|
+
//
|
|
920
|
+
// Why: when the LSP process exits, Node.js destroys its stdio streams and
|
|
921
|
+
// may emit 'error' (ERR_STREAM_DESTROYED / EPIPE / ECONNRESET) on them.
|
|
922
|
+
// Without a listener that becomes an uncaught exception.
|
|
923
|
+
//
|
|
924
|
+
// vscode-jsonrpc covers stdin/stdout during the connection lifetime but
|
|
925
|
+
// removes its listeners on dispose(). Our permanent listeners cover the gap.
|
|
926
|
+
const streamErrorHandler =
|
|
927
|
+
(_label: string) => (err: Error & { code?: string }) => {
|
|
928
|
+
if (
|
|
929
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
930
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
931
|
+
err.code === "EPIPE" ||
|
|
932
|
+
err.code === "ECONNRESET"
|
|
933
|
+
)
|
|
934
|
+
return;
|
|
935
|
+
};
|
|
936
|
+
(lspProcess.stdin as NodeJS.WritableStream).on(
|
|
937
|
+
"error",
|
|
938
|
+
streamErrorHandler("stdin"),
|
|
939
|
+
);
|
|
940
|
+
(lspProcess.stdout as NodeJS.ReadableStream).on(
|
|
941
|
+
"error",
|
|
942
|
+
streamErrorHandler("stdout"),
|
|
943
|
+
);
|
|
944
|
+
(lspProcess.stderr as NodeJS.ReadableStream).on(
|
|
945
|
+
"error",
|
|
946
|
+
streamErrorHandler("stderr"),
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
const connection = createMessageConnection(
|
|
950
|
+
new StreamMessageReader(lspProcess.stdout),
|
|
951
|
+
new StreamMessageWriter(lspProcess.stdin),
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
// Local event emitter — signals waitForDiagnostics when new diagnostics arrive.
|
|
955
|
+
// Scoped to this client instance. setMaxListeners guards against Node.js warning
|
|
956
|
+
// for concurrent waitForDiagnostics calls.
|
|
957
|
+
const diagnosticEmitter = new EventEmitter();
|
|
958
|
+
diagnosticEmitter.setMaxListeners(50);
|
|
959
|
+
|
|
960
|
+
const state: LSPClientState = {
|
|
961
|
+
isConnected: true,
|
|
962
|
+
isDestroyed: false,
|
|
963
|
+
connectionDisposed: false,
|
|
964
|
+
lastError: undefined,
|
|
965
|
+
connection,
|
|
966
|
+
pushDiagnostics: new Map(),
|
|
967
|
+
pushDiagnosticTimestamps: new Map(),
|
|
968
|
+
documentPullDiagnostics: new Map(),
|
|
969
|
+
documentPullDiagnosticTimestamps: new Map(),
|
|
970
|
+
pendingDiagnostics: new Map(),
|
|
971
|
+
diagnosticEmitter,
|
|
972
|
+
documentVersions: new Map(),
|
|
973
|
+
openDocuments: new Set(),
|
|
974
|
+
pendingOpens: new Set(),
|
|
975
|
+
// these are filled in after initialize — cast to avoid two-phase init
|
|
976
|
+
workspaceDiagnosticsSupport:
|
|
977
|
+
undefined as unknown as LSPWorkspaceDiagnosticsSupport,
|
|
978
|
+
operationSupport: undefined as unknown as LSPOperationSupport,
|
|
979
|
+
staticDiagnosticsMode: "push-only",
|
|
980
|
+
dynamicRegistrations: new Map(),
|
|
981
|
+
serverId,
|
|
982
|
+
root,
|
|
983
|
+
lspProcess,
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
setupIncomingHandlers(state, initialization);
|
|
987
|
+
connection.listen();
|
|
988
|
+
setupConnectionLifecycle(state);
|
|
989
|
+
|
|
990
|
+
let initResult: Awaited<ReturnType<typeof safeSendRequest>>;
|
|
991
|
+
try {
|
|
992
|
+
initResult = await withTimeout(
|
|
993
|
+
safeSendRequest(connection, "initialize", {
|
|
994
|
+
processId: process.pid,
|
|
995
|
+
rootUri: pathToFileURL(root).href,
|
|
996
|
+
workspaceFolders: [
|
|
997
|
+
{ name: "workspace", uri: pathToFileURL(root).href },
|
|
998
|
+
],
|
|
999
|
+
capabilities: {
|
|
1000
|
+
window: { workDoneProgress: true },
|
|
1001
|
+
workspace: {
|
|
1002
|
+
workspaceFolders: true,
|
|
1003
|
+
configuration: true,
|
|
1004
|
+
didChangeWatchedFiles: { dynamicRegistration: true },
|
|
1005
|
+
},
|
|
1006
|
+
textDocument: {
|
|
1007
|
+
synchronization: { didOpen: true, didChange: true },
|
|
1008
|
+
publishDiagnostics: { versionSupport: true },
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
initializationOptions: initialization,
|
|
1012
|
+
}),
|
|
1013
|
+
initializeTimeoutMs,
|
|
1014
|
+
);
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
// Hard-kill the hung process so it doesn't become a zombie.
|
|
1017
|
+
// SIGTERM alone is unreliable on Windows for cmd.exe/PowerShell trees.
|
|
1018
|
+
const pid = lspProcess.pid;
|
|
1019
|
+
void killProcessTree(lspProcess.process, pid);
|
|
1020
|
+
setTimeout(() => {
|
|
1021
|
+
if (!lspProcess.process.killed && process.platform !== "win32") {
|
|
1022
|
+
lspProcess.process.kill("SIGKILL");
|
|
1023
|
+
}
|
|
1024
|
+
}, 2000);
|
|
1025
|
+
throw err;
|
|
1026
|
+
} finally {
|
|
1027
|
+
(lspProcess.stderr as NodeJS.ReadableStream).off("data", onStderr);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (initResult === undefined) {
|
|
1031
|
+
const compactStderr = startupState.stderr
|
|
1032
|
+
.replace(/\s+/g, " ")
|
|
1033
|
+
.trim()
|
|
1034
|
+
.slice(0, 320);
|
|
1035
|
+
const reinstallHint =
|
|
1036
|
+
serverId === "cpp"
|
|
1037
|
+
? "Install clangd (LLVM/clang-tools) and ensure clangd.exe is on PATH."
|
|
1038
|
+
: `Try reinstalling: npm install -g ${serverId}-language-server.`;
|
|
1039
|
+
const telemetry = [
|
|
1040
|
+
`pid=${lspProcess.pid}`,
|
|
1041
|
+
`exitCode=${startupState.exitCode ?? "none"}`,
|
|
1042
|
+
`exitSignal=${startupState.exitSignal ?? "none"}`,
|
|
1043
|
+
`closeCode=${startupState.closeCode ?? "none"}`,
|
|
1044
|
+
`closeSignal=${startupState.closeSignal ?? "none"}`,
|
|
1045
|
+
`root=${root}`,
|
|
1046
|
+
compactStderr ? `stderr=${compactStderr}` : "stderr=<empty>",
|
|
1047
|
+
].join(" ");
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`[lsp] ${serverId} failed to initialize - stream may have been destroyed. ` +
|
|
1050
|
+
`The server binary may be missing or crashed immediately. ${reinstallHint} ` +
|
|
1051
|
+
`telemetry: ${telemetry}`,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
state.workspaceDiagnosticsSupport =
|
|
1056
|
+
detectWorkspaceDiagnosticsSupport(initResult);
|
|
1057
|
+
state.operationSupport = detectOperationSupport(initResult);
|
|
1058
|
+
state.staticDiagnosticsMode = state.workspaceDiagnosticsSupport.mode;
|
|
1059
|
+
|
|
1060
|
+
await safeSendNotification(connection, "initialized", {});
|
|
1061
|
+
if (initialization) {
|
|
1062
|
+
await safeSendNotification(connection, "workspace/didChangeConfiguration", {
|
|
1063
|
+
settings: initialization,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
serverId,
|
|
1069
|
+
root,
|
|
1070
|
+
connection,
|
|
1071
|
+
isAlive: () => isClientAlive(state),
|
|
1072
|
+
|
|
1073
|
+
/** True if the server process has exited or been killed. */
|
|
1074
|
+
processExited: () =>
|
|
1075
|
+
lspProcess.process.exitCode !== null ||
|
|
1076
|
+
(lspProcess.process as { killed?: boolean }).killed === true,
|
|
1077
|
+
|
|
1078
|
+
/** Last N lines of server stderr for diagnostics. */
|
|
1079
|
+
recentStderr: (lines?: number) => recentStderr(lines),
|
|
1080
|
+
|
|
1081
|
+
/** Pre-request health check — returns error string if dead. */
|
|
1082
|
+
checkAlive: () => checkProcessAlive(),
|
|
1083
|
+
|
|
1084
|
+
notify: {
|
|
1085
|
+
async open(filePath, content, languageId, preserveDiagnostics, silent) {
|
|
1086
|
+
return handleNotifyOpen(
|
|
1087
|
+
state,
|
|
1088
|
+
filePath,
|
|
1089
|
+
content,
|
|
1090
|
+
languageId,
|
|
1091
|
+
preserveDiagnostics,
|
|
1092
|
+
silent,
|
|
1093
|
+
);
|
|
1094
|
+
},
|
|
1095
|
+
async change(filePath, content) {
|
|
1096
|
+
return handleNotifyChange(state, filePath, content);
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
getDiagnostics(filePath) {
|
|
1101
|
+
return getMergedDiagnosticsForPath(state, normalizeMapKey(filePath));
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
getAllDiagnostics() {
|
|
1105
|
+
const result = new Map<string, { diags: LSPDiagnostic[]; ts: number }>();
|
|
1106
|
+
const keys = new Set([
|
|
1107
|
+
...state.pushDiagnostics.keys(),
|
|
1108
|
+
...state.documentPullDiagnostics.keys(),
|
|
1109
|
+
]);
|
|
1110
|
+
for (const key of keys) {
|
|
1111
|
+
result.set(key, {
|
|
1112
|
+
diags: getMergedDiagnosticsForPath(state, key),
|
|
1113
|
+
ts: Math.max(
|
|
1114
|
+
state.pushDiagnosticTimestamps.get(key) ?? 0,
|
|
1115
|
+
state.documentPullDiagnosticTimestamps.get(key) ?? 0,
|
|
1116
|
+
),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return result;
|
|
1120
|
+
},
|
|
1121
|
+
|
|
1122
|
+
pruneDiagnostics(predicate) {
|
|
1123
|
+
let removed = 0;
|
|
1124
|
+
const keys = new Set([
|
|
1125
|
+
...state.pushDiagnostics.keys(),
|
|
1126
|
+
...state.documentPullDiagnostics.keys(),
|
|
1127
|
+
]);
|
|
1128
|
+
for (const key of keys) {
|
|
1129
|
+
const diags = getMergedDiagnosticsForPath(state, key);
|
|
1130
|
+
const ts = Math.max(
|
|
1131
|
+
state.pushDiagnosticTimestamps.get(key) ?? 0,
|
|
1132
|
+
state.documentPullDiagnosticTimestamps.get(key) ?? 0,
|
|
1133
|
+
);
|
|
1134
|
+
if (!predicate(key, ts, diags)) continue;
|
|
1135
|
+
clearDiagnosticsForPath(state, key);
|
|
1136
|
+
removed++;
|
|
1137
|
+
}
|
|
1138
|
+
return removed;
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
getWorkspaceDiagnosticsSupport() {
|
|
1142
|
+
return state.workspaceDiagnosticsSupport;
|
|
1143
|
+
},
|
|
1144
|
+
|
|
1145
|
+
getOperationSupport() {
|
|
1146
|
+
return state.operationSupport;
|
|
1147
|
+
},
|
|
1148
|
+
|
|
1149
|
+
async waitForDiagnostics(
|
|
1150
|
+
filePath,
|
|
1151
|
+
timeoutMs = DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
1152
|
+
) {
|
|
1153
|
+
return clientWaitForDiagnostics(state, filePath, timeoutMs);
|
|
1154
|
+
},
|
|
1155
|
+
|
|
1156
|
+
async definition(filePath, line, character) {
|
|
1157
|
+
const result = await navRequest<LSPLocation | LSPLocation[]>(
|
|
1158
|
+
state,
|
|
1159
|
+
"textDocument/definition",
|
|
1160
|
+
{
|
|
1161
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1162
|
+
position: { line, character },
|
|
1163
|
+
},
|
|
1164
|
+
);
|
|
1165
|
+
if (!result) return [];
|
|
1166
|
+
return Array.isArray(result) ? result : [result];
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
async references(filePath, line, character, includeDeclaration = true) {
|
|
1170
|
+
const result = await navRequest<LSPLocation[]>(
|
|
1171
|
+
state,
|
|
1172
|
+
"textDocument/references",
|
|
1173
|
+
{
|
|
1174
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1175
|
+
position: { line, character },
|
|
1176
|
+
context: { includeDeclaration },
|
|
1177
|
+
},
|
|
1178
|
+
);
|
|
1179
|
+
return result ?? [];
|
|
1180
|
+
},
|
|
1181
|
+
|
|
1182
|
+
async hover(filePath, line, character) {
|
|
1183
|
+
const result = await navRequest<LSPHover>(state, "textDocument/hover", {
|
|
1184
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1185
|
+
position: { line, character },
|
|
1186
|
+
});
|
|
1187
|
+
return result ?? null;
|
|
1188
|
+
},
|
|
1189
|
+
|
|
1190
|
+
async signatureHelp(filePath, line, character) {
|
|
1191
|
+
const result = await navRequest<LSPSignatureHelp>(
|
|
1192
|
+
state,
|
|
1193
|
+
"textDocument/signatureHelp",
|
|
1194
|
+
{
|
|
1195
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1196
|
+
position: { line, character },
|
|
1197
|
+
},
|
|
1198
|
+
);
|
|
1199
|
+
return result ?? null;
|
|
1200
|
+
},
|
|
1201
|
+
|
|
1202
|
+
async documentSymbol(filePath) {
|
|
1203
|
+
const result = await navRequest<LSPSymbol[]>(
|
|
1204
|
+
state,
|
|
1205
|
+
"textDocument/documentSymbol",
|
|
1206
|
+
{ textDocument: { uri: pathToFileURL(filePath).href } },
|
|
1207
|
+
);
|
|
1208
|
+
return result ?? [];
|
|
1209
|
+
},
|
|
1210
|
+
|
|
1211
|
+
async workspaceSymbol(query) {
|
|
1212
|
+
if (!isClientAlive(state)) return [];
|
|
1213
|
+
const result = await safeSendRequest<LSPSymbol[]>(
|
|
1214
|
+
connection,
|
|
1215
|
+
"workspace/symbol",
|
|
1216
|
+
{ query },
|
|
1217
|
+
);
|
|
1218
|
+
return result ?? [];
|
|
1219
|
+
},
|
|
1220
|
+
|
|
1221
|
+
async codeAction(filePath, line, character, endLine, endCharacter) {
|
|
1222
|
+
if (!isClientAlive(state)) return [];
|
|
1223
|
+
const uri = pathToFileURL(filePath).href;
|
|
1224
|
+
const result = await safeSendRequest<unknown[]>(
|
|
1225
|
+
connection,
|
|
1226
|
+
"textDocument/codeAction",
|
|
1227
|
+
{
|
|
1228
|
+
textDocument: { uri },
|
|
1229
|
+
range: {
|
|
1230
|
+
start: { line, character },
|
|
1231
|
+
end: { line: endLine, character: endCharacter },
|
|
1232
|
+
},
|
|
1233
|
+
context: {
|
|
1234
|
+
diagnostics: getMergedDiagnosticsForPath(
|
|
1235
|
+
state,
|
|
1236
|
+
normalizeMapKey(filePath),
|
|
1237
|
+
),
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
);
|
|
1241
|
+
if (!result || !Array.isArray(result)) return [];
|
|
1242
|
+
return result.filter(
|
|
1243
|
+
(item): item is LSPCodeAction =>
|
|
1244
|
+
typeof item === "object" && item !== null && "title" in item,
|
|
1245
|
+
);
|
|
1246
|
+
},
|
|
1247
|
+
|
|
1248
|
+
async rename(filePath, line, character, newName) {
|
|
1249
|
+
const result = await navRequest<LSPWorkspaceEdit>(
|
|
1250
|
+
state,
|
|
1251
|
+
"textDocument/rename",
|
|
1252
|
+
{
|
|
1253
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1254
|
+
position: { line, character },
|
|
1255
|
+
newName,
|
|
1256
|
+
},
|
|
1257
|
+
);
|
|
1258
|
+
return result ?? null;
|
|
1259
|
+
},
|
|
1260
|
+
|
|
1261
|
+
async implementation(filePath, line, character) {
|
|
1262
|
+
const result = await navRequest<LSPLocation | LSPLocation[]>(
|
|
1263
|
+
state,
|
|
1264
|
+
"textDocument/implementation",
|
|
1265
|
+
{
|
|
1266
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1267
|
+
position: { line, character },
|
|
1268
|
+
},
|
|
1269
|
+
);
|
|
1270
|
+
if (!result) return [];
|
|
1271
|
+
return Array.isArray(result) ? result : [result];
|
|
1272
|
+
},
|
|
1273
|
+
|
|
1274
|
+
async prepareCallHierarchy(filePath, line, character) {
|
|
1275
|
+
const result = await navRequest<
|
|
1276
|
+
LSPCallHierarchyItem | LSPCallHierarchyItem[]
|
|
1277
|
+
>(state, "textDocument/prepareCallHierarchy", {
|
|
1278
|
+
textDocument: { uri: pathToFileURL(filePath).href },
|
|
1279
|
+
position: { line, character },
|
|
1280
|
+
});
|
|
1281
|
+
if (!result) return [];
|
|
1282
|
+
return Array.isArray(result) ? result : [result];
|
|
1283
|
+
},
|
|
1284
|
+
|
|
1285
|
+
async incomingCalls(item) {
|
|
1286
|
+
const result = await navRequest<LSPCallHierarchyIncomingCall[]>(
|
|
1287
|
+
state,
|
|
1288
|
+
"callHierarchy/incomingCalls",
|
|
1289
|
+
{ item },
|
|
1290
|
+
);
|
|
1291
|
+
return result ?? [];
|
|
1292
|
+
},
|
|
1293
|
+
|
|
1294
|
+
async outgoingCalls(item) {
|
|
1295
|
+
const result = await navRequest<LSPCallHierarchyOutgoingCall[]>(
|
|
1296
|
+
state,
|
|
1297
|
+
"callHierarchy/outgoingCalls",
|
|
1298
|
+
{ item },
|
|
1299
|
+
);
|
|
1300
|
+
return result ?? [];
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
async shutdown() {
|
|
1304
|
+
return clientShutdown(state);
|
|
1305
|
+
},
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Helper to safely send notifications - catches stream destruction
|
|
1310
|
+
async function safeSendNotification(
|
|
1311
|
+
connection: MessageConnection,
|
|
1312
|
+
method: string,
|
|
1313
|
+
params: unknown,
|
|
1314
|
+
): Promise<void> {
|
|
1315
|
+
try {
|
|
1316
|
+
await connection.sendNotification(method as never, params as never);
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
if (isStreamError(err)) {
|
|
1319
|
+
// Silently ignore - stream was destroyed, connection error handlers will update state
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
throw err;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Helper to safely send requests - catches stream destruction
|
|
1327
|
+
async function safeSendRequest<T>(
|
|
1328
|
+
connection: MessageConnection,
|
|
1329
|
+
method: string,
|
|
1330
|
+
params: unknown,
|
|
1331
|
+
): Promise<T | undefined> {
|
|
1332
|
+
try {
|
|
1333
|
+
return (await connection.sendRequest(
|
|
1334
|
+
method as never,
|
|
1335
|
+
params as never,
|
|
1336
|
+
)) as T;
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
if (isStreamError(err)) {
|
|
1339
|
+
// Silently ignore - stream was destroyed
|
|
1340
|
+
return undefined;
|
|
1341
|
+
}
|
|
1342
|
+
throw err;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Helper to detect stream destruction / connection disposal errors.
|
|
1347
|
+
// vscode-jsonrpc throws these when the LSP server process exits while
|
|
1348
|
+
// requests are still in flight:
|
|
1349
|
+
// "Connection is disposed."
|
|
1350
|
+
// "Pending response rejected since connection got disposed"
|
|
1351
|
+
// Neither phrase contains "stream", "destroyed", or "closed", which is
|
|
1352
|
+
// why we must also match "disposed" and "cancelled" here.
|
|
1353
|
+
function isStreamError(err: unknown): boolean {
|
|
1354
|
+
if (!(err instanceof Error)) return false;
|
|
1355
|
+
const msg = err.message.toLowerCase();
|
|
1356
|
+
return (
|
|
1357
|
+
msg.includes("stream") ||
|
|
1358
|
+
msg.includes("destroyed") ||
|
|
1359
|
+
msg.includes("closed") ||
|
|
1360
|
+
msg.includes("disposed") ||
|
|
1361
|
+
msg.includes("cancelled") ||
|
|
1362
|
+
(err as { code?: string }).code === "ERR_STREAM_DESTROYED" ||
|
|
1363
|
+
(err as { code?: string }).code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
1364
|
+
(err as { code?: string }).code === "EPIPE"
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Using shared path utilities from path-utils.ts
|
|
1369
|
+
|
|
1370
|
+
async function withTimeout<T>(
|
|
1371
|
+
promise: Promise<T>,
|
|
1372
|
+
timeoutMs: number,
|
|
1373
|
+
): Promise<T> {
|
|
1374
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1375
|
+
// Suppress unhandled rejection if `promise` rejects AFTER the timeout
|
|
1376
|
+
// wins the race — Promise.race settles on the first result but the
|
|
1377
|
+
// losing promises still run, and any later rejection would be uncaught.
|
|
1378
|
+
promise.catch(() => {});
|
|
1379
|
+
try {
|
|
1380
|
+
return await Promise.race([
|
|
1381
|
+
promise,
|
|
1382
|
+
new Promise<T>((_, reject) => {
|
|
1383
|
+
timeout = setTimeout(
|
|
1384
|
+
() => reject(new Error(`Timeout after ${timeoutMs}ms`)),
|
|
1385
|
+
timeoutMs,
|
|
1386
|
+
);
|
|
1387
|
+
}),
|
|
1388
|
+
]);
|
|
1389
|
+
} finally {
|
|
1390
|
+
if (timeout) clearTimeout(timeout);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function positiveIntFromEnv(name: string, fallback: number): number {
|
|
1395
|
+
const raw = process.env[name];
|
|
1396
|
+
if (!raw) return fallback;
|
|
1397
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1398
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
1399
|
+
return parsed;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function detectWorkspaceDiagnosticsSupport(
|
|
1403
|
+
initResult: unknown,
|
|
1404
|
+
): LSPWorkspaceDiagnosticsSupport {
|
|
1405
|
+
const capabilities =
|
|
1406
|
+
typeof initResult === "object" && initResult !== null
|
|
1407
|
+
? (initResult as { capabilities?: Record<string, unknown> }).capabilities
|
|
1408
|
+
: undefined;
|
|
1409
|
+
const diagnosticProvider = capabilities?.diagnosticProvider;
|
|
1410
|
+
if (!diagnosticProvider) {
|
|
1411
|
+
return {
|
|
1412
|
+
advertised: false,
|
|
1413
|
+
mode: "push-only",
|
|
1414
|
+
diagnosticProviderKind: "none",
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (typeof diagnosticProvider === "boolean") {
|
|
1419
|
+
return {
|
|
1420
|
+
advertised: diagnosticProvider,
|
|
1421
|
+
mode: diagnosticProvider ? "pull" : "push-only",
|
|
1422
|
+
diagnosticProviderKind: "boolean",
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (typeof diagnosticProvider === "object") {
|
|
1427
|
+
return {
|
|
1428
|
+
advertised: true,
|
|
1429
|
+
mode: "pull",
|
|
1430
|
+
diagnosticProviderKind: "object",
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return {
|
|
1435
|
+
advertised: false,
|
|
1436
|
+
mode: "push-only",
|
|
1437
|
+
diagnosticProviderKind: typeof diagnosticProvider,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function detectOperationSupport(initResult: unknown): LSPOperationSupport {
|
|
1442
|
+
const capabilities =
|
|
1443
|
+
typeof initResult === "object" && initResult !== null
|
|
1444
|
+
? (initResult as { capabilities?: Record<string, unknown> }).capabilities
|
|
1445
|
+
: undefined;
|
|
1446
|
+
|
|
1447
|
+
const hasProvider = (key: string): boolean => {
|
|
1448
|
+
const value = capabilities?.[key];
|
|
1449
|
+
if (value === undefined || value === null) return false;
|
|
1450
|
+
if (typeof value === "boolean") return value;
|
|
1451
|
+
return true;
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
return {
|
|
1455
|
+
definition: hasProvider("definitionProvider"),
|
|
1456
|
+
references: hasProvider("referencesProvider"),
|
|
1457
|
+
hover: hasProvider("hoverProvider"),
|
|
1458
|
+
signatureHelp: hasProvider("signatureHelpProvider"),
|
|
1459
|
+
documentSymbol: hasProvider("documentSymbolProvider"),
|
|
1460
|
+
workspaceSymbol: hasProvider("workspaceSymbolProvider"),
|
|
1461
|
+
codeAction: hasProvider("codeActionProvider"),
|
|
1462
|
+
rename: hasProvider("renameProvider"),
|
|
1463
|
+
implementation: hasProvider("implementationProvider"),
|
|
1464
|
+
callHierarchy: hasProvider("callHierarchyProvider"),
|
|
1465
|
+
};
|
|
1466
|
+
}
|