Skip to main content

rgx/input/
editor.rs

1use std::collections::VecDeque;
2
3use unicode_width::UnicodeWidthStr;
4
5const MAX_UNDO_STACK: usize = 500;
6
7#[derive(Debug, Clone)]
8pub struct Editor {
9    content: String,
10    cursor: usize,
11    scroll_offset: usize,
12    vertical_scroll: usize,
13    undo_stack: VecDeque<(String, usize)>,
14    redo_stack: VecDeque<(String, usize)>,
15}
16
17impl Editor {
18    pub fn new() -> Self {
19        Self {
20            content: String::new(),
21            cursor: 0,
22            scroll_offset: 0,
23            vertical_scroll: 0,
24            undo_stack: VecDeque::new(),
25            redo_stack: VecDeque::new(),
26        }
27    }
28
29    pub fn with_content(content: String) -> Self {
30        let cursor = content.len();
31        Self {
32            content,
33            cursor,
34            scroll_offset: 0,
35            vertical_scroll: 0,
36            undo_stack: VecDeque::new(),
37            redo_stack: VecDeque::new(),
38        }
39    }
40
41    pub fn content(&self) -> &str {
42        &self.content
43    }
44
45    pub fn cursor(&self) -> usize {
46        self.cursor
47    }
48
49    pub fn scroll_offset(&self) -> usize {
50        self.scroll_offset
51    }
52
53    pub fn vertical_scroll(&self) -> usize {
54        self.vertical_scroll
55    }
56
57    /// Returns (line, col) of the cursor where col is the display width within the line.
58    pub fn cursor_line_col(&self) -> (usize, usize) {
59        let before = &self.content[..self.cursor];
60        let line = before.matches('\n').count();
61        let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
62        let col = UnicodeWidthStr::width(&self.content[line_start..self.cursor]);
63        (line, col)
64    }
65
66    pub fn line_count(&self) -> usize {
67        self.content.matches('\n').count() + 1
68    }
69
70    /// Byte offset of the start of line `n` (0-indexed).
71    fn line_start(&self, n: usize) -> usize {
72        if n == 0 {
73            return 0;
74        }
75        let mut count = 0;
76        for (i, c) in self.content.char_indices() {
77            if c == '\n' {
78                count += 1;
79                if count == n {
80                    return i + 1;
81                }
82            }
83        }
84        self.content.len()
85    }
86
87    /// Byte offset of the end of line `n` (before the newline, or end of string).
88    fn line_end(&self, n: usize) -> usize {
89        let start = self.line_start(n);
90        match self.content[start..].find('\n') {
91            Some(pos) => start + pos,
92            None => self.content.len(),
93        }
94    }
95
96    /// Content of line `n`.
97    fn line_content(&self, n: usize) -> &str {
98        &self.content[self.line_start(n)..self.line_end(n)]
99    }
100
101    /// Visual cursor column within the current line.
102    pub fn visual_cursor(&self) -> usize {
103        let (_, col) = self.cursor_line_col();
104        col.saturating_sub(self.scroll_offset)
105    }
106
107    fn push_undo_snapshot(&mut self) {
108        self.undo_stack
109            .push_back((self.content.clone(), self.cursor));
110        if self.undo_stack.len() > MAX_UNDO_STACK {
111            self.undo_stack.pop_front();
112        }
113        self.redo_stack.clear();
114    }
115
116    pub fn undo(&mut self) -> bool {
117        if let Some((content, cursor)) = self.undo_stack.pop_back() {
118            self.redo_stack
119                .push_back((self.content.clone(), self.cursor));
120            self.content = content;
121            self.cursor = cursor;
122            true
123        } else {
124            false
125        }
126    }
127
128    pub fn redo(&mut self) -> bool {
129        if let Some((content, cursor)) = self.redo_stack.pop_back() {
130            self.undo_stack
131                .push_back((self.content.clone(), self.cursor));
132            self.content = content;
133            self.cursor = cursor;
134            true
135        } else {
136            false
137        }
138    }
139
140    pub fn insert_char(&mut self, c: char) {
141        self.push_undo_snapshot();
142        self.content.insert(self.cursor, c);
143        self.cursor += c.len_utf8();
144    }
145
146    pub fn insert_newline(&mut self) {
147        self.push_undo_snapshot();
148        self.content.insert(self.cursor, '\n');
149        self.cursor += 1;
150    }
151
152    pub fn delete_back(&mut self) {
153        if self.cursor > 0 {
154            self.push_undo_snapshot();
155            let prev = self.prev_char_boundary();
156            self.content.drain(prev..self.cursor);
157            self.cursor = prev;
158        }
159    }
160
161    pub fn delete_forward(&mut self) {
162        if self.cursor < self.content.len() {
163            self.push_undo_snapshot();
164            let next = self.next_char_boundary();
165            self.content.drain(self.cursor..next);
166        }
167    }
168
169    pub fn move_left(&mut self) {
170        if self.cursor > 0 {
171            self.cursor = self.prev_char_boundary();
172        }
173    }
174
175    /// Move cursor left by one char, but not past the start of the current line.
176    /// Used by vim Esc (EnterNormalMode) which should not cross line boundaries.
177    pub fn move_left_in_line(&mut self) {
178        if self.cursor > 0 && self.content.as_bytes()[self.cursor - 1] != b'\n' {
179            self.cursor = self.prev_char_boundary();
180        }
181    }
182
183    pub fn move_right(&mut self) {
184        if self.cursor < self.content.len() {
185            self.cursor = self.next_char_boundary();
186        }
187    }
188
189    /// Move cursor left by one word (to previous word boundary).
190    pub fn move_word_left(&mut self) {
191        if self.cursor == 0 {
192            return;
193        }
194        let before = &self.content[..self.cursor];
195        let mut chars = before.char_indices().rev();
196        // Skip any non-word chars immediately before cursor
197        let mut last_idx = self.cursor;
198        for (i, c) in &mut chars {
199            if c.is_alphanumeric() || c == '_' {
200                last_idx = i;
201                break;
202            }
203            last_idx = i;
204        }
205        // Skip word chars to find the start of the word
206        if last_idx < self.cursor {
207            let before_word = &self.content[..last_idx];
208            for (i, c) in before_word.char_indices().rev() {
209                if !(c.is_alphanumeric() || c == '_') {
210                    self.cursor = i + c.len_utf8();
211                    return;
212                }
213            }
214            // Reached start of string
215            self.cursor = 0;
216        } else {
217            self.cursor = 0;
218        }
219    }
220
221    /// Move cursor right by one word (to next word boundary).
222    pub fn move_word_right(&mut self) {
223        if self.cursor >= self.content.len() {
224            return;
225        }
226        let after = &self.content[self.cursor..];
227        let mut chars = after.char_indices();
228        // Skip any word chars from current position
229        let mut advanced = false;
230        for (i, c) in &mut chars {
231            if !(c.is_alphanumeric() || c == '_') {
232                if advanced {
233                    self.cursor += i;
234                    // Skip non-word chars to reach next word
235                    let remaining = &self.content[self.cursor..];
236                    for (j, c2) in remaining.char_indices() {
237                        if c2.is_alphanumeric() || c2 == '_' {
238                            self.cursor += j;
239                            return;
240                        }
241                    }
242                    self.cursor = self.content.len();
243                    return;
244                }
245                // We started on non-word chars, skip them
246                let remaining = &self.content[self.cursor + i + c.len_utf8()..];
247                for (j, c2) in remaining.char_indices() {
248                    if c2.is_alphanumeric() || c2 == '_' {
249                        self.cursor = self.cursor + i + c.len_utf8() + j;
250                        return;
251                    }
252                }
253                self.cursor = self.content.len();
254                return;
255            }
256            advanced = true;
257        }
258        self.cursor = self.content.len();
259    }
260
261    pub fn move_up(&mut self) {
262        let (line, col) = self.cursor_line_col();
263        if line > 0 {
264            let target_line = line - 1;
265            let target_start = self.line_start(target_line);
266            let target_content = self.line_content(target_line);
267            self.cursor = target_start + byte_offset_at_width(target_content, col);
268        }
269    }
270
271    pub fn move_down(&mut self) {
272        let (line, col) = self.cursor_line_col();
273        if line + 1 < self.line_count() {
274            let target_line = line + 1;
275            let target_start = self.line_start(target_line);
276            let target_content = self.line_content(target_line);
277            self.cursor = target_start + byte_offset_at_width(target_content, col);
278        }
279    }
280
281    /// Move to start of current line.
282    pub fn move_home(&mut self) {
283        let (line, _) = self.cursor_line_col();
284        self.cursor = self.line_start(line);
285        self.scroll_offset = 0;
286    }
287
288    /// Move to end of current line.
289    pub fn move_end(&mut self) {
290        let (line, _) = self.cursor_line_col();
291        self.cursor = self.line_end(line);
292    }
293
294    /// Delete character under cursor (vim `x`). Does nothing at end of content.
295    pub fn delete_char_at_cursor(&mut self) {
296        self.delete_forward();
297    }
298
299    /// Delete the current line (vim `dd`).
300    pub fn delete_line(&mut self) {
301        self.push_undo_snapshot();
302        let (line, _) = self.cursor_line_col();
303        let start = self.line_start(line);
304        let end = self.line_end(line);
305        let line_count = self.line_count();
306
307        if line_count == 1 {
308            self.content.clear();
309            self.cursor = 0;
310        } else if line + 1 < line_count {
311            // Not the last line — delete line including trailing newline
312            self.content.drain(start..end + 1);
313            self.cursor = start;
314        } else {
315            // Last line — delete leading newline + line content
316            self.content.drain(start - 1..end);
317            let prev = line.saturating_sub(1);
318            self.cursor = self.line_start(prev);
319        }
320    }
321
322    /// Clear the current line's content but keep the line (vim `cc`).
323    pub fn clear_line(&mut self) {
324        self.push_undo_snapshot();
325        let (line, _) = self.cursor_line_col();
326        let start = self.line_start(line);
327        let end = self.line_end(line);
328        self.content.drain(start..end);
329        self.cursor = start;
330    }
331
332    /// Insert a string at cursor (single undo snapshot). Used for paste.
333    pub fn insert_str(&mut self, s: &str) {
334        if s.is_empty() {
335            return;
336        }
337        self.push_undo_snapshot();
338        self.content.insert_str(self.cursor, s);
339        self.cursor += s.len();
340    }
341
342    /// Insert a new line below current and move cursor there (vim `o`).
343    pub fn open_line_below(&mut self) {
344        self.push_undo_snapshot();
345        let (line, _) = self.cursor_line_col();
346        let end = self.line_end(line);
347        self.content.insert(end, '\n');
348        self.cursor = end + 1;
349    }
350
351    /// Insert a new line above current and move cursor there (vim `O`).
352    pub fn open_line_above(&mut self) {
353        self.push_undo_snapshot();
354        let (line, _) = self.cursor_line_col();
355        let start = self.line_start(line);
356        self.content.insert(start, '\n');
357        self.cursor = start;
358    }
359
360    /// Move cursor to first non-whitespace character on current line (vim `^`).
361    pub fn move_to_first_non_blank(&mut self) {
362        let (line, _) = self.cursor_line_col();
363        let start = self.line_start(line);
364        let line_text = self.line_content(line);
365        let offset = line_text
366            .char_indices()
367            .find(|(_, c)| !c.is_whitespace())
368            .map(|(i, _)| i)
369            .unwrap_or(0);
370        self.cursor = start + offset;
371    }
372
373    /// Move cursor to start of first line (vim `gg`).
374    pub fn move_to_first_line(&mut self) {
375        self.cursor = 0;
376    }
377
378    /// Move cursor to start of last line (vim `G`).
379    pub fn move_to_last_line(&mut self) {
380        let last = self.line_count().saturating_sub(1);
381        self.cursor = self.line_start(last);
382    }
383
384    /// Move cursor forward to end of current/next word (vim `e`).
385    pub fn move_word_forward_end(&mut self) {
386        if self.cursor >= self.content.len() {
387            return;
388        }
389        let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
390        let after = &self.content[self.cursor..];
391        let mut chars = after.char_indices().peekable();
392
393        // Always advance at least one character
394        if chars.next().is_none() {
395            return;
396        }
397
398        // Skip whitespace
399        while let Some(&(_, c)) = chars.peek() {
400            if !c.is_whitespace() {
401                break;
402            }
403            chars.next();
404        }
405
406        // Find end of the word
407        if let Some(&(first_offset, first)) = chars.peek() {
408            let first_is_word = is_word_char(first);
409            let mut last_offset = first_offset;
410            chars.next();
411
412            for (i, c) in chars {
413                if is_word_char(c) != first_is_word || c.is_whitespace() {
414                    break;
415                }
416                last_offset = i;
417            }
418            self.cursor += last_offset;
419        }
420    }
421
422    /// Update horizontal scroll for the current line.
423    pub fn update_scroll(&mut self, visible_width: usize) {
424        let (_, col) = self.cursor_line_col();
425        if col < self.scroll_offset {
426            self.scroll_offset = col;
427        } else if col >= self.scroll_offset + visible_width {
428            self.scroll_offset = col - visible_width + 1;
429        }
430    }
431
432    /// Update vertical scroll to keep cursor visible within `visible_height` lines.
433    pub fn update_vertical_scroll(&mut self, visible_height: usize) {
434        let (line, _) = self.cursor_line_col();
435        if line < self.vertical_scroll {
436            self.vertical_scroll = line;
437        } else if line >= self.vertical_scroll + visible_height {
438            self.vertical_scroll = line - visible_height + 1;
439        }
440    }
441
442    /// Set cursor by display column (single-line editors / mouse click).
443    pub fn set_cursor_by_col(&mut self, col: usize) {
444        self.cursor = byte_offset_at_width(&self.content, col);
445    }
446
447    /// Set cursor by (line, col) position (multi-line editors / mouse click).
448    pub fn set_cursor_by_position(&mut self, line: usize, col: usize) {
449        let target_line = line.min(self.line_count().saturating_sub(1));
450        let start = self.line_start(target_line);
451        let line_text = self.line_content(target_line);
452        self.cursor = start + byte_offset_at_width(line_text, col);
453    }
454
455    fn prev_char_boundary(&self) -> usize {
456        let mut pos = self.cursor - 1;
457        while !self.content.is_char_boundary(pos) {
458            pos -= 1;
459        }
460        pos
461    }
462
463    fn next_char_boundary(&self) -> usize {
464        let mut pos = self.cursor + 1;
465        while pos < self.content.len() && !self.content.is_char_boundary(pos) {
466            pos += 1;
467        }
468        pos
469    }
470}
471
472/// Convert a target display column width to a byte offset within a line string.
473fn byte_offset_at_width(line: &str, target_width: usize) -> usize {
474    let mut width = 0;
475    for (i, c) in line.char_indices() {
476        if width >= target_width {
477            return i;
478        }
479        width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
480    }
481    line.len()
482}
483
484impl Default for Editor {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_insert_and_content() {
496        let mut editor = Editor::new();
497        editor.insert_char('h');
498        editor.insert_char('i');
499        assert_eq!(editor.content(), "hi");
500        assert_eq!(editor.cursor(), 2);
501    }
502
503    #[test]
504    fn test_delete_back() {
505        let mut editor = Editor::with_content("hello".to_string());
506        editor.delete_back();
507        assert_eq!(editor.content(), "hell");
508    }
509
510    #[test]
511    fn test_cursor_movement() {
512        let mut editor = Editor::with_content("hello".to_string());
513        editor.move_left();
514        assert_eq!(editor.cursor(), 4);
515        editor.move_home();
516        assert_eq!(editor.cursor(), 0);
517        editor.move_end();
518        assert_eq!(editor.cursor(), 5);
519    }
520
521    #[test]
522    fn test_insert_newline() {
523        let mut editor = Editor::new();
524        editor.insert_char('a');
525        editor.insert_newline();
526        editor.insert_char('b');
527        assert_eq!(editor.content(), "a\nb");
528        assert_eq!(editor.cursor(), 3);
529    }
530
531    #[test]
532    fn test_cursor_line_col() {
533        let editor = Editor::with_content("abc\ndef\nghi".to_string());
534        // cursor is at end: line 2, col 3
535        assert_eq!(editor.cursor_line_col(), (2, 3));
536    }
537
538    #[test]
539    fn test_move_up_down() {
540        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
541        // cursor at end of "ghi" (line 2, col 3)
542        editor.move_up();
543        assert_eq!(editor.cursor_line_col(), (1, 3));
544        assert_eq!(&editor.content()[..editor.cursor()], "abc\ndef");
545        editor.move_up();
546        assert_eq!(editor.cursor_line_col(), (0, 3));
547        assert_eq!(&editor.content()[..editor.cursor()], "abc");
548        // move_up at top does nothing
549        editor.move_up();
550        assert_eq!(editor.cursor_line_col(), (0, 3));
551        // move back down
552        editor.move_down();
553        assert_eq!(editor.cursor_line_col(), (1, 3));
554    }
555
556    #[test]
557    fn test_move_up_clamps_column() {
558        let mut editor = Editor::with_content("abcdef\nab\nxyz".to_string());
559        // cursor at end: line 2, col 3
560        editor.move_up();
561        // line 1 is "ab" (col 2) — should clamp to end of line
562        assert_eq!(editor.cursor_line_col(), (1, 2));
563        editor.move_up();
564        // line 0 is "abcdef" — col 2
565        assert_eq!(editor.cursor_line_col(), (0, 2));
566    }
567
568    #[test]
569    fn test_line_helpers() {
570        let editor = Editor::with_content("abc\ndef\nghi".to_string());
571        assert_eq!(editor.line_count(), 3);
572        assert_eq!(editor.line_content(0), "abc");
573        assert_eq!(editor.line_content(1), "def");
574        assert_eq!(editor.line_content(2), "ghi");
575    }
576
577    #[test]
578    fn test_home_end_multiline() {
579        let mut editor = Editor::with_content("abc\ndef".to_string());
580        // cursor at end of "def" (line 1)
581        editor.move_home();
582        // should go to start of line 1
583        assert_eq!(editor.cursor(), 4); // "abc\n" = 4 bytes
584        assert_eq!(editor.cursor_line_col(), (1, 0));
585        editor.move_end();
586        assert_eq!(editor.cursor(), 7); // "abc\ndef" = 7 bytes
587        assert_eq!(editor.cursor_line_col(), (1, 3));
588    }
589
590    #[test]
591    fn test_vertical_scroll() {
592        let mut editor = Editor::with_content("a\nb\nc\nd\ne".to_string());
593        editor.update_vertical_scroll(3);
594        // cursor at line 4, visible_height 3 => scroll to 2
595        assert_eq!(editor.vertical_scroll(), 2);
596    }
597
598    #[test]
599    fn test_undo_insert() {
600        let mut editor = Editor::new();
601        editor.insert_char('a');
602        editor.insert_char('b');
603        assert_eq!(editor.content(), "ab");
604        editor.undo();
605        assert_eq!(editor.content(), "a");
606        editor.undo();
607        assert_eq!(editor.content(), "");
608        // Undo on empty stack returns false
609        assert!(!editor.undo());
610    }
611
612    #[test]
613    fn test_undo_delete() {
614        let mut editor = Editor::with_content("abc".to_string());
615        editor.delete_back();
616        assert_eq!(editor.content(), "ab");
617        editor.undo();
618        assert_eq!(editor.content(), "abc");
619    }
620
621    #[test]
622    fn test_redo() {
623        let mut editor = Editor::new();
624        editor.insert_char('a');
625        editor.insert_char('b');
626        editor.undo();
627        assert_eq!(editor.content(), "a");
628        editor.redo();
629        assert_eq!(editor.content(), "ab");
630        // Redo on empty stack returns false
631        assert!(!editor.redo());
632    }
633
634    #[test]
635    fn test_redo_cleared_on_new_edit() {
636        let mut editor = Editor::new();
637        editor.insert_char('a');
638        editor.insert_char('b');
639        editor.undo();
640        // Now type something different — redo stack should clear
641        editor.insert_char('c');
642        assert_eq!(editor.content(), "ac");
643        assert!(!editor.redo());
644    }
645
646    #[test]
647    fn test_set_cursor_by_col() {
648        let mut editor = Editor::with_content("hello".to_string());
649        editor.set_cursor_by_col(3);
650        assert_eq!(editor.cursor(), 3);
651    }
652
653    #[test]
654    fn test_set_cursor_by_position() {
655        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
656        editor.set_cursor_by_position(1, 2);
657        assert_eq!(editor.cursor_line_col(), (1, 2));
658    }
659
660    #[test]
661    fn test_move_word_right() {
662        let mut editor = Editor::with_content("hello world foo".to_string());
663        editor.cursor = 0;
664        editor.move_word_right();
665        // Should skip past "hello" and stop at start of "world"
666        assert_eq!(editor.cursor(), 6);
667        editor.move_word_right();
668        assert_eq!(editor.cursor(), 12);
669        editor.move_word_right();
670        assert_eq!(editor.cursor(), 15); // end
671    }
672
673    #[test]
674    fn test_move_word_left() {
675        let mut editor = Editor::with_content("hello world foo".to_string());
676        // cursor at end
677        editor.move_word_left();
678        assert_eq!(editor.cursor(), 12); // start of "foo"
679        editor.move_word_left();
680        assert_eq!(editor.cursor(), 6); // start of "world"
681        editor.move_word_left();
682        assert_eq!(editor.cursor(), 0); // start of "hello"
683    }
684
685    #[test]
686    fn test_delete_back_across_newline() {
687        let mut editor = Editor::with_content("abc\ndef".to_string());
688        // cursor at start of "def" (byte 4)
689        editor.cursor = 4;
690        editor.delete_back();
691        assert_eq!(editor.content(), "abcdef");
692        assert_eq!(editor.cursor(), 3);
693    }
694
695    #[test]
696    fn test_delete_char_at_cursor() {
697        let mut editor = Editor::with_content("hello".to_string());
698        editor.cursor = 0;
699        editor.delete_char_at_cursor();
700        assert_eq!(editor.content(), "ello");
701        assert_eq!(editor.cursor(), 0);
702    }
703
704    #[test]
705    fn test_delete_char_at_cursor_end() {
706        let mut editor = Editor::with_content("hello".to_string());
707        editor.delete_char_at_cursor();
708        assert_eq!(editor.content(), "hello");
709    }
710
711    #[test]
712    fn test_delete_line() {
713        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
714        editor.cursor = 5;
715        editor.delete_line();
716        assert_eq!(editor.content(), "abc\nghi");
717        assert_eq!(editor.cursor(), 4);
718    }
719
720    #[test]
721    fn test_delete_line_last() {
722        let mut editor = Editor::with_content("abc\ndef".to_string());
723        editor.cursor = 5;
724        editor.delete_line();
725        assert_eq!(editor.content(), "abc");
726        assert_eq!(editor.cursor(), 0);
727    }
728
729    #[test]
730    fn test_delete_line_single() {
731        let mut editor = Editor::with_content("hello".to_string());
732        editor.cursor = 2;
733        editor.delete_line();
734        assert_eq!(editor.content(), "");
735        assert_eq!(editor.cursor(), 0);
736    }
737
738    #[test]
739    fn test_clear_line() {
740        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
741        editor.cursor = 5;
742        editor.clear_line();
743        assert_eq!(editor.content(), "abc\n\nghi");
744        assert_eq!(editor.cursor(), 4);
745    }
746
747    #[test]
748    fn test_clear_line_single() {
749        let mut editor = Editor::with_content("hello".to_string());
750        editor.cursor = 2;
751        editor.clear_line();
752        assert_eq!(editor.content(), "");
753        assert_eq!(editor.cursor(), 0);
754    }
755
756    #[test]
757    fn test_insert_str() {
758        let mut editor = Editor::with_content("hd".to_string());
759        editor.cursor = 1;
760        editor.insert_str("ello worl");
761        assert_eq!(editor.content(), "hello world");
762        assert_eq!(editor.cursor(), 10);
763    }
764
765    #[test]
766    fn test_insert_str_undo() {
767        let mut editor = Editor::with_content("ad".to_string());
768        editor.cursor = 1;
769        editor.insert_str("bc");
770        assert_eq!(editor.content(), "abcd");
771        editor.undo();
772        assert_eq!(editor.content(), "ad");
773    }
774
775    #[test]
776    fn test_open_line_below() {
777        let mut editor = Editor::with_content("abc\ndef".to_string());
778        editor.cursor = 1;
779        editor.open_line_below();
780        assert_eq!(editor.content(), "abc\n\ndef");
781        assert_eq!(editor.cursor(), 4);
782    }
783
784    #[test]
785    fn test_open_line_above() {
786        let mut editor = Editor::with_content("abc\ndef".to_string());
787        editor.cursor = 5;
788        editor.open_line_above();
789        assert_eq!(editor.content(), "abc\n\ndef");
790        assert_eq!(editor.cursor(), 4);
791    }
792
793    #[test]
794    fn test_move_to_first_non_blank() {
795        let mut editor = Editor::with_content("   hello".to_string());
796        editor.cursor = 7;
797        editor.move_to_first_non_blank();
798        assert_eq!(editor.cursor(), 3);
799    }
800
801    #[test]
802    fn test_move_to_first_line() {
803        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
804        editor.move_to_first_line();
805        assert_eq!(editor.cursor(), 0);
806    }
807
808    #[test]
809    fn test_move_to_last_line() {
810        let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
811        editor.cursor = 0;
812        editor.move_to_last_line();
813        let (line, _) = editor.cursor_line_col();
814        assert_eq!(line, 2);
815    }
816
817    #[test]
818    fn test_move_word_forward_end() {
819        let mut editor = Editor::with_content("hello world foo".to_string());
820        editor.cursor = 0;
821        editor.move_word_forward_end();
822        assert_eq!(editor.cursor(), 4);
823        editor.move_word_forward_end();
824        assert_eq!(editor.cursor(), 10);
825    }
826
827    #[test]
828    fn test_move_left_in_line_normal() {
829        let mut editor = Editor::with_content("hello".to_string());
830        // cursor at end (byte 5)
831        editor.move_left_in_line();
832        assert_eq!(editor.cursor(), 4);
833    }
834
835    #[test]
836    fn test_move_left_in_line_at_line_start() {
837        let mut editor = Editor::with_content("abc\ndef".to_string());
838        // cursor at start of "def" (byte 4, right after '\n')
839        editor.cursor = 4;
840        editor.move_left_in_line();
841        // Should NOT cross the newline
842        assert_eq!(editor.cursor(), 4);
843    }
844
845    #[test]
846    fn test_move_left_in_line_at_content_start() {
847        let mut editor = Editor::with_content("hello".to_string());
848        editor.cursor = 0;
849        editor.move_left_in_line();
850        assert_eq!(editor.cursor(), 0);
851    }
852}