iced_code_editor/canvas_editor/
update.rs

1//! Message handling and update logic.
2
3use iced::Task;
4use iced::widget::operation::{focus, select_all};
5
6use super::command::{
7    Command, CompositeCommand, DeleteCharCommand, DeleteForwardCommand,
8    InsertCharCommand, InsertNewlineCommand, ReplaceTextCommand,
9};
10use super::{CURSOR_BLINK_INTERVAL, CodeEditor, Message};
11
12impl CodeEditor {
13    /// Updates the editor state based on messages and returns scroll commands.
14    ///
15    /// # Arguments
16    ///
17    /// * `message` - The message to process
18    ///
19    /// # Returns
20    ///
21    /// A Task that may contain scroll commands to keep cursor visible
22    pub fn update(&mut self, message: &Message) -> Task<Message> {
23        match message {
24            Message::CharacterInput(ch) => {
25                // Start grouping if not already grouping (for smart undo)
26                if !self.is_grouping {
27                    self.history.begin_group("Typing");
28                    self.is_grouping = true;
29                }
30
31                let (line, col) = self.cursor;
32                let mut cmd =
33                    InsertCharCommand::new(line, col, *ch, self.cursor);
34                cmd.execute(&mut self.buffer, &mut self.cursor);
35                self.history.push(Box::new(cmd));
36
37                self.reset_cursor_blink();
38                self.refresh_search_matches_if_needed();
39                self.cache.clear();
40                Task::none()
41            }
42            Message::Backspace => {
43                // End grouping on backspace (separate from typing)
44                if self.is_grouping {
45                    self.history.end_group();
46                    self.is_grouping = false;
47                }
48
49                // Check if there's a selection - if so, delete it instead
50                if self.selection_start.is_some()
51                    && self.selection_end.is_some()
52                {
53                    self.delete_selection();
54                    self.reset_cursor_blink();
55                    self.refresh_search_matches_if_needed();
56                    self.cache.clear();
57                    return self.scroll_to_cursor();
58                }
59
60                // No selection - perform normal backspace
61                let (line, col) = self.cursor;
62                let mut cmd = DeleteCharCommand::new(
63                    &self.buffer,
64                    line,
65                    col,
66                    self.cursor,
67                );
68                cmd.execute(&mut self.buffer, &mut self.cursor);
69                self.history.push(Box::new(cmd));
70
71                self.reset_cursor_blink();
72                self.refresh_search_matches_if_needed();
73                self.cache.clear();
74                self.scroll_to_cursor()
75            }
76            Message::Delete => {
77                // End grouping on delete
78                if self.is_grouping {
79                    self.history.end_group();
80                    self.is_grouping = false;
81                }
82
83                // Check if there's a selection - if so, delete it instead
84                if self.selection_start.is_some()
85                    && self.selection_end.is_some()
86                {
87                    self.delete_selection();
88                    self.reset_cursor_blink();
89                    self.refresh_search_matches_if_needed();
90                    self.cache.clear();
91                    return self.scroll_to_cursor();
92                }
93
94                // No selection - perform normal forward delete
95                let (line, col) = self.cursor;
96                let mut cmd = DeleteForwardCommand::new(
97                    &self.buffer,
98                    line,
99                    col,
100                    self.cursor,
101                );
102                cmd.execute(&mut self.buffer, &mut self.cursor);
103                self.history.push(Box::new(cmd));
104
105                self.reset_cursor_blink();
106                self.refresh_search_matches_if_needed();
107                self.cache.clear();
108                Task::none()
109            }
110            Message::Enter => {
111                // End grouping on enter
112                if self.is_grouping {
113                    self.history.end_group();
114                    self.is_grouping = false;
115                }
116
117                let (line, col) = self.cursor;
118                let mut cmd = InsertNewlineCommand::new(line, col, self.cursor);
119                cmd.execute(&mut self.buffer, &mut self.cursor);
120                self.history.push(Box::new(cmd));
121
122                self.reset_cursor_blink();
123                self.refresh_search_matches_if_needed();
124                self.cache.clear();
125                self.scroll_to_cursor()
126            }
127            Message::Tab => {
128                // Insert 4 spaces for Tab
129                // Start grouping if not already grouping
130                if !self.is_grouping {
131                    self.history.begin_group("Tab");
132                    self.is_grouping = true;
133                }
134
135                let (line, col) = self.cursor;
136                // Insert 4 spaces
137                for i in 0..4 {
138                    let current_col = col + i;
139                    let mut cmd = InsertCharCommand::new(
140                        line,
141                        current_col,
142                        ' ',
143                        (line, current_col),
144                    );
145                    cmd.execute(&mut self.buffer, &mut self.cursor);
146                    self.history.push(Box::new(cmd));
147                }
148
149                self.reset_cursor_blink();
150                self.cache.clear();
151                Task::none()
152            }
153            Message::ArrowKey(direction, shift_pressed) => {
154                // End grouping on navigation
155                if self.is_grouping {
156                    self.history.end_group();
157                    self.is_grouping = false;
158                }
159
160                if *shift_pressed {
161                    // Start selection if not already started
162                    if self.selection_start.is_none() {
163                        self.selection_start = Some(self.cursor);
164                    }
165                    self.move_cursor(*direction);
166                    self.selection_end = Some(self.cursor);
167                } else {
168                    // Clear selection and move cursor
169                    self.clear_selection();
170                    self.move_cursor(*direction);
171                }
172                self.reset_cursor_blink();
173                self.cache.clear();
174                self.scroll_to_cursor()
175            }
176            Message::MouseClick(point) => {
177                // Capture focus when clicked
178                super::FOCUSED_EDITOR_ID.store(
179                    self.editor_id,
180                    std::sync::atomic::Ordering::Relaxed,
181                );
182
183                // End grouping on mouse click
184                if self.is_grouping {
185                    self.history.end_group();
186                    self.is_grouping = false;
187                }
188
189                self.handle_mouse_click(*point);
190                self.reset_cursor_blink();
191                // Clear selection on click
192                self.clear_selection();
193                self.is_dragging = true;
194                self.selection_start = Some(self.cursor);
195
196                // Gain canvas focus
197                self.has_canvas_focus = true;
198                self.show_cursor = true;
199
200                Task::none()
201            }
202            Message::MouseDrag(point) => {
203                if self.is_dragging {
204                    self.handle_mouse_drag(*point);
205                    self.cache.clear();
206                }
207                Task::none()
208            }
209            Message::MouseRelease => {
210                self.is_dragging = false;
211                Task::none()
212            }
213            Message::Copy => self.copy_selection(),
214            Message::Paste(text) => {
215                // End grouping on paste
216                if self.is_grouping {
217                    self.history.end_group();
218                    self.is_grouping = false;
219                }
220
221                // If text is empty, we need to read from clipboard
222                if text.is_empty() {
223                    // Return a task that reads clipboard and chains to paste
224                    iced::clipboard::read().and_then(|clipboard_text| {
225                        Task::done(Message::Paste(clipboard_text))
226                    })
227                } else {
228                    // We have the text, paste it
229                    self.paste_text(text);
230                    self.refresh_search_matches_if_needed();
231                    self.cache.clear();
232                    self.scroll_to_cursor()
233                }
234            }
235            Message::DeleteSelection => {
236                // End grouping on delete selection
237                if self.is_grouping {
238                    self.history.end_group();
239                    self.is_grouping = false;
240                }
241
242                // Delete selected text
243                self.delete_selection();
244                self.reset_cursor_blink();
245                self.cache.clear();
246                self.scroll_to_cursor()
247            }
248            Message::Tick => {
249                // Handle cursor blinking only if editor has focus
250                if self.is_focused()
251                    && self.has_canvas_focus
252                    && self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL
253                {
254                    self.cursor_visible = !self.cursor_visible;
255                    self.last_blink = std::time::Instant::now();
256                    self.cache.clear();
257                }
258
259                // Hide cursor if canvas doesn't have focus
260                if !self.has_canvas_focus {
261                    self.show_cursor = false;
262                }
263
264                Task::none()
265            }
266            Message::PageUp => {
267                self.page_up();
268                self.reset_cursor_blink();
269                self.scroll_to_cursor()
270            }
271            Message::PageDown => {
272                self.page_down();
273                self.reset_cursor_blink();
274                self.scroll_to_cursor()
275            }
276            Message::Home(shift_pressed) => {
277                if *shift_pressed {
278                    // Start selection if not already started
279                    if self.selection_start.is_none() {
280                        self.selection_start = Some(self.cursor);
281                    }
282                    self.cursor.1 = 0; // Move to start of line
283                    self.selection_end = Some(self.cursor);
284                } else {
285                    // Clear selection and move cursor
286                    self.clear_selection();
287                    self.cursor.1 = 0;
288                }
289                self.reset_cursor_blink();
290                self.cache.clear();
291                Task::none()
292            }
293            Message::End(shift_pressed) => {
294                let line = self.cursor.0;
295                let line_len = self.buffer.line_len(line);
296
297                if *shift_pressed {
298                    // Start selection if not already started
299                    if self.selection_start.is_none() {
300                        self.selection_start = Some(self.cursor);
301                    }
302                    self.cursor.1 = line_len; // Move to end of line
303                    self.selection_end = Some(self.cursor);
304                } else {
305                    // Clear selection and move cursor
306                    self.clear_selection();
307                    self.cursor.1 = line_len;
308                }
309                self.reset_cursor_blink();
310                self.cache.clear();
311                Task::none()
312            }
313            Message::CtrlHome => {
314                // Move cursor to the beginning of the document
315                self.clear_selection();
316                self.cursor = (0, 0);
317                self.reset_cursor_blink();
318                self.cache.clear();
319                self.scroll_to_cursor()
320            }
321            Message::CtrlEnd => {
322                // Move cursor to the end of the document
323                self.clear_selection();
324                let last_line = self.buffer.line_count().saturating_sub(1);
325                let last_col = self.buffer.line_len(last_line);
326                self.cursor = (last_line, last_col);
327                self.reset_cursor_blink();
328                self.cache.clear();
329                self.scroll_to_cursor()
330            }
331            Message::Scrolled(viewport) => {
332                // Track viewport scroll position, height, and width
333                self.viewport_scroll = viewport.absolute_offset().y;
334                let new_height = viewport.bounds().height;
335                let new_width = viewport.bounds().width;
336                // Clear cache when viewport dimensions change significantly
337                // to ensure proper redraw (e.g., window resize)
338                if (self.viewport_height - new_height).abs() > 1.0
339                    || (self.viewport_width - new_width).abs() > 1.0
340                {
341                    self.cache.clear();
342                }
343                self.viewport_height = new_height;
344                self.viewport_width = new_width;
345                Task::none()
346            }
347            Message::Undo => {
348                // End any current grouping before undoing
349                if self.is_grouping {
350                    self.history.end_group();
351                    self.is_grouping = false;
352                }
353
354                if self.history.undo(&mut self.buffer, &mut self.cursor) {
355                    self.clear_selection();
356                    self.reset_cursor_blink();
357                    self.refresh_search_matches_if_needed();
358                    self.cache.clear();
359                    self.scroll_to_cursor()
360                } else {
361                    Task::none()
362                }
363            }
364            Message::Redo => {
365                if self.history.redo(&mut self.buffer, &mut self.cursor) {
366                    self.clear_selection();
367                    self.reset_cursor_blink();
368                    self.refresh_search_matches_if_needed();
369                    self.cache.clear();
370                    self.scroll_to_cursor()
371                } else {
372                    Task::none()
373                }
374            }
375            Message::OpenSearch => {
376                self.search_state.open_search();
377                self.cache.clear();
378
379                // Focus the search input and select all text if any
380                Task::batch([
381                    focus(self.search_state.search_input_id.clone()),
382                    select_all(self.search_state.search_input_id.clone()),
383                ])
384            }
385            Message::OpenSearchReplace => {
386                self.search_state.open_replace();
387                self.cache.clear();
388
389                // Focus the search input and select all text if any
390                Task::batch([
391                    focus(self.search_state.search_input_id.clone()),
392                    select_all(self.search_state.search_input_id.clone()),
393                ])
394            }
395            Message::CloseSearch => {
396                self.search_state.close();
397                self.cache.clear();
398                Task::none()
399            }
400            Message::SearchQueryChanged(query) => {
401                self.search_state.set_query(query.clone(), &self.buffer);
402                self.cache.clear();
403
404                // Move cursor to first match if any
405                if let Some(match_pos) = self.search_state.current_match() {
406                    self.cursor = (match_pos.line, match_pos.col);
407                    self.clear_selection();
408                    return self.scroll_to_cursor();
409                }
410                Task::none()
411            }
412            Message::ReplaceQueryChanged(replace_text) => {
413                self.search_state.set_replace_with(replace_text.clone());
414                Task::none()
415            }
416            Message::ToggleCaseSensitive => {
417                self.search_state.toggle_case_sensitive(&self.buffer);
418                self.cache.clear();
419
420                // Move cursor to first match if any
421                if let Some(match_pos) = self.search_state.current_match() {
422                    self.cursor = (match_pos.line, match_pos.col);
423                    self.clear_selection();
424                    return self.scroll_to_cursor();
425                }
426                Task::none()
427            }
428            Message::FindNext => {
429                if !self.search_state.matches.is_empty() {
430                    self.search_state.next_match();
431                    if let Some(match_pos) = self.search_state.current_match() {
432                        self.cursor = (match_pos.line, match_pos.col);
433                        self.clear_selection();
434                        self.cache.clear();
435                        return self.scroll_to_cursor();
436                    }
437                }
438                Task::none()
439            }
440            Message::FindPrevious => {
441                if !self.search_state.matches.is_empty() {
442                    self.search_state.previous_match();
443                    if let Some(match_pos) = self.search_state.current_match() {
444                        self.cursor = (match_pos.line, match_pos.col);
445                        self.clear_selection();
446                        self.cache.clear();
447                        return self.scroll_to_cursor();
448                    }
449                }
450                Task::none()
451            }
452            Message::ReplaceNext => {
453                // Replace current match and move to next
454                if let Some(match_pos) = self.search_state.current_match() {
455                    let query_len = self.search_state.query.chars().count();
456                    let replace_text = self.search_state.replace_with.clone();
457
458                    // Create and execute replace command
459                    let mut cmd = ReplaceTextCommand::new(
460                        &self.buffer,
461                        (match_pos.line, match_pos.col),
462                        query_len,
463                        replace_text,
464                        self.cursor,
465                    );
466                    cmd.execute(&mut self.buffer, &mut self.cursor);
467                    self.history.push(Box::new(cmd));
468
469                    // Update matches after replacement
470                    self.search_state.update_matches(&self.buffer);
471
472                    // Move to next match if available
473                    if !self.search_state.matches.is_empty()
474                        && let Some(next_match) =
475                            self.search_state.current_match()
476                    {
477                        self.cursor = (next_match.line, next_match.col);
478                    }
479
480                    self.clear_selection();
481                    self.cache.clear();
482                    return self.scroll_to_cursor();
483                }
484                Task::none()
485            }
486            Message::ReplaceAll => {
487                // Replace all matches in reverse order (to preserve positions)
488                if !self.search_state.matches.is_empty() {
489                    let query_len = self.search_state.query.chars().count();
490                    let replace_text = self.search_state.replace_with.clone();
491
492                    // Create composite command for undo
493                    let mut composite =
494                        CompositeCommand::new("Replace All".to_string());
495
496                    // Process matches in reverse order
497                    for match_pos in self.search_state.matches.iter().rev() {
498                        let cmd = ReplaceTextCommand::new(
499                            &self.buffer,
500                            (match_pos.line, match_pos.col),
501                            query_len,
502                            replace_text.clone(),
503                            self.cursor,
504                        );
505                        composite.add(Box::new(cmd));
506                    }
507
508                    // Execute all replacements
509                    composite.execute(&mut self.buffer, &mut self.cursor);
510                    self.history.push(Box::new(composite));
511
512                    // Update matches (should be empty now)
513                    self.search_state.update_matches(&self.buffer);
514
515                    self.clear_selection();
516                    self.cache.clear();
517                    return self.scroll_to_cursor();
518                }
519                Task::none()
520            }
521            Message::SearchDialogTab => {
522                // Cycle focus forward (Search → Replace → Search)
523                self.search_state.focus_next_field();
524
525                // Focus the appropriate input based on new focused_field
526                match self.search_state.focused_field {
527                    crate::canvas_editor::search::SearchFocusedField::Search => {
528                        focus(self.search_state.search_input_id.clone())
529                    }
530                    crate::canvas_editor::search::SearchFocusedField::Replace => {
531                        focus(self.search_state.replace_input_id.clone())
532                    }
533                }
534            }
535            Message::SearchDialogShiftTab => {
536                // Cycle focus backward (Replace → Search → Replace)
537                self.search_state.focus_previous_field();
538
539                // Focus the appropriate input based on new focused_field
540                match self.search_state.focused_field {
541                    crate::canvas_editor::search::SearchFocusedField::Search => {
542                        focus(self.search_state.search_input_id.clone())
543                    }
544                    crate::canvas_editor::search::SearchFocusedField::Replace => {
545                        focus(self.search_state.replace_input_id.clone())
546                    }
547                }
548            }
549            Message::CanvasFocusGained => {
550                self.has_canvas_focus = true;
551                self.show_cursor = true;
552                self.reset_cursor_blink();
553                self.cache.clear();
554                Task::none()
555            }
556            Message::CanvasFocusLost => {
557                self.has_canvas_focus = false;
558                self.show_cursor = false;
559                self.cache.clear();
560                Task::none()
561            }
562        }
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::canvas_editor::ArrowDirection;
570
571    #[test]
572    fn test_new_canvas_editor() {
573        let editor = CodeEditor::new("line1\nline2", "py");
574        assert_eq!(editor.cursor, (0, 0));
575    }
576
577    #[test]
578    fn test_home_key() {
579        let mut editor = CodeEditor::new("hello world", "py");
580        editor.cursor = (0, 5); // Move to middle of line
581        let _ = editor.update(&Message::Home(false));
582        assert_eq!(editor.cursor, (0, 0));
583    }
584
585    #[test]
586    fn test_end_key() {
587        let mut editor = CodeEditor::new("hello world", "py");
588        editor.cursor = (0, 0);
589        let _ = editor.update(&Message::End(false));
590        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
591    }
592
593    #[test]
594    fn test_arrow_key_with_shift_creates_selection() {
595        let mut editor = CodeEditor::new("hello world", "py");
596        editor.cursor = (0, 0);
597
598        // Shift+Right should start selection
599        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
600        assert!(editor.selection_start.is_some());
601        assert!(editor.selection_end.is_some());
602    }
603
604    #[test]
605    fn test_arrow_key_without_shift_clears_selection() {
606        let mut editor = CodeEditor::new("hello world", "py");
607        editor.selection_start = Some((0, 0));
608        editor.selection_end = Some((0, 5));
609
610        // Regular arrow key should clear selection
611        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
612        assert_eq!(editor.selection_start, None);
613        assert_eq!(editor.selection_end, None);
614    }
615
616    #[test]
617    fn test_typing_with_selection() {
618        let mut editor = CodeEditor::new("hello world", "py");
619        editor.selection_start = Some((0, 0));
620        editor.selection_end = Some((0, 5));
621
622        let _ = editor.update(&Message::CharacterInput('X'));
623        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
624        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
625        assert_eq!(editor.buffer.line(0), "Xhello world");
626    }
627
628    #[test]
629    fn test_ctrl_home() {
630        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
631        editor.cursor = (2, 5); // Start at line 3, column 5
632        let _ = editor.update(&Message::CtrlHome);
633        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
634    }
635
636    #[test]
637    fn test_ctrl_end() {
638        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
639        editor.cursor = (0, 0); // Start at beginning
640        let _ = editor.update(&Message::CtrlEnd);
641        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
642    }
643
644    #[test]
645    fn test_ctrl_home_clears_selection() {
646        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
647        editor.cursor = (2, 5);
648        editor.selection_start = Some((0, 0));
649        editor.selection_end = Some((2, 5));
650
651        let _ = editor.update(&Message::CtrlHome);
652        assert_eq!(editor.cursor, (0, 0));
653        assert_eq!(editor.selection_start, None);
654        assert_eq!(editor.selection_end, None);
655    }
656
657    #[test]
658    fn test_ctrl_end_clears_selection() {
659        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
660        editor.cursor = (0, 0);
661        editor.selection_start = Some((0, 0));
662        editor.selection_end = Some((1, 3));
663
664        let _ = editor.update(&Message::CtrlEnd);
665        assert_eq!(editor.cursor, (2, 5));
666        assert_eq!(editor.selection_start, None);
667        assert_eq!(editor.selection_end, None);
668    }
669
670    #[test]
671    fn test_delete_selection_message() {
672        let mut editor = CodeEditor::new("hello world", "py");
673        editor.cursor = (0, 0);
674        editor.selection_start = Some((0, 0));
675        editor.selection_end = Some((0, 5));
676
677        let _ = editor.update(&Message::DeleteSelection);
678        assert_eq!(editor.buffer.line(0), " world");
679        assert_eq!(editor.cursor, (0, 0));
680        assert_eq!(editor.selection_start, None);
681        assert_eq!(editor.selection_end, None);
682    }
683
684    #[test]
685    fn test_delete_selection_multiline() {
686        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
687        editor.cursor = (0, 2);
688        editor.selection_start = Some((0, 2));
689        editor.selection_end = Some((2, 2));
690
691        let _ = editor.update(&Message::DeleteSelection);
692        assert_eq!(editor.buffer.line(0), "line3");
693        assert_eq!(editor.cursor, (0, 2));
694        assert_eq!(editor.selection_start, None);
695    }
696
697    #[test]
698    fn test_delete_selection_no_selection() {
699        let mut editor = CodeEditor::new("hello world", "py");
700        editor.cursor = (0, 5);
701
702        let _ = editor.update(&Message::DeleteSelection);
703        // Should do nothing if there's no selection
704        assert_eq!(editor.buffer.line(0), "hello world");
705        assert_eq!(editor.cursor, (0, 5));
706    }
707
708    #[test]
709    fn test_undo_char_insert() {
710        let mut editor = CodeEditor::new("hello", "py");
711        editor.cursor = (0, 5);
712
713        // Type a character
714        let _ = editor.update(&Message::CharacterInput('!'));
715        assert_eq!(editor.buffer.line(0), "hello!");
716        assert_eq!(editor.cursor, (0, 6));
717
718        // Undo should remove it (but first end the grouping)
719        editor.history.end_group();
720        let _ = editor.update(&Message::Undo);
721        assert_eq!(editor.buffer.line(0), "hello");
722        assert_eq!(editor.cursor, (0, 5));
723    }
724
725    #[test]
726    fn test_undo_redo_char_insert() {
727        let mut editor = CodeEditor::new("hello", "py");
728        editor.cursor = (0, 5);
729
730        // Type a character
731        let _ = editor.update(&Message::CharacterInput('!'));
732        editor.history.end_group();
733
734        // Undo
735        let _ = editor.update(&Message::Undo);
736        assert_eq!(editor.buffer.line(0), "hello");
737
738        // Redo
739        let _ = editor.update(&Message::Redo);
740        assert_eq!(editor.buffer.line(0), "hello!");
741        assert_eq!(editor.cursor, (0, 6));
742    }
743
744    #[test]
745    fn test_undo_backspace() {
746        let mut editor = CodeEditor::new("hello", "py");
747        editor.cursor = (0, 5);
748
749        // Backspace
750        let _ = editor.update(&Message::Backspace);
751        assert_eq!(editor.buffer.line(0), "hell");
752        assert_eq!(editor.cursor, (0, 4));
753
754        // Undo
755        let _ = editor.update(&Message::Undo);
756        assert_eq!(editor.buffer.line(0), "hello");
757        assert_eq!(editor.cursor, (0, 5));
758    }
759
760    #[test]
761    fn test_undo_newline() {
762        let mut editor = CodeEditor::new("hello world", "py");
763        editor.cursor = (0, 5);
764
765        // Insert newline
766        let _ = editor.update(&Message::Enter);
767        assert_eq!(editor.buffer.line(0), "hello");
768        assert_eq!(editor.buffer.line(1), " world");
769        assert_eq!(editor.cursor, (1, 0));
770
771        // Undo
772        let _ = editor.update(&Message::Undo);
773        assert_eq!(editor.buffer.line(0), "hello world");
774        assert_eq!(editor.cursor, (0, 5));
775    }
776
777    #[test]
778    fn test_undo_grouped_typing() {
779        let mut editor = CodeEditor::new("hello", "py");
780        editor.cursor = (0, 5);
781
782        // Type multiple characters (they should be grouped)
783        let _ = editor.update(&Message::CharacterInput(' '));
784        let _ = editor.update(&Message::CharacterInput('w'));
785        let _ = editor.update(&Message::CharacterInput('o'));
786        let _ = editor.update(&Message::CharacterInput('r'));
787        let _ = editor.update(&Message::CharacterInput('l'));
788        let _ = editor.update(&Message::CharacterInput('d'));
789
790        assert_eq!(editor.buffer.line(0), "hello world");
791
792        // End the group
793        editor.history.end_group();
794
795        // Single undo should remove all grouped characters
796        let _ = editor.update(&Message::Undo);
797        assert_eq!(editor.buffer.line(0), "hello");
798        assert_eq!(editor.cursor, (0, 5));
799    }
800
801    #[test]
802    fn test_navigation_ends_grouping() {
803        let mut editor = CodeEditor::new("hello", "py");
804        editor.cursor = (0, 5);
805
806        // Type a character (starts grouping)
807        let _ = editor.update(&Message::CharacterInput('!'));
808        assert!(editor.is_grouping);
809
810        // Move cursor (ends grouping)
811        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
812        assert!(!editor.is_grouping);
813
814        // Type another character (starts new group)
815        let _ = editor.update(&Message::CharacterInput('?'));
816        assert!(editor.is_grouping);
817
818        editor.history.end_group();
819
820        // Two separate undo operations
821        let _ = editor.update(&Message::Undo);
822        assert_eq!(editor.buffer.line(0), "hello!");
823
824        let _ = editor.update(&Message::Undo);
825        assert_eq!(editor.buffer.line(0), "hello");
826    }
827
828    #[test]
829    fn test_multiple_undo_redo() {
830        let mut editor = CodeEditor::new("a", "py");
831        editor.cursor = (0, 1);
832
833        // Make several changes
834        let _ = editor.update(&Message::CharacterInput('b'));
835        editor.history.end_group();
836
837        let _ = editor.update(&Message::CharacterInput('c'));
838        editor.history.end_group();
839
840        let _ = editor.update(&Message::CharacterInput('d'));
841        editor.history.end_group();
842
843        assert_eq!(editor.buffer.line(0), "abcd");
844
845        // Undo all
846        let _ = editor.update(&Message::Undo);
847        assert_eq!(editor.buffer.line(0), "abc");
848
849        let _ = editor.update(&Message::Undo);
850        assert_eq!(editor.buffer.line(0), "ab");
851
852        let _ = editor.update(&Message::Undo);
853        assert_eq!(editor.buffer.line(0), "a");
854
855        // Redo all
856        let _ = editor.update(&Message::Redo);
857        assert_eq!(editor.buffer.line(0), "ab");
858
859        let _ = editor.update(&Message::Redo);
860        assert_eq!(editor.buffer.line(0), "abc");
861
862        let _ = editor.update(&Message::Redo);
863        assert_eq!(editor.buffer.line(0), "abcd");
864    }
865
866    #[test]
867    fn test_delete_key_with_selection() {
868        let mut editor = CodeEditor::new("hello world", "py");
869        editor.selection_start = Some((0, 0));
870        editor.selection_end = Some((0, 5));
871        editor.cursor = (0, 5);
872
873        let _ = editor.update(&Message::Delete);
874
875        assert_eq!(editor.buffer.line(0), " world");
876        assert_eq!(editor.cursor, (0, 0));
877        assert_eq!(editor.selection_start, None);
878        assert_eq!(editor.selection_end, None);
879    }
880
881    #[test]
882    fn test_delete_key_without_selection() {
883        let mut editor = CodeEditor::new("hello", "py");
884        editor.cursor = (0, 0);
885
886        let _ = editor.update(&Message::Delete);
887
888        // Should delete the 'h'
889        assert_eq!(editor.buffer.line(0), "ello");
890        assert_eq!(editor.cursor, (0, 0));
891    }
892
893    #[test]
894    fn test_backspace_with_selection() {
895        let mut editor = CodeEditor::new("hello world", "py");
896        editor.selection_start = Some((0, 6));
897        editor.selection_end = Some((0, 11));
898        editor.cursor = (0, 11);
899
900        let _ = editor.update(&Message::Backspace);
901
902        assert_eq!(editor.buffer.line(0), "hello ");
903        assert_eq!(editor.cursor, (0, 6));
904        assert_eq!(editor.selection_start, None);
905        assert_eq!(editor.selection_end, None);
906    }
907
908    #[test]
909    fn test_backspace_without_selection() {
910        let mut editor = CodeEditor::new("hello", "py");
911        editor.cursor = (0, 5);
912
913        let _ = editor.update(&Message::Backspace);
914
915        // Should delete the 'o'
916        assert_eq!(editor.buffer.line(0), "hell");
917        assert_eq!(editor.cursor, (0, 4));
918    }
919
920    #[test]
921    fn test_delete_multiline_selection() {
922        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
923        editor.selection_start = Some((0, 2));
924        editor.selection_end = Some((2, 2));
925        editor.cursor = (2, 2);
926
927        let _ = editor.update(&Message::Delete);
928
929        assert_eq!(editor.buffer.line(0), "line3");
930        assert_eq!(editor.cursor, (0, 2));
931        assert_eq!(editor.selection_start, None);
932    }
933
934    #[test]
935    fn test_canvas_focus_gained() {
936        let mut editor = CodeEditor::new("hello world", "py");
937        assert!(!editor.has_canvas_focus);
938        assert!(!editor.show_cursor);
939
940        let _ = editor.update(&Message::CanvasFocusGained);
941
942        assert!(editor.has_canvas_focus);
943        assert!(editor.show_cursor);
944    }
945
946    #[test]
947    fn test_canvas_focus_lost() {
948        let mut editor = CodeEditor::new("hello world", "py");
949        editor.has_canvas_focus = true;
950        editor.show_cursor = true;
951
952        let _ = editor.update(&Message::CanvasFocusLost);
953
954        assert!(!editor.has_canvas_focus);
955        assert!(!editor.show_cursor);
956    }
957
958    #[test]
959    fn test_mouse_click_gains_focus() {
960        let mut editor = CodeEditor::new("hello world", "py");
961        editor.has_canvas_focus = false;
962        editor.show_cursor = false;
963
964        let _ =
965            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
966
967        assert!(editor.has_canvas_focus);
968        assert!(editor.show_cursor);
969    }
970}