Merge branch 'main' into folding-tui
`main` moved fromc49a8c7(folding Stage 1, #142) to47581f4(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:
commit
97a27e9873
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
183
src/highlight.rs
183
src/highlight.rs
|
|
@ -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(®istry, 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(®istry, 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(®istry, 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
143
src/syntax.rs
143
src/syntax.rs
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue