@herbertgao/pi-extensions 2026.8.4 → 2026.8.6
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 +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +32 -24
- package/node_modules/@narumitw/pi-btw/package.json +6 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +2535 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
- package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
- package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
- package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
- package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +51785 -36118
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +219 -46
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +40 -15
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +45 -3
- package/node_modules/pi-lens/docs/globalconfig.md +56 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +18 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +41 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
- package/node_modules/pi-mcp-adapter/README.md +38 -4
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +4 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +176 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +12 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Diagnostics with debouncing
|
|
8
8
|
* - Request/response handling
|
|
9
9
|
*/
|
|
10
|
+
import { logExtension } from "../extension-log.js";
|
|
10
11
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
11
12
|
import { EventEmitter } from "node:events";
|
|
12
13
|
import { access, readFile } from "node:fs/promises";
|
|
@@ -18,9 +19,12 @@ import { logLatency } from "../latency-logger.js";
|
|
|
18
19
|
// `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
|
|
19
20
|
import { CancellationTokenSource, createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "../deps/vscode-jsonrpc.js";
|
|
20
21
|
import { getAmbientAbortSignal } from "../safe-spawn.js";
|
|
21
|
-
import {
|
|
22
|
+
import { hashDiagnosticContent, } from "./diagnostic-binding.js";
|
|
23
|
+
import { newLspMutationCorrelationId, } from "../lsp-mutation.js";
|
|
24
|
+
import { applyWorkspaceEdit, normalizeWorkspaceEditToUtf16, } from "./edits.js";
|
|
22
25
|
import { recordLspChild, removeLspChild } from "../instance-registry.js";
|
|
23
26
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
27
|
+
import { probeTsserverProjectIdentity } from "./tsserver-sync.js";
|
|
24
28
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
25
29
|
import { getStrategy } from "./wait-policy/index.js";
|
|
26
30
|
import { WatchedFilesQueue } from "./watch-queue.js";
|
|
@@ -123,12 +127,35 @@ const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_
|
|
|
123
127
|
// (per #240) is NOT read as clean and falls through to the bounded push backstop.
|
|
124
128
|
const PULL_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_REQUEST_TIMEOUT_MS", 10_000);
|
|
125
129
|
const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS", 1000);
|
|
130
|
+
// #1277: cheap liveness round-trip for the silent-clean gates (`index.ts`).
|
|
131
|
+
// Those gates convert a diagnostics-wait timeout into a confirmed-clean
|
|
132
|
+
// result from a STATIC capability classification (`silentOnClean`) alone —
|
|
133
|
+
// but a wedged server (accepted the notify write, then hung) satisfies that
|
|
134
|
+
// classification identically to a genuinely clean one. This is deliberately
|
|
135
|
+
// short relative to NAV_REQUEST_TIMEOUT_MS: it only needs to prove the
|
|
136
|
+
// connection round-trips SOMETHING before the touch reports clean, not
|
|
137
|
+
// complete a real navigation request.
|
|
138
|
+
const LIVENESS_PING_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_LIVENESS_PING_TIMEOUT_MS", 300);
|
|
139
|
+
// Distinctive, unlikely-to-collide query string — the response content is
|
|
140
|
+
// never inspected, only whether one arrived before the timeout.
|
|
141
|
+
const LIVENESS_PING_QUERY = "__pi_lens_liveness_ping__";
|
|
142
|
+
// #1104: bound on `state.workspacePullResultCache` — one entry per distinct
|
|
143
|
+
// file the server has ever returned a `resultId` for across this client's
|
|
144
|
+
// lifetime. A full clear on overflow (rather than an LRU) is fine, same
|
|
145
|
+
// reasoning as `DISK_BINDING_MEMO_MAX` in diagnostic-binding.ts: each entry is
|
|
146
|
+
// cheaply rebuilt by the next full pull, the worst case is just one extra full
|
|
147
|
+
// (non-`unchanged`) report per affected file.
|
|
148
|
+
const WORKSPACE_PULL_RESULT_CACHE_MAX = 4096;
|
|
126
149
|
// Anti-deadlock backstop for workspace/executeCommand. Deliberately generous
|
|
127
150
|
// (30s): the command is mutating and legitimately long-running (a real server
|
|
128
151
|
// refactor / organize-imports), so this must not truncate valid work — it only
|
|
129
152
|
// stops a hung server from blocking the caller forever. On timeout the command
|
|
130
153
|
// may still be applying server-side; we surface that rather than pretend it ran.
|
|
131
154
|
const EXECUTE_COMMAND_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_EXECUTE_COMMAND_TIMEOUT_MS", 30_000);
|
|
155
|
+
// #1412 H1: short ceiling for the read-only tsserver project-identity probe.
|
|
156
|
+
// This is a telemetry sample, not a mutation — it must never hold the door
|
|
157
|
+
// open for anything close to EXECUTE_COMMAND_TIMEOUT_MS.
|
|
158
|
+
const PROBE_COMMAND_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PROJECT_IDENTITY_PROBE_TIMEOUT_MS", 2_500);
|
|
132
159
|
const LSP_CRASH_CODES = new Set([
|
|
133
160
|
"ERR_STREAM_DESTROYED",
|
|
134
161
|
"ERR_STREAM_WRITE_AFTER_END",
|
|
@@ -282,12 +309,34 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
282
309
|
}
|
|
283
310
|
};
|
|
284
311
|
try {
|
|
312
|
+
// #1114: gate the escalation on OBSERVED exit, not `proc.killed`. Node
|
|
313
|
+
// only sets `proc.killed = true` when `proc.kill()` (the ChildProcess
|
|
314
|
+
// method) successfully SENDS a signal — never when the process actually
|
|
315
|
+
// dies, and the primary SIGTERM path above goes through the raw
|
|
316
|
+
// `process.kill(-pid, …)` process-group call, which never touches
|
|
317
|
+
// `proc.killed` at all. Checking `!proc.killed` here was therefore
|
|
318
|
+
// either always-true (unconditional SIGKILL after the window,
|
|
319
|
+
// regardless of whether the group already died — group-kill path) or
|
|
320
|
+
// always-false/dead (direct-child fallback path, same shape as the
|
|
321
|
+
// safe-spawn escalation bug). An `exit` listener set once, up front,
|
|
322
|
+
// gives a real observed-death signal for both. Seeded from the same
|
|
323
|
+
// `exitCode`/`signalCode` pre-check the top-of-function early return
|
|
324
|
+
// uses (:689) — that early return is skipped when
|
|
325
|
+
// `options.processExiting` is set, so a process that was ALREADY dead
|
|
326
|
+
// on entry can still reach here; without seeding, `exited` would stay
|
|
327
|
+
// false (the "exit" event already fired before this listener was
|
|
328
|
+
// attached) and the fast/non-fast branches below would still fire a
|
|
329
|
+
// redundant group SIGKILL at the escalation window.
|
|
330
|
+
let exited = proc.exitCode != null || proc.signalCode != null;
|
|
331
|
+
proc.once?.("exit", () => {
|
|
332
|
+
exited = true;
|
|
333
|
+
});
|
|
285
334
|
if (!killPosixProcessGroup("SIGTERM")) {
|
|
286
335
|
killDirectChild("SIGTERM");
|
|
287
336
|
}
|
|
288
337
|
if (options.fast) {
|
|
289
338
|
const timer = setTimeout(() => {
|
|
290
|
-
if (!
|
|
339
|
+
if (!exited) {
|
|
291
340
|
logLatency({
|
|
292
341
|
type: "phase",
|
|
293
342
|
phase: "lsp_kill_escalation",
|
|
@@ -324,7 +373,7 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
324
373
|
}, 1500);
|
|
325
374
|
proc.once?.("exit", onExit);
|
|
326
375
|
});
|
|
327
|
-
if (!exitedInTime && !
|
|
376
|
+
if (!exitedInTime && !exited) {
|
|
328
377
|
logLatency({
|
|
329
378
|
type: "phase",
|
|
330
379
|
phase: "lsp_kill_escalation",
|
|
@@ -390,16 +439,73 @@ function getMergedDiagnosticsForPath(state, normalizedPath) {
|
|
|
390
439
|
return mergeDiagnosticLists(state.pushDiagnostics?.get(normalizedPath) ??
|
|
391
440
|
legacy.diagnostics?.get(normalizedPath), state.documentPullDiagnostics?.get(normalizedPath));
|
|
392
441
|
}
|
|
393
|
-
|
|
442
|
+
/** Exported for tests: the quiet-window timer cancel on clear/resync is the
|
|
443
|
+
* headline #1412 safety property (a stale versionless publication must never
|
|
444
|
+
* land after the document content changed). */
|
|
445
|
+
export function clearDiagnosticsForPath(state, normalizedPath) {
|
|
394
446
|
const legacy = state;
|
|
395
447
|
state.pushDiagnostics?.delete(normalizedPath);
|
|
448
|
+
const pending = state.pendingDiagnostics?.get(normalizedPath);
|
|
449
|
+
if (pending)
|
|
450
|
+
clearTimeout(pending);
|
|
451
|
+
state.pendingDiagnostics?.delete(normalizedPath);
|
|
396
452
|
state.pushDiagnosticTimestamps?.delete(normalizedPath);
|
|
397
453
|
state.documentPullDiagnostics?.delete(normalizedPath);
|
|
398
454
|
state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
|
|
399
455
|
state.diagnosticDocVersions?.delete(normalizedPath);
|
|
456
|
+
// #1095: a cleared path must never serve a stale content binding alongside a
|
|
457
|
+
// later publish — drop it with the diagnostics it described. (The last-sent
|
|
458
|
+
// `documentContentHashes` record is intentionally retained: it describes what
|
|
459
|
+
// we sent, which the NEXT publish for that version still needs to bind to.)
|
|
460
|
+
state.diagnosticBindings?.delete(normalizedPath);
|
|
461
|
+
// #1104: a resync invalidates any `unchanged`-report basis too — the next
|
|
462
|
+
// pull must not inherit a resultId/contentHash computed against the
|
|
463
|
+
// content this resync just replaced.
|
|
464
|
+
state.pullResultIds?.delete(normalizedPath);
|
|
465
|
+
state.workspacePullResultCache?.delete(normalizedPath);
|
|
400
466
|
legacy.diagnostics?.delete(normalizedPath);
|
|
401
467
|
legacy.diagnosticTimestamps?.delete(normalizedPath);
|
|
402
468
|
}
|
|
469
|
+
function logTypeScriptPullSettle(state, normalizedPath) {
|
|
470
|
+
if (state.serverId !== "typescript")
|
|
471
|
+
return;
|
|
472
|
+
const diagnostics = state.documentPullDiagnostics.get(normalizedPath) ?? [];
|
|
473
|
+
const elapsedSinceDidOpenMs = Math.max(0, Date.now() - (state.documentOpenedAt.get(normalizedPath) ?? Date.now()));
|
|
474
|
+
const diagnosticCodes = [...new Set(diagnostics
|
|
475
|
+
.map((diagnostic) => diagnostic.code)
|
|
476
|
+
.filter((code) => code !== undefined)
|
|
477
|
+
.map(String))].slice(0, 8);
|
|
478
|
+
logLatency({
|
|
479
|
+
type: "phase",
|
|
480
|
+
phase: "lsp_typescript_diagnostic_sequence",
|
|
481
|
+
filePath: normalizedPath,
|
|
482
|
+
durationMs: elapsedSinceDidOpenMs,
|
|
483
|
+
metadata: {
|
|
484
|
+
launchVariant: state.launchVariant ?? "unknown",
|
|
485
|
+
publicationIndex: state.diagnosticPublicationCounts.get(normalizedPath) ?? 0,
|
|
486
|
+
version: null,
|
|
487
|
+
diagnosticCount: diagnostics.length,
|
|
488
|
+
diagnosticCodes,
|
|
489
|
+
elapsedSinceDidOpenMs,
|
|
490
|
+
settledReturn: true,
|
|
491
|
+
settleSource: "pull",
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* #1095: fingerprint the EXACT didOpen/didChange payload text at SEND time and
|
|
497
|
+
* tag it with the document version it was sent as, so a later
|
|
498
|
+
* `publishDiagnostics` echoing that version can bind its diagnostics to the
|
|
499
|
+
* content they were computed against. Runs on in-memory content — never a disk
|
|
500
|
+
* read on the notification path (I1). Bounded by the same file-size gates the
|
|
501
|
+
* caller already applies to the content it hands us.
|
|
502
|
+
*/
|
|
503
|
+
function recordSentContent(state, normalizedPath, version, content) {
|
|
504
|
+
state.documentContentHashes.set(normalizedPath, {
|
|
505
|
+
version,
|
|
506
|
+
hash: hashDiagnosticContent(content),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
403
509
|
// Methods that can be registered dynamically and map to operationSupport keys
|
|
404
510
|
const DYNAMIC_OPERATION_METHOD_MAP = {
|
|
405
511
|
"textDocument/definition": "definition",
|
|
@@ -475,12 +581,60 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
475
581
|
state.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
476
582
|
const filePath = uriToPath(params.uri);
|
|
477
583
|
const normalizedPath = normalizeMapKey(filePath);
|
|
584
|
+
// A server can flush a queued publish after didClose during teardown.
|
|
585
|
+
// Do not resurrect diagnostics or their content binding for a document
|
|
586
|
+
// that is no longer open on this client.
|
|
587
|
+
if (state.closedDocuments?.has(normalizedPath))
|
|
588
|
+
return;
|
|
478
589
|
const newDiags = normalizeLspDiagnostics(params.diagnostics || []);
|
|
479
590
|
const docVersion = params.version;
|
|
480
591
|
if (PUB_DEBUG) {
|
|
481
|
-
|
|
592
|
+
// #1333: PUB_DEBUG gate preserved; sink is extension.log.
|
|
593
|
+
logExtension({
|
|
594
|
+
subsystem: "lsp-pub",
|
|
595
|
+
level: "debug",
|
|
596
|
+
message: `server=${state.serverId} pubVersion=${docVersion} docVersion=${state.documentVersions?.get(normalizedPath)} diags=${newDiags.length}`,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
600
|
+
// Publication counting and code extraction exist only for the
|
|
601
|
+
// TypeScript diagnostic-sequence telemetry; skip the bookkeeping for
|
|
602
|
+
// every other push server on this hot receive path.
|
|
603
|
+
const isTypeScriptTelemetry = state.serverId === "typescript";
|
|
604
|
+
const publicationIndex = isTypeScriptTelemetry
|
|
605
|
+
? (state.diagnosticPublicationCounts.get(normalizedPath) ?? 0) + 1
|
|
606
|
+
: 0;
|
|
607
|
+
if (isTypeScriptTelemetry) {
|
|
608
|
+
state.diagnosticPublicationCounts.set(normalizedPath, publicationIndex);
|
|
482
609
|
}
|
|
483
|
-
const
|
|
610
|
+
const diagnosticCodes = isTypeScriptTelemetry
|
|
611
|
+
? [...new Set(newDiags
|
|
612
|
+
.map((diagnostic) => diagnostic.code)
|
|
613
|
+
.filter((code) => code !== undefined)
|
|
614
|
+
.map(String))].slice(0, 8)
|
|
615
|
+
: [];
|
|
616
|
+
const logSequence = (settledReturn, settleSource) => {
|
|
617
|
+
if (state.serverId !== "typescript")
|
|
618
|
+
return;
|
|
619
|
+
const elapsedSinceDidOpenMs = Math.max(0, Date.now() -
|
|
620
|
+
(state.documentOpenedAt.get(normalizedPath) ?? Date.now()));
|
|
621
|
+
logLatency({
|
|
622
|
+
type: "phase",
|
|
623
|
+
phase: "lsp_typescript_diagnostic_sequence",
|
|
624
|
+
filePath: normalizedPath,
|
|
625
|
+
durationMs: elapsedSinceDidOpenMs,
|
|
626
|
+
metadata: {
|
|
627
|
+
launchVariant: state.launchVariant ?? "unknown",
|
|
628
|
+
publicationIndex,
|
|
629
|
+
version: docVersion ?? null,
|
|
630
|
+
diagnosticCount: newDiags.length,
|
|
631
|
+
diagnosticCodes,
|
|
632
|
+
elapsedSinceDidOpenMs,
|
|
633
|
+
settledReturn,
|
|
634
|
+
...(settleSource && { settleSource }),
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
};
|
|
484
638
|
// Record the document version these diagnostics were computed against
|
|
485
639
|
// (when the server reports it) so waitForDiagnostics can reject results
|
|
486
640
|
// that lag behind the latest didChange instead of serving them as fresh.
|
|
@@ -488,6 +642,26 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
488
642
|
if (docVersion !== undefined) {
|
|
489
643
|
state.diagnosticDocVersions.set(normalizedPath, docVersion);
|
|
490
644
|
}
|
|
645
|
+
recordBinding();
|
|
646
|
+
};
|
|
647
|
+
// #1095: bind the just-stored diagnostics to the content they were
|
|
648
|
+
// computed against. Only when the server reported a version AND we still
|
|
649
|
+
// hold the sent-content fingerprint for exactly that version — otherwise
|
|
650
|
+
// no contentHash is recorded, so the binding reads "unknown" and a
|
|
651
|
+
// version-less server behaves exactly as before. Runs at the same
|
|
652
|
+
// write-time moment as `pushDiagnostics.set` (superseded pushes are
|
|
653
|
+
// dropped before this via `isSupersededPush`, so a binding never lags the
|
|
654
|
+
// latest sent version).
|
|
655
|
+
const recordBinding = () => {
|
|
656
|
+
if (docVersion === undefined) {
|
|
657
|
+
state.diagnosticBindings.delete(normalizedPath);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const sent = state.documentContentHashes.get(normalizedPath);
|
|
661
|
+
state.diagnosticBindings.set(normalizedPath, {
|
|
662
|
+
version: docVersion,
|
|
663
|
+
contentHash: sent && sent.version === docVersion ? sent.hash : undefined,
|
|
664
|
+
});
|
|
491
665
|
};
|
|
492
666
|
// Late/superseded-push guard: if the server stamped this push with a
|
|
493
667
|
// version and that version already lags the latest didChange we sent,
|
|
@@ -526,8 +700,10 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
526
700
|
recordDocVersion();
|
|
527
701
|
state.diagnosticsVersion += 1;
|
|
528
702
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
703
|
+
logSequence(true, "first-push");
|
|
529
704
|
return;
|
|
530
705
|
}
|
|
706
|
+
logSequence(false);
|
|
531
707
|
const existingTimer = state.pendingDiagnostics.get(normalizedPath);
|
|
532
708
|
if (existingTimer)
|
|
533
709
|
clearTimeout(existingTimer);
|
|
@@ -540,6 +716,7 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
540
716
|
recordDocVersion();
|
|
541
717
|
state.diagnosticsVersion += 1;
|
|
542
718
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
719
|
+
logSequence(true, "quiet-window");
|
|
543
720
|
}, strategy.debounceMs);
|
|
544
721
|
state.pendingDiagnostics.set(normalizedPath, timer);
|
|
545
722
|
});
|
|
@@ -580,8 +757,21 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
580
757
|
if (state.serverEditsAllowed <= 0 || !params?.edit) {
|
|
581
758
|
return { applied: false, failureReason: "edit not solicited" };
|
|
582
759
|
}
|
|
760
|
+
const context = (state.activeMutationDepth ?? 0) === 1
|
|
761
|
+
? state.activeMutationContext
|
|
762
|
+
: undefined;
|
|
763
|
+
const telemetryContext = context ?? {
|
|
764
|
+
cwd: state.root,
|
|
765
|
+
correlationId: newLspMutationCorrelationId(),
|
|
766
|
+
tool: "lsp-workspace-applyEdit",
|
|
767
|
+
source: "lsp-edit",
|
|
768
|
+
};
|
|
583
769
|
try {
|
|
584
|
-
await applyWorkspaceEdit(params.edit, state.root
|
|
770
|
+
await applyWorkspaceEdit(params.edit, state.root, {
|
|
771
|
+
positionEncoding: state.positionEncoding,
|
|
772
|
+
documentVersions: state.documentVersions,
|
|
773
|
+
mutationContext: telemetryContext,
|
|
774
|
+
});
|
|
585
775
|
return { applied: true };
|
|
586
776
|
}
|
|
587
777
|
catch (err) {
|
|
@@ -603,16 +793,30 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
603
793
|
});
|
|
604
794
|
state.connection.onRequest("window/workDoneProgress/create", async () => { });
|
|
605
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* #1127: record the FIRST moment this client observed its own death. Detection
|
|
798
|
+
* of a dead client (the next `getClientForFile` attach in index.ts) can happen
|
|
799
|
+
* long after the process actually died — this timestamp is the only reliable
|
|
800
|
+
* "when did it die" signal, so it must be set here, at the earliest death
|
|
801
|
+
* signal, not derived later from detection time.
|
|
802
|
+
*/
|
|
803
|
+
function markExitedIfUnset(state) {
|
|
804
|
+
if (state.exitedAt === undefined) {
|
|
805
|
+
state.exitedAt = Date.now();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
606
808
|
function setupConnectionLifecycle(state, recentStderr) {
|
|
607
809
|
state.connection.onError(([error]) => {
|
|
608
810
|
state.lastError = error instanceof Error ? error : new Error(String(error));
|
|
609
811
|
state.isConnected = false;
|
|
610
812
|
state.isDestroyed = true;
|
|
813
|
+
markExitedIfUnset(state);
|
|
611
814
|
disposeClientConnection(state);
|
|
612
815
|
});
|
|
613
816
|
state.connection.onClose(() => {
|
|
614
817
|
state.isConnected = false;
|
|
615
818
|
state.isDestroyed = true;
|
|
819
|
+
markExitedIfUnset(state);
|
|
616
820
|
disposeClientConnection(state);
|
|
617
821
|
});
|
|
618
822
|
state.lspProcess.process.on("exit", (code, signal) => {
|
|
@@ -627,6 +831,7 @@ function setupConnectionLifecycle(state, recentStderr) {
|
|
|
627
831
|
const wasIntentional = state.shutdownRequested;
|
|
628
832
|
state.isConnected = false;
|
|
629
833
|
state.isDestroyed = true;
|
|
834
|
+
markExitedIfUnset(state);
|
|
630
835
|
disposeClientConnection(state);
|
|
631
836
|
if (!wasIntentional) {
|
|
632
837
|
logLatency({
|
|
@@ -649,6 +854,12 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
649
854
|
if (!isClientAlive(state))
|
|
650
855
|
return { status: "unavailable" };
|
|
651
856
|
const uri = pathToFileURL(filePath).href;
|
|
857
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
858
|
+
// #1104: echo the last resultId we hold for this document so a server that
|
|
859
|
+
// hasn't changed its view can answer `kind: "unchanged"` instead of
|
|
860
|
+
// recomputing — see the `kind === "unchanged"` branch below for how that's
|
|
861
|
+
// honored (inherit, never treat an omitted `items` as clean).
|
|
862
|
+
const previousResultId = state.pullResultIds.get(normalizedPath);
|
|
652
863
|
try {
|
|
653
864
|
// withTimeout is the backstop against a hung pull-mode server: without it
|
|
654
865
|
// this await never settles unless the stream is destroyed. Bounded by the
|
|
@@ -657,23 +868,69 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
657
868
|
// `clean`), so it falls through to the push-wait/timeout backstop.
|
|
658
869
|
const report = await withTimeout(safeSendRequest(state.connection, "textDocument/diagnostic", {
|
|
659
870
|
textDocument: { uri },
|
|
871
|
+
...(previousResultId !== undefined && { previousResultId }),
|
|
660
872
|
}), Math.max(1, Math.min(PULL_REQUEST_TIMEOUT_MS, budgetMs)));
|
|
661
|
-
if (!report)
|
|
873
|
+
if (!report) {
|
|
874
|
+
recordPullFailure(state, "textDocument/diagnostic", new Error("empty response"));
|
|
662
875
|
return { status: "unavailable" };
|
|
663
|
-
|
|
664
|
-
const primaryItems = normalizeLspDiagnostics(report.items ?? []);
|
|
876
|
+
}
|
|
665
877
|
const now = Date.now();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
878
|
+
// #1104: the fingerprint of the content we last sent for this document —
|
|
879
|
+
// the SAME `documentContentHashes` entry the push binding path uses
|
|
880
|
+
// (`recordSentContent` runs unconditionally on every didOpen/didChange,
|
|
881
|
+
// regardless of push/pull mode), so this costs no extra read. A pull
|
|
882
|
+
// response describes whatever the server had when it answered, which for
|
|
883
|
+
// a pi-lens-opened document is exactly that last-sent payload.
|
|
884
|
+
const sentHash = state.documentContentHashes.get(normalizedPath)?.hash;
|
|
885
|
+
let totalCount;
|
|
886
|
+
if (report.kind === "unchanged") {
|
|
887
|
+
// #1104: same resultId basis as last time — an omitted `items` here
|
|
888
|
+
// means "no change", NOT "clean". Overwriting with `[]` would be the
|
|
889
|
+
// exact false-clean shape #570/#571 already fixed for the touch path;
|
|
890
|
+
// keep the previously stored diagnostics and binding as-is.
|
|
891
|
+
totalCount = state.documentPullDiagnostics.get(normalizedPath)?.length ?? 0;
|
|
892
|
+
// Still a fresh confirmation as of `now` even though the content is
|
|
893
|
+
// unchanged — bump the timestamp so `getAllDiagnostics()` doesn't read
|
|
894
|
+
// this entry as aging purely because the server had nothing new to say.
|
|
895
|
+
state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
const primaryItems = normalizeLspDiagnostics(report.items ?? []);
|
|
899
|
+
state.documentPullDiagnostics.set(normalizedPath, primaryItems);
|
|
900
|
+
state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
|
|
901
|
+
state.diagnosticsVersion += 1;
|
|
902
|
+
state.diagnosticBindings.set(normalizedPath, { contentHash: sentHash });
|
|
903
|
+
totalCount = primaryItems.length;
|
|
904
|
+
}
|
|
905
|
+
if (report.resultId !== undefined) {
|
|
906
|
+
state.pullResultIds.set(normalizedPath, report.resultId);
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
state.pullResultIds.delete(normalizedPath);
|
|
910
|
+
}
|
|
670
911
|
if (report.relatedDocuments) {
|
|
671
912
|
for (const [relatedUri, related] of Object.entries(report.relatedDocuments)) {
|
|
672
913
|
const relatedPath = uriToPath(relatedUri);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
914
|
+
const relatedNormalized = normalizeMapKey(relatedPath);
|
|
915
|
+
if (related?.kind === "unchanged") {
|
|
916
|
+
totalCount +=
|
|
917
|
+
state.documentPullDiagnostics.get(relatedNormalized)?.length ?? 0;
|
|
918
|
+
state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
const relatedItems = normalizeLspDiagnostics(related?.items ?? []);
|
|
922
|
+
state.documentPullDiagnostics.set(relatedNormalized, relatedItems);
|
|
923
|
+
state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
|
|
924
|
+
// #1104: a related document's diagnostics were NOT computed against
|
|
925
|
+
// content we independently sent/fingerprinted (we never requested
|
|
926
|
+
// it directly) — its binding stays honestly "unknown" rather than
|
|
927
|
+
// borrowing the primary document's hash.
|
|
928
|
+
state.diagnosticBindings.delete(relatedNormalized);
|
|
929
|
+
totalCount += relatedItems.length;
|
|
930
|
+
}
|
|
931
|
+
if (related?.resultId !== undefined) {
|
|
932
|
+
state.pullResultIds.set(relatedNormalized, related.resultId);
|
|
933
|
+
}
|
|
677
934
|
}
|
|
678
935
|
}
|
|
679
936
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
@@ -681,10 +938,34 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
681
938
|
? { status: "found", count: totalCount }
|
|
682
939
|
: { status: "clean" };
|
|
683
940
|
}
|
|
684
|
-
catch {
|
|
941
|
+
catch (err) {
|
|
942
|
+
recordPullFailure(state, "textDocument/diagnostic", err);
|
|
685
943
|
return { status: "unavailable" };
|
|
686
944
|
}
|
|
687
945
|
}
|
|
946
|
+
const PULL_FAILURE_HISTORY_LIMIT = 10;
|
|
947
|
+
function recordPullFailure(state, method, error) {
|
|
948
|
+
const candidate = error;
|
|
949
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
950
|
+
const unsupportedMessage = /^(?:method not found|unknown method|unsupported method)(?::|$)/i;
|
|
951
|
+
if (candidate.code === -32601 ||
|
|
952
|
+
candidate.code === "-32601" ||
|
|
953
|
+
unsupportedMessage.test(message.trim()))
|
|
954
|
+
return;
|
|
955
|
+
state.pullFailureHistory.push({
|
|
956
|
+
timestamp: Date.now(),
|
|
957
|
+
method,
|
|
958
|
+
...(typeof candidate.code === "number" || typeof candidate.code === "string"
|
|
959
|
+
? { code: candidate.code }
|
|
960
|
+
: {}),
|
|
961
|
+
message: typeof candidate.message === "string"
|
|
962
|
+
? candidate.message
|
|
963
|
+
: String(error),
|
|
964
|
+
});
|
|
965
|
+
if (state.pullFailureHistory.length > PULL_FAILURE_HISTORY_LIMIT) {
|
|
966
|
+
state.pullFailureHistory.splice(0, state.pullFailureHistory.length - PULL_FAILURE_HISTORY_LIMIT);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
688
969
|
/**
|
|
689
970
|
* One project-wide `workspace/diagnostic` pull — a single request that returns
|
|
690
971
|
* diagnostics for every document the server knows, instead of opening N files.
|
|
@@ -698,23 +979,73 @@ export async function clientRequestWorkspaceDiagnostics(state, budgetMs) {
|
|
|
698
979
|
if (!state.workspaceDiagnosticsSupport.workspaceDiagnostics)
|
|
699
980
|
return undefined;
|
|
700
981
|
try {
|
|
701
|
-
|
|
982
|
+
// #1104: echo every resultId we hold from a PRIOR pull so the server can
|
|
983
|
+
// answer `kind: "unchanged"` for files it hasn't recomputed, instead of
|
|
984
|
+
// resending (and us re-hashing) every file on every sweep.
|
|
985
|
+
const previousResultIds = Array.from(state.workspacePullResultCache.values()).map((entry) => ({ uri: entry.uri, value: entry.resultId }));
|
|
986
|
+
const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds }), Math.max(1, budgetMs));
|
|
702
987
|
if (!report || !Array.isArray(report.items))
|
|
703
988
|
return undefined;
|
|
704
989
|
const out = [];
|
|
705
990
|
for (const item of report.items) {
|
|
706
|
-
|
|
707
|
-
// (none, since previousResultIds is empty on this one-shot request).
|
|
708
|
-
if (!item?.uri || item.kind !== "full")
|
|
991
|
+
if (!item?.uri)
|
|
709
992
|
continue;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
993
|
+
const filePath = uriToPath(item.uri);
|
|
994
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
995
|
+
if (item.kind === "unchanged") {
|
|
996
|
+
// #1104: inherit the prior pull's diagnostics + content binding for
|
|
997
|
+
// the SAME resultId basis — an "unchanged" report never carries
|
|
998
|
+
// `items`, so without this a file the server confirmed unchanged
|
|
999
|
+
// would silently drop out of the sweep result entirely.
|
|
1000
|
+
const prior = state.workspacePullResultCache.get(normalizedPath);
|
|
1001
|
+
if (!prior)
|
|
1002
|
+
continue; // no earlier basis to inherit — nothing to report
|
|
1003
|
+
if (item.resultId !== undefined) {
|
|
1004
|
+
state.workspacePullResultCache.set(normalizedPath, {
|
|
1005
|
+
...prior,
|
|
1006
|
+
resultId: item.resultId,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
out.push({
|
|
1010
|
+
filePath,
|
|
1011
|
+
diagnostics: prior.diagnostics,
|
|
1012
|
+
contentHash: prior.contentHash,
|
|
1013
|
+
});
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
// "full" (or a non-conforming server omitting `kind`, per the LSP
|
|
1017
|
+
// default) — recompute and re-fingerprint.
|
|
1018
|
+
const diagnostics = normalizeLspDiagnostics(item.items ?? []);
|
|
1019
|
+
// #1104: fingerprint the file bytes active AT REQUEST TIME. Best-effort —
|
|
1020
|
+
// a read failure (deleted/unreadable mid-sweep) just leaves contentHash
|
|
1021
|
+
// undefined, so the binding reads honestly "unknown", never fabricated.
|
|
1022
|
+
let contentHash;
|
|
1023
|
+
try {
|
|
1024
|
+
contentHash = hashDiagnosticContent(await readFile(filePath, "utf-8"));
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
contentHash = undefined;
|
|
1028
|
+
}
|
|
1029
|
+
if (item.resultId !== undefined) {
|
|
1030
|
+
if (state.workspacePullResultCache.size >= WORKSPACE_PULL_RESULT_CACHE_MAX) {
|
|
1031
|
+
state.workspacePullResultCache.clear();
|
|
1032
|
+
}
|
|
1033
|
+
state.workspacePullResultCache.set(normalizedPath, {
|
|
1034
|
+
uri: item.uri,
|
|
1035
|
+
resultId: item.resultId,
|
|
1036
|
+
diagnostics,
|
|
1037
|
+
contentHash,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
state.workspacePullResultCache.delete(normalizedPath);
|
|
1042
|
+
}
|
|
1043
|
+
out.push({ filePath, diagnostics, contentHash });
|
|
714
1044
|
}
|
|
715
1045
|
return out;
|
|
716
1046
|
}
|
|
717
|
-
catch {
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
recordPullFailure(state, "workspace/diagnostic", err);
|
|
718
1049
|
return undefined;
|
|
719
1050
|
}
|
|
720
1051
|
}
|
|
@@ -746,10 +1077,12 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
746
1077
|
// version baseline (`minVersion === undefined`), so a failed pull returned
|
|
747
1078
|
// 0 and was read as a fresh clean.
|
|
748
1079
|
let outcome = await clientRequestPullDiagnostics(state, filePath, timeoutMs);
|
|
749
|
-
if (outcome.status === "found")
|
|
1080
|
+
if (outcome.status === "found") {
|
|
1081
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
750
1082
|
return;
|
|
1083
|
+
}
|
|
751
1084
|
let sawClean = outcome.status === "clean";
|
|
752
|
-
const strategy = getStrategy(state.serverId);
|
|
1085
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
753
1086
|
const retryBudgetMs = strategy.pullRetryBudgetMs > 0
|
|
754
1087
|
? Math.min(timeoutMs, strategy.pullRetryBudgetMs)
|
|
755
1088
|
: 0;
|
|
@@ -764,10 +1097,16 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
764
1097
|
if (outcome.status === "clean")
|
|
765
1098
|
sawClean = true;
|
|
766
1099
|
}
|
|
767
|
-
if (options.pullOnly)
|
|
1100
|
+
if (options.pullOnly) {
|
|
1101
|
+
if (outcome.status === "found" || sawClean) {
|
|
1102
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
1103
|
+
}
|
|
768
1104
|
return;
|
|
769
|
-
|
|
1105
|
+
}
|
|
1106
|
+
if (outcome.status === "found" || sawClean) {
|
|
1107
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
770
1108
|
return;
|
|
1109
|
+
}
|
|
771
1110
|
}
|
|
772
1111
|
if (hasFreshDiagnostics() &&
|
|
773
1112
|
!isVersionStale() &&
|
|
@@ -785,7 +1124,7 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
785
1124
|
clearTimeout(debounceTimer);
|
|
786
1125
|
// Adaptive debounce: use time since last push to compute remaining
|
|
787
1126
|
// wait instead of always waiting the full debounce window.
|
|
788
|
-
const strategy = getStrategy(state.serverId);
|
|
1127
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
789
1128
|
const hit = state.pushDiagnosticTimestamps.get(normalizedPath);
|
|
790
1129
|
const timeSincePush = hit ? Date.now() - hit : Infinity;
|
|
791
1130
|
const remaining = Math.max(0, strategy.debounceMs - timeSincePush);
|
|
@@ -807,8 +1146,8 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
807
1146
|
export async function handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics = false, silent = false) {
|
|
808
1147
|
if (!isClientAlive(state))
|
|
809
1148
|
return;
|
|
810
|
-
const uri = pathToFileURL(filePath).href;
|
|
811
1149
|
const normalizedPath = normalizeMapKey(filePath);
|
|
1150
|
+
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
812
1151
|
if (state.openDocuments.has(normalizedPath) ||
|
|
813
1152
|
state.pendingOpens.has(normalizedPath)) {
|
|
814
1153
|
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
@@ -816,34 +1155,57 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
816
1155
|
// preserveDiagnostics: skip cache clear for format-only resyncs so
|
|
817
1156
|
// waitForDiagnostics fast-paths instead of waiting up to 5s for TypeScript
|
|
818
1157
|
// to re-publish what it already knows (formatting doesn't change semantics).
|
|
1158
|
+
// #1095 note: this also retains the prior content `binding`. Until the
|
|
1159
|
+
// server republishes for the new version, a binding read compares the OLD
|
|
1160
|
+
// content hash against the NEW disk bytes → boundToCurrentDisk `false` →
|
|
1161
|
+
// the consumer demotes to inconclusive. That's the SAFE (#533) direction (a
|
|
1162
|
+
// transient "unconfirmed", never a false-clean), self-healing on the next
|
|
1163
|
+
// publish — not a correctness hazard, unlike the reopen false-TRUE above.
|
|
819
1164
|
if (!preserveDiagnostics) {
|
|
820
1165
|
clearDiagnosticsForPath(state, normalizedPath);
|
|
821
1166
|
}
|
|
822
1167
|
// Scanners that only re-scan on a fresh open (opengrep ignores didChange):
|
|
823
1168
|
// close + reopen so the re-edit actually triggers a re-scan instead of
|
|
824
1169
|
// silently publishing nothing.
|
|
825
|
-
if (getStrategy(state.serverId).reopenOnResync) {
|
|
1170
|
+
if (getStrategy(state.serverId, state.launchVariant).reopenOnResync) {
|
|
826
1171
|
await safeSendNotification(state.connection, "textDocument/didClose", {
|
|
827
1172
|
textDocument: { uri },
|
|
828
1173
|
});
|
|
829
1174
|
state.openDocuments.delete(normalizedPath);
|
|
830
|
-
state.
|
|
1175
|
+
state.openDocumentUris?.delete(normalizedPath);
|
|
1176
|
+
// #1095 (P2-3): carry the version counter FORWARD across the
|
|
1177
|
+
// close+reopen instead of resetting to 0. LSP lets a didOpen use any
|
|
1178
|
+
// version, and reusing 0 for successive resyncs made the version
|
|
1179
|
+
// ambiguous — a late publish for an earlier resync's content echoed the
|
|
1180
|
+
// SAME 0 as the current send, so the superseded-push guard (0 < 0 is
|
|
1181
|
+
// false) accepted it and `recordBinding` bound STALE diagnostics to the
|
|
1182
|
+
// CURRENT content's fingerprint → an affirmative boundToCurrentDisk TRUE
|
|
1183
|
+
// for a stale view (worse than "unknown"). Monotonic versions make that
|
|
1184
|
+
// late echo strictly older → dropped by isSupersededPush → never bound.
|
|
1185
|
+
state.documentVersions.set(normalizedPath, version);
|
|
1186
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1187
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
831
1188
|
if (!isClientAlive(state))
|
|
832
1189
|
return;
|
|
833
1190
|
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
834
|
-
textDocument: { uri, languageId, version
|
|
1191
|
+
textDocument: { uri, languageId, version, text: content },
|
|
835
1192
|
});
|
|
1193
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
836
1194
|
state.openDocuments.add(normalizedPath);
|
|
1195
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
837
1196
|
return;
|
|
838
1197
|
}
|
|
839
1198
|
await safeSendNotification(state.connection, "textDocument/didChange", {
|
|
840
1199
|
textDocument: { uri, version },
|
|
841
1200
|
contentChanges: [{ text: content }],
|
|
842
1201
|
});
|
|
1202
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
843
1203
|
return;
|
|
844
1204
|
}
|
|
845
1205
|
state.pendingOpens.add(normalizedPath);
|
|
846
1206
|
state.documentVersions.set(normalizedPath, 0);
|
|
1207
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1208
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
847
1209
|
clearDiagnosticsForPath(state, normalizedPath); // always clear for initial open
|
|
848
1210
|
// Send workspace notification first (like opencode does).
|
|
849
1211
|
// Skipped in silent mode — cascade reads a file for diagnostics,
|
|
@@ -873,14 +1235,35 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
873
1235
|
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
874
1236
|
textDocument: { uri, languageId, version: 0, text: content },
|
|
875
1237
|
});
|
|
1238
|
+
recordSentContent(state, normalizedPath, 0, content);
|
|
876
1239
|
state.pendingOpens.delete(normalizedPath);
|
|
877
1240
|
state.openDocuments.add(normalizedPath);
|
|
1241
|
+
state.closedDocuments?.delete(normalizedPath);
|
|
1242
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
1243
|
+
// Telemetry is deliberately detached after didOpen succeeds.
|
|
1244
|
+
// #1412 H1: routed through runReadOnlyServerCommand, NOT runServerCommand —
|
|
1245
|
+
// the probe must never open the serverEditsAllowed/activeMutationContext
|
|
1246
|
+
// mutation-acceptance window; it is a diagnostic sample, not a mutation, and
|
|
1247
|
+
// carries its own short PROBE_COMMAND_TIMEOUT_MS backstop. The probe itself
|
|
1248
|
+
// is classic-only and swallows every failure.
|
|
1249
|
+
void probeTsserverProjectIdentity({
|
|
1250
|
+
serverId: state.serverId,
|
|
1251
|
+
launchVariant: state.launchVariant,
|
|
1252
|
+
clientRoot: state.root,
|
|
1253
|
+
file: filePath,
|
|
1254
|
+
normalizedFile: normalizedPath,
|
|
1255
|
+
probedFiles: state.projectIdentityProbedFiles ??
|
|
1256
|
+
(state.projectIdentityProbedFiles = new Set()),
|
|
1257
|
+
commandChannel: {
|
|
1258
|
+
executeCommand: (command, args) => runReadOnlyServerCommand(state, command, args),
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
878
1261
|
}
|
|
879
1262
|
export async function handleNotifyChange(state, filePath, content) {
|
|
880
1263
|
if (!isClientAlive(state))
|
|
881
1264
|
return;
|
|
882
|
-
const uri = pathToFileURL(filePath).href;
|
|
883
1265
|
const normalizedPath = normalizeMapKey(filePath);
|
|
1266
|
+
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
884
1267
|
if (!state.openDocuments.has(normalizedPath)) {
|
|
885
1268
|
// Safety fallback: keep protocol ordering valid even if caller sends
|
|
886
1269
|
// didChange before first didOpen for this document.
|
|
@@ -888,7 +1271,11 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
888
1271
|
textDocument: { uri, languageId: "plaintext", version: 0, text: content },
|
|
889
1272
|
});
|
|
890
1273
|
state.documentVersions.set(normalizedPath, 0);
|
|
1274
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1275
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
1276
|
+
recordSentContent(state, normalizedPath, 0, content);
|
|
891
1277
|
state.openDocuments.add(normalizedPath);
|
|
1278
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
892
1279
|
return;
|
|
893
1280
|
}
|
|
894
1281
|
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
@@ -900,6 +1287,32 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
900
1287
|
textDocument: { uri, version },
|
|
901
1288
|
contentChanges: [{ text: content }],
|
|
902
1289
|
});
|
|
1290
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
1291
|
+
}
|
|
1292
|
+
/** Close a document through the same lifecycle path exposed by the client. */
|
|
1293
|
+
export async function closeDocument(state, filePath) {
|
|
1294
|
+
if (!isClientAlive(state))
|
|
1295
|
+
return;
|
|
1296
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
1297
|
+
if (!state.openDocuments.has(normalizedPath))
|
|
1298
|
+
return;
|
|
1299
|
+
await safeSendNotification(state.connection, "textDocument/didClose", {
|
|
1300
|
+
textDocument: {
|
|
1301
|
+
uri: state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href,
|
|
1302
|
+
},
|
|
1303
|
+
});
|
|
1304
|
+
state.openDocuments.delete(normalizedPath);
|
|
1305
|
+
state.closedDocuments?.add(normalizedPath);
|
|
1306
|
+
state.openDocumentUris?.delete(normalizedPath);
|
|
1307
|
+
state.documentVersions.delete(normalizedPath);
|
|
1308
|
+
state.documentOpenedAt.delete(normalizedPath);
|
|
1309
|
+
state.diagnosticPublicationCounts.delete(normalizedPath);
|
|
1310
|
+
// #1412 L1: projectIdentityProbedFiles is a claim-once memo scoped to the
|
|
1311
|
+
// document's open lifetime (re-probing a closed-then-reopened file is
|
|
1312
|
+
// harmless and cheap) — mirror openDocuments' own per-close cleanup so it
|
|
1313
|
+
// doesn't grow unbounded across a long session's worth of open/close churn.
|
|
1314
|
+
state.projectIdentityProbedFiles?.delete(normalizedPath);
|
|
1315
|
+
clearDiagnosticsForPath(state, normalizedPath);
|
|
903
1316
|
}
|
|
904
1317
|
export async function clientShutdown(state, options = {}) {
|
|
905
1318
|
const shutdownStart = Date.now();
|
|
@@ -912,6 +1325,10 @@ export async function clientShutdown(state, options = {}) {
|
|
|
912
1325
|
state.pendingDiagnostics.clear();
|
|
913
1326
|
state.pendingOpens.clear();
|
|
914
1327
|
state.openDocuments.clear();
|
|
1328
|
+
state.openDocumentUris?.clear();
|
|
1329
|
+
// #1412 L1: mirror openDocuments' clear — a shut-down/evicted client's
|
|
1330
|
+
// probe memo is moot along with everything else document-scoped.
|
|
1331
|
+
state.projectIdentityProbedFiles?.clear();
|
|
915
1332
|
// #271: drop any pending watched-files batch + its timer (a dying client's
|
|
916
1333
|
// queued FS changes are moot, and the timer must not outlive the connection).
|
|
917
1334
|
state.watchQueue?.cancel();
|
|
@@ -1030,13 +1447,44 @@ signal = getAmbientAbortSignal()) {
|
|
|
1030
1447
|
}
|
|
1031
1448
|
return result;
|
|
1032
1449
|
}
|
|
1450
|
+
// #1277: cheap liveness round-trip used by the silent-clean gates in
|
|
1451
|
+
// `index.ts`. `isAlive()`/`checkAlive()` only look at process/connection
|
|
1452
|
+
// state — a server that accepted the notify write and then wedged (still
|
|
1453
|
+
// running, connection still open, just never replying) reads as "alive" by
|
|
1454
|
+
// those checks even though it will never answer anything again. This sends a
|
|
1455
|
+
// real request (`workspace/symbol`, chosen because it needs no open document)
|
|
1456
|
+
// and reports whether the connection round-tripped it — success, a genuine
|
|
1457
|
+
// protocol-level error (e.g. MethodNotFound), and a stream-destroyed/
|
|
1458
|
+
// cancelled response (safeSendRequest swallows those to `undefined`, so the
|
|
1459
|
+
// final `isClientAlive` re-check is what catches "died mid-flight") ALL count
|
|
1460
|
+
// as "alive"; only a real timeout, or the connection having gone down by the
|
|
1461
|
+
// time this resolves, reports dead. The response content itself is never
|
|
1462
|
+
// inspected — only whether one arrived in time.
|
|
1463
|
+
async function clientPingLiveness(state, timeoutMs = LIVENESS_PING_TIMEOUT_MS) {
|
|
1464
|
+
if (!isClientAlive(state))
|
|
1465
|
+
return false;
|
|
1466
|
+
try {
|
|
1467
|
+
await withTimeout(safeSendRequest(state.connection, "workspace/symbol", {
|
|
1468
|
+
query: LIVENESS_PING_QUERY,
|
|
1469
|
+
}), timeoutMs);
|
|
1470
|
+
}
|
|
1471
|
+
catch (err) {
|
|
1472
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
// A real protocol-level error reply still proves the server round-
|
|
1476
|
+
// tripped the request — fall through to the alive re-check below
|
|
1477
|
+
// rather than treating an error response as "dead".
|
|
1478
|
+
}
|
|
1479
|
+
return isClientAlive(state);
|
|
1480
|
+
}
|
|
1033
1481
|
// Run an advertised server command via workspace/executeCommand, with the
|
|
1034
1482
|
// generous EXECUTE_COMMAND_TIMEOUT_MS anti-deadlock backstop. Preserves the
|
|
1035
1483
|
// hardening invariants: allowlist-by-advertisement (only commands the server
|
|
1036
1484
|
// declared) and the serverEditsAllowed window that gates server-driven
|
|
1037
1485
|
// applyEdit to the duration of an explicit call. Exported with an overridable
|
|
1038
1486
|
// `timeoutMs` for the #365 regression tests.
|
|
1039
|
-
export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS) {
|
|
1487
|
+
export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS, mutationContext) {
|
|
1040
1488
|
if (!isClientAlive(state)) {
|
|
1041
1489
|
return { executed: false, reason: "lsp client not alive" };
|
|
1042
1490
|
}
|
|
@@ -1047,6 +1495,11 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
|
|
|
1047
1495
|
};
|
|
1048
1496
|
}
|
|
1049
1497
|
state.serverEditsAllowed += 1;
|
|
1498
|
+
state.activeMutationDepth = (state.activeMutationDepth ?? 0) + 1;
|
|
1499
|
+
if (state.activeMutationDepth === 1)
|
|
1500
|
+
state.activeMutationContext = mutationContext;
|
|
1501
|
+
else
|
|
1502
|
+
state.activeMutationContext = undefined;
|
|
1050
1503
|
try {
|
|
1051
1504
|
let result;
|
|
1052
1505
|
try {
|
|
@@ -1072,22 +1525,122 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
|
|
|
1072
1525
|
}
|
|
1073
1526
|
finally {
|
|
1074
1527
|
state.serverEditsAllowed -= 1;
|
|
1528
|
+
state.activeMutationDepth = Math.max(0, (state.activeMutationDepth ?? 0) - 1);
|
|
1529
|
+
if (state.activeMutationDepth === 0)
|
|
1530
|
+
state.activeMutationContext = undefined;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
// #1412 H1/H2: read-only sibling of runServerCommand for telemetry/identity
|
|
1534
|
+
// probes that must NOT participate in the mutation-acceptance window. Unlike
|
|
1535
|
+
// runServerCommand this never touches serverEditsAllowed, activeMutationDepth,
|
|
1536
|
+
// or activeMutationContext — a probe firing mid-flight must leave a concurrent
|
|
1537
|
+
// real executeCommand's mutation context untouched, and must not itself open
|
|
1538
|
+
// the workspace/applyEdit acceptance window (client.ts's applyEdit handler
|
|
1539
|
+
// gates on serverEditsAllowed > 0). Preserves the allowlist-by-advertisement
|
|
1540
|
+
// invariant. Short PROBE_COMMAND_TIMEOUT_MS backstop — this is a diagnostic
|
|
1541
|
+
// sample, not a mutation, and must never hold anything up for anywhere near
|
|
1542
|
+
// EXECUTE_COMMAND_TIMEOUT_MS.
|
|
1543
|
+
export async function runReadOnlyServerCommand(state, command, args, timeoutMs = PROBE_COMMAND_TIMEOUT_MS) {
|
|
1544
|
+
if (!isClientAlive(state)) {
|
|
1545
|
+
return { executed: false, reason: "lsp client not alive" };
|
|
1546
|
+
}
|
|
1547
|
+
if (!state.advertisedCommands.has(command)) {
|
|
1548
|
+
return {
|
|
1549
|
+
executed: false,
|
|
1550
|
+
reason: `command "${command}" is not advertised by the ${state.serverId} server`,
|
|
1551
|
+
};
|
|
1075
1552
|
}
|
|
1553
|
+
try {
|
|
1554
|
+
const result = await withTimeout(safeSendRequest(state.connection, "workspace/executeCommand", {
|
|
1555
|
+
command,
|
|
1556
|
+
arguments: args ?? [],
|
|
1557
|
+
}), timeoutMs);
|
|
1558
|
+
return { executed: true, result };
|
|
1559
|
+
}
|
|
1560
|
+
catch (err) {
|
|
1561
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
1562
|
+
return {
|
|
1563
|
+
executed: false,
|
|
1564
|
+
reason: `workspace/executeCommand timed out after ${timeoutMs}ms`,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
throw err;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function validateWorkspaceEditVersions(state, edit) {
|
|
1571
|
+
for (const change of edit.documentChanges ?? []) {
|
|
1572
|
+
if (typeof change !== "object" || change === null || !("textDocument" in change))
|
|
1573
|
+
continue;
|
|
1574
|
+
const textDocument = change.textDocument;
|
|
1575
|
+
if (!textDocument || typeof textDocument.uri !== "string" || textDocument.version == null)
|
|
1576
|
+
continue;
|
|
1577
|
+
const current = state.documentVersions.get(normalizeMapKey(uriToPath(textDocument.uri)));
|
|
1578
|
+
if (current === undefined || current !== textDocument.version) {
|
|
1579
|
+
throw new Error(`stale workspace edit document version for ${textDocument.uri}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
// Neutralize numeric `textDocument.version` stamps AFTER they have been
|
|
1584
|
+
// validated against the live document map. The tool apply paths (rename
|
|
1585
|
+
// apply:true in tools/lsp-navigation.ts, code-action autofix in
|
|
1586
|
+
// actionable-warnings.ts) call applyWorkspaceEdit without a documentVersions
|
|
1587
|
+
// map, so a preserved numeric version would fail preflight 100% of the time for
|
|
1588
|
+
// servers that stamp real versions (gopls stamps open documents). Setting the
|
|
1589
|
+
// version to null is the spec's "do not check" — the freshness guarantee has
|
|
1590
|
+
// already been provided here by validateWorkspaceEditVersions at the correct
|
|
1591
|
+
// moment. The server-initiated workspace/applyEdit handler does NOT route
|
|
1592
|
+
// through here (it applies params.edit directly with state.documentVersions),
|
|
1593
|
+
// so its real preflight version check is left fully intact.
|
|
1594
|
+
function stripDocumentVersions(edit) {
|
|
1595
|
+
if (!Array.isArray(edit.documentChanges))
|
|
1596
|
+
return edit;
|
|
1597
|
+
const documentChanges = edit.documentChanges.map((change) => {
|
|
1598
|
+
if (typeof change === "object" &&
|
|
1599
|
+
change !== null &&
|
|
1600
|
+
"textDocument" in change &&
|
|
1601
|
+
"edits" in change) {
|
|
1602
|
+
const textDocument = change
|
|
1603
|
+
.textDocument;
|
|
1604
|
+
if (textDocument && typeof textDocument.version === "number") {
|
|
1605
|
+
return {
|
|
1606
|
+
...change,
|
|
1607
|
+
textDocument: { ...textDocument, version: null },
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return change;
|
|
1612
|
+
});
|
|
1613
|
+
return { ...edit, documentChanges };
|
|
1614
|
+
}
|
|
1615
|
+
export async function normalizeClientWorkspaceEdit(state, edit) {
|
|
1616
|
+
validateWorkspaceEditVersions(state, edit);
|
|
1617
|
+
const normalized = (await normalizeWorkspaceEditToUtf16(edit, state.positionEncoding, state.root));
|
|
1618
|
+
return stripDocumentVersions(normalized);
|
|
1076
1619
|
}
|
|
1077
1620
|
async function resolveCodeActionBestEffort(state, action) {
|
|
1078
|
-
if (!isClientAlive(state)
|
|
1621
|
+
if (!isClientAlive(state))
|
|
1079
1622
|
return action;
|
|
1623
|
+
if (action.edit) {
|
|
1624
|
+
return {
|
|
1625
|
+
...action,
|
|
1626
|
+
edit: await normalizeClientWorkspaceEdit(state, action.edit),
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
let resolved;
|
|
1080
1630
|
try {
|
|
1081
|
-
|
|
1082
|
-
if (!resolved || typeof resolved !== "object")
|
|
1083
|
-
return action;
|
|
1084
|
-
return { ...action, ...resolved };
|
|
1631
|
+
resolved = await withTimeout(safeSendRequest(state.connection, "codeAction/resolve", action), NAV_REQUEST_TIMEOUT_MS);
|
|
1085
1632
|
}
|
|
1086
1633
|
catch {
|
|
1087
1634
|
// codeAction/resolve is optional. Keep the original lightweight action when
|
|
1088
1635
|
// the server does not support resolve or fails to populate an edit.
|
|
1089
1636
|
return action;
|
|
1090
1637
|
}
|
|
1638
|
+
if (!resolved || typeof resolved !== "object")
|
|
1639
|
+
return action;
|
|
1640
|
+
const merged = { ...action, ...resolved };
|
|
1641
|
+
return merged.edit
|
|
1642
|
+
? { ...merged, edit: await normalizeClientWorkspaceEdit(state, merged.edit) }
|
|
1643
|
+
: merged;
|
|
1091
1644
|
}
|
|
1092
1645
|
// --- Client Factory ---
|
|
1093
1646
|
export async function createLSPClient(options) {
|
|
@@ -1187,6 +1740,7 @@ export async function createLSPClient(options) {
|
|
|
1187
1740
|
isConnected: true,
|
|
1188
1741
|
isDestroyed: false,
|
|
1189
1742
|
shutdownRequested: false,
|
|
1743
|
+
exitedAt: undefined,
|
|
1190
1744
|
connectionDisposed: false,
|
|
1191
1745
|
lastError: undefined,
|
|
1192
1746
|
connection,
|
|
@@ -1194,13 +1748,23 @@ export async function createLSPClient(options) {
|
|
|
1194
1748
|
pushDiagnosticTimestamps: new Map(),
|
|
1195
1749
|
documentPullDiagnostics: new Map(),
|
|
1196
1750
|
documentPullDiagnosticTimestamps: new Map(),
|
|
1751
|
+
pullFailureHistory: [],
|
|
1197
1752
|
pendingDiagnostics: new Map(),
|
|
1753
|
+
diagnosticPublicationCounts: new Map(),
|
|
1754
|
+
documentOpenedAt: new Map(),
|
|
1198
1755
|
diagnosticEmitter,
|
|
1199
1756
|
diagnosticsVersion: 0,
|
|
1200
1757
|
documentVersions: new Map(),
|
|
1201
1758
|
diagnosticDocVersions: new Map(),
|
|
1759
|
+
documentContentHashes: new Map(),
|
|
1760
|
+
diagnosticBindings: new Map(),
|
|
1761
|
+
pullResultIds: new Map(),
|
|
1762
|
+
workspacePullResultCache: new Map(),
|
|
1202
1763
|
openDocuments: new Set(),
|
|
1764
|
+
closedDocuments: new Set(),
|
|
1765
|
+
openDocumentUris: new Map(),
|
|
1203
1766
|
pendingOpens: new Set(),
|
|
1767
|
+
projectIdentityProbedFiles: new Set(),
|
|
1204
1768
|
// these are filled in after initialize — cast to avoid two-phase init
|
|
1205
1769
|
workspaceDiagnosticsSupport: undefined,
|
|
1206
1770
|
operationSupport: undefined,
|
|
@@ -1209,6 +1773,7 @@ export async function createLSPClient(options) {
|
|
|
1209
1773
|
dynamicRegistrations: new Map(),
|
|
1210
1774
|
advertisedCommands: new Set(),
|
|
1211
1775
|
serverEditsAllowed: 0,
|
|
1776
|
+
activeMutationDepth: 0,
|
|
1212
1777
|
serverId,
|
|
1213
1778
|
launchVariant,
|
|
1214
1779
|
root,
|
|
@@ -1258,7 +1823,22 @@ export async function createLSPClient(options) {
|
|
|
1258
1823
|
});
|
|
1259
1824
|
});
|
|
1260
1825
|
setTimeout(() => {
|
|
1261
|
-
|
|
1826
|
+
// #1114: gate on the process's own observed `exitCode`/`signalCode`,
|
|
1827
|
+
// not `.killed` — `killProcessTree` above signals the POSIX process
|
|
1828
|
+
// GROUP via the raw `process.kill(-pid, …)`, which never touches
|
|
1829
|
+
// this `ChildProcess` instance's `.killed` flag, so `!…killed` here
|
|
1830
|
+
// was always true and this 2s backstop unconditionally re-sent
|
|
1831
|
+
// SIGKILL even when the group had already exited. `exitCode` alone
|
|
1832
|
+
// is insufficient too: a process that died FROM a signal (the
|
|
1833
|
+
// common case here — killProcessTree's own SIGTERM/SIGKILL) has
|
|
1834
|
+
// `exitCode === null` forever and only `signalCode` set, so
|
|
1835
|
+
// checking `exitCode === null` alone still re-SIGKILLs that corpse
|
|
1836
|
+
// on the common path (harmless — `kill()` on an already-exited pid
|
|
1837
|
+
// is a swallowed no-op — but not actually "observed still alive").
|
|
1838
|
+
// Require both null to mean "no exit observed by either signal".
|
|
1839
|
+
if (lspProcess.process.exitCode === null &&
|
|
1840
|
+
lspProcess.process.signalCode === null &&
|
|
1841
|
+
process.platform !== "win32") {
|
|
1262
1842
|
lspProcess.process.kill("SIGKILL");
|
|
1263
1843
|
}
|
|
1264
1844
|
}, 2000);
|
|
@@ -1312,10 +1892,20 @@ export async function createLSPClient(options) {
|
|
|
1312
1892
|
/** True if the server process has exited or been killed. */
|
|
1313
1893
|
processExited: () => lspProcess.process.exitCode !== null ||
|
|
1314
1894
|
lspProcess.process.killed === true,
|
|
1895
|
+
/** #1127: mirrors `state.shutdownRequested` — see interface doc. */
|
|
1896
|
+
wasShutdownIntentional: () => state.shutdownRequested,
|
|
1897
|
+
/** #1127: mirrors `state.exitedAt` — see interface doc. */
|
|
1898
|
+
getExitedAt: () => state.exitedAt,
|
|
1315
1899
|
/** Last N lines of server stderr for diagnostics. */
|
|
1316
1900
|
recentStderr: (lines) => recentStderr(lines),
|
|
1901
|
+
getPullFailureHistory: () => state.pullFailureHistory.map((entry) => ({
|
|
1902
|
+
...entry,
|
|
1903
|
+
message: entry.message.slice(0, 200),
|
|
1904
|
+
})),
|
|
1317
1905
|
/** Pre-request health check — returns error string if dead. */
|
|
1318
1906
|
checkAlive: () => checkProcessAlive(),
|
|
1907
|
+
/** #1277: cheap request round-trip proving the server still responds. */
|
|
1908
|
+
pingLiveness: (timeoutMs) => clientPingLiveness(state, timeoutMs),
|
|
1319
1909
|
notify: {
|
|
1320
1910
|
async open(filePath, content, languageId, preserveDiagnostics, silent) {
|
|
1321
1911
|
return handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics, silent);
|
|
@@ -1327,6 +1917,9 @@ export async function createLSPClient(options) {
|
|
|
1327
1917
|
getDiagnostics(filePath) {
|
|
1328
1918
|
return getMergedDiagnosticsForPath(state, normalizeMapKey(filePath));
|
|
1329
1919
|
},
|
|
1920
|
+
getDiagnosticBinding(filePath) {
|
|
1921
|
+
return state.diagnosticBindings.get(normalizeMapKey(filePath));
|
|
1922
|
+
},
|
|
1330
1923
|
getAllDiagnostics() {
|
|
1331
1924
|
const result = new Map();
|
|
1332
1925
|
const keys = new Set([
|
|
@@ -1337,17 +1930,16 @@ export async function createLSPClient(options) {
|
|
|
1337
1930
|
result.set(key, {
|
|
1338
1931
|
diags: getMergedDiagnosticsForPath(state, key),
|
|
1339
1932
|
ts: Math.max(state.pushDiagnosticTimestamps.get(key) ?? 0, state.documentPullDiagnosticTimestamps.get(key) ?? 0),
|
|
1933
|
+
binding: state.diagnosticBindings.get(key),
|
|
1340
1934
|
});
|
|
1341
1935
|
}
|
|
1342
1936
|
return result;
|
|
1343
1937
|
},
|
|
1344
1938
|
getTrackedDiagnosticPaths() {
|
|
1345
|
-
return [
|
|
1346
|
-
...new Set([
|
|
1939
|
+
return [...new Set([
|
|
1347
1940
|
...state.pushDiagnostics.keys(),
|
|
1348
1941
|
...state.documentPullDiagnostics.keys(),
|
|
1349
|
-
]),
|
|
1350
|
-
];
|
|
1942
|
+
])].map((filePath) => process.platform === "win32" ? filePath.replace(/\//g, "\\") : filePath);
|
|
1351
1943
|
},
|
|
1352
1944
|
pruneDiagnostics(predicate) {
|
|
1353
1945
|
let removed = 0;
|
|
@@ -1383,8 +1975,8 @@ export async function createLSPClient(options) {
|
|
|
1383
1975
|
getLaunchVariant() {
|
|
1384
1976
|
return state.launchVariant;
|
|
1385
1977
|
},
|
|
1386
|
-
async executeCommand(command, args) {
|
|
1387
|
-
return runServerCommand(state, command, args);
|
|
1978
|
+
async executeCommand(command, args, mutationContext) {
|
|
1979
|
+
return runServerCommand(state, command, args, EXECUTE_COMMAND_TIMEOUT_MS, mutationContext);
|
|
1388
1980
|
},
|
|
1389
1981
|
get diagnosticsVersion() {
|
|
1390
1982
|
return state.diagnosticsVersion;
|
|
@@ -1448,6 +2040,12 @@ export async function createLSPClient(options) {
|
|
|
1448
2040
|
isDocumentOpen(filePath) {
|
|
1449
2041
|
return state.openDocuments.has(normalizeMapKey(filePath));
|
|
1450
2042
|
},
|
|
2043
|
+
isBusy() {
|
|
2044
|
+
return (activeRequestsByConnection.get(connection) ?? 0) > 0;
|
|
2045
|
+
},
|
|
2046
|
+
getDocumentUri(filePath) {
|
|
2047
|
+
return state.openDocumentUris?.get(normalizeMapKey(filePath));
|
|
2048
|
+
},
|
|
1451
2049
|
async workspaceSymbol(query) {
|
|
1452
2050
|
if (!isClientAlive(state))
|
|
1453
2051
|
return [];
|
|
@@ -1487,8 +2085,9 @@ export async function createLSPClient(options) {
|
|
|
1487
2085
|
position: await toWirePosition(state, filePath, line, character),
|
|
1488
2086
|
newName,
|
|
1489
2087
|
}, filePath);
|
|
1490
|
-
return result
|
|
2088
|
+
return result ? await normalizeClientWorkspaceEdit(state, result) : null;
|
|
1491
2089
|
},
|
|
2090
|
+
closeDocument: (filePath) => closeDocument(state, filePath),
|
|
1492
2091
|
async willRenameFiles(oldFilePath, newFilePath) {
|
|
1493
2092
|
const result = await navRequest(state, "workspace/willRenameFiles", {
|
|
1494
2093
|
files: [
|
|
@@ -1498,16 +2097,16 @@ export async function createLSPClient(options) {
|
|
|
1498
2097
|
},
|
|
1499
2098
|
],
|
|
1500
2099
|
});
|
|
1501
|
-
return result
|
|
2100
|
+
return result ? await normalizeClientWorkspaceEdit(state, result) : null;
|
|
1502
2101
|
},
|
|
1503
|
-
async didRenameFiles(oldFilePath, newFilePath) {
|
|
2102
|
+
async didRenameFiles(oldFilePath, newFilePath, oldUri, newUri) {
|
|
1504
2103
|
if (!isClientAlive(state))
|
|
1505
2104
|
return;
|
|
1506
2105
|
await safeSendNotification(state.connection, "workspace/didRenameFiles", {
|
|
1507
2106
|
files: [
|
|
1508
2107
|
{
|
|
1509
|
-
oldUri: pathToFileURL(oldFilePath).href,
|
|
1510
|
-
newUri: pathToFileURL(newFilePath).href,
|
|
2108
|
+
oldUri: oldUri ?? pathToFileURL(oldFilePath).href,
|
|
2109
|
+
newUri: newUri ?? pathToFileURL(newFilePath).href,
|
|
1511
2110
|
},
|
|
1512
2111
|
],
|
|
1513
2112
|
});
|
|
@@ -1556,6 +2155,7 @@ async function safeSendNotification(connection, method, params) {
|
|
|
1556
2155
|
throw err;
|
|
1557
2156
|
}
|
|
1558
2157
|
}
|
|
2158
|
+
const activeRequestsByConnection = new WeakMap();
|
|
1559
2159
|
// Helper to safely send requests - catches stream destruction
|
|
1560
2160
|
async function safeSendRequest(connection, method, params,
|
|
1561
2161
|
// When provided, aborting the signal cancels the in-flight request via
|
|
@@ -1578,6 +2178,7 @@ signal) {
|
|
|
1578
2178
|
const send = () => tokenSource
|
|
1579
2179
|
? connection.sendRequest(method, params, tokenSource.token)
|
|
1580
2180
|
: connection.sendRequest(method, params);
|
|
2181
|
+
activeRequestsByConnection.set(connection, (activeRequestsByConnection.get(connection) ?? 0) + 1);
|
|
1581
2182
|
try {
|
|
1582
2183
|
// One safe retry on ContentModified (-32801): the document changed under
|
|
1583
2184
|
// us, so the server discarded the request. A single retry beats returning
|
|
@@ -1607,6 +2208,11 @@ signal) {
|
|
|
1607
2208
|
}
|
|
1608
2209
|
}
|
|
1609
2210
|
finally {
|
|
2211
|
+
const remaining = (activeRequestsByConnection.get(connection) ?? 1) - 1;
|
|
2212
|
+
if (remaining > 0)
|
|
2213
|
+
activeRequestsByConnection.set(connection, remaining);
|
|
2214
|
+
else
|
|
2215
|
+
activeRequestsByConnection.delete(connection);
|
|
1610
2216
|
if (signal && onAbort)
|
|
1611
2217
|
signal.removeEventListener("abort", onAbort);
|
|
1612
2218
|
tokenSource?.dispose();
|