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}