pi-lens 3.8.73 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in compact one-line tool-result rendering (#1327).
|
|
3
|
+
*
|
|
4
|
+
* pi renders a tool call as two rows by default: the tool-name/args row
|
|
5
|
+
* (`renderCall`) and the result row (`renderResult`). Most pi-lens tools
|
|
6
|
+
* already supply a `renderResult` via `tools/render-compact.ts`'s
|
|
7
|
+
* `compactRenderResult` — a collapsed one-line summary (brand-blue) plus the
|
|
8
|
+
* full output when expanded. This module adds a further, OPT-IN layer that
|
|
9
|
+
* collapses those two rows into ONE — `<status glyph> <name> — <summary>` —
|
|
10
|
+
* by reusing each tool's EXISTING collapsed summary text verbatim (never
|
|
11
|
+
* inventing new strings) and re-styling it through semantic `Theme` tokens
|
|
12
|
+
* instead of a fixed color.
|
|
13
|
+
*
|
|
14
|
+
* Applied generically at the registration seam (index.ts's registerTool
|
|
15
|
+
* loop), not as a per-tool copy: `wrapToolForCompactLine` decorates any
|
|
16
|
+
* `ToolDefinition` that already defines `renderResult`; tools without one are
|
|
17
|
+
* returned untouched. Callers gate the whole thing behind the `ui.compactToolLine`
|
|
18
|
+
* flag (`clients/lens-flag-registry.ts`) — when it is off, the ORIGINAL tool
|
|
19
|
+
* array is registered unwrapped, so behavior is byte-identical to today (no
|
|
20
|
+
* `renderCall`/`renderResult` are added or altered).
|
|
21
|
+
*
|
|
22
|
+
* `clients/` intentionally does NOT import `tools/render-compact.ts` (tools/
|
|
23
|
+
* depends on clients/, never the reverse — see AGENTS.md's seam discipline),
|
|
24
|
+
* so `fullTextOf` is duplicated here in miniature rather than inverting that
|
|
25
|
+
* dependency.
|
|
26
|
+
*
|
|
27
|
+
* API limitation vs the issue's ask: `ToolRenderResultOptions` (the type pi
|
|
28
|
+
* hands `renderResult`) exposes only `{ expanded, isPartial }` — no
|
|
29
|
+
* `outputPad`. `outputPad` exists solely on the unrelated `MessageRenderOptions`
|
|
30
|
+
* (custom message renderers, e.g. clients/turn-summary-render.ts), not on the
|
|
31
|
+
* tool-result render path. There is nothing to honor here; the collapsed line
|
|
32
|
+
* still respects the render width via `fitLines` so it never overflows the
|
|
33
|
+
* terminal (#513's crash class).
|
|
34
|
+
*/
|
|
35
|
+
import { stripAnsi } from "./sanitize.js";
|
|
36
|
+
import { fitLines } from "./tui-fit.js";
|
|
37
|
+
const EMPTY_COMPONENT = {
|
|
38
|
+
render: () => [],
|
|
39
|
+
invalidate: () => { },
|
|
40
|
+
};
|
|
41
|
+
function fullTextOf(result) {
|
|
42
|
+
return (result.content ?? [])
|
|
43
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
44
|
+
.map((c) => c.text)
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the collapsed compact line from a tool's ALREADY-COMPUTED summary
|
|
49
|
+
* text (verbatim reuse — see module doc). `summaryText` is expected in the
|
|
50
|
+
* `<name/hint> — <detail>` shape every pi-lens summarizer already produces
|
|
51
|
+
* (e.g. `lens_diagnostics all — 3 blocking · 3 errors · 2 warnings (1 file)`);
|
|
52
|
+
* when the separator is absent the whole string is styled as one span
|
|
53
|
+
* instead of guessing a split point.
|
|
54
|
+
*
|
|
55
|
+
* Pure and theme-driven: a status glyph (success/error tokens), the name
|
|
56
|
+
* portion (`toolTitle`, bold), and the detail portion (`error`/`toolOutput`)
|
|
57
|
+
* are each styled through semantic `Theme.fg` tokens — never a fixed color.
|
|
58
|
+
*/
|
|
59
|
+
export function buildCompactToolLine(summaryText, isError, theme) {
|
|
60
|
+
const glyph = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
61
|
+
const detailColor = isError ? "error" : "toolOutput";
|
|
62
|
+
const sepIndex = summaryText.indexOf(" — ");
|
|
63
|
+
if (sepIndex === -1) {
|
|
64
|
+
return `${glyph} ${theme.fg(detailColor, summaryText)}`;
|
|
65
|
+
}
|
|
66
|
+
const namePart = summaryText.slice(0, sepIndex);
|
|
67
|
+
const detailPart = summaryText.slice(sepIndex + " — ".length);
|
|
68
|
+
const styledName = theme.fg("toolTitle", theme.bold(namePart));
|
|
69
|
+
const styledDash = theme.fg("dim", "—");
|
|
70
|
+
const styledDetail = theme.fg(detailColor, detailPart);
|
|
71
|
+
return `${glyph} ${styledName} ${styledDash} ${styledDetail}`;
|
|
72
|
+
}
|
|
73
|
+
/** Render a component at a wide probe width to capture its full text,
|
|
74
|
+
* stripped of ANSI — the collapsed summary is re-styled through this
|
|
75
|
+
* module's own theme tokens rather than carrying over fixed-color escapes
|
|
76
|
+
* (e.g. tools/render-compact.ts's brand-blue). */
|
|
77
|
+
function captureComponentText(component) {
|
|
78
|
+
try {
|
|
79
|
+
const lines = component.render(4096);
|
|
80
|
+
return stripAnsi(lines.join(" ")).trim();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decorate a single tool definition with the opt-in compact-line renderers.
|
|
88
|
+
* Tools with no `renderResult` are returned UNCHANGED (nothing to compact).
|
|
89
|
+
* Call this only when the `ui.compactToolLine` flag is already known to be
|
|
90
|
+
* on — the caller (index.ts) skips the wrap entirely when it is off, which
|
|
91
|
+
* is what keeps the off path byte-identical to today rather than this
|
|
92
|
+
* function branching on a live flag read per render.
|
|
93
|
+
*/
|
|
94
|
+
export function wrapToolForCompactLine(tool) {
|
|
95
|
+
const originalRenderResult = tool.renderResult;
|
|
96
|
+
if (!originalRenderResult)
|
|
97
|
+
return tool;
|
|
98
|
+
const originalRenderCall = tool.renderCall;
|
|
99
|
+
const renderCall = (args, theme, context) => {
|
|
100
|
+
// Only blank the call row once a settled (non-partial) result exists —
|
|
101
|
+
// `renderResult` is about to paint the single combined line. While the
|
|
102
|
+
// tool is still running/streaming, keep showing the normal call row
|
|
103
|
+
// (mirrors the host's own createCallFallback) so the UI isn't blank.
|
|
104
|
+
if (!context.expanded && context.isPartial === false) {
|
|
105
|
+
return EMPTY_COMPONENT;
|
|
106
|
+
}
|
|
107
|
+
if (originalRenderCall)
|
|
108
|
+
return originalRenderCall(args, theme, context);
|
|
109
|
+
// Mirrors the host's own createCallFallback exactly: styled `tool.name`
|
|
110
|
+
// (the registered name, e.g. "lens_diagnostics"), not the display label.
|
|
111
|
+
const line = theme.fg("toolTitle", theme.bold(tool.name));
|
|
112
|
+
return {
|
|
113
|
+
render: (width) => fitLines([line], width),
|
|
114
|
+
invalidate: () => { },
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const renderResult = (result, options, theme, context) => {
|
|
118
|
+
if (options.expanded) {
|
|
119
|
+
return originalRenderResult(result, options, theme, context);
|
|
120
|
+
}
|
|
121
|
+
let summaryText;
|
|
122
|
+
try {
|
|
123
|
+
// Probe the tool's OWN collapsed renderer to capture its already-built
|
|
124
|
+
// summary text; `lastComponent` is cleared so the probe never reuses
|
|
125
|
+
// (and thereby mutates) the real row's retained component.
|
|
126
|
+
const probeContext = { ...context, lastComponent: undefined };
|
|
127
|
+
const component = originalRenderResult(result, { ...options, expanded: false }, theme, probeContext);
|
|
128
|
+
summaryText = captureComponentText(component);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
summaryText = "";
|
|
132
|
+
}
|
|
133
|
+
const resultLike = result;
|
|
134
|
+
if (!summaryText) {
|
|
135
|
+
summaryText = stripAnsi(fullTextOf(resultLike).split("\n")[0] ?? "").trim();
|
|
136
|
+
}
|
|
137
|
+
// pi invokes tool renderers with `{ content, details }` -- the failure
|
|
138
|
+
// bit lives on `context.isError`, not the result (#1341 review). Keep
|
|
139
|
+
// `result.isError` as a fallback for direct/legacy callers.
|
|
140
|
+
const contextIsError = context?.isError;
|
|
141
|
+
const isError = contextIsError ?? resultLike.isError === true;
|
|
142
|
+
const line = buildCompactToolLine(summaryText, isError === true, theme);
|
|
143
|
+
return {
|
|
144
|
+
render: (width) => fitLines([line], width),
|
|
145
|
+
invalidate: () => { },
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
return { ...tool, renderCall, renderResult };
|
|
149
|
+
}
|
|
150
|
+
/** Wrap every tool in an array — tools without `renderResult` pass through
|
|
151
|
+
* unchanged. Used by index.ts's registerTool loop, only when the
|
|
152
|
+
* `ui.compactToolLine` flag resolved on for this session. */
|
|
153
|
+
export function wrapToolsForCompactLine(tools) {
|
|
154
|
+
return tools.map((tool) => wrapToolForCompactLine(tool));
|
|
155
|
+
}
|
|
@@ -1,32 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tree-sitter Tree Cache
|
|
2
|
+
* Tree-sitter Tree Cache
|
|
3
3
|
*
|
|
4
|
-
* Caches parsed ASTs
|
|
5
|
-
*
|
|
4
|
+
* Caches parsed ASTs so a file written once is parsed once and reused by every
|
|
5
|
+
* subsystem that inspects it in the same process (#675).
|
|
6
|
+
*
|
|
7
|
+
* Freshness is hash-authoritative when the caller supplies content: identical
|
|
8
|
+
* bytes parse to an identical tree, so a hash match is a hit even if the file's
|
|
9
|
+
* mtime moved (an agent re-saving unchanged bytes must not force a reparse,
|
|
10
|
+
* #890). Eviction is true LRU — hits re-insert their entry, so set() dropping
|
|
11
|
+
* the Map's first key removes the least-recently-used file, not merely the
|
|
12
|
+
* oldest insertion, and hot per-edit files survive scan traffic (#890).
|
|
6
13
|
*/
|
|
14
|
+
import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
|
|
7
15
|
import * as crypto from "node:crypto";
|
|
8
16
|
import * as fs from "node:fs";
|
|
9
17
|
import { normalizeFilePath } from "./path-utils.js";
|
|
18
|
+
const TREE_RETIREMENT_GRACE_MICROTASKS = 4;
|
|
19
|
+
export const TREE_CACHE_COUNTER_KEYS = [
|
|
20
|
+
"lookups",
|
|
21
|
+
"hits",
|
|
22
|
+
"coldMisses",
|
|
23
|
+
"capacityMisses",
|
|
24
|
+
"contentChangedMisses",
|
|
25
|
+
"mtimeMisses",
|
|
26
|
+
"statFailedMisses",
|
|
27
|
+
"sets",
|
|
28
|
+
"replacements",
|
|
29
|
+
"evictions",
|
|
30
|
+
"clears",
|
|
31
|
+
"ghostHistoryDrops",
|
|
32
|
+
];
|
|
33
|
+
export function createTreeCacheCounters() {
|
|
34
|
+
return Object.fromEntries(TREE_CACHE_COUNTER_KEYS.map((key) => [key, 0]));
|
|
35
|
+
}
|
|
10
36
|
export class TreeCache {
|
|
11
37
|
cache = new Map();
|
|
38
|
+
recentlyEvicted = new Map();
|
|
12
39
|
maxSize;
|
|
40
|
+
evictionHistoryMax;
|
|
13
41
|
debug;
|
|
14
|
-
|
|
42
|
+
counters = createTreeCacheCounters();
|
|
43
|
+
counterObserver;
|
|
44
|
+
treeErrorObserver;
|
|
45
|
+
constructor(maxSize = 50, debug = false, evictionHistoryMax = 4096, counterObserver, treeErrorObserver) {
|
|
15
46
|
this.maxSize = maxSize;
|
|
47
|
+
this.evictionHistoryMax = Math.max(1, Math.floor(evictionHistoryMax));
|
|
48
|
+
this.counterObserver = counterObserver;
|
|
49
|
+
this.treeErrorObserver = treeErrorObserver;
|
|
16
50
|
this.debug = debug
|
|
17
|
-
? (msg) =>
|
|
51
|
+
? (msg) => logTreeSitterDiagnostic({
|
|
52
|
+
subsystem: "tree-cache",
|
|
53
|
+
level: "debug",
|
|
54
|
+
message: msg,
|
|
55
|
+
})
|
|
18
56
|
: () => { };
|
|
19
57
|
}
|
|
58
|
+
recordCounter(key, amount = 1) {
|
|
59
|
+
this.counters[key] += amount;
|
|
60
|
+
this.counterObserver?.(key, amount);
|
|
61
|
+
}
|
|
20
62
|
/**
|
|
21
63
|
* Free a tree-sitter Tree's WASM-heap allocation.
|
|
22
64
|
*
|
|
23
65
|
* web-tree-sitter Trees live in the WASM heap; JS GC reclaims only the wrapper,
|
|
24
66
|
* so the underlying memory leaks unless `tree.delete()` is called explicitly
|
|
25
67
|
* (no FinalizationRegistry auto-free in 0.25). Guarded — a tree may already be
|
|
26
|
-
* deleted, or `delete()` may throw on a corrupt/aborted runtime.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
68
|
+
* deleted, or `delete()` may throw on a corrupt/aborted runtime. Retirement is
|
|
69
|
+
* deferred so direct parse callers resume before deletion; consumers still
|
|
70
|
+
* traverse without another await, or use the cache-safe callback API (#417).
|
|
71
|
+
* The eviction target is the least-recently-used entry (get() re-inserts
|
|
72
|
+
* hits), never the just-parsed tree still in a caller's hand.
|
|
30
73
|
*/
|
|
31
74
|
// biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter Tree
|
|
32
75
|
freeTree(tree) {
|
|
@@ -34,17 +77,39 @@ export class TreeCache {
|
|
|
34
77
|
if (tree && typeof tree.delete === "function")
|
|
35
78
|
tree.delete();
|
|
36
79
|
}
|
|
37
|
-
catch {
|
|
38
|
-
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.treeErrorObserver?.(error);
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
|
-
|
|
84
|
+
retireTree(tree) {
|
|
85
|
+
let remaining = TREE_RETIREMENT_GRACE_MICROTASKS;
|
|
86
|
+
const retire = () => {
|
|
87
|
+
if (remaining-- > 0) {
|
|
88
|
+
queueMicrotask(retire);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.freeTree(tree);
|
|
92
|
+
};
|
|
93
|
+
queueMicrotask(retire);
|
|
94
|
+
}
|
|
95
|
+
/** Remove a cache entry and retire its WASM tree after current consumers run. */
|
|
42
96
|
removeEntry(key) {
|
|
43
97
|
const cached = this.cache.get(key);
|
|
44
98
|
if (cached)
|
|
45
|
-
this.
|
|
99
|
+
this.retireTree(cached.tree);
|
|
46
100
|
this.cache.delete(key);
|
|
47
101
|
}
|
|
102
|
+
rememberEviction(key, cached) {
|
|
103
|
+
this.recentlyEvicted.delete(key);
|
|
104
|
+
if (this.recentlyEvicted.size >= this.evictionHistoryMax) {
|
|
105
|
+
const oldestKey = this.recentlyEvicted.keys().next().value;
|
|
106
|
+
if (oldestKey !== undefined) {
|
|
107
|
+
this.recentlyEvicted.delete(oldestKey);
|
|
108
|
+
this.recordCounter("ghostHistoryDrops");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.recentlyEvicted.set(key, cached.contentHash);
|
|
112
|
+
}
|
|
48
113
|
/**
|
|
49
114
|
* Generate hash for file content
|
|
50
115
|
*/
|
|
@@ -62,28 +127,69 @@ export class TreeCache {
|
|
|
62
127
|
return `${languageId}:${normalizeFilePath(filePath)}`;
|
|
63
128
|
}
|
|
64
129
|
/**
|
|
65
|
-
* Check if tree is cached and valid
|
|
130
|
+
* Check if tree is cached and valid.
|
|
131
|
+
*
|
|
132
|
+
* When `content` is provided the content hash is AUTHORITATIVE: a hash match
|
|
133
|
+
* is a hit regardless of mtime, and the entry's stat metadata is refreshed
|
|
134
|
+
* so a save-without-change (same bytes, newer mtime) does not masquerade as
|
|
135
|
+
* a disk modification (#890). The mtime check remains the freshness signal
|
|
136
|
+
* only on the content-less path, where nothing else can prove the cached
|
|
137
|
+
* tree current (`mtimeMisses`). A stat failure invalidates on both paths —
|
|
138
|
+
* a deleted file's entry is dead weight and its WASM tree should be freed.
|
|
139
|
+
*
|
|
140
|
+
* Every hit re-inserts the entry (raw Map delete+set — NOT removeEntry,
|
|
141
|
+
* which would retire the live tree) so eviction is true LRU (#890).
|
|
66
142
|
*/
|
|
67
143
|
get(filePath, content, languageId) {
|
|
144
|
+
this.recordCounter("lookups");
|
|
68
145
|
const key = this.getCacheKey(filePath, languageId);
|
|
69
146
|
const cached = this.cache.get(key);
|
|
70
147
|
if (!cached) {
|
|
148
|
+
const evictedHash = this.recentlyEvicted.get(key);
|
|
149
|
+
if (evictedHash !== undefined &&
|
|
150
|
+
content !== undefined &&
|
|
151
|
+
evictedHash === this.hashContent(content)) {
|
|
152
|
+
this.recordCounter("capacityMisses");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.recordCounter("coldMisses");
|
|
156
|
+
}
|
|
71
157
|
this.debug(`Cache miss: ${filePath}`);
|
|
72
158
|
return null;
|
|
73
159
|
}
|
|
74
160
|
// (No language-mismatch check needed: the cache key is prefixed with
|
|
75
161
|
// languageId, so a key hit already implies the language matches.)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
if (content !== undefined) {
|
|
163
|
+
// Check content hash — authoritative when content is provided.
|
|
164
|
+
const contentHash = this.hashContent(content);
|
|
165
|
+
if (cached.contentHash !== contentHash) {
|
|
166
|
+
this.recordCounter("contentChangedMisses");
|
|
167
|
+
this.debug(`Content changed: ${filePath} (${cached.lineCount} → ${content.split("\n").length} lines)`);
|
|
168
|
+
// Keep old tree for potential incremental update, but mark as stale
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
82
171
|
}
|
|
83
|
-
// Check if file was modified on disk (mtime changed)
|
|
84
172
|
try {
|
|
85
173
|
const stats = fs.statSync(filePath);
|
|
86
|
-
if (
|
|
174
|
+
if (content !== undefined) {
|
|
175
|
+
// The hash already proved the tree current: an mtime delta is a
|
|
176
|
+
// save-without-change (or touch/clock drift). Refresh metadata
|
|
177
|
+
// instead of invalidating (#890). Caveat: this stamps the CURRENT
|
|
178
|
+
// disk mtime onto an entry validated against caller-supplied
|
|
179
|
+
// content — if that content lags a newer disk write, lastModified
|
|
180
|
+
// no longer vouches for the disk bytes. So lastModified means
|
|
181
|
+
// "mtime last observed on a hash-confirmed hit"; the content-less
|
|
182
|
+
// path below may only treat it as freshness proof while all
|
|
183
|
+
// callers pass content (today they all do — the undefined path is
|
|
184
|
+
// exercised only by tests).
|
|
185
|
+
if (stats.mtimeMs !== cached.lastModified) {
|
|
186
|
+
cached.lastModified = stats.mtimeMs;
|
|
187
|
+
this.debug(`Refreshed mtime on hash-matched entry: ${filePath}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (stats.mtimeMs !== cached.lastModified) {
|
|
191
|
+
// No content to hash — mtime is the only freshness proof.
|
|
192
|
+
this.recordCounter("mtimeMisses");
|
|
87
193
|
this.debug(`File modified on disk: ${filePath}`);
|
|
88
194
|
this.removeEntry(key);
|
|
89
195
|
return null;
|
|
@@ -91,9 +197,16 @@ export class TreeCache {
|
|
|
91
197
|
}
|
|
92
198
|
catch {
|
|
93
199
|
// File might be deleted, invalidate cache
|
|
200
|
+
this.recordCounter("statFailedMisses");
|
|
94
201
|
this.removeEntry(key);
|
|
95
202
|
return null;
|
|
96
203
|
}
|
|
204
|
+
// LRU touch: re-insert so this entry becomes the newest. The SAME entry
|
|
205
|
+
// object is re-set — no tree replacement, so nothing is retired and a
|
|
206
|
+
// retired tree can never be resurrected (#890).
|
|
207
|
+
this.cache.delete(key);
|
|
208
|
+
this.cache.set(key, cached);
|
|
209
|
+
this.recordCounter("hits");
|
|
97
210
|
this.debug(`Cache hit: ${filePath} (${cached.lineCount} lines)`);
|
|
98
211
|
return cached.tree;
|
|
99
212
|
}
|
|
@@ -101,16 +214,26 @@ export class TreeCache {
|
|
|
101
214
|
* Store parsed tree in cache
|
|
102
215
|
*/
|
|
103
216
|
set(filePath, content, languageId, tree) {
|
|
217
|
+
this.recordCounter("sets");
|
|
104
218
|
const key = this.getCacheKey(filePath, languageId);
|
|
219
|
+
const contentHash = this.hashContent(content);
|
|
220
|
+
this.recentlyEvicted.delete(key);
|
|
105
221
|
// Free the tree we're about to replace at this key (re-parse of the same
|
|
106
222
|
// file) so it doesn't leak its WASM heap.
|
|
107
223
|
if (this.cache.has(key)) {
|
|
224
|
+
this.recordCounter("replacements");
|
|
108
225
|
this.removeEntry(key);
|
|
109
226
|
}
|
|
110
227
|
else if (this.cache.size >= this.maxSize) {
|
|
111
|
-
// Evict + free the
|
|
228
|
+
// Evict + free the least-recently-used entry when the cache is full:
|
|
229
|
+
// get() re-inserts hits, so Map insertion order IS recency order and
|
|
230
|
+
// the first key is the LRU entry (#890).
|
|
112
231
|
const firstKey = this.cache.keys().next().value;
|
|
113
232
|
if (firstKey) {
|
|
233
|
+
const evicted = this.cache.get(firstKey);
|
|
234
|
+
if (evicted)
|
|
235
|
+
this.rememberEviction(firstKey, evicted);
|
|
236
|
+
this.recordCounter("evictions");
|
|
114
237
|
this.removeEntry(firstKey);
|
|
115
238
|
this.debug(`Evicted: ${firstKey}`);
|
|
116
239
|
}
|
|
@@ -125,136 +248,24 @@ export class TreeCache {
|
|
|
125
248
|
}
|
|
126
249
|
this.cache.set(key, {
|
|
127
250
|
tree,
|
|
128
|
-
contentHash
|
|
251
|
+
contentHash,
|
|
129
252
|
languageId,
|
|
130
|
-
fileSize: content
|
|
253
|
+
fileSize: Buffer.byteLength(content, "utf8"),
|
|
131
254
|
lineCount: content.split("\n").length,
|
|
132
255
|
lastModified: mtime,
|
|
133
256
|
});
|
|
134
257
|
this.debug(`Cached: ${filePath} (${content.split("\n").length} lines)`);
|
|
135
258
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Calculate the diff between old and new content
|
|
138
|
-
* Returns edit information for incremental parsing
|
|
139
|
-
*/
|
|
140
|
-
calculateEdit(oldContent, newContent) {
|
|
141
|
-
// Find the first difference
|
|
142
|
-
let startIndex = 0;
|
|
143
|
-
while (startIndex < oldContent.length &&
|
|
144
|
-
startIndex < newContent.length &&
|
|
145
|
-
oldContent[startIndex] === newContent[startIndex]) {
|
|
146
|
-
startIndex++;
|
|
147
|
-
}
|
|
148
|
-
// Find the last difference (working backwards)
|
|
149
|
-
let oldEndIndex = oldContent.length;
|
|
150
|
-
let newEndIndex = newContent.length;
|
|
151
|
-
while (oldEndIndex > startIndex &&
|
|
152
|
-
newEndIndex > startIndex &&
|
|
153
|
-
oldContent[oldEndIndex - 1] === newContent[newEndIndex - 1]) {
|
|
154
|
-
oldEndIndex--;
|
|
155
|
-
newEndIndex--;
|
|
156
|
-
}
|
|
157
|
-
// No change detected
|
|
158
|
-
if (startIndex === oldContent.length && startIndex === newContent.length) {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
// Calculate positions
|
|
162
|
-
const startPosition = this.indexToPosition(oldContent, startIndex);
|
|
163
|
-
const oldEndPosition = this.indexToPosition(oldContent, oldEndIndex);
|
|
164
|
-
const newEndPosition = this.indexToPosition(newContent, newEndIndex);
|
|
165
|
-
return {
|
|
166
|
-
startIndex,
|
|
167
|
-
oldEndIndex,
|
|
168
|
-
newEndIndex,
|
|
169
|
-
startPosition,
|
|
170
|
-
oldEndPosition,
|
|
171
|
-
newEndPosition,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Convert byte index to row/column position
|
|
176
|
-
*/
|
|
177
|
-
indexToPosition(content, index) {
|
|
178
|
-
const lines = content.slice(0, index).split("\n");
|
|
179
|
-
return {
|
|
180
|
-
row: lines.length - 1,
|
|
181
|
-
column: lines[lines.length - 1].length,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Attempt incremental update using tree.edit()
|
|
186
|
-
* Returns updated tree or null if incremental update failed
|
|
187
|
-
*/
|
|
188
|
-
async incrementalUpdate(filePath, oldContent, newContent, languageId, parser) {
|
|
189
|
-
const key = this.getCacheKey(filePath, languageId);
|
|
190
|
-
const cached = this.cache.get(key);
|
|
191
|
-
if (!cached) {
|
|
192
|
-
this.debug(`No cached tree for incremental update: ${filePath}`);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
// Only use incremental for large files (>100 lines)
|
|
196
|
-
const lineCount = oldContent.split("\n").length;
|
|
197
|
-
if (lineCount < 100) {
|
|
198
|
-
this.debug(`File too small for incremental: ${filePath} (${lineCount} lines)`);
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
// Calculate edit
|
|
202
|
-
const edit = this.calculateEdit(oldContent, newContent);
|
|
203
|
-
if (!edit) {
|
|
204
|
-
this.debug(`No edit detected for: ${filePath}`);
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
this.debug(`Incremental update: ${filePath} (lines ${edit.startPosition.row}-${edit.oldEndPosition.row})`);
|
|
208
|
-
try {
|
|
209
|
-
// Apply edit to tree
|
|
210
|
-
cached.tree.edit({
|
|
211
|
-
startIndex: edit.startIndex,
|
|
212
|
-
oldEndIndex: edit.oldEndIndex,
|
|
213
|
-
newEndIndex: edit.newEndIndex,
|
|
214
|
-
startPosition: edit.startPosition,
|
|
215
|
-
oldEndPosition: edit.oldEndPosition,
|
|
216
|
-
newEndPosition: edit.newEndPosition,
|
|
217
|
-
});
|
|
218
|
-
// Re-parse only changed region
|
|
219
|
-
const newTree = parser.parse(newContent, cached.tree);
|
|
220
|
-
// Update cache
|
|
221
|
-
this.set(filePath, newContent, languageId, newTree);
|
|
222
|
-
this.debug(`Incremental update successful: ${filePath}`);
|
|
223
|
-
return newTree;
|
|
224
|
-
}
|
|
225
|
-
catch (err) {
|
|
226
|
-
this.debug(`Incremental update failed: ${err}`);
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Clear cache for a specific file
|
|
232
|
-
*/
|
|
233
|
-
invalidate(filePath, languageId) {
|
|
234
|
-
if (languageId) {
|
|
235
|
-
const key = this.getCacheKey(filePath, languageId);
|
|
236
|
-
this.removeEntry(key);
|
|
237
|
-
this.debug(`Invalidated: ${key}`);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
// Invalidate all entries for this file
|
|
241
|
-
for (const [key, value] of this.cache.entries()) {
|
|
242
|
-
if (key.includes(filePath)) {
|
|
243
|
-
this.freeTree(value.tree);
|
|
244
|
-
this.cache.delete(key);
|
|
245
|
-
this.debug(`Invalidated: ${key}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
259
|
/**
|
|
251
260
|
* Clear entire cache
|
|
252
261
|
*/
|
|
253
262
|
clear() {
|
|
263
|
+
this.recordCounter("clears");
|
|
254
264
|
for (const entry of this.cache.values()) {
|
|
255
|
-
this.
|
|
265
|
+
this.retireTree(entry.tree);
|
|
256
266
|
}
|
|
257
267
|
this.cache.clear();
|
|
268
|
+
this.recentlyEvicted.clear();
|
|
258
269
|
this.debug("Cache cleared");
|
|
259
270
|
}
|
|
260
271
|
/**
|
|
@@ -268,10 +279,12 @@ export class TreeCache {
|
|
|
268
279
|
totalBytes += entry.fileSize;
|
|
269
280
|
}
|
|
270
281
|
return {
|
|
282
|
+
...this.counters,
|
|
271
283
|
size: this.cache.size,
|
|
272
284
|
maxSize: this.maxSize,
|
|
273
285
|
totalLines,
|
|
274
286
|
totalBytes,
|
|
287
|
+
misses: this.counters.lookups - this.counters.hits,
|
|
275
288
|
};
|
|
276
289
|
}
|
|
277
290
|
}
|