Commit Graph

4 Commits

Author SHA1 Message Date
Levi Neuwirth fb14dc9ec3 test(isolation): migrate the corpus off the ambient roots
The mechanical half, riding on the census in the previous commit.

* 342 in-process construction sites in 65 files now take
  `new_with_roots` / `open_with_roots` with `iso::roots()`. The isolated
  base is a pure function of `CARGO_TARGET_TMPDIR` — no counter, no
  `OnceLock` — so two copies of the module in one binary agree instead of
  racing, and the tree lives somewhere `cargo clean` owns rather than
  leaking into `/tmp` once per run. It is shared deliberately:
  materialization is content-gated and idempotent, so a per-test
  directory would repeat it ~330 times per run for a byte-identical
  result.

* `journey_acceptance` keeps the ambient `EditorState::open`, because
  proving the production entry point has a caller is the whole of what
  that ratchet is for. Rev 2's "isolated by the environment its binary is
  launched with" was not a mechanism — cargo launches each test binary
  with the caller's environment, and a binary cannot re-point its own
  roots before its tests run. Each test is now a thin parent that
  re-execs this binary for its own name with controlled roots, and the
  child runs the body against production's call. Two pins guard it: the
  child asserts all four roots resolve inside the controlled base, and
  the suite asserts against its own source that it has not quietly taken
  the seam. The parent also asserts the child ran `1 passed` — a stale
  `--exact` filter would otherwise hollow the whole thing out silently.

* The shared spawners take all five storage variables.
  `spawn_daemon_process_with_env` set `HOME` and `XDG_CONFIG_HOME` only;
  `HOME` is a FALLBACK, so it isolates a root only while the matching
  `XDG_*` is unset — the harness's apparent adequacy was a property of
  one developer's environment. The PTY spawner backfills whichever of the
  five its caller did not pin. The 10 direct `Command::new` daemon and
  attach spawns get the same treatment.

Three suites had `mod common;` behind `#[cfg(feature = "crdt")]`;
`common::iso` is needed in every build, so those are ungated. Files that
already pull in `common` reach `iso` through a `use` rather than a second
`#[path]` declaration — loading one file as two modules is
`clippy::duplicate_mod`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:48:45 -04:00
Levi Neuwirth 325dcd553f fix(injections): PR #122 round 2 — sibling precedence, observable cap, docs
Two follow-ups + doc cleanup.

[P2] Same-depth sibling precedence was reversed. The wire priority was
(depth, capture_order), omitting the layer ordinal, so two overlapping
spans from different sibling layers tied — and the active-set insert
then applied the later one first, making the earlier sibling win, the
opposite of the grid's layer-by-layer paint. Priority is now
(layer_index, capture_order): layer_index is the depth-ascending
position in bundle.layers, so a deeper layer AND a later same-depth
sibling both override, matching the grid exactly. New
flatten_same_depth_sibling_later_layer_wins pins it.

[P2] Cap surfacing had no end-to-end test. Added
injection_cap_surfaced_once_and_rearms_via_lua, which drives the real
Lua settle path (syntax.lua tick -> _injection_capped -> pmacs.error)
and asserts surfaced-once, suppressed-on-unchanged-reparse, and
re-armed-after-dropping-below-then-exceeding-the-cap.

Docs:
- Q#IJ6 now states the accurate bound O(n log n + Sum active) for the
  event sweep, not O(boundaries).
- The full-buffer perf test is renamed/narrowed to guard the FLATTENER
  regression; the summary's per-line dominant-style tally is a separate
  pre-existing O(lines x spans) loop, not claimed linear.
- Framing #9 now matches the test: it drives spans_from_segments (the
  extracted replace_style_spans transform) + source_color_at, not a live
  State render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
2026-07-15 12:07:21 +01:00
Levi Neuwirth 79d75a29e0 fix(injections): PR #122 round 1 — sweep flatten, sync aliases, real multi-range, surfaced cap
Four review findings + a cleanup bundle.

[P1] Wire flattener was O(spans²) and ran over the WHOLE buffer (the
file-style summary uses a whole-buffer viewport, not the visible one).
Replaced the per-interval full scan with an ordered active-set event
sweep (activate on start, expire on end, fold the active set) — linear
in practice. Added full_buffer_summary_scales_on_large_grammar_file
(1500-line rust) as the perf gate.

[P2] _parse_now used the empty alias map from make_request while
_dispatch snapshotted the registry map, so a `py` fence injected async
but not sync. Snapshot aliases on both paths; pinned by
sync_parse_now_resolves_alias.

[P2] The multi-range inline test used a one-line paragraph, whose block
inline node has no named children (link/emphasis are child-grammar
structures) — one range, so it couldn't falsify multi-range. Replaced
with a multi-line blockquote whose inline node carries a named
block_continuation: content_node_ranges now asserts >1 collected range
and emphasis parses on both lines.

[P2] The layer backstop dropped regions silently; the framing requires
a surfaced warning. run_parse now sets ParseTreeBundle::injection_capped;
syntax.lua's settle tick raises it once per buffer via pmacs.error
(_injection_capped). Added injection_layer_cap_surfaces_and_preserves_root
(drives >4096 fences, asserts the flag + bounded count + intact root).

Cleanup:
- The GPU acceptance test now drives the real StyleSpans full-frame
  transform (spans_from_segments, extracted from replace_style_spans)
  instead of a hand-rolled sort.
- content_node_ranges excludes NAMED children (documented as a round-1
  refinement); framing mechanic #3 / Q#IJ5 updated to match.
- parse_duration doc now says root parse; the markdown entry no longer
  describes inline as unhighlighted/future.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
2026-07-15 12:07:21 +01:00
Levi Neuwirth 4282b1c333 feat(injections): multi-language injection layers
Teach the syntax engine that one buffer can hold more than one
language. After the root parse, run the grammar's injections.scm, parse
each embedded region with the injected language, and merge every
layer's highlight spans. First consumer: markdown fenced code + inline
(zero new grammars — the block grammar already ships an injection query
and the injected langs already have grammars from #118).

Engine (src/syntax.rs):
- ParseTreeBundle now holds Vec<Layer> (root layer 0 + injected
  children, depth-ascending); installed atomically so the existing
  Arc::ptr_eq style gate and highlight cache keep working (Q#IJ1).
- run_parse builds layers on the worker: run injections.scm, resolve
  the injected language, compute Vec<Range> (exclude NAMED children,
  intersect the parent's ranges), set_included_ranges cold-parse,
  recurse — bounded by depth (3), a layer backstop (4096), and a
  (lang,ranges) visited guard; any child failure drops that child only
  (Q#IJ3/IJ5). LanguageEntry gains injections_query; markdown_inline is
  registered (retires the M9.7 block-only floor); markdown/rust carry
  injection queries.
- Injected languages resolve off the static BUILTIN_LANGUAGES table
  (Send loaders + query sources), preserving lazy loading. Dynamic
  fence names go through a case-folded alias map seeded with defaults
  and Lua-extensible via pmacs.parse.injection_aliases, snapshotted into
  ParseRequest at dispatch so the worker never touches the Rc registry
  or a Lua table (Q#IJ2/IJ4). Highlight queries are resolved at settle
  (resolve_layer_queries), keeping query compilation main-thread/cached.

Producers:
- SyntaxHighlightView (grid) iterates layers shallow-to-deep so a
  deeper layer's styling wins within its region (Q#IJ6/IJ7).
- scoped_style_spans (wire) flattens all layers into DISJOINT effective
  spans via a boundary sweep, since the GPU re-sorts spans by start
  (replace_style_spans / merge_style_spans) and would otherwise destroy
  producer order. The GPU source_color_at consumer is fixed to fold all
  covering spans (matching semantic_client's effective_style_at) rather
  than returning the first.

Named-children exclusion: content ranges exclude only NAMED children
(matching tree-sitter-md's own inline splitter) — excluding a block
inline node's anonymous text tokens would shred the paragraph into
unparseable fragments.

13 acceptance gates (framing docs/multi-language-injections-framing.md):
layer structure, absolute child offsets, alias resolution (static +
case-folded dynamic + unknown-skip + Lua-async override), multi-range
inline, recursion bounds, wire + grid + GPU producers, incremental edit
/ new fence, many-paragraph settle budget with tail coverage, and the
single-layer regression guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
2026-07-15 12:07:21 +01:00