Skip to main content

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}