iced_code_editor/canvas_editor/
update.rs

1//! Message handling and update logic.
2
3use iced::Task;
4
5use super::command::{
6    Command, DeleteCharCommand, DeleteForwardCommand, InsertCharCommand,
7    InsertNewlineCommand,
8};
9use super::{CURSOR_BLINK_INTERVAL, CodeEditor, Message};
10
11impl CodeEditor {
12    /// Updates the editor state based on messages and returns scroll commands.
13    ///
14    /// # Arguments
15    ///
16    /// * `message` - The message to process
17    ///
18    /// # Returns
19    ///
20    /// A Task that may contain scroll commands to keep cursor visible
21    pub fn update(&mut self, message: &Message) -> Task<Message> {
22        match message {
23            Message::CharacterInput(ch) => {
24                // Start grouping if not already grouping (for smart undo)
25                if !self.is_grouping {
26                    self.history.begin_group("Typing");
27                    self.is_grouping = true;
28                }
29
30                let (line, col) = self.cursor;
31                let mut cmd =
32                    InsertCharCommand::new(line, col, *ch, self.cursor);
33                cmd.execute(&mut self.buffer, &mut self.cursor);
34                self.history.push(Box::new(cmd));
35
36                self.reset_cursor_blink();
37                self.cache.clear();
38                Task::none()
39            }
40            Message::Backspace => {
41                // End grouping on backspace (separate from typing)
42                if self.is_grouping {
43                    self.history.end_group();
44                    self.is_grouping = false;
45                }
46
47                // Check if there's a selection - if so, delete it instead
48                if self.selection_start.is_some()
49                    && self.selection_end.is_some()
50                {
51                    self.delete_selection();
52                    self.reset_cursor_blink();
53                    self.cache.clear();
54                    return self.scroll_to_cursor();
55                }
56
57                // No selection - perform normal backspace
58                let (line, col) = self.cursor;
59                let mut cmd = DeleteCharCommand::new(
60                    &self.buffer,
61                    line,
62                    col,
63                    self.cursor,
64                );
65                cmd.execute(&mut self.buffer, &mut self.cursor);
66                self.history.push(Box::new(cmd));
67
68                self.reset_cursor_blink();
69                self.cache.clear();
70                self.scroll_to_cursor()
71            }
72            Message::Delete => {
73                // End grouping on delete
74                if self.is_grouping {
75                    self.history.end_group();
76                    self.is_grouping = false;
77                }
78
79                // Check if there's a selection - if so, delete it instead
80                if self.selection_start.is_some()
81                    && self.selection_end.is_some()
82                {
83                    self.delete_selection();
84                    self.reset_cursor_blink();
85                    self.cache.clear();
86                    return self.scroll_to_cursor();
87                }
88
89                // No selection - perform normal forward delete
90                let (line, col) = self.cursor;
91                let mut cmd = DeleteForwardCommand::new(
92                    &self.buffer,
93                    line,
94                    col,
95                    self.cursor,
96                );
97                cmd.execute(&mut self.buffer, &mut self.cursor);
98                self.history.push(Box::new(cmd));
99
100                self.reset_cursor_blink();
101                self.cache.clear();
102                Task::none()
103            }
104            Message::Enter => {
105                // End grouping on enter
106                if self.is_grouping {
107                    self.history.end_group();
108                    self.is_grouping = false;
109                }
110
111                let (line, col) = self.cursor;
112                let mut cmd = InsertNewlineCommand::new(line, col, self.cursor);
113                cmd.execute(&mut self.buffer, &mut self.cursor);
114                self.history.push(Box::new(cmd));
115
116                self.reset_cursor_blink();
117                self.cache.clear();
118                self.scroll_to_cursor()
119            }
120            Message::Tab => {
121                // Insert 4 spaces for Tab
122                // Start grouping if not already grouping
123                if !self.is_grouping {
124                    self.history.begin_group("Tab");
125                    self.is_grouping = true;
126                }
127
128                let (line, col) = self.cursor;
129                // Insert 4 spaces
130                for i in 0..4 {
131                    let current_col = col + i;
132                    let mut cmd = InsertCharCommand::new(
133                        line,
134                        current_col,
135                        ' ',
136                        (line, current_col),
137                    );
138                    cmd.execute(&mut self.buffer, &mut self.cursor);
139                    self.history.push(Box::new(cmd));
140                }
141
142                self.reset_cursor_blink();
143                self.cache.clear();
144                Task::none()
145            }
146            Message::ArrowKey(direction, shift_pressed) => {
147                // End grouping on navigation
148                if self.is_grouping {
149                    self.history.end_group();
150                    self.is_grouping = false;
151                }
152
153                if *shift_pressed {
154                    // Start selection if not already started
155                    if self.selection_start.is_none() {
156                        self.selection_start = Some(self.cursor);
157                    }
158                    self.move_cursor(*direction);
159                    self.selection_end = Some(self.cursor);
160                } else {
161                    // Clear selection and move cursor
162                    self.clear_selection();
163                    self.move_cursor(*direction);
164                }
165                self.reset_cursor_blink();
166                self.cache.clear();
167                self.scroll_to_cursor()
168            }
169            Message::MouseClick(point) => {
170                // End grouping on mouse click
171                if self.is_grouping {
172                    self.history.end_group();
173                    self.is_grouping = false;
174                }
175
176                self.handle_mouse_click(*point);
177                self.reset_cursor_blink();
178                // Clear selection on click
179                self.clear_selection();
180                self.is_dragging = true;
181                self.selection_start = Some(self.cursor);
182                Task::none()
183            }
184            Message::MouseDrag(point) => {
185                if self.is_dragging {
186                    self.handle_mouse_drag(*point);
187                    self.cache.clear();
188                }
189                Task::none()
190            }
191            Message::MouseRelease => {
192                self.is_dragging = false;
193                Task::none()
194            }
195            Message::Copy => self.copy_selection(),
196            Message::Paste(text) => {
197                // End grouping on paste
198                if self.is_grouping {
199                    self.history.end_group();
200                    self.is_grouping = false;
201                }
202
203                // If text is empty, we need to read from clipboard
204                if text.is_empty() {
205                    // Return a task that reads clipboard and chains to paste
206                    iced::clipboard::read().and_then(|clipboard_text| {
207                        Task::done(Message::Paste(clipboard_text))
208                    })
209                } else {
210                    // We have the text, paste it
211                    self.paste_text(text);
212                    self.cache.clear();
213                    self.scroll_to_cursor()
214                }
215            }
216            Message::DeleteSelection => {
217                // End grouping on delete selection
218                if self.is_grouping {
219                    self.history.end_group();
220                    self.is_grouping = false;
221                }
222
223                // Delete selected text
224                self.delete_selection();
225                self.reset_cursor_blink();
226                self.cache.clear();
227                self.scroll_to_cursor()
228            }
229            Message::Tick => {
230                // Handle cursor blinking
231                if self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL {
232                    self.cursor_visible = !self.cursor_visible;
233                    self.last_blink = std::time::Instant::now();
234                    self.cache.clear();
235                }
236                Task::none()
237            }
238            Message::PageUp => {
239                self.page_up();
240                self.reset_cursor_blink();
241                self.scroll_to_cursor()
242            }
243            Message::PageDown => {
244                self.page_down();
245                self.reset_cursor_blink();
246                self.scroll_to_cursor()
247            }
248            Message::Home(shift_pressed) => {
249                if *shift_pressed {
250                    // Start selection if not already started
251                    if self.selection_start.is_none() {
252                        self.selection_start = Some(self.cursor);
253                    }
254                    self.cursor.1 = 0; // Move to start of line
255                    self.selection_end = Some(self.cursor);
256                } else {
257                    // Clear selection and move cursor
258                    self.clear_selection();
259                    self.cursor.1 = 0;
260                }
261                self.reset_cursor_blink();
262                self.cache.clear();
263                Task::none()
264            }
265            Message::End(shift_pressed) => {
266                let line = self.cursor.0;
267                let line_len = self.buffer.line_len(line);
268
269                if *shift_pressed {
270                    // Start selection if not already started
271                    if self.selection_start.is_none() {
272                        self.selection_start = Some(self.cursor);
273                    }
274                    self.cursor.1 = line_len; // Move to end of line
275                    self.selection_end = Some(self.cursor);
276                } else {
277                    // Clear selection and move cursor
278                    self.clear_selection();
279                    self.cursor.1 = line_len;
280                }
281                self.reset_cursor_blink();
282                self.cache.clear();
283                Task::none()
284            }
285            Message::CtrlHome => {
286                // Move cursor to the beginning of the document
287                self.clear_selection();
288                self.cursor = (0, 0);
289                self.reset_cursor_blink();
290                self.cache.clear();
291                self.scroll_to_cursor()
292            }
293            Message::CtrlEnd => {
294                // Move cursor to the end of the document
295                self.clear_selection();
296                let last_line = self.buffer.line_count().saturating_sub(1);
297                let last_col = self.buffer.line_len(last_line);
298                self.cursor = (last_line, last_col);
299                self.reset_cursor_blink();
300                self.cache.clear();
301                self.scroll_to_cursor()
302            }
303            Message::Scrolled(viewport) => {
304                // Track viewport scroll position and height
305                self.viewport_scroll = viewport.absolute_offset().y;
306                let new_height = viewport.bounds().height;
307                // Clear cache when viewport height changes significantly
308                // to ensure proper redraw (e.g., window resize)
309                if (self.viewport_height - new_height).abs() > 1.0 {
310                    self.cache.clear();
311                }
312                self.viewport_height = new_height;
313                Task::none()
314            }
315            Message::Undo => {
316                // End any current grouping before undoing
317                if self.is_grouping {
318                    self.history.end_group();
319                    self.is_grouping = false;
320                }
321
322                if self.history.undo(&mut self.buffer, &mut self.cursor) {
323                    self.clear_selection();
324                    self.reset_cursor_blink();
325                    self.cache.clear();
326                    self.scroll_to_cursor()
327                } else {
328                    Task::none()
329                }
330            }
331            Message::Redo => {
332                if self.history.redo(&mut self.buffer, &mut self.cursor) {
333                    self.clear_selection();
334                    self.reset_cursor_blink();
335                    self.cache.clear();
336                    self.scroll_to_cursor()
337                } else {
338                    Task::none()
339                }
340            }
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::canvas_editor::ArrowDirection;
349
350    #[test]
351    fn test_new_canvas_editor() {
352        let editor = CodeEditor::new("line1\nline2", "py");
353        assert_eq!(editor.cursor, (0, 0));
354    }
355
356    #[test]
357    fn test_home_key() {
358        let mut editor = CodeEditor::new("hello world", "py");
359        editor.cursor = (0, 5); // Move to middle of line
360        let _ = editor.update(&Message::Home(false));
361        assert_eq!(editor.cursor, (0, 0));
362    }
363
364    #[test]
365    fn test_end_key() {
366        let mut editor = CodeEditor::new("hello world", "py");
367        editor.cursor = (0, 0);
368        let _ = editor.update(&Message::End(false));
369        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
370    }
371
372    #[test]
373    fn test_arrow_key_with_shift_creates_selection() {
374        let mut editor = CodeEditor::new("hello world", "py");
375        editor.cursor = (0, 0);
376
377        // Shift+Right should start selection
378        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
379        assert!(editor.selection_start.is_some());
380        assert!(editor.selection_end.is_some());
381    }
382
383    #[test]
384    fn test_arrow_key_without_shift_clears_selection() {
385        let mut editor = CodeEditor::new("hello world", "py");
386        editor.selection_start = Some((0, 0));
387        editor.selection_end = Some((0, 5));
388
389        // Regular arrow key should clear selection
390        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
391        assert_eq!(editor.selection_start, None);
392        assert_eq!(editor.selection_end, None);
393    }
394
395    #[test]
396    fn test_typing_with_selection() {
397        let mut editor = CodeEditor::new("hello world", "py");
398        editor.selection_start = Some((0, 0));
399        editor.selection_end = Some((0, 5));
400
401        let _ = editor.update(&Message::CharacterInput('X'));
402        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
403        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
404        assert_eq!(editor.buffer.line(0), "Xhello world");
405    }
406
407    #[test]
408    fn test_ctrl_home() {
409        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
410        editor.cursor = (2, 5); // Start at line 3, column 5
411        let _ = editor.update(&Message::CtrlHome);
412        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
413    }
414
415    #[test]
416    fn test_ctrl_end() {
417        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
418        editor.cursor = (0, 0); // Start at beginning
419        let _ = editor.update(&Message::CtrlEnd);
420        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
421    }
422
423    #[test]
424    fn test_ctrl_home_clears_selection() {
425        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
426        editor.cursor = (2, 5);
427        editor.selection_start = Some((0, 0));
428        editor.selection_end = Some((2, 5));
429
430        let _ = editor.update(&Message::CtrlHome);
431        assert_eq!(editor.cursor, (0, 0));
432        assert_eq!(editor.selection_start, None);
433        assert_eq!(editor.selection_end, None);
434    }
435
436    #[test]
437    fn test_ctrl_end_clears_selection() {
438        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
439        editor.cursor = (0, 0);
440        editor.selection_start = Some((0, 0));
441        editor.selection_end = Some((1, 3));
442
443        let _ = editor.update(&Message::CtrlEnd);
444        assert_eq!(editor.cursor, (2, 5));
445        assert_eq!(editor.selection_start, None);
446        assert_eq!(editor.selection_end, None);
447    }
448
449    #[test]
450    fn test_delete_selection_message() {
451        let mut editor = CodeEditor::new("hello world", "py");
452        editor.cursor = (0, 0);
453        editor.selection_start = Some((0, 0));
454        editor.selection_end = Some((0, 5));
455
456        let _ = editor.update(&Message::DeleteSelection);
457        assert_eq!(editor.buffer.line(0), " world");
458        assert_eq!(editor.cursor, (0, 0));
459        assert_eq!(editor.selection_start, None);
460        assert_eq!(editor.selection_end, None);
461    }
462
463    #[test]
464    fn test_delete_selection_multiline() {
465        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
466        editor.cursor = (0, 2);
467        editor.selection_start = Some((0, 2));
468        editor.selection_end = Some((2, 2));
469
470        let _ = editor.update(&Message::DeleteSelection);
471        assert_eq!(editor.buffer.line(0), "line3");
472        assert_eq!(editor.cursor, (0, 2));
473        assert_eq!(editor.selection_start, None);
474    }
475
476    #[test]
477    fn test_delete_selection_no_selection() {
478        let mut editor = CodeEditor::new("hello world", "py");
479        editor.cursor = (0, 5);
480
481        let _ = editor.update(&Message::DeleteSelection);
482        // Should do nothing if there's no selection
483        assert_eq!(editor.buffer.line(0), "hello world");
484        assert_eq!(editor.cursor, (0, 5));
485    }
486
487    #[test]
488    fn test_undo_char_insert() {
489        let mut editor = CodeEditor::new("hello", "py");
490        editor.cursor = (0, 5);
491
492        // Type a character
493        let _ = editor.update(&Message::CharacterInput('!'));
494        assert_eq!(editor.buffer.line(0), "hello!");
495        assert_eq!(editor.cursor, (0, 6));
496
497        // Undo should remove it (but first end the grouping)
498        editor.history.end_group();
499        let _ = editor.update(&Message::Undo);
500        assert_eq!(editor.buffer.line(0), "hello");
501        assert_eq!(editor.cursor, (0, 5));
502    }
503
504    #[test]
505    fn test_undo_redo_char_insert() {
506        let mut editor = CodeEditor::new("hello", "py");
507        editor.cursor = (0, 5);
508
509        // Type a character
510        let _ = editor.update(&Message::CharacterInput('!'));
511        editor.history.end_group();
512
513        // Undo
514        let _ = editor.update(&Message::Undo);
515        assert_eq!(editor.buffer.line(0), "hello");
516
517        // Redo
518        let _ = editor.update(&Message::Redo);
519        assert_eq!(editor.buffer.line(0), "hello!");
520        assert_eq!(editor.cursor, (0, 6));
521    }
522
523    #[test]
524    fn test_undo_backspace() {
525        let mut editor = CodeEditor::new("hello", "py");
526        editor.cursor = (0, 5);
527
528        // Backspace
529        let _ = editor.update(&Message::Backspace);
530        assert_eq!(editor.buffer.line(0), "hell");
531        assert_eq!(editor.cursor, (0, 4));
532
533        // Undo
534        let _ = editor.update(&Message::Undo);
535        assert_eq!(editor.buffer.line(0), "hello");
536        assert_eq!(editor.cursor, (0, 5));
537    }
538
539    #[test]
540    fn test_undo_newline() {
541        let mut editor = CodeEditor::new("hello world", "py");
542        editor.cursor = (0, 5);
543
544        // Insert newline
545        let _ = editor.update(&Message::Enter);
546        assert_eq!(editor.buffer.line(0), "hello");
547        assert_eq!(editor.buffer.line(1), " world");
548        assert_eq!(editor.cursor, (1, 0));
549
550        // Undo
551        let _ = editor.update(&Message::Undo);
552        assert_eq!(editor.buffer.line(0), "hello world");
553        assert_eq!(editor.cursor, (0, 5));
554    }
555
556    #[test]
557    fn test_undo_grouped_typing() {
558        let mut editor = CodeEditor::new("hello", "py");
559        editor.cursor = (0, 5);
560
561        // Type multiple characters (they should be grouped)
562        let _ = editor.update(&Message::CharacterInput(' '));
563        let _ = editor.update(&Message::CharacterInput('w'));
564        let _ = editor.update(&Message::CharacterInput('o'));
565        let _ = editor.update(&Message::CharacterInput('r'));
566        let _ = editor.update(&Message::CharacterInput('l'));
567        let _ = editor.update(&Message::CharacterInput('d'));
568
569        assert_eq!(editor.buffer.line(0), "hello world");
570
571        // End the group
572        editor.history.end_group();
573
574        // Single undo should remove all grouped characters
575        let _ = editor.update(&Message::Undo);
576        assert_eq!(editor.buffer.line(0), "hello");
577        assert_eq!(editor.cursor, (0, 5));
578    }
579
580    #[test]
581    fn test_navigation_ends_grouping() {
582        let mut editor = CodeEditor::new("hello", "py");
583        editor.cursor = (0, 5);
584
585        // Type a character (starts grouping)
586        let _ = editor.update(&Message::CharacterInput('!'));
587        assert!(editor.is_grouping);
588
589        // Move cursor (ends grouping)
590        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
591        assert!(!editor.is_grouping);
592
593        // Type another character (starts new group)
594        let _ = editor.update(&Message::CharacterInput('?'));
595        assert!(editor.is_grouping);
596
597        editor.history.end_group();
598
599        // Two separate undo operations
600        let _ = editor.update(&Message::Undo);
601        assert_eq!(editor.buffer.line(0), "hello!");
602
603        let _ = editor.update(&Message::Undo);
604        assert_eq!(editor.buffer.line(0), "hello");
605    }
606
607    #[test]
608    fn test_multiple_undo_redo() {
609        let mut editor = CodeEditor::new("a", "py");
610        editor.cursor = (0, 1);
611
612        // Make several changes
613        let _ = editor.update(&Message::CharacterInput('b'));
614        editor.history.end_group();
615
616        let _ = editor.update(&Message::CharacterInput('c'));
617        editor.history.end_group();
618
619        let _ = editor.update(&Message::CharacterInput('d'));
620        editor.history.end_group();
621
622        assert_eq!(editor.buffer.line(0), "abcd");
623
624        // Undo all
625        let _ = editor.update(&Message::Undo);
626        assert_eq!(editor.buffer.line(0), "abc");
627
628        let _ = editor.update(&Message::Undo);
629        assert_eq!(editor.buffer.line(0), "ab");
630
631        let _ = editor.update(&Message::Undo);
632        assert_eq!(editor.buffer.line(0), "a");
633
634        // Redo all
635        let _ = editor.update(&Message::Redo);
636        assert_eq!(editor.buffer.line(0), "ab");
637
638        let _ = editor.update(&Message::Redo);
639        assert_eq!(editor.buffer.line(0), "abc");
640
641        let _ = editor.update(&Message::Redo);
642        assert_eq!(editor.buffer.line(0), "abcd");
643    }
644
645    #[test]
646    fn test_delete_key_with_selection() {
647        let mut editor = CodeEditor::new("hello world", "py");
648        editor.selection_start = Some((0, 0));
649        editor.selection_end = Some((0, 5));
650        editor.cursor = (0, 5);
651
652        let _ = editor.update(&Message::Delete);
653
654        assert_eq!(editor.buffer.line(0), " world");
655        assert_eq!(editor.cursor, (0, 0));
656        assert_eq!(editor.selection_start, None);
657        assert_eq!(editor.selection_end, None);
658    }
659
660    #[test]
661    fn test_delete_key_without_selection() {
662        let mut editor = CodeEditor::new("hello", "py");
663        editor.cursor = (0, 0);
664
665        let _ = editor.update(&Message::Delete);
666
667        // Should delete the 'h'
668        assert_eq!(editor.buffer.line(0), "ello");
669        assert_eq!(editor.cursor, (0, 0));
670    }
671
672    #[test]
673    fn test_backspace_with_selection() {
674        let mut editor = CodeEditor::new("hello world", "py");
675        editor.selection_start = Some((0, 6));
676        editor.selection_end = Some((0, 11));
677        editor.cursor = (0, 11);
678
679        let _ = editor.update(&Message::Backspace);
680
681        assert_eq!(editor.buffer.line(0), "hello ");
682        assert_eq!(editor.cursor, (0, 6));
683        assert_eq!(editor.selection_start, None);
684        assert_eq!(editor.selection_end, None);
685    }
686
687    #[test]
688    fn test_backspace_without_selection() {
689        let mut editor = CodeEditor::new("hello", "py");
690        editor.cursor = (0, 5);
691
692        let _ = editor.update(&Message::Backspace);
693
694        // Should delete the 'o'
695        assert_eq!(editor.buffer.line(0), "hell");
696        assert_eq!(editor.cursor, (0, 4));
697    }
698
699    #[test]
700    fn test_delete_multiline_selection() {
701        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
702        editor.selection_start = Some((0, 2));
703        editor.selection_end = Some((2, 2));
704        editor.cursor = (2, 2);
705
706        let _ = editor.update(&Message::Delete);
707
708        assert_eq!(editor.buffer.line(0), "line3");
709        assert_eq!(editor.cursor, (0, 2));
710        assert_eq!(editor.selection_start, None);
711    }
712}