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                self.viewport_height = viewport.bounds().height;
307                Task::none()
308            }
309            Message::Undo => {
310                // End any current grouping before undoing
311                if self.is_grouping {
312                    self.history.end_group();
313                    self.is_grouping = false;
314                }
315
316                if self.history.undo(&mut self.buffer, &mut self.cursor) {
317                    self.clear_selection();
318                    self.reset_cursor_blink();
319                    self.cache.clear();
320                    self.scroll_to_cursor()
321                } else {
322                    Task::none()
323                }
324            }
325            Message::Redo => {
326                if self.history.redo(&mut self.buffer, &mut self.cursor) {
327                    self.clear_selection();
328                    self.reset_cursor_blink();
329                    self.cache.clear();
330                    self.scroll_to_cursor()
331                } else {
332                    Task::none()
333                }
334            }
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::canvas_editor::ArrowDirection;
343
344    #[test]
345    fn test_new_canvas_editor() {
346        let editor = CodeEditor::new("line1\nline2", "py");
347        assert_eq!(editor.cursor, (0, 0));
348    }
349
350    #[test]
351    fn test_home_key() {
352        let mut editor = CodeEditor::new("hello world", "py");
353        editor.cursor = (0, 5); // Move to middle of line
354        let _ = editor.update(&Message::Home(false));
355        assert_eq!(editor.cursor, (0, 0));
356    }
357
358    #[test]
359    fn test_end_key() {
360        let mut editor = CodeEditor::new("hello world", "py");
361        editor.cursor = (0, 0);
362        let _ = editor.update(&Message::End(false));
363        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
364    }
365
366    #[test]
367    fn test_arrow_key_with_shift_creates_selection() {
368        let mut editor = CodeEditor::new("hello world", "py");
369        editor.cursor = (0, 0);
370
371        // Shift+Right should start selection
372        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
373        assert!(editor.selection_start.is_some());
374        assert!(editor.selection_end.is_some());
375    }
376
377    #[test]
378    fn test_arrow_key_without_shift_clears_selection() {
379        let mut editor = CodeEditor::new("hello world", "py");
380        editor.selection_start = Some((0, 0));
381        editor.selection_end = Some((0, 5));
382
383        // Regular arrow key should clear selection
384        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
385        assert_eq!(editor.selection_start, None);
386        assert_eq!(editor.selection_end, None);
387    }
388
389    #[test]
390    fn test_typing_with_selection() {
391        let mut editor = CodeEditor::new("hello world", "py");
392        editor.selection_start = Some((0, 0));
393        editor.selection_end = Some((0, 5));
394
395        let _ = editor.update(&Message::CharacterInput('X'));
396        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
397        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
398        assert_eq!(editor.buffer.line(0), "Xhello world");
399    }
400
401    #[test]
402    fn test_ctrl_home() {
403        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
404        editor.cursor = (2, 5); // Start at line 3, column 5
405        let _ = editor.update(&Message::CtrlHome);
406        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
407    }
408
409    #[test]
410    fn test_ctrl_end() {
411        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
412        editor.cursor = (0, 0); // Start at beginning
413        let _ = editor.update(&Message::CtrlEnd);
414        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
415    }
416
417    #[test]
418    fn test_ctrl_home_clears_selection() {
419        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
420        editor.cursor = (2, 5);
421        editor.selection_start = Some((0, 0));
422        editor.selection_end = Some((2, 5));
423
424        let _ = editor.update(&Message::CtrlHome);
425        assert_eq!(editor.cursor, (0, 0));
426        assert_eq!(editor.selection_start, None);
427        assert_eq!(editor.selection_end, None);
428    }
429
430    #[test]
431    fn test_ctrl_end_clears_selection() {
432        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
433        editor.cursor = (0, 0);
434        editor.selection_start = Some((0, 0));
435        editor.selection_end = Some((1, 3));
436
437        let _ = editor.update(&Message::CtrlEnd);
438        assert_eq!(editor.cursor, (2, 5));
439        assert_eq!(editor.selection_start, None);
440        assert_eq!(editor.selection_end, None);
441    }
442
443    #[test]
444    fn test_delete_selection_message() {
445        let mut editor = CodeEditor::new("hello world", "py");
446        editor.cursor = (0, 0);
447        editor.selection_start = Some((0, 0));
448        editor.selection_end = Some((0, 5));
449
450        let _ = editor.update(&Message::DeleteSelection);
451        assert_eq!(editor.buffer.line(0), " world");
452        assert_eq!(editor.cursor, (0, 0));
453        assert_eq!(editor.selection_start, None);
454        assert_eq!(editor.selection_end, None);
455    }
456
457    #[test]
458    fn test_delete_selection_multiline() {
459        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
460        editor.cursor = (0, 2);
461        editor.selection_start = Some((0, 2));
462        editor.selection_end = Some((2, 2));
463
464        let _ = editor.update(&Message::DeleteSelection);
465        assert_eq!(editor.buffer.line(0), "line3");
466        assert_eq!(editor.cursor, (0, 2));
467        assert_eq!(editor.selection_start, None);
468    }
469
470    #[test]
471    fn test_delete_selection_no_selection() {
472        let mut editor = CodeEditor::new("hello world", "py");
473        editor.cursor = (0, 5);
474
475        let _ = editor.update(&Message::DeleteSelection);
476        // Should do nothing if there's no selection
477        assert_eq!(editor.buffer.line(0), "hello world");
478        assert_eq!(editor.cursor, (0, 5));
479    }
480
481    #[test]
482    fn test_undo_char_insert() {
483        let mut editor = CodeEditor::new("hello", "py");
484        editor.cursor = (0, 5);
485
486        // Type a character
487        let _ = editor.update(&Message::CharacterInput('!'));
488        assert_eq!(editor.buffer.line(0), "hello!");
489        assert_eq!(editor.cursor, (0, 6));
490
491        // Undo should remove it (but first end the grouping)
492        editor.history.end_group();
493        let _ = editor.update(&Message::Undo);
494        assert_eq!(editor.buffer.line(0), "hello");
495        assert_eq!(editor.cursor, (0, 5));
496    }
497
498    #[test]
499    fn test_undo_redo_char_insert() {
500        let mut editor = CodeEditor::new("hello", "py");
501        editor.cursor = (0, 5);
502
503        // Type a character
504        let _ = editor.update(&Message::CharacterInput('!'));
505        editor.history.end_group();
506
507        // Undo
508        let _ = editor.update(&Message::Undo);
509        assert_eq!(editor.buffer.line(0), "hello");
510
511        // Redo
512        let _ = editor.update(&Message::Redo);
513        assert_eq!(editor.buffer.line(0), "hello!");
514        assert_eq!(editor.cursor, (0, 6));
515    }
516
517    #[test]
518    fn test_undo_backspace() {
519        let mut editor = CodeEditor::new("hello", "py");
520        editor.cursor = (0, 5);
521
522        // Backspace
523        let _ = editor.update(&Message::Backspace);
524        assert_eq!(editor.buffer.line(0), "hell");
525        assert_eq!(editor.cursor, (0, 4));
526
527        // Undo
528        let _ = editor.update(&Message::Undo);
529        assert_eq!(editor.buffer.line(0), "hello");
530        assert_eq!(editor.cursor, (0, 5));
531    }
532
533    #[test]
534    fn test_undo_newline() {
535        let mut editor = CodeEditor::new("hello world", "py");
536        editor.cursor = (0, 5);
537
538        // Insert newline
539        let _ = editor.update(&Message::Enter);
540        assert_eq!(editor.buffer.line(0), "hello");
541        assert_eq!(editor.buffer.line(1), " world");
542        assert_eq!(editor.cursor, (1, 0));
543
544        // Undo
545        let _ = editor.update(&Message::Undo);
546        assert_eq!(editor.buffer.line(0), "hello world");
547        assert_eq!(editor.cursor, (0, 5));
548    }
549
550    #[test]
551    fn test_undo_grouped_typing() {
552        let mut editor = CodeEditor::new("hello", "py");
553        editor.cursor = (0, 5);
554
555        // Type multiple characters (they should be grouped)
556        let _ = editor.update(&Message::CharacterInput(' '));
557        let _ = editor.update(&Message::CharacterInput('w'));
558        let _ = editor.update(&Message::CharacterInput('o'));
559        let _ = editor.update(&Message::CharacterInput('r'));
560        let _ = editor.update(&Message::CharacterInput('l'));
561        let _ = editor.update(&Message::CharacterInput('d'));
562
563        assert_eq!(editor.buffer.line(0), "hello world");
564
565        // End the group
566        editor.history.end_group();
567
568        // Single undo should remove all grouped characters
569        let _ = editor.update(&Message::Undo);
570        assert_eq!(editor.buffer.line(0), "hello");
571        assert_eq!(editor.cursor, (0, 5));
572    }
573
574    #[test]
575    fn test_navigation_ends_grouping() {
576        let mut editor = CodeEditor::new("hello", "py");
577        editor.cursor = (0, 5);
578
579        // Type a character (starts grouping)
580        let _ = editor.update(&Message::CharacterInput('!'));
581        assert!(editor.is_grouping);
582
583        // Move cursor (ends grouping)
584        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
585        assert!(!editor.is_grouping);
586
587        // Type another character (starts new group)
588        let _ = editor.update(&Message::CharacterInput('?'));
589        assert!(editor.is_grouping);
590
591        editor.history.end_group();
592
593        // Two separate undo operations
594        let _ = editor.update(&Message::Undo);
595        assert_eq!(editor.buffer.line(0), "hello!");
596
597        let _ = editor.update(&Message::Undo);
598        assert_eq!(editor.buffer.line(0), "hello");
599    }
600
601    #[test]
602    fn test_multiple_undo_redo() {
603        let mut editor = CodeEditor::new("a", "py");
604        editor.cursor = (0, 1);
605
606        // Make several changes
607        let _ = editor.update(&Message::CharacterInput('b'));
608        editor.history.end_group();
609
610        let _ = editor.update(&Message::CharacterInput('c'));
611        editor.history.end_group();
612
613        let _ = editor.update(&Message::CharacterInput('d'));
614        editor.history.end_group();
615
616        assert_eq!(editor.buffer.line(0), "abcd");
617
618        // Undo all
619        let _ = editor.update(&Message::Undo);
620        assert_eq!(editor.buffer.line(0), "abc");
621
622        let _ = editor.update(&Message::Undo);
623        assert_eq!(editor.buffer.line(0), "ab");
624
625        let _ = editor.update(&Message::Undo);
626        assert_eq!(editor.buffer.line(0), "a");
627
628        // Redo all
629        let _ = editor.update(&Message::Redo);
630        assert_eq!(editor.buffer.line(0), "ab");
631
632        let _ = editor.update(&Message::Redo);
633        assert_eq!(editor.buffer.line(0), "abc");
634
635        let _ = editor.update(&Message::Redo);
636        assert_eq!(editor.buffer.line(0), "abcd");
637    }
638
639    #[test]
640    fn test_delete_key_with_selection() {
641        let mut editor = CodeEditor::new("hello world", "py");
642        editor.selection_start = Some((0, 0));
643        editor.selection_end = Some((0, 5));
644        editor.cursor = (0, 5);
645
646        let _ = editor.update(&Message::Delete);
647
648        assert_eq!(editor.buffer.line(0), " world");
649        assert_eq!(editor.cursor, (0, 0));
650        assert_eq!(editor.selection_start, None);
651        assert_eq!(editor.selection_end, None);
652    }
653
654    #[test]
655    fn test_delete_key_without_selection() {
656        let mut editor = CodeEditor::new("hello", "py");
657        editor.cursor = (0, 0);
658
659        let _ = editor.update(&Message::Delete);
660
661        // Should delete the 'h'
662        assert_eq!(editor.buffer.line(0), "ello");
663        assert_eq!(editor.cursor, (0, 0));
664    }
665
666    #[test]
667    fn test_backspace_with_selection() {
668        let mut editor = CodeEditor::new("hello world", "py");
669        editor.selection_start = Some((0, 6));
670        editor.selection_end = Some((0, 11));
671        editor.cursor = (0, 11);
672
673        let _ = editor.update(&Message::Backspace);
674
675        assert_eq!(editor.buffer.line(0), "hello ");
676        assert_eq!(editor.cursor, (0, 6));
677        assert_eq!(editor.selection_start, None);
678        assert_eq!(editor.selection_end, None);
679    }
680
681    #[test]
682    fn test_backspace_without_selection() {
683        let mut editor = CodeEditor::new("hello", "py");
684        editor.cursor = (0, 5);
685
686        let _ = editor.update(&Message::Backspace);
687
688        // Should delete the 'o'
689        assert_eq!(editor.buffer.line(0), "hell");
690        assert_eq!(editor.cursor, (0, 4));
691    }
692
693    #[test]
694    fn test_delete_multiline_selection() {
695        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
696        editor.selection_start = Some((0, 2));
697        editor.selection_end = Some((2, 2));
698        editor.cursor = (2, 2);
699
700        let _ = editor.update(&Message::Delete);
701
702        assert_eq!(editor.buffer.line(0), "line3");
703        assert_eq!(editor.cursor, (0, 2));
704        assert_eq!(editor.selection_start, None);
705    }
706}