// highlight.rs --- T M4.3 syntax-highlight view + theme. //! Syntax-highlight view (T M4.3). //! //! Glue between [`crate::syntax`] (parse trees + highlight queries) //! and the [`crate::view::View`] composition stack //! ([`crate::overlay`]). Reads the active [`Theme`] to translate //! tree-sitter capture names into [`Style`] values, then writes //! merged styles into the cells the base [`crate::text_view::TextView`] //! has already painted. //! //! # Lifecycle //! //! 1. [`crate::syntax::SyntaxRegistry`] holds the shared //! [`ThemeHandle`] and per-language compiled queries. //! 2. The Lua side ([`crate::lua_bindings`]) creates a //! [`SyntaxHighlightView`] when a buffer's grammar is detected //! and pushes it onto the active window's overlay stack. //! 3. Every render: the view checks whether the buffer's //! [`crate::syntax::ParseViewHandle`] holds a different parse //! tree than last frame (compared by `Arc::ptr_eq`); if so it //! re-runs the highlight query, caches the result, and recomputes //! a per-line spans index. Render reads from the cached index //! to apply styles cell by cell. //! //! # Threading //! //! [`SyntaxHighlightView`] holds only `Arc<...>`-based state so it //! satisfies the [`crate::view::View`]'s `Send` bound. In practice //! everything runs main-thread; the `Send` bound exists because //! `Box` is held by the buffer / window machinery. use std::collections::HashMap; use std::sync::{Arc, Mutex}; use crate::buffer::Buffer; use crate::cell::{CellCoord, CellGrid, Color, Style, UnderlineStyle}; use crate::display_width::byte_range_to_columns; use crate::lsp::SharedLspManager; use crate::overlay::merge_styles; use crate::syntax::{HighlightSpan, ParseTreeBundle, ParseViewHandle, compute_highlight_spans_for}; use crate::view::{View, Viewport}; // --------------------------------------------------------------------------- // Theme // --------------------------------------------------------------------------- /// Map from tree-sitter capture name to [`Style`] plus a fallback /// `default_style`. /// /// Capture names are dotted: `function.method`, `variable.parameter`, /// `keyword.control.return`. [`Self::lookup`] tries the full name /// first, then progressively shorter dot-separated prefixes, then /// falls back to `default_style`. This matches conventional editor /// theme behavior --- a theme can either be coarse (just `keyword`) /// or fine-grained (`keyword.control.return`) and the lookup walks /// the same hierarchy in both cases. #[derive(Clone, Debug, Default)] pub struct Theme { /// Direct map from capture name → style. T M4.3. Names matching /// [`is_face_name`] (`ui` / `ui.*`) are UI faces (themes arc /// Q#TH2), reserved by convention — no tree-sitter capture or LSP /// token type uses them. pub by_capture: HashMap, /// Fallback style when no capture matches. Defaults to the /// terminal default colors (no override) so unhighlighted text /// looks identical to plain rendering. pub default_style: Style, /// Monotonic syntax-mutation counter (themes arc Q#TH6). Bumped /// by every successful Lua mutation that commits a non-face key /// (or touches `default_style`); keys the `StyleGate` and the /// minimap summary so a mid-session recolor re-ships spans. /// INVARIANT: only ever incremented — a wholesale `set` must /// replace `by_capture`, never the whole `Theme`, or consecutive /// mutations share an epoch and become invisible to every gate. pub syntax_epoch: u64, /// Monotonic face-mutation counter (themes arc Q#TH6). Bumped by /// every successful Lua mutation that commits a face key /// ([`is_face_name`]); keys the `ThemeFacts` producer and the /// minimap summary (`ui.diag.*` feeds its marks). Same /// increment-only invariant as `syntax_epoch`. pub face_epoch: u64, } /// Themes arc Q#TH2: the face predicate. A theme key names a UI face /// iff it is exactly `ui` (the deliberate inheritance catch-all — /// [`Theme::face`]'s walk terminal) or starts with `ui.`. Shared by /// the namespace reservation, the mutation-counter classification, /// and the `ThemeFacts` producer's key filter. #[must_use] pub fn is_face_name(name: &str) -> bool { pmacs_protocol::is_ui_face_name(name) } impl Theme { /// Empty theme: no captures match anything; the default style /// is the terminal default. Useful for tests that want to start /// from a clean slate. #[must_use] pub fn empty() -> Self { Self::default() } /// A small built-in dark theme that picks up the most common /// tree-sitter captures **and** the LSP semantic-token type names /// clangd / rust-analyzer / gopls actually emit, so opening a code /// file produces visible highlighting without a user theme. Uses /// 8/16-color indexed terminal colors for portability — truecolor /// themes can be pushed in from Lua. /// /// Themes that want more granularity layer on top via /// [`Self::insert`] / Lua's `pmacs.theme.set`. The dotted-prefix /// [`Self::lookup`] means a modifier-refined name like /// `function.defaultLibrary` falls back to `function` if not /// defined — safe to query unconditionally. #[must_use] pub fn default_dark() -> Self { // Indexed terminal palette: 1 red, 2 green, 3 yellow, 4 blue, // 5 magenta, 6 cyan, 8 bright black/gray; bright variants 9..14. let fg = |c: u8| Style { fg: Color::Indexed(c), ..Style::default() }; let fg_bold = |c: u8| Style { fg: Color::Indexed(c), bold: true, ..Style::default() }; let fg_italic = |c: u8| Style { fg: Color::Indexed(c), italic: true, ..Style::default() }; let italic_only = Style { italic: true, ..Style::default() }; // (capture name, style). LSP semantic-token type names are the // unprefixed entries (`macro`, `namespace`, `parameter`, …); // tree-sitter captures are the dotted ones (`keyword.control`, // `function.method`, `type.builtin`, `constant.builtin`). let entries: &[(&str, Style)] = &[ ("keyword", fg_bold(5)), ("keyword.control", fg_bold(13)), ("function", fg(4)), ("function.method", fg(12)), ("type", fg(3)), ("type.builtin", fg(11)), ("string", fg(2)), ("comment", fg_italic(8)), ("constant", fg(1)), ("constant.builtin", fg(9)), ("number", fg(1)), ("operator", fg(6)), ("variable", Style::default()), ("punctuation", Style::default()), // LSP additions (no tree-sitter overlap). ("macro", fg_bold(13)), ("namespace", fg(11)), ("parameter", italic_only), ("property", fg(6)), ("class", fg(3)), ("struct", fg(3)), ("enum", fg(3)), ("interface", fg(3)), ("enumMember", fg(1)), ("modifier", fg_bold(5)), ("decorator", fg(13)), ("regexp", fg(2)), ("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)), // Lean 4 (framing Q#LN4). These four are the captures the Lean // query uses that the set above lacks — but three of them are // NOT Lean-only, and adding them here changes languages that // already ship. That is the #146 lesson (`attribute`, above, // retro-painted rust/lua/yaml) and it is deliberate, not // incidental: // // * `constructor` reaches SEVEN entries — rust, lua, python, // javascript, and (because `tree_sitter_javascript:: // HIGHLIGHT_QUERY` is concatenated base-first into them) // javascriptreact, typescript, typescriptreact. Its shape is // not "constructors": rust/python/javascript tag every // capitalized identifier (`#match? "^[A-Z]"`), and lua tags // every table-constructor brace. So this recolors `Some`, // `None`, `Ok`, `Err`, every class-cased name, and every Lua // `{}`. All of those render as unstyled default text today. // * `character` reaches zig only. // * `keyword.conditional` reaches cmake and zig, which // currently flatten it to `keyword`; giving it // `keyword.control`'s style makes their conditionals read the // way rust's already do. // * `warning` reaches no other grammar. It exists for Lean's // `sorry` — an unproved goal, the single most important thing // to see in a proof file. // // The alternative was an in-repo query overlay renaming these // into the existing vocabulary (the #144 LaTeX pattern), which // would fork a 213-line query we would then own and hand-merge // on every crate bump. There is no middle option: styling Lean's // constructors without touching the other seven entries requires // renaming the capture, which requires the overlay. ("constructor", fg(11)), ("character", fg(2)), ("keyword.conditional", fg_bold(13)), // Bold BRIGHT red, deliberately the loudest entry in the table // and deliberately distinct from `number`'s plain `fg(1)`: in a // proof file `sorry` means "this is admitted, not proved", which // is the one thing a reader must never skim past. Plain `fg(1)` // would have collided with every numeric literal on colour alone. ("warning", fg_bold(9)), ]; let by_capture = entries .iter() .map(|(name, style)| ((*name).to_owned(), *style)) .collect(); Self { by_capture, default_style: Style::default(), syntax_epoch: 0, face_epoch: 0, } } /// Resolve a capture name to a [`Style`]. Tries the full name, /// then strips one `.`-separated segment at a time, then falls /// back to `default_style`. #[must_use] pub fn lookup(&self, capture_name: &str) -> Style { let mut name = capture_name; loop { if let Some(s) = self.by_capture.get(name) { return *s; } match name.rfind('.') { Some(idx) => name = &name[..idx], None => return self.default_style, } } } /// Resolve a UI face name to its style, or `None` when unset /// (themes arc Q#TH4). Same dotted-prefix walk as [`Self::lookup`] /// — so `ui.search.match.active` falls back to `ui.search.match`, /// the `ui.diag.*` children to `ui.diag`, and everything to the /// bare-`ui` catch-all — but the walk returns `None` instead of /// falling back to `default_style`: an unset face must leave the /// paint site's hardcoded default untouched, and a user's /// `pmacs.theme.default` (a *syntax* fallback) must never bleed /// into chrome. An exact entry stops the walk, so an explicitly /// empty child (e.g. `ui.diag.error = {}`) blocks inheritance /// from a themed parent. Callers pass full face names only. #[must_use] pub fn face(&self, name: &str) -> Option — `color` (CSS property) at col 9. // Line 1: — `let` (JS keyword) at col 8. let src = b"\n\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 = 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, wrap: WrapMode::Truncate, view_left: 0, }; let registry = buf; hv.render(®istry, viewport, &mut grid); // Inside