Skip to main content

sivtr_core/buffer/
mod.rs

1pub mod cursor;
2pub mod line;
3pub mod viewport;
4
5use crate::selection::Selection;
6use cursor::Cursor;
7use line::Line;
8use viewport::Viewport;
9
10/// The main buffer holding all parsed lines and view state.
11pub struct Buffer {
12    pub lines: Vec<Line>,
13    pub viewport: Viewport,
14    pub cursor: Cursor,
15    pub selection: Option<Selection>,
16    preferred_col: usize,
17}
18
19impl Buffer {
20    /// Create a new buffer from parsed lines.
21    pub fn new(lines: Vec<Line>) -> Self {
22        Self {
23            lines,
24            viewport: Viewport::default(),
25            cursor: Cursor::default(),
26            selection: None,
27            preferred_col: 0,
28        }
29    }
30
31    /// Total number of lines in the buffer.
32    pub fn line_count(&self) -> usize {
33        self.lines.len()
34    }
35
36    /// Get a reference to a line by index, if it exists.
37    pub fn get_line(&self, index: usize) -> Option<&Line> {
38        self.lines.get(index)
39    }
40
41    pub fn current_line(&self) -> Option<&Line> {
42        self.get_line(self.cursor.row)
43    }
44
45    /// Get the lines currently visible in the viewport.
46    pub fn visible_lines(&self) -> &[Line] {
47        let start = self.viewport.offset;
48        let end = (start + self.viewport.height).min(self.lines.len());
49        &self.lines[start..end]
50    }
51
52    /// Scroll down by n lines, clamping to bounds.
53    pub fn scroll_down(&mut self, n: usize) {
54        let max_offset = self.lines.len().saturating_sub(self.viewport.height);
55        self.viewport.offset = (self.viewport.offset + n).min(max_offset);
56    }
57
58    /// Scroll up by n lines, clamping to bounds.
59    pub fn scroll_up(&mut self, n: usize) {
60        self.viewport.offset = self.viewport.offset.saturating_sub(n);
61    }
62
63    /// Move cursor down, scrolling viewport if needed.
64    pub fn cursor_down(&mut self, n: usize) {
65        let max_row = self.lines.len().saturating_sub(1);
66        self.cursor.row = (self.cursor.row + n).min(max_row);
67        self.clamp_cursor_col();
68        self.ensure_cursor_visible();
69    }
70
71    /// Move cursor up, scrolling viewport if needed.
72    pub fn cursor_up(&mut self, n: usize) {
73        self.cursor.row = self.cursor.row.saturating_sub(n);
74        self.clamp_cursor_col();
75        self.ensure_cursor_visible();
76    }
77
78    /// Move cursor right within the current line.
79    pub fn cursor_right(&mut self, n: usize) {
80        if let Some(line) = self.lines.get(self.cursor.row) {
81            let max_col = line.display_width().saturating_sub(1);
82            self.cursor.col = (self.cursor.col + n).min(max_col);
83            self.preferred_col = self.cursor.col;
84        }
85    }
86
87    /// Move cursor left within the current line.
88    pub fn cursor_left(&mut self, n: usize) {
89        self.cursor.col = self.cursor.col.saturating_sub(n);
90        self.preferred_col = self.cursor.col;
91    }
92
93    /// Jump cursor to the first line.
94    pub fn cursor_top(&mut self) {
95        self.cursor.row = 0;
96        self.cursor.col = 0;
97        self.preferred_col = 0;
98        self.ensure_cursor_visible();
99    }
100
101    /// Jump cursor to the last line.
102    pub fn cursor_bottom(&mut self) {
103        self.cursor.row = self.lines.len().saturating_sub(1);
104        self.clamp_cursor_col();
105        self.ensure_cursor_visible();
106    }
107
108    /// Half-page down.
109    pub fn half_page_down(&mut self) {
110        let half = self.viewport.height / 2;
111        self.cursor_down(half);
112    }
113
114    /// Half-page up.
115    pub fn half_page_up(&mut self) {
116        let half = self.viewport.height / 2;
117        self.cursor_up(half);
118    }
119
120    pub fn page_down(&mut self) {
121        let height = self.viewport.height.saturating_sub(1).max(1);
122        self.cursor_down(height);
123    }
124
125    pub fn page_up(&mut self) {
126        let height = self.viewport.height.saturating_sub(1).max(1);
127        self.cursor_up(height);
128    }
129
130    pub fn cursor_line_start(&mut self) {
131        self.cursor.col = 0;
132        self.preferred_col = 0;
133    }
134
135    pub fn cursor_line_end(&mut self) {
136        self.cursor.col = self.max_col_for_row(self.cursor.row);
137        self.preferred_col = self.cursor.col;
138    }
139
140    pub fn cursor_first_nonblank(&mut self) {
141        self.cursor.col = self.first_nonblank_col(self.cursor.row);
142        self.preferred_col = self.cursor.col;
143    }
144
145    pub fn cursor_view_top(&mut self) {
146        self.cursor.row = self.viewport.offset.min(self.lines.len().saturating_sub(1));
147        self.clamp_cursor_col();
148    }
149
150    pub fn cursor_view_middle(&mut self) {
151        let visible_height = self.viewport.height.max(1);
152        let row = self.viewport.offset + visible_height / 2;
153        self.cursor.row = row.min(self.lines.len().saturating_sub(1));
154        self.clamp_cursor_col();
155    }
156
157    pub fn cursor_view_bottom(&mut self) {
158        let visible_height = self.viewport.height.max(1);
159        let row = self.viewport.offset + visible_height.saturating_sub(1);
160        self.cursor.row = row.min(self.lines.len().saturating_sub(1));
161        self.clamp_cursor_col();
162    }
163
164    pub fn set_cursor(&mut self, row: usize, col: usize) {
165        self.cursor.row = row.min(self.lines.len().saturating_sub(1));
166        self.cursor.col = self.clamp_col_for_row(self.cursor.row, col);
167        self.preferred_col = col;
168        self.ensure_cursor_visible();
169    }
170
171    /// Ensure the cursor row is within the visible viewport, adjusting offset if needed.
172    fn ensure_cursor_visible(&mut self) {
173        if self.cursor.row < self.viewport.offset {
174            self.viewport.offset = self.cursor.row;
175        } else if self.cursor.row >= self.viewport.offset + self.viewport.height {
176            self.viewport.offset = self.cursor.row - self.viewport.height + 1;
177        }
178    }
179
180    /// Update viewport dimensions (called on terminal resize).
181    pub fn resize(&mut self, width: usize, height: usize) {
182        self.viewport.width = width;
183        self.viewport.height = height;
184    }
185
186    /// Public version of ensure_cursor_visible for external callers.
187    pub fn ensure_cursor_visible_pub(&mut self) {
188        self.ensure_cursor_visible();
189    }
190
191    pub fn preferred_col(&self) -> usize {
192        self.preferred_col
193    }
194
195    fn clamp_cursor_col(&mut self) {
196        self.cursor.col = self.clamp_col_for_row(self.cursor.row, self.preferred_col);
197    }
198
199    fn clamp_col_for_row(&self, row: usize, col: usize) -> usize {
200        col.min(self.max_col_for_row(row))
201    }
202
203    fn max_col_for_row(&self, row: usize) -> usize {
204        self.lines
205            .get(row)
206            .map(|line| line.display_width().saturating_sub(1))
207            .unwrap_or(0)
208    }
209
210    fn first_nonblank_col(&self, row: usize) -> usize {
211        self.lines
212            .get(row)
213            .map(|line| {
214                let char_idx = line
215                    .content
216                    .chars()
217                    .position(|ch| !ch.is_whitespace())
218                    .unwrap_or_else(|| line.char_count());
219                line.display_col_for_char_index(char_idx)
220            })
221            .unwrap_or(0)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    fn make_buffer(lines: &[&str]) -> Buffer {
230        let lines = lines
231            .iter()
232            .map(|content| Line {
233                content: (*content).to_string(),
234                display_widths: crate::parse::unicode::compute_display_widths(content),
235                styles: Vec::new(),
236            })
237            .collect();
238        Buffer::new(lines)
239    }
240
241    #[test]
242    fn vertical_motion_preserves_preferred_col() {
243        let mut buffer = make_buffer(&["abcd", "", "wxyz"]);
244        buffer.set_cursor(0, 3);
245        assert_eq!(buffer.cursor.col, 3);
246        assert_eq!(buffer.preferred_col(), 3);
247
248        buffer.cursor_down(1);
249        assert_eq!(buffer.cursor.row, 1);
250        assert_eq!(buffer.cursor.col, 0);
251        assert_eq!(buffer.preferred_col(), 3);
252
253        buffer.cursor_down(1);
254        assert_eq!(buffer.cursor.row, 2);
255        assert_eq!(buffer.cursor.col, 3);
256        assert_eq!(buffer.preferred_col(), 3);
257    }
258}