// 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 unicode_width::UnicodeWidthChar; use crate::buffer::Buffer; use crate::cell::{CellCoord, CellGrid, Color, Style, UnderlineStyle}; 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 { name == "ui" || name.starts_with("ui.") } 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)), ]; 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