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                Task::none()
196            }
197            Message::MouseDrag(point) => {
198                if self.is_dragging {
199                    self.handle_mouse_drag(*point);
200                    self.cache.clear();
201                }
202                Task::none()
203            }
204            Message::MouseRelease => {
205                self.is_dragging = false;
206                Task::none()
207            }
208            Message::Copy => self.copy_selection(),
209            Message::Paste(text) => {
210                // End grouping on paste
211                if self.is_grouping {
212                    self.history.end_group();
213                    self.is_grouping = false;
214                }
215
216                // If text is empty, we need to read from clipboard
217                if text.is_empty() {
218                    // Return a task that reads clipboard and chains to paste
219                    iced::clipboard::read().and_then(|clipboard_text| {
220                        Task::done(Message::Paste(clipboard_text))
221                    })
222                } else {
223                    // We have the text, paste it
224                    self.paste_text(text);
225                    self.refresh_search_matches_if_needed();
226                    self.cache.clear();
227                    self.scroll_to_cursor()
228                }
229            }
230            Message::DeleteSelection => {
231                // End grouping on delete selection
232                if self.is_grouping {
233                    self.history.end_group();
234                    self.is_grouping = false;
235                }
236
237                // Delete selected text
238                self.delete_selection();
239                self.reset_cursor_blink();
240                self.cache.clear();
241                self.scroll_to_cursor()
242            }
243            Message::Tick => {
244                // Handle cursor blinking
245                if self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL {
246                    self.cursor_visible = !self.cursor_visible;
247                    self.last_blink = std::time::Instant::now();
248                    self.cache.clear();
249                }
250                Task::none()
251            }
252            Message::PageUp => {
253                self.page_up();
254                self.reset_cursor_blink();
255                self.scroll_to_cursor()
256            }
257            Message::PageDown => {
258                self.page_down();
259                self.reset_cursor_blink();
260                self.scroll_to_cursor()
261            }
262            Message::Home(shift_pressed) => {
263                if *shift_pressed {
264                    // Start selection if not already started
265                    if self.selection_start.is_none() {
266                        self.selection_start = Some(self.cursor);
267                    }
268                    self.cursor.1 = 0; // Move to start of line
269                    self.selection_end = Some(self.cursor);
270                } else {
271                    // Clear selection and move cursor
272                    self.clear_selection();
273                    self.cursor.1 = 0;
274                }
275                self.reset_cursor_blink();
276                self.cache.clear();
277                Task::none()
278            }
279            Message::End(shift_pressed) => {
280                let line = self.cursor.0;
281                let line_len = self.buffer.line_len(line);
282
283                if *shift_pressed {
284                    // Start selection if not already started
285                    if self.selection_start.is_none() {
286                        self.selection_start = Some(self.cursor);
287                    }
288                    self.cursor.1 = line_len; // Move to end of line
289                    self.selection_end = Some(self.cursor);
290                } else {
291                    // Clear selection and move cursor
292                    self.clear_selection();
293                    self.cursor.1 = line_len;
294                }
295                self.reset_cursor_blink();
296                self.cache.clear();
297                Task::none()
298            }
299            Message::CtrlHome => {
300                // Move cursor to the beginning of the document
301                self.clear_selection();
302                self.cursor = (0, 0);
303                self.reset_cursor_blink();
304                self.cache.clear();
305                self.scroll_to_cursor()
306            }
307            Message::CtrlEnd => {
308                // Move cursor to the end of the document
309                self.clear_selection();
310                let last_line = self.buffer.line_count().saturating_sub(1);
311                let last_col = self.buffer.line_len(last_line);
312                self.cursor = (last_line, last_col);
313                self.reset_cursor_blink();
314                self.cache.clear();
315                self.scroll_to_cursor()
316            }
317            Message::Scrolled(viewport) => {
318                // Track viewport scroll position, height, and width
319                self.viewport_scroll = viewport.absolute_offset().y;
320                let new_height = viewport.bounds().height;
321                let new_width = viewport.bounds().width;
322                // Clear cache when viewport dimensions change significantly
323                // to ensure proper redraw (e.g., window resize)
324                if (self.viewport_height - new_height).abs() > 1.0
325                    || (self.viewport_width - new_width).abs() > 1.0
326                {
327                    self.cache.clear();
328                }
329                self.viewport_height = new_height;
330                self.viewport_width = new_width;
331                Task::none()
332            }
333            Message::Undo => {
334                // End any current grouping before undoing
335                if self.is_grouping {
336                    self.history.end_group();
337                    self.is_grouping = false;
338                }
339
340                if self.history.undo(&mut self.buffer, &mut self.cursor) {
341                    self.clear_selection();
342                    self.reset_cursor_blink();
343                    self.refresh_search_matches_if_needed();
344                    self.cache.clear();
345                    self.scroll_to_cursor()
346                } else {
347                    Task::none()
348                }
349            }
350            Message::Redo => {
351                if self.history.redo(&mut self.buffer, &mut self.cursor) {
352                    self.clear_selection();
353                    self.reset_cursor_blink();
354                    self.refresh_search_matches_if_needed();
355                    self.cache.clear();
356                    self.scroll_to_cursor()
357                } else {
358                    Task::none()
359                }
360            }
361            Message::OpenSearch => {
362                self.search_state.open_search();
363                self.cache.clear();
364
365                // Focus the search input and select all text if any
366                Task::batch([
367                    focus(self.search_state.search_input_id.clone()),
368                    select_all(self.search_state.search_input_id.clone()),
369                ])
370            }
371            Message::OpenSearchReplace => {
372                self.search_state.open_replace();
373                self.cache.clear();
374
375                // Focus the search input and select all text if any
376                Task::batch([
377                    focus(self.search_state.search_input_id.clone()),
378                    select_all(self.search_state.search_input_id.clone()),
379                ])
380            }
381            Message::CloseSearch => {
382                self.search_state.close();
383                self.cache.clear();
384                Task::none()
385            }
386            Message::SearchQueryChanged(query) => {
387                self.search_state.set_query(query.clone(), &self.buffer);
388                self.cache.clear();
389
390                // Move cursor to first match if any
391                if let Some(match_pos) = self.search_state.current_match() {
392                    self.cursor = (match_pos.line, match_pos.col);
393                    self.clear_selection();
394                    return self.scroll_to_cursor();
395                }
396                Task::none()
397            }
398            Message::ReplaceQueryChanged(replace_text) => {
399                self.search_state.set_replace_with(replace_text.clone());
400                Task::none()
401            }
402            Message::ToggleCaseSensitive => {
403                self.search_state.toggle_case_sensitive(&self.buffer);
404                self.cache.clear();
405
406                // Move cursor to first match if any
407                if let Some(match_pos) = self.search_state.current_match() {
408                    self.cursor = (match_pos.line, match_pos.col);
409                    self.clear_selection();
410                    return self.scroll_to_cursor();
411                }
412                Task::none()
413            }
414            Message::FindNext => {
415                if !self.search_state.matches.is_empty() {
416                    self.search_state.next_match();
417                    if let Some(match_pos) = self.search_state.current_match() {
418                        self.cursor = (match_pos.line, match_pos.col);
419                        self.clear_selection();
420                        self.cache.clear();
421                        return self.scroll_to_cursor();
422                    }
423                }
424                Task::none()
425            }
426            Message::FindPrevious => {
427                if !self.search_state.matches.is_empty() {
428                    self.search_state.previous_match();
429                    if let Some(match_pos) = self.search_state.current_match() {
430                        self.cursor = (match_pos.line, match_pos.col);
431                        self.clear_selection();
432                        self.cache.clear();
433                        return self.scroll_to_cursor();
434                    }
435                }
436                Task::none()
437            }
438            Message::ReplaceNext => {
439                // Replace current match and move to next
440                if let Some(match_pos) = self.search_state.current_match() {
441                    let query_len = self.search_state.query.chars().count();
442                    let replace_text = self.search_state.replace_with.clone();
443
444                    // Create and execute replace command
445                    let mut cmd = ReplaceTextCommand::new(
446                        &self.buffer,
447                        (match_pos.line, match_pos.col),
448                        query_len,
449                        replace_text,
450                        self.cursor,
451                    );
452                    cmd.execute(&mut self.buffer, &mut self.cursor);
453                    self.history.push(Box::new(cmd));
454
455                    // Update matches after replacement
456                    self.search_state.update_matches(&self.buffer);
457
458                    // Move to next match if available
459                    if !self.search_state.matches.is_empty()
460                        && let Some(next_match) =
461                            self.search_state.current_match()
462                    {
463                        self.cursor = (next_match.line, next_match.col);
464                    }
465
466                    self.clear_selection();
467                    self.cache.clear();
468                    return self.scroll_to_cursor();
469                }
470                Task::none()
471            }
472            Message::ReplaceAll => {
473                // Replace all matches in reverse order (to preserve positions)
474                if !self.search_state.matches.is_empty() {
475                    let query_len = self.search_state.query.chars().count();
476                    let replace_text = self.search_state.replace_with.clone();
477
478                    // Create composite command for undo
479                    let mut composite =
480                        CompositeCommand::new("Replace All".to_string());
481
482                    // Process matches in reverse order
483                    for match_pos in self.search_state.matches.iter().rev() {
484                        let cmd = ReplaceTextCommand::new(
485                            &self.buffer,
486                            (match_pos.line, match_pos.col),
487                            query_len,
488                            replace_text.clone(),
489                            self.cursor,
490                        );
491                        composite.add(Box::new(cmd));
492                    }
493
494                    // Execute all replacements
495                    composite.execute(&mut self.buffer, &mut self.cursor);
496                    self.history.push(Box::new(composite));
497
498                    // Update matches (should be empty now)
499                    self.search_state.update_matches(&self.buffer);
500
501                    self.clear_selection();
502                    self.cache.clear();
503                    return self.scroll_to_cursor();
504                }
505                Task::none()
506            }
507            Message::SearchDialogTab => {
508                // Cycle focus forward (Search → Replace → Search)
509                self.search_state.focus_next_field();
510
511                // Focus the appropriate input based on new focused_field
512                match self.search_state.focused_field {
513                    crate::canvas_editor::search::SearchFocusedField::Search => {
514                        focus(self.search_state.search_input_id.clone())
515                    }
516                    crate::canvas_editor::search::SearchFocusedField::Replace => {
517                        focus(self.search_state.replace_input_id.clone())
518                    }
519                }
520            }
521            Message::SearchDialogShiftTab => {
522                // Cycle focus backward (Replace → Search → Replace)
523                self.search_state.focus_previous_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        }
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use crate::canvas_editor::ArrowDirection;
543
544    #[test]
545    fn test_new_canvas_editor() {
546        let editor = CodeEditor::new("line1\nline2", "py");
547        assert_eq!(editor.cursor, (0, 0));
548    }
549
550    #[test]
551    fn test_home_key() {
552        let mut editor = CodeEditor::new("hello world", "py");
553        editor.cursor = (0, 5); // Move to middle of line
554        let _ = editor.update(&Message::Home(false));
555        assert_eq!(editor.cursor, (0, 0));
556    }
557
558    #[test]
559    fn test_end_key() {
560        let mut editor = CodeEditor::new("hello world", "py");
561        editor.cursor = (0, 0);
562        let _ = editor.update(&Message::End(false));
563        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
564    }
565
566    #[test]
567    fn test_arrow_key_with_shift_creates_selection() {
568        let mut editor = CodeEditor::new("hello world", "py");
569        editor.cursor = (0, 0);
570
571        // Shift+Right should start selection
572        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
573        assert!(editor.selection_start.is_some());
574        assert!(editor.selection_end.is_some());
575    }
576
577    #[test]
578    fn test_arrow_key_without_shift_clears_selection() {
579        let mut editor = CodeEditor::new("hello world", "py");
580        editor.selection_start = Some((0, 0));
581        editor.selection_end = Some((0, 5));
582
583        // Regular arrow key should clear selection
584        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
585        assert_eq!(editor.selection_start, None);
586        assert_eq!(editor.selection_end, None);
587    }
588
589    #[test]
590    fn test_typing_with_selection() {
591        let mut editor = CodeEditor::new("hello world", "py");
592        editor.selection_start = Some((0, 0));
593        editor.selection_end = Some((0, 5));
594
595        let _ = editor.update(&Message::CharacterInput('X'));
596        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
597        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
598        assert_eq!(editor.buffer.line(0), "Xhello world");
599    }
600
601    #[test]
602    fn test_ctrl_home() {
603        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
604        editor.cursor = (2, 5); // Start at line 3, column 5
605        let _ = editor.update(&Message::CtrlHome);
606        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
607    }
608
609    #[test]
610    fn test_ctrl_end() {
611        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
612        editor.cursor = (0, 0); // Start at beginning
613        let _ = editor.update(&Message::CtrlEnd);
614        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
615    }
616
617    #[test]
618    fn test_ctrl_home_clears_selection() {
619        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
620        editor.cursor = (2, 5);
621        editor.selection_start = Some((0, 0));
622        editor.selection_end = Some((2, 5));
623
624        let _ = editor.update(&Message::CtrlHome);
625        assert_eq!(editor.cursor, (0, 0));
626        assert_eq!(editor.selection_start, None);
627        assert_eq!(editor.selection_end, None);
628    }
629
630    #[test]
631    fn test_ctrl_end_clears_selection() {
632        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
633        editor.cursor = (0, 0);
634        editor.selection_start = Some((0, 0));
635        editor.selection_end = Some((1, 3));
636
637        let _ = editor.update(&Message::CtrlEnd);
638        assert_eq!(editor.cursor, (2, 5));
639        assert_eq!(editor.selection_start, None);
640        assert_eq!(editor.selection_end, None);
641    }
642
643    #[test]
644    fn test_delete_selection_message() {
645        let mut editor = CodeEditor::new("hello world", "py");
646        editor.cursor = (0, 0);
647        editor.selection_start = Some((0, 0));
648        editor.selection_end = Some((0, 5));
649
650        let _ = editor.update(&Message::DeleteSelection);
651        assert_eq!(editor.buffer.line(0), " world");
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_delete_selection_multiline() {
659        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
660        editor.cursor = (0, 2);
661        editor.selection_start = Some((0, 2));
662        editor.selection_end = Some((2, 2));
663
664        let _ = editor.update(&Message::DeleteSelection);
665        assert_eq!(editor.buffer.line(0), "line3");
666        assert_eq!(editor.cursor, (0, 2));
667        assert_eq!(editor.selection_start, None);
668    }
669
670    #[test]
671    fn test_delete_selection_no_selection() {
672        let mut editor = CodeEditor::new("hello world", "py");
673        editor.cursor = (0, 5);
674
675        let _ = editor.update(&Message::DeleteSelection);
676        // Should do nothing if there's no selection
677        assert_eq!(editor.buffer.line(0), "hello world");
678        assert_eq!(editor.cursor, (0, 5));
679    }
680
681    #[test]
682    fn test_undo_char_insert() {
683        let mut editor = CodeEditor::new("hello", "py");
684        editor.cursor = (0, 5);
685
686        // Type a character
687        let _ = editor.update(&Message::CharacterInput('!'));
688        assert_eq!(editor.buffer.line(0), "hello!");
689        assert_eq!(editor.cursor, (0, 6));
690
691        // Undo should remove it (but first end the grouping)
692        editor.history.end_group();
693        let _ = editor.update(&Message::Undo);
694        assert_eq!(editor.buffer.line(0), "hello");
695        assert_eq!(editor.cursor, (0, 5));
696    }
697
698    #[test]
699    fn test_undo_redo_char_insert() {
700        let mut editor = CodeEditor::new("hello", "py");
701        editor.cursor = (0, 5);
702
703        // Type a character
704        let _ = editor.update(&Message::CharacterInput('!'));
705        editor.history.end_group();
706
707        // Undo
708        let _ = editor.update(&Message::Undo);
709        assert_eq!(editor.buffer.line(0), "hello");
710
711        // Redo
712        let _ = editor.update(&Message::Redo);
713        assert_eq!(editor.buffer.line(0), "hello!");
714        assert_eq!(editor.cursor, (0, 6));
715    }
716
717    #[test]
718    fn test_undo_backspace() {
719        let mut editor = CodeEditor::new("hello", "py");
720        editor.cursor = (0, 5);
721
722        // Backspace
723        let _ = editor.update(&Message::Backspace);
724        assert_eq!(editor.buffer.line(0), "hell");
725        assert_eq!(editor.cursor, (0, 4));
726
727        // Undo
728        let _ = editor.update(&Message::Undo);
729        assert_eq!(editor.buffer.line(0), "hello");
730        assert_eq!(editor.cursor, (0, 5));
731    }
732
733    #[test]
734    fn test_undo_newline() {
735        let mut editor = CodeEditor::new("hello world", "py");
736        editor.cursor = (0, 5);
737
738        // Insert newline
739        let _ = editor.update(&Message::Enter);
740        assert_eq!(editor.buffer.line(0), "hello");
741        assert_eq!(editor.buffer.line(1), " world");
742        assert_eq!(editor.cursor, (1, 0));
743
744        // Undo
745        let _ = editor.update(&Message::Undo);
746        assert_eq!(editor.buffer.line(0), "hello world");
747        assert_eq!(editor.cursor, (0, 5));
748    }
749
750    #[test]
751    fn test_undo_grouped_typing() {
752        let mut editor = CodeEditor::new("hello", "py");
753        editor.cursor = (0, 5);
754
755        // Type multiple characters (they should be grouped)
756        let _ = editor.update(&Message::CharacterInput(' '));
757        let _ = editor.update(&Message::CharacterInput('w'));
758        let _ = editor.update(&Message::CharacterInput('o'));
759        let _ = editor.update(&Message::CharacterInput('r'));
760        let _ = editor.update(&Message::CharacterInput('l'));
761        let _ = editor.update(&Message::CharacterInput('d'));
762
763        assert_eq!(editor.buffer.line(0), "hello world");
764
765        // End the group
766        editor.history.end_group();
767
768        // Single undo should remove all grouped characters
769        let _ = editor.update(&Message::Undo);
770        assert_eq!(editor.buffer.line(0), "hello");
771        assert_eq!(editor.cursor, (0, 5));
772    }
773
774    #[test]
775    fn test_navigation_ends_grouping() {
776        let mut editor = CodeEditor::new("hello", "py");
777        editor.cursor = (0, 5);
778
779        // Type a character (starts grouping)
780        let _ = editor.update(&Message::CharacterInput('!'));
781        assert!(editor.is_grouping);
782
783        // Move cursor (ends grouping)
784        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
785        assert!(!editor.is_grouping);
786
787        // Type another character (starts new group)
788        let _ = editor.update(&Message::CharacterInput('?'));
789        assert!(editor.is_grouping);
790
791        editor.history.end_group();
792
793        // Two separate undo operations
794        let _ = editor.update(&Message::Undo);
795        assert_eq!(editor.buffer.line(0), "hello!");
796
797        let _ = editor.update(&Message::Undo);
798        assert_eq!(editor.buffer.line(0), "hello");
799    }
800
801    #[test]
802    fn test_multiple_undo_redo() {
803        let mut editor = CodeEditor::new("a", "py");
804        editor.cursor = (0, 1);
805
806        // Make several changes
807        let _ = editor.update(&Message::CharacterInput('b'));
808        editor.history.end_group();
809
810        let _ = editor.update(&Message::CharacterInput('c'));
811        editor.history.end_group();
812
813        let _ = editor.update(&Message::CharacterInput('d'));
814        editor.history.end_group();
815
816        assert_eq!(editor.buffer.line(0), "abcd");
817
818        // Undo all
819        let _ = editor.update(&Message::Undo);
820        assert_eq!(editor.buffer.line(0), "abc");
821
822        let _ = editor.update(&Message::Undo);
823        assert_eq!(editor.buffer.line(0), "ab");
824
825        let _ = editor.update(&Message::Undo);
826        assert_eq!(editor.buffer.line(0), "a");
827
828        // Redo all
829        let _ = editor.update(&Message::Redo);
830        assert_eq!(editor.buffer.line(0), "ab");
831
832        let _ = editor.update(&Message::Redo);
833        assert_eq!(editor.buffer.line(0), "abc");
834
835        let _ = editor.update(&Message::Redo);
836        assert_eq!(editor.buffer.line(0), "abcd");
837    }
838
839    #[test]
840    fn test_delete_key_with_selection() {
841        let mut editor = CodeEditor::new("hello world", "py");
842        editor.selection_start = Some((0, 0));
843        editor.selection_end = Some((0, 5));
844        editor.cursor = (0, 5);
845
846        let _ = editor.update(&Message::Delete);
847
848        assert_eq!(editor.buffer.line(0), " world");
849        assert_eq!(editor.cursor, (0, 0));
850        assert_eq!(editor.selection_start, None);
851        assert_eq!(editor.selection_end, None);
852    }
853
854    #[test]
855    fn test_delete_key_without_selection() {
856        let mut editor = CodeEditor::new("hello", "py");
857        editor.cursor = (0, 0);
858
859        let _ = editor.update(&Message::Delete);
860
861        // Should delete the 'h'
862        assert_eq!(editor.buffer.line(0), "ello");
863        assert_eq!(editor.cursor, (0, 0));
864    }
865
866    #[test]
867    fn test_backspace_with_selection() {
868        let mut editor = CodeEditor::new("hello world", "py");
869        editor.selection_start = Some((0, 6));
870        editor.selection_end = Some((0, 11));
871        editor.cursor = (0, 11);
872
873        let _ = editor.update(&Message::Backspace);
874
875        assert_eq!(editor.buffer.line(0), "hello ");
876        assert_eq!(editor.cursor, (0, 6));
877        assert_eq!(editor.selection_start, None);
878        assert_eq!(editor.selection_end, None);
879    }
880
881    #[test]
882    fn test_backspace_without_selection() {
883        let mut editor = CodeEditor::new("hello", "py");
884        editor.cursor = (0, 5);
885
886        let _ = editor.update(&Message::Backspace);
887
888        // Should delete the 'o'
889        assert_eq!(editor.buffer.line(0), "hell");
890        assert_eq!(editor.cursor, (0, 4));
891    }
892
893    #[test]
894    fn test_delete_multiline_selection() {
895        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
896        editor.selection_start = Some((0, 2));
897        editor.selection_end = Some((2, 2));
898        editor.cursor = (2, 2);
899
900        let _ = editor.update(&Message::Delete);
901
902        assert_eq!(editor.buffer.line(0), "line3");
903        assert_eq!(editor.cursor, (0, 2));
904        assert_eq!(editor.selection_start, None);
905    }
906}