Skip to main content

kode_core/
editor.rs

1use crate::buffer::Buffer;
2use crate::history::History;
3use crate::selection::{Position, Selection};
4use crate::transaction::{EditStep, Transaction};
5
6/// The editor state machine. Ties together buffer, selection, and history.
7/// This is the primary public API for kode-core.
8#[derive(Debug)]
9pub struct Editor {
10    buffer: Buffer,
11    selection: Selection,
12    history: History,
13    /// Desired column for vertical movement (remembered across up/down).
14    sticky_col: Option<usize>,
15}
16
17impl Editor {
18    /// Create an editor with the given text.
19    pub fn new(text: &str) -> Self {
20        Self {
21            buffer: Buffer::from_text(text),
22            selection: Selection::cursor(Position::zero()),
23            history: History::new(),
24            sticky_col: None,
25        }
26    }
27
28    /// Create an empty editor.
29    pub fn empty() -> Self {
30        Self::new("")
31    }
32
33    // ── Accessors ────────────────────────────────────────────────────────
34
35    pub fn buffer(&self) -> &Buffer {
36        &self.buffer
37    }
38
39    pub fn text(&self) -> String {
40        self.buffer.text()
41    }
42
43    pub fn selection(&self) -> Selection {
44        self.selection
45    }
46
47    pub fn cursor(&self) -> Position {
48        self.selection.head
49    }
50
51    pub fn version(&self) -> u64 {
52        self.buffer.version()
53    }
54
55    pub fn is_dirty(&self) -> bool {
56        self.history.is_dirty()
57    }
58
59    pub fn can_undo(&self) -> bool {
60        self.history.can_undo()
61    }
62
63    pub fn can_redo(&self) -> bool {
64        self.history.can_redo()
65    }
66
67    pub fn mark_clean(&mut self) {
68        self.history.mark_clean();
69    }
70
71    /// Get the selected text, or empty string if cursor only.
72    pub fn selected_text(&self) -> String {
73        if self.selection.is_cursor() {
74            return String::new();
75        }
76        let start = self.buffer.pos_to_char(self.selection.start());
77        let end = self.buffer.pos_to_char(self.selection.end());
78        self.buffer.rope().slice(start..end).to_string()
79    }
80
81    // ── Editing commands ─────────────────────────────────────────────────
82
83    /// Insert text at the cursor. If there's a selection, replace it.
84    pub fn insert(&mut self, text: &str) {
85        let cursor_before = self.cursor();
86        let (offset, step) = if self.selection.is_cursor() {
87            let offset = self.buffer.pos_to_char(self.cursor());
88            let step = EditStep::insert(offset, text);
89            (offset, step)
90        } else {
91            let start = self.buffer.pos_to_char(self.selection.start());
92            let end = self.buffer.pos_to_char(self.selection.end());
93            let deleted: String = self.buffer.rope().slice(start..end).to_string();
94            let step = EditStep::replace(start, deleted, text.to_string());
95            (start, step)
96        };
97
98        self.apply_step(&step);
99
100        let new_offset = offset + text.chars().count();
101        let new_pos = self.buffer.char_to_pos(new_offset);
102        self.selection = Selection::cursor(new_pos);
103        self.sticky_col = None;
104
105        let tx = Transaction::single(step).with_cursors(cursor_before, new_pos);
106        self.history.push(tx);
107    }
108
109    /// Delete the character before the cursor (backspace).
110    pub fn backspace(&mut self) {
111        if !self.selection.is_cursor() {
112            self.delete_selection();
113            return;
114        }
115
116        let offset = self.buffer.pos_to_char(self.cursor());
117        if offset == 0 {
118            return;
119        }
120
121        let cursor_before = self.cursor();
122        let prev_char: String = self.buffer.rope().slice((offset - 1)..offset).to_string();
123        let step = EditStep::delete(offset - 1, prev_char);
124
125        self.apply_step(&step);
126        let new_pos = self.buffer.char_to_pos(offset - 1);
127        self.selection = Selection::cursor(new_pos);
128        self.sticky_col = None;
129
130        let tx = Transaction::single(step).with_cursors(cursor_before, new_pos);
131        self.history.push(tx);
132    }
133
134    /// Delete the character after the cursor (forward delete).
135    pub fn delete_forward(&mut self) {
136        if !self.selection.is_cursor() {
137            self.delete_selection();
138            return;
139        }
140
141        let offset = self.buffer.pos_to_char(self.cursor());
142        if offset >= self.buffer.len_chars() {
143            return;
144        }
145
146        let cursor_before = self.cursor();
147        let next_char: String = self.buffer.rope().slice(offset..(offset + 1)).to_string();
148        let step = EditStep::delete(offset, next_char);
149
150        self.apply_step(&step);
151        // Cursor stays at same offset
152        let new_pos = self.buffer.char_to_pos(offset);
153        self.selection = Selection::cursor(new_pos);
154        self.sticky_col = None;
155
156        let tx = Transaction::single(step).with_cursors(cursor_before, new_pos);
157        self.history.push(tx);
158    }
159
160    /// Delete the current selection. No-op if cursor only.
161    pub fn delete_selection(&mut self) {
162        if self.selection.is_cursor() {
163            return;
164        }
165
166        let cursor_before = self.cursor();
167        let start = self.buffer.pos_to_char(self.selection.start());
168        let end = self.buffer.pos_to_char(self.selection.end());
169        let deleted: String = self.buffer.rope().slice(start..end).to_string();
170        let step = EditStep::delete(start, deleted);
171
172        self.apply_step(&step);
173        let new_pos = self.buffer.char_to_pos(start);
174        self.selection = Selection::cursor(new_pos);
175        self.sticky_col = None;
176
177        let tx = Transaction::single(step).with_cursors(cursor_before, new_pos);
178        self.history.push(tx);
179    }
180
181    /// Insert a newline at the cursor.
182    pub fn insert_newline(&mut self) {
183        self.insert("\n");
184    }
185
186    /// Apply a pre-built transaction atomically. All steps are applied
187    /// and a single history entry is created for undo.
188    ///
189    /// Steps must contain **sequential offsets**: each step's offset is relative
190    /// to the buffer state after all previous steps have been applied.
191    pub fn apply_transaction(&mut self, tx: Transaction) {
192        if tx.steps.is_empty() {
193            return;
194        }
195        let cursor_before = self.cursor();
196        for step in &tx.steps {
197            self.apply_step(step);
198        }
199        // Place cursor at end of last inserted text
200        let cursor_after = tx.steps.last().map(|step| {
201            self.buffer.char_to_pos(step.offset + step.inserted_len())
202        }).unwrap_or(cursor_before);
203        self.selection = Selection::cursor(self.buffer.clamp_pos(cursor_after));
204        self.sticky_col = None;
205
206        let tx = tx.with_cursors(cursor_before, cursor_after);
207        self.history.push(tx);
208    }
209
210    // ── Cursor movement ──────────────────────────────────────────────────
211
212    /// Set the cursor to a position, collapsing any selection.
213    pub fn set_cursor(&mut self, pos: Position) {
214        let clamped = self.buffer.clamp_pos(pos);
215        self.selection = Selection::cursor(clamped);
216        self.sticky_col = None;
217    }
218
219    /// Set a selection range.
220    pub fn set_selection(&mut self, anchor: Position, head: Position) {
221        let anchor = self.buffer.clamp_pos(anchor);
222        let head = self.buffer.clamp_pos(head);
223        self.selection = Selection::new(anchor, head);
224        self.sticky_col = None;
225    }
226
227    /// Extend the selection to a new head position (shift+arrow behavior).
228    pub fn extend_selection(&mut self, head: Position) {
229        let head = self.buffer.clamp_pos(head);
230        self.selection = Selection::new(self.selection.anchor, head);
231    }
232
233    /// Select all text.
234    pub fn select_all(&mut self) {
235        let last_line = self.buffer.len_lines().saturating_sub(1);
236        let last_col = self.buffer.line_len(last_line);
237        self.selection = Selection::new(
238            Position::zero(),
239            Position::new(last_line, last_col),
240        );
241        self.sticky_col = None;
242    }
243
244    /// Move cursor left by one character.
245    pub fn move_left(&mut self) {
246        if !self.selection.is_cursor() {
247            // Collapse to start of selection
248            self.set_cursor(self.selection.start());
249            return;
250        }
251        let offset = self.buffer.pos_to_char(self.cursor());
252        if offset > 0 {
253            self.set_cursor(self.buffer.char_to_pos(offset - 1));
254        }
255    }
256
257    /// Move cursor right by one character.
258    pub fn move_right(&mut self) {
259        if !self.selection.is_cursor() {
260            self.set_cursor(self.selection.end());
261            return;
262        }
263        let offset = self.buffer.pos_to_char(self.cursor());
264        if offset < self.buffer.len_chars() {
265            self.set_cursor(self.buffer.char_to_pos(offset + 1));
266        }
267    }
268
269    /// Move cursor up by one line.
270    pub fn move_up(&mut self) {
271        let pos = self.cursor();
272        if pos.line == 0 {
273            self.set_cursor(Position::new(0, 0));
274            return;
275        }
276        let target_col = self.sticky_col.unwrap_or(pos.col);
277        let new_line = pos.line - 1;
278        let max_col = self.buffer.line_len(new_line);
279        let new_pos = Position::new(new_line, target_col.min(max_col));
280        self.selection = Selection::cursor(new_pos);
281        self.sticky_col = Some(target_col);
282    }
283
284    /// Move cursor down by one line.
285    pub fn move_down(&mut self) {
286        let pos = self.cursor();
287        let last_line = self.buffer.len_lines().saturating_sub(1);
288        if pos.line >= last_line {
289            let max_col = self.buffer.line_len(last_line);
290            self.set_cursor(Position::new(last_line, max_col));
291            return;
292        }
293        let target_col = self.sticky_col.unwrap_or(pos.col);
294        let new_line = pos.line + 1;
295        let max_col = self.buffer.line_len(new_line);
296        let new_pos = Position::new(new_line, target_col.min(max_col));
297        self.selection = Selection::cursor(new_pos);
298        self.sticky_col = Some(target_col);
299    }
300
301    /// Move cursor to start of current line.
302    pub fn move_to_line_start(&mut self) {
303        self.set_cursor(Position::new(self.cursor().line, 0));
304    }
305
306    /// Move cursor to end of current line.
307    pub fn move_to_line_end(&mut self) {
308        let line = self.cursor().line;
309        let col = self.buffer.line_len(line);
310        self.set_cursor(Position::new(line, col));
311    }
312
313    /// Move cursor to start of document.
314    pub fn move_to_start(&mut self) {
315        self.set_cursor(Position::zero());
316    }
317
318    /// Move cursor to end of document.
319    pub fn move_to_end(&mut self) {
320        let last_line = self.buffer.len_lines().saturating_sub(1);
321        let last_col = self.buffer.line_len(last_line);
322        self.set_cursor(Position::new(last_line, last_col));
323    }
324
325    // ── Word movement ────────────────────────────────────────────────────
326
327    /// Move cursor left to the start of the previous word.
328    pub fn move_word_left(&mut self) {
329        if !self.selection.is_cursor() {
330            self.set_cursor(self.selection.start());
331            return;
332        }
333        let pos = self.find_word_boundary_left();
334        self.set_cursor(pos);
335    }
336
337    /// Move cursor right to the end of the next word.
338    pub fn move_word_right(&mut self) {
339        if !self.selection.is_cursor() {
340            self.set_cursor(self.selection.end());
341            return;
342        }
343        let pos = self.find_word_boundary_right();
344        self.set_cursor(pos);
345    }
346
347    // ── Selection extension ──────────────────────────────────────────────
348
349    /// Extend selection one character left (Shift+Left).
350    pub fn extend_selection_left(&mut self) {
351        let offset = self.buffer.pos_to_char(self.selection.head);
352        if offset > 0 {
353            let new_head = self.buffer.char_to_pos(offset - 1);
354            self.selection = Selection::new(self.selection.anchor, new_head);
355            self.sticky_col = None;
356        }
357    }
358
359    /// Extend selection one character right (Shift+Right).
360    pub fn extend_selection_right(&mut self) {
361        let offset = self.buffer.pos_to_char(self.selection.head);
362        if offset < self.buffer.len_chars() {
363            let new_head = self.buffer.char_to_pos(offset + 1);
364            self.selection = Selection::new(self.selection.anchor, new_head);
365            self.sticky_col = None;
366        }
367    }
368
369    /// Extend selection up one line (Shift+Up).
370    pub fn extend_selection_up(&mut self) {
371        let head = self.selection.head;
372        if head.line == 0 {
373            self.extend_selection(Position::new(0, 0));
374            return;
375        }
376        let target_col = self.sticky_col.unwrap_or(head.col);
377        let new_line = head.line - 1;
378        let max_col = self.buffer.line_len(new_line);
379        let new_head = Position::new(new_line, target_col.min(max_col));
380        self.selection = Selection::new(self.selection.anchor, new_head);
381        self.sticky_col = Some(target_col);
382    }
383
384    /// Extend selection down one line (Shift+Down).
385    pub fn extend_selection_down(&mut self) {
386        let head = self.selection.head;
387        let last_line = self.buffer.len_lines().saturating_sub(1);
388        if head.line >= last_line {
389            let max_col = self.buffer.line_len(last_line);
390            self.extend_selection(Position::new(last_line, max_col));
391            return;
392        }
393        let target_col = self.sticky_col.unwrap_or(head.col);
394        let new_line = head.line + 1;
395        let max_col = self.buffer.line_len(new_line);
396        let new_head = Position::new(new_line, target_col.min(max_col));
397        self.selection = Selection::new(self.selection.anchor, new_head);
398        self.sticky_col = Some(target_col);
399    }
400
401    /// Extend selection to word boundary left (Shift+Ctrl+Left).
402    pub fn extend_selection_word_left(&mut self) {
403        let pos = self.find_word_boundary_left();
404        self.selection = Selection::new(self.selection.anchor, pos);
405        self.sticky_col = None;
406    }
407
408    /// Extend selection to word boundary right (Shift+Ctrl+Right).
409    pub fn extend_selection_word_right(&mut self) {
410        let pos = self.find_word_boundary_right();
411        self.selection = Selection::new(self.selection.anchor, pos);
412        self.sticky_col = None;
413    }
414
415    /// Extend selection to line start (Shift+Home).
416    pub fn extend_selection_to_line_start(&mut self) {
417        let head = self.selection.head;
418        self.selection = Selection::new(self.selection.anchor, Position::new(head.line, 0));
419        self.sticky_col = None;
420    }
421
422    /// Extend selection to line end (Shift+End).
423    pub fn extend_selection_to_line_end(&mut self) {
424        let head = self.selection.head;
425        let col = self.buffer.line_len(head.line);
426        self.selection = Selection::new(self.selection.anchor, Position::new(head.line, col));
427        self.sticky_col = None;
428    }
429
430    /// Extend selection to document start (Ctrl+Shift+Home).
431    pub fn extend_selection_to_start(&mut self) {
432        self.selection = Selection::new(self.selection.anchor, Position::zero());
433        self.sticky_col = None;
434    }
435
436    /// Extend selection to document end (Ctrl+Shift+End).
437    pub fn extend_selection_to_end(&mut self) {
438        let last_line = self.buffer.len_lines().saturating_sub(1);
439        let last_col = self.buffer.line_len(last_line);
440        self.selection = Selection::new(self.selection.anchor, Position::new(last_line, last_col));
441        self.sticky_col = None;
442    }
443
444    /// Move cursor up by N lines (PageUp).
445    pub fn page_up(&mut self, page_lines: usize) {
446        let pos = self.cursor();
447        let target_col = self.sticky_col.unwrap_or(pos.col);
448        let new_line = pos.line.saturating_sub(page_lines);
449        let max_col = self.buffer.line_len(new_line);
450        let new_pos = Position::new(new_line, target_col.min(max_col));
451        self.selection = Selection::cursor(new_pos);
452        self.sticky_col = Some(target_col);
453    }
454
455    /// Move cursor down by N lines (PageDown).
456    pub fn page_down(&mut self, page_lines: usize) {
457        let pos = self.cursor();
458        let last_line = self.buffer.len_lines().saturating_sub(1);
459        let target_col = self.sticky_col.unwrap_or(pos.col);
460        let new_line = (pos.line + page_lines).min(last_line);
461        let max_col = self.buffer.line_len(new_line);
462        let new_pos = Position::new(new_line, target_col.min(max_col));
463        self.selection = Selection::cursor(new_pos);
464        self.sticky_col = Some(target_col);
465    }
466
467    // ── Smart selection ──────────────────────────────────────────────────
468
469    /// Select the word at the cursor (double-click behavior).
470    pub fn select_word(&mut self) {
471        let offset = self.buffer.pos_to_char(self.cursor());
472        let text = self.buffer.text();
473        let chars: Vec<char> = text.chars().collect();
474
475        if chars.is_empty() {
476            return;
477        }
478
479        let idx = offset.min(chars.len().saturating_sub(1));
480        let is_word = |c: char| c.is_alphanumeric() || c == '_';
481
482        // If on whitespace or punctuation, select just that char
483        if !is_word(chars[idx]) {
484            let start = self.buffer.char_to_pos(idx);
485            let end = self.buffer.char_to_pos(idx + 1);
486            self.selection = Selection::new(start, end);
487            self.sticky_col = None;
488            return;
489        }
490
491        // Find word boundaries
492        let mut start = idx;
493        while start > 0 && is_word(chars[start - 1]) {
494            start -= 1;
495        }
496        let mut end = idx;
497        while end < chars.len() && is_word(chars[end]) {
498            end += 1;
499        }
500
501        self.selection = Selection::new(
502            self.buffer.char_to_pos(start),
503            self.buffer.char_to_pos(end),
504        );
505        self.sticky_col = None;
506    }
507
508    /// Select the entire current line (triple-click behavior).
509    pub fn select_line(&mut self) {
510        let line = self.cursor().line;
511        let line_start = Position::new(line, 0);
512        let last_line = self.buffer.len_lines().saturating_sub(1);
513        let line_end = if line < last_line {
514            // Include the newline — select to start of next line
515            Position::new(line + 1, 0)
516        } else {
517            Position::new(line, self.buffer.line_len(line))
518        };
519        self.selection = Selection::new(line_start, line_end);
520        self.sticky_col = None;
521    }
522
523    // ── Word-level deletion ──────────────────────────────────────────────
524
525    /// Delete from cursor to start of previous word (Ctrl+Backspace).
526    pub fn delete_word_back(&mut self) {
527        if !self.selection.is_cursor() {
528            self.delete_selection();
529            return;
530        }
531        let word_start = self.find_word_boundary_left();
532        let cursor = self.cursor();
533        if word_start != cursor {
534            self.set_selection(word_start, cursor);
535            self.delete_selection();
536        }
537    }
538
539    /// Delete from cursor to end of next word (Ctrl+Delete).
540    pub fn delete_word_forward(&mut self) {
541        if !self.selection.is_cursor() {
542            self.delete_selection();
543            return;
544        }
545        let word_end = self.find_word_boundary_right();
546        let cursor = self.cursor();
547        if word_end != cursor {
548            self.set_selection(cursor, word_end);
549            self.delete_selection();
550        }
551    }
552
553    // ── Indentation ──────────────────────────────────────────────────────
554
555    /// Insert a tab (2 spaces) at the cursor, or indent all selected lines.
556    pub fn indent(&mut self) {
557        if self.selection.is_cursor() {
558            self.insert("  ");
559            return;
560        }
561        // Indent all lines in selection
562        let start_line = self.selection.start().line;
563        let end_line = self.selection.end().line;
564        let mut steps = Vec::new();
565        let mut offset_delta: isize = 0;
566        for line in start_line..=end_line {
567            let line_start = self.buffer.line_to_char(line);
568            let adjusted = (line_start as isize + offset_delta) as usize;
569            steps.push(EditStep::insert(adjusted, "  "));
570            offset_delta += 2;
571        }
572        if !steps.is_empty() {
573            self.apply_transaction(Transaction::new(steps));
574        }
575    }
576
577    /// Remove one level of indentation (2 spaces) from the current or selected lines.
578    pub fn outdent(&mut self) {
579        let start_line = self.selection.start().line;
580        let end_line = self.selection.end().line;
581        let mut steps = Vec::new();
582        let mut offset_delta: isize = 0;
583        for line in start_line..=end_line {
584            let line_text = self.buffer.line(line).to_string();
585            let spaces = line_text.chars().take(2).take_while(|&c| c == ' ').count();
586            if spaces > 0 {
587                let line_start = self.buffer.line_to_char(line);
588                let adjusted = (line_start as isize + offset_delta) as usize;
589                let removed: String = line_text.chars().take(spaces).collect();
590                steps.push(EditStep::delete(adjusted, removed));
591                offset_delta -= spaces as isize;
592            }
593        }
594        if !steps.is_empty() {
595            self.apply_transaction(Transaction::new(steps));
596        }
597    }
598
599    // ── Line operations ──────────────────────────────────────────────────
600
601    /// Duplicate the current line or all selected lines (Ctrl+D).
602    pub fn duplicate_lines(&mut self) {
603        let start_line = self.selection.start().line;
604        let end_line = self.selection.end().line;
605
606        // Collect the text of all lines to duplicate
607        let mut lines_text = String::new();
608        for line in start_line..=end_line {
609            let line_content = self.buffer.line(line).to_string();
610            lines_text.push_str(&line_content);
611        }
612        // Ensure it ends with a newline
613        if !lines_text.ends_with('\n') {
614            lines_text.push('\n');
615        }
616
617        // Insert the duplicate after the last selected line
618        let insert_after = if end_line < self.buffer.len_lines().saturating_sub(1) {
619            self.buffer.line_to_char(end_line + 1)
620        } else {
621            // At end of doc — insert newline first
622            self.buffer.len_chars()
623        };
624
625        let cursor_before = self.cursor();
626        let step = EditStep::insert(insert_after, &lines_text);
627        let tx = Transaction::single(step);
628        self.apply_transaction(tx);
629
630        // Move cursor to the duplicated region
631        let _new_line = end_line + 1 + (self.cursor().line.saturating_sub(end_line).saturating_sub(1));
632        let cursor_after = Position::new(cursor_before.line + (end_line - start_line + 1), cursor_before.col);
633        self.set_cursor(self.buffer.clamp_pos(cursor_after));
634    }
635
636    /// Get the position at the start of the word before/at the cursor.
637    /// Used by the autocomplete system to determine the prefix to replace.
638    pub fn word_start_before_cursor(&self) -> Position {
639        self.find_word_boundary_left()
640    }
641
642    // ── Private helpers ──────────────────────────────────────────────────
643
644    /// Find the position of the word boundary to the left of the cursor head.
645    fn find_word_boundary_left(&self) -> Position {
646        let offset = self.buffer.pos_to_char(self.selection.head);
647        if offset == 0 {
648            return Position::zero();
649        }
650        let text = self.buffer.text();
651        let chars: Vec<char> = text.chars().collect();
652        let mut pos = offset;
653
654        // Skip whitespace
655        while pos > 0 && chars[pos - 1].is_whitespace() {
656            pos -= 1;
657        }
658        // Skip word chars
659        let is_word = |c: char| c.is_alphanumeric() || c == '_';
660        if pos > 0 && is_word(chars[pos - 1]) {
661            while pos > 0 && is_word(chars[pos - 1]) {
662                pos -= 1;
663            }
664        } else if pos > 0 {
665            // Skip punctuation
666            while pos > 0 && !chars[pos - 1].is_whitespace() && !is_word(chars[pos - 1]) {
667                pos -= 1;
668            }
669        }
670
671        self.buffer.char_to_pos(pos)
672    }
673
674    /// Find the position of the word boundary to the right of the cursor head.
675    fn find_word_boundary_right(&self) -> Position {
676        let offset = self.buffer.pos_to_char(self.selection.head);
677        let total = self.buffer.len_chars();
678        if offset >= total {
679            return self.buffer.char_to_pos(total);
680        }
681        let text = self.buffer.text();
682        let chars: Vec<char> = text.chars().collect();
683        let mut pos = offset;
684
685        let is_word = |c: char| c.is_alphanumeric() || c == '_';
686        // Skip current word chars
687        if pos < chars.len() && is_word(chars[pos]) {
688            while pos < chars.len() && is_word(chars[pos]) {
689                pos += 1;
690            }
691        } else if pos < chars.len() && !chars[pos].is_whitespace() {
692            // Skip punctuation
693            while pos < chars.len() && !chars[pos].is_whitespace() && !is_word(chars[pos]) {
694                pos += 1;
695            }
696        }
697        // Skip whitespace
698        while pos < chars.len() && chars[pos].is_whitespace() {
699            pos += 1;
700        }
701
702        self.buffer.char_to_pos(pos)
703    }
704
705    // ── Undo/Redo ────────────────────────────────────────────────────────
706
707    /// Undo the last transaction.
708    pub fn undo(&mut self) {
709        if let Some(inverse) = self.history.undo() {
710            for step in &inverse.steps {
711                self.apply_step(step);
712            }
713            // Use stored cursor position if available, else compute from steps
714            let pos = inverse
715                .cursor_after
716                .unwrap_or_else(|| {
717                    inverse.steps.last().map(|step| {
718                        self.buffer.char_to_pos(step.offset + step.inserted_len())
719                    }).unwrap_or(Position::zero())
720                });
721            self.selection = Selection::cursor(self.buffer.clamp_pos(pos));
722            self.sticky_col = None;
723        }
724    }
725
726    /// Redo the last undone transaction.
727    pub fn redo(&mut self) {
728        if let Some(tx) = self.history.redo() {
729            for step in &tx.steps {
730                self.apply_step(step);
731            }
732            let pos = tx
733                .cursor_after
734                .unwrap_or_else(|| {
735                    tx.steps.last().map(|step| {
736                        self.buffer.char_to_pos(step.offset + step.inserted_len())
737                    }).unwrap_or(Position::zero())
738                });
739            self.selection = Selection::cursor(self.buffer.clamp_pos(pos));
740            self.sticky_col = None;
741        }
742    }
743
744    // ── Internal ─────────────────────────────────────────────────────────
745
746    /// Apply a single edit step to the buffer.
747    fn apply_step(&mut self, step: &EditStep) {
748        if !step.deleted.is_empty() && !step.inserted.is_empty() {
749            self.buffer.replace(step.offset, step.offset + step.deleted_len(), &step.inserted);
750        } else if !step.inserted.is_empty() {
751            self.buffer.insert(step.offset, &step.inserted);
752        } else if !step.deleted.is_empty() {
753            self.buffer.delete(step.offset, step.offset + step.deleted_len());
754        }
755    }
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    #[test]
763    fn new_editor() {
764        let ed = Editor::new("hello");
765        assert_eq!(ed.text(), "hello");
766        assert_eq!(ed.cursor(), Position::zero());
767        assert!(!ed.is_dirty());
768    }
769
770    #[test]
771    fn insert_at_cursor() {
772        let mut ed = Editor::empty();
773        ed.insert("hello");
774        assert_eq!(ed.text(), "hello");
775        assert_eq!(ed.cursor(), Position::new(0, 5));
776        assert!(ed.is_dirty());
777    }
778
779    #[test]
780    fn insert_multiline() {
781        let mut ed = Editor::empty();
782        ed.insert("line1\nline2");
783        assert_eq!(ed.text(), "line1\nline2");
784        assert_eq!(ed.cursor(), Position::new(1, 5));
785    }
786
787    #[test]
788    fn backspace() {
789        let mut ed = Editor::new("abc");
790        ed.set_cursor(Position::new(0, 3));
791        ed.backspace();
792        assert_eq!(ed.text(), "ab");
793        assert_eq!(ed.cursor(), Position::new(0, 2));
794    }
795
796    #[test]
797    fn backspace_at_start() {
798        let mut ed = Editor::new("abc");
799        ed.set_cursor(Position::new(0, 0));
800        ed.backspace();
801        assert_eq!(ed.text(), "abc"); // no change
802    }
803
804    #[test]
805    fn backspace_joins_lines() {
806        let mut ed = Editor::new("abc\ndef");
807        ed.set_cursor(Position::new(1, 0));
808        ed.backspace();
809        assert_eq!(ed.text(), "abcdef");
810        assert_eq!(ed.cursor(), Position::new(0, 3));
811    }
812
813    #[test]
814    fn delete_forward() {
815        let mut ed = Editor::new("abc");
816        ed.set_cursor(Position::new(0, 0));
817        ed.delete_forward();
818        assert_eq!(ed.text(), "bc");
819        assert_eq!(ed.cursor(), Position::new(0, 0));
820    }
821
822    #[test]
823    fn delete_selection() {
824        let mut ed = Editor::new("hello world");
825        ed.set_selection(Position::new(0, 5), Position::new(0, 11));
826        ed.delete_selection();
827        assert_eq!(ed.text(), "hello");
828        assert_eq!(ed.cursor(), Position::new(0, 5));
829    }
830
831    #[test]
832    fn insert_replaces_selection() {
833        let mut ed = Editor::new("hello world");
834        ed.set_selection(Position::new(0, 6), Position::new(0, 11));
835        ed.insert("rust");
836        assert_eq!(ed.text(), "hello rust");
837        assert_eq!(ed.cursor(), Position::new(0, 10));
838    }
839
840    #[test]
841    fn undo_redo() {
842        let mut ed = Editor::empty();
843        ed.insert("hello world");
844        assert_eq!(ed.text(), "hello world");
845
846        ed.undo();
847        assert_eq!(ed.text(), "");
848
849        ed.redo();
850        assert_eq!(ed.text(), "hello world");
851    }
852
853    #[test]
854    fn undo_coalesced_typing() {
855        let mut ed = Editor::empty();
856        ed.insert("h");
857        ed.insert("e");
858        ed.insert("l");
859        ed.insert("l");
860        ed.insert("o");
861
862        // All coalesced into one undo
863        ed.undo();
864        assert_eq!(ed.text(), "");
865    }
866
867    #[test]
868    fn undo_newline_breaks_coalescing() {
869        let mut ed = Editor::empty();
870        ed.insert("a");
871        ed.insert("b");
872        ed.insert("\n");
873        ed.insert("c");
874
875        // "c" is separate from "\n" which is separate from "ab"
876        ed.undo();
877        assert_eq!(ed.text(), "ab\n");
878        ed.undo();
879        assert_eq!(ed.text(), "ab");
880        ed.undo();
881        assert_eq!(ed.text(), "");
882    }
883
884    #[test]
885    fn cursor_movement() {
886        let mut ed = Editor::new("abc\ndef\nghi");
887
888        ed.move_to_end();
889        assert_eq!(ed.cursor(), Position::new(2, 3));
890
891        ed.move_to_start();
892        assert_eq!(ed.cursor(), Position::new(0, 0));
893
894        ed.move_right();
895        assert_eq!(ed.cursor(), Position::new(0, 1));
896
897        ed.move_to_line_end();
898        assert_eq!(ed.cursor(), Position::new(0, 3));
899
900        ed.move_down();
901        assert_eq!(ed.cursor(), Position::new(1, 3));
902
903        ed.move_to_line_start();
904        assert_eq!(ed.cursor(), Position::new(1, 0));
905    }
906
907    #[test]
908    fn sticky_column_on_vertical_movement() {
909        let mut ed = Editor::new("long line\nhi\nlong line");
910        ed.set_cursor(Position::new(0, 8));
911
912        ed.move_down(); // "hi" only has 2 chars
913        assert_eq!(ed.cursor(), Position::new(1, 2));
914
915        ed.move_down(); // back to long line, should restore col 8
916        assert_eq!(ed.cursor(), Position::new(2, 8));
917    }
918
919    #[test]
920    fn select_all() {
921        let mut ed = Editor::new("abc\ndef");
922        ed.select_all();
923        assert_eq!(ed.selected_text(), "abc\ndef");
924    }
925
926    #[test]
927    fn move_left_collapses_selection() {
928        let mut ed = Editor::new("hello");
929        ed.set_selection(Position::new(0, 1), Position::new(0, 4));
930        ed.move_left();
931        assert!(ed.selection().is_cursor());
932        assert_eq!(ed.cursor(), Position::new(0, 1));
933    }
934
935    #[test]
936    fn move_right_collapses_selection() {
937        let mut ed = Editor::new("hello");
938        ed.set_selection(Position::new(0, 1), Position::new(0, 4));
939        ed.move_right();
940        assert!(ed.selection().is_cursor());
941        assert_eq!(ed.cursor(), Position::new(0, 4));
942    }
943
944    #[test]
945    fn dirty_tracking() {
946        let mut ed = Editor::new("hello");
947        assert!(!ed.is_dirty());
948
949        ed.insert(" world");
950        assert!(ed.is_dirty());
951
952        ed.mark_clean();
953        assert!(!ed.is_dirty());
954
955        ed.insert("!");
956        assert!(ed.is_dirty());
957
958        ed.undo();
959        assert!(!ed.is_dirty());
960    }
961
962    #[test]
963    fn unicode_editing() {
964        let mut ed = Editor::new("café");
965        ed.set_cursor(Position::new(0, 4));
966        ed.backspace();
967        assert_eq!(ed.text(), "caf");
968
969        ed.insert("é");
970        assert_eq!(ed.text(), "café");
971    }
972
973    #[test]
974    fn empty_doc_operations() {
975        let mut ed = Editor::empty();
976        ed.backspace(); // no-op
977        ed.delete_forward(); // no-op
978        ed.move_left(); // no-op
979        ed.move_up(); // no-op
980        assert_eq!(ed.text(), "");
981        assert_eq!(ed.cursor(), Position::zero());
982    }
983
984    #[test]
985    fn extend_selection() {
986        let mut ed = Editor::new("hello world");
987        ed.set_cursor(Position::new(0, 0));
988        ed.extend_selection(Position::new(0, 5));
989        assert_eq!(ed.selected_text(), "hello");
990        assert!(!ed.selection().is_cursor());
991    }
992
993    #[test]
994    fn multiple_undo_redo_cycles() {
995        let mut ed = Editor::empty();
996        ed.insert("a");
997        ed.insert("b");
998        ed.insert("c");
999        // All coalesced
1000        assert_eq!(ed.text(), "abc");
1001
1002        ed.undo();
1003        assert_eq!(ed.text(), "");
1004
1005        ed.redo();
1006        assert_eq!(ed.text(), "abc");
1007
1008        // New edit clears redo
1009        ed.insert("d");
1010        assert!(!ed.can_redo());
1011        assert_eq!(ed.text(), "abcd");
1012    }
1013
1014    #[test]
1015    fn apply_transaction_multi_step() {
1016        let mut ed = Editor::new("Hello\nWorld");
1017        // Steps with sequential offsets (each relative to buffer after previous step)
1018        // Step 1: replace "Hello" (offset 0, 5 chars) with "> Hello" (7 chars)
1019        // Step 2: replace "World" (offset 8 in post-step-1 buffer) with "> World"
1020        let tx = Transaction::new(vec![
1021            EditStep::replace(0, "Hello".to_string(), "> Hello".to_string()),
1022            EditStep::replace(8, "World".to_string(), "> World".to_string()),
1023        ]);
1024        ed.apply_transaction(tx);
1025        assert_eq!(ed.text(), "> Hello\n> World");
1026
1027        // Undo should restore original
1028        ed.undo();
1029        assert_eq!(ed.text(), "Hello\nWorld");
1030
1031        // Redo should re-apply
1032        ed.redo();
1033        assert_eq!(ed.text(), "> Hello\n> World");
1034    }
1035
1036    #[test]
1037    fn apply_transaction_empty() {
1038        let mut ed = Editor::new("hello");
1039        ed.apply_transaction(Transaction::new(vec![]));
1040        assert_eq!(ed.text(), "hello");
1041        // Should not create an undo entry
1042        assert!(!ed.can_undo());
1043    }
1044
1045    #[test]
1046    fn undo_forward_delete_cursor_position() {
1047        let mut ed = Editor::new("hello world");
1048        ed.set_cursor(Position::new(0, 5));
1049        ed.delete_forward(); // delete " "
1050        ed.delete_forward(); // delete "w"
1051        ed.delete_forward(); // delete "o"
1052        assert_eq!(ed.text(), "hellorld");
1053
1054        ed.undo();
1055        // Cursor should be back at col 5 (where forward-deleting began)
1056        assert_eq!(ed.cursor(), Position::new(0, 5));
1057    }
1058
1059    // ── New command tests ────────────────────────────────────────────
1060
1061    #[test]
1062    fn word_movement() {
1063        let mut ed = Editor::new("hello world foo");
1064        ed.set_cursor(Position::new(0, 0));
1065
1066        ed.move_word_right();
1067        assert_eq!(ed.cursor(), Position::new(0, 6)); // after "hello "
1068
1069        ed.move_word_right();
1070        assert_eq!(ed.cursor(), Position::new(0, 12)); // after "world "
1071
1072        ed.move_word_left();
1073        assert_eq!(ed.cursor(), Position::new(0, 6)); // back to "world"
1074    }
1075
1076    #[test]
1077    fn select_word() {
1078        let mut ed = Editor::new("hello world");
1079        ed.set_cursor(Position::new(0, 7)); // inside "world"
1080        ed.select_word();
1081        assert_eq!(ed.selected_text(), "world");
1082    }
1083
1084    #[test]
1085    fn select_line() {
1086        let mut ed = Editor::new("line1\nline2\nline3");
1087        ed.set_cursor(Position::new(1, 2));
1088        ed.select_line();
1089        assert_eq!(ed.selected_text(), "line2\n");
1090    }
1091
1092    #[test]
1093    fn extend_selection_directions() {
1094        let mut ed = Editor::new("abc\ndef");
1095        ed.set_cursor(Position::new(0, 1));
1096
1097        ed.extend_selection_right();
1098        assert_eq!(ed.selected_text(), "b");
1099
1100        ed.extend_selection_right();
1101        assert_eq!(ed.selected_text(), "bc");
1102
1103        ed.extend_selection_left();
1104        assert_eq!(ed.selected_text(), "b");
1105
1106        ed.extend_selection_down();
1107        // From anchor (0,1) to head (1,2) — sticky_col is 2 from prior extends
1108        assert_eq!(ed.selected_text(), "bc\nde");
1109    }
1110
1111    #[test]
1112    fn delete_word_back() {
1113        let mut ed = Editor::new("hello world");
1114        ed.set_cursor(Position::new(0, 11));
1115        ed.delete_word_back();
1116        assert_eq!(ed.text(), "hello ");
1117    }
1118
1119    #[test]
1120    fn delete_word_forward() {
1121        let mut ed = Editor::new("hello world");
1122        ed.set_cursor(Position::new(0, 0));
1123        ed.delete_word_forward();
1124        assert_eq!(ed.text(), "world");
1125    }
1126
1127    #[test]
1128    fn indent_outdent() {
1129        let mut ed = Editor::new("line1\nline2");
1130        ed.set_selection(Position::new(0, 0), Position::new(1, 5));
1131        ed.indent();
1132        assert_eq!(ed.text(), "  line1\n  line2");
1133
1134        // Undo should be atomic
1135        ed.undo();
1136        assert_eq!(ed.text(), "line1\nline2");
1137
1138        // Re-indent then outdent
1139        ed.set_selection(Position::new(0, 0), Position::new(1, 5));
1140        ed.indent();
1141        ed.set_selection(Position::new(0, 0), Position::new(1, 7));
1142        ed.outdent();
1143        assert_eq!(ed.text(), "line1\nline2");
1144    }
1145
1146    #[test]
1147    fn duplicate_lines() {
1148        let mut ed = Editor::new("line1\nline2\nline3");
1149        ed.set_cursor(Position::new(1, 0)); // on line2
1150        ed.duplicate_lines();
1151        assert_eq!(ed.text(), "line1\nline2\nline2\nline3");
1152    }
1153
1154    #[test]
1155    fn extend_selection_word() {
1156        let mut ed = Editor::new("hello world");
1157        ed.set_cursor(Position::new(0, 0));
1158        ed.extend_selection_word_right();
1159        // From start: skips "hello", skips space → lands at 6
1160        assert_eq!(ed.selected_text(), "hello ");
1161    }
1162
1163    #[test]
1164    fn extend_selection_to_line_bounds() {
1165        let mut ed = Editor::new("hello world");
1166        ed.set_cursor(Position::new(0, 5));
1167        ed.extend_selection_to_line_start();
1168        assert_eq!(ed.selected_text(), "hello");
1169
1170        ed.set_cursor(Position::new(0, 5));
1171        ed.extend_selection_to_line_end();
1172        assert_eq!(ed.selected_text(), " world");
1173    }
1174
1175    #[test]
1176    fn word_start_before_cursor_at_end_of_dotted() {
1177        let mut ed = Editor::new("foo.bar");
1178        ed.set_cursor(Position::new(0, 7)); // end of "bar"
1179        let pos = ed.word_start_before_cursor();
1180        assert_eq!(pos, Position::new(0, 4)); // start of "bar"
1181    }
1182
1183    #[test]
1184    fn word_start_before_cursor_at_col_zero() {
1185        let mut ed = Editor::new("hello");
1186        ed.set_cursor(Position::new(0, 0));
1187        let pos = ed.word_start_before_cursor();
1188        assert_eq!(pos, Position::new(0, 0));
1189    }
1190
1191    #[test]
1192    fn word_start_before_cursor_middle_of_word() {
1193        let mut ed = Editor::new("hello");
1194        ed.set_cursor(Position::new(0, 3)); // middle of "hello"
1195        let pos = ed.word_start_before_cursor();
1196        assert_eq!(pos, Position::new(0, 0)); // start of "hello"
1197    }
1198}
1199