Skip to main content

lattice_ui_gpui/
pane_options.rs

1//! The view options for ONE pane, resolved against the buffer that pane shows.
2//!
3//! GPUI's peer of the TUI's `FrameView` option fields, and the answer to the
4//! same question: a pane's gutter, cursorline, sign columns and indent guides
5//! are properties of the buffer being painted, not of whichever buffer happens
6//! to be focused.
7//!
8//! ## Why this exists as a type rather than four inline reads
9//!
10//! It did exist as four inline reads, in the middle of the per-pane render
11//! method, and three of them resolved against `active_document.option_cache` —
12//! the FOCUSED buffer. So a magit view sitting beside a focused file grew line
13//! numbers its own mode turns off, and a file beside a focused magit view lost
14//! them. One of the four carried a comment admitting it ("inactive panes
15//! inherit the active value — the same pre-existing per-pane-option
16//! limitation"), and a fourth (`cursorline`) had a special case for preview
17//! panes only, because that was the one configuration where a pane's buffer was
18//! *known* to differ from the active document. It is true of every unfocused
19//! pane.
20//!
21//! `RenderState::resolved_option_for` (PI.4) is the renderer-agnostic seam
22//! built for exactly this, and its own doc names the defect it was meant to
23//! end: peers resolving options "instead of the TUI reading the live editor and
24//! GPUI reading the active document's `option_cache`". The TUI half landed;
25//! this is the other one.
26//!
27//! Gathering them into a struct is what makes the rule checkable — the reads
28//! are now in one place with one test, instead of four places where the next
29//! option added silently picks the wrong source.
30
31use lattice_core::BufferId;
32use lattice_host::render_state::RenderState;
33
34/// Per-pane resolved view options.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct PaneOptions {
37    /// `:set number` — whether to reserve line-number digits in the gutter.
38    pub show_line_numbers: bool,
39    /// `:set cursorline` / `current-line-highlight-mode`.
40    pub cursorline: bool,
41    /// `:set signcolumn` — whether the two sign cells are reserved.
42    pub sign_column: bool,
43    /// `ui.indent-guides.active` — whether the cursor's block is emphasised.
44    pub indent_guides_active: bool,
45}
46
47impl PaneOptions {
48    /// Resolve every option against `buffer_id` — the buffer the pane is
49    /// showing, never the active document.
50    ///
51    /// A buffer with no published entry falls back to the global typed-option
52    /// default inside `resolved_option_for`, so a transient publish gap renders
53    /// as the user's configured default rather than as another buffer's value.
54    pub fn for_pane(rs: &RenderState, buffer_id: BufferId) -> Self {
55        Self {
56            show_line_numbers: *rs.resolved_option_for::<lattice_config::Number>(buffer_id),
57            cursorline: rs.current_line_highlight_for(buffer_id),
58            sign_column: rs
59                .resolved_option_for::<lattice_config::SignColumnOption>(buffer_id)
60                .reserved(),
61            indent_guides_active: *rs
62                .resolved_option_for::<lattice_config::core_options::IndentGuidesActive>(buffer_id),
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    #![allow(clippy::unwrap_used)]
70    use super::*;
71    use lattice_config::ResolvedOptions;
72    use lattice_host::render_state::ResolvedOptionsRenderState;
73    use std::collections::HashMap;
74    use std::sync::Arc;
75
76    /// Two buffers, one of which turns `number` and `cursorline` off the way
77    /// every magit mode does. Each must get its OWN answer.
78    ///
79    /// This is the regression guard for the reported bug: with the options read
80    /// from `active_document.option_cache`, both panes got one answer, and
81    /// which answer depended on which pane was focused.
82    #[test]
83    fn each_pane_resolves_options_against_its_own_buffer() {
84        let gutterless = BufferId::next();
85        let ordinary = BufferId::next();
86
87        // Every option `for_pane` reads is populated for both buffers:
88        // `resolved_option_for` falls back to the global registry when a
89        // buffer has no entry, and a default `RenderState` has none
90        // registered, so a partial fixture panics rather than exercising the
91        // fallback.
92        let mut off = ResolvedOptions::new();
93        off.insert::<lattice_config::Number>(false);
94        off.insert::<lattice_config::CursorLine>(false);
95        off.insert::<lattice_config::SignColumnOption>(lattice_config::SignColumn::No);
96        off.insert::<lattice_config::core_options::IndentGuidesActive>(false);
97        let mut on = ResolvedOptions::new();
98        on.insert::<lattice_config::Number>(true);
99        on.insert::<lattice_config::CursorLine>(true);
100        on.insert::<lattice_config::SignColumnOption>(lattice_config::SignColumn::Yes);
101        on.insert::<lattice_config::core_options::IndentGuidesActive>(true);
102
103        // The absent-buffer fallback is `resolved_option_for`'s own contract
104        // and is left to that seam's tests rather than half-asserted here.
105        let mut map: HashMap<BufferId, Arc<ResolvedOptions>> = HashMap::new();
106        map.insert(gutterless, Arc::new(off));
107        map.insert(ordinary, Arc::new(on));
108
109        let rs = RenderState {
110            resolved_opts: Arc::new(ResolvedOptionsRenderState { map: Arc::new(map) }),
111            ..RenderState::default()
112        };
113
114        let a = PaneOptions::for_pane(&rs, gutterless);
115        let b = PaneOptions::for_pane(&rs, ordinary);
116
117        assert!(!a.show_line_numbers, "the buffer that turned `number` off");
118        assert!(!a.cursorline);
119        assert!(
120            b.show_line_numbers,
121            "…and its neighbour keeps its own, whichever pane is focused"
122        );
123        assert!(b.cursorline);
124        assert!(
125            !a.sign_column && b.sign_column,
126            "signcolumn is per-pane too"
127        );
128        assert!(!a.indent_guides_active && b.indent_guides_active);
129        assert_ne!(a, b, "two buffers, two answers — the whole point");
130    }
131}