Skip to main content

typ_panel_editor/
gutter.rs

1//! The gutter, as an ordered list of components rather than a line-number column.
2//!
3//! Helix's `helix-view/src/gutter.rs` does not draw line numbers; it draws
4//! `GutterType::{LineNumbers, Diagnostics, Diff, Spacer, CodeActionHint}`, each
5//! with a width and a renderer, in configurable order. That shape is taken here
6//! for a specific reason: diagnostics arrive at M3 and git-diff markers at M5,
7//! and both want this column. A hardcoded line-number gutter would land the
8//! feature and lose the design, and the second component is what forces the
9//! rewrite.
10//!
11//! So `Diagnostics` and `Diff` exist today, reserve their cell, and draw
12//! nothing. M3 and M5 fill in a function instead of re-laying-out the editor.
13
14use ratatui::style::Style;
15use ratatui::text::Span;
16use typ_core::ThemeColors;
17
18/// Digits needed to write the largest line number in a buffer of `line_count`
19/// lines. Never zero: an empty buffer still shows line 1.
20fn digits(line_count: usize) -> usize {
21    let mut n = line_count.max(1);
22    let mut digits = 1;
23    while n >= 10 {
24        n /= 10;
25        digits += 1;
26    }
27    digits
28}
29
30/// One column, or group of columns, in the gutter.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum GutterComponent {
33    /// Line numbers, as wide as the buffer's largest.
34    ///
35    /// `relative` numbers every line by its distance from the cursor, which is
36    /// a modal-editing idiom. TYPE is non-modal by default so it ships off —
37    /// but the field exists rather than the variant being split in two, so the
38    /// vim layer flips a bool instead of replacing the component.
39    LineNumbers { relative: bool },
40    /// Blank separation, so digits do not sit flush against the text.
41    Spacer,
42    /// Error and warning markers. **M3.** Reserves its cell and draws nothing.
43    Diagnostics,
44    /// Added/removed/changed markers from git. **M5.** Same.
45    Diff,
46}
47
48impl GutterComponent {
49    /// Cells this component occupies. Constant per frame — the gutter's width
50    /// must not change as the view scrolls, or the text shifts sideways when
51    /// the viewport reaches line 100.
52    pub fn width(&self, line_count: usize) -> usize {
53        match self {
54            GutterComponent::LineNumbers { .. } => digits(line_count),
55            GutterComponent::Spacer | GutterComponent::Diagnostics | GutterComponent::Diff => 1,
56        }
57    }
58
59    fn render_line(
60        &self,
61        line: usize,
62        cursor_line: usize,
63        line_count: usize,
64        theme: &ThemeColors,
65    ) -> Span<'static> {
66        match self {
67            GutterComponent::LineNumbers { relative } => {
68                let number = if *relative && line != cursor_line {
69                    cursor_line.abs_diff(line)
70                } else {
71                    // 1-based, matching every compiler error and every other
72                    // editor. Under relative numbering the cursor's own line
73                    // keeps its absolute number, which is what makes the pair
74                    // useful together.
75                    line + 1
76                };
77                let width = digits(line_count);
78                let style = if line == cursor_line {
79                    Style::default().fg(theme.line_number_current_fg)
80                } else {
81                    Style::default().fg(theme.line_number_fg)
82                };
83                // Right-aligned, so the text edge stays straight as the numbers
84                // grow a digit.
85                Span::styled(format!("{number:>width$}"), style)
86            }
87            // Reserved and empty until M3 and M5. They carry `gutter_fg` now so
88            // that filling them in is writing a glyph, not also deciding what
89            // colour the column was supposed to be.
90            GutterComponent::Diagnostics | GutterComponent::Diff => {
91                Span::styled(" ", Style::default().fg(theme.gutter_fg))
92            }
93            GutterComponent::Spacer => Span::raw(" "),
94        }
95    }
96}
97
98/// The gutter: components in draw order.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Gutter {
101    components: Vec<GutterComponent>,
102}
103
104impl Default for Gutter {
105    fn default() -> Self {
106        Self::new(vec![
107            GutterComponent::LineNumbers { relative: false },
108            GutterComponent::Spacer,
109        ])
110    }
111}
112
113impl Gutter {
114    pub fn new(components: Vec<GutterComponent>) -> Self {
115        Self { components }
116    }
117
118    /// Total cells, summed across components. Zero for an empty gutter, which
119    /// is how the column is turned off without a second code path.
120    pub fn width(&self, line_count: usize) -> usize {
121        self.components.iter().map(|c| c.width(line_count)).sum()
122    }
123
124    /// The spans for one buffer line.
125    pub fn render_line(
126        &self,
127        line: usize,
128        cursor_line: usize,
129        line_count: usize,
130        theme: &ThemeColors,
131    ) -> Vec<Span<'static>> {
132        self.components
133            .iter()
134            .map(|c| c.render_line(line, cursor_line, line_count, theme))
135            .collect()
136    }
137}