@herbertgao/pi-extensions 2026.8.4 → 2026.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
- 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/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 +5 -8
- 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 +8 -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 +15 -2
- 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/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -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 +8 -7
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
- package/node_modules/pi-lens/CHANGELOG.md +2440 -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/agent-behavior-client.js +21 -4
- 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 +29 -25
- 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-publish.js +10 -3
- 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/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 +14 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
- 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 +347 -75
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
- 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 +1085 -681
- 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 +14 -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/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -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 +10 -3
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +139 -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/format-events-publish.js +16 -3
- 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 +681 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
- 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 +390 -99
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
- package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
- 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 +23 -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 +27 -4
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
- 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 +196 -32
- 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 +123 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
- 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/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 +47 -13
- 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/runner-adapters/call-graph-impact.js +8 -0
- 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/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/runtime-agent-end.js +107 -73
- package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -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 +218 -23
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
- package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -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 +1 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -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 +33 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +49361 -36888
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +211 -44
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
- 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/docs/agent-guide.md +2 -2
- 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 +5 -2
- 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 +31 -2
- package/node_modules/pi-lens/docs/globalconfig.md +45 -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 +12 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +18 -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-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-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-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-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 +55 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
- package/node_modules/pi-mcp-adapter/README.md +34 -3
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
- 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/metadata-cache.ts +27 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -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 +42 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +93 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/package.json +11 -12
- 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,7 +19,9 @@ 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";
|
|
24
27
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
@@ -123,6 +126,25 @@ const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_
|
|
|
123
126
|
// (per #240) is NOT read as clean and falls through to the bounded push backstop.
|
|
124
127
|
const PULL_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_REQUEST_TIMEOUT_MS", 10_000);
|
|
125
128
|
const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS", 1000);
|
|
129
|
+
// #1277: cheap liveness round-trip for the silent-clean gates (`index.ts`).
|
|
130
|
+
// Those gates convert a diagnostics-wait timeout into a confirmed-clean
|
|
131
|
+
// result from a STATIC capability classification (`silentOnClean`) alone —
|
|
132
|
+
// but a wedged server (accepted the notify write, then hung) satisfies that
|
|
133
|
+
// classification identically to a genuinely clean one. This is deliberately
|
|
134
|
+
// short relative to NAV_REQUEST_TIMEOUT_MS: it only needs to prove the
|
|
135
|
+
// connection round-trips SOMETHING before the touch reports clean, not
|
|
136
|
+
// complete a real navigation request.
|
|
137
|
+
const LIVENESS_PING_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_LIVENESS_PING_TIMEOUT_MS", 300);
|
|
138
|
+
// Distinctive, unlikely-to-collide query string — the response content is
|
|
139
|
+
// never inspected, only whether one arrived before the timeout.
|
|
140
|
+
const LIVENESS_PING_QUERY = "__pi_lens_liveness_ping__";
|
|
141
|
+
// #1104: bound on `state.workspacePullResultCache` — one entry per distinct
|
|
142
|
+
// file the server has ever returned a `resultId` for across this client's
|
|
143
|
+
// lifetime. A full clear on overflow (rather than an LRU) is fine, same
|
|
144
|
+
// reasoning as `DISK_BINDING_MEMO_MAX` in diagnostic-binding.ts: each entry is
|
|
145
|
+
// cheaply rebuilt by the next full pull, the worst case is just one extra full
|
|
146
|
+
// (non-`unchanged`) report per affected file.
|
|
147
|
+
const WORKSPACE_PULL_RESULT_CACHE_MAX = 4096;
|
|
126
148
|
// Anti-deadlock backstop for workspace/executeCommand. Deliberately generous
|
|
127
149
|
// (30s): the command is mutating and legitimately long-running (a real server
|
|
128
150
|
// refactor / organize-imports), so this must not truncate valid work — it only
|
|
@@ -282,12 +304,34 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
282
304
|
}
|
|
283
305
|
};
|
|
284
306
|
try {
|
|
307
|
+
// #1114: gate the escalation on OBSERVED exit, not `proc.killed`. Node
|
|
308
|
+
// only sets `proc.killed = true` when `proc.kill()` (the ChildProcess
|
|
309
|
+
// method) successfully SENDS a signal — never when the process actually
|
|
310
|
+
// dies, and the primary SIGTERM path above goes through the raw
|
|
311
|
+
// `process.kill(-pid, …)` process-group call, which never touches
|
|
312
|
+
// `proc.killed` at all. Checking `!proc.killed` here was therefore
|
|
313
|
+
// either always-true (unconditional SIGKILL after the window,
|
|
314
|
+
// regardless of whether the group already died — group-kill path) or
|
|
315
|
+
// always-false/dead (direct-child fallback path, same shape as the
|
|
316
|
+
// safe-spawn escalation bug). An `exit` listener set once, up front,
|
|
317
|
+
// gives a real observed-death signal for both. Seeded from the same
|
|
318
|
+
// `exitCode`/`signalCode` pre-check the top-of-function early return
|
|
319
|
+
// uses (:689) — that early return is skipped when
|
|
320
|
+
// `options.processExiting` is set, so a process that was ALREADY dead
|
|
321
|
+
// on entry can still reach here; without seeding, `exited` would stay
|
|
322
|
+
// false (the "exit" event already fired before this listener was
|
|
323
|
+
// attached) and the fast/non-fast branches below would still fire a
|
|
324
|
+
// redundant group SIGKILL at the escalation window.
|
|
325
|
+
let exited = proc.exitCode != null || proc.signalCode != null;
|
|
326
|
+
proc.once?.("exit", () => {
|
|
327
|
+
exited = true;
|
|
328
|
+
});
|
|
285
329
|
if (!killPosixProcessGroup("SIGTERM")) {
|
|
286
330
|
killDirectChild("SIGTERM");
|
|
287
331
|
}
|
|
288
332
|
if (options.fast) {
|
|
289
333
|
const timer = setTimeout(() => {
|
|
290
|
-
if (!
|
|
334
|
+
if (!exited) {
|
|
291
335
|
logLatency({
|
|
292
336
|
type: "phase",
|
|
293
337
|
phase: "lsp_kill_escalation",
|
|
@@ -324,7 +368,7 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
324
368
|
}, 1500);
|
|
325
369
|
proc.once?.("exit", onExit);
|
|
326
370
|
});
|
|
327
|
-
if (!exitedInTime && !
|
|
371
|
+
if (!exitedInTime && !exited) {
|
|
328
372
|
logLatency({
|
|
329
373
|
type: "phase",
|
|
330
374
|
phase: "lsp_kill_escalation",
|
|
@@ -397,9 +441,33 @@ function clearDiagnosticsForPath(state, normalizedPath) {
|
|
|
397
441
|
state.documentPullDiagnostics?.delete(normalizedPath);
|
|
398
442
|
state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
|
|
399
443
|
state.diagnosticDocVersions?.delete(normalizedPath);
|
|
444
|
+
// #1095: a cleared path must never serve a stale content binding alongside a
|
|
445
|
+
// later publish — drop it with the diagnostics it described. (The last-sent
|
|
446
|
+
// `documentContentHashes` record is intentionally retained: it describes what
|
|
447
|
+
// we sent, which the NEXT publish for that version still needs to bind to.)
|
|
448
|
+
state.diagnosticBindings?.delete(normalizedPath);
|
|
449
|
+
// #1104: a resync invalidates any `unchanged`-report basis too — the next
|
|
450
|
+
// pull must not inherit a resultId/contentHash computed against the
|
|
451
|
+
// content this resync just replaced.
|
|
452
|
+
state.pullResultIds?.delete(normalizedPath);
|
|
453
|
+
state.workspacePullResultCache?.delete(normalizedPath);
|
|
400
454
|
legacy.diagnostics?.delete(normalizedPath);
|
|
401
455
|
legacy.diagnosticTimestamps?.delete(normalizedPath);
|
|
402
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* #1095: fingerprint the EXACT didOpen/didChange payload text at SEND time and
|
|
459
|
+
* tag it with the document version it was sent as, so a later
|
|
460
|
+
* `publishDiagnostics` echoing that version can bind its diagnostics to the
|
|
461
|
+
* content they were computed against. Runs on in-memory content — never a disk
|
|
462
|
+
* read on the notification path (I1). Bounded by the same file-size gates the
|
|
463
|
+
* caller already applies to the content it hands us.
|
|
464
|
+
*/
|
|
465
|
+
function recordSentContent(state, normalizedPath, version, content) {
|
|
466
|
+
state.documentContentHashes.set(normalizedPath, {
|
|
467
|
+
version,
|
|
468
|
+
hash: hashDiagnosticContent(content),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
403
471
|
// Methods that can be registered dynamically and map to operationSupport keys
|
|
404
472
|
const DYNAMIC_OPERATION_METHOD_MAP = {
|
|
405
473
|
"textDocument/definition": "definition",
|
|
@@ -475,10 +543,20 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
475
543
|
state.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
476
544
|
const filePath = uriToPath(params.uri);
|
|
477
545
|
const normalizedPath = normalizeMapKey(filePath);
|
|
546
|
+
// A server can flush a queued publish after didClose during teardown.
|
|
547
|
+
// Do not resurrect diagnostics or their content binding for a document
|
|
548
|
+
// that is no longer open on this client.
|
|
549
|
+
if (state.closedDocuments?.has(normalizedPath))
|
|
550
|
+
return;
|
|
478
551
|
const newDiags = normalizeLspDiagnostics(params.diagnostics || []);
|
|
479
552
|
const docVersion = params.version;
|
|
480
553
|
if (PUB_DEBUG) {
|
|
481
|
-
|
|
554
|
+
// #1333: PUB_DEBUG gate preserved; sink is extension.log.
|
|
555
|
+
logExtension({
|
|
556
|
+
subsystem: "lsp-pub",
|
|
557
|
+
level: "debug",
|
|
558
|
+
message: `server=${state.serverId} pubVersion=${docVersion} docVersion=${state.documentVersions?.get(normalizedPath)} diags=${newDiags.length}`,
|
|
559
|
+
});
|
|
482
560
|
}
|
|
483
561
|
const strategy = getStrategy(state.serverId);
|
|
484
562
|
// Record the document version these diagnostics were computed against
|
|
@@ -488,6 +566,26 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
488
566
|
if (docVersion !== undefined) {
|
|
489
567
|
state.diagnosticDocVersions.set(normalizedPath, docVersion);
|
|
490
568
|
}
|
|
569
|
+
recordBinding();
|
|
570
|
+
};
|
|
571
|
+
// #1095: bind the just-stored diagnostics to the content they were
|
|
572
|
+
// computed against. Only when the server reported a version AND we still
|
|
573
|
+
// hold the sent-content fingerprint for exactly that version — otherwise
|
|
574
|
+
// no contentHash is recorded, so the binding reads "unknown" and a
|
|
575
|
+
// version-less server behaves exactly as before. Runs at the same
|
|
576
|
+
// write-time moment as `pushDiagnostics.set` (superseded pushes are
|
|
577
|
+
// dropped before this via `isSupersededPush`, so a binding never lags the
|
|
578
|
+
// latest sent version).
|
|
579
|
+
const recordBinding = () => {
|
|
580
|
+
if (docVersion === undefined) {
|
|
581
|
+
state.diagnosticBindings.delete(normalizedPath);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const sent = state.documentContentHashes.get(normalizedPath);
|
|
585
|
+
state.diagnosticBindings.set(normalizedPath, {
|
|
586
|
+
version: docVersion,
|
|
587
|
+
contentHash: sent && sent.version === docVersion ? sent.hash : undefined,
|
|
588
|
+
});
|
|
491
589
|
};
|
|
492
590
|
// Late/superseded-push guard: if the server stamped this push with a
|
|
493
591
|
// version and that version already lags the latest didChange we sent,
|
|
@@ -580,8 +678,21 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
580
678
|
if (state.serverEditsAllowed <= 0 || !params?.edit) {
|
|
581
679
|
return { applied: false, failureReason: "edit not solicited" };
|
|
582
680
|
}
|
|
681
|
+
const context = (state.activeMutationDepth ?? 0) === 1
|
|
682
|
+
? state.activeMutationContext
|
|
683
|
+
: undefined;
|
|
684
|
+
const telemetryContext = context ?? {
|
|
685
|
+
cwd: state.root,
|
|
686
|
+
correlationId: newLspMutationCorrelationId(),
|
|
687
|
+
tool: "lsp-workspace-applyEdit",
|
|
688
|
+
source: "lsp-edit",
|
|
689
|
+
};
|
|
583
690
|
try {
|
|
584
|
-
await applyWorkspaceEdit(params.edit, state.root
|
|
691
|
+
await applyWorkspaceEdit(params.edit, state.root, {
|
|
692
|
+
positionEncoding: state.positionEncoding,
|
|
693
|
+
documentVersions: state.documentVersions,
|
|
694
|
+
mutationContext: telemetryContext,
|
|
695
|
+
});
|
|
585
696
|
return { applied: true };
|
|
586
697
|
}
|
|
587
698
|
catch (err) {
|
|
@@ -603,16 +714,30 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
603
714
|
});
|
|
604
715
|
state.connection.onRequest("window/workDoneProgress/create", async () => { });
|
|
605
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* #1127: record the FIRST moment this client observed its own death. Detection
|
|
719
|
+
* of a dead client (the next `getClientForFile` attach in index.ts) can happen
|
|
720
|
+
* long after the process actually died — this timestamp is the only reliable
|
|
721
|
+
* "when did it die" signal, so it must be set here, at the earliest death
|
|
722
|
+
* signal, not derived later from detection time.
|
|
723
|
+
*/
|
|
724
|
+
function markExitedIfUnset(state) {
|
|
725
|
+
if (state.exitedAt === undefined) {
|
|
726
|
+
state.exitedAt = Date.now();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
606
729
|
function setupConnectionLifecycle(state, recentStderr) {
|
|
607
730
|
state.connection.onError(([error]) => {
|
|
608
731
|
state.lastError = error instanceof Error ? error : new Error(String(error));
|
|
609
732
|
state.isConnected = false;
|
|
610
733
|
state.isDestroyed = true;
|
|
734
|
+
markExitedIfUnset(state);
|
|
611
735
|
disposeClientConnection(state);
|
|
612
736
|
});
|
|
613
737
|
state.connection.onClose(() => {
|
|
614
738
|
state.isConnected = false;
|
|
615
739
|
state.isDestroyed = true;
|
|
740
|
+
markExitedIfUnset(state);
|
|
616
741
|
disposeClientConnection(state);
|
|
617
742
|
});
|
|
618
743
|
state.lspProcess.process.on("exit", (code, signal) => {
|
|
@@ -627,6 +752,7 @@ function setupConnectionLifecycle(state, recentStderr) {
|
|
|
627
752
|
const wasIntentional = state.shutdownRequested;
|
|
628
753
|
state.isConnected = false;
|
|
629
754
|
state.isDestroyed = true;
|
|
755
|
+
markExitedIfUnset(state);
|
|
630
756
|
disposeClientConnection(state);
|
|
631
757
|
if (!wasIntentional) {
|
|
632
758
|
logLatency({
|
|
@@ -649,6 +775,12 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
649
775
|
if (!isClientAlive(state))
|
|
650
776
|
return { status: "unavailable" };
|
|
651
777
|
const uri = pathToFileURL(filePath).href;
|
|
778
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
779
|
+
// #1104: echo the last resultId we hold for this document so a server that
|
|
780
|
+
// hasn't changed its view can answer `kind: "unchanged"` instead of
|
|
781
|
+
// recomputing — see the `kind === "unchanged"` branch below for how that's
|
|
782
|
+
// honored (inherit, never treat an omitted `items` as clean).
|
|
783
|
+
const previousResultId = state.pullResultIds.get(normalizedPath);
|
|
652
784
|
try {
|
|
653
785
|
// withTimeout is the backstop against a hung pull-mode server: without it
|
|
654
786
|
// this await never settles unless the stream is destroyed. Bounded by the
|
|
@@ -657,23 +789,69 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
657
789
|
// `clean`), so it falls through to the push-wait/timeout backstop.
|
|
658
790
|
const report = await withTimeout(safeSendRequest(state.connection, "textDocument/diagnostic", {
|
|
659
791
|
textDocument: { uri },
|
|
792
|
+
...(previousResultId !== undefined && { previousResultId }),
|
|
660
793
|
}), Math.max(1, Math.min(PULL_REQUEST_TIMEOUT_MS, budgetMs)));
|
|
661
|
-
if (!report)
|
|
794
|
+
if (!report) {
|
|
795
|
+
recordPullFailure(state, "textDocument/diagnostic", new Error("empty response"));
|
|
662
796
|
return { status: "unavailable" };
|
|
663
|
-
|
|
664
|
-
const primaryItems = normalizeLspDiagnostics(report.items ?? []);
|
|
797
|
+
}
|
|
665
798
|
const now = Date.now();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
799
|
+
// #1104: the fingerprint of the content we last sent for this document —
|
|
800
|
+
// the SAME `documentContentHashes` entry the push binding path uses
|
|
801
|
+
// (`recordSentContent` runs unconditionally on every didOpen/didChange,
|
|
802
|
+
// regardless of push/pull mode), so this costs no extra read. A pull
|
|
803
|
+
// response describes whatever the server had when it answered, which for
|
|
804
|
+
// a pi-lens-opened document is exactly that last-sent payload.
|
|
805
|
+
const sentHash = state.documentContentHashes.get(normalizedPath)?.hash;
|
|
806
|
+
let totalCount;
|
|
807
|
+
if (report.kind === "unchanged") {
|
|
808
|
+
// #1104: same resultId basis as last time — an omitted `items` here
|
|
809
|
+
// means "no change", NOT "clean". Overwriting with `[]` would be the
|
|
810
|
+
// exact false-clean shape #570/#571 already fixed for the touch path;
|
|
811
|
+
// keep the previously stored diagnostics and binding as-is.
|
|
812
|
+
totalCount = state.documentPullDiagnostics.get(normalizedPath)?.length ?? 0;
|
|
813
|
+
// Still a fresh confirmation as of `now` even though the content is
|
|
814
|
+
// unchanged — bump the timestamp so `getAllDiagnostics()` doesn't read
|
|
815
|
+
// this entry as aging purely because the server had nothing new to say.
|
|
816
|
+
state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const primaryItems = normalizeLspDiagnostics(report.items ?? []);
|
|
820
|
+
state.documentPullDiagnostics.set(normalizedPath, primaryItems);
|
|
821
|
+
state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
|
|
822
|
+
state.diagnosticsVersion += 1;
|
|
823
|
+
state.diagnosticBindings.set(normalizedPath, { contentHash: sentHash });
|
|
824
|
+
totalCount = primaryItems.length;
|
|
825
|
+
}
|
|
826
|
+
if (report.resultId !== undefined) {
|
|
827
|
+
state.pullResultIds.set(normalizedPath, report.resultId);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
state.pullResultIds.delete(normalizedPath);
|
|
831
|
+
}
|
|
670
832
|
if (report.relatedDocuments) {
|
|
671
833
|
for (const [relatedUri, related] of Object.entries(report.relatedDocuments)) {
|
|
672
834
|
const relatedPath = uriToPath(relatedUri);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
835
|
+
const relatedNormalized = normalizeMapKey(relatedPath);
|
|
836
|
+
if (related?.kind === "unchanged") {
|
|
837
|
+
totalCount +=
|
|
838
|
+
state.documentPullDiagnostics.get(relatedNormalized)?.length ?? 0;
|
|
839
|
+
state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
const relatedItems = normalizeLspDiagnostics(related?.items ?? []);
|
|
843
|
+
state.documentPullDiagnostics.set(relatedNormalized, relatedItems);
|
|
844
|
+
state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
|
|
845
|
+
// #1104: a related document's diagnostics were NOT computed against
|
|
846
|
+
// content we independently sent/fingerprinted (we never requested
|
|
847
|
+
// it directly) — its binding stays honestly "unknown" rather than
|
|
848
|
+
// borrowing the primary document's hash.
|
|
849
|
+
state.diagnosticBindings.delete(relatedNormalized);
|
|
850
|
+
totalCount += relatedItems.length;
|
|
851
|
+
}
|
|
852
|
+
if (related?.resultId !== undefined) {
|
|
853
|
+
state.pullResultIds.set(relatedNormalized, related.resultId);
|
|
854
|
+
}
|
|
677
855
|
}
|
|
678
856
|
}
|
|
679
857
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
@@ -681,10 +859,34 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
|
|
|
681
859
|
? { status: "found", count: totalCount }
|
|
682
860
|
: { status: "clean" };
|
|
683
861
|
}
|
|
684
|
-
catch {
|
|
862
|
+
catch (err) {
|
|
863
|
+
recordPullFailure(state, "textDocument/diagnostic", err);
|
|
685
864
|
return { status: "unavailable" };
|
|
686
865
|
}
|
|
687
866
|
}
|
|
867
|
+
const PULL_FAILURE_HISTORY_LIMIT = 10;
|
|
868
|
+
function recordPullFailure(state, method, error) {
|
|
869
|
+
const candidate = error;
|
|
870
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
871
|
+
const unsupportedMessage = /^(?:method not found|unknown method|unsupported method)(?::|$)/i;
|
|
872
|
+
if (candidate.code === -32601 ||
|
|
873
|
+
candidate.code === "-32601" ||
|
|
874
|
+
unsupportedMessage.test(message.trim()))
|
|
875
|
+
return;
|
|
876
|
+
state.pullFailureHistory.push({
|
|
877
|
+
timestamp: Date.now(),
|
|
878
|
+
method,
|
|
879
|
+
...(typeof candidate.code === "number" || typeof candidate.code === "string"
|
|
880
|
+
? { code: candidate.code }
|
|
881
|
+
: {}),
|
|
882
|
+
message: typeof candidate.message === "string"
|
|
883
|
+
? candidate.message
|
|
884
|
+
: String(error),
|
|
885
|
+
});
|
|
886
|
+
if (state.pullFailureHistory.length > PULL_FAILURE_HISTORY_LIMIT) {
|
|
887
|
+
state.pullFailureHistory.splice(0, state.pullFailureHistory.length - PULL_FAILURE_HISTORY_LIMIT);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
688
890
|
/**
|
|
689
891
|
* One project-wide `workspace/diagnostic` pull — a single request that returns
|
|
690
892
|
* diagnostics for every document the server knows, instead of opening N files.
|
|
@@ -698,23 +900,73 @@ export async function clientRequestWorkspaceDiagnostics(state, budgetMs) {
|
|
|
698
900
|
if (!state.workspaceDiagnosticsSupport.workspaceDiagnostics)
|
|
699
901
|
return undefined;
|
|
700
902
|
try {
|
|
701
|
-
|
|
903
|
+
// #1104: echo every resultId we hold from a PRIOR pull so the server can
|
|
904
|
+
// answer `kind: "unchanged"` for files it hasn't recomputed, instead of
|
|
905
|
+
// resending (and us re-hashing) every file on every sweep.
|
|
906
|
+
const previousResultIds = Array.from(state.workspacePullResultCache.values()).map((entry) => ({ uri: entry.uri, value: entry.resultId }));
|
|
907
|
+
const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds }), Math.max(1, budgetMs));
|
|
702
908
|
if (!report || !Array.isArray(report.items))
|
|
703
909
|
return undefined;
|
|
704
910
|
const out = [];
|
|
705
911
|
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")
|
|
912
|
+
if (!item?.uri)
|
|
709
913
|
continue;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
914
|
+
const filePath = uriToPath(item.uri);
|
|
915
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
916
|
+
if (item.kind === "unchanged") {
|
|
917
|
+
// #1104: inherit the prior pull's diagnostics + content binding for
|
|
918
|
+
// the SAME resultId basis — an "unchanged" report never carries
|
|
919
|
+
// `items`, so without this a file the server confirmed unchanged
|
|
920
|
+
// would silently drop out of the sweep result entirely.
|
|
921
|
+
const prior = state.workspacePullResultCache.get(normalizedPath);
|
|
922
|
+
if (!prior)
|
|
923
|
+
continue; // no earlier basis to inherit — nothing to report
|
|
924
|
+
if (item.resultId !== undefined) {
|
|
925
|
+
state.workspacePullResultCache.set(normalizedPath, {
|
|
926
|
+
...prior,
|
|
927
|
+
resultId: item.resultId,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
out.push({
|
|
931
|
+
filePath,
|
|
932
|
+
diagnostics: prior.diagnostics,
|
|
933
|
+
contentHash: prior.contentHash,
|
|
934
|
+
});
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
// "full" (or a non-conforming server omitting `kind`, per the LSP
|
|
938
|
+
// default) — recompute and re-fingerprint.
|
|
939
|
+
const diagnostics = normalizeLspDiagnostics(item.items ?? []);
|
|
940
|
+
// #1104: fingerprint the file bytes active AT REQUEST TIME. Best-effort —
|
|
941
|
+
// a read failure (deleted/unreadable mid-sweep) just leaves contentHash
|
|
942
|
+
// undefined, so the binding reads honestly "unknown", never fabricated.
|
|
943
|
+
let contentHash;
|
|
944
|
+
try {
|
|
945
|
+
contentHash = hashDiagnosticContent(await readFile(filePath, "utf-8"));
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
contentHash = undefined;
|
|
949
|
+
}
|
|
950
|
+
if (item.resultId !== undefined) {
|
|
951
|
+
if (state.workspacePullResultCache.size >= WORKSPACE_PULL_RESULT_CACHE_MAX) {
|
|
952
|
+
state.workspacePullResultCache.clear();
|
|
953
|
+
}
|
|
954
|
+
state.workspacePullResultCache.set(normalizedPath, {
|
|
955
|
+
uri: item.uri,
|
|
956
|
+
resultId: item.resultId,
|
|
957
|
+
diagnostics,
|
|
958
|
+
contentHash,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
state.workspacePullResultCache.delete(normalizedPath);
|
|
963
|
+
}
|
|
964
|
+
out.push({ filePath, diagnostics, contentHash });
|
|
714
965
|
}
|
|
715
966
|
return out;
|
|
716
967
|
}
|
|
717
|
-
catch {
|
|
968
|
+
catch (err) {
|
|
969
|
+
recordPullFailure(state, "workspace/diagnostic", err);
|
|
718
970
|
return undefined;
|
|
719
971
|
}
|
|
720
972
|
}
|
|
@@ -807,8 +1059,8 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
807
1059
|
export async function handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics = false, silent = false) {
|
|
808
1060
|
if (!isClientAlive(state))
|
|
809
1061
|
return;
|
|
810
|
-
const uri = pathToFileURL(filePath).href;
|
|
811
1062
|
const normalizedPath = normalizeMapKey(filePath);
|
|
1063
|
+
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
812
1064
|
if (state.openDocuments.has(normalizedPath) ||
|
|
813
1065
|
state.pendingOpens.has(normalizedPath)) {
|
|
814
1066
|
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
@@ -816,6 +1068,12 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
816
1068
|
// preserveDiagnostics: skip cache clear for format-only resyncs so
|
|
817
1069
|
// waitForDiagnostics fast-paths instead of waiting up to 5s for TypeScript
|
|
818
1070
|
// to re-publish what it already knows (formatting doesn't change semantics).
|
|
1071
|
+
// #1095 note: this also retains the prior content `binding`. Until the
|
|
1072
|
+
// server republishes for the new version, a binding read compares the OLD
|
|
1073
|
+
// content hash against the NEW disk bytes → boundToCurrentDisk `false` →
|
|
1074
|
+
// the consumer demotes to inconclusive. That's the SAFE (#533) direction (a
|
|
1075
|
+
// transient "unconfirmed", never a false-clean), self-healing on the next
|
|
1076
|
+
// publish — not a correctness hazard, unlike the reopen false-TRUE above.
|
|
819
1077
|
if (!preserveDiagnostics) {
|
|
820
1078
|
clearDiagnosticsForPath(state, normalizedPath);
|
|
821
1079
|
}
|
|
@@ -827,19 +1085,32 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
827
1085
|
textDocument: { uri },
|
|
828
1086
|
});
|
|
829
1087
|
state.openDocuments.delete(normalizedPath);
|
|
830
|
-
state.
|
|
1088
|
+
state.openDocumentUris?.delete(normalizedPath);
|
|
1089
|
+
// #1095 (P2-3): carry the version counter FORWARD across the
|
|
1090
|
+
// close+reopen instead of resetting to 0. LSP lets a didOpen use any
|
|
1091
|
+
// version, and reusing 0 for successive resyncs made the version
|
|
1092
|
+
// ambiguous — a late publish for an earlier resync's content echoed the
|
|
1093
|
+
// SAME 0 as the current send, so the superseded-push guard (0 < 0 is
|
|
1094
|
+
// false) accepted it and `recordBinding` bound STALE diagnostics to the
|
|
1095
|
+
// CURRENT content's fingerprint → an affirmative boundToCurrentDisk TRUE
|
|
1096
|
+
// for a stale view (worse than "unknown"). Monotonic versions make that
|
|
1097
|
+
// late echo strictly older → dropped by isSupersededPush → never bound.
|
|
1098
|
+
state.documentVersions.set(normalizedPath, version);
|
|
831
1099
|
if (!isClientAlive(state))
|
|
832
1100
|
return;
|
|
833
1101
|
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
834
|
-
textDocument: { uri, languageId, version
|
|
1102
|
+
textDocument: { uri, languageId, version, text: content },
|
|
835
1103
|
});
|
|
1104
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
836
1105
|
state.openDocuments.add(normalizedPath);
|
|
1106
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
837
1107
|
return;
|
|
838
1108
|
}
|
|
839
1109
|
await safeSendNotification(state.connection, "textDocument/didChange", {
|
|
840
1110
|
textDocument: { uri, version },
|
|
841
1111
|
contentChanges: [{ text: content }],
|
|
842
1112
|
});
|
|
1113
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
843
1114
|
return;
|
|
844
1115
|
}
|
|
845
1116
|
state.pendingOpens.add(normalizedPath);
|
|
@@ -873,14 +1144,17 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
873
1144
|
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
874
1145
|
textDocument: { uri, languageId, version: 0, text: content },
|
|
875
1146
|
});
|
|
1147
|
+
recordSentContent(state, normalizedPath, 0, content);
|
|
876
1148
|
state.pendingOpens.delete(normalizedPath);
|
|
877
1149
|
state.openDocuments.add(normalizedPath);
|
|
1150
|
+
state.closedDocuments?.delete(normalizedPath);
|
|
1151
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
878
1152
|
}
|
|
879
1153
|
export async function handleNotifyChange(state, filePath, content) {
|
|
880
1154
|
if (!isClientAlive(state))
|
|
881
1155
|
return;
|
|
882
|
-
const uri = pathToFileURL(filePath).href;
|
|
883
1156
|
const normalizedPath = normalizeMapKey(filePath);
|
|
1157
|
+
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
884
1158
|
if (!state.openDocuments.has(normalizedPath)) {
|
|
885
1159
|
// Safety fallback: keep protocol ordering valid even if caller sends
|
|
886
1160
|
// didChange before first didOpen for this document.
|
|
@@ -888,7 +1162,9 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
888
1162
|
textDocument: { uri, languageId: "plaintext", version: 0, text: content },
|
|
889
1163
|
});
|
|
890
1164
|
state.documentVersions.set(normalizedPath, 0);
|
|
1165
|
+
recordSentContent(state, normalizedPath, 0, content);
|
|
891
1166
|
state.openDocuments.add(normalizedPath);
|
|
1167
|
+
state.openDocumentUris?.set(normalizedPath, uri);
|
|
892
1168
|
return;
|
|
893
1169
|
}
|
|
894
1170
|
const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
|
|
@@ -900,6 +1176,25 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
900
1176
|
textDocument: { uri, version },
|
|
901
1177
|
contentChanges: [{ text: content }],
|
|
902
1178
|
});
|
|
1179
|
+
recordSentContent(state, normalizedPath, version, content);
|
|
1180
|
+
}
|
|
1181
|
+
/** Close a document through the same lifecycle path exposed by the client. */
|
|
1182
|
+
export async function closeDocument(state, filePath) {
|
|
1183
|
+
if (!isClientAlive(state))
|
|
1184
|
+
return;
|
|
1185
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
1186
|
+
if (!state.openDocuments.has(normalizedPath))
|
|
1187
|
+
return;
|
|
1188
|
+
await safeSendNotification(state.connection, "textDocument/didClose", {
|
|
1189
|
+
textDocument: {
|
|
1190
|
+
uri: state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href,
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
state.openDocuments.delete(normalizedPath);
|
|
1194
|
+
state.closedDocuments?.add(normalizedPath);
|
|
1195
|
+
state.openDocumentUris?.delete(normalizedPath);
|
|
1196
|
+
state.documentVersions.delete(normalizedPath);
|
|
1197
|
+
clearDiagnosticsForPath(state, normalizedPath);
|
|
903
1198
|
}
|
|
904
1199
|
export async function clientShutdown(state, options = {}) {
|
|
905
1200
|
const shutdownStart = Date.now();
|
|
@@ -912,6 +1207,7 @@ export async function clientShutdown(state, options = {}) {
|
|
|
912
1207
|
state.pendingDiagnostics.clear();
|
|
913
1208
|
state.pendingOpens.clear();
|
|
914
1209
|
state.openDocuments.clear();
|
|
1210
|
+
state.openDocumentUris?.clear();
|
|
915
1211
|
// #271: drop any pending watched-files batch + its timer (a dying client's
|
|
916
1212
|
// queued FS changes are moot, and the timer must not outlive the connection).
|
|
917
1213
|
state.watchQueue?.cancel();
|
|
@@ -1030,13 +1326,44 @@ signal = getAmbientAbortSignal()) {
|
|
|
1030
1326
|
}
|
|
1031
1327
|
return result;
|
|
1032
1328
|
}
|
|
1329
|
+
// #1277: cheap liveness round-trip used by the silent-clean gates in
|
|
1330
|
+
// `index.ts`. `isAlive()`/`checkAlive()` only look at process/connection
|
|
1331
|
+
// state — a server that accepted the notify write and then wedged (still
|
|
1332
|
+
// running, connection still open, just never replying) reads as "alive" by
|
|
1333
|
+
// those checks even though it will never answer anything again. This sends a
|
|
1334
|
+
// real request (`workspace/symbol`, chosen because it needs no open document)
|
|
1335
|
+
// and reports whether the connection round-tripped it — success, a genuine
|
|
1336
|
+
// protocol-level error (e.g. MethodNotFound), and a stream-destroyed/
|
|
1337
|
+
// cancelled response (safeSendRequest swallows those to `undefined`, so the
|
|
1338
|
+
// final `isClientAlive` re-check is what catches "died mid-flight") ALL count
|
|
1339
|
+
// as "alive"; only a real timeout, or the connection having gone down by the
|
|
1340
|
+
// time this resolves, reports dead. The response content itself is never
|
|
1341
|
+
// inspected — only whether one arrived in time.
|
|
1342
|
+
async function clientPingLiveness(state, timeoutMs = LIVENESS_PING_TIMEOUT_MS) {
|
|
1343
|
+
if (!isClientAlive(state))
|
|
1344
|
+
return false;
|
|
1345
|
+
try {
|
|
1346
|
+
await withTimeout(safeSendRequest(state.connection, "workspace/symbol", {
|
|
1347
|
+
query: LIVENESS_PING_QUERY,
|
|
1348
|
+
}), timeoutMs);
|
|
1349
|
+
}
|
|
1350
|
+
catch (err) {
|
|
1351
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
// A real protocol-level error reply still proves the server round-
|
|
1355
|
+
// tripped the request — fall through to the alive re-check below
|
|
1356
|
+
// rather than treating an error response as "dead".
|
|
1357
|
+
}
|
|
1358
|
+
return isClientAlive(state);
|
|
1359
|
+
}
|
|
1033
1360
|
// Run an advertised server command via workspace/executeCommand, with the
|
|
1034
1361
|
// generous EXECUTE_COMMAND_TIMEOUT_MS anti-deadlock backstop. Preserves the
|
|
1035
1362
|
// hardening invariants: allowlist-by-advertisement (only commands the server
|
|
1036
1363
|
// declared) and the serverEditsAllowed window that gates server-driven
|
|
1037
1364
|
// applyEdit to the duration of an explicit call. Exported with an overridable
|
|
1038
1365
|
// `timeoutMs` for the #365 regression tests.
|
|
1039
|
-
export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS) {
|
|
1366
|
+
export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS, mutationContext) {
|
|
1040
1367
|
if (!isClientAlive(state)) {
|
|
1041
1368
|
return { executed: false, reason: "lsp client not alive" };
|
|
1042
1369
|
}
|
|
@@ -1047,6 +1374,11 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
|
|
|
1047
1374
|
};
|
|
1048
1375
|
}
|
|
1049
1376
|
state.serverEditsAllowed += 1;
|
|
1377
|
+
state.activeMutationDepth = (state.activeMutationDepth ?? 0) + 1;
|
|
1378
|
+
if (state.activeMutationDepth === 1)
|
|
1379
|
+
state.activeMutationContext = mutationContext;
|
|
1380
|
+
else
|
|
1381
|
+
state.activeMutationContext = undefined;
|
|
1050
1382
|
try {
|
|
1051
1383
|
let result;
|
|
1052
1384
|
try {
|
|
@@ -1072,22 +1404,85 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
|
|
|
1072
1404
|
}
|
|
1073
1405
|
finally {
|
|
1074
1406
|
state.serverEditsAllowed -= 1;
|
|
1407
|
+
state.activeMutationDepth = Math.max(0, (state.activeMutationDepth ?? 0) - 1);
|
|
1408
|
+
if (state.activeMutationDepth === 0)
|
|
1409
|
+
state.activeMutationContext = undefined;
|
|
1075
1410
|
}
|
|
1076
1411
|
}
|
|
1412
|
+
function validateWorkspaceEditVersions(state, edit) {
|
|
1413
|
+
for (const change of edit.documentChanges ?? []) {
|
|
1414
|
+
if (typeof change !== "object" || change === null || !("textDocument" in change))
|
|
1415
|
+
continue;
|
|
1416
|
+
const textDocument = change.textDocument;
|
|
1417
|
+
if (!textDocument || typeof textDocument.uri !== "string" || textDocument.version == null)
|
|
1418
|
+
continue;
|
|
1419
|
+
const current = state.documentVersions.get(normalizeMapKey(uriToPath(textDocument.uri)));
|
|
1420
|
+
if (current === undefined || current !== textDocument.version) {
|
|
1421
|
+
throw new Error(`stale workspace edit document version for ${textDocument.uri}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// Neutralize numeric `textDocument.version` stamps AFTER they have been
|
|
1426
|
+
// validated against the live document map. The tool apply paths (rename
|
|
1427
|
+
// apply:true in tools/lsp-navigation.ts, code-action autofix in
|
|
1428
|
+
// actionable-warnings.ts) call applyWorkspaceEdit without a documentVersions
|
|
1429
|
+
// map, so a preserved numeric version would fail preflight 100% of the time for
|
|
1430
|
+
// servers that stamp real versions (gopls stamps open documents). Setting the
|
|
1431
|
+
// version to null is the spec's "do not check" — the freshness guarantee has
|
|
1432
|
+
// already been provided here by validateWorkspaceEditVersions at the correct
|
|
1433
|
+
// moment. The server-initiated workspace/applyEdit handler does NOT route
|
|
1434
|
+
// through here (it applies params.edit directly with state.documentVersions),
|
|
1435
|
+
// so its real preflight version check is left fully intact.
|
|
1436
|
+
function stripDocumentVersions(edit) {
|
|
1437
|
+
if (!Array.isArray(edit.documentChanges))
|
|
1438
|
+
return edit;
|
|
1439
|
+
const documentChanges = edit.documentChanges.map((change) => {
|
|
1440
|
+
if (typeof change === "object" &&
|
|
1441
|
+
change !== null &&
|
|
1442
|
+
"textDocument" in change &&
|
|
1443
|
+
"edits" in change) {
|
|
1444
|
+
const textDocument = change
|
|
1445
|
+
.textDocument;
|
|
1446
|
+
if (textDocument && typeof textDocument.version === "number") {
|
|
1447
|
+
return {
|
|
1448
|
+
...change,
|
|
1449
|
+
textDocument: { ...textDocument, version: null },
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return change;
|
|
1454
|
+
});
|
|
1455
|
+
return { ...edit, documentChanges };
|
|
1456
|
+
}
|
|
1457
|
+
export async function normalizeClientWorkspaceEdit(state, edit) {
|
|
1458
|
+
validateWorkspaceEditVersions(state, edit);
|
|
1459
|
+
const normalized = (await normalizeWorkspaceEditToUtf16(edit, state.positionEncoding, state.root));
|
|
1460
|
+
return stripDocumentVersions(normalized);
|
|
1461
|
+
}
|
|
1077
1462
|
async function resolveCodeActionBestEffort(state, action) {
|
|
1078
|
-
if (!isClientAlive(state)
|
|
1463
|
+
if (!isClientAlive(state))
|
|
1079
1464
|
return action;
|
|
1465
|
+
if (action.edit) {
|
|
1466
|
+
return {
|
|
1467
|
+
...action,
|
|
1468
|
+
edit: await normalizeClientWorkspaceEdit(state, action.edit),
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
let resolved;
|
|
1080
1472
|
try {
|
|
1081
|
-
|
|
1082
|
-
if (!resolved || typeof resolved !== "object")
|
|
1083
|
-
return action;
|
|
1084
|
-
return { ...action, ...resolved };
|
|
1473
|
+
resolved = await withTimeout(safeSendRequest(state.connection, "codeAction/resolve", action), NAV_REQUEST_TIMEOUT_MS);
|
|
1085
1474
|
}
|
|
1086
1475
|
catch {
|
|
1087
1476
|
// codeAction/resolve is optional. Keep the original lightweight action when
|
|
1088
1477
|
// the server does not support resolve or fails to populate an edit.
|
|
1089
1478
|
return action;
|
|
1090
1479
|
}
|
|
1480
|
+
if (!resolved || typeof resolved !== "object")
|
|
1481
|
+
return action;
|
|
1482
|
+
const merged = { ...action, ...resolved };
|
|
1483
|
+
return merged.edit
|
|
1484
|
+
? { ...merged, edit: await normalizeClientWorkspaceEdit(state, merged.edit) }
|
|
1485
|
+
: merged;
|
|
1091
1486
|
}
|
|
1092
1487
|
// --- Client Factory ---
|
|
1093
1488
|
export async function createLSPClient(options) {
|
|
@@ -1187,6 +1582,7 @@ export async function createLSPClient(options) {
|
|
|
1187
1582
|
isConnected: true,
|
|
1188
1583
|
isDestroyed: false,
|
|
1189
1584
|
shutdownRequested: false,
|
|
1585
|
+
exitedAt: undefined,
|
|
1190
1586
|
connectionDisposed: false,
|
|
1191
1587
|
lastError: undefined,
|
|
1192
1588
|
connection,
|
|
@@ -1194,12 +1590,19 @@ export async function createLSPClient(options) {
|
|
|
1194
1590
|
pushDiagnosticTimestamps: new Map(),
|
|
1195
1591
|
documentPullDiagnostics: new Map(),
|
|
1196
1592
|
documentPullDiagnosticTimestamps: new Map(),
|
|
1593
|
+
pullFailureHistory: [],
|
|
1197
1594
|
pendingDiagnostics: new Map(),
|
|
1198
1595
|
diagnosticEmitter,
|
|
1199
1596
|
diagnosticsVersion: 0,
|
|
1200
1597
|
documentVersions: new Map(),
|
|
1201
1598
|
diagnosticDocVersions: new Map(),
|
|
1599
|
+
documentContentHashes: new Map(),
|
|
1600
|
+
diagnosticBindings: new Map(),
|
|
1601
|
+
pullResultIds: new Map(),
|
|
1602
|
+
workspacePullResultCache: new Map(),
|
|
1202
1603
|
openDocuments: new Set(),
|
|
1604
|
+
closedDocuments: new Set(),
|
|
1605
|
+
openDocumentUris: new Map(),
|
|
1203
1606
|
pendingOpens: new Set(),
|
|
1204
1607
|
// these are filled in after initialize — cast to avoid two-phase init
|
|
1205
1608
|
workspaceDiagnosticsSupport: undefined,
|
|
@@ -1209,6 +1612,7 @@ export async function createLSPClient(options) {
|
|
|
1209
1612
|
dynamicRegistrations: new Map(),
|
|
1210
1613
|
advertisedCommands: new Set(),
|
|
1211
1614
|
serverEditsAllowed: 0,
|
|
1615
|
+
activeMutationDepth: 0,
|
|
1212
1616
|
serverId,
|
|
1213
1617
|
launchVariant,
|
|
1214
1618
|
root,
|
|
@@ -1258,7 +1662,22 @@ export async function createLSPClient(options) {
|
|
|
1258
1662
|
});
|
|
1259
1663
|
});
|
|
1260
1664
|
setTimeout(() => {
|
|
1261
|
-
|
|
1665
|
+
// #1114: gate on the process's own observed `exitCode`/`signalCode`,
|
|
1666
|
+
// not `.killed` — `killProcessTree` above signals the POSIX process
|
|
1667
|
+
// GROUP via the raw `process.kill(-pid, …)`, which never touches
|
|
1668
|
+
// this `ChildProcess` instance's `.killed` flag, so `!…killed` here
|
|
1669
|
+
// was always true and this 2s backstop unconditionally re-sent
|
|
1670
|
+
// SIGKILL even when the group had already exited. `exitCode` alone
|
|
1671
|
+
// is insufficient too: a process that died FROM a signal (the
|
|
1672
|
+
// common case here — killProcessTree's own SIGTERM/SIGKILL) has
|
|
1673
|
+
// `exitCode === null` forever and only `signalCode` set, so
|
|
1674
|
+
// checking `exitCode === null` alone still re-SIGKILLs that corpse
|
|
1675
|
+
// on the common path (harmless — `kill()` on an already-exited pid
|
|
1676
|
+
// is a swallowed no-op — but not actually "observed still alive").
|
|
1677
|
+
// Require both null to mean "no exit observed by either signal".
|
|
1678
|
+
if (lspProcess.process.exitCode === null &&
|
|
1679
|
+
lspProcess.process.signalCode === null &&
|
|
1680
|
+
process.platform !== "win32") {
|
|
1262
1681
|
lspProcess.process.kill("SIGKILL");
|
|
1263
1682
|
}
|
|
1264
1683
|
}, 2000);
|
|
@@ -1312,10 +1731,20 @@ export async function createLSPClient(options) {
|
|
|
1312
1731
|
/** True if the server process has exited or been killed. */
|
|
1313
1732
|
processExited: () => lspProcess.process.exitCode !== null ||
|
|
1314
1733
|
lspProcess.process.killed === true,
|
|
1734
|
+
/** #1127: mirrors `state.shutdownRequested` — see interface doc. */
|
|
1735
|
+
wasShutdownIntentional: () => state.shutdownRequested,
|
|
1736
|
+
/** #1127: mirrors `state.exitedAt` — see interface doc. */
|
|
1737
|
+
getExitedAt: () => state.exitedAt,
|
|
1315
1738
|
/** Last N lines of server stderr for diagnostics. */
|
|
1316
1739
|
recentStderr: (lines) => recentStderr(lines),
|
|
1740
|
+
getPullFailureHistory: () => state.pullFailureHistory.map((entry) => ({
|
|
1741
|
+
...entry,
|
|
1742
|
+
message: entry.message.slice(0, 200),
|
|
1743
|
+
})),
|
|
1317
1744
|
/** Pre-request health check — returns error string if dead. */
|
|
1318
1745
|
checkAlive: () => checkProcessAlive(),
|
|
1746
|
+
/** #1277: cheap request round-trip proving the server still responds. */
|
|
1747
|
+
pingLiveness: (timeoutMs) => clientPingLiveness(state, timeoutMs),
|
|
1319
1748
|
notify: {
|
|
1320
1749
|
async open(filePath, content, languageId, preserveDiagnostics, silent) {
|
|
1321
1750
|
return handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics, silent);
|
|
@@ -1327,6 +1756,9 @@ export async function createLSPClient(options) {
|
|
|
1327
1756
|
getDiagnostics(filePath) {
|
|
1328
1757
|
return getMergedDiagnosticsForPath(state, normalizeMapKey(filePath));
|
|
1329
1758
|
},
|
|
1759
|
+
getDiagnosticBinding(filePath) {
|
|
1760
|
+
return state.diagnosticBindings.get(normalizeMapKey(filePath));
|
|
1761
|
+
},
|
|
1330
1762
|
getAllDiagnostics() {
|
|
1331
1763
|
const result = new Map();
|
|
1332
1764
|
const keys = new Set([
|
|
@@ -1337,17 +1769,16 @@ export async function createLSPClient(options) {
|
|
|
1337
1769
|
result.set(key, {
|
|
1338
1770
|
diags: getMergedDiagnosticsForPath(state, key),
|
|
1339
1771
|
ts: Math.max(state.pushDiagnosticTimestamps.get(key) ?? 0, state.documentPullDiagnosticTimestamps.get(key) ?? 0),
|
|
1772
|
+
binding: state.diagnosticBindings.get(key),
|
|
1340
1773
|
});
|
|
1341
1774
|
}
|
|
1342
1775
|
return result;
|
|
1343
1776
|
},
|
|
1344
1777
|
getTrackedDiagnosticPaths() {
|
|
1345
|
-
return [
|
|
1346
|
-
...new Set([
|
|
1778
|
+
return [...new Set([
|
|
1347
1779
|
...state.pushDiagnostics.keys(),
|
|
1348
1780
|
...state.documentPullDiagnostics.keys(),
|
|
1349
|
-
]),
|
|
1350
|
-
];
|
|
1781
|
+
])].map((filePath) => process.platform === "win32" ? filePath.replace(/\//g, "\\") : filePath);
|
|
1351
1782
|
},
|
|
1352
1783
|
pruneDiagnostics(predicate) {
|
|
1353
1784
|
let removed = 0;
|
|
@@ -1383,8 +1814,8 @@ export async function createLSPClient(options) {
|
|
|
1383
1814
|
getLaunchVariant() {
|
|
1384
1815
|
return state.launchVariant;
|
|
1385
1816
|
},
|
|
1386
|
-
async executeCommand(command, args) {
|
|
1387
|
-
return runServerCommand(state, command, args);
|
|
1817
|
+
async executeCommand(command, args, mutationContext) {
|
|
1818
|
+
return runServerCommand(state, command, args, EXECUTE_COMMAND_TIMEOUT_MS, mutationContext);
|
|
1388
1819
|
},
|
|
1389
1820
|
get diagnosticsVersion() {
|
|
1390
1821
|
return state.diagnosticsVersion;
|
|
@@ -1448,6 +1879,12 @@ export async function createLSPClient(options) {
|
|
|
1448
1879
|
isDocumentOpen(filePath) {
|
|
1449
1880
|
return state.openDocuments.has(normalizeMapKey(filePath));
|
|
1450
1881
|
},
|
|
1882
|
+
isBusy() {
|
|
1883
|
+
return (activeRequestsByConnection.get(connection) ?? 0) > 0;
|
|
1884
|
+
},
|
|
1885
|
+
getDocumentUri(filePath) {
|
|
1886
|
+
return state.openDocumentUris?.get(normalizeMapKey(filePath));
|
|
1887
|
+
},
|
|
1451
1888
|
async workspaceSymbol(query) {
|
|
1452
1889
|
if (!isClientAlive(state))
|
|
1453
1890
|
return [];
|
|
@@ -1487,8 +1924,9 @@ export async function createLSPClient(options) {
|
|
|
1487
1924
|
position: await toWirePosition(state, filePath, line, character),
|
|
1488
1925
|
newName,
|
|
1489
1926
|
}, filePath);
|
|
1490
|
-
return result
|
|
1927
|
+
return result ? await normalizeClientWorkspaceEdit(state, result) : null;
|
|
1491
1928
|
},
|
|
1929
|
+
closeDocument: (filePath) => closeDocument(state, filePath),
|
|
1492
1930
|
async willRenameFiles(oldFilePath, newFilePath) {
|
|
1493
1931
|
const result = await navRequest(state, "workspace/willRenameFiles", {
|
|
1494
1932
|
files: [
|
|
@@ -1498,16 +1936,16 @@ export async function createLSPClient(options) {
|
|
|
1498
1936
|
},
|
|
1499
1937
|
],
|
|
1500
1938
|
});
|
|
1501
|
-
return result
|
|
1939
|
+
return result ? await normalizeClientWorkspaceEdit(state, result) : null;
|
|
1502
1940
|
},
|
|
1503
|
-
async didRenameFiles(oldFilePath, newFilePath) {
|
|
1941
|
+
async didRenameFiles(oldFilePath, newFilePath, oldUri, newUri) {
|
|
1504
1942
|
if (!isClientAlive(state))
|
|
1505
1943
|
return;
|
|
1506
1944
|
await safeSendNotification(state.connection, "workspace/didRenameFiles", {
|
|
1507
1945
|
files: [
|
|
1508
1946
|
{
|
|
1509
|
-
oldUri: pathToFileURL(oldFilePath).href,
|
|
1510
|
-
newUri: pathToFileURL(newFilePath).href,
|
|
1947
|
+
oldUri: oldUri ?? pathToFileURL(oldFilePath).href,
|
|
1948
|
+
newUri: newUri ?? pathToFileURL(newFilePath).href,
|
|
1511
1949
|
},
|
|
1512
1950
|
],
|
|
1513
1951
|
});
|
|
@@ -1556,6 +1994,7 @@ async function safeSendNotification(connection, method, params) {
|
|
|
1556
1994
|
throw err;
|
|
1557
1995
|
}
|
|
1558
1996
|
}
|
|
1997
|
+
const activeRequestsByConnection = new WeakMap();
|
|
1559
1998
|
// Helper to safely send requests - catches stream destruction
|
|
1560
1999
|
async function safeSendRequest(connection, method, params,
|
|
1561
2000
|
// When provided, aborting the signal cancels the in-flight request via
|
|
@@ -1578,6 +2017,7 @@ signal) {
|
|
|
1578
2017
|
const send = () => tokenSource
|
|
1579
2018
|
? connection.sendRequest(method, params, tokenSource.token)
|
|
1580
2019
|
: connection.sendRequest(method, params);
|
|
2020
|
+
activeRequestsByConnection.set(connection, (activeRequestsByConnection.get(connection) ?? 0) + 1);
|
|
1581
2021
|
try {
|
|
1582
2022
|
// One safe retry on ContentModified (-32801): the document changed under
|
|
1583
2023
|
// us, so the server discarded the request. A single retry beats returning
|
|
@@ -1607,6 +2047,11 @@ signal) {
|
|
|
1607
2047
|
}
|
|
1608
2048
|
}
|
|
1609
2049
|
finally {
|
|
2050
|
+
const remaining = (activeRequestsByConnection.get(connection) ?? 1) - 1;
|
|
2051
|
+
if (remaining > 0)
|
|
2052
|
+
activeRequestsByConnection.set(connection, remaining);
|
|
2053
|
+
else
|
|
2054
|
+
activeRequestsByConnection.delete(connection);
|
|
1610
2055
|
if (signal && onAbort)
|
|
1611
2056
|
signal.removeEventListener("abort", onAbort);
|
|
1612
2057
|
tokenSource?.dispose();
|