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}