Skip to main content

ratatui_code_editor/
actions.rs

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