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::ArrowKey(direction, shift_pressed) => {
121                // End grouping on navigation
122                if self.is_grouping {
123                    self.history.end_group();
124                    self.is_grouping = false;
125                }
126
127                if *shift_pressed {
128                    // Start selection if not already started
129                    if self.selection_start.is_none() {
130                        self.selection_start = Some(self.cursor);
131                    }
132                    self.move_cursor(*direction);
133                    self.selection_end = Some(self.cursor);
134                } else {
135                    // Clear selection and move cursor
136                    self.clear_selection();
137                    self.move_cursor(*direction);
138                }
139                self.reset_cursor_blink();
140                self.cache.clear();
141                self.scroll_to_cursor()
142            }
143            Message::MouseClick(point) => {
144                // End grouping on mouse click
145                if self.is_grouping {
146                    self.history.end_group();
147                    self.is_grouping = false;
148                }
149
150                self.handle_mouse_click(*point);
151                self.reset_cursor_blink();
152                // Clear selection on click
153                self.clear_selection();
154                self.is_dragging = true;
155                self.selection_start = Some(self.cursor);
156                Task::none()
157            }
158            Message::MouseDrag(point) => {
159                if self.is_dragging {
160                    self.handle_mouse_drag(*point);
161                    self.cache.clear();
162                }
163                Task::none()
164            }
165            Message::MouseRelease => {
166                self.is_dragging = false;
167                Task::none()
168            }
169            Message::Copy => self.copy_selection(),
170            Message::Paste(text) => {
171                // End grouping on paste
172                if self.is_grouping {
173                    self.history.end_group();
174                    self.is_grouping = false;
175                }
176
177                // If text is empty, we need to read from clipboard
178                if text.is_empty() {
179                    // Return a task that reads clipboard and chains to paste
180                    iced::clipboard::read().and_then(|clipboard_text| {
181                        Task::done(Message::Paste(clipboard_text))
182                    })
183                } else {
184                    // We have the text, paste it
185                    self.paste_text(text);
186                    self.cache.clear();
187                    self.scroll_to_cursor()
188                }
189            }
190            Message::DeleteSelection => {
191                // End grouping on delete selection
192                if self.is_grouping {
193                    self.history.end_group();
194                    self.is_grouping = false;
195                }
196
197                // Delete selected text
198                self.delete_selection();
199                self.reset_cursor_blink();
200                self.cache.clear();
201                self.scroll_to_cursor()
202            }
203            Message::Tick => {
204                // Handle cursor blinking
205                if self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL {
206                    self.cursor_visible = !self.cursor_visible;
207                    self.last_blink = std::time::Instant::now();
208                    self.cache.clear();
209                }
210                Task::none()
211            }
212            Message::PageUp => {
213                self.page_up();
214                self.reset_cursor_blink();
215                self.scroll_to_cursor()
216            }
217            Message::PageDown => {
218                self.page_down();
219                self.reset_cursor_blink();
220                self.scroll_to_cursor()
221            }
222            Message::Home(shift_pressed) => {
223                if *shift_pressed {
224                    // Start selection if not already started
225                    if self.selection_start.is_none() {
226                        self.selection_start = Some(self.cursor);
227                    }
228                    self.cursor.1 = 0; // Move to start of line
229                    self.selection_end = Some(self.cursor);
230                } else {
231                    // Clear selection and move cursor
232                    self.clear_selection();
233                    self.cursor.1 = 0;
234                }
235                self.reset_cursor_blink();
236                self.cache.clear();
237                Task::none()
238            }
239            Message::End(shift_pressed) => {
240                let line = self.cursor.0;
241                let line_len = self.buffer.line_len(line);
242
243                if *shift_pressed {
244                    // Start selection if not already started
245                    if self.selection_start.is_none() {
246                        self.selection_start = Some(self.cursor);
247                    }
248                    self.cursor.1 = line_len; // Move to end of line
249                    self.selection_end = Some(self.cursor);
250                } else {
251                    // Clear selection and move cursor
252                    self.clear_selection();
253                    self.cursor.1 = line_len;
254                }
255                self.reset_cursor_blink();
256                self.cache.clear();
257                Task::none()
258            }
259            Message::CtrlHome => {
260                // Move cursor to the beginning of the document
261                self.clear_selection();
262                self.cursor = (0, 0);
263                self.reset_cursor_blink();
264                self.cache.clear();
265                self.scroll_to_cursor()
266            }
267            Message::CtrlEnd => {
268                // Move cursor to the end of the document
269                self.clear_selection();
270                let last_line = self.buffer.line_count().saturating_sub(1);
271                let last_col = self.buffer.line_len(last_line);
272                self.cursor = (last_line, last_col);
273                self.reset_cursor_blink();
274                self.cache.clear();
275                self.scroll_to_cursor()
276            }
277            Message::Scrolled(viewport) => {
278                // Track viewport scroll position and height
279                self.viewport_scroll = viewport.absolute_offset().y;
280                self.viewport_height = viewport.bounds().height;
281                Task::none()
282            }
283            Message::Undo => {
284                // End any current grouping before undoing
285                if self.is_grouping {
286                    self.history.end_group();
287                    self.is_grouping = false;
288                }
289
290                if self.history.undo(&mut self.buffer, &mut self.cursor) {
291                    self.clear_selection();
292                    self.reset_cursor_blink();
293                    self.cache.clear();
294                    self.scroll_to_cursor()
295                } else {
296                    Task::none()
297                }
298            }
299            Message::Redo => {
300                if self.history.redo(&mut self.buffer, &mut self.cursor) {
301                    self.clear_selection();
302                    self.reset_cursor_blink();
303                    self.cache.clear();
304                    self.scroll_to_cursor()
305                } else {
306                    Task::none()
307                }
308            }
309        }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::canvas_editor::ArrowDirection;
317
318    #[test]
319    fn test_new_canvas_editor() {
320        let editor = CodeEditor::new("line1\nline2", "py");
321        assert_eq!(editor.cursor, (0, 0));
322    }
323
324    #[test]
325    fn test_home_key() {
326        let mut editor = CodeEditor::new("hello world", "py");
327        editor.cursor = (0, 5); // Move to middle of line
328        let _ = editor.update(&Message::Home(false));
329        assert_eq!(editor.cursor, (0, 0));
330    }
331
332    #[test]
333    fn test_end_key() {
334        let mut editor = CodeEditor::new("hello world", "py");
335        editor.cursor = (0, 0);
336        let _ = editor.update(&Message::End(false));
337        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
338    }
339
340    #[test]
341    fn test_arrow_key_with_shift_creates_selection() {
342        let mut editor = CodeEditor::new("hello world", "py");
343        editor.cursor = (0, 0);
344
345        // Shift+Right should start selection
346        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
347        assert!(editor.selection_start.is_some());
348        assert!(editor.selection_end.is_some());
349    }
350
351    #[test]
352    fn test_arrow_key_without_shift_clears_selection() {
353        let mut editor = CodeEditor::new("hello world", "py");
354        editor.selection_start = Some((0, 0));
355        editor.selection_end = Some((0, 5));
356
357        // Regular arrow key should clear selection
358        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
359        assert_eq!(editor.selection_start, None);
360        assert_eq!(editor.selection_end, None);
361    }
362
363    #[test]
364    fn test_typing_with_selection() {
365        let mut editor = CodeEditor::new("hello world", "py");
366        editor.selection_start = Some((0, 0));
367        editor.selection_end = Some((0, 5));
368
369        let _ = editor.update(&Message::CharacterInput('X'));
370        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
371        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
372        assert_eq!(editor.buffer.line(0), "Xhello world");
373    }
374
375    #[test]
376    fn test_ctrl_home() {
377        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
378        editor.cursor = (2, 5); // Start at line 3, column 5
379        let _ = editor.update(&Message::CtrlHome);
380        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
381    }
382
383    #[test]
384    fn test_ctrl_end() {
385        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
386        editor.cursor = (0, 0); // Start at beginning
387        let _ = editor.update(&Message::CtrlEnd);
388        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
389    }
390
391    #[test]
392    fn test_ctrl_home_clears_selection() {
393        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
394        editor.cursor = (2, 5);
395        editor.selection_start = Some((0, 0));
396        editor.selection_end = Some((2, 5));
397
398        let _ = editor.update(&Message::CtrlHome);
399        assert_eq!(editor.cursor, (0, 0));
400        assert_eq!(editor.selection_start, None);
401        assert_eq!(editor.selection_end, None);
402    }
403
404    #[test]
405    fn test_ctrl_end_clears_selection() {
406        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
407        editor.cursor = (0, 0);
408        editor.selection_start = Some((0, 0));
409        editor.selection_end = Some((1, 3));
410
411        let _ = editor.update(&Message::CtrlEnd);
412        assert_eq!(editor.cursor, (2, 5));
413        assert_eq!(editor.selection_start, None);
414        assert_eq!(editor.selection_end, None);
415    }
416
417    #[test]
418    fn test_delete_selection_message() {
419        let mut editor = CodeEditor::new("hello world", "py");
420        editor.cursor = (0, 0);
421        editor.selection_start = Some((0, 0));
422        editor.selection_end = Some((0, 5));
423
424        let _ = editor.update(&Message::DeleteSelection);
425        assert_eq!(editor.buffer.line(0), " world");
426        assert_eq!(editor.cursor, (0, 0));
427        assert_eq!(editor.selection_start, None);
428        assert_eq!(editor.selection_end, None);
429    }
430
431    #[test]
432    fn test_delete_selection_multiline() {
433        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
434        editor.cursor = (0, 2);
435        editor.selection_start = Some((0, 2));
436        editor.selection_end = Some((2, 2));
437
438        let _ = editor.update(&Message::DeleteSelection);
439        assert_eq!(editor.buffer.line(0), "line3");
440        assert_eq!(editor.cursor, (0, 2));
441        assert_eq!(editor.selection_start, None);
442    }
443
444    #[test]
445    fn test_delete_selection_no_selection() {
446        let mut editor = CodeEditor::new("hello world", "py");
447        editor.cursor = (0, 5);
448
449        let _ = editor.update(&Message::DeleteSelection);
450        // Should do nothing if there's no selection
451        assert_eq!(editor.buffer.line(0), "hello world");
452        assert_eq!(editor.cursor, (0, 5));
453    }
454
455    #[test]
456    fn test_undo_char_insert() {
457        let mut editor = CodeEditor::new("hello", "py");
458        editor.cursor = (0, 5);
459
460        // Type a character
461        let _ = editor.update(&Message::CharacterInput('!'));
462        assert_eq!(editor.buffer.line(0), "hello!");
463        assert_eq!(editor.cursor, (0, 6));
464
465        // Undo should remove it (but first end the grouping)
466        editor.history.end_group();
467        let _ = editor.update(&Message::Undo);
468        assert_eq!(editor.buffer.line(0), "hello");
469        assert_eq!(editor.cursor, (0, 5));
470    }
471
472    #[test]
473    fn test_undo_redo_char_insert() {
474        let mut editor = CodeEditor::new("hello", "py");
475        editor.cursor = (0, 5);
476
477        // Type a character
478        let _ = editor.update(&Message::CharacterInput('!'));
479        editor.history.end_group();
480
481        // Undo
482        let _ = editor.update(&Message::Undo);
483        assert_eq!(editor.buffer.line(0), "hello");
484
485        // Redo
486        let _ = editor.update(&Message::Redo);
487        assert_eq!(editor.buffer.line(0), "hello!");
488        assert_eq!(editor.cursor, (0, 6));
489    }
490
491    #[test]
492    fn test_undo_backspace() {
493        let mut editor = CodeEditor::new("hello", "py");
494        editor.cursor = (0, 5);
495
496        // Backspace
497        let _ = editor.update(&Message::Backspace);
498        assert_eq!(editor.buffer.line(0), "hell");
499        assert_eq!(editor.cursor, (0, 4));
500
501        // Undo
502        let _ = editor.update(&Message::Undo);
503        assert_eq!(editor.buffer.line(0), "hello");
504        assert_eq!(editor.cursor, (0, 5));
505    }
506
507    #[test]
508    fn test_undo_newline() {
509        let mut editor = CodeEditor::new("hello world", "py");
510        editor.cursor = (0, 5);
511
512        // Insert newline
513        let _ = editor.update(&Message::Enter);
514        assert_eq!(editor.buffer.line(0), "hello");
515        assert_eq!(editor.buffer.line(1), " world");
516        assert_eq!(editor.cursor, (1, 0));
517
518        // Undo
519        let _ = editor.update(&Message::Undo);
520        assert_eq!(editor.buffer.line(0), "hello world");
521        assert_eq!(editor.cursor, (0, 5));
522    }
523
524    #[test]
525    fn test_undo_grouped_typing() {
526        let mut editor = CodeEditor::new("hello", "py");
527        editor.cursor = (0, 5);
528
529        // Type multiple characters (they should be grouped)
530        let _ = editor.update(&Message::CharacterInput(' '));
531        let _ = editor.update(&Message::CharacterInput('w'));
532        let _ = editor.update(&Message::CharacterInput('o'));
533        let _ = editor.update(&Message::CharacterInput('r'));
534        let _ = editor.update(&Message::CharacterInput('l'));
535        let _ = editor.update(&Message::CharacterInput('d'));
536
537        assert_eq!(editor.buffer.line(0), "hello world");
538
539        // End the group
540        editor.history.end_group();
541
542        // Single undo should remove all grouped characters
543        let _ = editor.update(&Message::Undo);
544        assert_eq!(editor.buffer.line(0), "hello");
545        assert_eq!(editor.cursor, (0, 5));
546    }
547
548    #[test]
549    fn test_navigation_ends_grouping() {
550        let mut editor = CodeEditor::new("hello", "py");
551        editor.cursor = (0, 5);
552
553        // Type a character (starts grouping)
554        let _ = editor.update(&Message::CharacterInput('!'));
555        assert!(editor.is_grouping);
556
557        // Move cursor (ends grouping)
558        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
559        assert!(!editor.is_grouping);
560
561        // Type another character (starts new group)
562        let _ = editor.update(&Message::CharacterInput('?'));
563        assert!(editor.is_grouping);
564
565        editor.history.end_group();
566
567        // Two separate undo operations
568        let _ = editor.update(&Message::Undo);
569        assert_eq!(editor.buffer.line(0), "hello!");
570
571        let _ = editor.update(&Message::Undo);
572        assert_eq!(editor.buffer.line(0), "hello");
573    }
574
575    #[test]
576    fn test_multiple_undo_redo() {
577        let mut editor = CodeEditor::new("a", "py");
578        editor.cursor = (0, 1);
579
580        // Make several changes
581        let _ = editor.update(&Message::CharacterInput('b'));
582        editor.history.end_group();
583
584        let _ = editor.update(&Message::CharacterInput('c'));
585        editor.history.end_group();
586
587        let _ = editor.update(&Message::CharacterInput('d'));
588        editor.history.end_group();
589
590        assert_eq!(editor.buffer.line(0), "abcd");
591
592        // Undo all
593        let _ = editor.update(&Message::Undo);
594        assert_eq!(editor.buffer.line(0), "abc");
595
596        let _ = editor.update(&Message::Undo);
597        assert_eq!(editor.buffer.line(0), "ab");
598
599        let _ = editor.update(&Message::Undo);
600        assert_eq!(editor.buffer.line(0), "a");
601
602        // Redo all
603        let _ = editor.update(&Message::Redo);
604        assert_eq!(editor.buffer.line(0), "ab");
605
606        let _ = editor.update(&Message::Redo);
607        assert_eq!(editor.buffer.line(0), "abc");
608
609        let _ = editor.update(&Message::Redo);
610        assert_eq!(editor.buffer.line(0), "abcd");
611    }
612
613    #[test]
614    fn test_delete_key_with_selection() {
615        let mut editor = CodeEditor::new("hello world", "py");
616        editor.selection_start = Some((0, 0));
617        editor.selection_end = Some((0, 5));
618        editor.cursor = (0, 5);
619
620        let _ = editor.update(&Message::Delete);
621
622        assert_eq!(editor.buffer.line(0), " world");
623        assert_eq!(editor.cursor, (0, 0));
624        assert_eq!(editor.selection_start, None);
625        assert_eq!(editor.selection_end, None);
626    }
627
628    #[test]
629    fn test_delete_key_without_selection() {
630        let mut editor = CodeEditor::new("hello", "py");
631        editor.cursor = (0, 0);
632
633        let _ = editor.update(&Message::Delete);
634
635        // Should delete the 'h'
636        assert_eq!(editor.buffer.line(0), "ello");
637        assert_eq!(editor.cursor, (0, 0));
638    }
639
640    #[test]
641    fn test_backspace_with_selection() {
642        let mut editor = CodeEditor::new("hello world", "py");
643        editor.selection_start = Some((0, 6));
644        editor.selection_end = Some((0, 11));
645        editor.cursor = (0, 11);
646
647        let _ = editor.update(&Message::Backspace);
648
649        assert_eq!(editor.buffer.line(0), "hello ");
650        assert_eq!(editor.cursor, (0, 6));
651        assert_eq!(editor.selection_start, None);
652        assert_eq!(editor.selection_end, None);
653    }
654
655    #[test]
656    fn test_backspace_without_selection() {
657        let mut editor = CodeEditor::new("hello", "py");
658        editor.cursor = (0, 5);
659
660        let _ = editor.update(&Message::Backspace);
661
662        // Should delete the 'o'
663        assert_eq!(editor.buffer.line(0), "hell");
664        assert_eq!(editor.cursor, (0, 4));
665    }
666
667    #[test]
668    fn test_delete_multiline_selection() {
669        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
670        editor.selection_start = Some((0, 2));
671        editor.selection_end = Some((2, 2));
672        editor.cursor = (2, 2);
673
674        let _ = editor.update(&Message::Delete);
675
676        assert_eq!(editor.buffer.line(0), "line3");
677        assert_eq!(editor.cursor, (0, 2));
678        assert_eq!(editor.selection_start, None);
679    }
680}