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            Message::ToggleFold(header_line) => {
1657                self.toggle_fold(*header_line);
1658                Task::none()
1659            }
1660            Message::ToggleFoldAtCursor => {
1661                self.toggle_fold_at(self.cursors.primary_position().0);
1662                Task::none()
1663            }
1664            Message::FoldAll => {
1665                self.fold_all();
1666                Task::none()
1667            }
1668            Message::UnfoldAll => {
1669                self.unfold_all();
1670                Task::none()
1671            }
1672        }
1673    }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678    use super::*;
1679    use crate::canvas_editor::ArrowDirection;
1680
1681    #[test]
1682    fn test_horizontal_scroll_initial_state() {
1683        let editor = CodeEditor::new("short line", "rs");
1684        assert!(
1685            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
1686            "Initial horizontal scroll offset should be 0"
1687        );
1688    }
1689
1690    #[test]
1691    fn test_set_wrap_enabled_resets_horizontal_offset() {
1692        let mut editor = CodeEditor::new("long line", "rs");
1693        editor.wrap_enabled = false;
1694        // Simulate a non-zero horizontal scroll
1695        editor.horizontal_scroll_offset = 100.0;
1696
1697        // Re-enabling wrap should reset horizontal offset
1698        editor.set_wrap_enabled(true);
1699        assert!(
1700            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
1701            "Horizontal scroll offset should be reset when wrap is re-enabled"
1702        );
1703    }
1704
1705    #[test]
1706    fn test_canvas_focus_lost() {
1707        let mut editor = CodeEditor::new("test", "rs");
1708        editor.has_canvas_focus = true;
1709
1710        let _ = editor.update(&Message::CanvasFocusLost);
1711
1712        assert!(!editor.has_canvas_focus);
1713        assert!(!editor.show_cursor);
1714        assert!(editor.focus_locked, "Focus should be locked when lost");
1715    }
1716
1717    #[test]
1718    fn test_canvas_focus_gained_resets_lock() {
1719        let mut editor = CodeEditor::new("test", "rs");
1720        editor.has_canvas_focus = false;
1721        editor.focus_locked = true;
1722
1723        let _ = editor.update(&Message::CanvasFocusGained);
1724
1725        assert!(editor.has_canvas_focus);
1726        assert!(
1727            !editor.focus_locked,
1728            "Focus lock should be reset when focus is gained"
1729        );
1730    }
1731
1732    #[test]
1733    fn test_focus_lock_state() {
1734        let mut editor = CodeEditor::new("test", "rs");
1735
1736        // Initially, focus should not be locked
1737        assert!(!editor.focus_locked);
1738
1739        // When focus is lost, it should be locked
1740        let _ = editor.update(&Message::CanvasFocusLost);
1741        assert!(editor.focus_locked, "Focus should be locked when lost");
1742
1743        // When focus is regained, it should be unlocked
1744        editor.request_focus();
1745        let _ = editor.update(&Message::CanvasFocusGained);
1746        assert!(!editor.focus_locked, "Focus should be unlocked when regained");
1747
1748        // Can manually reset focus lock
1749        editor.focus_locked = true;
1750        editor.reset_focus_lock();
1751        assert!(!editor.focus_locked, "Focus lock should be resetable");
1752    }
1753
1754    #[test]
1755    fn test_reset_focus_lock() {
1756        let mut editor = CodeEditor::new("test", "rs");
1757        editor.focus_locked = true;
1758
1759        editor.reset_focus_lock();
1760
1761        assert!(!editor.focus_locked);
1762    }
1763
1764    #[test]
1765    fn test_home_key() {
1766        let mut editor = CodeEditor::new("hello world", "py");
1767        editor.cursors.primary_mut().position = (0, 5); // Move to middle of line
1768        let _ = editor.update(&Message::Home(false));
1769        assert_eq!(editor.cursors.primary_position(), (0, 0));
1770    }
1771
1772    #[test]
1773    fn test_end_key() {
1774        let mut editor = CodeEditor::new("hello world", "py");
1775        editor.cursors.primary_mut().position = (0, 0);
1776        let _ = editor.update(&Message::End(false));
1777        assert_eq!(editor.cursors.primary_position(), (0, 11)); // Length of "hello world"
1778    }
1779
1780    #[test]
1781    fn test_arrow_key_with_shift_creates_selection() {
1782        let mut editor = CodeEditor::new("hello world", "py");
1783        editor.cursors.primary_mut().position = (0, 0);
1784
1785        // Shift+Right should start selection
1786        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
1787        assert!(editor.cursors.primary().anchor.is_some());
1788        assert!(editor.cursors.primary().has_selection());
1789    }
1790
1791    #[test]
1792    fn test_arrow_key_without_shift_clears_selection() {
1793        let mut editor = CodeEditor::new("hello world", "py");
1794        editor.cursors.primary_mut().anchor = Some((0, 0));
1795        editor.cursors.primary_mut().position = (0, 5);
1796
1797        // Regular arrow key should clear selection
1798        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
1799        assert!(editor.cursors.primary().anchor.is_none());
1800        assert!(!editor.cursors.primary().has_selection());
1801    }
1802
1803    #[test]
1804    fn test_typing_with_selection() {
1805        let mut editor = CodeEditor::new("hello world", "py");
1806        // Ensure editor has focus for character input
1807        editor.request_focus();
1808        editor.has_canvas_focus = true;
1809        editor.focus_locked = false;
1810
1811        editor.cursors.primary_mut().anchor = Some((0, 0));
1812        editor.cursors.primary_mut().position = (0, 5);
1813
1814        let _ = editor.update(&Message::CharacterInput('X'));
1815        // Current behavior: character is inserted at cursor, selection is NOT automatically deleted
1816        // This is expected behavior - user must delete selection first (Backspace/Delete) or use Paste
1817        assert_eq!(editor.buffer.line(0), "Xhello world");
1818    }
1819
1820    #[test]
1821    fn test_ctrl_home() {
1822        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1823        editor.cursors.primary_mut().position = (2, 5); // Start at line 3, column 5
1824        let _ = editor.update(&Message::CtrlHome);
1825        assert_eq!(editor.cursors.primary_position(), (0, 0)); // Should move to beginning of document
1826    }
1827
1828    #[test]
1829    fn test_ctrl_end() {
1830        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1831        editor.cursors.primary_mut().position = (0, 0); // Start at beginning
1832        let _ = editor.update(&Message::CtrlEnd);
1833        assert_eq!(editor.cursors.primary_position(), (2, 5)); // Should move to end of last line (line3 has 5 chars)
1834    }
1835
1836    #[test]
1837    fn test_ctrl_home_clears_selection() {
1838        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1839        editor.cursors.primary_mut().position = (2, 5);
1840        editor.cursors.primary_mut().anchor = Some((0, 0));
1841        editor.cursors.primary_mut().position = (2, 5);
1842
1843        let _ = editor.update(&Message::CtrlHome);
1844        assert_eq!(editor.cursors.primary_position(), (0, 0));
1845        assert!(editor.cursors.primary().anchor.is_none());
1846        assert!(!editor.cursors.primary().has_selection());
1847    }
1848
1849    #[test]
1850    fn test_ctrl_end_clears_selection() {
1851        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1852        editor.cursors.primary_mut().position = (0, 0);
1853        editor.cursors.primary_mut().anchor = Some((0, 0));
1854        editor.cursors.primary_mut().position = (1, 3);
1855
1856        let _ = editor.update(&Message::CtrlEnd);
1857        assert_eq!(editor.cursors.primary_position(), (2, 5));
1858        assert!(editor.cursors.primary().anchor.is_none());
1859        assert!(!editor.cursors.primary().has_selection());
1860    }
1861
1862    #[test]
1863    fn test_goto_position_sets_cursor_and_clears_selection() {
1864        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1865        editor.cursors.primary_mut().anchor = Some((0, 0));
1866        editor.cursors.primary_mut().position = (1, 2);
1867
1868        let _ = editor.update(&Message::GotoPosition(1, 3));
1869
1870        assert_eq!(editor.cursors.primary_position(), (1, 3));
1871        assert!(editor.cursors.primary().anchor.is_none());
1872        assert!(!editor.cursors.primary().has_selection());
1873    }
1874
1875    #[test]
1876    fn test_goto_position_clamps_out_of_range() {
1877        let mut editor = CodeEditor::new("a\nbb", "py");
1878
1879        let _ = editor.update(&Message::GotoPosition(99, 99));
1880
1881        // Clamped to last line (index 1) and end of that line (len = 2)
1882        assert_eq!(editor.cursors.primary_position(), (1, 2));
1883    }
1884
1885    #[test]
1886    fn test_scroll_sets_initial_cache_window() {
1887        let content =
1888            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1889        let mut editor = CodeEditor::new(&content, "py");
1890
1891        // Simulate initial viewport
1892        let height = 400.0;
1893        let width = 800.0;
1894        let scroll = 0.0;
1895
1896        // Expected derived ranges
1897        let visible_lines_count =
1898            (height / editor.line_height).ceil() as usize + 2;
1899        let first_visible_line = (scroll / editor.line_height).floor() as usize;
1900        let last_visible_line = first_visible_line + visible_lines_count;
1901        let margin = visible_lines_count * 2;
1902        let window_start = first_visible_line.saturating_sub(margin);
1903        let window_end = last_visible_line + margin;
1904
1905        // Apply logic similar to Message::Scrolled branch
1906        editor.viewport_height = height;
1907        editor.viewport_width = width;
1908        editor.viewport_scroll = -1.0;
1909        let scroll_changed = (editor.viewport_scroll - scroll).abs() > 0.1;
1910        let need_rewindow = true;
1911        if (editor.viewport_height - height).abs() > 1.0
1912            || (editor.viewport_width - width).abs() > 1.0
1913            || (scroll_changed && need_rewindow)
1914        {
1915            editor.cache_window_start_line = window_start;
1916            editor.cache_window_end_line = window_end;
1917            editor.last_first_visible_line = first_visible_line;
1918        }
1919        editor.viewport_scroll = scroll;
1920
1921        assert_eq!(editor.last_first_visible_line, first_visible_line);
1922        assert!(editor.cache_window_end_line > editor.cache_window_start_line);
1923        assert_eq!(editor.cache_window_start_line, window_start);
1924        assert_eq!(editor.cache_window_end_line, window_end);
1925    }
1926
1927    #[test]
1928    fn test_small_scroll_keeps_window() {
1929        let content =
1930            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1931        let mut editor = CodeEditor::new(&content, "py");
1932        let height = 400.0;
1933        let width = 800.0;
1934        let initial_scroll = 0.0;
1935        let visible_lines_count =
1936            (height / editor.line_height).ceil() as usize + 2;
1937        let first_visible_line =
1938            (initial_scroll / editor.line_height).floor() as usize;
1939        let last_visible_line = first_visible_line + visible_lines_count;
1940        let margin = visible_lines_count * 2;
1941        let window_start = first_visible_line.saturating_sub(margin);
1942        let window_end = last_visible_line + margin;
1943        editor.cache_window_start_line = window_start;
1944        editor.cache_window_end_line = window_end;
1945        editor.viewport_height = height;
1946        editor.viewport_width = width;
1947        editor.viewport_scroll = initial_scroll;
1948
1949        // Small scroll inside window
1950        let small_scroll =
1951            editor.line_height * (visible_lines_count as f32 / 4.0);
1952        let first_visible_line2 =
1953            (small_scroll / editor.line_height).floor() as usize;
1954        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1955        let lower_boundary_trigger = editor.cache_window_start_line > 0
1956            && first_visible_line2
1957                < editor
1958                    .cache_window_start_line
1959                    .saturating_add(visible_lines_count / 2);
1960        let upper_boundary_trigger = last_visible_line2
1961            > editor
1962                .cache_window_end_line
1963                .saturating_sub(visible_lines_count / 2);
1964        let need_rewindow = lower_boundary_trigger || upper_boundary_trigger;
1965
1966        assert!(!need_rewindow, "Small scroll should be inside the window");
1967        // Window remains unchanged
1968        assert_eq!(editor.cache_window_start_line, window_start);
1969        assert_eq!(editor.cache_window_end_line, window_end);
1970    }
1971
1972    #[test]
1973    fn test_large_scroll_rewindows() {
1974        let content =
1975            (0..1000).map(|i| format!("line{}\n", i)).collect::<String>();
1976        let mut editor = CodeEditor::new(&content, "py");
1977        let height = 400.0;
1978        let width = 800.0;
1979        let initial_scroll = 0.0;
1980        let visible_lines_count =
1981            (height / editor.line_height).ceil() as usize + 2;
1982        let first_visible_line =
1983            (initial_scroll / editor.line_height).floor() as usize;
1984        let last_visible_line = first_visible_line + visible_lines_count;
1985        let margin = visible_lines_count * 2;
1986        editor.cache_window_start_line =
1987            first_visible_line.saturating_sub(margin);
1988        editor.cache_window_end_line = last_visible_line + margin;
1989        editor.viewport_height = height;
1990        editor.viewport_width = width;
1991        editor.viewport_scroll = initial_scroll;
1992
1993        // Large scroll beyond window boundary
1994        let large_scroll =
1995            editor.line_height * ((visible_lines_count * 4) as f32);
1996        let first_visible_line2 =
1997            (large_scroll / editor.line_height).floor() as usize;
1998        let last_visible_line2 = first_visible_line2 + visible_lines_count;
1999        let window_start2 = first_visible_line2.saturating_sub(margin);
2000        let window_end2 = last_visible_line2 + margin;
2001        let need_rewindow = first_visible_line2
2002            < editor
2003                .cache_window_start_line
2004                .saturating_add(visible_lines_count / 2)
2005            || last_visible_line2
2006                > editor
2007                    .cache_window_end_line
2008                    .saturating_sub(visible_lines_count / 2);
2009        assert!(need_rewindow, "Large scroll should trigger window update");
2010
2011        // Apply rewindow
2012        editor.cache_window_start_line = window_start2;
2013        editor.cache_window_end_line = window_end2;
2014        editor.last_first_visible_line = first_visible_line2;
2015
2016        assert_eq!(editor.cache_window_start_line, window_start2);
2017        assert_eq!(editor.cache_window_end_line, window_end2);
2018        assert_eq!(editor.last_first_visible_line, first_visible_line2);
2019    }
2020
2021    #[test]
2022    fn test_delete_selection_message() {
2023        let mut editor = CodeEditor::new("hello world", "py");
2024        editor.cursors.primary_mut().position = (0, 0);
2025        editor.cursors.primary_mut().anchor = Some((0, 0));
2026        editor.cursors.primary_mut().position = (0, 5);
2027
2028        let _ = editor.update(&Message::DeleteSelection);
2029        assert_eq!(editor.buffer.line(0), " world");
2030        assert_eq!(editor.cursors.primary_position(), (0, 0));
2031        assert!(editor.cursors.primary().anchor.is_none());
2032        assert!(!editor.cursors.primary().has_selection());
2033    }
2034
2035    #[test]
2036    fn test_delete_selection_multiline() {
2037        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
2038        editor.cursors.primary_mut().position = (0, 2);
2039        editor.cursors.primary_mut().anchor = Some((0, 2));
2040        editor.cursors.primary_mut().position = (2, 2);
2041
2042        let _ = editor.update(&Message::DeleteSelection);
2043        assert_eq!(editor.buffer.line(0), "line3");
2044        assert_eq!(editor.cursors.primary_position(), (0, 2));
2045        assert!(editor.cursors.primary().anchor.is_none());
2046    }
2047
2048    #[test]
2049    fn test_delete_selection_no_selection() {
2050        let mut editor = CodeEditor::new("hello world", "py");
2051        editor.cursors.primary_mut().position = (0, 5);
2052
2053        let _ = editor.update(&Message::DeleteSelection);
2054        // Should do nothing if there's no selection
2055        assert_eq!(editor.buffer.line(0), "hello world");
2056        assert_eq!(editor.cursors.primary_position(), (0, 5));
2057    }
2058
2059    #[test]
2060    #[allow(clippy::unwrap_used)]
2061    fn test_ime_preedit_and_commit_chinese() {
2062        let mut editor = CodeEditor::new("", "py");
2063        // Simulate IME opened
2064        let _ = editor.update(&Message::ImeOpened);
2065        assert!(editor.ime_preedit.is_none());
2066
2067        // Preedit with Chinese content and a selection range
2068        let content = "安全与合规".to_string();
2069        let selection = Some(0..3); // range aligned to UTF-8 character boundary
2070        let _ = editor
2071            .update(&Message::ImePreedit(content.clone(), selection.clone()));
2072
2073        assert!(editor.ime_preedit.is_some());
2074        assert_eq!(
2075            editor.ime_preedit.as_ref().unwrap().content.clone(),
2076            content
2077        );
2078        assert_eq!(
2079            editor.ime_preedit.as_ref().unwrap().selection.clone(),
2080            selection
2081        );
2082
2083        // Commit should insert the text and clear preedit
2084        let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
2085        assert!(editor.ime_preedit.is_none());
2086        assert_eq!(editor.buffer.line(0), "安全与合规");
2087        assert_eq!(
2088            editor.cursors.primary_position(),
2089            (0, "安全与合规".chars().count())
2090        );
2091    }
2092
2093    #[test]
2094    fn test_undo_char_insert() {
2095        let mut editor = CodeEditor::new("hello", "py");
2096        // Ensure editor has focus for character input
2097        editor.request_focus();
2098        editor.has_canvas_focus = true;
2099        editor.focus_locked = false;
2100
2101        editor.cursors.primary_mut().position = (0, 5);
2102
2103        // Type a character
2104        let _ = editor.update(&Message::CharacterInput('!'));
2105        assert_eq!(editor.buffer.line(0), "hello!");
2106        assert_eq!(editor.cursors.primary_position(), (0, 6));
2107
2108        // Undo should remove it (but first end the grouping)
2109        editor.history.end_group();
2110        let _ = editor.update(&Message::Undo);
2111        assert_eq!(editor.buffer.line(0), "hello");
2112        assert_eq!(editor.cursors.primary_position(), (0, 5));
2113    }
2114
2115    #[test]
2116    fn test_undo_redo_char_insert() {
2117        let mut editor = CodeEditor::new("hello", "py");
2118        // Ensure editor has focus for character input
2119        editor.request_focus();
2120        editor.has_canvas_focus = true;
2121        editor.focus_locked = false;
2122
2123        editor.cursors.primary_mut().position = (0, 5);
2124
2125        // Type a character
2126        let _ = editor.update(&Message::CharacterInput('!'));
2127        editor.history.end_group();
2128
2129        // Undo
2130        let _ = editor.update(&Message::Undo);
2131        assert_eq!(editor.buffer.line(0), "hello");
2132
2133        // Redo
2134        let _ = editor.update(&Message::Redo);
2135        assert_eq!(editor.buffer.line(0), "hello!");
2136        assert_eq!(editor.cursors.primary_position(), (0, 6));
2137    }
2138
2139    #[test]
2140    fn test_undo_backspace() {
2141        let mut editor = CodeEditor::new("hello", "py");
2142        editor.cursors.primary_mut().position = (0, 5);
2143
2144        // Backspace
2145        let _ = editor.update(&Message::Backspace);
2146        assert_eq!(editor.buffer.line(0), "hell");
2147        assert_eq!(editor.cursors.primary_position(), (0, 4));
2148
2149        // Undo
2150        let _ = editor.update(&Message::Undo);
2151        assert_eq!(editor.buffer.line(0), "hello");
2152        assert_eq!(editor.cursors.primary_position(), (0, 5));
2153    }
2154
2155    #[test]
2156    fn test_undo_newline() {
2157        let mut editor = CodeEditor::new("hello world", "py");
2158        editor.cursors.primary_mut().position = (0, 5);
2159
2160        // Insert newline
2161        let _ = editor.update(&Message::Enter);
2162        assert_eq!(editor.buffer.line(0), "hello");
2163        assert_eq!(editor.buffer.line(1), " world");
2164        assert_eq!(editor.cursors.primary_position(), (1, 0));
2165
2166        // Undo
2167        let _ = editor.update(&Message::Undo);
2168        assert_eq!(editor.buffer.line(0), "hello world");
2169        assert_eq!(editor.cursors.primary_position(), (0, 5));
2170    }
2171
2172    #[test]
2173    fn test_undo_grouped_typing() {
2174        let mut editor = CodeEditor::new("hello", "py");
2175        // Ensure editor has focus for character input
2176        editor.request_focus();
2177        editor.has_canvas_focus = true;
2178        editor.focus_locked = false;
2179
2180        editor.cursors.primary_mut().position = (0, 5);
2181
2182        // Type multiple characters (they should be grouped)
2183        let _ = editor.update(&Message::CharacterInput(' '));
2184        let _ = editor.update(&Message::CharacterInput('w'));
2185        let _ = editor.update(&Message::CharacterInput('o'));
2186        let _ = editor.update(&Message::CharacterInput('r'));
2187        let _ = editor.update(&Message::CharacterInput('l'));
2188        let _ = editor.update(&Message::CharacterInput('d'));
2189
2190        assert_eq!(editor.buffer.line(0), "hello world");
2191
2192        // End the group
2193        editor.history.end_group();
2194
2195        // Single undo should remove all grouped characters
2196        let _ = editor.update(&Message::Undo);
2197        assert_eq!(editor.buffer.line(0), "hello");
2198        assert_eq!(editor.cursors.primary_position(), (0, 5));
2199    }
2200
2201    #[test]
2202    fn test_navigation_ends_grouping() {
2203        let mut editor = CodeEditor::new("hello", "py");
2204        // Ensure editor has focus for character input
2205        editor.request_focus();
2206        editor.has_canvas_focus = true;
2207        editor.focus_locked = false;
2208
2209        editor.cursors.primary_mut().position = (0, 5);
2210
2211        // Type a character (starts grouping)
2212        let _ = editor.update(&Message::CharacterInput('!'));
2213        assert!(editor.is_grouping);
2214
2215        // Move cursor (ends grouping)
2216        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
2217        assert!(!editor.is_grouping);
2218
2219        // Type another character (starts new group)
2220        let _ = editor.update(&Message::CharacterInput('?'));
2221        assert!(editor.is_grouping);
2222
2223        editor.history.end_group();
2224
2225        // Two separate undo operations
2226        let _ = editor.update(&Message::Undo);
2227        assert_eq!(editor.buffer.line(0), "hello!");
2228
2229        let _ = editor.update(&Message::Undo);
2230        assert_eq!(editor.buffer.line(0), "hello");
2231    }
2232
2233    #[test]
2234    fn test_edit_increments_revision_and_clears_visual_lines_cache() {
2235        let mut editor = CodeEditor::new("hello", "rs");
2236        editor.request_focus();
2237        editor.has_canvas_focus = true;
2238        editor.focus_locked = false;
2239        editor.cursors.primary_mut().position = (0, 5);
2240
2241        let _ = editor.visual_lines_cached(800.0);
2242        assert!(
2243            editor.visual_lines_cache.borrow().is_some(),
2244            "visual_lines_cached should populate the cache"
2245        );
2246
2247        let previous_revision = editor.buffer_revision;
2248
2249        let _ = editor.update(&Message::CharacterInput('!'));
2250        assert_eq!(
2251            editor.buffer_revision,
2252            previous_revision.wrapping_add(1),
2253            "buffer_revision should change on buffer edits"
2254        );
2255        // `scroll_to_cursor` repopulates the cache after the edit with the new
2256        // revision, so the cache may be `Some`.  What must never happen is that
2257        // stale data (an old revision) survives an edit.
2258        assert!(
2259            editor
2260                .visual_lines_cache
2261                .borrow()
2262                .as_ref()
2263                .is_none_or(|c| c.key.buffer_revision == editor.buffer_revision),
2264            "buffer edits should not leave stale data in the visual lines cache"
2265        );
2266    }
2267
2268    #[test]
2269    fn test_multiple_undo_redo() {
2270        let mut editor = CodeEditor::new("a", "py");
2271        // Ensure editor has focus for character input
2272        editor.request_focus();
2273        editor.has_canvas_focus = true;
2274        editor.focus_locked = false;
2275
2276        editor.cursors.primary_mut().position = (0, 1);
2277
2278        // Make several changes
2279        let _ = editor.update(&Message::CharacterInput('b'));
2280        editor.history.end_group();
2281
2282        let _ = editor.update(&Message::CharacterInput('c'));
2283        editor.history.end_group();
2284
2285        let _ = editor.update(&Message::CharacterInput('d'));
2286        editor.history.end_group();
2287
2288        assert_eq!(editor.buffer.line(0), "abcd");
2289
2290        // Undo all
2291        let _ = editor.update(&Message::Undo);
2292        assert_eq!(editor.buffer.line(0), "abc");
2293
2294        let _ = editor.update(&Message::Undo);
2295        assert_eq!(editor.buffer.line(0), "ab");
2296
2297        let _ = editor.update(&Message::Undo);
2298        assert_eq!(editor.buffer.line(0), "a");
2299
2300        // Redo all
2301        let _ = editor.update(&Message::Redo);
2302        assert_eq!(editor.buffer.line(0), "ab");
2303
2304        let _ = editor.update(&Message::Redo);
2305        assert_eq!(editor.buffer.line(0), "abc");
2306
2307        let _ = editor.update(&Message::Redo);
2308        assert_eq!(editor.buffer.line(0), "abcd");
2309    }
2310
2311    #[test]
2312    fn test_delete_key_with_selection() {
2313        let mut editor = CodeEditor::new("hello world", "py");
2314        editor.cursors.primary_mut().anchor = Some((0, 0));
2315        editor.cursors.primary_mut().position = (0, 5);
2316        editor.cursors.primary_mut().position = (0, 5);
2317
2318        let _ = editor.update(&Message::Delete);
2319
2320        assert_eq!(editor.buffer.line(0), " world");
2321        assert_eq!(editor.cursors.primary_position(), (0, 0));
2322        assert!(editor.cursors.primary().anchor.is_none());
2323        assert!(!editor.cursors.primary().has_selection());
2324    }
2325
2326    #[test]
2327    fn test_delete_key_without_selection() {
2328        let mut editor = CodeEditor::new("hello", "py");
2329        editor.cursors.primary_mut().position = (0, 0);
2330
2331        let _ = editor.update(&Message::Delete);
2332
2333        // Should delete the 'h'
2334        assert_eq!(editor.buffer.line(0), "ello");
2335        assert_eq!(editor.cursors.primary_position(), (0, 0));
2336    }
2337
2338    #[test]
2339    fn test_backspace_with_selection() {
2340        let mut editor = CodeEditor::new("hello world", "py");
2341        editor.cursors.primary_mut().anchor = Some((0, 6));
2342        editor.cursors.primary_mut().position = (0, 11);
2343        editor.cursors.primary_mut().position = (0, 11);
2344
2345        let _ = editor.update(&Message::Backspace);
2346
2347        assert_eq!(editor.buffer.line(0), "hello ");
2348        assert_eq!(editor.cursors.primary_position(), (0, 6));
2349        assert!(editor.cursors.primary().anchor.is_none());
2350        assert!(!editor.cursors.primary().has_selection());
2351    }
2352
2353    #[test]
2354    fn test_backspace_without_selection() {
2355        let mut editor = CodeEditor::new("hello", "py");
2356        editor.cursors.primary_mut().position = (0, 5);
2357
2358        let _ = editor.update(&Message::Backspace);
2359
2360        // Should delete the 'o'
2361        assert_eq!(editor.buffer.line(0), "hell");
2362        assert_eq!(editor.cursors.primary_position(), (0, 4));
2363    }
2364
2365    #[test]
2366    fn test_delete_multiline_selection() {
2367        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
2368        editor.cursors.primary_mut().anchor = Some((0, 2));
2369        editor.cursors.primary_mut().position = (2, 2);
2370        editor.cursors.primary_mut().position = (2, 2);
2371
2372        let _ = editor.update(&Message::Delete);
2373
2374        assert_eq!(editor.buffer.line(0), "line3");
2375        assert_eq!(editor.cursors.primary_position(), (0, 2));
2376        assert!(editor.cursors.primary().anchor.is_none());
2377    }
2378
2379    #[test]
2380    fn test_canvas_focus_gained() {
2381        let mut editor = CodeEditor::new("hello world", "py");
2382        assert!(!editor.has_canvas_focus);
2383        assert!(!editor.show_cursor);
2384
2385        let _ = editor.update(&Message::CanvasFocusGained);
2386
2387        assert!(editor.has_canvas_focus);
2388        assert!(editor.show_cursor);
2389    }
2390
2391    #[test]
2392    fn test_mouse_click_gains_focus() {
2393        let mut editor = CodeEditor::new("hello world", "py");
2394        editor.has_canvas_focus = false;
2395        editor.show_cursor = false;
2396
2397        let _ =
2398            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
2399
2400        assert!(editor.has_canvas_focus);
2401        assert!(editor.show_cursor);
2402    }
2403
2404    #[test]
2405    fn test_enter_no_indent() {
2406        let mut editor = CodeEditor::new("hello", "rs");
2407        editor.cursors.primary_mut().position = (0, 5);
2408        let _ = editor.update(&Message::Enter);
2409        assert_eq!(editor.buffer.line(0), "hello");
2410        assert_eq!(editor.buffer.line(1), "");
2411        assert_eq!(editor.cursors.primary_position(), (1, 0));
2412    }
2413
2414    #[test]
2415    fn test_enter_auto_indent_spaces() {
2416        let mut editor = CodeEditor::new("    hello", "rs");
2417        editor.cursors.primary_mut().position = (0, 9);
2418        let _ = editor.update(&Message::Enter);
2419        assert_eq!(editor.buffer.line(0), "    hello");
2420        assert_eq!(editor.buffer.line(1), "    ");
2421        assert_eq!(editor.cursors.primary_position(), (1, 4));
2422    }
2423
2424    #[test]
2425    fn test_enter_auto_indent_tab() {
2426        let mut editor = CodeEditor::new("\thello", "rs");
2427        editor.cursors.primary_mut().position = (0, 6);
2428        let _ = editor.update(&Message::Enter);
2429        assert_eq!(editor.buffer.line(0), "\thello");
2430        assert_eq!(editor.buffer.line(1), "\t");
2431        assert_eq!(editor.cursors.primary_position(), (1, 1));
2432    }
2433
2434    #[test]
2435    fn test_enter_auto_indent_undo() {
2436        let mut editor = CodeEditor::new("    hello", "rs");
2437        editor.cursors.primary_mut().position = (0, 9);
2438        let _ = editor.update(&Message::Enter);
2439        assert_eq!(editor.buffer.line_count(), 2);
2440
2441        let _ = editor.update(&Message::Undo);
2442        assert_eq!(editor.buffer.line_count(), 1);
2443        assert_eq!(editor.buffer.line(0), "    hello");
2444        assert_eq!(editor.cursors.primary_position(), (0, 9));
2445    }
2446
2447    // =========================================================================
2448    // Multi-cursor tests
2449    // =========================================================================
2450
2451    #[test]
2452    fn test_multi_cursor_char_input_different_lines() {
2453        let mut editor = CodeEditor::new("aaa\nbbb", "rs");
2454        editor.request_focus();
2455        editor.has_canvas_focus = true;
2456        editor.focus_locked = false;
2457        // Place cursors at (0, 1) and (1, 1)
2458        editor.cursors.primary_mut().position = (0, 1);
2459        editor.cursors.add_cursor((1, 1));
2460
2461        let _ = editor.update(&Message::CharacterInput('X'));
2462
2463        // Both lines should have 'X' inserted at col 1
2464        assert_eq!(editor.buffer.line(0), "aXaa");
2465        assert_eq!(editor.buffer.line(1), "bXbb");
2466    }
2467
2468    #[test]
2469    fn test_multi_cursor_char_input_same_line() {
2470        let mut editor = CodeEditor::new("abcd", "rs");
2471        editor.request_focus();
2472        editor.has_canvas_focus = true;
2473        editor.focus_locked = false;
2474        // Place cursors at col 1 and col 3 (same line)
2475        editor.cursors.primary_mut().position = (0, 1);
2476        editor.cursors.add_cursor((0, 3));
2477
2478        let _ = editor.update(&Message::CharacterInput('X'));
2479
2480        // Process descending: col 3 first → "abcXd"; then col 1 → "aXbcXd"
2481        // Col 1 cursor adjustment: insert at col 3 does not affect col 1 (col 1 < 3)
2482        assert_eq!(editor.buffer.line(0), "aXbcXd");
2483    }
2484
2485    #[test]
2486    fn test_add_cursor_above() {
2487        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
2488        editor.cursors.primary_mut().position = (1, 3);
2489
2490        let _ = editor.update(&Message::AddCursorAbove);
2491
2492        assert!(editor.cursors.is_multi());
2493        // New cursor should be at line 0, col 3
2494        assert_eq!(editor.cursors.as_slice()[0].position, (0, 3));
2495    }
2496
2497    #[test]
2498    fn test_add_cursor_below() {
2499        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
2500        editor.cursors.primary_mut().position = (1, 3);
2501
2502        let _ = editor.update(&Message::AddCursorBelow);
2503
2504        assert!(editor.cursors.is_multi());
2505        // New cursor should be at line 2, col 3
2506        assert_eq!(
2507            editor
2508                .cursors
2509                .as_slice()
2510                .iter()
2511                .find(|c| c.position.0 == 2)
2512                .map(|c| c.position),
2513            Some((2, 3))
2514        );
2515    }
2516
2517    #[test]
2518    fn test_escape_collapses_multi_cursor() {
2519        let mut editor = CodeEditor::new("line0\nline1", "rs");
2520        editor.cursors.primary_mut().position = (0, 0);
2521        editor.cursors.add_cursor((1, 0));
2522        assert!(editor.cursors.is_multi());
2523
2524        let _ = editor.update(&Message::CloseSearch);
2525
2526        assert!(!editor.cursors.is_multi());
2527    }
2528
2529    #[test]
2530    fn test_select_next_occurrence_selects_word() {
2531        let mut editor = CodeEditor::new("foo bar foo", "rs");
2532        editor.cursors.primary_mut().position = (0, 1); // inside "foo"
2533
2534        let _ = editor.update(&Message::SelectNextOccurrence);
2535
2536        // Primary cursor should now have "foo" selected
2537        let range = editor.cursors.primary().selection_range();
2538        assert_eq!(range, Some(((0, 0), (0, 3))));
2539    }
2540
2541    #[test]
2542    fn test_select_next_occurrence_adds_cursor_for_second_occurrence() {
2543        let mut editor = CodeEditor::new("foo bar foo", "rs");
2544        // Set up primary cursor with "foo" selected
2545        editor.cursors.primary_mut().anchor = Some((0, 0));
2546        editor.cursors.primary_mut().position = (0, 3);
2547
2548        let _ = editor.update(&Message::SelectNextOccurrence);
2549
2550        // Should now have 2 cursors: primary at "foo" (0..3) and new at "foo" (8..11)
2551        assert_eq!(editor.cursors.len(), 2);
2552    }
2553
2554    #[test]
2555    fn test_multi_cursor_backspace() {
2556        let mut editor = CodeEditor::new("abc\ndef", "rs");
2557        editor.cursors.primary_mut().position = (0, 2);
2558        editor.cursors.add_cursor((1, 2));
2559
2560        let _ = editor.update(&Message::Backspace);
2561
2562        assert_eq!(editor.buffer.line(0), "ac");
2563        assert_eq!(editor.buffer.line(1), "df");
2564    }
2565}