lattice_config/expand_height.rs
1//! Value type for the `command-line.expand-height` typed option
2//! (rich-minibuffer MB.2e).
3//!
4//! When the `:` command line is expanded into its full-modal
5//! mini-buffer band (`<C-x><C-e>`), this option decides how tall
6//! the band grows. It is pure display policy read by the renderers
7//! (TUI + GPUI) — like [`crate::SignColumn`] — so the value type
8//! lives in `lattice-config` and impls [`OptionType`] locally.
9//!
10//! Default is [`ExpandHeight::Half`] (the band claims half the
11//! frame, the MB.2b/c behaviour). `full` grows it as tall as the
12//! frame allows (leaving one pane row); a bare integer pins it to a
13//! fixed row count. The renderer resolves the policy against the
14//! *current* frame height via [`ExpandHeight::rows`] — the host
15//! publishes the policy, the renderer applies it, because only the
16//! renderer knows the live frame height.
17
18use crate::option_type::{EnumeratedValue, OptionType};
19
20/// `command-line.expand-height` — how tall the expanded `:` band grows.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum ExpandHeight {
23 /// Half the frame height (clamped to a sane minimum). The default,
24 /// matching the MB.2b/c band.
25 #[default]
26 Half,
27 /// As tall as the frame allows, leaving a single pane row above.
28 Full,
29 /// A fixed number of rows, clamped to what the frame can show.
30 Fixed(u16),
31}
32
33impl ExpandHeight {
34 /// Resolve the policy to a concrete band height for a frame of
35 /// `frame_height` rows. Always leaves at least one row for the
36 /// pane area above the band (`max`), and never returns 0. `Half`
37 /// preserves the original MB.2b clamp (≥3 rows where the frame
38 /// permits). Pure + total — no panic even for tiny frames (the
39 /// lower bound is itself clamped to `max`).
40 pub fn rows(&self, frame_height: u16) -> u16 {
41 // Upper bound: everything except one pane row and (implicitly)
42 // the tabline the layout reserves. Never below 1.
43 let max = frame_height.saturating_sub(2).max(1);
44 match self {
45 ExpandHeight::Half => {
46 let lo = 3.min(max);
47 (frame_height / 2).clamp(lo, max)
48 }
49 ExpandHeight::Full => max,
50 ExpandHeight::Fixed(n) => (*n).clamp(1, max),
51 }
52 }
53
54 /// The on-disk / `:set` spelling: `half`, `full`, or the row count
55 /// as a bare integer. The inverse of [`Self::parse_label`]. Owned
56 /// (unlike the other value types' `label`) because `Fixed(n)` is
57 /// formatted.
58 pub fn label(&self) -> String {
59 match self {
60 ExpandHeight::Half => "half".to_string(),
61 ExpandHeight::Full => "full".to_string(),
62 ExpandHeight::Fixed(n) => n.to_string(),
63 }
64 }
65
66 /// One-line description of this value for the `:set` value
67 /// completion marginalia. `Fixed` shares one description whatever
68 /// its row count.
69 pub fn doc(&self) -> &'static str {
70 match self {
71 ExpandHeight::Half => "Half the frame height (default)",
72 ExpandHeight::Full => "As tall as the frame allows (one pane row kept)",
73 ExpandHeight::Fixed(_) => "A fixed number of rows",
74 }
75 }
76
77 /// Parse `half`, `full`, or a `u16` row count (surrounding
78 /// whitespace ignored). `0` parses as `Fixed(0)`; [`Self::rows`]
79 /// clamps it up to one row.
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use lattice_config::ExpandHeight;
85 ///
86 /// assert_eq!(ExpandHeight::parse_label(" 12 "), Ok(ExpandHeight::Fixed(12)));
87 /// // A 40-row frame: `half` is 20 rows, `full` keeps two rows back.
88 /// assert_eq!(ExpandHeight::Half.rows(40), 20);
89 /// assert_eq!(ExpandHeight::Full.rows(40), 38);
90 /// // Fixed heights clamp to what the frame can show.
91 /// assert_eq!(ExpandHeight::Fixed(100).rows(40), 38);
92 /// assert!(ExpandHeight::parse_label("tall").is_err());
93 /// ```
94 pub fn parse_label(s: &str) -> Result<Self, String> {
95 match s.trim() {
96 "half" => Ok(ExpandHeight::Half),
97 "full" => Ok(ExpandHeight::Full),
98 other => other.parse::<u16>().map(ExpandHeight::Fixed).map_err(|_| {
99 format!(
100 "command-line.expand-height: expected `half`, `full`, or a row count, got `{other}`"
101 )
102 }),
103 }
104 }
105}
106
107impl OptionType for ExpandHeight {
108 fn parse(s: &str) -> Result<Self, String> {
109 ExpandHeight::parse_label(s)
110 }
111 fn format(&self) -> String {
112 self.label()
113 }
114 fn type_label() -> &'static str {
115 "expand-height"
116 }
117 fn enumerate() -> Option<Vec<&'static str>> {
118 // The `Fixed(n)` case is free-form, so only the two named
119 // forms enumerate for `<Tab>` completion; a bare number is
120 // still accepted by `parse`.
121 Some(vec!["half", "full"])
122 }
123 fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
124 Some(vec![
125 EnumeratedValue {
126 form: "half",
127 doc: ExpandHeight::Half.doc(),
128 },
129 EnumeratedValue {
130 form: "full",
131 doc: ExpandHeight::Full.doc(),
132 },
133 ])
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn default_is_half() {
143 assert_eq!(ExpandHeight::default(), ExpandHeight::Half);
144 }
145
146 #[test]
147 fn parse_round_trips_named_and_numeric() {
148 assert_eq!(
149 ExpandHeight::parse_label("half").unwrap(),
150 ExpandHeight::Half
151 );
152 assert_eq!(
153 ExpandHeight::parse_label("full").unwrap(),
154 ExpandHeight::Full
155 );
156 assert_eq!(
157 ExpandHeight::parse_label("12").unwrap(),
158 ExpandHeight::Fixed(12)
159 );
160 assert_eq!(ExpandHeight::Fixed(12).format(), "12");
161 assert_eq!(ExpandHeight::Half.format(), "half");
162 }
163
164 #[test]
165 fn parse_rejects_garbage() {
166 assert!(ExpandHeight::parse_label("tall").is_err());
167 assert!(ExpandHeight::parse_label("-3").is_err());
168 }
169
170 #[test]
171 fn half_matches_the_original_mb2_clamp() {
172 // 40-row frame → half is 20, within [3, 38].
173 assert_eq!(ExpandHeight::Half.rows(40), 20);
174 // Tiny frame stays sane (no panic, never 0).
175 assert_eq!(ExpandHeight::Half.rows(4), 2);
176 assert_eq!(ExpandHeight::Half.rows(2), 1);
177 }
178
179 #[test]
180 fn full_leaves_one_pane_row() {
181 assert_eq!(ExpandHeight::Full.rows(40), 38);
182 assert_eq!(ExpandHeight::Full.rows(3), 1);
183 }
184
185 #[test]
186 fn fixed_is_clamped_to_the_frame() {
187 assert_eq!(ExpandHeight::Fixed(10).rows(40), 10);
188 // Asking for more than the frame allows clamps to `max`.
189 assert_eq!(ExpandHeight::Fixed(100).rows(40), 38);
190 // Never 0.
191 assert_eq!(ExpandHeight::Fixed(0).rows(40), 1);
192 }
193}