Skip to main content

rgx/input/
editor.rs

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