Skip to main content

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}