Skip to main content

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::{
11    ArrowDirection, CURSOR_BLINK_INTERVAL, CodeEditor, ImePreedit, Message,
12};
13
14impl CodeEditor {
15    // =========================================================================
16    // Helper Methods
17    // =========================================================================
18
19    /// Performs common cleanup operations after edit operations.
20    ///
21    /// This method should be called after any operation that modifies the buffer content.
22    /// It resets the cursor blink animation, refreshes search matches if search is active,
23    /// and invalidates all caches that depend on buffer content or layout:
24    /// - `buffer_revision` is bumped to invalidate layout-derived caches
25    /// - `visual_lines_cache` is cleared so wrapping is recalculated on next use
26    /// - `content_cache` and `overlay_cache` are cleared to rebuild canvas geometry
27    fn finish_edit_operation(&mut self) {
28        self.reset_cursor_blink();
29        self.refresh_search_matches_if_needed();
30        // The exact revision value is not semantically meaningful; it only needs
31        // to change on edits, so `wrapping_add` is sufficient and overflow-safe.
32        self.buffer_revision = self.buffer_revision.wrapping_add(1);
33        *self.visual_lines_cache.borrow_mut() = None;
34        self.content_cache.clear();
35        self.overlay_cache.clear();
36    }
37
38    /// Performs common cleanup operations after navigation operations.
39    ///
40    /// This method should be called after cursor movement operations.
41    /// It resets the cursor blink animation and invalidates only the overlay
42    /// rendering cache. Cursor movement and selection changes do not modify the
43    /// buffer content, so keeping the content cache intact avoids unnecessary
44    /// re-rendering of syntax-highlighted text.
45    fn finish_navigation_operation(&mut self) {
46        self.reset_cursor_blink();
47        self.overlay_cache.clear();
48    }
49
50    /// Starts command grouping with the given label if not already grouping.
51    ///
52    /// This is used for smart undo functionality, allowing multiple related
53    /// operations to be undone as a single unit.
54    ///
55    /// # Arguments
56    ///
57    /// * `label` - A descriptive label for the group of commands
58    fn ensure_grouping_started(&mut self, label: &str) {
59        if !self.is_grouping {
60            self.history.begin_group(label);
61            self.is_grouping = true;
62        }
63    }
64
65    /// Ends command grouping if currently active.
66    ///
67    /// This should be called when a series of related operations is complete,
68    /// or when starting a new type of operation that shouldn't be grouped
69    /// with previous operations.
70    fn end_grouping_if_active(&mut self) {
71        if self.is_grouping {
72            self.history.end_group();
73            self.is_grouping = false;
74        }
75    }
76
77    /// Deletes the current selection and performs cleanup if a selection exists.
78    ///
79    /// # Returns
80    ///
81    /// `true` if a selection was deleted, `false` if no selection existed
82    fn delete_selection_if_present(&mut self) -> bool {
83        if self.selection_start.is_some() && self.selection_end.is_some() {
84            self.delete_selection();
85            self.finish_edit_operation();
86            true
87        } else {
88            false
89        }
90    }
91
92    // =========================================================================
93    // Text Input Handlers
94    // =========================================================================
95
96    /// Handles character input message operations.
97    ///
98    /// Inserts a character at the current cursor position and adds it to the
99    /// undo history. Characters are grouped together for smart undo.
100    /// Only processes input when the editor has active focus and is not locked.
101    ///
102    /// # Arguments
103    ///
104    /// * `ch` - The character to insert
105    ///
106    /// # Returns
107    ///
108    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
109    fn handle_character_input_msg(&mut self, ch: char) -> Task<Message> {
110        // Guard clause: only process character input if editor has focus and is not locked
111        if !self.has_focus() {
112            return Task::none();
113        }
114
115        // Start grouping if not already grouping (for smart undo)
116        self.ensure_grouping_started("Typing");
117
118        let (line, col) = self.cursor;
119        let mut cmd = InsertCharCommand::new(line, col, ch, self.cursor);
120        cmd.execute(&mut self.buffer, &mut self.cursor);
121        self.history.push(Box::new(cmd));
122
123        self.finish_edit_operation();
124        Task::none()
125    }
126
127    /// Handles Tab key press (inserts 4 spaces).
128    ///
129    /// # Returns
130    ///
131    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
132    fn handle_tab(&mut self) -> Task<Message> {
133        // Insert 4 spaces for Tab
134        // Start grouping if not already grouping
135        self.ensure_grouping_started("Tab");
136
137        let (line, col) = self.cursor;
138        // Insert 4 spaces
139        for i in 0..4 {
140            let current_col = col + i;
141            let mut cmd = InsertCharCommand::new(
142                line,
143                current_col,
144                ' ',
145                (line, current_col),
146            );
147            cmd.execute(&mut self.buffer, &mut self.cursor);
148            self.history.push(Box::new(cmd));
149        }
150
151        self.finish_navigation_operation();
152        Task::none()
153    }
154
155    /// Handles Tab key press for focus navigation (when search dialog is not open).
156    ///
157    /// # Returns
158    ///
159    /// A `Task<Message>` that may navigate focus to another editor
160    fn handle_focus_navigation_tab(&mut self) -> Task<Message> {
161        // Only handle focus navigation if search dialog is not open
162        if !self.search_state.is_open {
163            // Lose focus from current editor
164            self.has_canvas_focus = false;
165            self.show_cursor = false;
166
167            // Return a task that could potentially focus another editor
168            // This implements focus chain management by allowing the parent application
169            // to handle focus navigation between multiple editors
170            Task::none()
171        } else {
172            Task::none()
173        }
174    }
175
176    /// Handles Shift+Tab key press for focus navigation (when search dialog is not open).
177    ///
178    /// # Returns
179    ///
180    /// A `Task<Message>` that may navigate focus to another editor
181    fn handle_focus_navigation_shift_tab(&mut self) -> Task<Message> {
182        // Only handle focus navigation if search dialog is not open
183        if !self.search_state.is_open {
184            // Lose focus from current editor
185            self.has_canvas_focus = false;
186            self.show_cursor = false;
187
188            // Return a task that could potentially focus another editor
189            // This implements focus chain management by allowing the parent application
190            // to handle focus navigation between multiple editors
191            Task::none()
192        } else {
193            Task::none()
194        }
195    }
196
197    /// Handles Enter key press (inserts newline).
198    ///
199    /// # Returns
200    ///
201    /// A `Task<Message>` that scrolls to keep the cursor visible
202    fn handle_enter(&mut self) -> Task<Message> {
203        // End grouping on enter
204        self.end_grouping_if_active();
205
206        let (line, col) = self.cursor;
207        let mut cmd = InsertNewlineCommand::new(line, col, self.cursor);
208        cmd.execute(&mut self.buffer, &mut self.cursor);
209        self.history.push(Box::new(cmd));
210
211        self.finish_edit_operation();
212        self.scroll_to_cursor()
213    }
214
215    // =========================================================================
216    // Deletion Handlers
217    // =========================================================================
218
219    /// Handles Backspace key press.
220    ///
221    /// If there's a selection, deletes the selection. Otherwise, deletes the
222    /// character before the cursor.
223    ///
224    /// # Returns
225    ///
226    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
227    fn handle_backspace(&mut self) -> Task<Message> {
228        // End grouping on backspace (separate from typing)
229        self.end_grouping_if_active();
230
231        // Check if there's a selection - if so, delete it instead
232        if self.delete_selection_if_present() {
233            return self.scroll_to_cursor();
234        }
235
236        // No selection - perform normal backspace
237        let (line, col) = self.cursor;
238        let mut cmd =
239            DeleteCharCommand::new(&self.buffer, line, col, self.cursor);
240        cmd.execute(&mut self.buffer, &mut self.cursor);
241        self.history.push(Box::new(cmd));
242
243        self.finish_edit_operation();
244        self.scroll_to_cursor()
245    }
246
247    /// Handles Delete key press.
248    ///
249    /// If there's a selection, deletes the selection. Otherwise, deletes the
250    /// character after the cursor.
251    ///
252    /// # Returns
253    ///
254    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
255    fn handle_delete(&mut self) -> Task<Message> {
256        // End grouping on delete
257        self.end_grouping_if_active();
258
259        // Check if there's a selection - if so, delete it instead
260        if self.delete_selection_if_present() {
261            return self.scroll_to_cursor();
262        }
263
264        // No selection - perform normal forward delete
265        let (line, col) = self.cursor;
266        let mut cmd =
267            DeleteForwardCommand::new(&self.buffer, line, col, self.cursor);
268        cmd.execute(&mut self.buffer, &mut self.cursor);
269        self.history.push(Box::new(cmd));
270
271        self.finish_edit_operation();
272        Task::none()
273    }
274
275    /// Handles explicit selection deletion (Shift+Delete).
276    ///
277    /// Deletes the selected text if a selection exists.
278    ///
279    /// # Returns
280    ///
281    /// A `Task<Message>` that scrolls to keep the cursor visible
282    fn handle_delete_selection(&mut self) -> Task<Message> {
283        // End grouping on delete selection
284        self.end_grouping_if_active();
285
286        if self.selection_start.is_some() && self.selection_end.is_some() {
287            self.delete_selection();
288            self.finish_edit_operation();
289            self.scroll_to_cursor()
290        } else {
291            Task::none()
292        }
293    }
294
295    // =========================================================================
296    // Navigation Handlers
297    // =========================================================================
298
299    /// Handles arrow key navigation.
300    ///
301    /// # Arguments
302    ///
303    /// * `direction` - The direction of movement
304    /// * `shift_pressed` - Whether Shift is held (for selection)
305    ///
306    /// # Returns
307    ///
308    /// A `Task<Message>` that scrolls to keep the cursor visible
309    fn handle_arrow_key(
310        &mut self,
311        direction: ArrowDirection,
312        shift_pressed: bool,
313    ) -> Task<Message> {
314        // End grouping on navigation
315        self.end_grouping_if_active();
316
317        if shift_pressed {
318            // Start selection if not already started
319            if self.selection_start.is_none() {
320                self.selection_start = Some(self.cursor);
321            }
322            self.move_cursor(direction);
323            self.selection_end = Some(self.cursor);
324        } else {
325            // Clear selection and move cursor
326            self.clear_selection();
327            self.move_cursor(direction);
328        }
329        self.finish_navigation_operation();
330        self.scroll_to_cursor()
331    }
332
333    /// Handles Home key press.
334    ///
335    /// Moves the cursor to the start of the current line.
336    ///
337    /// # Arguments
338    ///
339    /// * `shift_pressed` - Whether Shift is held (for selection)
340    ///
341    /// # Returns
342    ///
343    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
344    fn handle_home(&mut self, shift_pressed: bool) -> Task<Message> {
345        if shift_pressed {
346            // Start selection if not already started
347            if self.selection_start.is_none() {
348                self.selection_start = Some(self.cursor);
349            }
350            self.cursor.1 = 0; // Move to start of line
351            self.selection_end = Some(self.cursor);
352        } else {
353            // Clear selection and move cursor
354            self.clear_selection();
355            self.cursor.1 = 0;
356        }
357        self.finish_navigation_operation();
358        Task::none()
359    }
360
361    /// Handles End key press.
362    ///
363    /// Moves the cursor to the end of the current line.
364    ///
365    /// # Arguments
366    ///
367    /// * `shift_pressed` - Whether Shift is held (for selection)
368    ///
369    /// # Returns
370    ///
371    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
372    fn handle_end(&mut self, shift_pressed: bool) -> Task<Message> {
373        let line = self.cursor.0;
374        let line_len = self.buffer.line_len(line);
375
376        if shift_pressed {
377            // Start selection if not already started
378            if self.selection_start.is_none() {
379                self.selection_start = Some(self.cursor);
380            }
381            self.cursor.1 = line_len; // Move to end of line
382            self.selection_end = Some(self.cursor);
383        } else {
384            // Clear selection and move cursor
385            self.clear_selection();
386            self.cursor.1 = line_len;
387        }
388        self.finish_navigation_operation();
389        Task::none()
390    }
391
392    /// Handles Ctrl+Home key press.
393    ///
394    /// Moves the cursor to the beginning of the document.
395    ///
396    /// # Returns
397    ///
398    /// A `Task<Message>` that scrolls to keep the cursor visible
399    fn handle_ctrl_home(&mut self) -> Task<Message> {
400        // Move cursor to the beginning of the document
401        self.clear_selection();
402        self.cursor = (0, 0);
403        self.finish_navigation_operation();
404        self.scroll_to_cursor()
405    }
406
407    /// Handles Ctrl+End key press.
408    ///
409    /// Moves the cursor to the end of the document.
410    ///
411    /// # Returns
412    ///
413    /// A `Task<Message>` that scrolls to keep the cursor visible
414    fn handle_ctrl_end(&mut self) -> Task<Message> {
415        // Move cursor to the end of the document
416        self.clear_selection();
417        let last_line = self.buffer.line_count().saturating_sub(1);
418        let last_col = self.buffer.line_len(last_line);
419        self.cursor = (last_line, last_col);
420        self.finish_navigation_operation();
421        self.scroll_to_cursor()
422    }
423
424    /// Handles Page Up key press.
425    ///
426    /// Scrolls the view up by one page.
427    ///
428    /// # Returns
429    ///
430    /// A `Task<Message>` that scrolls to keep the cursor visible
431    fn handle_page_up(&mut self) -> Task<Message> {
432        self.page_up();
433        self.finish_navigation_operation();
434        self.scroll_to_cursor()
435    }
436
437    /// Handles Page Down key press.
438    ///
439    /// Scrolls the view down by one page.
440    ///
441    /// # Returns
442    ///
443    /// A `Task<Message>` that scrolls to keep the cursor visible
444    fn handle_page_down(&mut self) -> Task<Message> {
445        self.page_down();
446        self.finish_navigation_operation();
447        self.scroll_to_cursor()
448    }
449
450    // =========================================================================
451    // Mouse and Selection Handlers
452    // =========================================================================
453
454    /// Handles mouse click operations.
455    ///
456    /// Sets focus, ends command grouping, positions cursor, starts selection tracking.
457    ///
458    /// # Arguments
459    ///
460    /// * `point` - The click position
461    ///
462    /// # Returns
463    ///
464    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
465    fn handle_mouse_click_msg(&mut self, point: iced::Point) -> Task<Message> {
466        // Capture focus when clicked using the new focus method
467        self.request_focus();
468
469        // Set internal canvas focus state
470        self.has_canvas_focus = true;
471
472        // End grouping on mouse click
473        self.end_grouping_if_active();
474
475        self.handle_mouse_click(point);
476        self.reset_cursor_blink();
477        // Clear selection on click
478        self.clear_selection();
479        self.is_dragging = true;
480        self.selection_start = Some(self.cursor);
481
482        // Show cursor when focused
483        self.show_cursor = true;
484
485        Task::none()
486    }
487
488    /// Handles mouse drag operations for selection.
489    ///
490    /// # Arguments
491    ///
492    /// * `point` - The drag position
493    ///
494    /// # Returns
495    ///
496    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
497    fn handle_mouse_drag_msg(&mut self, point: iced::Point) -> Task<Message> {
498        if self.is_dragging {
499            let before_cursor = self.cursor;
500            let before_selection_end = self.selection_end;
501            self.handle_mouse_drag(point);
502            if self.cursor != before_cursor
503                || self.selection_end != before_selection_end
504            {
505                // Mouse move events can be very frequent. Only invalidate the
506                // overlay cache if the drag actually changed selection/cursor.
507                self.overlay_cache.clear();
508            }
509        }
510        Task::none()
511    }
512
513    /// Handles mouse release operations.
514    ///
515    /// # Returns
516    ///
517    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
518    fn handle_mouse_release_msg(&mut self) -> Task<Message> {
519        self.is_dragging = false;
520        Task::none()
521    }
522
523    // =========================================================================
524    // Clipboard Handlers
525    // =========================================================================
526
527    /// Handles paste operations.
528    ///
529    /// If the provided text is empty, reads from clipboard. Otherwise pastes
530    /// the provided text at the cursor position.
531    ///
532    /// # Arguments
533    ///
534    /// * `text` - The text to paste (empty string triggers clipboard read)
535    ///
536    /// # Returns
537    ///
538    /// A `Task<Message>` that may read clipboard or scroll to cursor
539    fn handle_paste_msg(&mut self, text: &str) -> Task<Message> {
540        // End grouping on paste
541        self.end_grouping_if_active();
542
543        // If text is empty, we need to read from clipboard
544        if text.is_empty() {
545            // Return a task that reads clipboard and chains to paste
546            iced::clipboard::read().and_then(|clipboard_text| {
547                Task::done(Message::Paste(clipboard_text))
548            })
549        } else {
550            // We have the text, paste it
551            self.paste_text(text);
552            self.finish_edit_operation();
553            self.scroll_to_cursor()
554        }
555    }
556
557    // =========================================================================
558    // History (Undo/Redo) Handlers
559    // =========================================================================
560
561    /// Handles undo operations.
562    ///
563    /// # Returns
564    ///
565    /// A `Task<Message>` that scrolls to cursor if undo succeeded
566    fn handle_undo_msg(&mut self) -> Task<Message> {
567        // End any current grouping before undoing
568        self.end_grouping_if_active();
569
570        if self.history.undo(&mut self.buffer, &mut self.cursor) {
571            self.clear_selection();
572            self.finish_edit_operation();
573            self.scroll_to_cursor()
574        } else {
575            Task::none()
576        }
577    }
578
579    /// Handles redo operations.
580    ///
581    /// # Returns
582    ///
583    /// A `Task<Message>` that scrolls to cursor if redo succeeded
584    fn handle_redo_msg(&mut self) -> Task<Message> {
585        if self.history.redo(&mut self.buffer, &mut self.cursor) {
586            self.clear_selection();
587            self.finish_edit_operation();
588            self.scroll_to_cursor()
589        } else {
590            Task::none()
591        }
592    }
593
594    // =========================================================================
595    // Search and Replace Handlers
596    // =========================================================================
597
598    /// Handles opening the search dialog.
599    ///
600    /// # Returns
601    ///
602    /// A `Task<Message>` that focuses and selects all in the search input
603    fn handle_open_search_msg(&mut self) -> Task<Message> {
604        self.search_state.open_search();
605        self.overlay_cache.clear();
606
607        // Focus the search input and select all text if any
608        Task::batch([
609            focus(self.search_state.search_input_id.clone()),
610            select_all(self.search_state.search_input_id.clone()),
611        ])
612    }
613
614    /// Handles opening the search and replace dialog.
615    ///
616    /// # Returns
617    ///
618    /// A `Task<Message>` that focuses and selects all in the search input
619    fn handle_open_search_replace_msg(&mut self) -> Task<Message> {
620        self.search_state.open_replace();
621        self.overlay_cache.clear();
622
623        // Focus the search input and select all text if any
624        Task::batch([
625            focus(self.search_state.search_input_id.clone()),
626            select_all(self.search_state.search_input_id.clone()),
627        ])
628    }
629
630    /// Handles closing the search dialog.
631    ///
632    /// # Returns
633    ///
634    /// A `Task<Message>` (currently Task::none())
635    fn handle_close_search_msg(&mut self) -> Task<Message> {
636        self.search_state.close();
637        self.overlay_cache.clear();
638        Task::none()
639    }
640
641    /// Handles search query text changes.
642    ///
643    /// # Arguments
644    ///
645    /// * `query` - The new search query
646    ///
647    /// # Returns
648    ///
649    /// A `Task<Message>` that scrolls to first match if any
650    fn handle_search_query_changed_msg(
651        &mut self,
652        query: &str,
653    ) -> Task<Message> {
654        self.search_state.set_query(query.to_string(), &self.buffer);
655        self.overlay_cache.clear();
656
657        // Move cursor to first match if any
658        if let Some(match_pos) = self.search_state.current_match() {
659            self.cursor = (match_pos.line, match_pos.col);
660            self.clear_selection();
661            return self.scroll_to_cursor();
662        }
663        Task::none()
664    }
665
666    /// Handles replace query text changes.
667    ///
668    /// # Arguments
669    ///
670    /// * `replace_text` - The new replacement text
671    ///
672    /// # Returns
673    ///
674    /// A `Task<Message>` (currently Task::none())
675    fn handle_replace_query_changed_msg(
676        &mut self,
677        replace_text: &str,
678    ) -> Task<Message> {
679        self.search_state.set_replace_with(replace_text.to_string());
680        Task::none()
681    }
682
683    /// Handles toggling case-sensitive search.
684    ///
685    /// # Returns
686    ///
687    /// A `Task<Message>` that scrolls to first match if any
688    fn handle_toggle_case_sensitive_msg(&mut self) -> Task<Message> {
689        self.search_state.toggle_case_sensitive(&self.buffer);
690        self.overlay_cache.clear();
691
692        // Move cursor to first match if any
693        if let Some(match_pos) = self.search_state.current_match() {
694            self.cursor = (match_pos.line, match_pos.col);
695            self.clear_selection();
696            return self.scroll_to_cursor();
697        }
698        Task::none()
699    }
700
701    /// Handles finding the next match.
702    ///
703    /// # Returns
704    ///
705    /// A `Task<Message>` that scrolls to the next match if any
706    fn handle_find_next_msg(&mut self) -> Task<Message> {
707        if !self.search_state.matches.is_empty() {
708            self.search_state.next_match();
709            if let Some(match_pos) = self.search_state.current_match() {
710                self.cursor = (match_pos.line, match_pos.col);
711                self.clear_selection();
712                self.overlay_cache.clear();
713                return self.scroll_to_cursor();
714            }
715        }
716        Task::none()
717    }
718
719    /// Handles finding the previous match.
720    ///
721    /// # Returns
722    ///
723    /// A `Task<Message>` that scrolls to the previous match if any
724    fn handle_find_previous_msg(&mut self) -> Task<Message> {
725        if !self.search_state.matches.is_empty() {
726            self.search_state.previous_match();
727            if let Some(match_pos) = self.search_state.current_match() {
728                self.cursor = (match_pos.line, match_pos.col);
729                self.clear_selection();
730                self.overlay_cache.clear();
731                return self.scroll_to_cursor();
732            }
733        }
734        Task::none()
735    }
736
737    /// Handles replacing the current match and moving to the next.
738    ///
739    /// # Returns
740    ///
741    /// A `Task<Message>` that scrolls to the next match if any
742    fn handle_replace_next_msg(&mut self) -> Task<Message> {
743        // Replace current match and move to next
744        if let Some(match_pos) = self.search_state.current_match() {
745            let query_len = self.search_state.query.chars().count();
746            let replace_text = self.search_state.replace_with.clone();
747
748            // Create and execute replace command
749            let mut cmd = ReplaceTextCommand::new(
750                &self.buffer,
751                (match_pos.line, match_pos.col),
752                query_len,
753                replace_text,
754                self.cursor,
755            );
756            cmd.execute(&mut self.buffer, &mut self.cursor);
757            self.history.push(Box::new(cmd));
758
759            // Update matches after replacement
760            self.search_state.update_matches(&self.buffer);
761
762            // Move to next match if available
763            if !self.search_state.matches.is_empty()
764                && let Some(next_match) = self.search_state.current_match()
765            {
766                self.cursor = (next_match.line, next_match.col);
767            }
768
769            self.clear_selection();
770            self.finish_edit_operation();
771            return self.scroll_to_cursor();
772        }
773        Task::none()
774    }
775
776    /// Handles replacing all matches.
777    ///
778    /// # Returns
779    ///
780    /// A `Task<Message>` that scrolls to cursor after replacement
781    fn handle_replace_all_msg(&mut self) -> Task<Message> {
782        // Perform a fresh search to find ALL matches (ignoring the display limit)
783        let all_matches = super::search::find_matches(
784            &self.buffer,
785            &self.search_state.query,
786            self.search_state.case_sensitive,
787            None, // No limit for Replace All
788        );
789
790        if !all_matches.is_empty() {
791            let query_len = self.search_state.query.chars().count();
792            let replace_text = self.search_state.replace_with.clone();
793
794            // Create composite command for undo
795            let mut composite =
796                CompositeCommand::new("Replace All".to_string());
797
798            // Process matches in reverse order (to preserve positions)
799            for match_pos in all_matches.iter().rev() {
800                let cmd = ReplaceTextCommand::new(
801                    &self.buffer,
802                    (match_pos.line, match_pos.col),
803                    query_len,
804                    replace_text.clone(),
805                    self.cursor,
806                );
807                composite.add(Box::new(cmd));
808            }
809
810            // Execute all replacements
811            composite.execute(&mut self.buffer, &mut self.cursor);
812            self.history.push(Box::new(composite));
813
814            // Update matches (should be empty now)
815            self.search_state.update_matches(&self.buffer);
816            self.clear_selection();
817            self.finish_edit_operation();
818            self.scroll_to_cursor()
819        } else {
820            Task::none()
821        }
822    }
823
824    /// Handles Tab key in search dialog (cycle forward).
825    ///
826    /// # Returns
827    ///
828    /// A `Task<Message>` that focuses the next field
829    fn handle_search_dialog_tab_msg(&mut self) -> Task<Message> {
830        // Cycle focus forward (Search → Replace → Search)
831        self.search_state.focus_next_field();
832
833        // Focus the appropriate input based on new focused_field
834        match self.search_state.focused_field {
835            crate::canvas_editor::search::SearchFocusedField::Search => {
836                focus(self.search_state.search_input_id.clone())
837            }
838            crate::canvas_editor::search::SearchFocusedField::Replace => {
839                focus(self.search_state.replace_input_id.clone())
840            }
841        }
842    }
843
844    /// Handles Shift+Tab key in search dialog (cycle backward).
845    ///
846    /// # Returns
847    ///
848    /// A `Task<Message>` that focuses the previous field
849    fn handle_search_dialog_shift_tab_msg(&mut self) -> Task<Message> {
850        // Cycle focus backward (Replace → Search → Replace)
851        self.search_state.focus_previous_field();
852
853        // Focus the appropriate input based on new focused_field
854        match self.search_state.focused_field {
855            crate::canvas_editor::search::SearchFocusedField::Search => {
856                focus(self.search_state.search_input_id.clone())
857            }
858            crate::canvas_editor::search::SearchFocusedField::Replace => {
859                focus(self.search_state.replace_input_id.clone())
860            }
861        }
862    }
863
864    // =========================================================================
865    // Focus and IME Handlers
866    // =========================================================================
867
868    /// Handles canvas focus gained event.
869    ///
870    /// # Returns
871    ///
872    /// A `Task<Message>` (currently Task::none())
873    fn handle_canvas_focus_gained_msg(&mut self) -> Task<Message> {
874        self.has_canvas_focus = true;
875        self.focus_locked = false; // Unlock focus when gained
876        self.show_cursor = true;
877        self.reset_cursor_blink();
878        self.overlay_cache.clear();
879        Task::none()
880    }
881
882    /// Handles canvas focus lost event.
883    ///
884    /// # Returns
885    ///
886    /// A `Task<Message>` (currently Task::none())
887    fn handle_canvas_focus_lost_msg(&mut self) -> Task<Message> {
888        self.has_canvas_focus = false;
889        self.focus_locked = true; // Lock focus when lost to prevent focus stealing
890        self.show_cursor = false;
891        self.ime_preedit = None;
892        self.overlay_cache.clear();
893        Task::none()
894    }
895
896    /// Handles IME opened event.
897    ///
898    /// Clears current preedit content to accept new input.
899    ///
900    /// # Returns
901    ///
902    /// A `Task<Message>` (currently Task::none())
903    fn handle_ime_opened_msg(&mut self) -> Task<Message> {
904        self.ime_preedit = None;
905        self.overlay_cache.clear();
906        Task::none()
907    }
908
909    /// Handles IME preedit event.
910    ///
911    /// Updates the preedit text and selection while the user is composing.
912    ///
913    /// # Arguments
914    ///
915    /// * `content` - The preedit text content
916    /// * `selection` - The selection range within the preedit text
917    ///
918    /// # Returns
919    ///
920    /// A `Task<Message>` (currently Task::none())
921    fn handle_ime_preedit_msg(
922        &mut self,
923        content: &str,
924        selection: &Option<std::ops::Range<usize>>,
925    ) -> Task<Message> {
926        if content.is_empty() {
927            self.ime_preedit = None;
928        } else {
929            self.ime_preedit = Some(ImePreedit {
930                content: content.to_string(),
931                selection: selection.clone(),
932            });
933        }
934
935        self.overlay_cache.clear();
936        Task::none()
937    }
938
939    /// Handles IME commit event.
940    ///
941    /// Inserts the committed text at the cursor position.
942    ///
943    /// # Arguments
944    ///
945    /// * `text` - The committed text
946    ///
947    /// # Returns
948    ///
949    /// A `Task<Message>` that scrolls to cursor after insertion
950    fn handle_ime_commit_msg(&mut self, text: &str) -> Task<Message> {
951        self.ime_preedit = None;
952
953        if text.is_empty() {
954            self.overlay_cache.clear();
955            return Task::none();
956        }
957
958        self.ensure_grouping_started("Typing");
959
960        self.paste_text(text);
961        self.finish_edit_operation();
962        self.scroll_to_cursor()
963    }
964
965    /// Handles IME closed event.
966    ///
967    /// Clears preedit state to return to normal input mode.
968    ///
969    /// # Returns
970    ///
971    /// A `Task<Message>` (currently Task::none())
972    fn handle_ime_closed_msg(&mut self) -> Task<Message> {
973        self.ime_preedit = None;
974        self.overlay_cache.clear();
975        Task::none()
976    }
977
978    // =========================================================================
979    // Complex Standalone Handlers
980    // =========================================================================
981
982    /// Handles cursor blink tick event.
983    ///
984    /// Updates cursor visibility for blinking animation.
985    ///
986    /// # Returns
987    ///
988    /// A `Task<Message>` (currently Task::none())
989    fn handle_tick_msg(&mut self) -> Task<Message> {
990        // Handle cursor blinking only if editor has focus
991        if self.has_focus()
992            && self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL
993        {
994            self.cursor_visible = !self.cursor_visible;
995            self.last_blink = super::Instant::now();
996            self.overlay_cache.clear();
997        }
998
999        // Hide cursor if editor doesn't have focus
1000        if !self.has_focus() {
1001            self.show_cursor = false;
1002        }
1003
1004        Task::none()
1005    }
1006
1007    /// Handles viewport scrolled event.
1008    ///
1009    /// Manages the virtual scrolling cache window to optimize rendering
1010    /// for large files. Only clears the cache when scrolling crosses the
1011    /// cached window boundary or when viewport dimensions change.
1012    ///
1013    /// # Arguments
1014    ///
1015    /// * `viewport` - The viewport information after scrolling
1016    ///
1017    /// # Returns
1018    ///
1019    /// A `Task<Message>` (currently Task::none())
1020    fn handle_scrolled_msg(
1021        &mut self,
1022        viewport: iced::widget::scrollable::Viewport,
1023    ) -> Task<Message> {
1024        // Virtual-scrolling cache window:
1025        // Instead of clearing the canvas cache for every small scroll,
1026        // we maintain a larger "render window" of visual lines around
1027        // the visible range. We only clear the cache and re-window
1028        // when the scroll crosses the window boundary or the viewport
1029        // size changes significantly. This prevents frequent re-highlighting
1030        // and layout recomputation for very large files while ensuring
1031        // the first scroll renders correctly without requiring a click.
1032        let new_scroll = viewport.absolute_offset().y;
1033        let new_height = viewport.bounds().height;
1034        let new_width = viewport.bounds().width;
1035        let scroll_changed = (self.viewport_scroll - new_scroll).abs() > 0.1;
1036        let visible_lines_count =
1037            (new_height / self.line_height).ceil() as usize + 2;
1038        let first_visible_line =
1039            (new_scroll / self.line_height).floor() as usize;
1040        let last_visible_line = first_visible_line + visible_lines_count;
1041        let margin = visible_lines_count
1042            * crate::canvas_editor::CACHE_WINDOW_MARGIN_MULTIPLIER;
1043        let window_start = first_visible_line.saturating_sub(margin);
1044        let window_end = last_visible_line + margin;
1045        // Decide whether we need to re-window the cache.
1046        // Special-case top-of-file: when window_start == 0, allow small forward scrolls
1047        // without forcing a rewindow, to avoid thrashing when the visible range is near 0.
1048        let need_rewindow =
1049            if self.cache_window_end_line > self.cache_window_start_line {
1050                let lower_boundary_trigger = self.cache_window_start_line > 0
1051                    && first_visible_line
1052                        < self
1053                            .cache_window_start_line
1054                            .saturating_add(visible_lines_count / 2);
1055                let upper_boundary_trigger = last_visible_line
1056                    > self
1057                        .cache_window_end_line
1058                        .saturating_sub(visible_lines_count / 2);
1059                lower_boundary_trigger || upper_boundary_trigger
1060            } else {
1061                true
1062            };
1063        // Clear cache when viewport dimensions change significantly
1064        // to ensure proper redraw (e.g., window resize)
1065        if (self.viewport_height - new_height).abs() > 1.0
1066            || (self.viewport_width - new_width).abs() > 1.0
1067            || (scroll_changed && need_rewindow)
1068        {
1069            self.cache_window_start_line = window_start;
1070            self.cache_window_end_line = window_end;
1071            self.last_first_visible_line = first_visible_line;
1072            self.content_cache.clear();
1073            self.overlay_cache.clear();
1074        }
1075        self.viewport_scroll = new_scroll;
1076        self.viewport_height = new_height;
1077        self.viewport_width = new_width;
1078        Task::none()
1079    }
1080
1081    // =========================================================================
1082    // Main Update Method
1083    // =========================================================================
1084
1085    /// Updates the editor state based on messages and returns scroll commands.
1086    ///
1087    /// # Arguments
1088    ///
1089    /// * `message` - The message to process for updating the editor state
1090    ///
1091    /// # Returns
1092    /// A `Task<Message>` for any asynchronous operations, such as scrolling to keep the cursor visible after state updates
1093    pub fn update(&mut self, message: &Message) -> Task<Message> {
1094        match message {
1095            // Text input operations
1096            Message::CharacterInput(ch) => self.handle_character_input_msg(*ch),
1097            Message::Tab => self.handle_tab(),
1098            Message::Enter => self.handle_enter(),
1099
1100            // Deletion operations
1101            Message::Backspace => self.handle_backspace(),
1102            Message::Delete => self.handle_delete(),
1103            Message::DeleteSelection => self.handle_delete_selection(),
1104
1105            // Navigation operations
1106            Message::ArrowKey(direction, shift) => {
1107                self.handle_arrow_key(*direction, *shift)
1108            }
1109            Message::Home(shift) => self.handle_home(*shift),
1110            Message::End(shift) => self.handle_end(*shift),
1111            Message::CtrlHome => self.handle_ctrl_home(),
1112            Message::CtrlEnd => self.handle_ctrl_end(),
1113            Message::PageUp => self.handle_page_up(),
1114            Message::PageDown => self.handle_page_down(),
1115
1116            // Mouse and selection operations
1117            Message::MouseClick(point) => self.handle_mouse_click_msg(*point),
1118            Message::MouseDrag(point) => self.handle_mouse_drag_msg(*point),
1119            Message::MouseRelease => self.handle_mouse_release_msg(),
1120
1121            // Clipboard operations
1122            Message::Copy => self.copy_selection(),
1123            Message::Paste(text) => self.handle_paste_msg(text),
1124
1125            // History operations
1126            Message::Undo => self.handle_undo_msg(),
1127            Message::Redo => self.handle_redo_msg(),
1128
1129            // Search and replace operations
1130            Message::OpenSearch => self.handle_open_search_msg(),
1131            Message::OpenSearchReplace => self.handle_open_search_replace_msg(),
1132            Message::CloseSearch => self.handle_close_search_msg(),
1133            Message::SearchQueryChanged(query) => {
1134                self.handle_search_query_changed_msg(query)
1135            }
1136            Message::ReplaceQueryChanged(text) => {
1137                self.handle_replace_query_changed_msg(text)
1138            }
1139            Message::ToggleCaseSensitive => {
1140                self.handle_toggle_case_sensitive_msg()
1141            }
1142            Message::FindNext => self.handle_find_next_msg(),
1143            Message::FindPrevious => self.handle_find_previous_msg(),
1144            Message::ReplaceNext => self.handle_replace_next_msg(),
1145            Message::ReplaceAll => self.handle_replace_all_msg(),
1146            Message::SearchDialogTab => self.handle_search_dialog_tab_msg(),
1147            Message::SearchDialogShiftTab => {
1148                self.handle_search_dialog_shift_tab_msg()
1149            }
1150            Message::FocusNavigationTab => self.handle_focus_navigation_tab(),
1151            Message::FocusNavigationShiftTab => {
1152                self.handle_focus_navigation_shift_tab()
1153            }
1154
1155            // Focus and IME operations
1156            Message::CanvasFocusGained => self.handle_canvas_focus_gained_msg(),
1157            Message::CanvasFocusLost => self.handle_canvas_focus_lost_msg(),
1158            Message::ImeOpened => self.handle_ime_opened_msg(),
1159            Message::ImePreedit(content, selection) => {
1160                self.handle_ime_preedit_msg(content, selection)
1161            }
1162            Message::ImeCommit(text) => self.handle_ime_commit_msg(text),
1163            Message::ImeClosed => self.handle_ime_closed_msg(),
1164
1165            // UI update operations
1166            Message::Tick => self.handle_tick_msg(),
1167            Message::Scrolled(viewport) => self.handle_scrolled_msg(*viewport),
1168        }
1169    }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use super::*;
1175    use crate::canvas_editor::ArrowDirection;
1176
1177    #[test]
1178    fn test_canvas_focus_lost() {
1179        let mut editor = CodeEditor::new("test", "rs");
1180        editor.has_canvas_focus = true;
1181
1182        let _ = editor.update(&Message::CanvasFocusLost);
1183
1184        assert!(!editor.has_canvas_focus);
1185        assert!(!editor.show_cursor);
1186        assert!(editor.focus_locked, "Focus should be locked when lost");
1187    }
1188
1189    #[test]
1190    fn test_canvas_focus_gained_resets_lock() {
1191        let mut editor = CodeEditor::new("test", "rs");
1192        editor.has_canvas_focus = false;
1193        editor.focus_locked = true;
1194
1195        let _ = editor.update(&Message::CanvasFocusGained);
1196
1197        assert!(editor.has_canvas_focus);
1198        assert!(
1199            !editor.focus_locked,
1200            "Focus lock should be reset when focus is gained"
1201        );
1202    }
1203
1204    #[test]
1205    fn test_focus_lock_state() {
1206        let mut editor = CodeEditor::new("test", "rs");
1207
1208        // Initially, focus should not be locked
1209        assert!(!editor.focus_locked);
1210
1211        // When focus is lost, it should be locked
1212        let _ = editor.update(&Message::CanvasFocusLost);
1213        assert!(editor.focus_locked, "Focus should be locked when lost");
1214
1215        // When focus is regained, it should be unlocked
1216        editor.request_focus();
1217        let _ = editor.update(&Message::CanvasFocusGained);
1218        assert!(!editor.focus_locked, "Focus should be unlocked when regained");
1219
1220        // Can manually reset focus lock
1221        editor.focus_locked = true;
1222        editor.reset_focus_lock();
1223        assert!(!editor.focus_locked, "Focus lock should be resetable");
1224    }
1225
1226    #[test]
1227    fn test_reset_focus_lock() {
1228        let mut editor = CodeEditor::new("test", "rs");
1229        editor.focus_locked = true;
1230
1231        editor.reset_focus_lock();
1232
1233        assert!(!editor.focus_locked);
1234    }
1235
1236    #[test]
1237    fn test_home_key() {
1238        let mut editor = CodeEditor::new("hello world", "py");
1239        editor.cursor = (0, 5); // Move to middle of line
1240        let _ = editor.update(&Message::Home(false));
1241        assert_eq!(editor.cursor, (0, 0));
1242    }
1243
1244    #[test]
1245    fn test_end_key() {
1246        let mut editor = CodeEditor::new("hello world", "py");
1247        editor.cursor = (0, 0);
1248        let _ = editor.update(&Message::End(false));
1249        assert_eq!(editor.cursor, (0, 11)); // Length of "hello world"
1250    }
1251
1252    #[test]
1253    fn test_arrow_key_with_shift_creates_selection() {
1254        let mut editor = CodeEditor::new("hello world", "py");
1255        editor.cursor = (0, 0);
1256
1257        // Shift+Right should start selection
1258        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
1259        assert!(editor.selection_start.is_some());
1260        assert!(editor.selection_end.is_some());
1261    }
1262
1263    #[test]
1264    fn test_arrow_key_without_shift_clears_selection() {
1265        let mut editor = CodeEditor::new("hello world", "py");
1266        editor.selection_start = Some((0, 0));
1267        editor.selection_end = Some((0, 5));
1268
1269        // Regular arrow key should clear selection
1270        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
1271        assert_eq!(editor.selection_start, None);
1272        assert_eq!(editor.selection_end, None);
1273    }
1274
1275    #[test]
1276    fn test_typing_with_selection() {
1277        let mut editor = CodeEditor::new("hello world", "py");
1278        // Ensure editor has focus for character input
1279        editor.request_focus();
1280        editor.has_canvas_focus = true;
1281        editor.focus_locked = false;
1282
1283        editor.selection_start = Some((0, 0));
1284        editor.selection_end = Some((0, 5));
1285
1286        let _ = editor.update(&Message::CharacterInput('X'));
1287        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
1288        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
1289        assert_eq!(editor.buffer.line(0), "Xhello world");
1290    }
1291
1292    #[test]
1293    fn test_ctrl_home() {
1294        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1295        editor.cursor = (2, 5); // Start at line 3, column 5
1296        let _ = editor.update(&Message::CtrlHome);
1297        assert_eq!(editor.cursor, (0, 0)); // Should move to beginning of document
1298    }
1299
1300    #[test]
1301    fn test_ctrl_end() {
1302        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1303        editor.cursor = (0, 0); // Start at beginning
1304        let _ = editor.update(&Message::CtrlEnd);
1305        assert_eq!(editor.cursor, (2, 5)); // Should move to end of last line (line3 has 5 chars)
1306    }
1307
1308    #[test]
1309    fn test_ctrl_home_clears_selection() {
1310        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1311        editor.cursor = (2, 5);
1312        editor.selection_start = Some((0, 0));
1313        editor.selection_end = Some((2, 5));
1314
1315        let _ = editor.update(&Message::CtrlHome);
1316        assert_eq!(editor.cursor, (0, 0));
1317        assert_eq!(editor.selection_start, None);
1318        assert_eq!(editor.selection_end, None);
1319    }
1320
1321    #[test]
1322    fn test_ctrl_end_clears_selection() {
1323        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1324        editor.cursor = (0, 0);
1325        editor.selection_start = Some((0, 0));
1326        editor.selection_end = Some((1, 3));
1327
1328        let _ = editor.update(&Message::CtrlEnd);
1329        assert_eq!(editor.cursor, (2, 5));
1330        assert_eq!(editor.selection_start, None);
1331        assert_eq!(editor.selection_end, None);
1332    }
1333
1334    #[test]
1335    fn test_scroll_sets_initial_cache_window() {
1336        let content =
1337            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1338        let mut editor = CodeEditor::new(&content, "py");
1339
1340        // Simulate initial viewport
1341        let height = 400.0;
1342        let width = 800.0;
1343        let scroll = 0.0;
1344
1345        // Expected derived ranges
1346        let visible_lines_count =
1347            (height / editor.line_height).ceil() as usize + 2;
1348        let first_visible_line = (scroll / editor.line_height).floor() as usize;
1349        let last_visible_line = first_visible_line + visible_lines_count;
1350        let margin = visible_lines_count * 2;
1351        let window_start = first_visible_line.saturating_sub(margin);
1352        let window_end = last_visible_line + margin;
1353
1354        // Apply logic similar to Message::Scrolled branch
1355        editor.viewport_height = height;
1356        editor.viewport_width = width;
1357        editor.viewport_scroll = -1.0;
1358        let scroll_changed = (editor.viewport_scroll - scroll).abs() > 0.1;
1359        let need_rewindow = true;
1360        if (editor.viewport_height - height).abs() > 1.0
1361            || (editor.viewport_width - width).abs() > 1.0
1362            || (scroll_changed && need_rewindow)
1363        {
1364            editor.cache_window_start_line = window_start;
1365            editor.cache_window_end_line = window_end;
1366            editor.last_first_visible_line = first_visible_line;
1367        }
1368        editor.viewport_scroll = scroll;
1369
1370        assert_eq!(editor.last_first_visible_line, first_visible_line);
1371        assert!(editor.cache_window_end_line > editor.cache_window_start_line);
1372        assert_eq!(editor.cache_window_start_line, window_start);
1373        assert_eq!(editor.cache_window_end_line, window_end);
1374    }
1375
1376    #[test]
1377    fn test_small_scroll_keeps_window() {
1378        let content =
1379            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1380        let mut editor = CodeEditor::new(&content, "py");
1381        let height = 400.0;
1382        let width = 800.0;
1383        let initial_scroll = 0.0;
1384        let visible_lines_count =
1385            (height / editor.line_height).ceil() as usize + 2;
1386        let first_visible_line =
1387            (initial_scroll / editor.line_height).floor() as usize;
1388        let last_visible_line = first_visible_line + visible_lines_count;
1389        let margin = visible_lines_count * 2;
1390        let window_start = first_visible_line.saturating_sub(margin);
1391        let window_end = last_visible_line + margin;
1392        editor.cache_window_start_line = window_start;
1393        editor.cache_window_end_line = window_end;
1394        editor.viewport_height = height;
1395        editor.viewport_width = width;
1396        editor.viewport_scroll = initial_scroll;
1397
1398        // Small scroll inside window
1399        let small_scroll =
1400            editor.line_height * (visible_lines_count as f32 / 4.0);
1401        let first_visible_line2 =
1402            (small_scroll / editor.line_height).floor() as usize;
1403        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1404        let lower_boundary_trigger = editor.cache_window_start_line > 0
1405            && first_visible_line2
1406                < editor
1407                    .cache_window_start_line
1408                    .saturating_add(visible_lines_count / 2);
1409        let upper_boundary_trigger = last_visible_line2
1410            > editor
1411                .cache_window_end_line
1412                .saturating_sub(visible_lines_count / 2);
1413        let need_rewindow = lower_boundary_trigger || upper_boundary_trigger;
1414
1415        assert!(!need_rewindow, "Small scroll should be inside the window");
1416        // Window remains unchanged
1417        assert_eq!(editor.cache_window_start_line, window_start);
1418        assert_eq!(editor.cache_window_end_line, window_end);
1419    }
1420
1421    #[test]
1422    fn test_large_scroll_rewindows() {
1423        let content =
1424            (0..1000).map(|i| format!("line{}\n", i)).collect::<String>();
1425        let mut editor = CodeEditor::new(&content, "py");
1426        let height = 400.0;
1427        let width = 800.0;
1428        let initial_scroll = 0.0;
1429        let visible_lines_count =
1430            (height / editor.line_height).ceil() as usize + 2;
1431        let first_visible_line =
1432            (initial_scroll / editor.line_height).floor() as usize;
1433        let last_visible_line = first_visible_line + visible_lines_count;
1434        let margin = visible_lines_count * 2;
1435        editor.cache_window_start_line =
1436            first_visible_line.saturating_sub(margin);
1437        editor.cache_window_end_line = last_visible_line + margin;
1438        editor.viewport_height = height;
1439        editor.viewport_width = width;
1440        editor.viewport_scroll = initial_scroll;
1441
1442        // Large scroll beyond window boundary
1443        let large_scroll =
1444            editor.line_height * ((visible_lines_count * 4) as f32);
1445        let first_visible_line2 =
1446            (large_scroll / editor.line_height).floor() as usize;
1447        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1448        let window_start2 = first_visible_line2.saturating_sub(margin);
1449        let window_end2 = last_visible_line2 + margin;
1450        let need_rewindow = first_visible_line2
1451            < editor
1452                .cache_window_start_line
1453                .saturating_add(visible_lines_count / 2)
1454            || last_visible_line2
1455                > editor
1456                    .cache_window_end_line
1457                    .saturating_sub(visible_lines_count / 2);
1458        assert!(need_rewindow, "Large scroll should trigger window update");
1459
1460        // Apply rewindow
1461        editor.cache_window_start_line = window_start2;
1462        editor.cache_window_end_line = window_end2;
1463        editor.last_first_visible_line = first_visible_line2;
1464
1465        assert_eq!(editor.cache_window_start_line, window_start2);
1466        assert_eq!(editor.cache_window_end_line, window_end2);
1467        assert_eq!(editor.last_first_visible_line, first_visible_line2);
1468    }
1469
1470    #[test]
1471    fn test_delete_selection_message() {
1472        let mut editor = CodeEditor::new("hello world", "py");
1473        editor.cursor = (0, 0);
1474        editor.selection_start = Some((0, 0));
1475        editor.selection_end = Some((0, 5));
1476
1477        let _ = editor.update(&Message::DeleteSelection);
1478        assert_eq!(editor.buffer.line(0), " world");
1479        assert_eq!(editor.cursor, (0, 0));
1480        assert_eq!(editor.selection_start, None);
1481        assert_eq!(editor.selection_end, None);
1482    }
1483
1484    #[test]
1485    fn test_delete_selection_multiline() {
1486        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1487        editor.cursor = (0, 2);
1488        editor.selection_start = Some((0, 2));
1489        editor.selection_end = Some((2, 2));
1490
1491        let _ = editor.update(&Message::DeleteSelection);
1492        assert_eq!(editor.buffer.line(0), "line3");
1493        assert_eq!(editor.cursor, (0, 2));
1494        assert_eq!(editor.selection_start, None);
1495    }
1496
1497    #[test]
1498    fn test_delete_selection_no_selection() {
1499        let mut editor = CodeEditor::new("hello world", "py");
1500        editor.cursor = (0, 5);
1501
1502        let _ = editor.update(&Message::DeleteSelection);
1503        // Should do nothing if there's no selection
1504        assert_eq!(editor.buffer.line(0), "hello world");
1505        assert_eq!(editor.cursor, (0, 5));
1506    }
1507
1508    #[test]
1509    #[allow(clippy::unwrap_used)]
1510    fn test_ime_preedit_and_commit_chinese() {
1511        let mut editor = CodeEditor::new("", "py");
1512        // Simulate IME opened
1513        let _ = editor.update(&Message::ImeOpened);
1514        assert!(editor.ime_preedit.is_none());
1515
1516        // Preedit with Chinese content and a selection range
1517        let content = "安全与合规".to_string();
1518        let selection = Some(0..3); // range aligned to UTF-8 character boundary
1519        let _ = editor
1520            .update(&Message::ImePreedit(content.clone(), selection.clone()));
1521
1522        assert!(editor.ime_preedit.is_some());
1523        assert_eq!(
1524            editor.ime_preedit.as_ref().unwrap().content.clone(),
1525            content
1526        );
1527        assert_eq!(
1528            editor.ime_preedit.as_ref().unwrap().selection.clone(),
1529            selection
1530        );
1531
1532        // Commit should insert the text and clear preedit
1533        let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
1534        assert!(editor.ime_preedit.is_none());
1535        assert_eq!(editor.buffer.line(0), "安全与合规");
1536        assert_eq!(editor.cursor, (0, "安全与合规".chars().count()));
1537    }
1538
1539    #[test]
1540    fn test_undo_char_insert() {
1541        let mut editor = CodeEditor::new("hello", "py");
1542        // Ensure editor has focus for character input
1543        editor.request_focus();
1544        editor.has_canvas_focus = true;
1545        editor.focus_locked = false;
1546
1547        editor.cursor = (0, 5);
1548
1549        // Type a character
1550        let _ = editor.update(&Message::CharacterInput('!'));
1551        assert_eq!(editor.buffer.line(0), "hello!");
1552        assert_eq!(editor.cursor, (0, 6));
1553
1554        // Undo should remove it (but first end the grouping)
1555        editor.history.end_group();
1556        let _ = editor.update(&Message::Undo);
1557        assert_eq!(editor.buffer.line(0), "hello");
1558        assert_eq!(editor.cursor, (0, 5));
1559    }
1560
1561    #[test]
1562    fn test_undo_redo_char_insert() {
1563        let mut editor = CodeEditor::new("hello", "py");
1564        // Ensure editor has focus for character input
1565        editor.request_focus();
1566        editor.has_canvas_focus = true;
1567        editor.focus_locked = false;
1568
1569        editor.cursor = (0, 5);
1570
1571        // Type a character
1572        let _ = editor.update(&Message::CharacterInput('!'));
1573        editor.history.end_group();
1574
1575        // Undo
1576        let _ = editor.update(&Message::Undo);
1577        assert_eq!(editor.buffer.line(0), "hello");
1578
1579        // Redo
1580        let _ = editor.update(&Message::Redo);
1581        assert_eq!(editor.buffer.line(0), "hello!");
1582        assert_eq!(editor.cursor, (0, 6));
1583    }
1584
1585    #[test]
1586    fn test_undo_backspace() {
1587        let mut editor = CodeEditor::new("hello", "py");
1588        editor.cursor = (0, 5);
1589
1590        // Backspace
1591        let _ = editor.update(&Message::Backspace);
1592        assert_eq!(editor.buffer.line(0), "hell");
1593        assert_eq!(editor.cursor, (0, 4));
1594
1595        // Undo
1596        let _ = editor.update(&Message::Undo);
1597        assert_eq!(editor.buffer.line(0), "hello");
1598        assert_eq!(editor.cursor, (0, 5));
1599    }
1600
1601    #[test]
1602    fn test_undo_newline() {
1603        let mut editor = CodeEditor::new("hello world", "py");
1604        editor.cursor = (0, 5);
1605
1606        // Insert newline
1607        let _ = editor.update(&Message::Enter);
1608        assert_eq!(editor.buffer.line(0), "hello");
1609        assert_eq!(editor.buffer.line(1), " world");
1610        assert_eq!(editor.cursor, (1, 0));
1611
1612        // Undo
1613        let _ = editor.update(&Message::Undo);
1614        assert_eq!(editor.buffer.line(0), "hello world");
1615        assert_eq!(editor.cursor, (0, 5));
1616    }
1617
1618    #[test]
1619    fn test_undo_grouped_typing() {
1620        let mut editor = CodeEditor::new("hello", "py");
1621        // Ensure editor has focus for character input
1622        editor.request_focus();
1623        editor.has_canvas_focus = true;
1624        editor.focus_locked = false;
1625
1626        editor.cursor = (0, 5);
1627
1628        // Type multiple characters (they should be grouped)
1629        let _ = editor.update(&Message::CharacterInput(' '));
1630        let _ = editor.update(&Message::CharacterInput('w'));
1631        let _ = editor.update(&Message::CharacterInput('o'));
1632        let _ = editor.update(&Message::CharacterInput('r'));
1633        let _ = editor.update(&Message::CharacterInput('l'));
1634        let _ = editor.update(&Message::CharacterInput('d'));
1635
1636        assert_eq!(editor.buffer.line(0), "hello world");
1637
1638        // End the group
1639        editor.history.end_group();
1640
1641        // Single undo should remove all grouped characters
1642        let _ = editor.update(&Message::Undo);
1643        assert_eq!(editor.buffer.line(0), "hello");
1644        assert_eq!(editor.cursor, (0, 5));
1645    }
1646
1647    #[test]
1648    fn test_navigation_ends_grouping() {
1649        let mut editor = CodeEditor::new("hello", "py");
1650        // Ensure editor has focus for character input
1651        editor.request_focus();
1652        editor.has_canvas_focus = true;
1653        editor.focus_locked = false;
1654
1655        editor.cursor = (0, 5);
1656
1657        // Type a character (starts grouping)
1658        let _ = editor.update(&Message::CharacterInput('!'));
1659        assert!(editor.is_grouping);
1660
1661        // Move cursor (ends grouping)
1662        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
1663        assert!(!editor.is_grouping);
1664
1665        // Type another character (starts new group)
1666        let _ = editor.update(&Message::CharacterInput('?'));
1667        assert!(editor.is_grouping);
1668
1669        editor.history.end_group();
1670
1671        // Two separate undo operations
1672        let _ = editor.update(&Message::Undo);
1673        assert_eq!(editor.buffer.line(0), "hello!");
1674
1675        let _ = editor.update(&Message::Undo);
1676        assert_eq!(editor.buffer.line(0), "hello");
1677    }
1678
1679    #[test]
1680    fn test_edit_increments_revision_and_clears_visual_lines_cache() {
1681        let mut editor = CodeEditor::new("hello", "rs");
1682        editor.request_focus();
1683        editor.has_canvas_focus = true;
1684        editor.focus_locked = false;
1685        editor.cursor = (0, 5);
1686
1687        let _ = editor.visual_lines_cached(800.0);
1688        assert!(
1689            editor.visual_lines_cache.borrow().is_some(),
1690            "visual_lines_cached should populate the cache"
1691        );
1692
1693        let previous_revision = editor.buffer_revision;
1694
1695        let _ = editor.update(&Message::CharacterInput('!'));
1696        assert_eq!(
1697            editor.buffer_revision,
1698            previous_revision.wrapping_add(1),
1699            "buffer_revision should change on buffer edits"
1700        );
1701        assert!(
1702            editor.visual_lines_cache.borrow().is_none(),
1703            "buffer edits should invalidate the visual lines cache"
1704        );
1705    }
1706
1707    #[test]
1708    fn test_multiple_undo_redo() {
1709        let mut editor = CodeEditor::new("a", "py");
1710        // Ensure editor has focus for character input
1711        editor.request_focus();
1712        editor.has_canvas_focus = true;
1713        editor.focus_locked = false;
1714
1715        editor.cursor = (0, 1);
1716
1717        // Make several changes
1718        let _ = editor.update(&Message::CharacterInput('b'));
1719        editor.history.end_group();
1720
1721        let _ = editor.update(&Message::CharacterInput('c'));
1722        editor.history.end_group();
1723
1724        let _ = editor.update(&Message::CharacterInput('d'));
1725        editor.history.end_group();
1726
1727        assert_eq!(editor.buffer.line(0), "abcd");
1728
1729        // Undo all
1730        let _ = editor.update(&Message::Undo);
1731        assert_eq!(editor.buffer.line(0), "abc");
1732
1733        let _ = editor.update(&Message::Undo);
1734        assert_eq!(editor.buffer.line(0), "ab");
1735
1736        let _ = editor.update(&Message::Undo);
1737        assert_eq!(editor.buffer.line(0), "a");
1738
1739        // Redo all
1740        let _ = editor.update(&Message::Redo);
1741        assert_eq!(editor.buffer.line(0), "ab");
1742
1743        let _ = editor.update(&Message::Redo);
1744        assert_eq!(editor.buffer.line(0), "abc");
1745
1746        let _ = editor.update(&Message::Redo);
1747        assert_eq!(editor.buffer.line(0), "abcd");
1748    }
1749
1750    #[test]
1751    fn test_delete_key_with_selection() {
1752        let mut editor = CodeEditor::new("hello world", "py");
1753        editor.selection_start = Some((0, 0));
1754        editor.selection_end = Some((0, 5));
1755        editor.cursor = (0, 5);
1756
1757        let _ = editor.update(&Message::Delete);
1758
1759        assert_eq!(editor.buffer.line(0), " world");
1760        assert_eq!(editor.cursor, (0, 0));
1761        assert_eq!(editor.selection_start, None);
1762        assert_eq!(editor.selection_end, None);
1763    }
1764
1765    #[test]
1766    fn test_delete_key_without_selection() {
1767        let mut editor = CodeEditor::new("hello", "py");
1768        editor.cursor = (0, 0);
1769
1770        let _ = editor.update(&Message::Delete);
1771
1772        // Should delete the 'h'
1773        assert_eq!(editor.buffer.line(0), "ello");
1774        assert_eq!(editor.cursor, (0, 0));
1775    }
1776
1777    #[test]
1778    fn test_backspace_with_selection() {
1779        let mut editor = CodeEditor::new("hello world", "py");
1780        editor.selection_start = Some((0, 6));
1781        editor.selection_end = Some((0, 11));
1782        editor.cursor = (0, 11);
1783
1784        let _ = editor.update(&Message::Backspace);
1785
1786        assert_eq!(editor.buffer.line(0), "hello ");
1787        assert_eq!(editor.cursor, (0, 6));
1788        assert_eq!(editor.selection_start, None);
1789        assert_eq!(editor.selection_end, None);
1790    }
1791
1792    #[test]
1793    fn test_backspace_without_selection() {
1794        let mut editor = CodeEditor::new("hello", "py");
1795        editor.cursor = (0, 5);
1796
1797        let _ = editor.update(&Message::Backspace);
1798
1799        // Should delete the 'o'
1800        assert_eq!(editor.buffer.line(0), "hell");
1801        assert_eq!(editor.cursor, (0, 4));
1802    }
1803
1804    #[test]
1805    fn test_delete_multiline_selection() {
1806        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1807        editor.selection_start = Some((0, 2));
1808        editor.selection_end = Some((2, 2));
1809        editor.cursor = (2, 2);
1810
1811        let _ = editor.update(&Message::Delete);
1812
1813        assert_eq!(editor.buffer.line(0), "line3");
1814        assert_eq!(editor.cursor, (0, 2));
1815        assert_eq!(editor.selection_start, None);
1816    }
1817
1818    #[test]
1819    fn test_canvas_focus_gained() {
1820        let mut editor = CodeEditor::new("hello world", "py");
1821        assert!(!editor.has_canvas_focus);
1822        assert!(!editor.show_cursor);
1823
1824        let _ = editor.update(&Message::CanvasFocusGained);
1825
1826        assert!(editor.has_canvas_focus);
1827        assert!(editor.show_cursor);
1828    }
1829
1830    #[test]
1831    fn test_mouse_click_gains_focus() {
1832        let mut editor = CodeEditor::new("hello world", "py");
1833        editor.has_canvas_focus = false;
1834        editor.show_cursor = false;
1835
1836        let _ =
1837            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
1838
1839        assert!(editor.has_canvas_focus);
1840        assert!(editor.show_cursor);
1841    }
1842}