@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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 +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -9,19 +9,22 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
|
-
import { access, readFile } from "node:fs/promises";
|
|
12
|
+
import { access, lstat, readFile } from "node:fs/promises";
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import { pathToFileURL } from "node:url";
|
|
15
15
|
import { emitBounded } from "../bounded-telemetry.js";
|
|
16
16
|
import { withTimeout } from "../deadline-utils.js";
|
|
17
|
-
import {
|
|
17
|
+
import { minimatch } from "../deps/minimatch.js";
|
|
18
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "../degradation-ledger.js";
|
|
18
19
|
// vscode-jsonrpc v9 ships an `exports` map exposing the Node entry as the
|
|
19
20
|
// `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
|
|
20
21
|
import { CancellationTokenSource, createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "../deps/vscode-jsonrpc.js";
|
|
21
22
|
import { logExtension } from "../extension-log.js";
|
|
22
23
|
import { recordLspChild, removeLspChild } from "../instance-registry.js";
|
|
24
|
+
import { workspaceDiagnosticsCacheSessionStart } from "./workspace-diagnostics-session.js";
|
|
23
25
|
import { logLatency } from "../latency-logger.js";
|
|
24
26
|
import { newLspMutationCorrelationId, } from "../lsp-mutation.js";
|
|
27
|
+
import { getProcessSingleton } from "../process-singletons.js";
|
|
25
28
|
import { getAmbientAbortSignal } from "../safe-spawn.js";
|
|
26
29
|
import { raceToCompletion } from "./aggregation.js";
|
|
27
30
|
import { hashDiagnosticContent, } from "./diagnostic-binding.js";
|
|
@@ -67,7 +70,7 @@ function extractSpawnMarker(args) {
|
|
|
67
70
|
return undefined;
|
|
68
71
|
}
|
|
69
72
|
// --- Constants ---
|
|
70
|
-
const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_INIT_TIMEOUT_MS", 15_000); // 15s — npx downloads are handled by ensureTool, not here
|
|
73
|
+
export const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_INIT_TIMEOUT_MS", 15_000); // 15s — npx downloads are handled by ensureTool, not here
|
|
71
74
|
/**
|
|
72
75
|
* The client capabilities advertised in every `initialize`. The textDocument set
|
|
73
76
|
* is intentionally COMPLETE and spec-compliant: servers built on
|
|
@@ -96,6 +99,11 @@ export const CLIENT_CAPABILITIES = {
|
|
|
96
99
|
workspaceFolders: true,
|
|
97
100
|
configuration: true,
|
|
98
101
|
didChangeWatchedFiles: { dynamicRegistration: true },
|
|
102
|
+
fileOperations: {
|
|
103
|
+
dynamicRegistration: false,
|
|
104
|
+
willRename: true,
|
|
105
|
+
didRename: true,
|
|
106
|
+
},
|
|
99
107
|
},
|
|
100
108
|
textDocument: {
|
|
101
109
|
synchronization: {
|
|
@@ -115,7 +123,27 @@ export const CLIENT_CAPABILITIES = {
|
|
|
115
123
|
implementation: { dynamicRegistration: false },
|
|
116
124
|
references: { dynamicRegistration: false },
|
|
117
125
|
documentSymbol: { dynamicRegistration: false },
|
|
118
|
-
codeAction: {
|
|
126
|
+
codeAction: {
|
|
127
|
+
dynamicRegistration: false,
|
|
128
|
+
dataSupport: true,
|
|
129
|
+
resolveSupport: { properties: ["edit", "command"] },
|
|
130
|
+
// #1971 review: without literal support, LSP 3.17 constrains
|
|
131
|
+
// textDocument/codeAction responses to Command[] — resolve of an edit
|
|
132
|
+
// would then be protocol-invalid. We parse CodeAction objects.
|
|
133
|
+
codeActionLiteralSupport: {
|
|
134
|
+
codeActionKind: {
|
|
135
|
+
valueSet: [
|
|
136
|
+
"quickfix",
|
|
137
|
+
"refactor",
|
|
138
|
+
"refactor.extract",
|
|
139
|
+
"refactor.inline",
|
|
140
|
+
"refactor.rewrite",
|
|
141
|
+
"source",
|
|
142
|
+
"source.organizeImports",
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
119
147
|
rename: { dynamicRegistration: false },
|
|
120
148
|
publishDiagnostics: {
|
|
121
149
|
relatedInformation: true,
|
|
@@ -253,6 +281,78 @@ function installCrashGuard() {
|
|
|
253
281
|
throw reason instanceof Error ? reason : new Error(String(reason));
|
|
254
282
|
});
|
|
255
283
|
}
|
|
284
|
+
/** #2065: bound both the path count and UTF-16 heap represented by retained
|
|
285
|
+
* Incremental-sync text. The map keeps the newest sent paths when evicting. */
|
|
286
|
+
export const MAX_INCREMENTAL_TEXT_RETAINED_ENTRIES = 128;
|
|
287
|
+
export const MAX_INCREMENTAL_TEXT_RETAINED_BYTES = 64 * 1024 * 1024;
|
|
288
|
+
/**
|
|
289
|
+
* #2065 fix round 1: a PROJECTION registry, not a second source of truth for
|
|
290
|
+
* "which clients are live" — its only job is letting `memory-sampler.ts`
|
|
291
|
+
* total retained-text bytes across clients without importing `LSPService`.
|
|
292
|
+
* Considered projecting straight off `LSPService.state.clients` (the
|
|
293
|
+
* manager's own client map, `clients/lsp/index.ts`) instead of keeping this
|
|
294
|
+
* Set at all, and rejected it for two reasons:
|
|
295
|
+
* 1. Import direction: `clients/lsp/index.ts` imports THIS file, not the
|
|
296
|
+
* reverse, so reading `LSPService.state.clients` from here (or from
|
|
297
|
+
* `memory-sampler.ts`, which every other snapshot in that module reaches
|
|
298
|
+
* by importing the owning subsystem directly, e.g.
|
|
299
|
+
* `getReviewGraphWorkspaceCacheSnapshot`, `getDispatchCascadeCacheStats`)
|
|
300
|
+
* means importing the higher-level manager from the lower-level client
|
|
301
|
+
* module — a layering inversion, not a cycle fix.
|
|
302
|
+
* 2. `LSPService.state.clients` is ITSELF only lazily reconciled against
|
|
303
|
+
* real client death — a crashed client's slot is noticed and evicted on
|
|
304
|
+
* the next `getClientForFile` attach, not at crash time (see
|
|
305
|
+
* `markExitedIfUnset`'s docstring above). Projecting off it would not
|
|
306
|
+
* give memory-sampler any more real-time accuracy than this Set; it
|
|
307
|
+
* would just move the same "who deregisters, and when" question onto a
|
|
308
|
+
* larger, higher-risk surface (the manager's routing map) for no gain.
|
|
309
|
+
*
|
|
310
|
+
* What makes this safe to keep as a second collection: it has exactly ONE
|
|
311
|
+
* lifecycle seam. `spawnClient` is the only writer that adds (below), and
|
|
312
|
+
* `disposeClientConnection` is the only writer that removes — and that
|
|
313
|
+
* function is itself the single convergence point every permanent-death path
|
|
314
|
+
* (`connection.onError`, `connection.onClose`, the process `exit` handler,
|
|
315
|
+
* and `clientShutdownOnce`'s `finally`) already funnels through, guarded by
|
|
316
|
+
* the same `connectionDisposed` idempotency flag that gates the connection
|
|
317
|
+
* teardown itself. Membership can therefore never drift from "has this
|
|
318
|
+
* client's connection been torn down" — there is nowhere else in the file
|
|
319
|
+
* that can flip a client from alive to permanently dead without going
|
|
320
|
+
* through `disposeClientConnection`.
|
|
321
|
+
*
|
|
322
|
+
* PROCESS-SCOPED, not module-scoped (#2130 criterion 2). This Set is what
|
|
323
|
+
* `memory_sample.subsystems.lsp.clients` counts, and a module-scope Set counted
|
|
324
|
+
* only the clients this MODULE EVALUATION spawned. pi evaluates the pi-lens
|
|
325
|
+
* graph up to nine times per process (#2146, measured `host_boot` = 9) and each
|
|
326
|
+
* evaluation builds its own `LSPService` and fleet (#2157 item 1), so a
|
|
327
|
+
* secondary root's servers — spawned by a later evaluation — were invisible to
|
|
328
|
+
* the sampler running in the first. That is the reported symptom exactly:
|
|
329
|
+
* `clients: 1` in `memory_sample` beside `lspChildCount: 2` in `instances.json`,
|
|
330
|
+
* with two tsservers alive. Sharing the Set through `getProcessSingleton` makes
|
|
331
|
+
* both the count and `roots` span every root the process serves.
|
|
332
|
+
*
|
|
333
|
+
* Both seams share it, which is the whole requirement: sharing only the add
|
|
334
|
+
* site would build a Set that never shrinks, a worse leak than the undercount.
|
|
335
|
+
*/
|
|
336
|
+
const ACTIVE_CLIENTS_FAMILY = "lsp-client.active-clients";
|
|
337
|
+
/** Bump when the cell's shape changes. */
|
|
338
|
+
const ACTIVE_CLIENTS_VERSION = 1;
|
|
339
|
+
function activeLspClientSet() {
|
|
340
|
+
return getProcessSingleton(ACTIVE_CLIENTS_FAMILY, ACTIVE_CLIENTS_VERSION, () => new Set());
|
|
341
|
+
}
|
|
342
|
+
export function getLspDocumentTextRetentionSnapshot() {
|
|
343
|
+
let entries = 0;
|
|
344
|
+
let bytes = 0;
|
|
345
|
+
const roots = new Set();
|
|
346
|
+
const active = activeLspClientSet();
|
|
347
|
+
for (const state of active) {
|
|
348
|
+
entries += state.incrementalTextRetainedEntries ?? 0;
|
|
349
|
+
bytes += state.incrementalTextRetainedBytes ?? 0;
|
|
350
|
+
if (typeof state.root === "string" && state.root.length > 0) {
|
|
351
|
+
roots.add(state.root);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { clients: active.size, entries, bytes, roots: roots.size };
|
|
355
|
+
}
|
|
256
356
|
function isClientAlive(state) {
|
|
257
357
|
return (state.isConnected && !state.isDestroyed && !state.lspProcess.process.killed);
|
|
258
358
|
}
|
|
@@ -260,6 +360,13 @@ function disposeClientConnection(state) {
|
|
|
260
360
|
if (state.connectionDisposed)
|
|
261
361
|
return;
|
|
262
362
|
state.connectionDisposed = true;
|
|
363
|
+
// #2065 fix round 1: this is the ONE place every permanent-death path
|
|
364
|
+
// converges (onError, onClose, the process 'exit' handler, and
|
|
365
|
+
// clientShutdownOnce's finally) — see setupConnectionLifecycle below and
|
|
366
|
+
// clientShutdownOnce. Deregistering here, guarded by the idempotency flag
|
|
367
|
+
// above, means a crash that never re-attaches still frees its retained
|
|
368
|
+
// text instead of pinning it for the rest of the process lifetime.
|
|
369
|
+
activeLspClientSet().delete(state);
|
|
263
370
|
try {
|
|
264
371
|
state.connection.dispose();
|
|
265
372
|
}
|
|
@@ -788,6 +895,50 @@ function logTypeScriptPullSettle(state, normalizedPath, durationMs, pullSettleSo
|
|
|
788
895
|
},
|
|
789
896
|
});
|
|
790
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* #2066: the ONE full-document walk a send needs, replacing the two that used
|
|
900
|
+
* to run independently — a `charCodeAt` loop counting `\n` for the
|
|
901
|
+
* `lsp_document_send` line count, and a `.split(/\r\n|\r|\n/)` in
|
|
902
|
+
* `buildContentChanges` that materialized one substring per line to read the
|
|
903
|
+
* last one. The #1095 sha256 is a third walk and stays; it is native, and it
|
|
904
|
+
* is load-bearing.
|
|
905
|
+
*
|
|
906
|
+
* The two counts here are deliberately NOT the same number and must not be
|
|
907
|
+
* collapsed into one. `lfNewlineCount` counts `\n` only, because its consumer
|
|
908
|
+
* pairs against `diagnostic_past_eof`, whose gate counts LF BYTES
|
|
909
|
+
* (`clients/diagnostic-line-freshness.ts`); merging the conventions would put
|
|
910
|
+
* the two records one apart on exactly the mixed-ending documents the pairing
|
|
911
|
+
* exists to debug. `lastLine` uses the full LSP terminator set (`\r\n`, lone
|
|
912
|
+
* `\r`, `\n` — #1669 review F6), because a CRLF or classic-Mac document whose
|
|
913
|
+
* terminators went uncounted computes a range ending mid-document.
|
|
914
|
+
*/
|
|
915
|
+
function scanSentContent(content) {
|
|
916
|
+
let lfNewlineCount = 0;
|
|
917
|
+
let index = 0;
|
|
918
|
+
let start = 0;
|
|
919
|
+
// `charCodeAt`, not `codePointAt` (typescript:S7758, accepted): both are
|
|
920
|
+
// compared against 10/13, which no surrogate unit or astral code point can
|
|
921
|
+
// equal, so they are interchangeable here — but `codePointAt` measured 8%
|
|
922
|
+
// slower on a 400 KiB LF document (0.716 vs 0.663 ms/scan), because every
|
|
923
|
+
// ordinary character pays a surrogate-pair check this loop never uses.
|
|
924
|
+
for (let i = 0; i < content.length; i++) {
|
|
925
|
+
const code = content.charCodeAt(i);
|
|
926
|
+
if (code !== 10 && code !== 13)
|
|
927
|
+
continue;
|
|
928
|
+
if (code === 10) {
|
|
929
|
+
lfNewlineCount++;
|
|
930
|
+
}
|
|
931
|
+
else if (content.charCodeAt(i + 1) === 10) {
|
|
932
|
+
// `\r\n` is ONE line terminator, and still carries the `\n` the
|
|
933
|
+
// LF-only count wants.
|
|
934
|
+
lfNewlineCount++;
|
|
935
|
+
i++;
|
|
936
|
+
}
|
|
937
|
+
index++;
|
|
938
|
+
start = i + 1;
|
|
939
|
+
}
|
|
940
|
+
return { lfNewlineCount, lastLine: { index, start } };
|
|
941
|
+
}
|
|
791
942
|
/**
|
|
792
943
|
* #1095: fingerprint the EXACT didOpen/didChange payload text at SEND time and
|
|
793
944
|
* tag it with the document version it was sent as, so a later
|
|
@@ -797,16 +948,84 @@ function logTypeScriptPullSettle(state, normalizedPath, durationMs, pullSettleSo
|
|
|
797
948
|
* caller already applies to the content it hands us.
|
|
798
949
|
*/
|
|
799
950
|
function recordSentContent(state, normalizedPath, version, content) {
|
|
951
|
+
const scan = scanSentContent(content);
|
|
952
|
+
const previous = state.documentContentHashes.get(normalizedPath);
|
|
953
|
+
if (previous && previous.version > version) {
|
|
954
|
+
incrementDegradationCount({
|
|
955
|
+
kind: "lsp-document-send-order",
|
|
956
|
+
subject: `${state.serverId}:${normalizedPath}`,
|
|
957
|
+
reason: `recorded version ${version} after newer version ${previous.version}`,
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (previous?.text !== undefined) {
|
|
962
|
+
state.incrementalTextRetainedEntries = Math.max(0, (state.incrementalTextRetainedEntries ?? 1) - 1);
|
|
963
|
+
state.incrementalTextRetainedBytes = Math.max(0, (state.incrementalTextRetainedBytes ?? previous.text.length * 2) -
|
|
964
|
+
previous.text.length * 2);
|
|
965
|
+
// #2065 fix round 1 F5: the path is about to be rewritten below (and
|
|
966
|
+
// possibly re-marked text-bearing) — drop the stale membership first so
|
|
967
|
+
// the reinsert further down lands at the END of the recency order
|
|
968
|
+
// instead of leaving a duplicate-free but stale position.
|
|
969
|
+
state.incrementalTextBearingPaths?.delete(normalizedPath);
|
|
970
|
+
}
|
|
971
|
+
// #2065 fix round 1 F5: eviction recency now lives in
|
|
972
|
+
// `incrementalTextBearingPaths` (above/below), not this map's own
|
|
973
|
+
// insertion order — no remaining reader iterates `documentContentHashes`
|
|
974
|
+
// for order (every other site below is a keyed `.get`/`.delete`). The
|
|
975
|
+
// reinsert is kept anyway so a log or future debug dump that DOES walk
|
|
976
|
+
// this map in insertion order still reads "most recently sent last",
|
|
977
|
+
// matching the aux set it mirrors, rather than a stale mixed order.
|
|
978
|
+
state.documentContentHashes.delete(normalizedPath);
|
|
800
979
|
state.documentContentHashes.set(normalizedPath, {
|
|
801
980
|
version,
|
|
802
981
|
hash: hashDiagnosticContent(content),
|
|
803
982
|
// #1669: retain the full text only for Incremental — the sole reader
|
|
804
983
|
// (`buildContentChanges`) needs it to compute the NEXT change against
|
|
805
984
|
// what the server last saw; Full/None never read this field.
|
|
985
|
+
// #2066: `lastLine` is that reader's answer, already computed by the
|
|
986
|
+
// scan above — it rides along with the text it describes.
|
|
806
987
|
...(state.syncKind === TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL && {
|
|
807
988
|
text: content,
|
|
989
|
+
lastLine: scan.lastLine,
|
|
808
990
|
}),
|
|
809
991
|
});
|
|
992
|
+
if (state.syncKind === TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL) {
|
|
993
|
+
state.incrementalTextRetainedEntries =
|
|
994
|
+
(state.incrementalTextRetainedEntries ?? 0) + 1;
|
|
995
|
+
state.incrementalTextRetainedBytes =
|
|
996
|
+
(state.incrementalTextRetainedBytes ?? 0) + content.length * 2;
|
|
997
|
+
// #2065 fix round 1 F5: track membership in the same insertion (=
|
|
998
|
+
// recency) order as `documentContentHashes` itself, so eviction below
|
|
999
|
+
// never needs to spread and scan the full content-binding map —
|
|
1000
|
+
// including paths whose text was already stripped — just to find the
|
|
1001
|
+
// oldest text-bearing one.
|
|
1002
|
+
if (!state.incrementalTextBearingPaths) {
|
|
1003
|
+
state.incrementalTextBearingPaths = new Set();
|
|
1004
|
+
}
|
|
1005
|
+
state.incrementalTextBearingPaths.add(normalizedPath);
|
|
1006
|
+
while ((state.incrementalTextRetainedEntries ?? 0) >
|
|
1007
|
+
MAX_INCREMENTAL_TEXT_RETAINED_ENTRIES ||
|
|
1008
|
+
(state.incrementalTextRetainedBytes ?? 0) >
|
|
1009
|
+
MAX_INCREMENTAL_TEXT_RETAINED_BYTES) {
|
|
1010
|
+
const oldestPath = state.incrementalTextBearingPaths
|
|
1011
|
+
?.values()
|
|
1012
|
+
.next().value;
|
|
1013
|
+
if (oldestPath === undefined)
|
|
1014
|
+
break;
|
|
1015
|
+
const binding = state.documentContentHashes.get(oldestPath);
|
|
1016
|
+
state.incrementalTextBearingPaths.delete(oldestPath);
|
|
1017
|
+
if (!binding || binding.text === undefined)
|
|
1018
|
+
continue;
|
|
1019
|
+
// #2066: `lastLine` is dropped here with the text it describes. The
|
|
1020
|
+
// #1095 version/hash binding is what must survive the strip.
|
|
1021
|
+
state.documentContentHashes.set(oldestPath, {
|
|
1022
|
+
version: binding.version,
|
|
1023
|
+
hash: binding.hash,
|
|
1024
|
+
});
|
|
1025
|
+
state.incrementalTextRetainedEntries = Math.max(0, (state.incrementalTextRetainedEntries ?? 1) - 1);
|
|
1026
|
+
state.incrementalTextRetainedBytes = Math.max(0, (state.incrementalTextRetainedBytes ?? 0) - binding.text.length * 2);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
810
1029
|
// #1641 criterion 3: the in-memory document's version + content length AT
|
|
811
1030
|
// SEND TIME, so a later "diagnostic cited a line past current disk EOF"
|
|
812
1031
|
// record (`diagnostic_past_eof`, clients/diagnostic-line-freshness.ts) can be
|
|
@@ -818,14 +1037,9 @@ function recordSentContent(state, normalizedPath, version, content) {
|
|
|
818
1037
|
// convention as the gate itself (newline count + 1 — a trailing `\n` adds
|
|
819
1038
|
// one more, empty, addressable line; an empty document is still 1 line),
|
|
820
1039
|
// or the two records disagree by one at exactly the boundary this counter
|
|
821
|
-
// exists to help debug.
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
let newlineCount = 0;
|
|
825
|
-
for (let i = 0; i < content.length; i++) {
|
|
826
|
-
if (content.charCodeAt(i) === 10)
|
|
827
|
-
newlineCount++;
|
|
828
|
-
}
|
|
1040
|
+
// exists to help debug. #2066: the count comes from `scanSentContent`
|
|
1041
|
+
// above, which folded this loop into the walk `buildContentChanges` was
|
|
1042
|
+
// already paying for; the LF-only convention is unchanged.
|
|
829
1043
|
logLatency({
|
|
830
1044
|
type: "phase",
|
|
831
1045
|
phase: "lsp_document_send",
|
|
@@ -834,7 +1048,7 @@ function recordSentContent(state, normalizedPath, version, content) {
|
|
|
834
1048
|
metadata: {
|
|
835
1049
|
version,
|
|
836
1050
|
contentLength: content.length,
|
|
837
|
-
contentLineCount:
|
|
1051
|
+
contentLineCount: scan.lfNewlineCount + 1,
|
|
838
1052
|
},
|
|
839
1053
|
});
|
|
840
1054
|
}
|
|
@@ -864,23 +1078,24 @@ function buildContentChanges(state, normalizedPath, content) {
|
|
|
864
1078
|
if (state.syncKind !== TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL) {
|
|
865
1079
|
return [{ text: content }];
|
|
866
1080
|
}
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
1081
|
+
const previous = state.documentContentHashes.get(normalizedPath);
|
|
1082
|
+
if (previous?.text === undefined) {
|
|
869
1083
|
return [{ text: content }];
|
|
870
1084
|
}
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
1085
|
+
const previousText = previous.text;
|
|
1086
|
+
// #2066: `recordSentContent` scanned this exact text when it sent it, so
|
|
1087
|
+
// read where it left the last line instead of walking (and splitting) the
|
|
1088
|
+
// document a second time. An entry carrying text without that position was
|
|
1089
|
+
// written by something else — resolve it through the SAME scanner, never a
|
|
1090
|
+
// second convention that can drift from it.
|
|
1091
|
+
const lastLine = previous.lastLine ?? scanSentContent(previousText).lastLine;
|
|
1092
|
+
const lastLineText = previousText.slice(lastLine.start);
|
|
878
1093
|
return [
|
|
879
1094
|
{
|
|
880
1095
|
range: {
|
|
881
1096
|
start: { line: 0, character: 0 },
|
|
882
1097
|
end: {
|
|
883
|
-
line: lastLine,
|
|
1098
|
+
line: lastLine.index,
|
|
884
1099
|
character: convertCharacterOffset(state.positionEncoding, lastLineText, lastLineText.length),
|
|
885
1100
|
},
|
|
886
1101
|
},
|
|
@@ -2332,10 +2547,9 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
2332
2547
|
},
|
|
2333
2548
|
});
|
|
2334
2549
|
}
|
|
2335
|
-
|
|
2550
|
+
async function handleNotifyChangeOnce(state, filePath, content, normalizedPath) {
|
|
2336
2551
|
if (!isClientAlive(state))
|
|
2337
2552
|
return;
|
|
2338
|
-
const normalizedPath = normalizeMapKey(filePath);
|
|
2339
2553
|
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
2340
2554
|
if (!state.openDocuments.has(normalizedPath)) {
|
|
2341
2555
|
// Safety fallback: keep protocol ordering valid even if caller sends
|
|
@@ -2369,6 +2583,27 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
2369
2583
|
if (changeSent)
|
|
2370
2584
|
recordSentContent(state, normalizedPath, version, content);
|
|
2371
2585
|
}
|
|
2586
|
+
/**
|
|
2587
|
+
* #2113: serialize the read/build/send/record transaction per document. The
|
|
2588
|
+
* content-change payload must be built against the content recorded by the
|
|
2589
|
+
* preceding send for this path; unrelated paths retain parallel sends.
|
|
2590
|
+
*/
|
|
2591
|
+
export function handleNotifyChange(state, filePath, content) {
|
|
2592
|
+
if (!isClientAlive(state))
|
|
2593
|
+
return Promise.resolve();
|
|
2594
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
2595
|
+
const previous = state.notifyChangeQueues.get(normalizedPath) ?? Promise.resolve();
|
|
2596
|
+
const queued = previous
|
|
2597
|
+
.catch(() => undefined)
|
|
2598
|
+
.then(() => handleNotifyChangeOnce(state, filePath, content, normalizedPath));
|
|
2599
|
+
const settled = queued.finally(() => {
|
|
2600
|
+
if (state.notifyChangeQueues.get(normalizedPath) === settled) {
|
|
2601
|
+
state.notifyChangeQueues.delete(normalizedPath);
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
state.notifyChangeQueues.set(normalizedPath, settled);
|
|
2605
|
+
return settled;
|
|
2606
|
+
}
|
|
2372
2607
|
/** Close a document through the same lifecycle path exposed by the client. */
|
|
2373
2608
|
export async function closeDocument(state, filePath) {
|
|
2374
2609
|
if (!isClientAlive(state))
|
|
@@ -2393,7 +2628,44 @@ export async function closeDocument(state, filePath) {
|
|
|
2393
2628
|
// harmless and cheap) — mirror openDocuments' own per-close cleanup so it
|
|
2394
2629
|
// doesn't grow unbounded across a long session's worth of open/close churn.
|
|
2395
2630
|
state.projectIdentityProbedFiles?.delete(normalizedPath);
|
|
2631
|
+
const retained = state.documentContentHashes.get(normalizedPath);
|
|
2632
|
+
if (retained?.text !== undefined) {
|
|
2633
|
+
state.incrementalTextRetainedEntries = Math.max(0, (state.incrementalTextRetainedEntries ?? 1) - 1);
|
|
2634
|
+
state.incrementalTextRetainedBytes = Math.max(0, (state.incrementalTextRetainedBytes ?? retained.text.length * 2) -
|
|
2635
|
+
retained.text.length * 2);
|
|
2636
|
+
// #2065 fix round 2 N1: `recordSentContent` and the eviction loop both
|
|
2637
|
+
// delete from `incrementalTextBearingPaths` when they strip a path's
|
|
2638
|
+
// text, but neither runs on close — this was the missing third writer.
|
|
2639
|
+
// Without it, a closed path's membership in the aux set survives
|
|
2640
|
+
// forever: below the 128-entry/64 MiB cap the eviction loop that would
|
|
2641
|
+
// otherwise clean it up never runs at all, so the set grows by one
|
|
2642
|
+
// stale entry per open/close cycle with nothing ever draining it — the
|
|
2643
|
+
// exact unbounded-per-path-store class #2065 exists to close, just
|
|
2644
|
+
// moved into the aux index instead of the byte-retention count itself.
|
|
2645
|
+
state.incrementalTextBearingPaths?.delete(normalizedPath);
|
|
2646
|
+
}
|
|
2647
|
+
// Keep the hash/version binding only while the document is open; the full
|
|
2648
|
+
// text is needed for the next edit, never after didClose (#2065).
|
|
2649
|
+
state.documentContentHashes.delete(normalizedPath);
|
|
2396
2650
|
clearDiagnosticsForPath(state, normalizedPath);
|
|
2651
|
+
// #2065 fix round 2 (reverts fix round 1 F4): `pullGenerations` is the
|
|
2652
|
+
// eighth per-path map in this family, but it is deliberately NOT cleared
|
|
2653
|
+
// here — the same reasoning `clearDiagnosticsForPath` already documents
|
|
2654
|
+
// above for `pullRequestSequences` applies to this counter too, and fix
|
|
2655
|
+
// round 1's F4 missed it. `pullGenerationFor` defaults a missing entry to
|
|
2656
|
+
// `0`, so deleting the entry on close resets the counter. A pull already
|
|
2657
|
+
// in flight before the close captured its generation BEFORE this close's
|
|
2658
|
+
// `clearDiagnosticsForPath` bump above ran. If the document is closed and
|
|
2659
|
+
// reopened before that in-flight pull's write lands, deleting the entry
|
|
2660
|
+
// here would let the counter read back as `0` again — the exact value the
|
|
2661
|
+
// stale write's stale captured generation equals — so the write-time
|
|
2662
|
+
// `!== generation` check at its call site would wrongly accept a stale
|
|
2663
|
+
// answer computed against the pre-close content. Leaving the counter
|
|
2664
|
+
// monotonic and un-reset keeps every pre-close capture permanently
|
|
2665
|
+
// distinguishable from whatever the counter reads afterward, no matter how
|
|
2666
|
+
// many closes or reopens happen in between. The cost is one integer per
|
|
2667
|
+
// path that outlives the document's open lifetime — negligible next to
|
|
2668
|
+
// correctness here.
|
|
2397
2669
|
}
|
|
2398
2670
|
/**
|
|
2399
2671
|
* #1620 residual review F1: `fast` and `processExiting` only ever escalate —
|
|
@@ -2532,6 +2804,9 @@ async function clientShutdownOnce(state, options) {
|
|
|
2532
2804
|
}
|
|
2533
2805
|
}
|
|
2534
2806
|
finally {
|
|
2807
|
+
// #2065 fix round 1: deregistration now lives in disposeClientConnection
|
|
2808
|
+
// itself (the convergence point for every crash/dispose path, not just
|
|
2809
|
+
// this graceful one) — see its comment.
|
|
2535
2810
|
disposeClientConnection(state);
|
|
2536
2811
|
const pid = state.lspProcess.pid;
|
|
2537
2812
|
logLatency({
|
|
@@ -3060,6 +3335,14 @@ async function resolveCodeActionBestEffort(state, action) {
|
|
|
3060
3335
|
edit: await normalizeClientWorkspaceEdit(state, action.edit),
|
|
3061
3336
|
};
|
|
3062
3337
|
}
|
|
3338
|
+
if (!state.operationSupport.codeActionResolve) {
|
|
3339
|
+
recordDegradationOnce({
|
|
3340
|
+
kind: "lsp-capability-skip",
|
|
3341
|
+
subject: `${state.serverId}:codeAction/resolve`,
|
|
3342
|
+
reason: "server did not advertise codeActionProvider.resolveProvider",
|
|
3343
|
+
});
|
|
3344
|
+
return action;
|
|
3345
|
+
}
|
|
3063
3346
|
let resolved;
|
|
3064
3347
|
try {
|
|
3065
3348
|
resolved = await withTimeout(safeSendRequest(state.connection, "codeAction/resolve", action), NAV_REQUEST_TIMEOUT_MS);
|
|
@@ -3082,7 +3365,7 @@ async function resolveCodeActionBestEffort(state, action) {
|
|
|
3082
3365
|
// --- Client Factory ---
|
|
3083
3366
|
export async function createLSPClient(options) {
|
|
3084
3367
|
installCrashGuard();
|
|
3085
|
-
const { serverId, process: lspProcess, root, initialization, initializeTimeoutMs = INITIALIZE_TIMEOUT_MS, launchVariant, } = options;
|
|
3368
|
+
const { serverId, process: lspProcess, root, sessionCwd, initialization, initializeTimeoutMs = INITIALIZE_TIMEOUT_MS, launchVariant, } = options;
|
|
3086
3369
|
// #449/#472: register this LSP child in the cross-process instance registry
|
|
3087
3370
|
// as soon as we have a live pid — BEFORE `initialize` completes, not after.
|
|
3088
3371
|
// Registering early means a child that dies/hangs during initialize (the
|
|
@@ -3095,6 +3378,11 @@ export async function createLSPClient(options) {
|
|
|
3095
3378
|
serverId,
|
|
3096
3379
|
command: lspProcess.command,
|
|
3097
3380
|
marker: extractSpawnMarker(lspProcess.args),
|
|
3381
|
+
sessionIdentity: {
|
|
3382
|
+
projectRoot: sessionCwd ?? process.cwd(),
|
|
3383
|
+
rootSource: sessionCwd ? "service-cwd" : "lsp-fallback",
|
|
3384
|
+
startedAt: new Date(workspaceDiagnosticsCacheSessionStart()).toISOString(),
|
|
3385
|
+
},
|
|
3098
3386
|
}).catch((err) => {
|
|
3099
3387
|
// best-effort observability — never fail LSP startup over this
|
|
3100
3388
|
logLatency({
|
|
@@ -3204,8 +3492,12 @@ export async function createLSPClient(options) {
|
|
|
3204
3492
|
diagnosticsVersion: 0,
|
|
3205
3493
|
diagnosticsVersionsByPath: new Map(),
|
|
3206
3494
|
documentVersions: new Map(),
|
|
3495
|
+
notifyChangeQueues: new Map(),
|
|
3207
3496
|
diagnosticDocVersions: new Map(),
|
|
3208
3497
|
documentContentHashes: new Map(),
|
|
3498
|
+
incrementalTextRetainedEntries: 0,
|
|
3499
|
+
incrementalTextBearingPaths: new Set(),
|
|
3500
|
+
incrementalTextRetainedBytes: 0,
|
|
3209
3501
|
diagnosticBindings: new Map(),
|
|
3210
3502
|
pullResultIds: new Map(),
|
|
3211
3503
|
documentPullDiagnosticsBySource: new Map(),
|
|
@@ -3238,6 +3530,7 @@ export async function createLSPClient(options) {
|
|
|
3238
3530
|
// SAFETY: state construction completes before the flush closure can run.
|
|
3239
3531
|
watchQueue: undefined,
|
|
3240
3532
|
};
|
|
3533
|
+
activeLspClientSet().add(state);
|
|
3241
3534
|
// #271: batch per-file workspace/didChangeWatchedFiles into one notification
|
|
3242
3535
|
// per debounce window, so an N-file turn re-indexes the server once, not N×.
|
|
3243
3536
|
state.watchQueue = new WatchedFilesQueue((changes) => {
|
|
@@ -3340,6 +3633,31 @@ export async function createLSPClient(options) {
|
|
|
3340
3633
|
state.workspaceDiagnosticsSupport =
|
|
3341
3634
|
detectWorkspaceDiagnosticsSupport(initResult);
|
|
3342
3635
|
state.operationSupport = detectOperationSupport(initResult);
|
|
3636
|
+
const capabilities = (initResult
|
|
3637
|
+
?.capabilities ?? {});
|
|
3638
|
+
const workspace = isCapabilityRecord(capabilities.workspace)
|
|
3639
|
+
? capabilities.workspace
|
|
3640
|
+
: undefined;
|
|
3641
|
+
const fileOperations = isCapabilityRecord(workspace)
|
|
3642
|
+
? workspace.fileOperations
|
|
3643
|
+
: undefined;
|
|
3644
|
+
const malformedFileOperationRegistrations = new Set();
|
|
3645
|
+
if (isCapabilityRecord(fileOperations)) {
|
|
3646
|
+
for (const operation of ["willRename", "didRename"]) {
|
|
3647
|
+
if (fileOperations[operation] !== undefined &&
|
|
3648
|
+
parseFileOperationFilters(fileOperations[operation]) === undefined) {
|
|
3649
|
+
malformedFileOperationRegistrations.add(operation);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
state.fileOperationMalformedRegistrations =
|
|
3654
|
+
malformedFileOperationRegistrations;
|
|
3655
|
+
if (isCapabilityRecord(fileOperations)) {
|
|
3656
|
+
state.fileOperationFilters = {
|
|
3657
|
+
willRename: parseFileOperationFilters(fileOperations.willRename),
|
|
3658
|
+
didRename: parseFileOperationFilters(fileOperations.didRename),
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3343
3661
|
state.positionEncoding = negotiatePositionEncoding(initResult?.capabilities);
|
|
3344
3662
|
state.syncKind = negotiateSyncKind(initResult?.capabilities);
|
|
3345
3663
|
state.rawCapabilityKeys = Object.keys(initResult?.capabilities ??
|
|
@@ -3444,6 +3762,9 @@ export async function createLSPClient(options) {
|
|
|
3444
3762
|
getOperationSupport() {
|
|
3445
3763
|
return state.operationSupport;
|
|
3446
3764
|
},
|
|
3765
|
+
getMalformedFileOperationRegistrations() {
|
|
3766
|
+
return state.fileOperationMalformedRegistrations ?? new Set();
|
|
3767
|
+
},
|
|
3447
3768
|
getAdvertisedCommands() {
|
|
3448
3769
|
return [...state.advertisedCommands];
|
|
3449
3770
|
},
|
|
@@ -3570,11 +3891,26 @@ export async function createLSPClient(options) {
|
|
|
3570
3891
|
},
|
|
3571
3892
|
closeDocument: (filePath) => closeDocument(state, filePath),
|
|
3572
3893
|
async willRenameFiles(oldFilePath, newFilePath) {
|
|
3894
|
+
if (!isClientAlive(state))
|
|
3895
|
+
return null;
|
|
3896
|
+
// #1971 review: the server registered WHICH paths it cares about.
|
|
3897
|
+
// Sending outside those filters asks a question the server never
|
|
3898
|
+
// signed up to answer.
|
|
3899
|
+
const oldUri = pathToFileURL(oldFilePath).href;
|
|
3900
|
+
const newUri = pathToFileURL(newFilePath).href;
|
|
3901
|
+
if (!(await fileOperationFiltersMatch(state.fileOperationFilters?.willRename, oldUri, newUri, oldFilePath, newFilePath))) {
|
|
3902
|
+
recordDegradationOnce({
|
|
3903
|
+
kind: "lsp-capability-skip",
|
|
3904
|
+
subject: `${state.serverId}:workspace/willRenameFiles:filter`,
|
|
3905
|
+
reason: "filter-mismatch",
|
|
3906
|
+
});
|
|
3907
|
+
return null;
|
|
3908
|
+
}
|
|
3573
3909
|
const result = await navRequest(state, "workspace/willRenameFiles", {
|
|
3574
3910
|
files: [
|
|
3575
3911
|
{
|
|
3576
|
-
oldUri
|
|
3577
|
-
newUri
|
|
3912
|
+
oldUri,
|
|
3913
|
+
newUri,
|
|
3578
3914
|
},
|
|
3579
3915
|
],
|
|
3580
3916
|
});
|
|
@@ -3583,11 +3919,28 @@ export async function createLSPClient(options) {
|
|
|
3583
3919
|
async didRenameFiles(oldFilePath, newFilePath, oldUri, newUri) {
|
|
3584
3920
|
if (!isClientAlive(state))
|
|
3585
3921
|
return;
|
|
3922
|
+
// `fileOperationFilters.didRename` is parsed by the same predicate that
|
|
3923
|
+
// derives operationSupport.didRenameFiles (see
|
|
3924
|
+
// isFileOperationRegistrationOptions). Thus filters undefined iff the
|
|
3925
|
+
// capability is absent; the service-level gate remains the enforcement
|
|
3926
|
+
// point for aggregated dispatch.
|
|
3927
|
+
// #1971 review: same registration-filter discipline as willRename —
|
|
3928
|
+
// the server only signed up to hear about matching paths.
|
|
3929
|
+
const wireOldUri = oldUri ?? pathToFileURL(oldFilePath).href;
|
|
3930
|
+
const wireNewUri = newUri ?? pathToFileURL(newFilePath).href;
|
|
3931
|
+
if (!(await fileOperationFiltersMatch(state.fileOperationFilters?.didRename, wireOldUri, wireNewUri, oldFilePath, newFilePath))) {
|
|
3932
|
+
recordDegradationOnce({
|
|
3933
|
+
kind: "lsp-capability-skip",
|
|
3934
|
+
subject: `${state.serverId}:workspace/didRenameFiles:filter`,
|
|
3935
|
+
reason: "filter-mismatch",
|
|
3936
|
+
});
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3586
3939
|
await safeSendNotification(state.connection, "workspace/didRenameFiles", {
|
|
3587
3940
|
files: [
|
|
3588
3941
|
{
|
|
3589
|
-
oldUri:
|
|
3590
|
-
newUri:
|
|
3942
|
+
oldUri: wireOldUri,
|
|
3943
|
+
newUri: wireNewUri,
|
|
3591
3944
|
},
|
|
3592
3945
|
],
|
|
3593
3946
|
});
|
|
@@ -3818,8 +4171,13 @@ function detectOperationSupport(initResult) {
|
|
|
3818
4171
|
return false;
|
|
3819
4172
|
if (typeof value === "boolean")
|
|
3820
4173
|
return value;
|
|
3821
|
-
return
|
|
4174
|
+
return isCapabilityRecord(value);
|
|
3822
4175
|
};
|
|
4176
|
+
const codeActionProvider = capabilities?.codeActionProvider;
|
|
4177
|
+
const workspace = capabilities?.workspace;
|
|
4178
|
+
const fileOperations = isCapabilityRecord(workspace)
|
|
4179
|
+
? workspace.fileOperations
|
|
4180
|
+
: undefined;
|
|
3823
4181
|
return {
|
|
3824
4182
|
definition: hasProvider("definitionProvider"),
|
|
3825
4183
|
typeDefinition: hasProvider("typeDefinitionProvider"),
|
|
@@ -3830,8 +4188,135 @@ function detectOperationSupport(initResult) {
|
|
|
3830
4188
|
documentSymbol: hasProvider("documentSymbolProvider"),
|
|
3831
4189
|
workspaceSymbol: hasProvider("workspaceSymbolProvider"),
|
|
3832
4190
|
codeAction: hasProvider("codeActionProvider"),
|
|
4191
|
+
codeActionResolve: isCapabilityRecord(codeActionProvider) &&
|
|
4192
|
+
codeActionProvider.resolveProvider === true,
|
|
3833
4193
|
rename: hasProvider("renameProvider"),
|
|
4194
|
+
willRenameFiles: isCapabilityRecord(fileOperations) &&
|
|
4195
|
+
isFileOperationRegistrationOptions(fileOperations.willRename),
|
|
4196
|
+
didRenameFiles: isCapabilityRecord(fileOperations) &&
|
|
4197
|
+
isFileOperationRegistrationOptions(fileOperations.didRename),
|
|
3834
4198
|
implementation: hasProvider("implementationProvider"),
|
|
3835
4199
|
callHierarchy: hasProvider("callHierarchyProvider"),
|
|
3836
4200
|
};
|
|
3837
4201
|
}
|
|
4202
|
+
function isCapabilityRecord(value) {
|
|
4203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4204
|
+
}
|
|
4205
|
+
function isFileOperationRegistrationOptions(value) {
|
|
4206
|
+
return parseFileOperationFilters(value) !== undefined;
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Parse the filters of a `FileOperationRegistrationOptions` object. Returns
|
|
4210
|
+
* `undefined` when the options are absent or malformed — callers treat that as
|
|
4211
|
+
* "no interest declared" and fail closed, never sending the operation.
|
|
4212
|
+
*/
|
|
4213
|
+
function parseFileOperationFilters(value) {
|
|
4214
|
+
if (!isCapabilityRecord(value) || !Array.isArray(value.filters)) {
|
|
4215
|
+
return undefined;
|
|
4216
|
+
}
|
|
4217
|
+
const filters = [];
|
|
4218
|
+
for (const entry of value.filters) {
|
|
4219
|
+
if (!isCapabilityRecord(entry))
|
|
4220
|
+
return undefined;
|
|
4221
|
+
const pattern = entry.pattern;
|
|
4222
|
+
if (!isCapabilityRecord(pattern) ||
|
|
4223
|
+
typeof pattern.glob !== "string" ||
|
|
4224
|
+
pattern.glob.trim() === "") {
|
|
4225
|
+
return undefined;
|
|
4226
|
+
}
|
|
4227
|
+
if (entry.scheme !== undefined &&
|
|
4228
|
+
(typeof entry.scheme !== "string" ||
|
|
4229
|
+
entry.scheme.trim() === "" ||
|
|
4230
|
+
!/^[A-Za-z][A-Za-z0-9+.-]*$/.test(entry.scheme))) {
|
|
4231
|
+
return undefined;
|
|
4232
|
+
}
|
|
4233
|
+
const matches = pattern.matches;
|
|
4234
|
+
if (matches !== undefined && matches !== "file" && matches !== "folder") {
|
|
4235
|
+
return undefined;
|
|
4236
|
+
}
|
|
4237
|
+
if (pattern.options !== undefined && !isCapabilityRecord(pattern.options)) {
|
|
4238
|
+
return undefined;
|
|
4239
|
+
}
|
|
4240
|
+
const options = isCapabilityRecord(pattern.options)
|
|
4241
|
+
? pattern.options
|
|
4242
|
+
: undefined;
|
|
4243
|
+
if (options?.ignoreCase !== undefined &&
|
|
4244
|
+
typeof options.ignoreCase !== "boolean") {
|
|
4245
|
+
return undefined;
|
|
4246
|
+
}
|
|
4247
|
+
filters.push({
|
|
4248
|
+
scheme: entry.scheme?.toLowerCase(),
|
|
4249
|
+
glob: pattern.glob,
|
|
4250
|
+
matches: matches === "file" || matches === "folder" ? matches : undefined,
|
|
4251
|
+
ignoreCase: options?.ignoreCase === true,
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
return filters;
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4257
|
+
* Whether ANY parsed filter expresses interest in either path of a rename.
|
|
4258
|
+
* Conservative by design: unsupported URI schemes, an unresolved explicit
|
|
4259
|
+
* file/folder kind, malformed URIs, and zero filters all match nothing.
|
|
4260
|
+
*/
|
|
4261
|
+
async function fileOperationFiltersMatch(filters, oldUri, newUri, oldFilePath, newFilePath) {
|
|
4262
|
+
if (!filters || filters.length === 0)
|
|
4263
|
+
return false;
|
|
4264
|
+
const candidates = [oldUri, newUri].flatMap(fileOperationUriCandidates);
|
|
4265
|
+
const entityKind = filters.some((filter) => filter.matches !== undefined)
|
|
4266
|
+
? await probeRenameEntityKind(oldFilePath, newFilePath)
|
|
4267
|
+
: undefined;
|
|
4268
|
+
for (const filter of filters) {
|
|
4269
|
+
if (filter.scheme !== undefined && filter.scheme !== "file")
|
|
4270
|
+
continue;
|
|
4271
|
+
if (filter.matches !== undefined && filter.matches !== entityKind)
|
|
4272
|
+
continue;
|
|
4273
|
+
for (const candidate of candidates) {
|
|
4274
|
+
try {
|
|
4275
|
+
if (minimatch(candidate, filter.glob, {
|
|
4276
|
+
nocase: filter.ignoreCase,
|
|
4277
|
+
dot: true,
|
|
4278
|
+
})) {
|
|
4279
|
+
return true;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
catch {
|
|
4283
|
+
// Malformed glob implementations must never widen server fan-out.
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return false;
|
|
4288
|
+
}
|
|
4289
|
+
function fileOperationUriCandidates(uri) {
|
|
4290
|
+
let parsed;
|
|
4291
|
+
try {
|
|
4292
|
+
parsed = new URL(uri);
|
|
4293
|
+
}
|
|
4294
|
+
catch {
|
|
4295
|
+
return [];
|
|
4296
|
+
}
|
|
4297
|
+
// pi-lens performs local filesystem renames only. Even when a caller supplies
|
|
4298
|
+
// preserved URI spelling, another scheme cannot describe that operation.
|
|
4299
|
+
if (parsed.protocol.toLowerCase() !== "file:")
|
|
4300
|
+
return [];
|
|
4301
|
+
let uriPath;
|
|
4302
|
+
try {
|
|
4303
|
+
uriPath = decodeURIComponent(parsed.pathname).replace(/\\/g, "/");
|
|
4304
|
+
}
|
|
4305
|
+
catch {
|
|
4306
|
+
return [];
|
|
4307
|
+
}
|
|
4308
|
+
return [uriPath];
|
|
4309
|
+
}
|
|
4310
|
+
async function probeRenameEntityKind(oldFilePath, newFilePath) {
|
|
4311
|
+
for (const filePath of [oldFilePath, newFilePath]) {
|
|
4312
|
+
try {
|
|
4313
|
+
const info = await lstat(filePath);
|
|
4314
|
+
return info.isDirectory() ? "folder" : "file";
|
|
4315
|
+
}
|
|
4316
|
+
catch {
|
|
4317
|
+
// Before the rename the old path normally exists; after it, the new path
|
|
4318
|
+
// does. Probe both instead of deriving behavior from the host OS.
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return undefined;
|
|
4322
|
+
}
|