Merge branch 'main' into folding-tui

`main` moved from c49a8c7 (folding Stage 1, #142) to 47581f4 (web
grammars, #146) while Stage 2 was in framing and implementation. The
text merge is clean, but it is NOT semantically clean: #146 added three
new `Viewport { .. }` literals to `src/highlight.rs`'s unit tests, and
Stage 2 gives `Viewport` a `folds` field. Merged alone, `cargo test
--lib` fails to compile — so the carry-over is resolved here rather
than left for CI to discover.

Merged (not rebased) so the four framing revisions the review rounds
cite by SHA (59410c0, e221f13, 8160d66, 4222ffa) stay reachable.

Full gate suite re-run on the merged tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
This commit is contained in:
Levi Neuwirth 2026-07-23 19:43:13 -04:00
commit 97a27e9873
5 changed files with 675 additions and 0 deletions

22
Cargo.lock generated
View File

@ -2566,8 +2566,10 @@ dependencies = [
"tree-sitter-cmake", "tree-sitter-cmake",
"tree-sitter-containerfile", "tree-sitter-containerfile",
"tree-sitter-cpp", "tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-cuda", "tree-sitter-cuda",
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-html",
"tree-sitter-javascript", "tree-sitter-javascript",
"tree-sitter-json", "tree-sitter-json",
"tree-sitter-lua", "tree-sitter-lua",
@ -3794,6 +3796,16 @@ dependencies = [
"tree-sitter-language", "tree-sitter-language",
] ]
[[package]]
name = "tree-sitter-css"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5cbc5e18f29a2c6d6435891f42569525cf95435a3e01c2f1947abcde178686f"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]] [[package]]
name = "tree-sitter-cuda" name = "tree-sitter-cuda"
version = "0.21.1" version = "0.21.1"
@ -3814,6 +3826,16 @@ dependencies = [
"tree-sitter-language", "tree-sitter-language",
] ]
[[package]]
name = "tree-sitter-html"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]] [[package]]
name = "tree-sitter-javascript" name = "tree-sitter-javascript"
version = "0.25.0" version = "0.25.0"

View File

@ -233,6 +233,14 @@ tree-sitter-md = "0.5"
# uses the in-repo overlay `builtin/queries/latex/highlights.scm` (the first # uses the in-repo overlay `builtin/queries/latex/highlights.scm` (the first
# such overlay; `include_str!`'d in `crate::syntax::BUILTIN_LANGUAGES`). # such overlay; `include_str!`'d in `crate::syntax::BUILTIN_LANGUAGES`).
codebook-tree-sitter-latex = "0.6" codebook-tree-sitter-latex = "0.6"
# Web grammars: HTML + CSS (`.html`/`.htm`/`.xhtml`, `.css`). The official
# tree-sitter-org grammars; both export `LANGUAGE` + query constants over
# `tree-sitter-language 0.1` (shared ABI crate, `tree-sitter` dev-only), so no
# overlay is needed. HTML's `INJECTIONS_QUERY` lights up `<script>` ->
# javascript (already registered) and `<style>` -> css via the #122 injection
# engine (see `crate::syntax::BUILTIN_LANGUAGES`).
tree-sitter-html = "0.23"
tree-sitter-css = "0.25"
# T M4.4 process supervisor: signal sending without `unsafe`. Keep # T M4.4 process supervisor: signal sending without `unsafe`. Keep
# the feature surface tight to keep build time low. `poll` feeds the # the feature surface tight to keep build time low. `poll` feeds the
# compile-mode group readers (cancellable poll-based reads, Q#CM3). # compile-mode group readers (cancellable poll-based reads, Q#CM3).

View File

@ -0,0 +1,319 @@
# Web grammars (HTML + CSS) + HTML injections — framing
**Revision 4 — implemented on branch `web-grammars` (PR #146). Ground truth:
canonical `main` @ `4daa1b8` (after LaTeX #144 and inline-math-docs #145),
2026-07-23.** Rev 2 settled the capture set, injection scope, and LSP claim
(round 1); rev 3 corrected the `#match?` predicate analysis (round 2); rev 4
names the intended `@attribute` retro-paint on the already-bundled rust/lua/yaml
grammars — verified on a Rust buffer and pinned by a test (round 3). See §0.1.
## 0.1 Revision history
### Round 1 (rev 1 → rev 2)
- **F1 (high).** Q#WEB4 deferred the capture mapping. The upstream queries
settle it: HTML and CSS need exactly **`tag`** and **`attribute`**; `@tag.error`
prefix-walks to `tag`; there is **no** `@tag.delimiter` (rev-1 speculation).
Q#WEB4 now names both mappings and their styles, and acceptance 6 paints an
HTML **attribute** explicitly (a tag/property-only test could pass with
`attribute` unverified since `property` is already recognized).
- **F2 (medium).** The HTML injections query has **exactly two** patterns —
`script_element`→javascript, `style_element`→css. Q#WEB5 no longer claims
event-handler / `style=` coverage; inline-attribute injection is now a named
deferral.
- **F3 (medium).** No HTML/CSS language server is bundled: `lsp.lua`'s default
config list ends at YAML (`:247`) and `:521` returns `nil` without a
configured command. §2 and §5 corrected — the grammars give stable language
IDs so a *user-supplied* config attaches automatically, but nothing ships or
starts by default.
### Round 2 (rev 2 → rev 3)
- **R2-1 (medium).** The rev-2 claim that pmacs *ignores* `#match?` was wrong.
`src/syntax.rs:1701` passes the source to `QueryCursor::captures`, so
tree-sitter evaluates the standard `#match?`/`#eq?`/`#any-of?` text predicates
natively (pmacs special-cases only `#is? local`, `:1703`). Corrected in §2 and
Q#WEB4: an ordinary CSS property has a single `@property` capture; only a
`--custom-property` also receives `@variable` (a benign double-capture).
Acceptance 6 no longer claims an ordinary property pins custom-property
precedence — it just verifies `color``@property` paints.
- **R2-2 (low).** The folding footprint in §0 was stale. At
`githubsucks/folding` @ `036a994` the branch touches `src/fold.rs`,
`editor.rs`, `editor_core.rs`, `lib.rs`, `lua_bindings/{fold.rs,mod.rs}`,
`semantic_render.rs`, `fold.lua`, and tests — not
`overlay.rs`/`daemon.rs`/`syntax.rs`. §0 now lists the real set; the
zero-overlap conclusion is unchanged (stronger, if anything).
### Round 3 (rev 3 → rev 4)
- **R3-1 (medium).** Q#WEB4 verified which captures pmacs *lacks* but not the
reverse: which already-bundled grammars *use* `@attribute`. Three do — rust
(`attribute_item`), lua (`<const>`), yaml (directives) — so the new
`("attribute", fg(3))` entry retro-paints their previously-unpainted spans
yellow. Named in Q#WEB4 as intended (verified on a Rust buffer: uniform
yellow, an improvement over unpainted), and pinned by
`rust_attribute_repaints_via_shared_attribute_capture`. `tag` is unaffected
(HTML/CSS only).
Add tree-sitter **HTML** and **CSS** grammars so `.html`/`.htm` and `.css`
buffers get lexical highlighting, and — the north-star payoff — light up
HTML's embedded-language **injections**: `<script>` → JavaScript (already
registered) and `<style>` → CSS (added here). This is the side-quest
backlog's "HTML/CSS grammars that light up more injection consumers."
## 0. Why this lane, why now (parallel-safety)
Board at this snapshot: LaTeX #144 and inline-math-docs #145 merged;
**folding Stage 1 (#142) is the only open PR** — a headless fold engine. Its
current diff (`main...githubsucks/folding` @ `036a994`) touches `src/fold.rs`
(new), `src/editor.rs`, `src/editor_core.rs`, `src/lib.rs`,
`src/lua_bindings/{fold.rs,mod.rs}`, `src/semantic_render.rs`,
`builtin/runtime/fold.lua`, and tests — **none of which this lane touches**.
gpu-invocation has landed. The GPU render path (`pmacs-gpu/src/main.rs`) remains
contended by folding's later Stage 3 — this lane never touches it.
This lane's footprint: two `Cargo.toml` grammar deps, two `BUILTIN_LANGUAGES`
entries, and — per Q#WEB4 — a small extension of `src/highlight.rs`'s recognized
capture set. **Zero file overlap with folding**; the three files it edits:
- `src/syntax.rs` — a localized append to the language table (after the `latex`
entry, currently ~`:1111`). Folding does not touch this file.
- `Cargo.toml` — two dependency lines in the grammar block (`:142``:220`).
- `src/highlight.rs` — appends to the capture-style `entries` table
(`:143-172`). Folding does not touch this file.
All are localized, low-conflict edits — the same pattern #144 and the
config-registry ∥ Vterm lanes landed conflict-free.
Unlike LaTeX (#144), **no in-repo query overlay is needed** — both crates
export their queries as constants. The overlay convention #144 established
stays available for future crates that don't (e.g. some of ruby/php).
## 1. What ships
- **HTML grammar** (`.html`/`.htm`/`.xhtml`) — tags, attributes, text,
comments, doctype.
- **CSS grammar** (`.css`) — selectors, properties, values, at-rules,
comments.
- **HTML injections**`<script>…</script>` parses as JavaScript and
`<style>…</style>` parses as CSS, via HTML's crate-exported injections query
riding the #122 injection engine. CSS is registered here so the `<style>`
injection resolves.
## 2. Ground truth (scouted 2026-07-23, `main` @ `4daa1b8`)
### Crate facts (verified)
- **`tree-sitter-html` 0.23.2** (2026-06). Exports `LANGUAGE: LanguageFn`,
**`HIGHLIGHTS_QUERY`**, **`INJECTIONS_QUERY`**, `NODE_TYPES`. Deps:
`tree-sitter-language ^0.1` (the shim every pmacs grammar uses),
`tree-sitter ^0.24` **dev-only** → ABI-compatible with our `tree-sitter 0.26`
via `.into()`. Its `INJECTIONS_QUERY` is the load-bearing piece: it names
`javascript` and `css` via `#set! injection.language`.
- **`tree-sitter-css` 0.25.0** (2026-05). Exports `LANGUAGE: LanguageFn`,
**`HIGHLIGHTS_QUERY`**, `NODE_TYPES`. Deps: `tree-sitter-language ^0.1`,
`tree-sitter ^0.25` **dev-only** → shim-ABI-fine. No injections query (CSS
injects nothing).
Both export their highlights query as a constant, so **no overlay/vendoring**
(the LaTeX complication) applies.
### Codebase
- **`BUILTIN_LANGUAGES`** (`src/syntax.rs:816`…`];`) currently ends at the
`latex` entry (~`:1104-1111`, added by #144). No `html`/`css` entry. Append
the two new entries before `];`.
- **`javascript` is registered** (`src/syntax.rs:1010`, `extensions: js/mjs/cjs`,
`injections_query: &[]`), so HTML's `<script>``javascript` resolves today.
`css` does not exist → HTML's `<style>``css` resolves only once this lane
adds it.
- **Injection engine**`collect_injection_matches` (`src/syntax.rs:375-427`)
reads `@injection.content` (`:381`) + `injection.language` (dynamic node text
or `#set!`, `:387-402`) + `injection.include-children` (`:402`).
`resolve_injected_language` (`:304`) resolves the name against
`BUILTIN_LANGUAGES` + `default_injection_aliases` (`:234`). Working
precedents: rust `INJECTIONS_QUERY` (`:823`), markdown block/inline
(`:848`/`:862`). HTML's `#set! injection.language "javascript"|"css"` is the
same shape.
- **Recognized capture set**`src/highlight.rs:143-172`, resolved by a
dotted-prefix walk (`:189`). Confirmed against the two upstream queries
(HTML v0.23.2 `highlights.scm`, CSS v0.25.0 `highlights.scm`): the **only**
captures not already recognized are **`@tag`** and **`@attribute`** — used by
BOTH grammars (`@tag.error` prefix-walks to `tag`; there is no `@tag.delimiter`).
Everything else maps: HTML's `@constant` (doctype), `@string` (attribute
value), `@comment`, `@punctuation.bracket`; CSS's `@operator`/`@property`/
`@function`/`@keyword`/`@number`/`@type`/`@string.special`/`@punctuation.*`.
CSS also has two `#match?`-gated `@variable` patterns for `--custom-props`.
pmacs passes the buffer text to `QueryCursor::captures` (`src/syntax.rs:1701`),
so tree-sitter **evaluates** the standard `#match?`/`#eq?`/`#any-of?` text
predicates natively; pmacs adds handling only for the `#is? local` property
predicate (the `property_predicates … "local"` filter at `:1703`). So an
ordinary property (`color`) has a single `@property` capture, and only a custom
property (`--brand`) additionally receives `@variable` — see Q#WEB4.
- **Detection** — the `extensions` field drives `language_name_for_path`
(`:1223`) ahead of the LSP filetype map in the Lua chain
(`builtin/runtime/syntax.lua:452-466`); no Lua edit. **No HTML/CSS language
server is bundled**: `builtin/runtime/lsp.lua`'s default config list ends at
YAML (`:247`), and `:521` returns `nil` without a configured command. The
grammars provide stable language IDs (`html`/`css`), so a **user-supplied**
`pmacs.lsp.config.html`/`.css` attaches automatically, but nothing ships or
starts by default.
## 3. Decisions
### Q#WEB1 — Bundle `tree-sitter-html` 0.23 + `tree-sitter-css` 0.25
Add both to the `Cargo.toml` grammar block; loaders
`|| tree_sitter_html::LANGUAGE.into()` and `|| tree_sitter_css::LANGUAGE.into()`.
ABI is fine via the shared `tree-sitter-language` shim, as every current
grammar. No provenance saga (these are the official tree-sitter-org grammars,
not squatted republishes).
### Q#WEB2 — Register both; CSS before HTML's injection can resolve
- `html`: `highlights_query: &[tree_sitter_html::HIGHLIGHTS_QUERY]`,
`injections_query: &[tree_sitter_html::INJECTIONS_QUERY]`, `locals_query: &[]`.
- `css`: `highlights_query: &[tree_sitter_css::HIGHLIGHTS_QUERY]`, injections and
locals empty.
Both live in the same `BUILTIN_LANGUAGES`, so `resolve_injected_language`
finds `css` (and the existing `javascript`) when HTML's injections query fires.
### Q#WEB3 — Extensions
`html`: `["html", "htm", "xhtml"]`. `css`: `["css"]`. SCSS/LESS/Sass are
distinct grammars (`scss`/`less` node sets) and are deferred (§5).
### Q#WEB4 — Add exactly two capture entries: `tag` and `attribute`
The LaTeX lane could rename captures because it owned an editable overlay.
Here the highlights queries are **crate constants** — not editable — so the
reconciliation is to **extend `src/highlight.rs`'s `entries` table**
(`:143-172`). The upstream queries settle the exact set: the **only** captures
neither grammar's query already resolves are `tag` and `attribute`. Add exactly
two entries:
- `("tag", fg(5))` — HTML `(tag_name)` and CSS element/nesting/universal
selectors, in the keyword hue (magenta) but non-bold to stay light in dense
markup. `@tag.error` (HTML erroneous end tags) prefix-walks to this entry, so
it needs no separate mapping.
- `("attribute", fg(3))` — HTML `(attribute_name)` and CSS
pseudo-/attribute-selector names, in the type hue (yellow) — distinct from
`tag`, from `property`/`operator` (cyan), and from `string` values (green).
There is **no** `tag.delimiter` (rev-1 speculation, removed); HTML's `<`/`>`/
`</`/`/>` are `@punctuation.bracket`, already handled. This extension is:
- **low-conflict** — folding does not touch `highlight.rs`;
- **general**`tag`/`attribute` are standard tree-sitter web captures, so it
also serves future html-ish grammars (vue/svelte/astro).
**Retro-paint on already-bundled languages (intended, added rev 4).** The
capture table is global, so adding `attribute` also colours the `@attribute`
capture that three bundled grammars already emit but which was previously
unrecognized (and so unpainted): **rust** (`attribute_item`/`inner_attribute_item`
— every `#[derive(…)]` / `#![…]`), **lua** (the `<const>`/`<close>` variable
attribute), and **yaml** (`%YAML`/`%TAG` directives). On merge these begin
painting `attribute` yellow (`fg 3`). Verified on a Rust buffer: `#[derive(Debug)]`
paints uniformly yellow (the `attribute_item` span carries no narrower overriding
captures) — a distinct-attribute convention most editors follow, and an
improvement over unpainted. This is **chosen, not incidental**: it is pinned by
`rust_attribute_repaints_via_shared_attribute_capture` (`src/highlight.rs`).
`tag` is clean — only HTML/CSS use it, so it has no retro-effect.
**On CSS custom properties (corrected in rev 3):** pmacs passes the buffer text
to `QueryCursor::captures` (`src/syntax.rs:1701`), so tree-sitter evaluates the
standard `#match?`/`#eq?`/`#any-of?` predicates natively — pmacs special-cases
only `#is? local`. So an ordinary property (`color`) matches only the
unconditional `(property_name) @property` and paints cleanly; a custom property
(`--brand`) additionally satisfies `#match? "^--"` and also receives `@variable`
— a benign double-capture whose winner is a within-layer precedence detail, out
of scope for v0 and not relied upon. Rejected: a shadow overlay re-capturing the
same nodes with recognized names (fragile, precedence-dependent, duplicative).
### Q#WEB5 — Injection scope: script + style only
HTML's `INJECTIONS_QUERY` (v0.23.2) has **exactly two** patterns:
`(script_element (raw_text) @injection.content) (#set! injection.language "javascript")`
and the same for `(style_element …)``"css"`. There is **no** event-handler
(`onclick=…`) or inline `style=` attribute injection — those are a named
deferral (§5). Each element is its own subtree, so `injection.combined` (many
matches → one shared parse, the PHP-in-HTML case) is not needed and stays
deferred. Acceptance pins script + style.
### Q#WEB6 — No protocol, frontend, or GPU change
Pure instance-side: two grammar entries, one injections query, a capture-table
extension. No wire type, no TUI/GPU edit.
## 4. Categorical bets
1. **The shim makes ABI a non-issue.** Both crates ride `tree-sitter-language
0.1`, exactly like every bundled grammar.
2. **HTML's value is the injection, not the tags.** Highlighting a web page
*with* its embedded JS and CSS is the north-star injection consumer;
registering CSS is the prerequisite, which is why the two grammars ship
together as one lane.
3. **Extending the core capture table is the correct reconciliation for
crate-exported queries.** You cannot rename a `const` query's captures;
teaching the highlighter the standard web captures is the general fix and
costs one localized, uncontended edit.
## 5. Deferred (named)
- **SCSS / LESS / Sass** grammars (distinct node sets; own extensions).
- **`injection.combined`** (PHP-in-HTML and other many→one-parse schemes) — the
existing side-quest deferral; not needed for script/style.
- **CSS-in-JS / HTML-in-JS** via `tree-sitter-javascript`'s `INJECTIONS_QUERY`
(the `javascript` entry is `injections_query: &[]` today) — a clean
follow-up: add the JS injections query so tagged template literals
(`` css`…` ``, `` html`…` ``) parse. Separate consumer, separate PR.
- **Inline HTML attribute injection** — event handlers (`onclick=…`) and
`style=` attributes are not in the upstream injections query (Q#WEB5); adding
them would need a pmacs-side injections overlay.
- **Vue / Svelte / Astro** single-file-component grammars.
- **Bundled HTML/CSS LSP configs** — no server ships today (§2). Shipping
default `pmacs.lsp.config.html`/`.css` entries (vscode-langservers-extracted)
is a separate follow-up; until then the grammar's stable language ID lets a
user config attach automatically.
## 6. Acceptance
Mirrors the #144 / CUDA conventions (`src/syntax.rs` table guards + smokes;
`src/highlight.rs` paint template `grid_paints_injected_child_keyword`).
1. **Table guards** `builtin_languages_include_html` / `_css`: entries exist,
claim their extensions, carry non-empty highlights; `html` also carries
`INJECTIONS_QUERY`.
2. **Load-and-parse smokes**: a minimal HTML document and a CSS rule parse with
the expected root node kind and `!has_error()` (exact kinds — likely
`document` / `stylesheet` — confirmed at implementation).
3. **Highlights resolve**: `reg.highlights_query("html")` / `("css")` compile
against their grammars (the node-name compatibility gate).
4. **Extension resolution**: `.html`/`.htm`/`.xhtml` → `html`; `.css``css`.
5. **Injection (the payoff)**: an HTML buffer with
`<style>a{color:red}</style>` and `<script>let x=1</script>` produces a
`ParseTreeBundle` whose child layers resolve to `css` and `javascript`, and
a grid-paint asserts an injected CSS property and JS keyword paint **inside**
the embedded regions (mirroring `grid_paints_injected_child_keyword`).
6. **Paint (non-vacuous highlighting)**: parse `<a href="x">` and assert **both**
the `<a>` tag (`@tag`) **and** the `href` attribute name (`@attribute`) paint
the two new non-default styles — the attribute assertion is load-bearing, since
a tag-only test could pass with `@attribute` unverified. Also assert a CSS
selector (`@tag`) paints the new `tag` style and an ordinary CSS property
(`color` → `@property`) paints non-default (an ordinary property has a single
unconditional capture, so no precedence subtlety — Q#WEB4).
7. **Full gate suite** per `CLAUDE.md`.
## 7. Prior art in pmacs
- **LaTeX lane #144** (`docs/latex-grammar-math-substrate-framing.md`) — the
grammar-add mechanics, the table-guard/smoke/paint test conventions, and the
compile-gate-plus-paint-test discipline. (Its overlay convention is not
needed here.)
- **Multi-language injections #122**
(`docs/multi-language-injections-framing.md`) — the `ParseTreeBundle` +
`Layer` engine HTML's injections ride.
- **markdown → rust fenced code** (`src/syntax.rs:848`, tested by
`src/highlight.rs`'s `grid_paints_injected_child_keyword`) — the working
injection precedent this lane's acceptance 5 mirrors.

View File

@ -169,6 +169,11 @@ impl Theme {
("decorator", fg(13)), ("decorator", fg(13)),
("regexp", fg(2)), ("regexp", fg(2)),
("typeParameter", fg_italic(11)), ("typeParameter", fg_italic(11)),
// Web grammars (HTML/CSS, framing Q#WEB4): the only captures their
// crate-exported queries use that the set above lacks. `@tag.error`
// prefix-walks to `tag`.
("tag", fg(5)),
("attribute", fg(3)),
]; ];
let by_capture = entries let by_capture = entries
.iter() .iter()
@ -1422,4 +1427,182 @@ mod tests {
"the \\section title text is painted @keyword.control (bold)" "the \\section title text is painted @keyword.control (bold)"
); );
} }
#[test]
fn rust_attribute_repaints_via_shared_attribute_capture() {
// Intended side effect, named in the framing (Q#WEB4): the
// `("attribute", fg(3))` entry added for HTML/CSS also colours the
// `@attribute` capture that tree-sitter-rust (attribute_item), -lua
// (`<const>`), and -yaml (directives) already emit. A Rust
// `#[derive(Debug)]` — previously unpainted, since `@attribute` was
// unrecognized — now paints the attribute style throughout. Pinned so
// the retro-paint on this repo's primary language is a chosen effect,
// not an incidental one.
use crate::buffer::{Buffer, BufferId, EditOp};
use crate::cell::{Cell, CellSize};
use crate::syntax::{ParseView, SyntaxRegistry};
let reg = SyntaxRegistry::new();
let language = reg.language("rust").expect("rust grammar");
let src = b"#[derive(Debug)]\nstruct S;\n";
let mut buf = Buffer::new(BufferId::next(), "a.rs");
buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
.unwrap();
let view = ParseView::new(&buf, language, "rust".to_owned());
let handle = view.handle();
let _vid = buf.attach_view(Box::new(view));
let mut req = handle.make_request();
req.injection_aliases = reg.injection_alias_snapshot();
let bundle = crate::syntax::run_parse(req).expect("rust parse");
handle.install(reg.resolve_layer_queries(&bundle));
let mut hv = SyntaxHighlightView::new(handle, reg.theme());
let (rows, cols) = (1usize, 20usize);
let mut backing: Vec<Cell> = vec![Cell::default(); rows * cols];
let mut grid = CellGrid {
cells: &mut backing,
stride: cols as u32,
size: CellSize::new(rows as u32, cols as u32),
};
let viewport = Viewport {
buffer_start: 0,
buffer_end: u64::MAX,
cell_origin: CellCoord::new(0, 0),
cell_size: CellSize::new(rows as u32, cols as u32),
gutter_w: 0,
folds: None,
};
let registry = buf;
hv.render(&registry, viewport, &mut grid);
// `derive` (col 2) sits inside the `attribute_item` and paints the
// shared @attribute style (fg 3) — the intended retro-paint.
assert_eq!(
grid.get(CellCoord::new(0, 2)).style.fg,
pmacs_protocol::cell::Color::Indexed(3),
"a Rust #[derive] attribute paints the shared @attribute style (fg 3)"
);
}
#[test]
fn web_grid_paints_html_tag_and_attribute() {
// Q#WEB4 acceptance: the two capture entries this lane adds (`tag`,
// `attribute`) actually reach painted cells. The attribute assertion is
// load-bearing — a tag-only test could pass with `@attribute` unverified.
use crate::buffer::{Buffer, BufferId, EditOp};
use crate::cell::{Cell, CellSize};
use crate::syntax::{ParseView, SyntaxRegistry};
let reg = SyntaxRegistry::new();
let language = reg.language("html").expect("html grammar");
// Line 0: <a href="x">Hi</a> — `a` (tag_name) at col 1, `href`
// (attribute_name) at col 3.
let src = b"<a href=\"x\">Hi</a>\n";
let mut buf = Buffer::new(BufferId::next(), "index.html");
buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
.unwrap();
let view = ParseView::new(&buf, language, "html".to_owned());
let handle = view.handle();
let _vid = buf.attach_view(Box::new(view));
let mut req = handle.make_request();
req.injection_aliases = reg.injection_alias_snapshot();
let bundle = crate::syntax::run_parse(req).expect("html parse");
handle.install(reg.resolve_layer_queries(&bundle));
let mut hv = SyntaxHighlightView::new(handle, reg.theme());
let (rows, cols) = (1usize, 40usize);
let mut backing: Vec<Cell> = vec![Cell::default(); rows * cols];
let mut grid = CellGrid {
cells: &mut backing,
stride: cols as u32,
size: CellSize::new(rows as u32, cols as u32),
};
let viewport = Viewport {
buffer_start: 0,
buffer_end: u64::MAX,
cell_origin: CellCoord::new(0, 0),
cell_size: CellSize::new(rows as u32, cols as u32),
gutter_w: 0,
folds: None,
};
let registry = buf;
hv.render(&registry, viewport, &mut grid);
let tag_cell = grid.get(CellCoord::new(0, 1));
let attr_cell = grid.get(CellCoord::new(0, 3));
assert_ne!(
tag_cell.style,
Cell::default().style,
"the <a> tag_name paints @tag (non-default)"
);
assert_ne!(
attr_cell.style,
Cell::default().style,
"the href attribute_name paints @attribute (non-default)"
);
assert_ne!(
tag_cell.style, attr_cell.style,
"@tag and @attribute use the two distinct new styles"
);
}
#[test]
fn html_injects_css_and_js() {
// The payoff (Q#WEB acceptance 5): HTML's INJECTIONS_QUERY parses
// <style> as CSS and <script> as JavaScript, and the child layers paint
// INSIDE the embedded regions — styling only the injected grammars can
// produce. `css` is resolvable only because this lane registered it.
use crate::buffer::{Buffer, BufferId, EditOp};
use crate::cell::{Cell, CellSize};
use crate::syntax::{ParseView, SyntaxRegistry};
let reg = SyntaxRegistry::new();
let language = reg.language("html").expect("html grammar");
// Line 0: <style>a{color:red}</style> — `color` (CSS property) at col 9.
// Line 1: <script>let x=1</script> — `let` (JS keyword) at col 8.
let src = b"<style>a{color:red}</style>\n<script>let x=1</script>\n";
let mut buf = Buffer::new(BufferId::next(), "page.html");
buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
.unwrap();
let view = ParseView::new(&buf, language, "html".to_owned());
let handle = view.handle();
let _vid = buf.attach_view(Box::new(view));
let mut req = handle.make_request();
req.injection_aliases = reg.injection_alias_snapshot();
let bundle = crate::syntax::run_parse(req).expect("html parse");
handle.install(reg.resolve_layer_queries(&bundle));
let mut hv = SyntaxHighlightView::new(handle, reg.theme());
let (rows, cols) = (2usize, 60usize);
let mut backing: Vec<Cell> = vec![Cell::default(); rows * cols];
let mut grid = CellGrid {
cells: &mut backing,
stride: cols as u32,
size: CellSize::new(rows as u32, cols as u32),
};
let viewport = Viewport {
buffer_start: 0,
buffer_end: u64::MAX,
cell_origin: CellCoord::new(0, 0),
cell_size: CellSize::new(rows as u32, cols as u32),
gutter_w: 0,
folds: None,
};
let registry = buf;
hv.render(&registry, viewport, &mut grid);
// Inside <style>: the CSS `color` property paints (non-default) —
// proves the <style> -> css injection resolved and parsed.
assert_ne!(
grid.get(CellCoord::new(0, 9)).style,
Cell::default().style,
"the CSS `color` property paints inside the <style> injection"
);
// Inside <script>: the JS `let` keyword paints bold — proves the
// <script> -> javascript injection resolved and parsed.
assert!(
grid.get(CellCoord::new(1, 8)).style.bold,
"the JS `let` keyword paints (bold) inside the <script> injection"
);
}
} }

View File

@ -1108,6 +1108,28 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
locals_query: &[], locals_query: &[],
injections_query: &[], injections_query: &[],
}, },
// HTML + CSS (framing `docs/web-grammars-html-css-framing.md`). Both crates
// export their query constants (no overlay). HTML's `INJECTIONS_QUERY`
// wires `<script>` -> javascript (already registered) and `<style>` -> css
// (below), riding the #122 injection engine; `css` must be registered here
// for that injection to resolve. The `tag`/`attribute` captures these
// queries use are taught to the highlighter in `crate::highlight` (Q#WEB4).
LanguageEntry {
name: "html",
extensions: &["html", "htm", "xhtml"],
loader: || tree_sitter_html::LANGUAGE.into(),
highlights_query: &[tree_sitter_html::HIGHLIGHTS_QUERY],
locals_query: &[],
injections_query: &[tree_sitter_html::INJECTIONS_QUERY],
},
LanguageEntry {
name: "css",
extensions: &["css"],
loader: || tree_sitter_css::LANGUAGE.into(),
highlights_query: &[tree_sitter_css::HIGHLIGHTS_QUERY],
locals_query: &[],
injections_query: &[],
},
]; ];
/// LaTeX highlights overlay (framing Q#LX2). The chosen grammar crate /// LaTeX highlights overlay (framing Q#LX2). The chosen grammar crate
@ -2372,6 +2394,127 @@ mod tests {
} }
} }
#[test]
fn builtin_languages_include_html_and_css() {
// Both crate grammars export their query constants (no overlay). HTML
// additionally carries an injections query (script/style); CSS does not.
let html = BUILTIN_LANGUAGES
.iter()
.find(|l| l.name == "html")
.expect("`html` language entry must be present");
for ext in ["html", "htm", "xhtml"] {
assert!(html.extensions.contains(&ext), "`html` claims `.{ext}`");
}
assert!(
!html.highlights_query.is_empty(),
"`html` ships a highlights query"
);
assert!(
!html.injections_query.is_empty(),
"`html` ships an injections query (script/style)"
);
let css = BUILTIN_LANGUAGES
.iter()
.find(|l| l.name == "css")
.expect("`css` language entry must be present");
assert!(css.extensions.contains(&"css"), "`css` claims `.css`");
assert!(
!css.highlights_query.is_empty(),
"`css` ships a highlights query"
);
}
#[test]
fn html_grammar_loads_and_parses() {
// ABI acceptance: `tree-sitter-html` (LanguageFn over
// `tree-sitter-language 0.1`) is accepted by our `tree-sitter` 0.26 core.
let reg = SyntaxRegistry::new();
let language = reg
.language("html")
.expect("`html` language loads from BUILTIN_LANGUAGES");
let mut buf = fresh_buffer("index.html");
buf.apply_edit(EditOp::Insert {
pos: 0,
bytes: b"<!DOCTYPE html>\n<html><body><a href=\"x\">Hi</a></body></html>\n",
})
.unwrap();
let view = ParseView::new(&buf, language, "html".to_owned());
let handle = view.handle();
let _vid = buf.attach_view(Box::new(view));
let bundle = parse_synchronously(&handle);
assert_eq!(
bundle.root_tree().root_node().kind(),
"document",
"HTML grammar roots at document"
);
assert!(
!bundle.root_tree().root_node().has_error(),
"HTML grammar parses a document without error"
);
}
#[test]
fn css_grammar_loads_and_parses() {
let reg = SyntaxRegistry::new();
let language = reg
.language("css")
.expect("`css` language loads from BUILTIN_LANGUAGES");
let mut buf = fresh_buffer("style.css");
buf.apply_edit(EditOp::Insert {
pos: 0,
bytes: b"a { color: red; }\n",
})
.unwrap();
let view = ParseView::new(&buf, language, "css".to_owned());
let handle = view.handle();
let _vid = buf.attach_view(Box::new(view));
let bundle = parse_synchronously(&handle);
assert_eq!(
bundle.root_tree().root_node().kind(),
"stylesheet",
"CSS grammar roots at stylesheet"
);
assert!(
!bundle.root_tree().root_node().has_error(),
"CSS grammar parses a rule without error"
);
}
#[test]
fn html_and_css_highlights_resolve() {
// The crate-exported queries compile against their grammars (node-name
// compatibility gate), and both use the `@tag` capture this lane teaches
// the highlighter (Q#WEB4).
let reg = SyntaxRegistry::new();
for lang in ["html", "css"] {
let query = reg
.highlights_query(lang)
.unwrap_or_else(|| panic!("{lang} highlights compile against the grammar"));
let names = query.capture_names();
assert!(
names.contains(&"tag"),
"{lang} highlights use the @tag capture; got {names:?}"
);
}
}
#[test]
fn language_for_path_resolves_web_extensions() {
let reg = SyntaxRegistry::new();
for (path, lang) in [
("index.html", "html"),
("page.htm", "html"),
("doc.xhtml", "html"),
("style.css", "css"),
] {
assert_eq!(
reg.language_name_for_path(path).as_deref(),
Some(lang),
"{path} resolves to {lang}"
);
}
}
#[test] #[test]
fn builtin_languages_include_bash() { fn builtin_languages_include_bash() {
// Regression guard: the bash entry claims the wider shell family // Regression guard: the bash entry claims the wider shell family