Skip to main content

lattice_cells/
style.rs

1//! Syntax-highlight style types and the `ExcerptHighlighter` trait.
2//!
3//! Moved from `lattice-syntax` so that `lattice-runtime` (which defines the
4//! `Document` trait) can reference `ExcerptHighlighter` without pulling in
5//! `lattice-syntax` — which has a transitive dep on `lattice-mode` →
6//! `lattice-runtime` (cycle). `lattice-cells` has no lattice deps, breaking
7//! the chain cleanly.
8//!
9//! `lattice-syntax` re-exports everything from here so call-sites outside
10//! `lattice-cells` / `lattice-runtime` see no path change.
11
12use std::sync::Arc;
13
14/// Semantic style category emitted by the tree-sitter highlighter.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Style {
17    Default,
18    Comment,
19    LineComment,
20    String,
21    Keyword,
22    Type,
23    Number,
24    Function,
25    Constant,
26    Variable,
27    Operator,
28    Punctuation,
29    Attribute,
30    // ---- Markup styles (markdown / org / future rich-text modes) ----
31    /// `# Heading` — level 1.
32    Heading1,
33    /// `## Heading` — level 2.
34    Heading2,
35    /// `### Heading` — level 3.
36    Heading3,
37    /// `#### Heading` — level 4.
38    Heading4,
39    /// `##### Heading` — level 5.
40    Heading5,
41    /// `###### Heading` — level 6.
42    Heading6,
43    /// `**bold**` / `__bold__` text.
44    Bold,
45    /// `*italic*` / `_italic_` text.
46    Italic,
47    /// Link label / link text (`[label]`). Distinct from [`Style::Url`] so
48    /// the renderer can underline navigable labels without underlining the URL.
49    Link,
50    /// Link destination (`(url)`) and autolinks.
51    Url,
52    /// Inline `` `code` ``, fenced code blocks without an info string, link
53    /// titles.
54    MarkupRaw,
55    /// List markers, thematic breaks, blockquote markers, and other markup
56    /// punctuation.
57    Markup,
58    // ---- Diagnostic severities (L4b: the `gl` popup colours each line
59    // by its diagnostic's severity). These resolve to the same theme
60    // colours the gutter glyph + inline underline use
61    // (`diagnostic_{error,warning,info,hint}` elements), via
62    // `syntax_element_id`. Not produced by any tree-sitter grammar —
63    // only the diagnostics-popup highlight builder emits them. ----
64    /// Error-severity diagnostic line.
65    DiagnosticError,
66    /// Warning-severity diagnostic line.
67    DiagnosticWarning,
68    /// Information-severity diagnostic line.
69    DiagnosticInfo,
70    /// Hint-severity diagnostic line.
71    DiagnosticHint,
72    // ---- Diff text styles (magit inline diff content) ----
73    /// Added line content (`+` lines) in a unified diff.
74    DiffAdd,
75    /// Removed line content (`-` lines) in a unified diff.
76    DiffRemove,
77    // ---- Magit-owned styles ----
78    // Git concepts magit's own buffers render (commit SHAs, the
79    // checked-out branch, ref-decoration lists, rebase-todo verbs,
80    // blame authors) are NOT tree-sitter syntax categories — giving
81    // them their own `Style` variants (rather than reusing
82    // `Keyword`/`Link`/`Type`/`Comment`, which name unrelated
83    // source-code concepts) keeps the mapping honest and lets a theme
84    // retune magit's palette independently of its code-syntax colors.
85    /// A commit SHA (magit-log, magit-blame, magit-rebase's todo).
86    // `*messages*` log levels. Their own family rather than the
87    // `Diagnostic*` one, because `messages.timestamp` / `messages.error` /
88    // … are registered, user-settable theme elements: folding them into the
89    // diagnostic colours would silently orphan a vocabulary people can
90    // already theme.
91    MessagesTimestamp,
92    MessagesTrace,
93    MessagesDebug,
94    MessagesInfo,
95    MessagesWarn,
96    MessagesError,
97    MagitSha,
98    /// The checked-out branch in a branch list (magit-branch's `* `
99    /// marker + name).
100    MagitBranchCurrent,
101    /// A ref-decoration list after a log SHA (`(HEAD -> main, ...)`).
102    MagitRefDecoration,
103    /// A rebase-todo verb (`pick`/`reword`/`edit`/`squash`/`fixup`/`drop`).
104    MagitRebaseVerb,
105    /// The author column in `magit-blame` output.
106    MagitAuthor,
107    // ---- Help-owned styles (HP.2) ----
108    // Help pages are markdown, and the markdown BLOCK grammar has no
109    // `code_span` node — that lives in the inline grammar, which is not
110    // wired up. So every `` `gr` ``, `` `:magit-status` `` and
111    // `` `action:magit-refresh` `` in every help page rendered as plain
112    // prose with visible backticks.
113    //
114    // Classifying them into four styles rather than one is what lets a
115    // theme make a KEY YOU PRESS look different from a COMMAND YOU TYPE
116    // — the distinction a reader is actually scanning for. They get
117    // their own variants for the same reason the `Magit*` family does:
118    // reusing `Keyword` or `Link` would name an unrelated source-code
119    // concept and tie help's palette to the code palette.
120    /// A key or chord you press (`` `gr` ``, `` `<C-c>g` ``, `` `]]` ``).
121    HelpKey,
122    /// An ex-command you type (`` `:magit-status` ``).
123    HelpCommand,
124    /// An action id (`` `action:magit-refresh` ``).
125    HelpAction,
126    /// Any other inline literal — a path, a filename, a git argument.
127    HelpLiteral,
128    /// Inline virtual text — an LSP inlay hint, or any other leading /
129    /// trailing text a producer splices into a row without it being in
130    /// the buffer.
131    ///
132    /// DL.3a: inlay runs used to be painted with a hardcoded
133    /// `Color::Named(DarkGray)` in the cells worker, with the run's
134    /// style discarded — while `inlay.hint` had been a registered theme
135    /// element the whole time. Giving inlays a real style variant is
136    /// what routes them through the theme like everything else, and it
137    /// is what lets a producer override the colour per inlay (the
138    /// listing icons use [`Style::Element`]).
139    InlayHint,
140    // ---- The open end of the vocabulary (DL.1) ----
141    /// A span styled by a **registered theme element**, named directly.
142    ///
143    /// Every variant above is a closed, editor-owned category, and that
144    /// is right for concepts the editor itself understands. It cannot
145    /// work for vocabularies that are open by nature — a per-language
146    /// file-icon palette has ~50 entries and grows, and a WASM plugin
147    /// can register a theme element by name but can **never** add a
148    /// variant to a Rust enum. Without this, themed highlighting is
149    /// reachable only by editing core, which makes it impossible for
150    /// plugins by construction (paramount goal #2).
151    ///
152    /// `syntax_element_id` returns the id unchanged, so this resolves
153    /// through exactly the same `ResolvedTheme` lookup as every builtin
154    /// category — a theme retunes it by name like any other element.
155    ///
156    /// Carries [`lattice_theme::ElementId`] (a `u32` newtype), so
157    /// `Style` stays `Copy` and its size is unchanged.
158    Element(lattice_theme::ElementId),
159}
160
161impl Style {
162    /// A stable-within-this-process numeric fingerprint, for folding a
163    /// style into a cache-version hash.
164    ///
165    /// DL.1: this exists because [`Style`] stopped being field-less.
166    /// Consumers used to write `style as u64`, which the compiler
167    /// allowed only while every variant was a unit — so adding
168    /// [`Style::Element`] would have broken them silently in spirit
169    /// (loudly in practice, which is how this was found). Routing them
170    /// through a named method means the payload is *included* in the
171    /// fingerprint: two spans differing only in which registered
172    /// element they name must not collide, or a theme-element change
173    /// would leave a stale matrix on screen.
174    pub fn fingerprint(self) -> u64 {
175        use std::hash::{Hash, Hasher};
176        struct Fnv(u64);
177        impl Hasher for Fnv {
178            fn finish(&self) -> u64 {
179                self.0
180            }
181            fn write(&mut self, bytes: &[u8]) {
182                for b in bytes {
183                    self.0 ^= u64::from(*b);
184                    self.0 = self.0.wrapping_mul(1099511628211);
185                }
186            }
187        }
188        let mut h = Fnv(14695981039346656037);
189        std::mem::discriminant(&self).hash(&mut h);
190        if let Style::Element(id) = self {
191            h.write(&id.0.to_le_bytes());
192        }
193        h.finish()
194    }
195}
196
197/// Byte-range span within one source line, carrying a semantic [`Style`].
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub struct StyledSpan {
200    /// Byte offset within the line where the span starts.
201    pub start: usize,
202    /// Byte offset within the line (exclusive).
203    pub end: usize,
204    pub style: Style,
205}
206
207/// DR.2 (2026-08-12): a byte range whose **background** differs from
208/// its row's — intra-line diff refinement.
209///
210/// A second, independent axis from [`StyledSpan`], and deliberately a
211/// separate type rather than a `bg` field on that one. The foreground
212/// axis resolves by first-match-wins over a concatenated list; a
213/// background is a different question with different precedence, and
214/// fusing them would force every existing span producer to have an
215/// opinion about a concern it does not have.
216///
217/// See `docs/dev/architecture/diff-refinement.md` §3 and
218/// `span-layering.md` §1 (the two-axis contract this extends).
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct RefineSpan {
221    /// Byte offset within the line where the span starts.
222    pub start: usize,
223    /// Byte offset within the line (exclusive).
224    pub end: usize,
225    pub kind: RefineKind,
226}
227
228/// Which side of a refined pair a [`RefineSpan`] belongs to. Picks the
229/// theme element, and nothing else.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum RefineKind {
232    /// Bytes added on this line — `diff.add.refine.bg`.
233    Added,
234    /// Bytes removed from this line — `diff.remove.refine.bg`.
235    Removed,
236}
237
238/// Trait implemented by `SyntaxHandle` (and future highlight providers).
239/// Used by `Document::excerpt_highlights` so that `lattice-runtime` can
240/// expose per-excerpt highlighting through the `Document` trait without
241/// taking a direct dep on `lattice-syntax`.
242pub trait ExcerptHighlighter: Send + Sync {
243    /// Return per-line styled spans for source rows `lo..hi` (exclusive).
244    /// The returned `Vec` has exactly `(hi - lo)` entries; an empty inner
245    /// `Vec` means "no spans on that line" (fall back to default fg).
246    /// Returns `None` when the highlight snapshot is stale or unavailable.
247    fn highlight_lines(&self, lo: u32, hi: u32) -> Option<Vec<Vec<StyledSpan>>>;
248
249    /// Monotonic version of the last-published parse result. Used to build
250    /// `MatrixVersion::syntax` without taking a dep on `SyntaxHandle` internals.
251    fn highlight_version(&self) -> u64;
252}
253
254/// Per-excerpt entry produced by [`Document::excerpt_highlights`].
255///
256/// `composed_start` / `composed_end` are line numbers in the multibuffer's
257/// composed coordinate space. `source_start` is the first source line mapped
258/// to `composed_start`. The highlighter operates in source coordinates.
259pub struct ExcerptHighlight {
260    pub composed_start: u32,
261    pub composed_end: u32,
262    pub source_start: u32,
263    pub highlighter: Arc<dyn ExcerptHighlighter>,
264    /// OA.7b: the excerpt's grammar name, for the conceal rules that apply to
265    /// its rows.
266    ///
267    /// Conceal is otherwise resolved once per PANE from the buffer's single
268    /// syntax handle — and a multibuffer has no single language, so it
269    /// resolved to no rules and an org link in an agenda row showed its raw
270    /// brackets while the same line concealed correctly in its own file.
271    ///
272    /// A `&'static str` rather than the rules themselves: `lattice-cells` has
273    /// no business knowing what a conceal rule is, and the name is what the
274    /// registry is keyed by anyway.
275    pub lang: Option<&'static str>,
276}