Skip to main content

ratatui_code_editor/
actions.rs

1use crate::editor::{Editor};
2use crate::selection::{Selection};
3use crate::code::{EditKind};
4
5pub trait Action {
6    fn apply(&mut self, editor: &mut Editor);
7}
8
9
10/// Moves the cursor one character to the right.
11///
12/// If `shift` is true, the selection is extended to the new cursor position.
13/// If `shift` is false and there is an active selection, the cursor jumps
14/// to the end of the selection and the selection is cleared.
15/// Otherwise, the cursor moves one position to the right.
16pub struct MoveRight {
17    pub shift: bool,
18}
19
20impl Action for MoveRight {
21    fn apply(&mut self, editor: &mut Editor) {
22        let cursor = editor.get_cursor();
23
24        if !self.shift {
25            if let Some(sel) = editor.get_selection() {
26                if !sel.is_empty() {
27                    let (_, end) = sel.sorted();
28                    editor.set_cursor(end);
29                    editor.clear_selection();
30                    return;
31                }
32            }
33        }
34
35        if cursor < editor.code_mut().len() {
36            let new_cursor = cursor.saturating_add(1);
37            if self.shift {
38                editor.extend_selection(new_cursor);
39            } else {
40                editor.clear_selection();
41            }
42            editor.set_cursor(new_cursor);
43        }
44    }
45}
46
47/// Moves the cursor one character to the left.
48///
49/// If `shift` is true, the selection is extended to the new cursor position.
50/// If `shift` is false and there is an active selection, the cursor jumps
51/// to the start of the selection and the selection is cleared.
52/// Otherwise, the cursor moves one position to the left.
53pub struct MoveLeft {
54    pub shift: bool,
55}
56
57impl Action for MoveLeft {
58    fn apply(&mut self, editor: &mut Editor) {
59        let cursor = editor.get_cursor();
60
61        if !self.shift {
62            if let Some(sel) = editor.get_selection() {
63                if !sel.is_empty() {
64                    let (start, _) = sel.sorted();
65                    editor.set_cursor(start);
66                    editor.clear_selection();
67                    return;
68                }
69            }
70        }
71
72        if cursor > 0 {
73            let new_cursor = cursor.saturating_sub(1);
74            if self.shift {
75                editor.extend_selection(new_cursor);
76            } else {
77                editor.clear_selection();
78            }
79            editor.set_cursor(new_cursor);
80        }
81    }
82}
83
84/// Moves the cursor one line up.
85///
86/// If the previous line is shorter, the cursor is placed at the end of that line.
87/// If `shift` is true, the selection is extended to the new cursor position.
88/// If `shift` is false, the selection is cleared.
89pub struct MoveUp {
90    pub shift: bool,
91}
92
93impl Action for MoveUp {
94    fn apply(&mut self, editor: &mut Editor) {
95        let cursor = editor.get_cursor();
96        let code = editor.code_mut();
97        let (row, col) = code.point(cursor);
98
99        if row == 0 { return }
100
101        let prev_start = code.line_to_char(row - 1);
102        let prev_len = code.line_len(row - 1);
103        let new_col = col.min(prev_len);
104        let new_cursor = prev_start + new_col;
105
106        // Update selection or clear it
107        if self.shift {
108            editor.extend_selection(new_cursor);
109        } else {
110            editor.clear_selection();
111        }
112
113        // Set the new cursor position
114        editor.set_cursor(new_cursor);
115    }
116}
117
118/// Moves the cursor one line down.
119/// 
120/// If the next line is shorter, the cursor is placed at the end of that line.
121/// If `shift` is true, the selection is extended to the new cursor position.
122/// If `shift` is false, the selection is cleared.
123/// 
124pub struct MoveDown {
125    pub shift: bool,
126}
127
128impl Action for MoveDown {
129    fn apply(&mut self, editor: &mut Editor) {
130        let cursor = editor.get_cursor();
131        let code = editor.code_mut();
132        let (row, col) = code.point(cursor);
133        let is_last_line = row + 1 >= code.len_lines();
134        if is_last_line { return }
135
136        let next_start = code.line_to_char(row + 1);
137        let next_len = code.line_len(row + 1);
138        let new_col = col.min(next_len);
139        let new_cursor = next_start + new_col;
140
141        // Update selection or clear it
142        if self.shift {
143            editor.extend_selection(new_cursor);
144        } else {
145            editor.clear_selection();
146        }
147
148        // Set the new cursor position
149        editor.set_cursor(new_cursor);
150    }
151}
152
153/// Inserts arbitrary text at the cursor, replacing the selection if any.
154pub struct InsertText {
155    pub text: String,
156}
157
158impl Action for InsertText {
159    fn apply(&mut self, editor: &mut Editor) {
160        // 1. Extract current cursor and selection
161        let mut cursor = editor.get_cursor();
162        let mut selection = editor.get_selection();
163
164        // 2. Work with code
165        let code = editor.code_mut();
166        code.tx();
167        code.set_state_before(cursor, selection);
168
169        // 3. Remove selection if present
170        if let Some(sel) = &selection {
171            if !sel.is_empty() {
172                let (start, end) = sel.sorted();
173                code.remove(start, end);
174                cursor = start;
175            }
176        }
177        selection = None;
178
179        // 4. Insert the text at the cursor
180        code.insert(cursor, &self.text);
181        cursor += self.text.chars().count();
182
183        // 5. Update editor state
184        code.set_state_after(cursor, selection);
185        code.commit();
186
187        editor.set_cursor(cursor);
188        editor.set_selection(selection);
189        editor.reset_highlight_cache();
190    }
191}
192
193/// Inserts a newline at the cursor with automatic indentation.
194/// 
195/// The indentation is computed based on the current line and column.
196/// Delegates the actual insertion to `InsertText`.
197pub struct InsertNewline;
198
199impl Action for InsertNewline {
200    fn apply(&mut self, editor: &mut Editor) {
201        // 1. Get current cursor position
202        let cursor = editor.get_cursor();
203        let code = editor.code_mut();
204        let (row, col) = code.point(cursor);
205
206        // 2. Compute indentation for the new line
207        let indent_level = code.indentation_level(row, col);
208        let indent_text = code.indent().repeat(indent_level);
209
210        // 3. Prepare the text to insert
211        let text_to_insert = format!("\n{}", indent_text);
212
213        // 4. Use InsertText action to insert the text
214        let mut insert_action = InsertText { text: text_to_insert };
215        insert_action.apply(editor);
216    }
217}
218
219/// Deletes the selected text or the character before the cursor.
220/// 
221/// - If there is a non-empty selection, deletes the selection.
222/// - If there is no selection, deletes the previous character.
223/// - If the cursor is after indentation only, deletes the entire indentation.
224pub struct Delete;
225
226impl Action for Delete {
227    fn apply(&mut self, editor: &mut Editor) {
228        // 1. Extract current cursor and selection
229        let mut cursor = editor.get_cursor();
230        let mut selection = editor.get_selection();
231
232        // 2. Work with code
233        let code = editor.code_mut();
234        code.tx();
235        code.set_state_before(cursor, selection);
236
237        if let Some(sel) = &selection && !sel.is_empty() {
238            // Delete selection
239            let (start, end) = sel.sorted();
240            code.remove(start, end);
241            cursor = start;
242            selection = None;
243        } else if cursor > 0 {
244            // Delete single char or indentation
245            let (row, col) = code.point(cursor);
246            if code.is_only_indentation_before(row, col) {
247                let from = cursor - col;
248                code.remove(from, cursor);
249                cursor = from;
250            } else {
251                code.remove(cursor - 1, cursor);
252                cursor -= 1;
253            }
254        }
255
256        // 3. Commit changes and update editor
257        code.set_state_after(cursor, selection);
258        code.commit();
259
260        editor.set_cursor(cursor);
261        editor.set_selection(selection);
262        editor.reset_highlight_cache();
263    }
264}
265
266pub struct ToggleComment;
267
268impl Action for ToggleComment {
269    /// The `ToggleComment` action toggles line comments at the start of the selected lines.
270    ///
271    /// If all lines in the selection already start with the language's comment string
272    /// (e.g., "//" for Rust), this action removes the comment string from each affected line.
273    /// Otherwise, it prepends the comment string to the beginning of each line in the selection.
274    ///
275    /// If there is no selection, the action is applied to the line under the cursor.
276    fn apply(&mut self, editor: &mut Editor) {
277        // 1. Extract current cursor and selection
278        let mut cursor = editor.get_cursor();
279        let mut selection = editor.get_selection();
280        let selection_anchor = editor.selection_anchor();
281
282        // 2. Work with code
283        let code = editor.code_mut();
284
285        code.tx();
286        code.set_state_before(cursor, selection);
287
288        let comment_text = code.comment();
289        let comment_len = comment_text.chars().count();
290
291        // 3. Determine lines to modify
292        let lines_to_handle = if let Some(sel) = &selection && !sel.is_empty() {
293            let (start, end) = sel.sorted();
294            let (start_row, _) = code.point(start);
295            let (end_row, _) = code.point(end);
296            (start_row..=end_row).collect::<Vec<_>>()
297        } else {
298            let (row, _) = code.point(cursor);
299            vec![row]
300        };
301
302        // 4. Check if all lines already have the comment
303        let all_have_comment = lines_to_handle.iter().all(|&line_idx| {
304            let line_start = code.line_to_char(line_idx);
305            let line_len = code.line_len(line_idx);
306            line_start + comment_len <= line_start + line_len &&
307                code.slice(line_start, line_start + comment_len) == comment_text
308        });
309
310        // 5. Apply changes (add or remove comment)
311        let mut comments_added = 0usize;
312        let mut comments_removed = 0usize;
313
314        for &line_idx in lines_to_handle.iter().rev() {
315            let start = code.line_to_char(line_idx);
316            if all_have_comment {
317                // Remove comment if present at start
318                let slice = code.slice(start, start + comment_len);
319                if slice == comment_text {
320                    code.remove(start, start + comment_len);
321                    comments_removed += 1;
322                }
323            } else {
324                // Add comment at start
325                code.insert(start, &comment_text);
326                comments_added += 1;
327            }
328        }
329
330        // 6. Update cursor and selection
331        if let Some(sel) = &selection && !sel.is_empty() {
332            let (smin, _) = sel.sorted();
333            let mut anchor = selection_anchor;
334            let is_forward = anchor == smin;
335
336            if is_forward {
337                if !all_have_comment {
338                    cursor += comment_len * comments_added;
339                    anchor += comment_len;
340                } else {
341                    cursor = cursor.saturating_sub(comment_len * comments_removed);
342                    anchor = anchor.saturating_sub(comment_len);
343                }
344            } else {
345                if !all_have_comment {
346                    cursor += comment_len;
347                    anchor += comment_len * comments_added;
348                } else {
349                    cursor = cursor.saturating_sub(comment_len);
350                    anchor = anchor.saturating_sub(comment_len * comments_removed);
351                }
352            }
353
354            selection = Some(Selection::from_anchor_and_cursor(anchor, cursor));
355        } else {
356            if !all_have_comment {
357                cursor += comment_len;
358            } else {
359                cursor = cursor.saturating_sub(comment_len);
360            }
361        }
362
363        // 7. Commit changes
364        code.set_state_after(cursor, selection);
365        code.commit();
366
367        // 8. Return changed values to the editor
368        editor.set_cursor(cursor);
369        editor.set_selection(selection);
370        editor.reset_highlight_cache();
371    }
372}
373
374/// Inserts indentation at the beginning of the current line or selected lines.
375/// 
376/// - If there is a selection, inserts indentation at the start of each selected line.
377/// - If there is no selection, inserts indentation at the current line.
378/// - Updates cursor and selection accordingly.
379pub struct Indent;
380
381impl Action for Indent {
382    fn apply(&mut self, editor: &mut Editor) {
383        // 1. Extract current cursor and selection
384        let mut cursor = editor.get_cursor();
385        let mut selection = editor.get_selection();
386        let selection_anchor = editor.selection_anchor();
387
388        // 2. Work with code
389        let code = editor.code_mut();
390        code.tx();
391        code.set_state_before(cursor, selection);
392
393        let indent_text = code.indent();
394
395        // 3. Determine lines to handle
396        let lines_to_handle = if let Some(sel) = &selection && !sel.is_empty() {
397            let (start, end) = sel.sorted();
398            let (start_row, _) = code.point(start);
399            let (end_row, _) = code.point(end);
400            (start_row..=end_row).collect::<Vec<_>>()
401        } else {
402            let (row, _) = code.point(cursor);
403            vec![row]
404        };
405
406        // 4. Insert indentation for each line (reverse to not shift indices)
407        let mut indents_added = 0;
408        for &line_idx in lines_to_handle.iter().rev() {
409            let line_start = code.line_to_char(line_idx);
410            code.insert(line_start, &indent_text);
411            indents_added += 1;
412        }
413
414        // 5. Update cursor and selection
415        if let Some(sel) = &selection && !sel.is_empty() {
416            let (smin, _) = sel.sorted();
417            let mut anchor = selection_anchor;
418            let is_forward = anchor == smin;
419
420            if is_forward {
421                cursor += indent_text.len() * indents_added;
422                anchor += indent_text.len();
423            } else {
424                cursor += indent_text.len();
425                anchor += indent_text.len() * indents_added;
426            }
427
428            selection = Some(Selection::from_anchor_and_cursor(anchor, cursor));
429        } else {
430            cursor += indent_text.len();
431        }
432
433        // 6. Commit changes
434        code.set_state_after(cursor, selection);
435        code.commit();
436
437        editor.set_cursor(cursor);
438        editor.set_selection(selection);
439        editor.reset_highlight_cache();
440    }
441}
442
443
444/// Removes one indentation level from the start of the current line or selected lines.
445/// 
446/// - If there is a selection, removes indentation from each selected line.
447/// - If there is no selection, removes indentation from the current line.
448/// - Updates cursor and selection accordingly.
449pub struct UnIndent;
450
451impl Action for UnIndent {
452    fn apply(&mut self, editor: &mut Editor) {
453        // 1. Extract current cursor and selection
454        let mut cursor = editor.get_cursor();
455        let mut selection = editor.get_selection();
456        let selection_anchor = editor.selection_anchor();
457
458        // 2. Work with code
459        let code = editor.code_mut();
460        code.tx();
461        code.set_state_before(cursor, selection);
462
463        let indent_text = code.indent();
464        let indent_len = indent_text.chars().count();
465
466        // 3. Determine lines to handle
467        let lines_to_handle = if let Some(sel) = &selection && !sel.is_empty() {
468            let (start, end) = sel.sorted();
469            let (start_row, _) = code.point(start);
470            let (end_row, _) = code.point(end);
471            (start_row..=end_row).collect::<Vec<_>>()
472        } else {
473            let (row, _) = code.point(cursor);
474            vec![row]
475        };
476
477        // 4. Remove indentation from each line
478        let mut lines_untabbed = 0;
479        for &line_idx in lines_to_handle.iter().rev() {
480            if let Some(indent_cols) = code.find_indent_at_line_start(line_idx) {
481                let remove_count = indent_cols.min(indent_len);
482                if remove_count > 0 {
483                    let line_start = code.line_to_char(line_idx);
484                    code.remove(line_start, line_start + remove_count);
485                    lines_untabbed += 1;
486                }
487            }
488        }
489
490        // 5. Update cursor and selection
491        if let Some(sel) = &selection && !sel.is_empty() {
492            let (smin, _) = sel.sorted();
493            let mut anchor = selection_anchor;
494            let is_forward = anchor == smin;
495
496            if is_forward {
497                cursor = cursor.saturating_sub(indent_len * lines_untabbed);
498                anchor = anchor.saturating_sub(indent_len);
499            } else {
500                cursor = cursor.saturating_sub(indent_len);
501                anchor = anchor.saturating_sub(indent_len * lines_untabbed);
502            }
503
504            selection = Some(Selection::from_anchor_and_cursor(anchor, cursor));
505        } else {
506            cursor = cursor.saturating_sub(indent_len * lines_untabbed);
507        }
508
509        // 6. Commit changes
510        code.set_state_after(cursor, selection);
511        code.commit();
512
513        editor.set_cursor(cursor);
514        editor.set_selection(selection);
515        editor.reset_highlight_cache();
516    }
517}
518
519/// Selects the entire text in the editor.
520pub struct SelectAll;
521
522impl Action for SelectAll {
523    fn apply(&mut self, editor: &mut Editor) {
524        // Set selection from start to end of the document
525        let from = 0;
526        let code = editor.code_mut();
527        let to = code.len_chars();
528        let sel = Selection::new(from, to);
529        editor.set_selection(Some(sel));
530    }
531}
532
533/// Duplicates the selected text or the current line if no selection exists.
534///
535/// If there is a selection, it duplicates the selected text immediately after it.
536/// If there is no selection, it duplicates the entire line under the cursor,
537/// preserving the cursor's relative column position.
538pub struct Duplicate;
539
540impl Action for Duplicate {
541    fn apply(&mut self, editor: &mut Editor) {
542        // 1. Extract current cursor and selection
543        let mut cursor = editor.get_cursor();
544        let mut selection = editor.get_selection();
545        let code = editor.code_mut();
546
547        code.tx();
548        code.set_state_before(cursor, selection);
549
550        if let Some(sel) = &selection {
551            // Duplicate selected text
552            let text = code.slice(sel.start, sel.end);
553            let insert_pos = sel.end;
554            code.insert(insert_pos, &text);
555            cursor = insert_pos + text.chars().count();
556            selection = None;
557        } else {
558            // Duplicate the current line
559            let (line_start, line_end) = code.line_boundaries(cursor);
560            let line_text = code.slice(line_start, line_end);
561            let column = cursor - line_start;
562
563            let insert_pos = line_end;
564            let to_insert = if line_text.ends_with('\n') {
565                line_text.clone()
566            } else {
567                format!("{}\n", line_text)
568            };
569            code.insert(insert_pos, &to_insert);
570
571            // Keep cursor on the same relative column in the new line
572            let new_line_len = to_insert.trim_end_matches('\n').chars().count();
573            let new_column = column.min(new_line_len);
574            cursor = insert_pos + new_column;
575        }
576
577        code.set_state_after(cursor, selection);
578        code.commit();
579
580        // Update editor state
581        editor.set_cursor(cursor);
582        editor.set_selection(selection);
583        editor.reset_highlight_cache();
584    }
585}
586
587/// Deletes the entire line under the cursor.
588pub struct DeleteLine;
589
590impl Action for DeleteLine {
591    fn apply(&mut self, editor: &mut Editor) {
592        // 1. Extract current cursor and selection
593        let mut cursor = editor.get_cursor();
594        let mut selection = editor.get_selection();
595        let code = editor.code_mut();
596
597        // 2. Compute line boundaries
598        let (start, end) = code.line_boundaries(cursor);
599
600        // Do nothing if the line is empty and at the end of file
601        if start == end && start == code.len() {
602            return;
603        }
604
605        // 3. Remove the line
606        code.tx();
607        code.set_state_before(cursor, selection);
608        code.remove(start, end);
609        code.set_state_after(start, None);
610        code.commit();
611
612        // 4. Update editor state
613        cursor = start;
614        selection = None;
615        editor.set_cursor(cursor);
616        editor.set_selection(selection);
617        editor.reset_highlight_cache();
618    }
619}
620
621/// Cuts the current selection: copies it to the clipboard and removes it from the editor.
622pub struct Cut;
623
624impl Action for Cut {
625    fn apply(&mut self, editor: &mut Editor) {
626        // 1. Extract current cursor and selection
627        let mut cursor = editor.get_cursor();
628        let mut selection = editor.get_selection();
629
630        let sel = match &selection {
631            Some(sel) if !sel.is_empty() => sel.clone(),
632            _ => return, // nothing to cut
633        };
634
635        // 2. Copy to clipboard first, before borrowing code mutably
636        let text = editor.code_ref().slice(sel.start, sel.end);
637        let _ = editor.set_clipboard(&text);
638
639        // 3. Now borrow code mutably
640        let code = editor.code_mut();
641        code.tx();
642        code.set_state_before(cursor, selection);
643        code.remove(sel.start, sel.end);
644        code.set_state_after(sel.start, None);
645        code.commit();
646
647        // 4. Update editor state
648        cursor = sel.start;
649        selection = None;
650        editor.set_cursor(cursor);
651        editor.set_selection(selection);
652        editor.reset_highlight_cache();
653    }
654}
655
656/// Copies the selected text to the clipboard.
657/// 
658/// Does nothing if there is no active selection.
659pub struct Copy;
660
661impl Action for Copy {
662    fn apply(&mut self, editor: &mut Editor) {
663        // 1. Extract current selection
664        let selection = editor.get_selection();
665
666        // 2. Return early if no selection
667        let Some(sel) = selection else { return };
668        if sel.is_empty() { return }
669
670        // 3. Get text and copy to clipboard
671        let text = editor.code_ref().slice(sel.start, sel.end);
672        let _ = editor.set_clipboard(&text);
673    }
674}
675
676/// Pastes text from the clipboard at the current cursor position.
677/// 
678/// If a selection exists, it will be replaced by the pasted text.
679/// The pasted text is adjusted using language-specific indentation rules.
680pub struct Paste;
681
682impl Action for Paste {
683    fn apply(&mut self, editor: &mut Editor) {
684        // 1. Get clipboard contents
685        let Ok(text) = editor.get_clipboard() else { return };
686        if text.is_empty() { return }
687
688        // 2. Extract current cursor and selection
689        let mut cursor = editor.get_cursor();
690        let mut selection = editor.get_selection();
691        let code = editor.code_mut();
692
693        // 3. Prepare transaction
694        code.tx();
695        code.set_state_before(cursor, selection);
696
697        // 4. Remove selection if present
698        if let Some(sel) = &selection {
699            if !sel.is_empty() {
700                let (start, end) = sel.sorted();
701                code.remove(start, end);
702                cursor = start;
703                selection = None;
704            }
705        }
706
707        // 5. Perform paste with smart indentation
708        let inserted = code.smart_paste(cursor, &text);
709        cursor += inserted;
710
711        // 6. Finalize transaction
712        code.set_state_after(cursor, selection);
713        code.commit();
714
715        // 7. Update editor state
716        editor.set_cursor(cursor);
717        editor.set_selection(selection);
718        editor.reset_highlight_cache();
719    }
720}
721
722/// Undoes the last edit in the code buffer.
723/// 
724/// Restores both the cursor position and selection state
725/// from the saved editor snapshot if available.
726pub struct Undo;
727
728impl Action for Undo {
729    fn apply(&mut self, editor: &mut Editor) {
730        // 1. Get mutable access to code
731        let code = editor.code_mut();
732
733        // 2. Try to undo
734        let edits = code.undo();
735        editor.reset_highlight_cache();
736
737        // 3. If nothing to undo, return
738        let Some(batch) = edits else { return };
739
740        // 4. Restore cursor and selection from saved state if possible
741        if let Some(before) = batch.state_before {
742            editor.set_cursor(before.offset);
743            editor.set_selection(before.selection);
744            return;
745        }
746
747        // 5. Otherwise infer cursor position from edits
748        for edit in batch.edits.iter().rev() {
749            match &edit.kind {
750                EditKind::Insert { offset, .. } => {
751                    editor.set_cursor(*offset);
752                }
753                EditKind::Remove { offset, text } => {
754                    editor.set_cursor(*offset + text.chars().count());
755                }
756            }
757        }
758    }
759}
760
761/// Redoes the last undone edit in the code buffer.
762/// 
763/// Restores both the cursor position and selection state
764/// from the saved editor snapshot if available.
765pub struct Redo;
766
767impl Action for Redo {
768    fn apply(&mut self, editor: &mut Editor) {
769        // 1. Get mutable access to code
770        let code = editor.code_mut();
771
772        // 2. Try to redo
773        let edits = code.redo();
774        editor.reset_highlight_cache();
775
776        // 3. If nothing to redo, return
777        let Some(batch) = edits else { return };
778
779        // 4. Restore cursor and selection from saved state if possible
780        if let Some(after) = batch.state_after {
781            editor.set_cursor(after.offset);
782            editor.set_selection(after.selection);
783            return;
784        }
785
786        // 5. Otherwise infer cursor position from edits
787        for edit in batch.edits {
788            match &edit.kind {
789                EditKind::Insert { offset, text } => {
790                    editor.set_cursor(*offset + text.chars().count());
791                }
792                EditKind::Remove { offset, .. } => {
793                    editor.set_cursor(*offset);
794                }
795            }
796        }
797    }
798}