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