Skip to main content

rgx/input/
editor.rs

1use unicode_width::UnicodeWidthStr;
2
3#[derive(Debug, Clone)]
4pub struct Editor {
5    content: String,
6    cursor: usize,
7    scroll_offset: usize,
8    vertical_scroll: usize,
9    undo_stack: Vec<(String, usize)>,
10    redo_stack: Vec<(String, usize)>,
11}
12
13impl Editor {
14    pub fn new() -> Self {
15        Self {
16            content: String::new(),
17            cursor: 0,
18            scroll_offset: 0,
19            vertical_scroll: 0,
20            undo_stack: Vec::new(),
21            redo_stack: Vec::new(),
22        }
23    }
24
25    pub fn with_content(content: String) -> Self {
26        let cursor = content.len();
27        Self {
28            content,
29            cursor,
30            scroll_offset: 0,
31            vertical_scroll: 0,
32            undo_stack: Vec::new(),
33            redo_stack: Vec::new(),
34        }
35    }
36
37    pub fn content(&self) -> &str {
38        &self.content
39    }
40
41    pub fn cursor(&self) -> usize {
42        self.cursor
43    }
44
45    pub fn scroll_offset(&self) -> usize {
46        self.scroll_offset
47    }
48
49    pub fn vertical_scroll(&self) -> usize {
50        self.vertical_scroll
51    }
52
53    /// Returns (line, col) of the cursor where col is the display width within the line.
54    pub fn cursor_line_col(&self) -> (usize, usize) {
55        let before = &self.content[..self.cursor];
56        let line = before.matches('\n').count();
57        let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
58        let col = UnicodeWidthStr::width(&self.content[line_start..self.cursor]);
59        (line, col)
60    }
61
62    pub fn line_count(&self) -> usize {
63        self.content.matches('\n').count() + 1
64    }
65
66    /// Byte offset of the start of line `n` (0-indexed).
67    fn line_start(&self, n: usize) -> usize {
68        if n == 0 {
69            return 0;
70        }
71        let mut count = 0;
72        for (i, c) in self.content.char_indices() {
73            if c == '\n' {
74                count += 1;
75                if count == n {
76                    return i + 1;
77                }
78            }
79        }
80        self.content.len()
81    }
82
83    /// Byte offset of the end of line `n` (before the newline, or end of string).
84    fn line_end(&self, n: usize) -> usize {
85        let start = self.line_start(n);
86        match self.content[start..].find('\n') {
87            Some(pos) => start + pos,
88            None => self.content.len(),
89        }
90    }
91
92    /// Content of line `n`.
93    fn line_content(&self, n: usize) -> &str {
94        &self.content[self.line_start(n)..self.line_end(n)]
95    }
96
97    /// Visual cursor column within the current line.
98    pub fn visual_cursor(&self) -> usize {
99        let (_, col) = self.cursor_line_col();
100        col.saturating_sub(self.scroll_offset)
101    }
102
103    fn push_undo_snapshot(&mut self) {
104        self.undo_stack.push((self.content.clone(), self.cursor));
105        if self.undo_stack.len() > 500 {
106            self.undo_stack.remove(0);
107        }
108        self.redo_stack.clear();
109    }
110
111    pub fn undo(&mut self) -> bool {
112        if let Some((content, cursor)) = self.undo_stack.pop() {
113            self.redo_stack.push((self.content.clone(), self.cursor));
114            self.content = content;
115            self.cursor = cursor;
116            true
117        } else {
118            false
119        }
120    }
121
122    pub fn redo(&mut self) -> bool {
123        if let Some((content, cursor)) = self.redo_stack.pop() {
124            self.undo_stack.push((self.content.clone(), self.cursor));
125            self.content = content;
126            self.cursor = cursor;
127            true
128        } else {
129            false
130        }
131    }
132
133    pub fn insert_char(&mut self, c: char) {
134        self.push_undo_snapshot();
135        self.content.insert(self.cursor, c);
136        self.cursor += c.len_utf8();
137    }
138
139    pub fn insert_newline(&mut self) {
140        self.push_undo_snapshot();
141        self.content.insert(self.cursor, '\n');
142        self.cursor += 1;
143    }
144
145    pub fn delete_back(&mut self) {
146        if self.cursor > 0 {
147            self.push_undo_snapshot();
148            let prev = self.prev_char_boundary();
149            self.content.drain(prev..self.cursor);
150            self.cursor = prev;
151        }
152    }
153
154    pub fn delete_forward(&mut self) {
155        if self.cursor < self.content.len() {
156            self.push_undo_snapshot();
157            let next = self.next_char_boundary();
158            self.content.drain(self.cursor..next);
159        }
160    }
161
162    pub fn move_left(&mut self) {
163        if self.cursor > 0 {
164            self.cursor = self.prev_char_boundary();
165        }
166    }
167
168    pub fn move_right(&mut self) {
169        if self.cursor < self.content.len() {
170            self.cursor = self.next_char_boundary();
171        }
172    }
173
174    pub fn move_up(&mut self) {
175        let (line, col) = self.cursor_line_col();
176        if line > 0 {
177            let target_line = line - 1;
178            let target_start = self.line_start(target_line);
179            let target_content = self.line_content(target_line);
180            self.cursor = target_start + byte_offset_at_width(target_content, col);
181        }
182    }
183
184    pub fn move_down(&mut self) {
185        let (line, col) = self.cursor_line_col();
186        if line + 1 < self.line_count() {
187            let target_line = line + 1;
188            let target_start = self.line_start(target_line);
189            let target_content = self.line_content(target_line);
190            self.cursor = target_start + byte_offset_at_width(target_content, col);
191        }
192    }
193
194    /// Move to start of current line.
195    pub fn move_home(&mut self) {
196        let (line, _) = self.cursor_line_col();
197        self.cursor = self.line_start(line);
198        self.scroll_offset = 0;
199    }
200
201    /// Move to end of current line.
202    pub fn move_end(&mut self) {
203        let (line, _) = self.cursor_line_col();
204        self.cursor = self.line_end(line);
205    }
206
207    /// Update horizontal scroll for the current line.
208    pub fn update_scroll(&mut self, visible_width: usize) {
209        let (_, col) = self.cursor_line_col();
210        if col < self.scroll_offset {
211            self.scroll_offset = col;
212        } else if col >= self.scroll_offset + visible_width {
213            self.scroll_offset = col - visible_width + 1;
214        }
215    }
216
217    /// Update vertical scroll to keep cursor visible within `visible_height` lines.
218    pub fn update_vertical_scroll(&mut self, visible_height: usize) {
219        let (line, _) = self.cursor_line_col();
220        if line < self.vertical_scroll {
221            self.vertical_scroll = line;
222        } else if line >= self.vertical_scroll + visible_height {
223            self.vertical_scroll = line - visible_height + 1;
224        }
225    }
226
227    /// Set cursor by display column (single-line editors / mouse click).
228    pub fn set_cursor_by_col(&mut self, col: usize) {
229        self.cursor = byte_offset_at_width(&self.content, col);
230    }
231
232    /// Set cursor by (line, col) position (multi-line editors / mouse click).
233    pub fn set_cursor_by_position(&mut self, line: usize, col: usize) {
234        let target_line = line.min(self.line_count().saturating_sub(1));
235        let start = self.line_start(target_line);
236        let line_text = self.line_content(target_line);
237        self.cursor = start + byte_offset_at_width(line_text, col);
238    }
239
240    fn prev_char_boundary(&self) -> usize {
241        let mut pos = self.cursor - 1;
242        while !self.content.is_char_boundary(pos) {
243            pos -= 1;
244        }
245        pos
246    }
247
248    fn next_char_boundary(&self) -> usize {
249        let mut pos = self.cursor + 1;
250        while pos < self.content.len() && !self.content.is_char_boundary(pos) {
251            pos += 1;
252        }
253        pos
254    }
255}
256
257/// Convert a target display column width to a byte offset within a line string.
258fn byte_offset_at_width(line: &str, target_width: usize) -> usize {
259    let mut width = 0;
260    for (i, c) in line.char_indices() {
261        if width >= target_width {
262            return i;
263        }
264        width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
265    }
266    line.len()
267}
268
269impl Default for Editor {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_insert_and_content() {
281        let mut editor = Editor::new();
282        editor.insert_char('h');
283        editor.insert_char('i');
284        assert_eq!(editor.content(), "hi");
285        assert_eq!(editor.cursor(), 2);
286    }
287
288    #[test]
289    fn test_delete_back() {
290        let mut editor = Editor::with_content("hello".to_string());
291        editor.delete_back();
292        assert_eq!(editor.content(), "hell");
293    }
294
295    #[test]
296    fn test_cursor_movement() {
297        let mut editor = Editor::with_content("hello".to_string());
298        editor.move_left();
299        assert_eq!(editor.cursor(), 4);
300        editor.move_home();
301        assert_eq!(editor.cursor(), 0);
302        editor.move_end();
303        assert_eq!(editor.cursor(), 5);
304    }
305
306    #[test]
307    fn test_insert_newline() {
308        let mut editor = Editor::new();
309        editor.insert_char('a');
310        editor.insert_newline();
311        editor.insert_char('b');
312        assert_eq!(editor.content(), "a\nb");
313        assert_eq!(editor.cursor(), 3);
314    }
315
316    #[test]
317    fn test_cursor_line_col() {
318        let editor = Editor::with_content("abc\ndef\nghi".to_string());
319        // cursor is at end: line 2, col 3
320        assert_eq!(editor.cursor_line_col(), (2, 3));
321    }
322
323    #[test]
324    fn test_move_up_down() {
325        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
326        // cursor at end of "ghi" (line 2, col 3)
327        editor.move_up();
328        assert_eq!(editor.cursor_line_col(), (1, 3));
329        assert_eq!(&editor.content()[..editor.cursor()], "abc\ndef");
330        editor.move_up();
331        assert_eq!(editor.cursor_line_col(), (0, 3));
332        assert_eq!(&editor.content()[..editor.cursor()], "abc");
333        // move_up at top does nothing
334        editor.move_up();
335        assert_eq!(editor.cursor_line_col(), (0, 3));
336        // move back down
337        editor.move_down();
338        assert_eq!(editor.cursor_line_col(), (1, 3));
339    }
340
341    #[test]
342    fn test_move_up_clamps_column() {
343        let mut editor = Editor::with_content("abcdef\nab\nxyz".to_string());
344        // cursor at end: line 2, col 3
345        editor.move_up();
346        // line 1 is "ab" (col 2) — should clamp to end of line
347        assert_eq!(editor.cursor_line_col(), (1, 2));
348        editor.move_up();
349        // line 0 is "abcdef" — col 2
350        assert_eq!(editor.cursor_line_col(), (0, 2));
351    }
352
353    #[test]
354    fn test_line_helpers() {
355        let editor = Editor::with_content("abc\ndef\nghi".to_string());
356        assert_eq!(editor.line_count(), 3);
357        assert_eq!(editor.line_content(0), "abc");
358        assert_eq!(editor.line_content(1), "def");
359        assert_eq!(editor.line_content(2), "ghi");
360    }
361
362    #[test]
363    fn test_home_end_multiline() {
364        let mut editor = Editor::with_content("abc\ndef".to_string());
365        // cursor at end of "def" (line 1)
366        editor.move_home();
367        // should go to start of line 1
368        assert_eq!(editor.cursor(), 4); // "abc\n" = 4 bytes
369        assert_eq!(editor.cursor_line_col(), (1, 0));
370        editor.move_end();
371        assert_eq!(editor.cursor(), 7); // "abc\ndef" = 7 bytes
372        assert_eq!(editor.cursor_line_col(), (1, 3));
373    }
374
375    #[test]
376    fn test_vertical_scroll() {
377        let mut editor = Editor::with_content("a\nb\nc\nd\ne".to_string());
378        editor.update_vertical_scroll(3);
379        // cursor at line 4, visible_height 3 => scroll to 2
380        assert_eq!(editor.vertical_scroll(), 2);
381    }
382
383    #[test]
384    fn test_undo_insert() {
385        let mut editor = Editor::new();
386        editor.insert_char('a');
387        editor.insert_char('b');
388        assert_eq!(editor.content(), "ab");
389        editor.undo();
390        assert_eq!(editor.content(), "a");
391        editor.undo();
392        assert_eq!(editor.content(), "");
393        // Undo on empty stack returns false
394        assert!(!editor.undo());
395    }
396
397    #[test]
398    fn test_undo_delete() {
399        let mut editor = Editor::with_content("abc".to_string());
400        editor.delete_back();
401        assert_eq!(editor.content(), "ab");
402        editor.undo();
403        assert_eq!(editor.content(), "abc");
404    }
405
406    #[test]
407    fn test_redo() {
408        let mut editor = Editor::new();
409        editor.insert_char('a');
410        editor.insert_char('b');
411        editor.undo();
412        assert_eq!(editor.content(), "a");
413        editor.redo();
414        assert_eq!(editor.content(), "ab");
415        // Redo on empty stack returns false
416        assert!(!editor.redo());
417    }
418
419    #[test]
420    fn test_redo_cleared_on_new_edit() {
421        let mut editor = Editor::new();
422        editor.insert_char('a');
423        editor.insert_char('b');
424        editor.undo();
425        // Now type something different — redo stack should clear
426        editor.insert_char('c');
427        assert_eq!(editor.content(), "ac");
428        assert!(!editor.redo());
429    }
430
431    #[test]
432    fn test_set_cursor_by_col() {
433        let mut editor = Editor::with_content("hello".to_string());
434        editor.set_cursor_by_col(3);
435        assert_eq!(editor.cursor(), 3);
436    }
437
438    #[test]
439    fn test_set_cursor_by_position() {
440        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
441        editor.set_cursor_by_position(1, 2);
442        assert_eq!(editor.cursor_line_col(), (1, 2));
443    }
444
445    #[test]
446    fn test_delete_back_across_newline() {
447        let mut editor = Editor::with_content("abc\ndef".to_string());
448        // cursor at start of "def" (byte 4)
449        editor.cursor = 4;
450        editor.delete_back();
451        assert_eq!(editor.content(), "abcdef");
452        assert_eq!(editor.cursor(), 3);
453    }
454}