lattice_host/sticky_context.rs
1//! TC.3b — the sticky-context layer both renderers paint from.
2//!
3//! [`StickyContext`] is the resolved answer to "which source lines are pinned
4//! above this pane's text, and what do they look like". It is built by
5//! `cells_worker` in the same pass that builds the pane's
6//! [`DisplayMatrix`](crate::display_matrix::DisplayMatrix) and
7//! [`IndentGuides`](crate::indent_guides::IndentGuides), from the same snapshot
8//! and stamped with the same [`MatrixVersion`] — so the three cannot disagree
9//! and this layer needs no staleness axis of its own.
10//!
11//! ## Why the rows are built here rather than copied by the renderer
12//!
13//! A context header is, by construction, a line that has scrolled ABOVE the
14//! viewport. The `CellMatrix` is chunked above `4 × viewport_height` lines, so
15//! a header three thousand lines up is routinely **not resident** in any built
16//! chunk. A renderer copying from the published matrix would find nothing and
17//! have to fall back to unhighlighted text — a visible colour flicker on
18//! scroll, which the UX contract vetoes.
19//!
20//! The worker holds the rope, the syntax snapshot and the cell builder, so it
21//! can build a row for any line whether or not a chunk covers it. That is also
22//! what makes the highlighting *identical* to the document's rather than merely
23//! similar: there is one derivation, not two.
24//!
25//! ## Why it is keyed by pane and not by buffer
26//!
27//! Every other per-pane layer is keyed by `BufferId`, which means two panes
28//! showing one buffer share it. `IndentGuides` gets away with that by
29//! publishing block extents and letting each renderer pick the active one from
30//! its own cursor. Context cannot: the ROWS differ per pane, not just which one
31//! is emphasised. One buffer open in two splits with cursors in different
32//! scopes must show different context.
33//!
34//! Design: `docs/dev/architecture/treesitter-context.md`.
35
36use std::sync::Arc;
37
38use lattice_cells::{Cell, MatrixVersion};
39
40/// One pinned context row: the source line it mirrors, and its cells.
41///
42/// `source_line` is carried so a renderer can show the real line number in the
43/// gutter (`context.line-numbers`) without a second lookup, and so a future
44/// click-to-jump has the target without re-resolving.
45#[derive(Clone, Debug)]
46pub struct StickyContextRow {
47 pub source_line: u32,
48 pub cells: Arc<[Cell]>,
49}
50
51/// The pane's pinned context strip, outermost scope first.
52///
53/// Empty is the overwhelmingly common case — no context plugin loaded, or
54/// nothing enclosing the cursor has scrolled away — and it is represented as an
55/// empty `rows` rather than an `Option` so both renderers can iterate
56/// unconditionally.
57#[derive(Clone, Debug, Default)]
58pub struct StickyContext {
59 /// Outermost first: the row nearest the text is the nearest enclosing
60 /// scope, so the strip reads as a continuation of the code.
61 pub rows: Vec<StickyContextRow>,
62 /// The build's version stamp — the same `MatrixVersion` the pane's
63 /// `DisplayMatrix` carries, so a renderer can tell the two came from one
64 /// build.
65 pub version: MatrixVersion,
66 /// Resolved backdrop (`0xRRGGBB`) from the `sticky.context.background`
67 /// theme element, or `None` when the theme leaves it unset.
68 ///
69 /// Resolved once here rather than per row in each renderer: the strip is
70 /// host chrome — the host builds, reserves and paints it — so the host
71 /// owns its styling, while the plugin owns only the scopes. Without a
72 /// backdrop the strip is the same colour as the code beneath it and reads
73 /// as ordinary text that will not scroll, which is what prompted this.
74 pub bg: Option<u32>,
75 /// TC.8: whether the rows show their source line number in the gutter
76 /// (`context.line-numbers`).
77 ///
78 /// Resolved here, alongside the backdrop, for the same reason: the strip
79 /// is host chrome, so the host owns its presentation and neither renderer
80 /// reads a plugin option. Both peers then reduce to one expression —
81 /// `line_numbers.then_some(row.source_line)` — which is hard to get
82 /// differently wrong in two places.
83 pub line_numbers: bool,
84 /// TC.11: resolved `sticky.context.line_number` foreground, or `None` when
85 /// the theme leaves it unset (the renderer then uses its default gutter
86 /// colour, as it does for document rows).
87 pub line_number_fg: Option<u32>,
88 /// TC.11: resolved `sticky.context.active` backdrop for the INNERMOST row
89 /// — the scope the cursor is actually in.
90 ///
91 /// Rows are outermost-first, so this applies to the last one. It is the
92 /// line the reader is looking for; giving it the same backdrop as its
93 /// ancestors makes a deep strip read as an undifferentiated block.
94 pub active_bg: Option<u32>,
95 /// TC.12: whether the LAST row is a separator rule rather than a scope.
96 ///
97 /// The rule is built as a real row, not carried as a glyph for renderers
98 /// to append, so that it is counted by `len()` — which is what the scroll
99 /// model reserves from. A separator drawn outside the row list would be
100 /// one row the reservation did not know about, and the text under it would
101 /// sit one line too high.
102 pub has_separator: bool,
103}
104
105impl StickyContext {
106 pub fn empty() -> Self {
107 Self::default()
108 }
109
110 pub fn is_empty(&self) -> bool {
111 self.rows.is_empty()
112 }
113
114 pub fn len(&self) -> usize {
115 self.rows.len()
116 }
117
118 /// Index of the innermost SCOPE row — the one the cursor is in — skipping
119 /// a trailing separator. `None` when there are no scope rows.
120 pub fn innermost_scope(&self) -> Option<usize> {
121 let scopes = self.rows.len() - usize::from(self.has_separator);
122 (scopes > 0).then(|| scopes - 1)
123 }
124}