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