Merge pull request #134 from levineuwirth/locals-query-processing
Process Tree-sitter locals queries
This commit is contained in:
commit
8cbb9f4377
|
|
@ -14,7 +14,7 @@ backlog.
|
||||||
machine-local: `origin` may name this canonical URL, a release mirror,
|
machine-local: `origin` may name this canonical URL, a release mirror,
|
||||||
or something else, and therefore has no authority by name alone.
|
or something else, and therefore has no authority by name alone.
|
||||||
- Canonical base at this snapshot:
|
- Canonical base at this snapshot:
|
||||||
`githubsucks/main` @ `1dd47fc` (modeline detection #132 merged; protocol v18).
|
`githubsucks/main` @ `8bd8298` (landed-state handoff #133; protocol v18).
|
||||||
- On the transfer source, `origin/main` named a release mirror at
|
- On the transfer source, `origin/main` named a release mirror at
|
||||||
`d3fa632` and lagged badly. On the current destination, `origin` names
|
`d3fa632` and lagged badly. On the current destination, `origin` names
|
||||||
the canonical URL. This difference is why all recovery begins by
|
the canonical URL. This difference is why all recovery begins by
|
||||||
|
|
@ -48,9 +48,43 @@ git worktree list
|
||||||
git status --short --branch
|
git status --short --branch
|
||||||
```
|
```
|
||||||
|
|
||||||
The first command must expose `1dd47fc` or a newer intentional main.
|
The first command must expose `8bd8298` or a newer intentional main.
|
||||||
If it does not, stop and repair the remote/fetch configuration.
|
If it does not, stop and repair the remote/fetch configuration.
|
||||||
|
|
||||||
|
## Locals-query processing lane
|
||||||
|
|
||||||
|
- Portable branch: `githubsucks/locals-query-processing`
|
||||||
|
- Base: canonical `main` @ `8bd8298`.
|
||||||
|
- Framing head: `42a28b9` (`docs/locals-query-processing-framing.md`).
|
||||||
|
- Implementation head: `47ffe5d`.
|
||||||
|
- PR: #134, <https://github.com/levineuwirth/pmacs/pull/134>, open for review.
|
||||||
|
Implementation is complete; protocol remains v18.
|
||||||
|
- Scope: bundled Lua/JavaScript/TypeScript locals metadata, lexical
|
||||||
|
scope/definition/value/reference resolution cached on each settled layer,
|
||||||
|
and shared `#is?`/`#is-not? local` filtering in the TUI and semantic/GPU
|
||||||
|
highlight producers.
|
||||||
|
- Focused verification:
|
||||||
|
- default and Lua 5.4 lexical suites: 35 passed each;
|
||||||
|
- default and Lua 5.4 end-to-end render/edit acceptance: 1 passed each.
|
||||||
|
- Full pre-PR verification:
|
||||||
|
- `cargo fmt --check`: clean;
|
||||||
|
- strict workspace Clippy: clean;
|
||||||
|
- default library: 1,757 passed (3 ignored);
|
||||||
|
- CRDT library: 1,933 passed (3 ignored);
|
||||||
|
- M4 acceptance excluding basedpyright: 121 passed, 3 ignored, 1 filtered;
|
||||||
|
- required GPU: 109 passed;
|
||||||
|
- workspace: 2,893 passed across 82 suites, 19 ignored, 1 filtered;
|
||||||
|
- `git diff --check`: clean.
|
||||||
|
|
||||||
|
Recovery worktree on a machine that does not already own the branch:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git worktree add --track \
|
||||||
|
-b locals-query-processing \
|
||||||
|
../pmacs-locals-query \
|
||||||
|
githubsucks/locals-query-processing
|
||||||
|
```
|
||||||
|
|
||||||
## Parked lane: kill-ring browser + persistence
|
## Parked lane: kill-ring browser + persistence
|
||||||
|
|
||||||
- Portable branch: `githubsucks/kill-ring-browser`
|
- Portable branch: `githubsucks/kill-ring-browser`
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Agent handoff — cross-machine continuity
|
# Agent handoff — cross-machine continuity
|
||||||
|
|
||||||
**Last updated: 2026-07-22, after modeline language detection (#132) and
|
**Last updated: 2026-07-22, with locals-query processing (#134) implemented
|
||||||
Vterm Stage 2 (#130) landed on `main`, atop mode system wiring (#129/#131),
|
and awaiting review, after modeline language
|
||||||
config registry (#127), Vterm Stage 1 terminal core (#126), and completed
|
detection (#132), Vterm Stage 2 (#130), and mode system wiring (#129/#131)
|
||||||
Themes Arc 4 (#120/#124/#125). Vterm Stage 3 is not implemented.**
|
landed on `main`. Vterm Stage 3 is not implemented.**
|
||||||
This file is the
|
This file is the
|
||||||
bridge between development machines. If you are an agent reading
|
bridge between development machines. If you are an agent reading
|
||||||
this on a fresh clone: this document plus the `docs/*-framing.md`
|
this on a fresh clone: this document plus the `docs/*-framing.md`
|
||||||
|
|
@ -17,7 +17,8 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
|
|
||||||
## 1. Where the project stands (2026-07-22)
|
## 1. Where the project stands (2026-07-22)
|
||||||
|
|
||||||
- `main` @ `1dd47fc` (modeline detection #132), protocol **v18**
|
- `main` @ `8bd8298` (landed-state handoff #133; latest runtime merge
|
||||||
|
`1dd47fc`, modeline detection #132), protocol **v18**
|
||||||
(`SUPPORTED=[6..18]`; v16 = `ThemeFacts`, v17 = `FontFacts`, v18 =
|
(`SUPPORTED=[6..18]`; v16 = `ThemeFacts`, v17 = `FontFacts`, v18 =
|
||||||
`StatuslineSegments`).
|
`StatuslineSegments`).
|
||||||
- **Config registry LANDED — #127** (`docs/config-registry-framing.md`
|
- **Config registry LANDED — #127** (`docs/config-registry-framing.md`
|
||||||
|
|
@ -117,12 +118,16 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
cmake (`cmake-language-server`, config via
|
cmake (`cmake-language-server`, config via
|
||||||
`init_options.buildDirectory="build"` — it does NOT pull
|
`init_options.buildDirectory="build"` — it does NOT pull
|
||||||
`workspace/configuration`). Make has no server.
|
`workspace/configuration`). Make has no server.
|
||||||
- **Substrate**: `LanguageEntry.highlights_query` is now
|
- **Substrate**: `LanguageEntry.highlights_query` and `.locals_query` are
|
||||||
`&[&'static str]` — fragments joined base-first, for grammars whose
|
`&[&'static str]` fragments joined base-first (cuda over c/cpp; ts over
|
||||||
bundled highlights are a `; inherits:` delta (cuda over c/cpp; ts
|
js/jsx). On `locals-query-processing` @ `47ffe5d`, settle compiles the
|
||||||
over js/jsx). `compute_highlight_spans` FAILS CLOSED on the
|
grammar's `LOCALS_QUERY`, resolves Tree-sitter's scope/definition/value/
|
||||||
`#is?`/`#is-not? local` property predicate (no locals processing) —
|
reference conventions into sorted `LocalFacts`, and stores them beside
|
||||||
drops those captures so shadowed builtins aren't mis-styled.
|
each layer's tree/query. Work runs once per fresh bundle and only when the
|
||||||
|
highlight query asks about `local`; viewport rendering remains bounded.
|
||||||
|
Both TUI and semantic/GPU producers evaluate `#is?`/`#is-not? local`
|
||||||
|
through the shared capture walk. Non-shadowed JS/TS builtins are restored;
|
||||||
|
shadowed definitions/references keep ordinary variable styling.
|
||||||
- **Multi-language injections (#122) LANDED** — the direct continuation
|
- **Multi-language injections (#122) LANDED** — the direct continuation
|
||||||
of the #114–#118 highlight arc; four review rounds, framing
|
of the #114–#118 highlight arc; four review rounds, framing
|
||||||
`docs/multi-language-injections-framing.md` (Q#IJ1–IJ11). A buffer can
|
`docs/multi-language-injections-framing.md` (Q#IJ1–IJ11). A buffer can
|
||||||
|
|
@ -142,7 +147,8 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
worker never touches the `Rc` registry or Lua);
|
worker never touches the `Rc` registry or Lua);
|
||||||
`ParseTreeBundle.injection_capped` (the 4096-layer backstop, surfaced
|
`ParseTreeBundle.injection_capped` (the 4096-layer backstop, surfaced
|
||||||
once/buffer via `pmacs.error` at settle);
|
once/buffer via `pmacs.error` at settle);
|
||||||
`compute_highlight_spans_for(query, tree, source, range)` (per-layer);
|
`compute_highlight_spans_for(query, tree, source, local_facts, range)`
|
||||||
|
(per-layer);
|
||||||
the wire `flatten_layer_spans` event-sweep → DISJOINT effective spans
|
the wire `flatten_layer_spans` event-sweep → DISJOINT effective spans
|
||||||
(deeper / later-sibling / narrower wins, keyed by `(layer_index,
|
(deeper / later-sibling / narrower wins, keyed by `(layer_index,
|
||||||
capture_order)`); GPU `spans_from_segments` + `source_color_at` fold.
|
capture_order)`); GPU `spans_from_segments` + `source_color_at` fold.
|
||||||
|
|
@ -335,6 +341,10 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
editing/indent/comment items that were config-blocked.
|
editing/indent/comment items that were config-blocked.
|
||||||
- **Mode system wiring COMPLETE (#129)** — major-mode keymaps,
|
- **Mode system wiring COMPLETE (#129)** — major-mode keymaps,
|
||||||
introspection, lifecycle initialization, and statusline display shipped.
|
introspection, lifecycle initialization, and statusline display shipped.
|
||||||
|
- **Locals-query processing IN REVIEW — #134** — grammar locals metadata,
|
||||||
|
lexical resolution, settled per-layer facts, and shared TUI/GPU
|
||||||
|
local-predicate filtering are implemented on `locals-query-processing`
|
||||||
|
@ `47ffe5d`.
|
||||||
- Remaining ranked arcs: 6 folding, 7 DAP, 8 GPU splits, plus the
|
- Remaining ranked arcs: 6 folding, 7 DAP, 8 GPU splits, plus the
|
||||||
`.ipynb` arc (its JSON-grammar prerequisite shipped in #123).
|
`.ipynb` arc (its JSON-grammar prerequisite shipped in #123).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
# Locals-query processing - syntax-highlight completion
|
||||||
|
|
||||||
|
**Status:** Revision 1, implementation active by user direction, 2026-07-22.
|
||||||
|
|
||||||
|
**Base:** `githubsucks/main` at `8bd8298` (mode system #129, Vterm Stage 2
|
||||||
|
#130, modeline detection #132, and landed-state handoff #133). Protocol remains
|
||||||
|
v18.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
pmacs runs each grammar's `highlights.scm` directly through a
|
||||||
|
`tree_sitter::QueryCursor`. That cursor evaluates text predicates such as
|
||||||
|
`#eq?`, `#match?`, and `#any-of?`, but it does not assign or evaluate semantic
|
||||||
|
properties. In particular, JavaScript's bundled highlight query contains:
|
||||||
|
|
||||||
|
```scheme
|
||||||
|
((identifier) @variable.builtin
|
||||||
|
(#match? @variable.builtin "^(arguments|module|console|window|document)$")
|
||||||
|
(#is-not? local))
|
||||||
|
|
||||||
|
((identifier) @function.builtin
|
||||||
|
(#eq? @function.builtin "require")
|
||||||
|
(#is-not? local))
|
||||||
|
```
|
||||||
|
|
||||||
|
A correct highlighter must first run the grammar's `LOCALS_QUERY`, resolve
|
||||||
|
lexical definitions and references, and then apply `#is? local` /
|
||||||
|
`#is-not? local` while selecting highlight captures. Without those facts,
|
||||||
|
a local `console` or `require` can be styled as a builtin.
|
||||||
|
|
||||||
|
The current implementation fails closed by dropping every highlight pattern
|
||||||
|
that carries a `local` property predicate. That prevents the false-positive
|
||||||
|
shadowed-builtin style, but it also drops legitimate builtin highlighting for
|
||||||
|
non-shadowed `console`, `window`, `require`, and peers. This is the first
|
||||||
|
remaining item in the side-quest north-star list at
|
||||||
|
`docs/side-quest-backlog.md:244-249`.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Run the bundled locals query as part of each settled syntax layer, retain a
|
||||||
|
compact lexical-local map, and use that map to evaluate `#is? local` and
|
||||||
|
`#is-not? local` in both syntax-highlight producers:
|
||||||
|
|
||||||
|
1. `highlight::SyntaxHighlightView` (TUI / overlay path), and
|
||||||
|
2. `semantic_render::scoped_style_spans` (semantic/GPU path).
|
||||||
|
|
||||||
|
A shadowed builtin must retain its ordinary fallback capture but lose the
|
||||||
|
builtin refinement. A non-shadowed builtin must regain the builtin capture.
|
||||||
|
The result must stay correct for nested scopes, TypeScript's inherited locals
|
||||||
|
query, injected-language layers, viewport-limited rendering, and edits that
|
||||||
|
settle a new parse bundle.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In
|
||||||
|
|
||||||
|
- Add bundled `locals.scm` fragments to syntax grammar metadata.
|
||||||
|
- Compose inherited locals fragments base-first, exactly as highlight fragments
|
||||||
|
already compose.
|
||||||
|
- Implement Tree-sitter's local-scope conventions:
|
||||||
|
- `@local.scope`
|
||||||
|
- `@local.definition`
|
||||||
|
- `@local.definition-value`
|
||||||
|
- `@local.reference`
|
||||||
|
- `#set! local.scope-inherits false`
|
||||||
|
- Evaluate positive and negative `local` property predicates, including the
|
||||||
|
optional capture-qualified form accepted by Tree-sitter's query parser.
|
||||||
|
- Cache local facts on each settled parse layer.
|
||||||
|
- Feed those facts to both highlight producers.
|
||||||
|
- Cover lexical behavior and end-to-end rendering under both supported Lua
|
||||||
|
backends.
|
||||||
|
|
||||||
|
### Out
|
||||||
|
|
||||||
|
- No new grammar, parser, or language-detection behavior.
|
||||||
|
- No Lua API, command, config key, theme key, protocol field, or frontend-only
|
||||||
|
state.
|
||||||
|
- No LSP semantic-token changes.
|
||||||
|
- No user-defined query registration surface.
|
||||||
|
- No cross-language name resolution between a host layer and an injected
|
||||||
|
child layer.
|
||||||
|
- No Tree-sitter-highlight `reference_highlight` propagation from a local
|
||||||
|
definition's syntactic capture to all references. The target is property
|
||||||
|
predicate correctness. Adding propagation would change ordinary variable
|
||||||
|
styling beyond the reported builtin bug and needs separate framing.
|
||||||
|
- No general engine for arbitrary `#is?` property keys. This side quest owns
|
||||||
|
the Tree-sitter-defined boolean `local` property only; other property
|
||||||
|
predicates retain their current behavior.
|
||||||
|
|
||||||
|
## Existing contracts to preserve
|
||||||
|
|
||||||
|
1. **Layering:** every injection `Layer` owns its grammar tree and highlight
|
||||||
|
query. Deeper layers and later same-depth siblings retain their existing
|
||||||
|
precedence.
|
||||||
|
2. **Viewport work:** the semantic producer's highlight capture walk remains
|
||||||
|
restricted with `QueryCursor::set_byte_range`; rendering a frame must not
|
||||||
|
launch a whole-file locals walk.
|
||||||
|
3. **Settle freshness:** a new parse bundle replaces the old bundle
|
||||||
|
atomically. Local facts must travel with the same bundle, never in a side
|
||||||
|
cache that can pair old scopes with a new tree.
|
||||||
|
4. **Incremental edits:** a completed reparse produces new local facts before
|
||||||
|
the bundle becomes visible. Until settle, producers continue to use the
|
||||||
|
previous internally consistent bundle.
|
||||||
|
5. **Query inheritance:** fragments are newline-joined base-first. Bare
|
||||||
|
concatenation can extend a trailing Scheme comment and is forbidden.
|
||||||
|
6. **Fallback styling:** suppressing a builtin refinement must not suppress
|
||||||
|
an independent ordinary-variable capture for the same identifier.
|
||||||
|
7. **No protocol change:** all work is internal render state.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Q#LQ1 - Grammar metadata carries locals fragments
|
||||||
|
|
||||||
|
`LanguageEntry` gains `locals_query: &'static [&'static str]`, parallel to
|
||||||
|
`highlights_query` and `injections_query`.
|
||||||
|
|
||||||
|
The bundled crates currently exposing `LOCALS_QUERY` are wired as follows:
|
||||||
|
|
||||||
|
| pmacs language | effective locals fragments |
|
||||||
|
| --- | --- |
|
||||||
|
| `lua` | `tree_sitter_lua::LOCALS_QUERY` |
|
||||||
|
| `javascript` | `tree_sitter_javascript::LOCALS_QUERY` |
|
||||||
|
| `javascriptreact` | `tree_sitter_javascript::LOCALS_QUERY` |
|
||||||
|
| `typescript` | JavaScript locals, then TypeScript locals |
|
||||||
|
| `typescriptreact` | JavaScript locals, then TypeScript locals |
|
||||||
|
|
||||||
|
All other entries use an empty slice. TypeScript's locals query is a small
|
||||||
|
parameter-definition delta, so compiling it alone would omit JavaScript's
|
||||||
|
scopes, declarations, and references. JSX uses the JavaScript grammar and
|
||||||
|
therefore the JavaScript locals query. TSX uses the TypeScript crate's TSX
|
||||||
|
language with the same base-plus-delta locals composition.
|
||||||
|
|
||||||
|
`SyntaxRegistry` lazily compiles and caches one effective locals query per
|
||||||
|
language, including cached failure/no-query results, matching the existing
|
||||||
|
highlight-query policy.
|
||||||
|
|
||||||
|
### Q#LQ2 - Local facts follow Tree-sitter lexical semantics
|
||||||
|
|
||||||
|
A locals capture walk maintains a stack of scopes. The stack begins with one
|
||||||
|
non-inheriting root scope covering the layer. Captures are processed in query
|
||||||
|
order and source order:
|
||||||
|
|
||||||
|
1. `@local.scope` pushes a scope covering that node. It inherits outer
|
||||||
|
definitions unless its pattern sets `local.scope-inherits` to `false`.
|
||||||
|
2. `@local.definition` records the identifier in the innermost scope and marks
|
||||||
|
that identifier range local.
|
||||||
|
3. A sibling `@local.definition-value` capture records the initializer/value
|
||||||
|
range. That new definition is not visible while resolving references inside
|
||||||
|
its value, so an outer definition with the same name can still win there.
|
||||||
|
4. `@local.reference` searches definitions newest-first, then scopes
|
||||||
|
innermost-first. Search stops at a non-inheriting scope. A resolved
|
||||||
|
reference range is marked local; an unresolved reference remains non-local.
|
||||||
|
5. Scopes whose end precedes the next capture are popped.
|
||||||
|
|
||||||
|
Definition names are compared as borrowed source byte slices. No identifier
|
||||||
|
strings are allocated. Invalid or out-of-bounds ranges do not produce local
|
||||||
|
facts.
|
||||||
|
|
||||||
|
The result is an opaque `LocalFacts` value containing sorted, deduplicated
|
||||||
|
`(start_byte, end_byte)` ranges. Highlight predicate checks use binary search;
|
||||||
|
they do not hash, copy source text, or rebuild scope state.
|
||||||
|
|
||||||
|
### Q#LQ3 - Predicate evaluation is per emitted capture
|
||||||
|
|
||||||
|
Before emitting a highlight capture, inspect
|
||||||
|
`Query::property_predicates(pattern_index)`:
|
||||||
|
|
||||||
|
- `#is? local` passes only when the selected node is in `LocalFacts`.
|
||||||
|
- `#is-not? local` passes only when the selected node is not in `LocalFacts`.
|
||||||
|
- If the property names a capture, test that capture's node.
|
||||||
|
- If it does not name a capture, test the highlight capture currently being
|
||||||
|
considered, matching Tree-sitter-highlight's per-node behavior.
|
||||||
|
- Multiple `local` predicates on one pattern are conjunctive.
|
||||||
|
- A missing or failed locals query yields no local ranges: negative predicates
|
||||||
|
pass and positive predicates fail. This preserves useful non-local
|
||||||
|
highlighting without inventing local classifications.
|
||||||
|
- Predicates with keys other than `local` remain ignored, preserving the
|
||||||
|
current query engine's scope.
|
||||||
|
|
||||||
|
Text predicates remain the query cursor's responsibility. Property settings
|
||||||
|
such as `local.scope-inherits` are read only by local analysis; they are not
|
||||||
|
mistaken for highlight predicates.
|
||||||
|
|
||||||
|
### Q#LQ4 - Facts are computed once at settle and owned by the layer
|
||||||
|
|
||||||
|
`SyntaxRegistry::resolve_layer_queries` remains the main-thread Stage 2 handoff.
|
||||||
|
For each raw worker layer it:
|
||||||
|
|
||||||
|
1. resolves the cached highlight query;
|
||||||
|
2. checks whether that query contains any `local` property predicate;
|
||||||
|
3. only when needed, resolves the cached locals query and walks the layer tree;
|
||||||
|
4. stores `Arc<LocalFacts>` beside the layer's tree and highlight query.
|
||||||
|
|
||||||
|
This is a single whole-layer analysis per completed parse, not per render.
|
||||||
|
Languages whose highlights never ask about `local` pay only the cheap predicate
|
||||||
|
scan and carry no facts. This includes Lua today even though its locals query
|
||||||
|
is correctly registered for future local-sensitive highlight patterns.
|
||||||
|
|
||||||
|
Putting facts on `Layer` makes the consistency invariant structural:
|
||||||
|
|
||||||
|
```text
|
||||||
|
settled Layer = tree + grammar + highlight query + local facts
|
||||||
|
```
|
||||||
|
|
||||||
|
A producer cannot accidentally retrieve facts for another buffer revision.
|
||||||
|
Child injection layers compute facts against their own tree and grammar; local
|
||||||
|
bindings never cross layer boundaries. A deliberately combined injection
|
||||||
|
layer shares one tree and therefore one lexical environment, matching its
|
||||||
|
combined parse semantics.
|
||||||
|
|
||||||
|
### Q#LQ5 - Both producers share one capture-selection function
|
||||||
|
|
||||||
|
`compute_highlight_spans_for` accepts the layer's optional local facts and
|
||||||
|
owns predicate evaluation. The whole-buffer overlay path and the
|
||||||
|
viewport-limited semantic path both call this function. There is no second
|
||||||
|
predicate implementation in a frontend.
|
||||||
|
|
||||||
|
`compute_highlight_spans_in_range` passes the root layer's facts. Injection
|
||||||
|
producers pass each layer's facts. Existing wider-first ordering and
|
||||||
|
cross-layer merge precedence remain unchanged after captures are selected.
|
||||||
|
|
||||||
|
### Q#LQ6 - Performance boundary
|
||||||
|
|
||||||
|
The locals-query capture walk is linear for one settled layer. Like upstream,
|
||||||
|
resolving references scans visible definitions newest-first, so the worst case
|
||||||
|
is $O(\text{captures} + \text{references} \times \text{definitions})$.
|
||||||
|
It runs only for a language whose highlight query actually contains a `local`
|
||||||
|
predicate, and only once when a fresh parse bundle settles.
|
||||||
|
|
||||||
|
The render hot paths remain:
|
||||||
|
|
||||||
|
- TUI: cached whole-layer highlight spans, rebuilt only when the bundle pointer
|
||||||
|
changes;
|
||||||
|
- semantic/GPU: viewport-bounded highlight query plus binary-search local
|
||||||
|
checks.
|
||||||
|
|
||||||
|
No frame performs a whole-file locals query. Memory is two `u32` offsets per
|
||||||
|
local definition/resolved reference plus vector capacity; ranges are sorted
|
||||||
|
and deduplicated before storage.
|
||||||
|
|
||||||
|
Incremental locals invalidation is intentionally bundle-granular. A local edit
|
||||||
|
can alter all later name resolution in a scope, so attempting to splice only
|
||||||
|
changed ranges without a scope dependency graph risks stale classifications.
|
||||||
|
The parse itself remains incremental; this bounded lexical pass is the boring,
|
||||||
|
correct cutover.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
worker parse
|
||||||
|
-> raw ParseTreeBundle { Layer { tree, language, no queries/facts } }
|
||||||
|
-> main-thread resolve_layer_queries
|
||||||
|
-> cached highlights query
|
||||||
|
-> cached locals query (only if highlights uses local predicates)
|
||||||
|
-> lexical capture walk -> sorted LocalFacts
|
||||||
|
-> settled ParseTreeBundle
|
||||||
|
-> TUI SyntaxHighlightView cache rebuild
|
||||||
|
-> semantic/GPU viewport capture walk
|
||||||
|
-> shared local predicate filter
|
||||||
|
-> existing span ordering/merge/theme lookup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
1. **Non-shadowed builtin restored:** JavaScript `console`, `window`, or
|
||||||
|
`require` with no matching lexical definition emits its bundled
|
||||||
|
`*.builtin` capture.
|
||||||
|
2. **Shadowed builtin suppressed:** a parameter or local declaration named
|
||||||
|
`console`/`require` and references resolved to it do not emit a builtin
|
||||||
|
capture; their ordinary variable captures remain.
|
||||||
|
3. **Scope correctness:** shadowing is confined to its lexical scope. A builtin
|
||||||
|
before/after the scope remains builtin, while the definition and references
|
||||||
|
inside are local.
|
||||||
|
4. **Positive predicate:** a focused custom highlight query using `#is? local`
|
||||||
|
emits resolved definitions/references and rejects an unresolved identifier.
|
||||||
|
5. **TypeScript inheritance:** parameter shadowing in TypeScript and TSX uses
|
||||||
|
the JavaScript base locals query plus the TypeScript delta; query compilation
|
||||||
|
and classification succeed for both grammars.
|
||||||
|
6. **End-to-end render:** opening a JavaScript buffer through the shipped
|
||||||
|
runtime and applying a theme that styles only `variable.builtin` renders an
|
||||||
|
unshadowed builtin with that style and a shadowed occurrence without it.
|
||||||
|
7. **Edit freshness:** after changing a shadowing identifier and settling the
|
||||||
|
new parse, the next render reflects the new local/non-local classification;
|
||||||
|
no stale local facts survive.
|
||||||
|
8. **Producer coverage:** both the overlay and semantic/GPU callsites pass the
|
||||||
|
corresponding layer facts to the shared capture walk; injected layers keep
|
||||||
|
their own facts.
|
||||||
|
9. **Backend parity:** the focused acceptance test passes under default Luau
|
||||||
|
and `--no-default-features --features lua54`.
|
||||||
|
10. **No regressions:** formatting, Clippy, default library tests, CRDT library
|
||||||
|
tests, M4 acceptance (excluding the machine-broken basedpyright case),
|
||||||
|
required GPU tests, workspace sweep, and `git diff --check` pass.
|
||||||
|
|
||||||
|
## Expected files
|
||||||
|
|
||||||
|
- `src/syntax.rs` - grammar metadata, locals-query cache, lexical analysis,
|
||||||
|
layer facts, predicate selection, unit coverage.
|
||||||
|
- `src/highlight.rs` - pass per-layer facts to the overlay producer.
|
||||||
|
- `src/semantic_render.rs` - pass per-layer facts to the viewport producer.
|
||||||
|
- `tests/m4_acceptance.rs` - end-to-end local/non-local rendering and edit
|
||||||
|
freshness.
|
||||||
|
- `docs/agent-handoff.md` and `docs/active-work.md` - updated only after the
|
||||||
|
implementation is proven and published according to their protocols.
|
||||||
|
|
||||||
|
No other runtime, Lua, frontend, protocol, or theme file should need a behavior
|
||||||
|
change.
|
||||||
|
|
@ -374,7 +374,13 @@ impl SyntaxHighlightView {
|
||||||
let Some(query) = layer.highlight_query.as_ref() else {
|
let Some(query) = layer.highlight_query.as_ref() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let spans = compute_highlight_spans_for(query, &layer.tree, source, None);
|
let spans = compute_highlight_spans_for(
|
||||||
|
query,
|
||||||
|
&layer.tree,
|
||||||
|
source,
|
||||||
|
layer.local_facts.as_deref(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
if spans.is_empty() {
|
if spans.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1964,6 +1964,7 @@ fn scoped_style_spans(state: &EditorState, vp: &DeclaredViewport) -> Vec<StyleSp
|
||||||
query,
|
query,
|
||||||
&layer.tree,
|
&layer.tree,
|
||||||
source,
|
source,
|
||||||
|
layer.local_facts.as_deref(),
|
||||||
Some(vis_start as usize..vis_end as usize),
|
Some(vis_start as usize..vis_end as usize),
|
||||||
);
|
);
|
||||||
for (order, hs) in highlights.iter().enumerate() {
|
for (order, hs) in highlights.iter().enumerate() {
|
||||||
|
|
@ -3918,15 +3919,17 @@ mod tests {
|
||||||
sid
|
sid
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed_rust_parse_view(
|
fn seed_parse_view(
|
||||||
state: &EditorState,
|
state: &EditorState,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
text: &[u8],
|
text: &[u8],
|
||||||
|
language_name: &str,
|
||||||
|
path: &str,
|
||||||
) -> crate::syntax::ParseViewHandle {
|
) -> crate::syntax::ParseViewHandle {
|
||||||
let language = state
|
let language = state
|
||||||
.syntax_registry
|
.syntax_registry
|
||||||
.language("rust")
|
.language(language_name)
|
||||||
.expect("rust language");
|
.unwrap_or_else(|| panic!("{language_name} language"));
|
||||||
let mut core = state.core.borrow_mut();
|
let mut core = state.core.borrow_mut();
|
||||||
let registry_handle = core.registry.clone();
|
let registry_handle = core.registry.clone();
|
||||||
let mut registry = registry_handle.borrow_mut();
|
let mut registry = registry_handle.borrow_mut();
|
||||||
|
|
@ -3936,23 +3939,31 @@ mod tests {
|
||||||
pos: 0,
|
pos: 0,
|
||||||
bytes: text,
|
bytes: text,
|
||||||
})
|
})
|
||||||
.expect("seed rust text");
|
.expect("seed syntax text");
|
||||||
}
|
}
|
||||||
let parse_view = crate::syntax::ParseView::new(buf, language, "rust".to_owned());
|
let parse_view = crate::syntax::ParseView::new(buf, language, language_name.to_owned());
|
||||||
let handle = parse_view.handle();
|
let handle = parse_view.handle();
|
||||||
let req = handle.make_request();
|
let req = handle.make_request();
|
||||||
let bundle = crate::syntax::run_parse(req).expect("initial rust parse");
|
let bundle = crate::syntax::run_parse(req).expect("initial syntax parse");
|
||||||
// Mirror the production settle path: resolve each layer's highlight
|
// Mirror the production settle path: queries and lexical facts travel
|
||||||
// query before install so the producer can style it (framing Q#IJ2).
|
// with the same bundle the producer reads.
|
||||||
handle.install(state.syntax_registry.resolve_layer_queries(&bundle));
|
handle.install(state.syntax_registry.resolve_layer_queries(&bundle));
|
||||||
buf.attach_view(Box::new(parse_view));
|
buf.attach_view(Box::new(parse_view));
|
||||||
drop(registry);
|
drop(registry);
|
||||||
core.set_buffer_path(buffer_id, Some(std::path::PathBuf::from("/tmp/x.rs")));
|
core.set_buffer_path(buffer_id, Some(std::path::PathBuf::from(path)));
|
||||||
drop(core);
|
drop(core);
|
||||||
state.syntax_registry.attach_view(buffer_id, handle.clone());
|
state.syntax_registry.attach_view(buffer_id, handle.clone());
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn seed_rust_parse_view(
|
||||||
|
state: &EditorState,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
text: &[u8],
|
||||||
|
) -> crate::syntax::ParseViewHandle {
|
||||||
|
seed_parse_view(state, buffer_id, text, "rust", "/tmp/x.rs")
|
||||||
|
}
|
||||||
|
|
||||||
fn seed_markdown_parse_view(
|
fn seed_markdown_parse_view(
|
||||||
state: &EditorState,
|
state: &EditorState,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
|
|
@ -4037,6 +4048,57 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn viewport_style_producer_uses_settled_local_facts() {
|
||||||
|
let state = empty_state();
|
||||||
|
let buffer_id = active_buffer(&state);
|
||||||
|
let source = b"console;\nfunction f(console) { console; }\n";
|
||||||
|
seed_parse_view(&state, buffer_id, source, "javascript", "/tmp/locals.js");
|
||||||
|
state.syntax_registry.theme().lock().expect("theme").insert(
|
||||||
|
"variable.builtin",
|
||||||
|
Style {
|
||||||
|
fg: crate::cell::Color::Indexed(6),
|
||||||
|
..Style::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let style_at = |visible: ByteRange, offset: u64| {
|
||||||
|
scoped_style_spans(
|
||||||
|
&state,
|
||||||
|
&DeclaredViewport {
|
||||||
|
buffer_id,
|
||||||
|
visible,
|
||||||
|
frontend_generation: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.find(|span| span.range.start <= offset && offset < span.range.end)
|
||||||
|
.map_or_else(Style::default, |span| span.style)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
style_at(ByteRange { start: 0, end: 7 }, 0).fg,
|
||||||
|
crate::cell::Color::Indexed(6),
|
||||||
|
"unresolved outer `console` receives the builtin capture"
|
||||||
|
);
|
||||||
|
let inner = source
|
||||||
|
.windows("console".len())
|
||||||
|
.rposition(|window| window == b"console")
|
||||||
|
.expect("inner console") as u64;
|
||||||
|
assert_ne!(
|
||||||
|
style_at(
|
||||||
|
ByteRange {
|
||||||
|
start: inner,
|
||||||
|
end: inner + "console".len() as u64,
|
||||||
|
},
|
||||||
|
inner,
|
||||||
|
)
|
||||||
|
.fg,
|
||||||
|
crate::cell::Color::Indexed(6),
|
||||||
|
"viewport-only highlighting still sees the parameter definition outside the viewport"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_buffer_summary_flatten_scales_on_large_grammar_file() {
|
fn full_buffer_summary_flatten_scales_on_large_grammar_file() {
|
||||||
// Perf gate (round-1 finding 1): the file-style summary runs the
|
// Perf gate (round-1 finding 1): the file-style summary runs the
|
||||||
|
|
|
||||||
645
src/syntax.rs
645
src/syntax.rs
|
|
@ -115,6 +115,14 @@ pub struct ParseTreeBundle {
|
||||||
pub injection_capped: bool,
|
pub injection_capped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lexically-local identifier ranges derived from a grammar's bundled
|
||||||
|
/// `locals.scm` query. Ranges are sorted and deduplicated so highlight
|
||||||
|
/// predicate checks are allocation-free binary searches.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct LocalFacts {
|
||||||
|
ranges: Box<[(u32, u32)]>,
|
||||||
|
}
|
||||||
|
|
||||||
/// One injection layer within a [`ParseTreeBundle`] (framing Q#IJ1). A
|
/// One injection layer within a [`ParseTreeBundle`] (framing Q#IJ1). A
|
||||||
/// layer pairs a parse tree with the language that produced it and the
|
/// layer pairs a parse tree with the language that produced it and the
|
||||||
/// injection-nesting depth (root = 0). `highlight_query` is resolved on
|
/// injection-nesting depth (root = 0). `highlight_query` is resolved on
|
||||||
|
|
@ -133,6 +141,10 @@ pub struct Layer {
|
||||||
/// `None` when the language ships no highlights, or on the worker
|
/// `None` when the language ships no highlights, or on the worker
|
||||||
/// (pre-settle). Producers read it to style this layer.
|
/// (pre-settle). Producers read it to style this layer.
|
||||||
pub highlight_query: Option<Arc<tree_sitter::Query>>,
|
pub highlight_query: Option<Arc<tree_sitter::Query>>,
|
||||||
|
/// Lexically-local definitions and resolved references for this tree.
|
||||||
|
/// Present only when the highlight query asks about the `local`
|
||||||
|
/// property; computed once when the bundle settles.
|
||||||
|
pub local_facts: Option<Arc<LocalFacts>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParseTreeBundle {
|
impl ParseTreeBundle {
|
||||||
|
|
@ -184,6 +196,7 @@ pub fn run_parse(req: ParseRequest) -> Result<ParseTreeBundle, String> {
|
||||||
tree: root_tree,
|
tree: root_tree,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
highlight_query: None,
|
highlight_query: None,
|
||||||
|
local_facts: None,
|
||||||
}];
|
}];
|
||||||
let injection_capped =
|
let injection_capped =
|
||||||
build_injection_layers(&mut layers, req.source.as_ref(), &req.injection_aliases);
|
build_injection_layers(&mut layers, req.source.as_ref(), &req.injection_aliases);
|
||||||
|
|
@ -309,6 +322,7 @@ fn build_injection_layers(
|
||||||
tree,
|
tree,
|
||||||
depth: depth + 1,
|
depth: depth + 1,
|
||||||
highlight_query: None,
|
highlight_query: None,
|
||||||
|
local_facts: None,
|
||||||
},
|
},
|
||||||
ranges,
|
ranges,
|
||||||
));
|
));
|
||||||
|
|
@ -773,6 +787,10 @@ pub struct LanguageEntry {
|
||||||
/// slice (or all-empty fragments) means no highlights: the view
|
/// slice (or all-empty fragments) means no highlights: the view
|
||||||
/// runs but emits nothing.
|
/// runs but emits nothing.
|
||||||
pub highlights_query: &'static [&'static str],
|
pub highlights_query: &'static [&'static str],
|
||||||
|
/// Bundled `locals.scm` query fragments, composed base-first like
|
||||||
|
/// [`Self::highlights_query`]. The query supplies lexical scopes,
|
||||||
|
/// definitions, values, and references for `local` property predicates.
|
||||||
|
pub locals_query: &'static [&'static str],
|
||||||
/// Bundled `injections.scm` fragments (framing Q#IJ2), joined with a
|
/// Bundled `injections.scm` fragments (framing Q#IJ2), joined with a
|
||||||
/// newline and compiled on the parse worker to find embedded-language
|
/// newline and compiled on the parse worker to find embedded-language
|
||||||
/// regions. Empty for the many grammars that ship none (or don't
|
/// regions. Empty for the many grammars that ship none (or don't
|
||||||
|
|
@ -801,6 +819,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["rs"],
|
extensions: &["rs"],
|
||||||
loader: || tree_sitter_rust::LANGUAGE.into(),
|
loader: || tree_sitter_rust::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_rust::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_rust::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[tree_sitter_rust::INJECTIONS_QUERY],
|
injections_query: &[tree_sitter_rust::INJECTIONS_QUERY],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -808,6 +827,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["lua"],
|
extensions: &["lua"],
|
||||||
loader: || tree_sitter_lua::LANGUAGE.into(),
|
loader: || tree_sitter_lua::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_lua::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_lua::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[tree_sitter_lua::LOCALS_QUERY],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// T M9.7: markdown block grammar (`tree_sitter_md::LANGUAGE`) — headers,
|
// T M9.7: markdown block grammar (`tree_sitter_md::LANGUAGE`) — headers,
|
||||||
|
|
@ -824,6 +844,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["md", "markdown"],
|
extensions: &["md", "markdown"],
|
||||||
loader: || tree_sitter_md::LANGUAGE.into(),
|
loader: || tree_sitter_md::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_BLOCK],
|
highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_BLOCK],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[tree_sitter_md::INJECTION_QUERY_BLOCK],
|
injections_query: &[tree_sitter_md::INJECTION_QUERY_BLOCK],
|
||||||
},
|
},
|
||||||
// markdown_inline (framing Q#IJ10) — the inline grammar the block
|
// markdown_inline (framing Q#IJ10) — the inline grammar the block
|
||||||
|
|
@ -837,6 +858,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &[],
|
extensions: &[],
|
||||||
loader: || tree_sitter_md::INLINE_LANGUAGE.into(),
|
loader: || tree_sitter_md::INLINE_LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_INLINE],
|
highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_INLINE],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[tree_sitter_md::INJECTION_QUERY_INLINE],
|
injections_query: &[tree_sitter_md::INJECTION_QUERY_INLINE],
|
||||||
},
|
},
|
||||||
// T M_B3 — C / C++. Lexical highlighting (keywords / strings /
|
// T M_B3 — C / C++. Lexical highlighting (keywords / strings /
|
||||||
|
|
@ -859,6 +881,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["c", "h"],
|
extensions: &["c", "h"],
|
||||||
loader: || tree_sitter_c::LANGUAGE.into(),
|
loader: || tree_sitter_c::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_c::HIGHLIGHT_QUERY],
|
highlights_query: &[tree_sitter_c::HIGHLIGHT_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -866,6 +889,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cppm"],
|
extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cppm"],
|
||||||
loader: || tree_sitter_cpp::LANGUAGE.into(),
|
loader: || tree_sitter_cpp::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_cpp::HIGHLIGHT_QUERY],
|
highlights_query: &[tree_sitter_cpp::HIGHLIGHT_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// CUDA (`.cu` source, `.cuh` header). A dedicated grammar rather
|
// CUDA (`.cu` source, `.cuh` header). A dedicated grammar rather
|
||||||
|
|
@ -897,6 +921,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
tree_sitter_cpp::HIGHLIGHT_QUERY,
|
tree_sitter_cpp::HIGHLIGHT_QUERY,
|
||||||
tree_sitter_cuda::HIGHLIGHTS_QUERY,
|
tree_sitter_cuda::HIGHLIGHTS_QUERY,
|
||||||
],
|
],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// Shell / bash. Lexical highlighting for the shell family; the LSP
|
// Shell / bash. Lexical highlighting for the shell family; the LSP
|
||||||
|
|
@ -915,6 +940,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["sh", "bash", "zsh", "ksh", "ash", "bats"],
|
extensions: &["sh", "bash", "zsh", "ksh", "ash", "bats"],
|
||||||
loader: || tree_sitter_bash::LANGUAGE.into(),
|
loader: || tree_sitter_bash::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_bash::HIGHLIGHT_QUERY],
|
highlights_query: &[tree_sitter_bash::HIGHLIGHT_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// Filename-identified languages. These files usually have no useful
|
// Filename-identified languages. These files usually have no useful
|
||||||
|
|
@ -931,6 +957,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["dockerfile", "containerfile"],
|
extensions: &["dockerfile", "containerfile"],
|
||||||
loader: || tree_sitter_containerfile::LANGUAGE.into(),
|
loader: || tree_sitter_containerfile::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_containerfile::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_containerfile::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -938,6 +965,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["mk", "make"],
|
extensions: &["mk", "make"],
|
||||||
loader: || tree_sitter_make::LANGUAGE.into(),
|
loader: || tree_sitter_make::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_make::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_make::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -945,6 +973,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["cmake"],
|
extensions: &["cmake"],
|
||||||
loader: || tree_sitter_cmake::LANGUAGE.into(),
|
loader: || tree_sitter_cmake::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_cmake::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_cmake::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// Grammar-gap languages — these already had LSP configs but no
|
// Grammar-gap languages — these already had LSP configs but no
|
||||||
|
|
@ -958,6 +987,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["py", "pyi"],
|
extensions: &["py", "pyi"],
|
||||||
loader: || tree_sitter_python::LANGUAGE.into(),
|
loader: || tree_sitter_python::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_python::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_python::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -965,6 +995,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["go"],
|
extensions: &["go"],
|
||||||
loader: || tree_sitter_go::LANGUAGE.into(),
|
loader: || tree_sitter_go::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_go::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_go::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// JavaScript / TypeScript. One `tree-sitter-javascript` grammar parses
|
// JavaScript / TypeScript. One `tree-sitter-javascript` grammar parses
|
||||||
|
|
@ -980,6 +1011,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["js", "mjs", "cjs"],
|
extensions: &["js", "mjs", "cjs"],
|
||||||
loader: || tree_sitter_javascript::LANGUAGE.into(),
|
loader: || tree_sitter_javascript::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_javascript::HIGHLIGHT_QUERY],
|
highlights_query: &[tree_sitter_javascript::HIGHLIGHT_QUERY],
|
||||||
|
locals_query: &[tree_sitter_javascript::LOCALS_QUERY],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -990,6 +1022,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||||
tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
|
tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
|
||||||
],
|
],
|
||||||
|
locals_query: &[tree_sitter_javascript::LOCALS_QUERY],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -1000,6 +1033,10 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||||
],
|
],
|
||||||
|
locals_query: &[
|
||||||
|
tree_sitter_javascript::LOCALS_QUERY,
|
||||||
|
tree_sitter_typescript::LOCALS_QUERY,
|
||||||
|
],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -1011,6 +1048,10 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
|
tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
|
||||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||||
],
|
],
|
||||||
|
locals_query: &[
|
||||||
|
tree_sitter_javascript::LOCALS_QUERY,
|
||||||
|
tree_sitter_typescript::LOCALS_QUERY,
|
||||||
|
],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -1018,6 +1059,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["toml"],
|
extensions: &["toml"],
|
||||||
loader: || tree_sitter_toml_ng::LANGUAGE.into(),
|
loader: || tree_sitter_toml_ng::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_toml_ng::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_toml_ng::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -1025,6 +1067,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["zig", "zon"],
|
extensions: &["zig", "zon"],
|
||||||
loader: || tree_sitter_zig::LANGUAGE.into(),
|
loader: || tree_sitter_zig::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_zig::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_zig::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
// JSON + YAML — config formats, both self-contained highlights and no
|
// JSON + YAML — config formats, both self-contained highlights and no
|
||||||
|
|
@ -1039,6 +1082,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["json"],
|
extensions: &["json"],
|
||||||
loader: || tree_sitter_json::LANGUAGE.into(),
|
loader: || tree_sitter_json::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_json::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_json::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
LanguageEntry {
|
LanguageEntry {
|
||||||
|
|
@ -1046,6 +1090,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
||||||
extensions: &["yaml", "yml"],
|
extensions: &["yaml", "yml"],
|
||||||
loader: || tree_sitter_yaml::LANGUAGE.into(),
|
loader: || tree_sitter_yaml::LANGUAGE.into(),
|
||||||
highlights_query: &[tree_sitter_yaml::HIGHLIGHTS_QUERY],
|
highlights_query: &[tree_sitter_yaml::HIGHLIGHTS_QUERY],
|
||||||
|
locals_query: &[],
|
||||||
injections_query: &[],
|
injections_query: &[],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -1076,6 +1121,9 @@ pub struct SyntaxRegistry {
|
||||||
/// compilation failure (e.g. grammar / query ABI skew) is
|
/// compilation failure (e.g. grammar / query ABI skew) is
|
||||||
/// cached as `Err(message)` so we don't burn cycles re-trying.
|
/// cached as `Err(message)` so we don't burn cycles re-trying.
|
||||||
queries: RefCell<HashMap<String, Result<Arc<tree_sitter::Query>, String>>>,
|
queries: RefCell<HashMap<String, Result<Arc<tree_sitter::Query>, String>>>,
|
||||||
|
/// Compiled `locals.scm` query per language. Like `queries`, both
|
||||||
|
/// compilation failures and absent query sources are cached.
|
||||||
|
local_queries: RefCell<HashMap<String, Result<Arc<tree_sitter::Query>, String>>>,
|
||||||
/// Fence-name → canonical-language alias map (framing Q#IJ4). Seeded
|
/// Fence-name → canonical-language alias map (framing Q#IJ4). Seeded
|
||||||
/// with [`default_injection_aliases`]; Lua adds to it through
|
/// with [`default_injection_aliases`]; Lua adds to it through
|
||||||
/// [`Self::register_injection_alias`]. Snapshotted into each
|
/// [`Self::register_injection_alias`]. Snapshotted into each
|
||||||
|
|
@ -1107,6 +1155,7 @@ impl SyntaxRegistry {
|
||||||
parse_jobs: RefCell::new(HashMap::new()),
|
parse_jobs: RefCell::new(HashMap::new()),
|
||||||
extra_extensions: RefCell::new(HashMap::new()),
|
extra_extensions: RefCell::new(HashMap::new()),
|
||||||
queries: RefCell::new(HashMap::new()),
|
queries: RefCell::new(HashMap::new()),
|
||||||
|
local_queries: RefCell::new(HashMap::new()),
|
||||||
injection_aliases: RefCell::new(default_injection_aliases()),
|
injection_aliases: RefCell::new(default_injection_aliases()),
|
||||||
theme: Arc::new(Mutex::new(Theme::default_dark())),
|
theme: Arc::new(Mutex::new(Theme::default_dark())),
|
||||||
}
|
}
|
||||||
|
|
@ -1273,6 +1322,32 @@ impl SyntaxRegistry {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lazy-compile and cache the bundled `locals.scm` query for
|
||||||
|
/// `lang_name`. Empty sources and compilation failures are cached.
|
||||||
|
#[must_use]
|
||||||
|
pub fn locals_query(&self, lang_name: &str) -> Option<Arc<tree_sitter::Query>> {
|
||||||
|
if let Some(slot) = self.local_queries.borrow().get(lang_name) {
|
||||||
|
return slot.as_ref().ok().cloned();
|
||||||
|
}
|
||||||
|
let language = self.language(lang_name)?;
|
||||||
|
let entry = BUILTIN_LANGUAGES.iter().find(|e| e.name == lang_name);
|
||||||
|
let source = entry.map_or_else(String::new, |e| e.locals_query.join("\n"));
|
||||||
|
if source.trim().is_empty() {
|
||||||
|
self.local_queries
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(lang_name.to_owned(), Err("no locals query".to_owned()));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let compiled = tree_sitter::Query::new(&language, &source)
|
||||||
|
.map(Arc::new)
|
||||||
|
.map_err(|e| format!("compile {lang_name} locals: {e:?}"));
|
||||||
|
let result = compiled.as_ref().ok().cloned();
|
||||||
|
self.local_queries
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(lang_name.to_owned(), compiled);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Add or override a fence-name → language alias (framing Q#IJ4). The
|
/// Add or override a fence-name → language alias (framing Q#IJ4). The
|
||||||
/// alias key is case-folded to match the resolver. Called from Lua via
|
/// alias key is case-folded to match the resolver. Called from Lua via
|
||||||
/// `pmacs.parse.injection_aliases`.
|
/// `pmacs.parse.injection_aliases`.
|
||||||
|
|
@ -1299,11 +1374,26 @@ impl SyntaxRegistry {
|
||||||
let layers = raw
|
let layers = raw
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| Layer {
|
.map(|layer| {
|
||||||
language_name: l.language_name.clone(),
|
let highlight_query = self.highlights_query(&layer.language_name);
|
||||||
tree: l.tree.clone(),
|
let local_facts = highlight_query
|
||||||
depth: l.depth,
|
.as_deref()
|
||||||
highlight_query: self.highlights_query(&l.language_name),
|
.filter(|query| query_uses_local_predicates(query))
|
||||||
|
.and_then(|_| self.locals_query(&layer.language_name))
|
||||||
|
.map(|query| {
|
||||||
|
Arc::new(compute_local_facts(
|
||||||
|
&query,
|
||||||
|
&layer.tree,
|
||||||
|
raw.source.as_ref(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
Layer {
|
||||||
|
language_name: layer.language_name.clone(),
|
||||||
|
tree: layer.tree.clone(),
|
||||||
|
depth: layer.depth,
|
||||||
|
highlight_query,
|
||||||
|
local_facts,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Arc::new(ParseTreeBundle {
|
Arc::new(ParseTreeBundle {
|
||||||
|
|
@ -1316,6 +1406,148 @@ impl SyntaxRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_uses_local_predicates(query: &tree_sitter::Query) -> bool {
|
||||||
|
(0..query.pattern_count()).any(|pattern| {
|
||||||
|
query
|
||||||
|
.property_predicates(pattern)
|
||||||
|
.iter()
|
||||||
|
.any(|(property, _)| property.key.as_ref() == "local")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct LocalDefinition {
|
||||||
|
name_range: std::ops::Range<usize>,
|
||||||
|
value_range: std::ops::Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct LocalScope {
|
||||||
|
inherits: bool,
|
||||||
|
range: std::ops::Range<usize>,
|
||||||
|
definitions: Vec<LocalDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalFacts {
|
||||||
|
fn contains(&self, start_byte: usize, end_byte: usize) -> bool {
|
||||||
|
let (Ok(start_byte), Ok(end_byte)) = (u32::try_from(start_byte), u32::try_from(end_byte))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
self.ranges.binary_search(&(start_byte, end_byte)).is_ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve lexical definitions and references according to Tree-sitter's
|
||||||
|
/// standard `locals.scm` capture conventions.
|
||||||
|
fn compute_local_facts(
|
||||||
|
query: &tree_sitter::Query,
|
||||||
|
tree: &tree_sitter::Tree,
|
||||||
|
source: &[u8],
|
||||||
|
) -> LocalFacts {
|
||||||
|
let scope_capture = query.capture_index_for_name("local.scope");
|
||||||
|
let definition_capture = query.capture_index_for_name("local.definition");
|
||||||
|
let value_capture = query.capture_index_for_name("local.definition-value");
|
||||||
|
let reference_capture = query.capture_index_for_name("local.reference");
|
||||||
|
|
||||||
|
let mut scopes = vec![LocalScope {
|
||||||
|
inherits: false,
|
||||||
|
range: 0..source.len(),
|
||||||
|
definitions: Vec::new(),
|
||||||
|
}];
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
let mut cursor = tree_sitter::QueryCursor::new();
|
||||||
|
let mut captures = cursor.captures(query, tree.root_node(), source);
|
||||||
|
|
||||||
|
while let Some((query_match, capture_index)) = captures.next() {
|
||||||
|
let capture = query_match.captures[*capture_index];
|
||||||
|
let node_range = capture.node.byte_range();
|
||||||
|
while scopes.len() > 1
|
||||||
|
&& node_range.start > scopes.last().expect("root scope exists").range.end
|
||||||
|
{
|
||||||
|
scopes.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if Some(capture.index) == scope_capture {
|
||||||
|
let mut inherits = true;
|
||||||
|
for property in query.property_settings(query_match.pattern_index) {
|
||||||
|
if property.key.as_ref() == "local.scope-inherits" {
|
||||||
|
inherits = property
|
||||||
|
.value
|
||||||
|
.as_deref()
|
||||||
|
.is_none_or(|value| value == "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scopes.push(LocalScope {
|
||||||
|
inherits,
|
||||||
|
range: node_range,
|
||||||
|
definitions: Vec::new(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Some(capture.index) == definition_capture {
|
||||||
|
let Some(_) = source.get(node_range.clone()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value_range = query_match
|
||||||
|
.captures
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| Some(candidate.index) == value_capture)
|
||||||
|
.map_or(0..0, |candidate| candidate.node.byte_range());
|
||||||
|
scopes
|
||||||
|
.last_mut()
|
||||||
|
.expect("root scope exists")
|
||||||
|
.definitions
|
||||||
|
.push(LocalDefinition {
|
||||||
|
name_range: node_range.clone(),
|
||||||
|
value_range,
|
||||||
|
});
|
||||||
|
if let (Ok(start), Ok(end)) = (
|
||||||
|
u32::try_from(node_range.start),
|
||||||
|
u32::try_from(node_range.end),
|
||||||
|
) {
|
||||||
|
ranges.push((start, end));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Some(capture.index) != reference_capture {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(name) = source.get(node_range.clone()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut resolved = false;
|
||||||
|
for scope in scopes.iter().rev() {
|
||||||
|
if scope.definitions.iter().rev().any(|definition| {
|
||||||
|
node_range.start >= definition.value_range.end
|
||||||
|
&& source.get(definition.name_range.clone()) == Some(name)
|
||||||
|
}) {
|
||||||
|
resolved = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !scope.inherits {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resolved
|
||||||
|
&& let (Ok(start), Ok(end)) = (
|
||||||
|
u32::try_from(node_range.start),
|
||||||
|
u32::try_from(node_range.end),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ranges.push((start, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.sort_unstable();
|
||||||
|
ranges.dedup();
|
||||||
|
LocalFacts {
|
||||||
|
ranges: ranges.into_boxed_slice(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for SyntaxRegistry {
|
impl Default for SyntaxRegistry {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
|
|
@ -1418,20 +1650,22 @@ pub fn compute_highlight_spans_in_range(
|
||||||
query,
|
query,
|
||||||
bundle.root_tree(),
|
bundle.root_tree(),
|
||||||
bundle.source.as_ref(),
|
bundle.source.as_ref(),
|
||||||
|
bundle.layers[0].local_facts.as_deref(),
|
||||||
byte_range,
|
byte_range,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`compute_highlight_spans_in_range`] but over an explicit
|
/// Like [`compute_highlight_spans_in_range`] but over an explicit
|
||||||
/// `(tree, source)` — the per-layer form the producers call for each
|
/// `(tree, source, local_facts)` layer tuple — the form producers call for
|
||||||
/// injection layer (framing Q#IJ7). `source` is the whole buffer; a
|
/// each injection layer (framing Q#IJ7 and Q#LQ5). `source` is the whole
|
||||||
/// child layer's tree carries absolute offsets into it, so the same
|
/// buffer; a child layer's tree carries absolute offsets into it, so the same
|
||||||
/// capture walk works unchanged.
|
/// capture walk works unchanged.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn compute_highlight_spans_for(
|
pub fn compute_highlight_spans_for(
|
||||||
query: &tree_sitter::Query,
|
query: &tree_sitter::Query,
|
||||||
tree: &tree_sitter::Tree,
|
tree: &tree_sitter::Tree,
|
||||||
source: &[u8],
|
source: &[u8],
|
||||||
|
local_facts: Option<&LocalFacts>,
|
||||||
byte_range: Option<std::ops::Range<usize>>,
|
byte_range: Option<std::ops::Range<usize>>,
|
||||||
) -> Vec<HighlightSpan> {
|
) -> Vec<HighlightSpan> {
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
|
|
@ -1441,30 +1675,33 @@ pub fn compute_highlight_spans_for(
|
||||||
}
|
}
|
||||||
let root = tree.root_node();
|
let root = tree.root_node();
|
||||||
let mut iter = cursor.captures(query, root, source);
|
let mut iter = cursor.captures(query, root, source);
|
||||||
while let Some((qmatch, capture_idx)) = iter.next() {
|
while let Some((query_match, capture_index)) = iter.next() {
|
||||||
// Fail-closed on the locals property predicate. The capture
|
let capture = query_match.captures[*capture_index];
|
||||||
// iterator already applies text predicates (`#eq?`/`#match?`/
|
let local_predicates_match = query
|
||||||
// `#any-of?`), but `#is? local` / `#is-not? local` are *property*
|
.property_predicates(query_match.pattern_index)
|
||||||
// predicates (`Query::property_predicates`) that need a scope map
|
|
||||||
// built from the grammar's LOCALS_QUERY, which pmacs does not run.
|
|
||||||
// Applying such a capture regardless mis-styles shadowed locals —
|
|
||||||
// e.g. a local `console`/`require` in JS/TS would still capture as
|
|
||||||
// `@variable.builtin`/`@function.builtin`. Until locals processing
|
|
||||||
// exists, drop captures whose pattern carries one; the identifier
|
|
||||||
// falls back to its non-builtin capture. `#set!` (property
|
|
||||||
// *settings*) is a different API and is not consulted here.
|
|
||||||
if query
|
|
||||||
.property_predicates(qmatch.pattern_index)
|
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(prop, _)| &*prop.key == "local")
|
.filter(|(property, _)| property.key.as_ref() == "local")
|
||||||
{
|
.all(|(property, positive)| {
|
||||||
|
let node = property.capture_id.map_or(Some(capture.node), |target| {
|
||||||
|
query_match
|
||||||
|
.captures
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| candidate.index as usize == target)
|
||||||
|
.map(|candidate| candidate.node)
|
||||||
|
});
|
||||||
|
let is_local = node.is_some_and(|node| {
|
||||||
|
local_facts
|
||||||
|
.is_some_and(|facts| facts.contains(node.start_byte(), node.end_byte()))
|
||||||
|
});
|
||||||
|
is_local == *positive
|
||||||
|
});
|
||||||
|
if !local_predicates_match {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let cap = qmatch.captures[*capture_idx];
|
|
||||||
spans.push(HighlightSpan {
|
spans.push(HighlightSpan {
|
||||||
start_byte: cap.node.start_byte() as u32,
|
start_byte: capture.node.start_byte() as u32,
|
||||||
end_byte: cap.node.end_byte() as u32,
|
end_byte: capture.node.end_byte() as u32,
|
||||||
capture_index: cap.index,
|
capture_index: capture.index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Wider-first ordering at equal start: later writes (the
|
// Wider-first ordering at equal start: later writes (the
|
||||||
|
|
@ -1684,7 +1921,13 @@ mod tests {
|
||||||
.highlight_query
|
.highlight_query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("inline highlights resolved at settle");
|
.expect("inline highlights resolved at settle");
|
||||||
let spans = compute_highlight_spans_for(hquery, &inline.tree, &bundle.source, None);
|
let spans = compute_highlight_spans_for(
|
||||||
|
hquery,
|
||||||
|
&inline.tree,
|
||||||
|
&bundle.source,
|
||||||
|
inline.local_facts.as_deref(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!spans.is_empty(),
|
!spans.is_empty(),
|
||||||
"the inline layer produces highlight spans across both ranges"
|
"the inline layer produces highlight spans across both ranges"
|
||||||
|
|
@ -2221,7 +2464,13 @@ mod tests {
|
||||||
.highlight_query
|
.highlight_query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("yaml highlights resolved");
|
.expect("yaml highlights resolved");
|
||||||
let spans = compute_highlight_spans_for(query, &yaml.tree, &bundle.source, None);
|
let spans = compute_highlight_spans_for(
|
||||||
|
query,
|
||||||
|
&yaml.tree,
|
||||||
|
&bundle.source,
|
||||||
|
yaml.local_facts.as_deref(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(!spans.is_empty(), "the yaml frontmatter layer highlights");
|
assert!(!spans.is_empty(), "the yaml frontmatter layer highlights");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2418,54 +2667,296 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn javascript_shadowed_builtin_is_not_mislabeled() {
|
fn local_sensitive_builtin_highlights_have_compilable_locals_queries() {
|
||||||
// `#is-not? local` (JS/TS use it for console/require/etc.) needs a
|
let registry = SyntaxRegistry::new();
|
||||||
// scope map from the LOCALS_QUERY we don't run, so
|
for entry in BUILTIN_LANGUAGES {
|
||||||
// `compute_highlight_spans` drops captures guarded by it. Here
|
let Some(highlights) = registry.highlights_query(entry.name) else {
|
||||||
// `console` is a LOCAL declaration — it must not surface as a
|
continue;
|
||||||
// `*.builtin` capture (which is what a naive run of the shared JS
|
};
|
||||||
// query would produce).
|
if !query_uses_local_predicates(&highlights) {
|
||||||
let reg = SyntaxRegistry::new();
|
continue;
|
||||||
let language = reg.language("javascript").expect("javascript loads");
|
}
|
||||||
let query = reg
|
|
||||||
.highlights_query("javascript")
|
|
||||||
.expect("javascript highlights compile");
|
|
||||||
let mut buf = fresh_buffer("shadow.js");
|
|
||||||
buf.apply_edit(EditOp::Insert {
|
|
||||||
pos: 0,
|
|
||||||
bytes: b"const console = 5;\nconsole;\n",
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
let view = ParseView::new(&buf, language, "javascript".to_owned());
|
|
||||||
let handle = view.handle();
|
|
||||||
let _vid = buf.attach_view(Box::new(view));
|
|
||||||
let bundle = parse_synchronously(&handle);
|
|
||||||
let spans = compute_highlight_spans(&query, &bundle);
|
|
||||||
assert!(!spans.is_empty(), "the JS query produced highlight spans");
|
|
||||||
let names = query.capture_names();
|
|
||||||
let builtin: Vec<&str> = spans
|
|
||||||
.iter()
|
|
||||||
.map(|s| names[s.capture_index as usize])
|
|
||||||
.filter(|n| n.contains("builtin"))
|
|
||||||
.collect();
|
|
||||||
assert!(
|
|
||||||
builtin.is_empty(),
|
|
||||||
"a locally-shadowed `console` must not get a *.builtin capture; got {builtin:?}"
|
|
||||||
);
|
|
||||||
// ...and dropping the builtin pattern must not strip *all* styling:
|
|
||||||
// each `console` occurrence still keeps its ordinary `@variable`
|
|
||||||
// capture (the fallback), so the loss is only the `.builtin` refine.
|
|
||||||
let src = "const console = 5;\nconsole;\n";
|
|
||||||
for (pos, _) in src.match_indices("console") {
|
|
||||||
let (start, end) = (pos as u32, (pos + "console".len()) as u32);
|
|
||||||
let caps: Vec<&str> = spans
|
|
||||||
.iter()
|
|
||||||
.filter(|s| s.start_byte == start && s.end_byte == end)
|
|
||||||
.map(|s| names[s.capture_index as usize])
|
|
||||||
.collect();
|
|
||||||
assert!(
|
assert!(
|
||||||
caps.iter().any(|n| n.starts_with("variable")),
|
entry
|
||||||
"`console` at byte {pos} keeps a variable capture; got {caps:?}"
|
.locals_query
|
||||||
|
.iter()
|
||||||
|
.any(|fragment| !fragment.trim().is_empty()),
|
||||||
|
"`{}` highlights use a local predicate but ship no locals query",
|
||||||
|
entry.name
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
registry.locals_query(entry.name).is_some(),
|
||||||
|
"`{}` highlights use a local predicate but its locals query does not compile",
|
||||||
|
entry.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn javascript_local_predicates_distinguish_lexical_scope() {
|
||||||
|
let registry = SyntaxRegistry::new();
|
||||||
|
let source = b"console.log('outer');\n\
|
||||||
|
require('outer');\n\
|
||||||
|
function f(console, require) {\n\
|
||||||
|
console.log('inner');\n\
|
||||||
|
require('inner');\n\
|
||||||
|
}\n\
|
||||||
|
window.alert('outer');\n";
|
||||||
|
let bundle = parse_layered(®istry, "javascript", source);
|
||||||
|
let layer = &bundle.layers[0];
|
||||||
|
let query = layer
|
||||||
|
.highlight_query
|
||||||
|
.as_deref()
|
||||||
|
.expect("javascript highlights compile");
|
||||||
|
assert!(
|
||||||
|
layer.local_facts.is_some(),
|
||||||
|
"a local-sensitive highlight query must settle lexical facts"
|
||||||
|
);
|
||||||
|
let spans = compute_highlight_spans(query, &bundle);
|
||||||
|
let names = query.capture_names();
|
||||||
|
let captures_at = |start: usize, len: usize| -> Vec<&str> {
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.filter(|span| {
|
||||||
|
span.start_byte == start as u32 && span.end_byte == (start + len) as u32
|
||||||
|
})
|
||||||
|
.map(|span| names[span.capture_index as usize])
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for identifier in ["console", "require"] {
|
||||||
|
let positions: Vec<usize> = std::str::from_utf8(source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.match_indices(identifier)
|
||||||
|
.map(|(position, _)| position)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(positions.len(), 3, "fixture has three `{identifier}` uses");
|
||||||
|
assert!(
|
||||||
|
captures_at(positions[0], identifier.len())
|
||||||
|
.iter()
|
||||||
|
.any(|name| name.ends_with(".builtin")),
|
||||||
|
"unshadowed outer `{identifier}` keeps its builtin refinement"
|
||||||
|
);
|
||||||
|
for position in &positions[1..] {
|
||||||
|
let captures = captures_at(*position, identifier.len());
|
||||||
|
assert!(
|
||||||
|
!captures.iter().any(|name| name.ends_with(".builtin")),
|
||||||
|
"local `{identifier}` at byte {position} is not builtin: {captures:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
captures.iter().any(|name| name.starts_with("variable")),
|
||||||
|
"local `{identifier}` keeps an ordinary variable capture: {captures:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = std::str::from_utf8(source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.find("window")
|
||||||
|
.expect("window fixture");
|
||||||
|
assert!(
|
||||||
|
captures_at(window, "window".len())
|
||||||
|
.iter()
|
||||||
|
.any(|name| name == &"variable.builtin"),
|
||||||
|
"an unresolved builtin after the function remains builtin"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positive_and_capture_qualified_local_predicates_use_resolved_facts() {
|
||||||
|
let registry = SyntaxRegistry::new();
|
||||||
|
let source = b"let f = () => {};\nf();\ng();\n";
|
||||||
|
let bundle = parse_layered(®istry, "javascript", source);
|
||||||
|
let language = registry.language("javascript").expect("javascript loads");
|
||||||
|
let facts = bundle.layers[0]
|
||||||
|
.local_facts
|
||||||
|
.as_deref()
|
||||||
|
.expect("javascript local facts settle");
|
||||||
|
|
||||||
|
let positive = tree_sitter::Query::new(&language, "((identifier) @local-id (#is? local))")
|
||||||
|
.expect("positive local predicate compiles");
|
||||||
|
let positive_spans =
|
||||||
|
compute_highlight_spans_for(&positive, bundle.root_tree(), source, Some(facts), None);
|
||||||
|
let f_positions: Vec<usize> = std::str::from_utf8(source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.match_indices('f')
|
||||||
|
.map(|(position, _)| position)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(f_positions.len(), 2);
|
||||||
|
for position in f_positions {
|
||||||
|
assert!(
|
||||||
|
positive_spans.iter().any(|span| {
|
||||||
|
span.start_byte == position as u32 && span.end_byte == (position + 1) as u32
|
||||||
|
}),
|
||||||
|
"definition/reference `f` at byte {position} is local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let g_position = std::str::from_utf8(source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.find("g()")
|
||||||
|
.expect("g call");
|
||||||
|
assert!(
|
||||||
|
positive_spans
|
||||||
|
.iter()
|
||||||
|
.all(|span| span.start_byte != g_position as u32),
|
||||||
|
"unresolved `g` does not satisfy #is? local"
|
||||||
|
);
|
||||||
|
|
||||||
|
let qualified = tree_sitter::Query::new(
|
||||||
|
&language,
|
||||||
|
"((call_expression function: (identifier) @callee) @call \
|
||||||
|
(#is? @callee local))",
|
||||||
|
)
|
||||||
|
.expect("capture-qualified local predicate compiles");
|
||||||
|
let qualified_spans =
|
||||||
|
compute_highlight_spans_for(&qualified, bundle.root_tree(), source, Some(facts), None);
|
||||||
|
let qualified_names = qualified.capture_names();
|
||||||
|
assert!(
|
||||||
|
qualified_spans.iter().any(|span| {
|
||||||
|
qualified_names[span.capture_index as usize] == "call"
|
||||||
|
&& span.start_byte
|
||||||
|
== source
|
||||||
|
.windows(4)
|
||||||
|
.position(|window| window == b"f();")
|
||||||
|
.expect("f call") as u32
|
||||||
|
}),
|
||||||
|
"the call whose @callee is local satisfies the qualified predicate"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
qualified_spans
|
||||||
|
.iter()
|
||||||
|
.all(|span| span.start_byte != g_position as u32),
|
||||||
|
"the call whose @callee is unresolved fails the qualified predicate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_definition_value_and_scope_inheritance_control_resolution() {
|
||||||
|
let registry = SyntaxRegistry::new();
|
||||||
|
let language = registry.language("javascript").expect("javascript loads");
|
||||||
|
|
||||||
|
let value_source = b"let x = x;\nx;\n";
|
||||||
|
let value_tree = {
|
||||||
|
let mut parser = tree_sitter::Parser::new();
|
||||||
|
parser
|
||||||
|
.set_language(&language)
|
||||||
|
.expect("set javascript language");
|
||||||
|
parser
|
||||||
|
.parse(value_source, None)
|
||||||
|
.expect("parse value fixture")
|
||||||
|
};
|
||||||
|
let value_locals = tree_sitter::Query::new(
|
||||||
|
&language,
|
||||||
|
"(variable_declarator \
|
||||||
|
name: (identifier) @local.definition \
|
||||||
|
value: (identifier) @local.definition-value) \
|
||||||
|
(identifier) @local.reference",
|
||||||
|
)
|
||||||
|
.expect("definition-value locals query compiles");
|
||||||
|
let value_facts = compute_local_facts(&value_locals, &value_tree, value_source);
|
||||||
|
let x_positions: Vec<usize> = std::str::from_utf8(value_source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.match_indices('x')
|
||||||
|
.map(|(position, _)| position)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(x_positions.len(), 3);
|
||||||
|
assert!(value_facts.contains(x_positions[0], x_positions[0] + 1));
|
||||||
|
assert!(
|
||||||
|
!value_facts.contains(x_positions[1], x_positions[1] + 1),
|
||||||
|
"a definition is not visible inside its own value"
|
||||||
|
);
|
||||||
|
assert!(value_facts.contains(x_positions[2], x_positions[2] + 1));
|
||||||
|
|
||||||
|
let scope_source = b"let x = 1;\nfunction f() { x; }\nx;\n";
|
||||||
|
let scope_tree = {
|
||||||
|
let mut parser = tree_sitter::Parser::new();
|
||||||
|
parser
|
||||||
|
.set_language(&language)
|
||||||
|
.expect("set javascript language");
|
||||||
|
parser
|
||||||
|
.parse(scope_source, None)
|
||||||
|
.expect("parse scope fixture")
|
||||||
|
};
|
||||||
|
let scope_locals = tree_sitter::Query::new(
|
||||||
|
&language,
|
||||||
|
"((function_declaration) @local.scope \
|
||||||
|
(#set! local.scope-inherits false)) \
|
||||||
|
(variable_declarator name: (identifier) @local.definition) \
|
||||||
|
(identifier) @local.reference",
|
||||||
|
)
|
||||||
|
.expect("non-inheriting locals query compiles");
|
||||||
|
let scope_facts = compute_local_facts(&scope_locals, &scope_tree, scope_source);
|
||||||
|
let x_positions: Vec<usize> = std::str::from_utf8(scope_source)
|
||||||
|
.expect("fixture is UTF-8")
|
||||||
|
.match_indices('x')
|
||||||
|
.map(|(position, _)| position)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(x_positions.len(), 3);
|
||||||
|
assert!(scope_facts.contains(x_positions[0], x_positions[0] + 1));
|
||||||
|
assert!(
|
||||||
|
!scope_facts.contains(x_positions[1], x_positions[1] + 1),
|
||||||
|
"a non-inheriting scope cannot see the outer `x`"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
scope_facts.contains(x_positions[2], x_positions[2] + 1),
|
||||||
|
"leaving the scope restores outer resolution"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typescript_locals_compose_javascript_scopes_and_parameter_delta() {
|
||||||
|
let registry = SyntaxRegistry::new();
|
||||||
|
for (language_name, source) in [
|
||||||
|
(
|
||||||
|
"typescript",
|
||||||
|
&b"function f(console: string) { console.log('x'); }\n\
|
||||||
|
window.alert('x');\n"[..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"typescriptreact",
|
||||||
|
&b"function F(console: string) { return <div>{console}</div>; }\n\
|
||||||
|
window.alert('x');\n"[..],
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let locals = registry
|
||||||
|
.locals_query(language_name)
|
||||||
|
.unwrap_or_else(|| panic!("{language_name} locals compile"));
|
||||||
|
assert!(
|
||||||
|
locals.capture_index_for_name("local.scope").is_some()
|
||||||
|
&& locals.capture_index_for_name("local.definition").is_some()
|
||||||
|
&& locals.capture_index_for_name("local.reference").is_some(),
|
||||||
|
"{language_name} includes JavaScript's scopes and references"
|
||||||
|
);
|
||||||
|
|
||||||
|
let bundle = parse_layered(®istry, language_name, source);
|
||||||
|
let layer = &bundle.layers[0];
|
||||||
|
let query = layer
|
||||||
|
.highlight_query
|
||||||
|
.as_deref()
|
||||||
|
.expect("highlights compile");
|
||||||
|
let spans = compute_highlight_spans(query, &bundle);
|
||||||
|
let names = query.capture_names();
|
||||||
|
let text = std::str::from_utf8(source).expect("fixture is UTF-8");
|
||||||
|
for (position, _) in text.match_indices("console") {
|
||||||
|
assert!(
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.filter(|span| {
|
||||||
|
span.start_byte == position as u32
|
||||||
|
&& span.end_byte == (position + "console".len()) as u32
|
||||||
|
})
|
||||||
|
.all(|span| !names[span.capture_index as usize].ends_with(".builtin")),
|
||||||
|
"{language_name} parameter/reference `console` is local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let window = text.find("window").expect("window fixture");
|
||||||
|
assert!(
|
||||||
|
spans.iter().any(|span| {
|
||||||
|
span.start_byte == window as u32
|
||||||
|
&& span.end_byte == (window + "window".len()) as u32
|
||||||
|
&& names[span.capture_index as usize] == "variable.builtin"
|
||||||
|
}),
|
||||||
|
"{language_name} unresolved `window` remains builtin"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,98 @@ fn m4_3_theming_via_lua_color_scheme() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Locals-query acceptance: the shipped JavaScript grammar must distinguish an
|
||||||
|
/// unresolved builtin from a lexically-shadowed parameter, then replace the
|
||||||
|
/// classification with the fresh parse bundle after an edit removes the
|
||||||
|
/// shadow. The theme maps only `variable.builtin`, making the classification
|
||||||
|
/// observable in rendered cells rather than through an internal scope map.
|
||||||
|
#[test]
|
||||||
|
fn m4_locals_query_shadowing_and_edit_freshness() {
|
||||||
|
use pmacs::cell::Color;
|
||||||
|
|
||||||
|
const COLS: usize = 40;
|
||||||
|
const BUILTIN: Color = Color::Indexed(6);
|
||||||
|
let initial = "console;\nfunction f(console) {\n console;\n}\n";
|
||||||
|
let edited = "console;\nfunction f(logger) {\n console;\n}\n";
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("locals.js");
|
||||||
|
std::fs::write(&path, initial.as_bytes()).expect("write JavaScript fixture");
|
||||||
|
|
||||||
|
let mut state = open_and_wait_for_parse(path);
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(
|
||||||
|
r#"
|
||||||
|
pmacs.theme.set {
|
||||||
|
["variable.builtin"] = { fg = 6, bold = true },
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("apply builtin-only theme");
|
||||||
|
|
||||||
|
let before = render_active_window_to_grid(&mut state, 5, COLS as u32);
|
||||||
|
for (col, cell) in before.iter().take(7).enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
cell.style.fg, BUILTIN,
|
||||||
|
"unshadowed row-0 `console` byte {col} is builtin"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cell.style.bold,
|
||||||
|
"builtin-only theme reaches row-0 `console` byte {col}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for col in 11..18 {
|
||||||
|
assert_ne!(
|
||||||
|
before[COLS + col].style.fg,
|
||||||
|
BUILTIN,
|
||||||
|
"parameter `console` byte {col} is lexically local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for col in 2..9 {
|
||||||
|
assert_ne!(
|
||||||
|
before[2 * COLS + col].style.fg,
|
||||||
|
BUILTIN,
|
||||||
|
"reference to shadowing parameter at row 2, col {col} is not builtin"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(
|
||||||
|
r"
|
||||||
|
local buf = pmacs.window.buffer()
|
||||||
|
buf:replace(20, 27, 'logger')
|
||||||
|
pmacs.parse._dispatch(buf, 'javascript')
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("rename shadowing parameter");
|
||||||
|
pump_async(&mut state, |s| {
|
||||||
|
current_tree_text(s).as_deref() == Some(edited)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
current_tree_text(&state).as_deref(),
|
||||||
|
Some(edited),
|
||||||
|
"the edited JavaScript parse settled"
|
||||||
|
);
|
||||||
|
|
||||||
|
let after = render_active_window_to_grid(&mut state, 5, COLS as u32);
|
||||||
|
for col in 2..9 {
|
||||||
|
assert_eq!(
|
||||||
|
after[2 * COLS + col].style.fg,
|
||||||
|
BUILTIN,
|
||||||
|
"fresh local facts restore builtin styling at row 2, col {col}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
after[2 * COLS + col].style.bold,
|
||||||
|
"fresh builtin capture reaches the rendered cell at row 2, col {col}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sanity: the bundled `default_dark` theme produces a non-empty
|
/// Sanity: the bundled `default_dark` theme produces a non-empty
|
||||||
/// capture map and resolves common captures to non-default styles.
|
/// capture map and resolves common captures to non-default styles.
|
||||||
/// Catches accidental regressions to a literally empty theme that
|
/// Catches accidental regressions to a literally empty theme that
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue