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, ImePreedit, 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.ime_preedit = None;
560                self.cache.clear();
561                Task::none()
562            }
563            Message::ImeOpened => {
564                // IME opened event
565                // -------------------------------------------------------------
566                // Triggered when the user activates the input method.
567                // Action: clear current preedit content (ime_preedit) to accept new input.
568                // This avoids carrying over the previous composition state.
569                // -------------------------------------------------------------
570                self.ime_preedit = None;
571                self.cache.clear();
572                Task::none()
573            }
574            Message::ImePreedit(content, selection) => {
575                // IME preedit event
576                // -------------------------------------------------------------
577                // Triggered while the user is composing text but not committed yet.
578                // Params:
579                // - content: current preedit text (e.g. "ni h").
580                // - selection: caret/selection inside the preedit text.
581                //
582                // Note: Iced provides selection as a byte index range, not character indices.
583                // When rendering or slicing, use UTF-8 byte offsets to avoid panics.
584                // -------------------------------------------------------------
585                if content.is_empty() {
586                    self.ime_preedit = None;
587                } else {
588                    self.ime_preedit = Some(ImePreedit {
589                        content: content.clone(),
590                        selection: selection.clone(),
591                    });
592                }
593
594                self.cache.clear();
595                Task::none()
596            }
597            Message::ImeCommit(text) => {
598                // IME commit event
599                // -------------------------------------------------------------
600                // Triggered when the user confirms a candidate and commits text.
601                // Actions:
602                // 1. Clear preedit state (ime_preedit = None).
603                // 2. If text is not empty, insert it at the current cursor position.
604                // 3. Begin a "Typing" undo group so consecutive IME commits undo together.
605                // -------------------------------------------------------------
606                self.ime_preedit = None;
607
608                if text.is_empty() {
609                    self.cache.clear();
610                    return Task::none();
611                }
612
613                if !self.is_grouping {
614                    self.history.begin_group("Typing");
615                    self.is_grouping = true;
616                }
617
618                self.paste_text(text);
619                self.reset_cursor_blink();
620                self.refresh_search_matches_if_needed();
621                self.cache.clear();
622                self.scroll_to_cursor()
623            }
624            Message::ImeClosed => {
625                // IME closed event
626                // -------------------------------------------------------------
627                // Triggered when the input method is closed or loses focus.
628                // Action: clear preedit state to return to normal input mode.
629                // -------------------------------------------------------------
630                self.ime_preedit = None;
631                self.cache.clear();
632                Task::none()
633            }
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use crate::canvas_editor::ArrowDirection;
642
643    #[test]
644    fn test_new_canvas_editor() {
645        let editor = CodeEditor::new("line1\nline2", "py");
646        assert_eq!(editor.cursor, (0, 0));
647    }
648
649    #[test]
650    fn test_home_key() {
651        let mut editor = CodeEditor::new("hello world", "py");
652        editor.cursor = (0, 5); // Move to middle of line
653        let _ = editor.update(&Message::Home(false));
654        assert_eq!(editor.cursor, (0, 0));
655    }
656
657    #[test]
658    fn test_end_key() {
659        let mut editor = CodeEditor::new("hello world", "py");
660        editor.cursor = (0, 0);
661        let _ = editor.update(&Message::End(false));
662        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
663    }
664
665    #[test]
666    fn test_arrow_key_with_shift_creates_selection() {
667        let mut editor = CodeEditor::new("hello world", "py");
668        editor.cursor = (0, 0);
669
670        // Shift+Right should start selection
671        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
672        assert!(editor.selection_start.is_some());
673        assert!(editor.selection_end.is_some());
674    }
675
676    #[test]
677    fn test_arrow_key_without_shift_clears_selection() {
678        let mut editor = CodeEditor::new("hello world", "py");
679        editor.selection_start = Some((0, 0));
680        editor.selection_end = Some((0, 5));
681
682        // Regular arrow key should clear selection
683        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
684        assert_eq!(editor.selection_start, None);
685        assert_eq!(editor.selection_end, None);
686    }
687
688    #[test]
689    fn test_typing_with_selection() {
690        let mut editor = CodeEditor::new("hello world", "py");
691        editor.selection_start = Some((0, 0));
692        editor.selection_end = Some((0, 5));
693
694        let _ = editor.update(&Message::CharacterInput('X'));
695        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
696        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
697        assert_eq!(editor.buffer.line(0), "Xhello world");
698    }
699
700    #[test]
701    fn test_ctrl_home() {
702        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
703        editor.cursor = (2, 5); // Start at line 3, column 5
704        let _ = editor.update(&Message::CtrlHome);
705        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
706    }
707
708    #[test]
709    fn test_ctrl_end() {
710        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
711        editor.cursor = (0, 0); // Start at beginning
712        let _ = editor.update(&Message::CtrlEnd);
713        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
714    }
715
716    #[test]
717    fn test_ctrl_home_clears_selection() {
718        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
719        editor.cursor = (2, 5);
720        editor.selection_start = Some((0, 0));
721        editor.selection_end = Some((2, 5));
722
723        let _ = editor.update(&Message::CtrlHome);
724        assert_eq!(editor.cursor, (0, 0));
725        assert_eq!(editor.selection_start, None);
726        assert_eq!(editor.selection_end, None);
727    }
728
729    #[test]
730    fn test_ctrl_end_clears_selection() {
731        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
732        editor.cursor = (0, 0);
733        editor.selection_start = Some((0, 0));
734        editor.selection_end = Some((1, 3));
735
736        let _ = editor.update(&Message::CtrlEnd);
737        assert_eq!(editor.cursor, (2, 5));
738        assert_eq!(editor.selection_start, None);
739        assert_eq!(editor.selection_end, None);
740    }
741
742    #[test]
743    fn test_delete_selection_message() {
744        let mut editor = CodeEditor::new("hello world", "py");
745        editor.cursor = (0, 0);
746        editor.selection_start = Some((0, 0));
747        editor.selection_end = Some((0, 5));
748
749        let _ = editor.update(&Message::DeleteSelection);
750        assert_eq!(editor.buffer.line(0), " world");
751        assert_eq!(editor.cursor, (0, 0));
752        assert_eq!(editor.selection_start, None);
753        assert_eq!(editor.selection_end, None);
754    }
755
756    #[test]
757    fn test_delete_selection_multiline() {
758        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
759        editor.cursor = (0, 2);
760        editor.selection_start = Some((0, 2));
761        editor.selection_end = Some((2, 2));
762
763        let _ = editor.update(&Message::DeleteSelection);
764        assert_eq!(editor.buffer.line(0), "line3");
765        assert_eq!(editor.cursor, (0, 2));
766        assert_eq!(editor.selection_start, None);
767    }
768
769    #[test]
770    fn test_delete_selection_no_selection() {
771        let mut editor = CodeEditor::new("hello world", "py");
772        editor.cursor = (0, 5);
773
774        let _ = editor.update(&Message::DeleteSelection);
775        // Should do nothing if there's no selection
776        assert_eq!(editor.buffer.line(0), "hello world");
777        assert_eq!(editor.cursor, (0, 5));
778    }
779
780    #[test]
781    #[allow(clippy::unwrap_used)]
782    fn test_ime_preedit_and_commit_chinese() {
783        let mut editor = CodeEditor::new("", "py");
784        // Simulate IME opened
785        let _ = editor.update(&Message::ImeOpened);
786        assert!(editor.ime_preedit.is_none());
787
788        // Preedit with Chinese content and a selection range
789        let content = "安全与合规".to_string();
790        let selection = Some(0..3); // range aligned to UTF-8 character boundary
791        let _ = editor
792            .update(&Message::ImePreedit(content.clone(), selection.clone()));
793
794        assert!(editor.ime_preedit.is_some());
795        assert_eq!(
796            editor.ime_preedit.as_ref().unwrap().content.clone(),
797            content
798        );
799        assert_eq!(
800            editor.ime_preedit.as_ref().unwrap().selection.clone(),
801            selection
802        );
803
804        // Commit should insert the text and clear preedit
805        let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
806        assert!(editor.ime_preedit.is_none());
807        assert_eq!(editor.buffer.line(0), "安全与合规");
808        assert_eq!(editor.cursor, (0, "安全与合规".chars().count()));
809    }
810
811    #[test]
812    fn test_undo_char_insert() {
813        let mut editor = CodeEditor::new("hello", "py");
814        editor.cursor = (0, 5);
815
816        // Type a character
817        let _ = editor.update(&Message::CharacterInput('!'));
818        assert_eq!(editor.buffer.line(0), "hello!");
819        assert_eq!(editor.cursor, (0, 6));
820
821        // Undo should remove it (but first end the grouping)
822        editor.history.end_group();
823        let _ = editor.update(&Message::Undo);
824        assert_eq!(editor.buffer.line(0), "hello");
825        assert_eq!(editor.cursor, (0, 5));
826    }
827
828    #[test]
829    fn test_undo_redo_char_insert() {
830        let mut editor = CodeEditor::new("hello", "py");
831        editor.cursor = (0, 5);
832
833        // Type a character
834        let _ = editor.update(&Message::CharacterInput('!'));
835        editor.history.end_group();
836
837        // Undo
838        let _ = editor.update(&Message::Undo);
839        assert_eq!(editor.buffer.line(0), "hello");
840
841        // Redo
842        let _ = editor.update(&Message::Redo);
843        assert_eq!(editor.buffer.line(0), "hello!");
844        assert_eq!(editor.cursor, (0, 6));
845    }
846
847    #[test]
848    fn test_undo_backspace() {
849        let mut editor = CodeEditor::new("hello", "py");
850        editor.cursor = (0, 5);
851
852        // Backspace
853        let _ = editor.update(&Message::Backspace);
854        assert_eq!(editor.buffer.line(0), "hell");
855        assert_eq!(editor.cursor, (0, 4));
856
857        // Undo
858        let _ = editor.update(&Message::Undo);
859        assert_eq!(editor.buffer.line(0), "hello");
860        assert_eq!(editor.cursor, (0, 5));
861    }
862
863    #[test]
864    fn test_undo_newline() {
865        let mut editor = CodeEditor::new("hello world", "py");
866        editor.cursor = (0, 5);
867
868        // Insert newline
869        let _ = editor.update(&Message::Enter);
870        assert_eq!(editor.buffer.line(0), "hello");
871        assert_eq!(editor.buffer.line(1), " world");
872        assert_eq!(editor.cursor, (1, 0));
873
874        // Undo
875        let _ = editor.update(&Message::Undo);
876        assert_eq!(editor.buffer.line(0), "hello world");
877        assert_eq!(editor.cursor, (0, 5));
878    }
879
880    #[test]
881    fn test_undo_grouped_typing() {
882        let mut editor = CodeEditor::new("hello", "py");
883        editor.cursor = (0, 5);
884
885        // Type multiple characters (they should be grouped)
886        let _ = editor.update(&Message::CharacterInput(' '));
887        let _ = editor.update(&Message::CharacterInput('w'));
888        let _ = editor.update(&Message::CharacterInput('o'));
889        let _ = editor.update(&Message::CharacterInput('r'));
890        let _ = editor.update(&Message::CharacterInput('l'));
891        let _ = editor.update(&Message::CharacterInput('d'));
892
893        assert_eq!(editor.buffer.line(0), "hello world");
894
895        // End the group
896        editor.history.end_group();
897
898        // Single undo should remove all grouped characters
899        let _ = editor.update(&Message::Undo);
900        assert_eq!(editor.buffer.line(0), "hello");
901        assert_eq!(editor.cursor, (0, 5));
902    }
903
904    #[test]
905    fn test_navigation_ends_grouping() {
906        let mut editor = CodeEditor::new("hello", "py");
907        editor.cursor = (0, 5);
908
909        // Type a character (starts grouping)
910        let _ = editor.update(&Message::CharacterInput('!'));
911        assert!(editor.is_grouping);
912
913        // Move cursor (ends grouping)
914        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
915        assert!(!editor.is_grouping);
916
917        // Type another character (starts new group)
918        let _ = editor.update(&Message::CharacterInput('?'));
919        assert!(editor.is_grouping);
920
921        editor.history.end_group();
922
923        // Two separate undo operations
924        let _ = editor.update(&Message::Undo);
925        assert_eq!(editor.buffer.line(0), "hello!");
926
927        let _ = editor.update(&Message::Undo);
928        assert_eq!(editor.buffer.line(0), "hello");
929    }
930
931    #[test]
932    fn test_multiple_undo_redo() {
933        let mut editor = CodeEditor::new("a", "py");
934        editor.cursor = (0, 1);
935
936        // Make several changes
937        let _ = editor.update(&Message::CharacterInput('b'));
938        editor.history.end_group();
939
940        let _ = editor.update(&Message::CharacterInput('c'));
941        editor.history.end_group();
942
943        let _ = editor.update(&Message::CharacterInput('d'));
944        editor.history.end_group();
945
946        assert_eq!(editor.buffer.line(0), "abcd");
947
948        // Undo all
949        let _ = editor.update(&Message::Undo);
950        assert_eq!(editor.buffer.line(0), "abc");
951
952        let _ = editor.update(&Message::Undo);
953        assert_eq!(editor.buffer.line(0), "ab");
954
955        let _ = editor.update(&Message::Undo);
956        assert_eq!(editor.buffer.line(0), "a");
957
958        // Redo all
959        let _ = editor.update(&Message::Redo);
960        assert_eq!(editor.buffer.line(0), "ab");
961
962        let _ = editor.update(&Message::Redo);
963        assert_eq!(editor.buffer.line(0), "abc");
964
965        let _ = editor.update(&Message::Redo);
966        assert_eq!(editor.buffer.line(0), "abcd");
967    }
968
969    #[test]
970    fn test_delete_key_with_selection() {
971        let mut editor = CodeEditor::new("hello world", "py");
972        editor.selection_start = Some((0, 0));
973        editor.selection_end = Some((0, 5));
974        editor.cursor = (0, 5);
975
976        let _ = editor.update(&Message::Delete);
977
978        assert_eq!(editor.buffer.line(0), " world");
979        assert_eq!(editor.cursor, (0, 0));
980        assert_eq!(editor.selection_start, None);
981        assert_eq!(editor.selection_end, None);
982    }
983
984    #[test]
985    fn test_delete_key_without_selection() {
986        let mut editor = CodeEditor::new("hello", "py");
987        editor.cursor = (0, 0);
988
989        let _ = editor.update(&Message::Delete);
990
991        // Should delete the 'h'
992        assert_eq!(editor.buffer.line(0), "ello");
993        assert_eq!(editor.cursor, (0, 0));
994    }
995
996    #[test]
997    fn test_backspace_with_selection() {
998        let mut editor = CodeEditor::new("hello world", "py");
999        editor.selection_start = Some((0, 6));
1000        editor.selection_end = Some((0, 11));
1001        editor.cursor = (0, 11);
1002
1003        let _ = editor.update(&Message::Backspace);
1004
1005        assert_eq!(editor.buffer.line(0), "hello ");
1006        assert_eq!(editor.cursor, (0, 6));
1007        assert_eq!(editor.selection_start, None);
1008        assert_eq!(editor.selection_end, None);
1009    }
1010
1011    #[test]
1012    fn test_backspace_without_selection() {
1013        let mut editor = CodeEditor::new("hello", "py");
1014        editor.cursor = (0, 5);
1015
1016        let _ = editor.update(&Message::Backspace);
1017
1018        // Should delete the 'o'
1019        assert_eq!(editor.buffer.line(0), "hell");
1020        assert_eq!(editor.cursor, (0, 4));
1021    }
1022
1023    #[test]
1024    fn test_delete_multiline_selection() {
1025        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1026        editor.selection_start = Some((0, 2));
1027        editor.selection_end = Some((2, 2));
1028        editor.cursor = (2, 2);
1029
1030        let _ = editor.update(&Message::Delete);
1031
1032        assert_eq!(editor.buffer.line(0), "line3");
1033        assert_eq!(editor.cursor, (0, 2));
1034        assert_eq!(editor.selection_start, None);
1035    }
1036
1037    #[test]
1038    fn test_canvas_focus_gained() {
1039        let mut editor = CodeEditor::new("hello world", "py");
1040        assert!(!editor.has_canvas_focus);
1041        assert!(!editor.show_cursor);
1042
1043        let _ = editor.update(&Message::CanvasFocusGained);
1044
1045        assert!(editor.has_canvas_focus);
1046        assert!(editor.show_cursor);
1047    }
1048
1049    #[test]
1050    fn test_canvas_focus_lost() {
1051        let mut editor = CodeEditor::new("hello world", "py");
1052        editor.has_canvas_focus = true;
1053        editor.show_cursor = true;
1054
1055        let _ = editor.update(&Message::CanvasFocusLost);
1056
1057        assert!(!editor.has_canvas_focus);
1058        assert!(!editor.show_cursor);
1059    }
1060
1061    #[test]
1062    fn test_mouse_click_gains_focus() {
1063        let mut editor = CodeEditor::new("hello world", "py");
1064        editor.has_canvas_focus = false;
1065        editor.show_cursor = false;
1066
1067        let _ =
1068            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
1069
1070        assert!(editor.has_canvas_focus);
1071        assert!(editor.show_cursor);
1072    }
1073}