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}