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