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, IndentStyle,
12    Message, cursor_set,
13};
14
15// =========================================================================
16// Cursor adjustment helpers for multi-cursor editing
17// =========================================================================
18
19/// Describes the kind of edit applied to a single position.
20#[derive(Clone, Copy)]
21enum EditType {
22    /// Insert one char at `(edit_line, edit_col)`.
23    InsertChar,
24    /// Backspace: delete char at `(edit_line, edit_col - 1)`.
25    DeleteCharBack,
26    /// Delete-forward: delete char at `(edit_line, edit_col)`.
27    DeleteCharForward,
28    /// Enter: split `edit_line` at `edit_col`; new line has `extra` indent chars.
29    InsertNewline { indent_len: usize },
30    /// Backspace-at-col-0: merge `edit_line` into `edit_line - 1`.
31    /// `extra` = length of the previous line before merge.
32    MergePrev { prev_line_len: usize },
33    /// Delete-at-end-of-line: merge `edit_line + 1` into `edit_line`.
34    /// `extra` = length of `edit_line` before merge.
35    MergeNext { edit_line_len: usize },
36}
37
38/// Adjusts a single `(line, col)` pair after an edit.
39fn adjust_pos(
40    pos: &mut (usize, usize),
41    edit_line: usize,
42    edit_col: usize,
43    kind: EditType,
44) {
45    match kind {
46        EditType::InsertChar => {
47            if pos.0 == edit_line && pos.1 >= edit_col {
48                pos.1 += 1;
49            }
50        }
51        EditType::DeleteCharBack => {
52            if edit_col > 0 && pos.0 == edit_line && pos.1 > edit_col - 1 {
53                pos.1 -= 1;
54            }
55        }
56        EditType::DeleteCharForward => {
57            if pos.0 == edit_line && pos.1 > edit_col {
58                pos.1 -= 1;
59            }
60        }
61        EditType::InsertNewline { indent_len } => {
62            if pos.0 > edit_line {
63                pos.0 += 1;
64            } else if pos.0 == edit_line && pos.1 >= edit_col {
65                pos.0 += 1;
66                pos.1 = pos.1 - edit_col + indent_len;
67            }
68        }
69        EditType::MergePrev { prev_line_len } => {
70            if pos.0 == edit_line {
71                pos.0 -= 1;
72                pos.1 += prev_line_len;
73            } else if pos.0 > edit_line {
74                pos.0 -= 1;
75            }
76        }
77        EditType::MergeNext { edit_line_len } => {
78            if pos.0 == edit_line + 1 {
79                pos.0 = edit_line;
80                pos.1 += edit_line_len;
81            } else if pos.0 > edit_line + 1 {
82                pos.0 -= 1;
83            }
84        }
85    }
86}
87
88/// Adjusts all cursors except `skip_idx` after an edit at `(edit_line, edit_col)`.
89fn adjust_other_cursors(
90    cursors: &mut [cursor_set::Cursor],
91    skip_idx: usize,
92    edit_line: usize,
93    edit_col: usize,
94    kind: EditType,
95) {
96    for (i, cursor) in cursors.iter_mut().enumerate() {
97        if i == skip_idx {
98            continue;
99        }
100        adjust_pos(&mut cursor.position, edit_line, edit_col, kind);
101        if let Some(ref mut anchor) = cursor.anchor {
102            adjust_pos(anchor, edit_line, edit_col, kind);
103        }
104    }
105}
106
107impl CodeEditor {
108    // =========================================================================
109    // Helper Methods
110    // =========================================================================
111
112    /// Performs common cleanup operations after edit operations.
113    ///
114    /// This method should be called after any operation that modifies the buffer content.
115    /// It resets the cursor blink animation, refreshes search matches if search is active,
116    /// and invalidates all caches that depend on buffer content or layout:
117    /// - `buffer_revision` is bumped to invalidate layout-derived caches
118    /// - `visual_lines_cache` is cleared so wrapping is recalculated on next use
119    /// - `content_cache` and `overlay_cache` are cleared to rebuild canvas geometry
120    fn finish_edit_operation(&mut self) {
121        self.reset_cursor_blink();
122        self.refresh_search_matches_if_needed();
123        // The exact revision value is not semantically meaningful; it only needs
124        // to change on edits, so `wrapping_add` is sufficient and overflow-safe.
125        self.buffer_revision = self.buffer_revision.wrapping_add(1);
126        *self.visual_lines_cache.borrow_mut() = None;
127        self.content_cache.clear();
128        self.overlay_cache.clear();
129        self.enqueue_lsp_change();
130    }
131
132    /// Performs common cleanup operations after navigation operations.
133    ///
134    /// This method should be called after cursor movement operations.
135    /// It resets the cursor blink animation and invalidates only the overlay
136    /// rendering cache. Cursor movement and selection changes do not modify the
137    /// buffer content, so keeping the content cache intact avoids unnecessary
138    /// re-rendering of syntax-highlighted text.
139    fn finish_navigation_operation(&mut self) {
140        self.reset_cursor_blink();
141        self.overlay_cache.clear();
142    }
143
144    /// Starts command grouping with the given label if not already grouping.
145    ///
146    /// This is used for smart undo functionality, allowing multiple related
147    /// operations to be undone as a single unit.
148    ///
149    /// # Arguments
150    ///
151    /// * `label` - A descriptive label for the group of commands
152    fn ensure_grouping_started(&mut self, label: &str) {
153        if !self.is_grouping {
154            self.history.begin_group(label);
155            self.is_grouping = true;
156        }
157    }
158
159    /// Ends command grouping if currently active.
160    ///
161    /// This should be called when a series of related operations is complete,
162    /// or when starting a new type of operation that shouldn't be grouped
163    /// with previous operations.
164    fn end_grouping_if_active(&mut self) {
165        if self.is_grouping {
166            self.history.end_group();
167            self.is_grouping = false;
168        }
169    }
170
171    /// Deletes all active selections across every cursor and performs cleanup.
172    ///
173    /// # Returns
174    ///
175    /// `true` if at least one selection was deleted, `false` if no cursor had a selection
176    fn delete_selection_if_present(&mut self) -> bool {
177        if self.cursors.iter().any(|c| c.has_selection()) {
178            self.delete_selection();
179            self.finish_edit_operation();
180            true
181        } else {
182            false
183        }
184    }
185
186    // =========================================================================
187    // Text Input Handlers
188    // =========================================================================
189
190    /// Handles character input message operations.
191    ///
192    /// Inserts a character at the current cursor position and adds it to the
193    /// undo history. Characters are grouped together for smart undo.
194    /// Only processes input when the editor has active focus and is not locked.
195    ///
196    /// # Arguments
197    ///
198    /// * `ch` - The character to insert
199    ///
200    /// # Returns
201    ///
202    /// A `Task<Message>` that scrolls to keep the cursor visible (including
203    /// horizontal scroll when wrap is disabled)
204    fn handle_character_input_msg(&mut self, ch: char) -> Task<Message> {
205        // Guard clause: only process character input if editor has focus and is not locked
206        if !self.has_focus() {
207            return Task::none();
208        }
209
210        // Start grouping if not already grouping (for smart undo)
211        self.ensure_grouping_started("Typing");
212
213        // Multi-cursor: build a sorted index list (descending document order)
214        // so that edits at higher positions don't invalidate lower positions.
215        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
216        order.sort_by(|&a, &b| {
217            self.cursors.as_slice()[b]
218                .position
219                .cmp(&self.cursors.as_slice()[a].position)
220        });
221
222        for &idx in &order {
223            let cursor = &self.cursors.as_slice()[idx];
224            // Insert at the selection start when the cursor has an active selection,
225            // otherwise insert at the cursor position.
226            let pos = match cursor.anchor {
227                Some(anchor) if anchor < cursor.position => anchor,
228                _ => cursor.position,
229            };
230            let mut cmd = InsertCharCommand::new(pos.0, pos.1, ch, pos);
231            let mut cursor_pos = pos;
232            cmd.execute(&mut self.buffer, &mut cursor_pos);
233            self.cursors.as_mut_slice()[idx].position = cursor_pos;
234            adjust_other_cursors(
235                self.cursors.as_mut_slice(),
236                idx,
237                pos.0,
238                pos.1,
239                EditType::InsertChar,
240            );
241            self.history.push(Box::new(cmd));
242        }
243
244        self.finish_edit_operation();
245
246        // Auto-trigger LSP completion for identifier characters and trigger characters
247        if ch.is_alphanumeric() || ch == '_' || ch == '.' {
248            self.lsp_flush_pending_changes();
249            self.lsp_request_completion();
250        }
251
252        self.scroll_to_cursor()
253    }
254
255    /// Handles Tab key press (inserts 4 spaces).
256    ///
257    /// # Returns
258    ///
259    /// A `Task<Message>` that scrolls to keep the cursor visible (including
260    /// horizontal scroll when wrap is disabled)
261    fn handle_tab(&mut self) -> Task<Message> {
262        self.ensure_grouping_started("Tab");
263
264        // Multi-cursor: process in descending document order
265        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
266        order.sort_by(|&a, &b| {
267            self.cursors.as_slice()[b]
268                .position
269                .cmp(&self.cursors.as_slice()[a].position)
270        });
271
272        for &idx in &order {
273            let pos = self.cursors.as_slice()[idx].position;
274            match self.indent_style {
275                IndentStyle::Spaces(n) => {
276                    let mut cursor_pos = pos;
277                    for _i in 0..n as usize {
278                        let current_col = cursor_pos.1;
279                        let mut cmd = InsertCharCommand::new(
280                            pos.0,
281                            current_col,
282                            ' ',
283                            cursor_pos,
284                        );
285                        cmd.execute(&mut self.buffer, &mut cursor_pos);
286                        adjust_other_cursors(
287                            self.cursors.as_mut_slice(),
288                            idx,
289                            pos.0,
290                            current_col,
291                            EditType::InsertChar,
292                        );
293                        self.history.push(Box::new(cmd));
294                    }
295                    self.cursors.as_mut_slice()[idx].position = cursor_pos;
296                }
297                IndentStyle::Tab => {
298                    let mut cmd =
299                        InsertCharCommand::new(pos.0, pos.1, '\t', pos);
300                    let mut cursor_pos = pos;
301                    cmd.execute(&mut self.buffer, &mut cursor_pos);
302                    adjust_other_cursors(
303                        self.cursors.as_mut_slice(),
304                        idx,
305                        pos.0,
306                        pos.1,
307                        EditType::InsertChar,
308                    );
309                    self.cursors.as_mut_slice()[idx].position = cursor_pos;
310                    self.history.push(Box::new(cmd));
311                }
312            }
313        }
314
315        self.finish_edit_operation();
316        self.scroll_to_cursor()
317    }
318
319    /// Handles Tab key press for focus navigation (when search dialog is not open).
320    ///
321    /// # Returns
322    ///
323    /// A `Task<Message>` that may navigate focus to another editor
324    fn handle_focus_navigation_tab(&mut self) -> Task<Message> {
325        // Only handle focus navigation if search dialog is not open
326        if !self.search_state.is_open {
327            // Lose focus from current editor
328            self.has_canvas_focus = false;
329            self.show_cursor = false;
330
331            // Return a task that could potentially focus another editor
332            // This implements focus chain management by allowing the parent application
333            // to handle focus navigation between multiple editors
334            Task::none()
335        } else {
336            Task::none()
337        }
338    }
339
340    /// Handles Shift+Tab key press for focus navigation (when search dialog is not open).
341    ///
342    /// # Returns
343    ///
344    /// A `Task<Message>` that may navigate focus to another editor
345    fn handle_focus_navigation_shift_tab(&mut self) -> Task<Message> {
346        // Only handle focus navigation if search dialog is not open
347        if !self.search_state.is_open {
348            // Lose focus from current editor
349            self.has_canvas_focus = false;
350            self.show_cursor = false;
351
352            // Return a task that could potentially focus another editor
353            // This implements focus chain management by allowing the parent application
354            // to handle focus navigation between multiple editors
355            Task::none()
356        } else {
357            Task::none()
358        }
359    }
360
361    /// Handles Enter key press (inserts newline).
362    ///
363    /// # Returns
364    ///
365    /// A `Task<Message>` that scrolls to keep the cursor visible
366    fn handle_enter(&mut self) -> Task<Message> {
367        // End grouping on enter
368        self.end_grouping_if_active();
369
370        // Multi-cursor: process in descending document order
371        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
372        order.sort_by(|&a, &b| {
373            self.cursors.as_slice()[b]
374                .position
375                .cmp(&self.cursors.as_slice()[a].position)
376        });
377
378        for &idx in &order {
379            let pos = self.cursors.as_slice()[idx].position;
380
381            // Copy leading whitespace of the current line to the new line (if enabled)
382            let indent: String = if self.auto_indent_enabled {
383                self.buffer
384                    .line(pos.0)
385                    .chars()
386                    .take_while(|c| c.is_whitespace())
387                    .collect()
388            } else {
389                String::new()
390            };
391            let indent_len = indent.chars().count();
392
393            let mut cmd =
394                InsertNewlineCommand::with_indent(pos.0, pos.1, pos, indent);
395            let mut cursor_pos = pos;
396            cmd.execute(&mut self.buffer, &mut cursor_pos);
397            self.cursors.as_mut_slice()[idx].position = cursor_pos;
398            adjust_other_cursors(
399                self.cursors.as_mut_slice(),
400                idx,
401                pos.0,
402                pos.1,
403                EditType::InsertNewline { indent_len },
404            );
405            self.history.push(Box::new(cmd));
406        }
407
408        self.finish_edit_operation();
409        self.scroll_to_cursor()
410    }
411
412    // =========================================================================
413    // Deletion Handlers
414    // =========================================================================
415
416    /// Handles Backspace key press.
417    ///
418    /// If there's a selection, deletes the selection. Otherwise, deletes the
419    /// character before the cursor.
420    ///
421    /// # Returns
422    ///
423    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
424    fn handle_backspace(&mut self) -> Task<Message> {
425        // End grouping on backspace (separate from typing)
426        self.end_grouping_if_active();
427
428        // If any cursor has a selection, delete all selections first
429        if self.delete_selection_if_present() {
430            return self.scroll_to_cursor();
431        }
432
433        // Multi-cursor: process in descending document order
434        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
435        order.sort_by(|&a, &b| {
436            self.cursors.as_slice()[b]
437                .position
438                .cmp(&self.cursors.as_slice()[a].position)
439        });
440
441        for &idx in &order {
442            let pos = self.cursors.as_slice()[idx].position;
443            // Determine edit type for adjusting other cursors
444            let edit_kind = if pos.1 > 0 {
445                EditType::DeleteCharBack
446            } else if pos.0 > 0 {
447                let prev_line_len = self.buffer.line_len(pos.0 - 1);
448                EditType::MergePrev { prev_line_len }
449            } else {
450                // At very start of document: nothing to delete
451                continue;
452            };
453            let mut cmd =
454                DeleteCharCommand::new(&self.buffer, pos.0, pos.1, pos);
455            let mut cursor_pos = pos;
456            cmd.execute(&mut self.buffer, &mut cursor_pos);
457            self.cursors.as_mut_slice()[idx].position = cursor_pos;
458            adjust_other_cursors(
459                self.cursors.as_mut_slice(),
460                idx,
461                pos.0,
462                pos.1,
463                edit_kind,
464            );
465            self.history.push(Box::new(cmd));
466        }
467
468        self.finish_edit_operation();
469        self.scroll_to_cursor()
470    }
471
472    /// Handles Delete key press.
473    ///
474    /// If there's a selection, deletes the selection. Otherwise, deletes the
475    /// character after the cursor.
476    ///
477    /// # Returns
478    ///
479    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
480    fn handle_delete(&mut self) -> Task<Message> {
481        // End grouping on delete
482        self.end_grouping_if_active();
483
484        // If any cursor has a selection, delete all selections first
485        if self.delete_selection_if_present() {
486            return self.scroll_to_cursor();
487        }
488
489        // Multi-cursor: process in descending document order
490        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
491        order.sort_by(|&a, &b| {
492            self.cursors.as_slice()[b]
493                .position
494                .cmp(&self.cursors.as_slice()[a].position)
495        });
496
497        for &idx in &order {
498            let pos = self.cursors.as_slice()[idx].position;
499            let line_len = self.buffer.line_len(pos.0);
500            let edit_kind = if pos.1 < line_len {
501                EditType::DeleteCharForward
502            } else if pos.0 + 1 < self.buffer.line_count() {
503                EditType::MergeNext { edit_line_len: line_len }
504            } else {
505                // At very end of document: nothing to delete
506                continue;
507            };
508            let mut cmd =
509                DeleteForwardCommand::new(&self.buffer, pos.0, pos.1, pos);
510            let mut cursor_pos = pos;
511            cmd.execute(&mut self.buffer, &mut cursor_pos);
512            self.cursors.as_mut_slice()[idx].position = cursor_pos;
513            adjust_other_cursors(
514                self.cursors.as_mut_slice(),
515                idx,
516                pos.0,
517                pos.1,
518                edit_kind,
519            );
520            self.history.push(Box::new(cmd));
521        }
522
523        self.finish_edit_operation();
524        Task::none()
525    }
526
527    /// Handles explicit selection deletion (Shift+Delete).
528    ///
529    /// Deletes the selected text if a selection exists.
530    ///
531    /// # Returns
532    ///
533    /// A `Task<Message>` that scrolls to keep the cursor visible
534    fn handle_delete_selection(&mut self) -> Task<Message> {
535        // End grouping on delete selection
536        self.end_grouping_if_active();
537
538        if self.cursors.iter().any(|c| c.has_selection()) {
539            self.delete_selection();
540            self.finish_edit_operation();
541            self.scroll_to_cursor()
542        } else {
543            Task::none()
544        }
545    }
546
547    // =========================================================================
548    // Navigation Handlers
549    // =========================================================================
550
551    /// Handles arrow key navigation.
552    ///
553    /// # Arguments
554    ///
555    /// * `direction` - The direction of movement
556    /// * `shift_pressed` - Whether Shift is held (for selection)
557    ///
558    /// # Returns
559    ///
560    /// A `Task<Message>` that scrolls to keep the cursor visible
561    fn handle_arrow_key(
562        &mut self,
563        direction: ArrowDirection,
564        shift_pressed: bool,
565    ) -> Task<Message> {
566        // End grouping on navigation
567        self.end_grouping_if_active();
568
569        if shift_pressed {
570            // Set anchor on ALL cursors that don't yet have one
571            for cursor in self.cursors.as_mut_slice() {
572                if cursor.anchor.is_none() {
573                    cursor.set_anchor();
574                }
575            }
576            self.move_cursor(direction);
577        } else {
578            // Clear all selections, then move all cursors
579            self.clear_selection();
580            self.move_cursor(direction);
581        }
582        self.finish_navigation_operation();
583        self.scroll_to_cursor()
584    }
585
586    /// Handles Home key press.
587    ///
588    /// Moves the cursor to the start of the current line.
589    ///
590    /// # Arguments
591    ///
592    /// * `shift_pressed` - Whether Shift is held (for selection)
593    ///
594    /// # Returns
595    ///
596    /// A `Task<Message>` that scrolls to keep the cursor visible (including
597    /// horizontal scroll back to x=0 when wrap is disabled)
598    fn handle_home(&mut self, shift_pressed: bool) -> Task<Message> {
599        if shift_pressed {
600            for cursor in self.cursors.as_mut_slice() {
601                if cursor.anchor.is_none() {
602                    cursor.set_anchor();
603                }
604                cursor.position.1 = 0;
605            }
606        } else {
607            self.clear_selection();
608            for cursor in self.cursors.as_mut_slice() {
609                cursor.position.1 = 0;
610            }
611        }
612        self.cursors.sort_and_merge();
613        self.finish_navigation_operation();
614        self.scroll_to_cursor()
615    }
616
617    /// Handles End key press.
618    ///
619    /// Moves the cursor to the end of the current line.
620    ///
621    /// # Arguments
622    ///
623    /// * `shift_pressed` - Whether Shift is held (for selection)
624    ///
625    /// # Returns
626    ///
627    /// A `Task<Message>` that scrolls to keep the cursor visible (including
628    /// horizontal scroll to end of line when wrap is disabled)
629    fn handle_end(&mut self, shift_pressed: bool) -> Task<Message> {
630        if shift_pressed {
631            for cursor in self.cursors.as_mut_slice() {
632                if cursor.anchor.is_none() {
633                    cursor.set_anchor();
634                }
635                cursor.position.1 = self.buffer.line_len(cursor.position.0);
636            }
637        } else {
638            self.clear_selection();
639            for cursor in self.cursors.as_mut_slice() {
640                cursor.position.1 = self.buffer.line_len(cursor.position.0);
641            }
642        }
643        self.cursors.sort_and_merge();
644        self.finish_navigation_operation();
645        self.scroll_to_cursor()
646    }
647
648    /// Handles Ctrl+Home key press.
649    ///
650    /// Moves the cursor to the beginning of the document.
651    ///
652    /// # Returns
653    ///
654    /// A `Task<Message>` that scrolls to keep the cursor visible
655    fn handle_ctrl_home(&mut self) -> Task<Message> {
656        // Move cursor to the beginning of the document
657        self.clear_selection();
658        self.cursors.set_single((0, 0));
659        self.finish_navigation_operation();
660        self.scroll_to_cursor()
661    }
662
663    /// Handles Ctrl+End key press.
664    ///
665    /// Moves the cursor to the end of the document.
666    ///
667    /// # Returns
668    ///
669    /// A `Task<Message>` that scrolls to keep the cursor visible
670    fn handle_ctrl_end(&mut self) -> Task<Message> {
671        // Move cursor to the end of the document
672        self.clear_selection();
673        let last_line = self.buffer.line_count().saturating_sub(1);
674        let last_col = self.buffer.line_len(last_line);
675        self.cursors.set_single((last_line, last_col));
676        self.finish_navigation_operation();
677        self.scroll_to_cursor()
678    }
679
680    /// Handles Page Up key press.
681    ///
682    /// Scrolls the view up by one page.
683    ///
684    /// # Returns
685    ///
686    /// A `Task<Message>` that scrolls to keep the cursor visible
687    fn handle_page_up(&mut self) -> Task<Message> {
688        self.page_up();
689        self.finish_navigation_operation();
690        self.scroll_to_cursor()
691    }
692
693    /// Handles Page Down key press.
694    ///
695    /// Scrolls the view down by one page.
696    ///
697    /// # Returns
698    ///
699    /// A `Task<Message>` that scrolls to keep the cursor visible
700    fn handle_page_down(&mut self) -> Task<Message> {
701        self.page_down();
702        self.finish_navigation_operation();
703        self.scroll_to_cursor()
704    }
705
706    /// Handles direct navigation to an explicit logical position.
707    ///
708    /// # Arguments
709    ///
710    /// * `line` - Target line index (0-based)
711    /// * `col` - Target column index (0-based)
712    ///
713    /// # Returns
714    ///
715    /// A `Task<Message>` that scrolls to keep the cursor visible
716    fn handle_goto_position(
717        &mut self,
718        line: usize,
719        col: usize,
720    ) -> Task<Message> {
721        // End grouping on navigation command
722        self.end_grouping_if_active();
723        self.set_cursor(line, col)
724    }
725
726    // =========================================================================
727    // Mouse and Selection Handlers
728    // =========================================================================
729
730    /// Handles mouse click operations.
731    ///
732    /// Sets focus, ends command grouping, positions cursor, starts selection tracking.
733    ///
734    /// # Arguments
735    ///
736    /// * `point` - The click position
737    ///
738    /// # Returns
739    ///
740    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
741    fn handle_mouse_click_msg(&mut self, point: iced::Point) -> Task<Message> {
742        // Capture focus when clicked using the new focus method
743        self.request_focus();
744
745        // Set internal canvas focus state
746        self.has_canvas_focus = true;
747
748        // End grouping on mouse click
749        self.end_grouping_if_active();
750
751        // Regular click collapses any multi-cursor state to a single cursor
752        // positioned at the click location.
753        self.cursors.remove_all_but_primary();
754
755        self.handle_mouse_click(point);
756        self.reset_cursor_blink();
757        // Clear selection on click, then set anchor for potential drag selection
758        self.clear_selection();
759        self.is_dragging = true;
760        self.cursors.primary_mut().set_anchor();
761
762        // Show cursor when focused
763        self.show_cursor = true;
764
765        Task::none()
766    }
767
768    /// Handles mouse drag operations for selection.
769    ///
770    /// # Arguments
771    ///
772    /// * `point` - The drag position
773    ///
774    /// # Returns
775    ///
776    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
777    fn handle_mouse_drag_msg(&mut self, point: iced::Point) -> Task<Message> {
778        if self.is_dragging {
779            let before_pos = self.cursors.primary_position();
780            self.handle_mouse_drag(point);
781            if self.cursors.primary_position() != before_pos {
782                // Mouse move events can be very frequent. Only invalidate the
783                // overlay cache if the drag actually changed selection/cursor.
784                self.overlay_cache.clear();
785            }
786        }
787        Task::none()
788    }
789
790    /// Handles mouse release operations.
791    ///
792    /// # Returns
793    ///
794    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
795    fn handle_mouse_release_msg(&mut self) -> Task<Message> {
796        self.is_dragging = false;
797        Task::none()
798    }
799
800    // =========================================================================
801    // Clipboard Handlers
802    // =========================================================================
803
804    /// Handles paste operations.
805    ///
806    /// If the provided text is empty, reads from clipboard. Otherwise pastes
807    /// the provided text at the cursor position.
808    ///
809    /// # Arguments
810    ///
811    /// * `text` - The text to paste (empty string triggers clipboard read)
812    ///
813    /// # Returns
814    ///
815    /// A `Task<Message>` that may read clipboard or scroll to cursor
816    fn handle_paste_msg(&mut self, text: &str) -> Task<Message> {
817        // End grouping on paste
818        self.end_grouping_if_active();
819
820        // If text is empty, we need to read from clipboard
821        if text.is_empty() {
822            // Return a task that reads clipboard and chains to paste
823            iced::clipboard::read().and_then(|clipboard_text| {
824                Task::done(Message::Paste(clipboard_text))
825            })
826        } else {
827            // We have the text, paste it
828            self.paste_text(text);
829            self.finish_edit_operation();
830            self.scroll_to_cursor()
831        }
832    }
833
834    // =========================================================================
835    // History (Undo/Redo) Handlers
836    // =========================================================================
837
838    /// Handles undo operations.
839    ///
840    /// # Returns
841    ///
842    /// A `Task<Message>` that scrolls to cursor if undo succeeded
843    fn handle_undo_msg(&mut self) -> Task<Message> {
844        // End any current grouping before undoing
845        self.end_grouping_if_active();
846
847        let mut cursor_pos = self.cursors.primary_position();
848        if self.history.undo(&mut self.buffer, &mut cursor_pos) {
849            self.cursors.primary_mut().position = cursor_pos;
850            self.clear_selection();
851            self.finish_edit_operation();
852            self.scroll_to_cursor()
853        } else {
854            Task::none()
855        }
856    }
857
858    /// Handles redo operations.
859    ///
860    /// # Returns
861    ///
862    /// A `Task<Message>` that scrolls to cursor if redo succeeded
863    fn handle_redo_msg(&mut self) -> Task<Message> {
864        let mut cursor_pos = self.cursors.primary_position();
865        if self.history.redo(&mut self.buffer, &mut cursor_pos) {
866            self.cursors.primary_mut().position = cursor_pos;
867            self.clear_selection();
868            self.finish_edit_operation();
869            self.scroll_to_cursor()
870        } else {
871            Task::none()
872        }
873    }
874
875    // =========================================================================
876    // Search and Replace Handlers
877    // =========================================================================
878
879    /// Handles opening the search dialog.
880    ///
881    /// # Returns
882    ///
883    /// A `Task<Message>` that focuses and selects all in the search input
884    fn handle_open_search_msg(&mut self) -> Task<Message> {
885        self.search_state.open_search();
886        self.overlay_cache.clear();
887
888        // Focus the search input and select all text if any
889        Task::batch([
890            focus(self.search_state.search_input_id.clone()),
891            select_all(self.search_state.search_input_id.clone()),
892        ])
893    }
894
895    /// Handles opening the search and replace dialog.
896    ///
897    /// # Returns
898    ///
899    /// A `Task<Message>` that focuses and selects all in the search input
900    fn handle_open_search_replace_msg(&mut self) -> Task<Message> {
901        self.search_state.open_replace();
902        self.overlay_cache.clear();
903
904        // Focus the search input and select all text if any
905        Task::batch([
906            focus(self.search_state.search_input_id.clone()),
907            select_all(self.search_state.search_input_id.clone()),
908        ])
909    }
910
911    /// Handles closing the search dialog.
912    ///
913    /// # Returns
914    ///
915    /// A `Task<Message>` (currently Task::none())
916    fn handle_close_search_msg(&mut self) -> Task<Message> {
917        // Escape with multiple cursors and no open search: collapse to primary cursor
918        if self.cursors.is_multi() && !self.search_state.is_open {
919            self.cursors.remove_all_but_primary();
920            self.overlay_cache.clear();
921            return Task::none();
922        }
923        self.search_state.close();
924        self.overlay_cache.clear();
925        Task::none()
926    }
927
928    /// Handles search query text changes.
929    ///
930    /// # Arguments
931    ///
932    /// * `query` - The new search query
933    ///
934    /// # Returns
935    ///
936    /// A `Task<Message>` that scrolls to first match if any
937    fn handle_search_query_changed_msg(
938        &mut self,
939        query: &str,
940    ) -> Task<Message> {
941        self.search_state.set_query(query.to_string(), &self.buffer);
942        self.overlay_cache.clear();
943
944        // Move cursor to first match if any
945        if let Some(match_pos) = self.search_state.current_match() {
946            self.cursors.primary_mut().position =
947                (match_pos.line, match_pos.col);
948            self.clear_selection();
949            return self.scroll_to_cursor();
950        }
951        Task::none()
952    }
953
954    /// Handles replace query text changes.
955    ///
956    /// # Arguments
957    ///
958    /// * `replace_text` - The new replacement text
959    ///
960    /// # Returns
961    ///
962    /// A `Task<Message>` (currently Task::none())
963    fn handle_replace_query_changed_msg(
964        &mut self,
965        replace_text: &str,
966    ) -> Task<Message> {
967        self.search_state.set_replace_with(replace_text.to_string());
968        Task::none()
969    }
970
971    /// Handles toggling case-sensitive search.
972    ///
973    /// # Returns
974    ///
975    /// A `Task<Message>` that scrolls to first match if any
976    fn handle_toggle_case_sensitive_msg(&mut self) -> Task<Message> {
977        self.search_state.toggle_case_sensitive(&self.buffer);
978        self.overlay_cache.clear();
979
980        // Move cursor to first match if any
981        if let Some(match_pos) = self.search_state.current_match() {
982            self.cursors.primary_mut().position =
983                (match_pos.line, match_pos.col);
984            self.clear_selection();
985            return self.scroll_to_cursor();
986        }
987        Task::none()
988    }
989
990    /// Handles finding the next match.
991    ///
992    /// # Returns
993    ///
994    /// A `Task<Message>` that scrolls to the next match if any
995    fn handle_find_next_msg(&mut self) -> Task<Message> {
996        if !self.search_state.matches.is_empty() {
997            self.search_state.next_match();
998            if let Some(match_pos) = self.search_state.current_match() {
999                self.cursors.primary_mut().position =
1000                    (match_pos.line, match_pos.col);
1001                self.clear_selection();
1002                self.overlay_cache.clear();
1003                return self.scroll_to_cursor();
1004            }
1005        }
1006        Task::none()
1007    }
1008
1009    /// Handles finding the previous match.
1010    ///
1011    /// # Returns
1012    ///
1013    /// A `Task<Message>` that scrolls to the previous match if any
1014    fn handle_find_previous_msg(&mut self) -> Task<Message> {
1015        if !self.search_state.matches.is_empty() {
1016            self.search_state.previous_match();
1017            if let Some(match_pos) = self.search_state.current_match() {
1018                self.cursors.primary_mut().position =
1019                    (match_pos.line, match_pos.col);
1020                self.clear_selection();
1021                self.overlay_cache.clear();
1022                return self.scroll_to_cursor();
1023            }
1024        }
1025        Task::none()
1026    }
1027
1028    /// Handles replacing the current match and moving to the next.
1029    ///
1030    /// # Returns
1031    ///
1032    /// A `Task<Message>` that scrolls to the next match if any
1033    fn handle_replace_next_msg(&mut self) -> Task<Message> {
1034        // Replace current match and move to next
1035        if let Some(match_pos) = self.search_state.current_match() {
1036            let query_len = self.search_state.query.chars().count();
1037            let replace_text = self.search_state.replace_with.clone();
1038
1039            // Create and execute replace command
1040            let pos = self.cursors.primary_position();
1041            let mut cmd = ReplaceTextCommand::new(
1042                &self.buffer,
1043                (match_pos.line, match_pos.col),
1044                query_len,
1045                replace_text,
1046                pos,
1047            );
1048            let mut cursor_pos = pos;
1049            cmd.execute(&mut self.buffer, &mut cursor_pos);
1050            self.cursors.primary_mut().position = cursor_pos;
1051            self.history.push(Box::new(cmd));
1052
1053            // Update matches after replacement
1054            self.search_state.update_matches(&self.buffer);
1055
1056            // Move to next match if available
1057            if !self.search_state.matches.is_empty()
1058                && let Some(next_match) = self.search_state.current_match()
1059            {
1060                self.cursors.primary_mut().position =
1061                    (next_match.line, next_match.col);
1062            }
1063
1064            self.clear_selection();
1065            self.finish_edit_operation();
1066            return self.scroll_to_cursor();
1067        }
1068        Task::none()
1069    }
1070
1071    /// Handles replacing all matches.
1072    ///
1073    /// # Returns
1074    ///
1075    /// A `Task<Message>` that scrolls to cursor after replacement
1076    fn handle_replace_all_msg(&mut self) -> Task<Message> {
1077        // Perform a fresh search to find ALL matches (ignoring the display limit)
1078        let all_matches = super::search::find_matches(
1079            &self.buffer,
1080            &self.search_state.query,
1081            self.search_state.case_sensitive,
1082            None, // No limit for Replace All
1083        );
1084
1085        if !all_matches.is_empty() {
1086            let query_len = self.search_state.query.chars().count();
1087            let replace_text = self.search_state.replace_with.clone();
1088
1089            // Create composite command for undo
1090            let mut composite =
1091                CompositeCommand::new("Replace All".to_string());
1092
1093            // Process matches in reverse order (to preserve positions)
1094            for match_pos in all_matches.iter().rev() {
1095                let pos = self.cursors.primary_position();
1096                let cmd = ReplaceTextCommand::new(
1097                    &self.buffer,
1098                    (match_pos.line, match_pos.col),
1099                    query_len,
1100                    replace_text.clone(),
1101                    pos,
1102                );
1103                composite.add(Box::new(cmd));
1104            }
1105
1106            // Execute all replacements
1107            let mut cursor_pos = self.cursors.primary_position();
1108            composite.execute(&mut self.buffer, &mut cursor_pos);
1109            self.cursors.primary_mut().position = cursor_pos;
1110            self.history.push(Box::new(composite));
1111
1112            // Update matches (should be empty now)
1113            self.search_state.update_matches(&self.buffer);
1114            self.clear_selection();
1115            self.finish_edit_operation();
1116            self.scroll_to_cursor()
1117        } else {
1118            Task::none()
1119        }
1120    }
1121
1122    /// Handles Tab key in search dialog (cycle forward).
1123    ///
1124    /// # Returns
1125    ///
1126    /// A `Task<Message>` that focuses the next field
1127    fn handle_search_dialog_tab_msg(&mut self) -> Task<Message> {
1128        // Cycle focus forward (Search → Replace → Search)
1129        self.search_state.focus_next_field();
1130
1131        // Focus the appropriate input based on new focused_field
1132        match self.search_state.focused_field {
1133            crate::canvas_editor::search::SearchFocusedField::Search => {
1134                focus(self.search_state.search_input_id.clone())
1135            }
1136            crate::canvas_editor::search::SearchFocusedField::Replace => {
1137                focus(self.search_state.replace_input_id.clone())
1138            }
1139        }
1140    }
1141
1142    /// Handles Shift+Tab key in search dialog (cycle backward).
1143    ///
1144    /// # Returns
1145    ///
1146    /// A `Task<Message>` that focuses the previous field
1147    fn handle_search_dialog_shift_tab_msg(&mut self) -> Task<Message> {
1148        // Cycle focus backward (Replace → Search → Replace)
1149        self.search_state.focus_previous_field();
1150
1151        // Focus the appropriate input based on new focused_field
1152        match self.search_state.focused_field {
1153            crate::canvas_editor::search::SearchFocusedField::Search => {
1154                focus(self.search_state.search_input_id.clone())
1155            }
1156            crate::canvas_editor::search::SearchFocusedField::Replace => {
1157                focus(self.search_state.replace_input_id.clone())
1158            }
1159        }
1160    }
1161
1162    // =========================================================================
1163    // Focus and IME Handlers
1164    // =========================================================================
1165
1166    /// Handles canvas focus gained event.
1167    ///
1168    /// # Returns
1169    ///
1170    /// A `Task<Message>` (currently Task::none())
1171    fn handle_canvas_focus_gained_msg(&mut self) -> Task<Message> {
1172        self.has_canvas_focus = true;
1173        self.focus_locked = false; // Unlock focus when gained
1174        self.show_cursor = true;
1175        self.reset_cursor_blink();
1176        self.overlay_cache.clear();
1177        Task::none()
1178    }
1179
1180    /// Handles canvas focus lost event.
1181    ///
1182    /// # Returns
1183    ///
1184    /// A `Task<Message>` (currently Task::none())
1185    fn handle_canvas_focus_lost_msg(&mut self) -> Task<Message> {
1186        self.has_canvas_focus = false;
1187        self.focus_locked = true; // Lock focus when lost to prevent focus stealing
1188        self.show_cursor = false;
1189        self.ime_preedit = None;
1190        self.overlay_cache.clear();
1191        Task::none()
1192    }
1193
1194    /// Handles IME opened event.
1195    ///
1196    /// Clears current preedit content to accept new input.
1197    ///
1198    /// # Returns
1199    ///
1200    /// A `Task<Message>` (currently Task::none())
1201    fn handle_ime_opened_msg(&mut self) -> Task<Message> {
1202        self.ime_preedit = None;
1203        self.overlay_cache.clear();
1204        Task::none()
1205    }
1206
1207    /// Handles IME preedit event.
1208    ///
1209    /// Updates the preedit text and selection while the user is composing.
1210    ///
1211    /// # Arguments
1212    ///
1213    /// * `content` - The preedit text content
1214    /// * `selection` - The selection range within the preedit text
1215    ///
1216    /// # Returns
1217    ///
1218    /// A `Task<Message>` (currently Task::none())
1219    fn handle_ime_preedit_msg(
1220        &mut self,
1221        content: &str,
1222        selection: &Option<std::ops::Range<usize>>,
1223    ) -> Task<Message> {
1224        if content.is_empty() {
1225            self.ime_preedit = None;
1226        } else {
1227            self.ime_preedit = Some(ImePreedit {
1228                content: content.to_string(),
1229                selection: selection.clone(),
1230            });
1231        }
1232
1233        self.overlay_cache.clear();
1234        Task::none()
1235    }
1236
1237    /// Handles IME commit event.
1238    ///
1239    /// Inserts the committed text at the cursor position.
1240    ///
1241    /// # Arguments
1242    ///
1243    /// * `text` - The committed text
1244    ///
1245    /// # Returns
1246    ///
1247    /// A `Task<Message>` that scrolls to cursor after insertion
1248    fn handle_ime_commit_msg(&mut self, text: &str) -> Task<Message> {
1249        self.ime_preedit = None;
1250
1251        if text.is_empty() {
1252            self.overlay_cache.clear();
1253            return Task::none();
1254        }
1255
1256        self.ensure_grouping_started("Typing");
1257
1258        self.paste_text(text);
1259        self.finish_edit_operation();
1260        self.scroll_to_cursor()
1261    }
1262
1263    /// Handles IME closed event.
1264    ///
1265    /// Clears preedit state to return to normal input mode.
1266    ///
1267    /// # Returns
1268    ///
1269    /// A `Task<Message>` (currently Task::none())
1270    fn handle_ime_closed_msg(&mut self) -> Task<Message> {
1271        self.ime_preedit = None;
1272        self.overlay_cache.clear();
1273        Task::none()
1274    }
1275
1276    // =========================================================================
1277    // Complex Standalone Handlers
1278    // =========================================================================
1279
1280    /// Handles cursor blink tick event.
1281    ///
1282    /// Updates cursor visibility for blinking animation.
1283    ///
1284    /// # Returns
1285    ///
1286    /// A `Task<Message>` (currently Task::none())
1287    fn handle_tick_msg(&mut self) -> Task<Message> {
1288        // Handle cursor blinking only if editor has focus
1289        if self.has_focus()
1290            && self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL
1291        {
1292            self.cursor_visible = !self.cursor_visible;
1293            self.last_blink = super::Instant::now();
1294            self.overlay_cache.clear();
1295        }
1296
1297        // Hide cursor if editor doesn't have focus
1298        if !self.has_focus() {
1299            self.show_cursor = false;
1300        }
1301
1302        Task::none()
1303    }
1304
1305    /// Handles viewport scrolled event.
1306    ///
1307    /// Manages the virtual scrolling cache window to optimize rendering
1308    /// for large files. Only clears the cache when scrolling crosses the
1309    /// cached window boundary or when viewport dimensions change.
1310    ///
1311    /// # Arguments
1312    ///
1313    /// * `viewport` - The viewport information after scrolling
1314    ///
1315    /// # Returns
1316    ///
1317    /// A `Task<Message>` (currently Task::none())
1318    fn handle_scrolled_msg(
1319        &mut self,
1320        viewport: iced::widget::scrollable::Viewport,
1321    ) -> Task<Message> {
1322        // Virtual-scrolling cache window:
1323        // Instead of clearing the canvas cache for every small scroll,
1324        // we maintain a larger "render window" of visual lines around
1325        // the visible range. We only clear the cache and re-window
1326        // when the scroll crosses the window boundary or the viewport
1327        // size changes significantly. This prevents frequent re-highlighting
1328        // and layout recomputation for very large files while ensuring
1329        // the first scroll renders correctly without requiring a click.
1330        let new_scroll = viewport.absolute_offset().y;
1331        let new_height = viewport.bounds().height;
1332        let new_width = viewport.bounds().width;
1333        let scroll_changed = (self.viewport_scroll - new_scroll).abs() > 0.1;
1334        let visible_lines_count =
1335            (new_height / self.line_height).ceil() as usize + 2;
1336        let first_visible_line =
1337            (new_scroll / self.line_height).floor() as usize;
1338        let last_visible_line = first_visible_line + visible_lines_count;
1339        let margin = visible_lines_count
1340            * crate::canvas_editor::CACHE_WINDOW_MARGIN_MULTIPLIER;
1341        let window_start = first_visible_line.saturating_sub(margin);
1342        let window_end = last_visible_line + margin;
1343        // Decide whether we need to re-window the cache.
1344        // Special-case top-of-file: when window_start == 0, allow small forward scrolls
1345        // without forcing a rewindow, to avoid thrashing when the visible range is near 0.
1346        let need_rewindow =
1347            if self.cache_window_end_line > self.cache_window_start_line {
1348                let lower_boundary_trigger = self.cache_window_start_line > 0
1349                    && first_visible_line
1350                        < self
1351                            .cache_window_start_line
1352                            .saturating_add(visible_lines_count / 2);
1353                let upper_boundary_trigger = last_visible_line
1354                    > self
1355                        .cache_window_end_line
1356                        .saturating_sub(visible_lines_count / 2);
1357                lower_boundary_trigger || upper_boundary_trigger
1358            } else {
1359                true
1360            };
1361        // Clear cache when viewport dimensions change significantly
1362        // to ensure proper redraw (e.g., window resize)
1363        if (self.viewport_height - new_height).abs() > 1.0
1364            || (self.viewport_width - new_width).abs() > 1.0
1365            || (scroll_changed && need_rewindow)
1366        {
1367            self.cache_window_start_line = window_start;
1368            self.cache_window_end_line = window_end;
1369            self.last_first_visible_line = first_visible_line;
1370            self.content_cache.clear();
1371            self.overlay_cache.clear();
1372        }
1373        self.viewport_scroll = new_scroll;
1374        self.viewport_height = new_height;
1375        self.viewport_width = new_width;
1376        Task::none()
1377    }
1378
1379    /// Handles horizontal scrollbar scrolled event (only active when wrap is disabled).
1380    ///
1381    /// Updates `horizontal_scroll_offset` and clears render caches when the offset
1382    /// changes by more than 0.1 pixels to avoid unnecessary redraws.
1383    ///
1384    /// # Arguments
1385    ///
1386    /// * `viewport` - The viewport information after scrolling
1387    ///
1388    /// # Returns
1389    ///
1390    /// A `Task<Message>` (currently `Task::none()`)
1391    fn handle_horizontal_scrolled_msg(
1392        &mut self,
1393        viewport: iced::widget::scrollable::Viewport,
1394    ) -> Task<Message> {
1395        let new_x = viewport.absolute_offset().x;
1396        if (self.horizontal_scroll_offset - new_x).abs() > 0.1 {
1397            self.horizontal_scroll_offset = new_x;
1398            self.content_cache.clear();
1399            self.overlay_cache.clear();
1400        }
1401        Task::none()
1402    }
1403
1404    // =========================================================================
1405    // Multi-cursor operations
1406    // =========================================================================
1407
1408    /// Handles Alt+Click: adds a new cursor at the clicked position without
1409    /// disturbing existing cursors.
1410    ///
1411    /// # Arguments
1412    ///
1413    /// * `point` - Canvas-local position of the click
1414    ///
1415    /// # Returns
1416    ///
1417    /// `Task::none()` — no async work needed
1418    fn handle_alt_click_msg(&mut self, point: iced::Point) -> Task<Message> {
1419        if let Some(pos) = self.calculate_cursor_from_point(point) {
1420            self.cursors.add_cursor(pos);
1421            self.overlay_cache.clear();
1422            self.reset_cursor_blink();
1423        }
1424        Task::none()
1425    }
1426
1427    /// Handles Ctrl+Alt+Up: adds a cursor on the line above the primary cursor,
1428    /// at the same column (clamped to line length).
1429    ///
1430    /// # Returns
1431    ///
1432    /// `Task::none()`
1433    fn handle_add_cursor_above_msg(&mut self) -> Task<Message> {
1434        let (line, col) = self.cursors.primary_position();
1435        if line == 0 {
1436            return Task::none();
1437        }
1438        let new_line = line - 1;
1439        let new_col = col.min(self.buffer.line_len(new_line));
1440        self.cursors.add_cursor((new_line, new_col));
1441        self.overlay_cache.clear();
1442        self.reset_cursor_blink();
1443        Task::none()
1444    }
1445
1446    /// Handles Ctrl+Alt+Down: adds a cursor on the line below the primary cursor,
1447    /// at the same column (clamped to line length).
1448    ///
1449    /// # Returns
1450    ///
1451    /// `Task::none()`
1452    fn handle_add_cursor_below_msg(&mut self) -> Task<Message> {
1453        let (line, col) = self.cursors.primary_position();
1454        let last_line = self.buffer.line_count().saturating_sub(1);
1455        if line >= last_line {
1456            return Task::none();
1457        }
1458        let new_line = line + 1;
1459        let new_col = col.min(self.buffer.line_len(new_line));
1460        self.cursors.add_cursor((new_line, new_col));
1461        self.overlay_cache.clear();
1462        self.reset_cursor_blink();
1463        Task::none()
1464    }
1465
1466    /// Handles Ctrl+D: selects the next occurrence of the text currently selected
1467    /// by the primary cursor, or the word under the primary cursor if there is no
1468    /// selection. A new cursor with that selection is added.
1469    ///
1470    /// # Returns
1471    ///
1472    /// `Task::none()`
1473    fn handle_select_next_occurrence_msg(&mut self) -> Task<Message> {
1474        // Determine the search text: selected text on primary cursor, or word under cursor
1475        let search_text = if let Some(text) = self.get_selected_text() {
1476            text
1477        } else {
1478            // Select word under primary cursor first
1479            let (line, col) = self.cursors.primary_position();
1480            let line_str = self.buffer.line(line).to_string();
1481            let word_start = Self::word_start_in_line(&line_str, col);
1482            let word_end = Self::word_end_in_line(&line_str, col);
1483            if word_start == word_end {
1484                return Task::none();
1485            }
1486            // Apply selection to primary cursor and stop: the next Ctrl+D call
1487            // will find the next occurrence (selection will be non-empty then).
1488            self.cursors.primary_mut().anchor = Some((line, word_start));
1489            self.cursors.primary_mut().position = (line, word_end);
1490            self.overlay_cache.clear();
1491            return Task::none();
1492        };
1493
1494        if search_text.is_empty() {
1495            return Task::none();
1496        }
1497
1498        // Find the search start position: just after the last cursor's selection end
1499        let search_start = self
1500            .cursors
1501            .as_slice()
1502            .last()
1503            .map(|last| {
1504                last.selection_range()
1505                    .map(|(_, end)| end)
1506                    .unwrap_or(last.position)
1507            })
1508            .unwrap_or((0, 0));
1509
1510        // Search forward from search_start for the next occurrence
1511        let (start_line, start_col) = search_start;
1512        let line_count = self.buffer.line_count();
1513
1514        for line_offset in 0..=line_count {
1515            let line_idx = (start_line + line_offset) % line_count;
1516            let line_str = self.buffer.line(line_idx).to_string();
1517            let chars: Vec<char> = line_str.chars().collect();
1518
1519            // On the first iteration, start after start_col; on wrap-around, start from 0
1520            let search_col = if line_offset == 0 { start_col } else { 0 };
1521
1522            // Build substring from search_col onward (char-indexed)
1523            let prefix_bytes: usize =
1524                chars.iter().take(search_col).map(|c| c.len_utf8()).sum();
1525            let haystack = &line_str[prefix_bytes..];
1526
1527            // The search_text is also char-based; find it as a substring
1528            if let Some(byte_offset) = haystack.find(search_text.as_str()) {
1529                // Convert byte_offset back to char offset
1530                let char_start =
1531                    search_col + haystack[..byte_offset].chars().count();
1532                let char_end = char_start + search_text.chars().count();
1533
1534                // Build cursor with selection for the found occurrence
1535                let found_cursor = cursor_set::Cursor {
1536                    position: (line_idx, char_end),
1537                    anchor: Some((line_idx, char_start)),
1538                };
1539                self.cursors.add_cursor_with_selection(found_cursor);
1540                self.overlay_cache.clear();
1541                self.reset_cursor_blink();
1542                return self.scroll_to_cursor();
1543            }
1544        }
1545
1546        Task::none()
1547    }
1548
1549    // =========================================================================
1550    // Main Update Method
1551    // =========================================================================
1552
1553    /// Updates the editor state based on messages and returns scroll commands.
1554    ///
1555    /// # Arguments
1556    ///
1557    /// * `message` - The message to process for updating the editor state
1558    ///
1559    /// # Returns
1560    /// A `Task<Message>` for any asynchronous operations, such as scrolling to keep the cursor visible after state updates
1561    pub fn update(&mut self, message: &Message) -> Task<Message> {
1562        match message {
1563            // Text input operations
1564            Message::CharacterInput(ch) => self.handle_character_input_msg(*ch),
1565            Message::Tab => self.handle_tab(),
1566            Message::Enter => self.handle_enter(),
1567
1568            // Deletion operations
1569            Message::Backspace => self.handle_backspace(),
1570            Message::Delete => self.handle_delete(),
1571            Message::DeleteSelection => self.handle_delete_selection(),
1572
1573            // Navigation operations
1574            Message::ArrowKey(direction, shift) => {
1575                self.handle_arrow_key(*direction, *shift)
1576            }
1577            Message::Home(shift) => self.handle_home(*shift),
1578            Message::End(shift) => self.handle_end(*shift),
1579            Message::CtrlHome => self.handle_ctrl_home(),
1580            Message::CtrlEnd => self.handle_ctrl_end(),
1581            Message::GotoPosition(line, col) => {
1582                self.handle_goto_position(*line, *col)
1583            }
1584            Message::PageUp => self.handle_page_up(),
1585            Message::PageDown => self.handle_page_down(),
1586
1587            // Mouse and selection operations
1588            Message::MouseClick(point) => self.handle_mouse_click_msg(*point),
1589            Message::MouseDrag(point) => self.handle_mouse_drag_msg(*point),
1590            Message::MouseHover(point) => self.handle_mouse_drag_msg(*point),
1591            Message::MouseRelease => self.handle_mouse_release_msg(),
1592
1593            // Clipboard operations
1594            Message::Copy => self.copy_selection(),
1595            Message::Paste(text) => self.handle_paste_msg(text),
1596
1597            // History operations
1598            Message::Undo => self.handle_undo_msg(),
1599            Message::Redo => self.handle_redo_msg(),
1600
1601            // Search and replace operations
1602            Message::OpenSearch => self.handle_open_search_msg(),
1603            Message::OpenSearchReplace => self.handle_open_search_replace_msg(),
1604            Message::CloseSearch => self.handle_close_search_msg(),
1605            Message::SearchQueryChanged(query) => {
1606                self.handle_search_query_changed_msg(query)
1607            }
1608            Message::ReplaceQueryChanged(text) => {
1609                self.handle_replace_query_changed_msg(text)
1610            }
1611            Message::ToggleCaseSensitive => {
1612                self.handle_toggle_case_sensitive_msg()
1613            }
1614            Message::FindNext => self.handle_find_next_msg(),
1615            Message::FindPrevious => self.handle_find_previous_msg(),
1616            Message::ReplaceNext => self.handle_replace_next_msg(),
1617            Message::ReplaceAll => self.handle_replace_all_msg(),
1618            Message::SearchDialogTab => self.handle_search_dialog_tab_msg(),
1619            Message::SearchDialogShiftTab => {
1620                self.handle_search_dialog_shift_tab_msg()
1621            }
1622            Message::FocusNavigationTab => self.handle_focus_navigation_tab(),
1623            Message::FocusNavigationShiftTab => {
1624                self.handle_focus_navigation_shift_tab()
1625            }
1626
1627            // Focus and IME operations
1628            Message::CanvasFocusGained => self.handle_canvas_focus_gained_msg(),
1629            Message::CanvasFocusLost => self.handle_canvas_focus_lost_msg(),
1630            Message::ImeOpened => self.handle_ime_opened_msg(),
1631            Message::ImePreedit(content, selection) => {
1632                self.handle_ime_preedit_msg(content, selection)
1633            }
1634            Message::ImeCommit(text) => self.handle_ime_commit_msg(text),
1635            Message::ImeClosed => self.handle_ime_closed_msg(),
1636
1637            // UI update operations
1638            Message::Tick => self.handle_tick_msg(),
1639            Message::Scrolled(viewport) => self.handle_scrolled_msg(*viewport),
1640            Message::HorizontalScrolled(viewport) => {
1641                self.handle_horizontal_scrolled_msg(*viewport)
1642            }
1643
1644            // Handle the "Jump to Definition" action triggered by Ctrl+Click.
1645            // Currently, this returns `Task::none()` as the actual navigation logic
1646            // is delegated to the `LspClient` implementation or handled elsewhere.
1647            Message::JumpClick(_point) => Task::none(),
1648
1649            // Multi-cursor operations
1650            Message::AltClick(point) => self.handle_alt_click_msg(*point),
1651            Message::AddCursorAbove => self.handle_add_cursor_above_msg(),
1652            Message::AddCursorBelow => self.handle_add_cursor_below_msg(),
1653            Message::SelectNextOccurrence => {
1654                self.handle_select_next_occurrence_msg()
1655            }
1656        }
1657    }
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662    use super::*;
1663    use crate::canvas_editor::ArrowDirection;
1664
1665    #[test]
1666    fn test_horizontal_scroll_initial_state() {
1667        let editor = CodeEditor::new("short line", "rs");
1668        assert!(
1669            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
1670            "Initial horizontal scroll offset should be 0"
1671        );
1672    }
1673
1674    #[test]
1675    fn test_set_wrap_enabled_resets_horizontal_offset() {
1676        let mut editor = CodeEditor::new("long line", "rs");
1677        editor.wrap_enabled = false;
1678        // Simulate a non-zero horizontal scroll
1679        editor.horizontal_scroll_offset = 100.0;
1680
1681        // Re-enabling wrap should reset horizontal offset
1682        editor.set_wrap_enabled(true);
1683        assert!(
1684            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
1685            "Horizontal scroll offset should be reset when wrap is re-enabled"
1686        );
1687    }
1688
1689    #[test]
1690    fn test_canvas_focus_lost() {
1691        let mut editor = CodeEditor::new("test", "rs");
1692        editor.has_canvas_focus = true;
1693
1694        let _ = editor.update(&Message::CanvasFocusLost);
1695
1696        assert!(!editor.has_canvas_focus);
1697        assert!(!editor.show_cursor);
1698        assert!(editor.focus_locked, "Focus should be locked when lost");
1699    }
1700
1701    #[test]
1702    fn test_canvas_focus_gained_resets_lock() {
1703        let mut editor = CodeEditor::new("test", "rs");
1704        editor.has_canvas_focus = false;
1705        editor.focus_locked = true;
1706
1707        let _ = editor.update(&Message::CanvasFocusGained);
1708
1709        assert!(editor.has_canvas_focus);
1710        assert!(
1711            !editor.focus_locked,
1712            "Focus lock should be reset when focus is gained"
1713        );
1714    }
1715
1716    #[test]
1717    fn test_focus_lock_state() {
1718        let mut editor = CodeEditor::new("test", "rs");
1719
1720        // Initially, focus should not be locked
1721        assert!(!editor.focus_locked);
1722
1723        // When focus is lost, it should be locked
1724        let _ = editor.update(&Message::CanvasFocusLost);
1725        assert!(editor.focus_locked, "Focus should be locked when lost");
1726
1727        // When focus is regained, it should be unlocked
1728        editor.request_focus();
1729        let _ = editor.update(&Message::CanvasFocusGained);
1730        assert!(!editor.focus_locked, "Focus should be unlocked when regained");
1731
1732        // Can manually reset focus lock
1733        editor.focus_locked = true;
1734        editor.reset_focus_lock();
1735        assert!(!editor.focus_locked, "Focus lock should be resetable");
1736    }
1737
1738    #[test]
1739    fn test_reset_focus_lock() {
1740        let mut editor = CodeEditor::new("test", "rs");
1741        editor.focus_locked = true;
1742
1743        editor.reset_focus_lock();
1744
1745        assert!(!editor.focus_locked);
1746    }
1747
1748    #[test]
1749    fn test_home_key() {
1750        let mut editor = CodeEditor::new("hello world", "py");
1751        editor.cursors.primary_mut().position = (0, 5); // Move to middle of line
1752        let _ = editor.update(&Message::Home(false));
1753        assert_eq!(editor.cursors.primary_position(), (0, 0));
1754    }
1755
1756    #[test]
1757    fn test_end_key() {
1758        let mut editor = CodeEditor::new("hello world", "py");
1759        editor.cursors.primary_mut().position = (0, 0);
1760        let _ = editor.update(&Message::End(false));
1761        assert_eq!(editor.cursors.primary_position(), (0, 11)); // Length of "hello world"
1762    }
1763
1764    #[test]
1765    fn test_arrow_key_with_shift_creates_selection() {
1766        let mut editor = CodeEditor::new("hello world", "py");
1767        editor.cursors.primary_mut().position = (0, 0);
1768
1769        // Shift+Right should start selection
1770        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
1771        assert!(editor.cursors.primary().anchor.is_some());
1772        assert!(editor.cursors.primary().has_selection());
1773    }
1774
1775    #[test]
1776    fn test_arrow_key_without_shift_clears_selection() {
1777        let mut editor = CodeEditor::new("hello world", "py");
1778        editor.cursors.primary_mut().anchor = Some((0, 0));
1779        editor.cursors.primary_mut().position = (0, 5);
1780
1781        // Regular arrow key should clear selection
1782        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
1783        assert!(editor.cursors.primary().anchor.is_none());
1784        assert!(!editor.cursors.primary().has_selection());
1785    }
1786
1787    #[test]
1788    fn test_typing_with_selection() {
1789        let mut editor = CodeEditor::new("hello world", "py");
1790        // Ensure editor has focus for character input
1791        editor.request_focus();
1792        editor.has_canvas_focus = true;
1793        editor.focus_locked = false;
1794
1795        editor.cursors.primary_mut().anchor = Some((0, 0));
1796        editor.cursors.primary_mut().position = (0, 5);
1797
1798        let _ = editor.update(&Message::CharacterInput('X'));
1799        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
1800        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
1801        assert_eq!(editor.buffer.line(0), "Xhello world");
1802    }
1803
1804    #[test]
1805    fn test_ctrl_home() {
1806        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1807        editor.cursors.primary_mut().position = (2, 5); // Start at line 3, column 5
1808        let _ = editor.update(&Message::CtrlHome);
1809        assert_eq!(editor.cursors.primary_position(), (0, 0)); // Should move to beginning of document
1810    }
1811
1812    #[test]
1813    fn test_ctrl_end() {
1814        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1815        editor.cursors.primary_mut().position = (0, 0); // Start at beginning
1816        let _ = editor.update(&Message::CtrlEnd);
1817        assert_eq!(editor.cursors.primary_position(), (2, 5)); // Should move to end of last line (line3 has 5 chars)
1818    }
1819
1820    #[test]
1821    fn test_ctrl_home_clears_selection() {
1822        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1823        editor.cursors.primary_mut().position = (2, 5);
1824        editor.cursors.primary_mut().anchor = Some((0, 0));
1825        editor.cursors.primary_mut().position = (2, 5);
1826
1827        let _ = editor.update(&Message::CtrlHome);
1828        assert_eq!(editor.cursors.primary_position(), (0, 0));
1829        assert!(editor.cursors.primary().anchor.is_none());
1830        assert!(!editor.cursors.primary().has_selection());
1831    }
1832
1833    #[test]
1834    fn test_ctrl_end_clears_selection() {
1835        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1836        editor.cursors.primary_mut().position = (0, 0);
1837        editor.cursors.primary_mut().anchor = Some((0, 0));
1838        editor.cursors.primary_mut().position = (1, 3);
1839
1840        let _ = editor.update(&Message::CtrlEnd);
1841        assert_eq!(editor.cursors.primary_position(), (2, 5));
1842        assert!(editor.cursors.primary().anchor.is_none());
1843        assert!(!editor.cursors.primary().has_selection());
1844    }
1845
1846    #[test]
1847    fn test_goto_position_sets_cursor_and_clears_selection() {
1848        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1849        editor.cursors.primary_mut().anchor = Some((0, 0));
1850        editor.cursors.primary_mut().position = (1, 2);
1851
1852        let _ = editor.update(&Message::GotoPosition(1, 3));
1853
1854        assert_eq!(editor.cursors.primary_position(), (1, 3));
1855        assert!(editor.cursors.primary().anchor.is_none());
1856        assert!(!editor.cursors.primary().has_selection());
1857    }
1858
1859    #[test]
1860    fn test_goto_position_clamps_out_of_range() {
1861        let mut editor = CodeEditor::new("a\nbb", "py");
1862
1863        let _ = editor.update(&Message::GotoPosition(99, 99));
1864
1865        // Clamped to last line (index 1) and end of that line (len = 2)
1866        assert_eq!(editor.cursors.primary_position(), (1, 2));
1867    }
1868
1869    #[test]
1870    fn test_scroll_sets_initial_cache_window() {
1871        let content =
1872            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1873        let mut editor = CodeEditor::new(&content, "py");
1874
1875        // Simulate initial viewport
1876        let height = 400.0;
1877        let width = 800.0;
1878        let scroll = 0.0;
1879
1880        // Expected derived ranges
1881        let visible_lines_count =
1882            (height / editor.line_height).ceil() as usize + 2;
1883        let first_visible_line = (scroll / editor.line_height).floor() as usize;
1884        let last_visible_line = first_visible_line + visible_lines_count;
1885        let margin = visible_lines_count * 2;
1886        let window_start = first_visible_line.saturating_sub(margin);
1887        let window_end = last_visible_line + margin;
1888
1889        // Apply logic similar to Message::Scrolled branch
1890        editor.viewport_height = height;
1891        editor.viewport_width = width;
1892        editor.viewport_scroll = -1.0;
1893        let scroll_changed = (editor.viewport_scroll - scroll).abs() > 0.1;
1894        let need_rewindow = true;
1895        if (editor.viewport_height - height).abs() > 1.0
1896            || (editor.viewport_width - width).abs() > 1.0
1897            || (scroll_changed && need_rewindow)
1898        {
1899            editor.cache_window_start_line = window_start;
1900            editor.cache_window_end_line = window_end;
1901            editor.last_first_visible_line = first_visible_line;
1902        }
1903        editor.viewport_scroll = scroll;
1904
1905        assert_eq!(editor.last_first_visible_line, first_visible_line);
1906        assert!(editor.cache_window_end_line > editor.cache_window_start_line);
1907        assert_eq!(editor.cache_window_start_line, window_start);
1908        assert_eq!(editor.cache_window_end_line, window_end);
1909    }
1910
1911    #[test]
1912    fn test_small_scroll_keeps_window() {
1913        let content =
1914            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1915        let mut editor = CodeEditor::new(&content, "py");
1916        let height = 400.0;
1917        let width = 800.0;
1918        let initial_scroll = 0.0;
1919        let visible_lines_count =
1920            (height / editor.line_height).ceil() as usize + 2;
1921        let first_visible_line =
1922            (initial_scroll / editor.line_height).floor() as usize;
1923        let last_visible_line = first_visible_line + visible_lines_count;
1924        let margin = visible_lines_count * 2;
1925        let window_start = first_visible_line.saturating_sub(margin);
1926        let window_end = last_visible_line + margin;
1927        editor.cache_window_start_line = window_start;
1928        editor.cache_window_end_line = window_end;
1929        editor.viewport_height = height;
1930        editor.viewport_width = width;
1931        editor.viewport_scroll = initial_scroll;
1932
1933        // Small scroll inside window
1934        let small_scroll =
1935            editor.line_height * (visible_lines_count as f32 / 4.0);
1936        let first_visible_line2 =
1937            (small_scroll / editor.line_height).floor() as usize;
1938        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1939        let lower_boundary_trigger = editor.cache_window_start_line > 0
1940            && first_visible_line2
1941                < editor
1942                    .cache_window_start_line
1943                    .saturating_add(visible_lines_count / 2);
1944        let upper_boundary_trigger = last_visible_line2
1945            > editor
1946                .cache_window_end_line
1947                .saturating_sub(visible_lines_count / 2);
1948        let need_rewindow = lower_boundary_trigger || upper_boundary_trigger;
1949
1950        assert!(!need_rewindow, "Small scroll should be inside the window");
1951        // Window remains unchanged
1952        assert_eq!(editor.cache_window_start_line, window_start);
1953        assert_eq!(editor.cache_window_end_line, window_end);
1954    }
1955
1956    #[test]
1957    fn test_large_scroll_rewindows() {
1958        let content =
1959            (0..1000).map(|i| format!("line{}\n", i)).collect::<String>();
1960        let mut editor = CodeEditor::new(&content, "py");
1961        let height = 400.0;
1962        let width = 800.0;
1963        let initial_scroll = 0.0;
1964        let visible_lines_count =
1965            (height / editor.line_height).ceil() as usize + 2;
1966        let first_visible_line =
1967            (initial_scroll / editor.line_height).floor() as usize;
1968        let last_visible_line = first_visible_line + visible_lines_count;
1969        let margin = visible_lines_count * 2;
1970        editor.cache_window_start_line =
1971            first_visible_line.saturating_sub(margin);
1972        editor.cache_window_end_line = last_visible_line + margin;
1973        editor.viewport_height = height;
1974        editor.viewport_width = width;
1975        editor.viewport_scroll = initial_scroll;
1976
1977        // Large scroll beyond window boundary
1978        let large_scroll =
1979            editor.line_height * ((visible_lines_count * 4) as f32);
1980        let first_visible_line2 =
1981            (large_scroll / editor.line_height).floor() as usize;
1982        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1983        let window_start2 = first_visible_line2.saturating_sub(margin);
1984        let window_end2 = last_visible_line2 + margin;
1985        let need_rewindow = first_visible_line2
1986            < editor
1987                .cache_window_start_line
1988                .saturating_add(visible_lines_count / 2)
1989            || last_visible_line2
1990                > editor
1991                    .cache_window_end_line
1992                    .saturating_sub(visible_lines_count / 2);
1993        assert!(need_rewindow, "Large scroll should trigger window update");
1994
1995        // Apply rewindow
1996        editor.cache_window_start_line = window_start2;
1997        editor.cache_window_end_line = window_end2;
1998        editor.last_first_visible_line = first_visible_line2;
1999
2000        assert_eq!(editor.cache_window_start_line, window_start2);
2001        assert_eq!(editor.cache_window_end_line, window_end2);
2002        assert_eq!(editor.last_first_visible_line, first_visible_line2);
2003    }
2004
2005    #[test]
2006    fn test_delete_selection_message() {
2007        let mut editor = CodeEditor::new("hello world", "py");
2008        editor.cursors.primary_mut().position = (0, 0);
2009        editor.cursors.primary_mut().anchor = Some((0, 0));
2010        editor.cursors.primary_mut().position = (0, 5);
2011
2012        let _ = editor.update(&Message::DeleteSelection);
2013        assert_eq!(editor.buffer.line(0), " world");
2014        assert_eq!(editor.cursors.primary_position(), (0, 0));
2015        assert!(editor.cursors.primary().anchor.is_none());
2016        assert!(!editor.cursors.primary().has_selection());
2017    }
2018
2019    #[test]
2020    fn test_delete_selection_multiline() {
2021        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
2022        editor.cursors.primary_mut().position = (0, 2);
2023        editor.cursors.primary_mut().anchor = Some((0, 2));
2024        editor.cursors.primary_mut().position = (2, 2);
2025
2026        let _ = editor.update(&Message::DeleteSelection);
2027        assert_eq!(editor.buffer.line(0), "line3");
2028        assert_eq!(editor.cursors.primary_position(), (0, 2));
2029        assert!(editor.cursors.primary().anchor.is_none());
2030    }
2031
2032    #[test]
2033    fn test_delete_selection_no_selection() {
2034        let mut editor = CodeEditor::new("hello world", "py");
2035        editor.cursors.primary_mut().position = (0, 5);
2036
2037        let _ = editor.update(&Message::DeleteSelection);
2038        // Should do nothing if there's no selection
2039        assert_eq!(editor.buffer.line(0), "hello world");
2040        assert_eq!(editor.cursors.primary_position(), (0, 5));
2041    }
2042
2043    #[test]
2044    #[allow(clippy::unwrap_used)]
2045    fn test_ime_preedit_and_commit_chinese() {
2046        let mut editor = CodeEditor::new("", "py");
2047        // Simulate IME opened
2048        let _ = editor.update(&Message::ImeOpened);
2049        assert!(editor.ime_preedit.is_none());
2050
2051        // Preedit with Chinese content and a selection range
2052        let content = "安全与合规".to_string();
2053        let selection = Some(0..3); // range aligned to UTF-8 character boundary
2054        let _ = editor
2055            .update(&Message::ImePreedit(content.clone(), selection.clone()));
2056
2057        assert!(editor.ime_preedit.is_some());
2058        assert_eq!(
2059            editor.ime_preedit.as_ref().unwrap().content.clone(),
2060            content
2061        );
2062        assert_eq!(
2063            editor.ime_preedit.as_ref().unwrap().selection.clone(),
2064            selection
2065        );
2066
2067        // Commit should insert the text and clear preedit
2068        let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
2069        assert!(editor.ime_preedit.is_none());
2070        assert_eq!(editor.buffer.line(0), "安全与合规");
2071        assert_eq!(
2072            editor.cursors.primary_position(),
2073            (0, "安全与合规".chars().count())
2074        );
2075    }
2076
2077    #[test]
2078    fn test_undo_char_insert() {
2079        let mut editor = CodeEditor::new("hello", "py");
2080        // Ensure editor has focus for character input
2081        editor.request_focus();
2082        editor.has_canvas_focus = true;
2083        editor.focus_locked = false;
2084
2085        editor.cursors.primary_mut().position = (0, 5);
2086
2087        // Type a character
2088        let _ = editor.update(&Message::CharacterInput('!'));
2089        assert_eq!(editor.buffer.line(0), "hello!");
2090        assert_eq!(editor.cursors.primary_position(), (0, 6));
2091
2092        // Undo should remove it (but first end the grouping)
2093        editor.history.end_group();
2094        let _ = editor.update(&Message::Undo);
2095        assert_eq!(editor.buffer.line(0), "hello");
2096        assert_eq!(editor.cursors.primary_position(), (0, 5));
2097    }
2098
2099    #[test]
2100    fn test_undo_redo_char_insert() {
2101        let mut editor = CodeEditor::new("hello", "py");
2102        // Ensure editor has focus for character input
2103        editor.request_focus();
2104        editor.has_canvas_focus = true;
2105        editor.focus_locked = false;
2106
2107        editor.cursors.primary_mut().position = (0, 5);
2108
2109        // Type a character
2110        let _ = editor.update(&Message::CharacterInput('!'));
2111        editor.history.end_group();
2112
2113        // Undo
2114        let _ = editor.update(&Message::Undo);
2115        assert_eq!(editor.buffer.line(0), "hello");
2116
2117        // Redo
2118        let _ = editor.update(&Message::Redo);
2119        assert_eq!(editor.buffer.line(0), "hello!");
2120        assert_eq!(editor.cursors.primary_position(), (0, 6));
2121    }
2122
2123    #[test]
2124    fn test_undo_backspace() {
2125        let mut editor = CodeEditor::new("hello", "py");
2126        editor.cursors.primary_mut().position = (0, 5);
2127
2128        // Backspace
2129        let _ = editor.update(&Message::Backspace);
2130        assert_eq!(editor.buffer.line(0), "hell");
2131        assert_eq!(editor.cursors.primary_position(), (0, 4));
2132
2133        // Undo
2134        let _ = editor.update(&Message::Undo);
2135        assert_eq!(editor.buffer.line(0), "hello");
2136        assert_eq!(editor.cursors.primary_position(), (0, 5));
2137    }
2138
2139    #[test]
2140    fn test_undo_newline() {
2141        let mut editor = CodeEditor::new("hello world", "py");
2142        editor.cursors.primary_mut().position = (0, 5);
2143
2144        // Insert newline
2145        let _ = editor.update(&Message::Enter);
2146        assert_eq!(editor.buffer.line(0), "hello");
2147        assert_eq!(editor.buffer.line(1), " world");
2148        assert_eq!(editor.cursors.primary_position(), (1, 0));
2149
2150        // Undo
2151        let _ = editor.update(&Message::Undo);
2152        assert_eq!(editor.buffer.line(0), "hello world");
2153        assert_eq!(editor.cursors.primary_position(), (0, 5));
2154    }
2155
2156    #[test]
2157    fn test_undo_grouped_typing() {
2158        let mut editor = CodeEditor::new("hello", "py");
2159        // Ensure editor has focus for character input
2160        editor.request_focus();
2161        editor.has_canvas_focus = true;
2162        editor.focus_locked = false;
2163
2164        editor.cursors.primary_mut().position = (0, 5);
2165
2166        // Type multiple characters (they should be grouped)
2167        let _ = editor.update(&Message::CharacterInput(' '));
2168        let _ = editor.update(&Message::CharacterInput('w'));
2169        let _ = editor.update(&Message::CharacterInput('o'));
2170        let _ = editor.update(&Message::CharacterInput('r'));
2171        let _ = editor.update(&Message::CharacterInput('l'));
2172        let _ = editor.update(&Message::CharacterInput('d'));
2173
2174        assert_eq!(editor.buffer.line(0), "hello world");
2175
2176        // End the group
2177        editor.history.end_group();
2178
2179        // Single undo should remove all grouped characters
2180        let _ = editor.update(&Message::Undo);
2181        assert_eq!(editor.buffer.line(0), "hello");
2182        assert_eq!(editor.cursors.primary_position(), (0, 5));
2183    }
2184
2185    #[test]
2186    fn test_navigation_ends_grouping() {
2187        let mut editor = CodeEditor::new("hello", "py");
2188        // Ensure editor has focus for character input
2189        editor.request_focus();
2190        editor.has_canvas_focus = true;
2191        editor.focus_locked = false;
2192
2193        editor.cursors.primary_mut().position = (0, 5);
2194
2195        // Type a character (starts grouping)
2196        let _ = editor.update(&Message::CharacterInput('!'));
2197        assert!(editor.is_grouping);
2198
2199        // Move cursor (ends grouping)
2200        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
2201        assert!(!editor.is_grouping);
2202
2203        // Type another character (starts new group)
2204        let _ = editor.update(&Message::CharacterInput('?'));
2205        assert!(editor.is_grouping);
2206
2207        editor.history.end_group();
2208
2209        // Two separate undo operations
2210        let _ = editor.update(&Message::Undo);
2211        assert_eq!(editor.buffer.line(0), "hello!");
2212
2213        let _ = editor.update(&Message::Undo);
2214        assert_eq!(editor.buffer.line(0), "hello");
2215    }
2216
2217    #[test]
2218    fn test_edit_increments_revision_and_clears_visual_lines_cache() {
2219        let mut editor = CodeEditor::new("hello", "rs");
2220        editor.request_focus();
2221        editor.has_canvas_focus = true;
2222        editor.focus_locked = false;
2223        editor.cursors.primary_mut().position = (0, 5);
2224
2225        let _ = editor.visual_lines_cached(800.0);
2226        assert!(
2227            editor.visual_lines_cache.borrow().is_some(),
2228            "visual_lines_cached should populate the cache"
2229        );
2230
2231        let previous_revision = editor.buffer_revision;
2232
2233        let _ = editor.update(&Message::CharacterInput('!'));
2234        assert_eq!(
2235            editor.buffer_revision,
2236            previous_revision.wrapping_add(1),
2237            "buffer_revision should change on buffer edits"
2238        );
2239        // `scroll_to_cursor` repopulates the cache after the edit with the new
2240        // revision, so the cache may be `Some`.  What must never happen is that
2241        // stale data (an old revision) survives an edit.
2242        assert!(
2243            editor
2244                .visual_lines_cache
2245                .borrow()
2246                .as_ref()
2247                .is_none_or(|c| c.key.buffer_revision == editor.buffer_revision),
2248            "buffer edits should not leave stale data in the visual lines cache"
2249        );
2250    }
2251
2252    #[test]
2253    fn test_multiple_undo_redo() {
2254        let mut editor = CodeEditor::new("a", "py");
2255        // Ensure editor has focus for character input
2256        editor.request_focus();
2257        editor.has_canvas_focus = true;
2258        editor.focus_locked = false;
2259
2260        editor.cursors.primary_mut().position = (0, 1);
2261
2262        // Make several changes
2263        let _ = editor.update(&Message::CharacterInput('b'));
2264        editor.history.end_group();
2265
2266        let _ = editor.update(&Message::CharacterInput('c'));
2267        editor.history.end_group();
2268
2269        let _ = editor.update(&Message::CharacterInput('d'));
2270        editor.history.end_group();
2271
2272        assert_eq!(editor.buffer.line(0), "abcd");
2273
2274        // Undo all
2275        let _ = editor.update(&Message::Undo);
2276        assert_eq!(editor.buffer.line(0), "abc");
2277
2278        let _ = editor.update(&Message::Undo);
2279        assert_eq!(editor.buffer.line(0), "ab");
2280
2281        let _ = editor.update(&Message::Undo);
2282        assert_eq!(editor.buffer.line(0), "a");
2283
2284        // Redo all
2285        let _ = editor.update(&Message::Redo);
2286        assert_eq!(editor.buffer.line(0), "ab");
2287
2288        let _ = editor.update(&Message::Redo);
2289        assert_eq!(editor.buffer.line(0), "abc");
2290
2291        let _ = editor.update(&Message::Redo);
2292        assert_eq!(editor.buffer.line(0), "abcd");
2293    }
2294
2295    #[test]
2296    fn test_delete_key_with_selection() {
2297        let mut editor = CodeEditor::new("hello world", "py");
2298        editor.cursors.primary_mut().anchor = Some((0, 0));
2299        editor.cursors.primary_mut().position = (0, 5);
2300        editor.cursors.primary_mut().position = (0, 5);
2301
2302        let _ = editor.update(&Message::Delete);
2303
2304        assert_eq!(editor.buffer.line(0), " world");
2305        assert_eq!(editor.cursors.primary_position(), (0, 0));
2306        assert!(editor.cursors.primary().anchor.is_none());
2307        assert!(!editor.cursors.primary().has_selection());
2308    }
2309
2310    #[test]
2311    fn test_delete_key_without_selection() {
2312        let mut editor = CodeEditor::new("hello", "py");
2313        editor.cursors.primary_mut().position = (0, 0);
2314
2315        let _ = editor.update(&Message::Delete);
2316
2317        // Should delete the 'h'
2318        assert_eq!(editor.buffer.line(0), "ello");
2319        assert_eq!(editor.cursors.primary_position(), (0, 0));
2320    }
2321
2322    #[test]
2323    fn test_backspace_with_selection() {
2324        let mut editor = CodeEditor::new("hello world", "py");
2325        editor.cursors.primary_mut().anchor = Some((0, 6));
2326        editor.cursors.primary_mut().position = (0, 11);
2327        editor.cursors.primary_mut().position = (0, 11);
2328
2329        let _ = editor.update(&Message::Backspace);
2330
2331        assert_eq!(editor.buffer.line(0), "hello ");
2332        assert_eq!(editor.cursors.primary_position(), (0, 6));
2333        assert!(editor.cursors.primary().anchor.is_none());
2334        assert!(!editor.cursors.primary().has_selection());
2335    }
2336
2337    #[test]
2338    fn test_backspace_without_selection() {
2339        let mut editor = CodeEditor::new("hello", "py");
2340        editor.cursors.primary_mut().position = (0, 5);
2341
2342        let _ = editor.update(&Message::Backspace);
2343
2344        // Should delete the 'o'
2345        assert_eq!(editor.buffer.line(0), "hell");
2346        assert_eq!(editor.cursors.primary_position(), (0, 4));
2347    }
2348
2349    #[test]
2350    fn test_delete_multiline_selection() {
2351        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
2352        editor.cursors.primary_mut().anchor = Some((0, 2));
2353        editor.cursors.primary_mut().position = (2, 2);
2354        editor.cursors.primary_mut().position = (2, 2);
2355
2356        let _ = editor.update(&Message::Delete);
2357
2358        assert_eq!(editor.buffer.line(0), "line3");
2359        assert_eq!(editor.cursors.primary_position(), (0, 2));
2360        assert!(editor.cursors.primary().anchor.is_none());
2361    }
2362
2363    #[test]
2364    fn test_canvas_focus_gained() {
2365        let mut editor = CodeEditor::new("hello world", "py");
2366        assert!(!editor.has_canvas_focus);
2367        assert!(!editor.show_cursor);
2368
2369        let _ = editor.update(&Message::CanvasFocusGained);
2370
2371        assert!(editor.has_canvas_focus);
2372        assert!(editor.show_cursor);
2373    }
2374
2375    #[test]
2376    fn test_mouse_click_gains_focus() {
2377        let mut editor = CodeEditor::new("hello world", "py");
2378        editor.has_canvas_focus = false;
2379        editor.show_cursor = false;
2380
2381        let _ =
2382            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
2383
2384        assert!(editor.has_canvas_focus);
2385        assert!(editor.show_cursor);
2386    }
2387
2388    #[test]
2389    fn test_enter_no_indent() {
2390        let mut editor = CodeEditor::new("hello", "rs");
2391        editor.cursors.primary_mut().position = (0, 5);
2392        let _ = editor.update(&Message::Enter);
2393        assert_eq!(editor.buffer.line(0), "hello");
2394        assert_eq!(editor.buffer.line(1), "");
2395        assert_eq!(editor.cursors.primary_position(), (1, 0));
2396    }
2397
2398    #[test]
2399    fn test_enter_auto_indent_spaces() {
2400        let mut editor = CodeEditor::new("    hello", "rs");
2401        editor.cursors.primary_mut().position = (0, 9);
2402        let _ = editor.update(&Message::Enter);
2403        assert_eq!(editor.buffer.line(0), "    hello");
2404        assert_eq!(editor.buffer.line(1), "    ");
2405        assert_eq!(editor.cursors.primary_position(), (1, 4));
2406    }
2407
2408    #[test]
2409    fn test_enter_auto_indent_tab() {
2410        let mut editor = CodeEditor::new("\thello", "rs");
2411        editor.cursors.primary_mut().position = (0, 6);
2412        let _ = editor.update(&Message::Enter);
2413        assert_eq!(editor.buffer.line(0), "\thello");
2414        assert_eq!(editor.buffer.line(1), "\t");
2415        assert_eq!(editor.cursors.primary_position(), (1, 1));
2416    }
2417
2418    #[test]
2419    fn test_enter_auto_indent_undo() {
2420        let mut editor = CodeEditor::new("    hello", "rs");
2421        editor.cursors.primary_mut().position = (0, 9);
2422        let _ = editor.update(&Message::Enter);
2423        assert_eq!(editor.buffer.line_count(), 2);
2424
2425        let _ = editor.update(&Message::Undo);
2426        assert_eq!(editor.buffer.line_count(), 1);
2427        assert_eq!(editor.buffer.line(0), "    hello");
2428        assert_eq!(editor.cursors.primary_position(), (0, 9));
2429    }
2430
2431    // =========================================================================
2432    // Multi-cursor tests
2433    // =========================================================================
2434
2435    #[test]
2436    fn test_multi_cursor_char_input_different_lines() {
2437        let mut editor = CodeEditor::new("aaa\nbbb", "rs");
2438        editor.request_focus();
2439        editor.has_canvas_focus = true;
2440        editor.focus_locked = false;
2441        // Place cursors at (0, 1) and (1, 1)
2442        editor.cursors.primary_mut().position = (0, 1);
2443        editor.cursors.add_cursor((1, 1));
2444
2445        let _ = editor.update(&Message::CharacterInput('X'));
2446
2447        // Both lines should have 'X' inserted at col 1
2448        assert_eq!(editor.buffer.line(0), "aXaa");
2449        assert_eq!(editor.buffer.line(1), "bXbb");
2450    }
2451
2452    #[test]
2453    fn test_multi_cursor_char_input_same_line() {
2454        let mut editor = CodeEditor::new("abcd", "rs");
2455        editor.request_focus();
2456        editor.has_canvas_focus = true;
2457        editor.focus_locked = false;
2458        // Place cursors at col 1 and col 3 (same line)
2459        editor.cursors.primary_mut().position = (0, 1);
2460        editor.cursors.add_cursor((0, 3));
2461
2462        let _ = editor.update(&Message::CharacterInput('X'));
2463
2464        // Process descending: col 3 first → "abcXd"; then col 1 → "aXbcXd"
2465        // Col 1 cursor adjustment: insert at col 3 does not affect col 1 (col 1 < 3)
2466        assert_eq!(editor.buffer.line(0), "aXbcXd");
2467    }
2468
2469    #[test]
2470    fn test_add_cursor_above() {
2471        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
2472        editor.cursors.primary_mut().position = (1, 3);
2473
2474        let _ = editor.update(&Message::AddCursorAbove);
2475
2476        assert!(editor.cursors.is_multi());
2477        // New cursor should be at line 0, col 3
2478        assert_eq!(editor.cursors.as_slice()[0].position, (0, 3));
2479    }
2480
2481    #[test]
2482    fn test_add_cursor_below() {
2483        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
2484        editor.cursors.primary_mut().position = (1, 3);
2485
2486        let _ = editor.update(&Message::AddCursorBelow);
2487
2488        assert!(editor.cursors.is_multi());
2489        // New cursor should be at line 2, col 3
2490        assert_eq!(
2491            editor
2492                .cursors
2493                .as_slice()
2494                .iter()
2495                .find(|c| c.position.0 == 2)
2496                .map(|c| c.position),
2497            Some((2, 3))
2498        );
2499    }
2500
2501    #[test]
2502    fn test_escape_collapses_multi_cursor() {
2503        let mut editor = CodeEditor::new("line0\nline1", "rs");
2504        editor.cursors.primary_mut().position = (0, 0);
2505        editor.cursors.add_cursor((1, 0));
2506        assert!(editor.cursors.is_multi());
2507
2508        let _ = editor.update(&Message::CloseSearch);
2509
2510        assert!(!editor.cursors.is_multi());
2511    }
2512
2513    #[test]
2514    fn test_select_next_occurrence_selects_word() {
2515        let mut editor = CodeEditor::new("foo bar foo", "rs");
2516        editor.cursors.primary_mut().position = (0, 1); // inside "foo"
2517
2518        let _ = editor.update(&Message::SelectNextOccurrence);
2519
2520        // Primary cursor should now have "foo" selected
2521        let range = editor.cursors.primary().selection_range();
2522        assert_eq!(range, Some(((0, 0), (0, 3))));
2523    }
2524
2525    #[test]
2526    fn test_select_next_occurrence_adds_cursor_for_second_occurrence() {
2527        let mut editor = CodeEditor::new("foo bar foo", "rs");
2528        // Set up primary cursor with "foo" selected
2529        editor.cursors.primary_mut().anchor = Some((0, 0));
2530        editor.cursors.primary_mut().position = (0, 3);
2531
2532        let _ = editor.update(&Message::SelectNextOccurrence);
2533
2534        // Should now have 2 cursors: primary at "foo" (0..3) and new at "foo" (8..11)
2535        assert_eq!(editor.cursors.len(), 2);
2536    }
2537
2538    #[test]
2539    fn test_multi_cursor_backspace() {
2540        let mut editor = CodeEditor::new("abc\ndef", "rs");
2541        editor.cursors.primary_mut().position = (0, 2);
2542        editor.cursors.add_cursor((1, 2));
2543
2544        let _ = editor.update(&Message::Backspace);
2545
2546        assert_eq!(editor.buffer.line(0), "ac");
2547        assert_eq!(editor.buffer.line(1), "df");
2548    }
2549}