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 crate::text_utils::char_to_byte_index;
7
8use super::command::{
9    Command, CompositeCommand, DeleteCharCommand, DeleteForwardCommand,
10    DeleteRangeCommand, DuplicateLinesCommand, InsertCharCommand,
11    InsertNewlineCommand, InsertTextCommand, MoveLinesCommand,
12    ReplaceTextCommand, ToggleCommentCommand, line_comment_token,
13};
14use super::vim::{
15    VimAction, VimInsertPosition, VimMotion, VimOperator, VimPastePosition,
16    VimRegister, VimRegisterKind,
17};
18use super::{
19    ArrowDirection, CURSOR_BLINK_INTERVAL, CodeEditor, ImePreedit, IndentStyle,
20    LspEditSnapshot, Message, VimMode, cursor_set, lsp,
21};
22
23// =========================================================================
24// Cursor adjustment helpers for multi-cursor editing
25// =========================================================================
26
27/// Describes the kind of edit applied to a single position.
28#[derive(Clone, Copy)]
29enum EditType {
30    /// Insert one char at `(edit_line, edit_col)`.
31    InsertChar,
32    /// Backspace: delete char at `(edit_line, edit_col - 1)`.
33    DeleteCharBack,
34    /// Delete-forward: delete char at `(edit_line, edit_col)`.
35    DeleteCharForward,
36    /// Enter: split `edit_line` at `edit_col`; new line has `extra` indent chars.
37    InsertNewline { indent_len: usize },
38    /// Backspace-at-col-0: merge `edit_line` into `edit_line - 1`.
39    /// `extra` = length of the previous line before merge.
40    MergePrev { prev_line_len: usize },
41    /// Delete-at-end-of-line: merge `edit_line + 1` into `edit_line`.
42    /// `extra` = length of `edit_line` before merge.
43    MergeNext { edit_line_len: usize },
44}
45
46/// Adjusts a single `(line, col)` pair after an edit.
47fn adjust_pos(
48    pos: &mut (usize, usize),
49    edit_line: usize,
50    edit_col: usize,
51    kind: EditType,
52) {
53    match kind {
54        EditType::InsertChar => {
55            if pos.0 == edit_line && pos.1 >= edit_col {
56                pos.1 += 1;
57            }
58        }
59        EditType::DeleteCharBack => {
60            if edit_col > 0 && pos.0 == edit_line && pos.1 > edit_col - 1 {
61                pos.1 -= 1;
62            }
63        }
64        EditType::DeleteCharForward => {
65            if pos.0 == edit_line && pos.1 > edit_col {
66                pos.1 -= 1;
67            }
68        }
69        EditType::InsertNewline { indent_len } => {
70            if pos.0 > edit_line {
71                pos.0 += 1;
72            } else if pos.0 == edit_line && pos.1 >= edit_col {
73                pos.0 += 1;
74                pos.1 = pos.1 - edit_col + indent_len;
75            }
76        }
77        EditType::MergePrev { prev_line_len } => {
78            if pos.0 == edit_line {
79                pos.0 -= 1;
80                pos.1 += prev_line_len;
81            } else if pos.0 > edit_line {
82                pos.0 -= 1;
83            }
84        }
85        EditType::MergeNext { edit_line_len } => {
86            if pos.0 == edit_line + 1 {
87                pos.0 = edit_line;
88                pos.1 += edit_line_len;
89            } else if pos.0 > edit_line + 1 {
90                pos.0 -= 1;
91            }
92        }
93    }
94}
95
96/// Adjusts all cursors except `skip_idx` after an edit at `(edit_line, edit_col)`.
97fn adjust_other_cursors(
98    cursors: &mut [cursor_set::Cursor],
99    skip_idx: usize,
100    edit_line: usize,
101    edit_col: usize,
102    kind: EditType,
103) {
104    for (i, cursor) in cursors.iter_mut().enumerate() {
105        if i == skip_idx {
106            continue;
107        }
108        adjust_pos(&mut cursor.position, edit_line, edit_col, kind);
109        if let Some(ref mut anchor) = cursor.anchor {
110            adjust_pos(anchor, edit_line, edit_col, kind);
111        }
112    }
113}
114
115impl CodeEditor {
116    // =========================================================================
117    // Helper Methods
118    // =========================================================================
119
120    /// Performs common cleanup operations after edit operations.
121    ///
122    /// This method should be called after any operation that modifies the buffer content.
123    /// It resets the cursor blink animation, refreshes search matches if search is active,
124    /// and invalidates all caches that depend on buffer content or layout:
125    /// - `buffer_revision` is bumped to invalidate layout-derived caches
126    /// - `visual_lines_cache` is cleared so wrapping is recalculated on next use
127    /// - `content_cache` and `overlay_cache` are cleared to rebuild canvas geometry
128    fn finish_edit_operation(&mut self) {
129        self.reset_cursor_blink();
130        self.refresh_search_matches_if_needed();
131        // The exact revision value is not semantically meaningful; it only needs
132        // to change on edits, so `wrapping_add` is sufficient and overflow-safe.
133        let previous_revision = self.buffer_revision;
134        self.buffer_revision = self.buffer_revision.wrapping_add(1);
135        self.refresh_visual_lines_after_edit(previous_revision);
136        self.refresh_max_content_width_after_edit(previous_revision);
137        // Truncate the syntax-highlight cache from the first line the edit may
138        // have changed. `pre_edit_line` is the topmost active line captured
139        // before the edit; the extra line of margin covers edits that merge
140        // with the preceding line (e.g. backspace at column 0).
141        self.invalidate_highlight_from(self.pre_edit_line.saturating_sub(1));
142        self.content_cache.clear();
143        self.overlay_cache.clear();
144        self.enqueue_incremental_lsp_change();
145    }
146
147    /// Returns the topmost logical line currently touched by any cursor or its
148    /// selection anchor.
149    ///
150    /// This is captured before an edit to bound which highlight-cache lines may
151    /// change. With no cursors it defaults to line `0`.
152    pub(crate) fn min_active_line(&self) -> usize {
153        self.cursors
154            .iter()
155            .map(|cursor| match cursor.anchor {
156                Some(anchor) => cursor.position.0.min(anchor.0),
157                None => cursor.position.0,
158            })
159            .min()
160            .unwrap_or(0)
161    }
162
163    /// Returns the bottommost logical line touched by a cursor or selection.
164    fn max_active_line(&self) -> usize {
165        self.cursors
166            .iter()
167            .map(|cursor| match cursor.anchor {
168                Some(anchor) => cursor.position.0.max(anchor.0),
169                None => cursor.position.0,
170            })
171            .max()
172            .unwrap_or(0)
173    }
174
175    /// Captures a conservative old-document line range for an incremental LSP
176    /// replacement. Non-editing messages do not allocate or retain a snapshot.
177    fn capture_lsp_edit_snapshot(&mut self, message: &Message) {
178        if self.lsp_document.is_none() {
179            self.lsp_edit_snapshot = None;
180            return;
181        }
182
183        let is_local_edit = matches!(
184            message,
185            Message::CharacterInput(_)
186                | Message::Tab
187                | Message::Enter
188                | Message::Backspace
189                | Message::Delete
190                | Message::DeleteSelection
191                | Message::Paste(_)
192                | Message::ImeCommit(_)
193                | Message::MoveLineUp
194                | Message::MoveLineDown
195                | Message::DuplicateLineUp
196                | Message::DuplicateLineDown
197                | Message::ToggleComment
198        );
199        let is_global_edit = matches!(
200            message,
201            Message::Undo | Message::Redo | Message::ReplaceAll
202        );
203        let is_replace_next = matches!(message, Message::ReplaceNext);
204        if !is_local_edit && !is_global_edit && !is_replace_next {
205            self.lsp_edit_snapshot = None;
206            return;
207        }
208
209        let line_count = self.buffer.line_count();
210        let (mut first_line, mut last_line) = if is_global_edit {
211            (0, line_count.saturating_sub(1))
212        } else {
213            (self.pre_edit_line, self.pre_edit_last_line)
214        };
215        if is_replace_next
216            && let Some(search_match) = self.search_state.current_match()
217        {
218            first_line = first_line.min(search_match.line);
219            last_line = last_line.max(search_match.line);
220        }
221
222        let start_line =
223            if is_global_edit { 0 } else { first_line.saturating_sub(1) };
224        let old_end_exclusive = if is_global_edit {
225            line_count
226        } else {
227            last_line.saturating_add(2).min(line_count)
228        };
229        let old_end = if old_end_exclusive < line_count {
230            lsp::LspPosition {
231                line: u32::try_from(old_end_exclusive).unwrap_or(u32::MAX),
232                character: 0,
233            }
234        } else {
235            let last_line = line_count.saturating_sub(1);
236            lsp::LspPosition {
237                line: u32::try_from(last_line).unwrap_or(u32::MAX),
238                character: u32::try_from(self.buffer.line_len(last_line))
239                    .unwrap_or(u32::MAX),
240            }
241        };
242
243        self.lsp_edit_snapshot = Some(LspEditSnapshot {
244            start_line,
245            old_end_exclusive,
246            old_line_count: line_count,
247            old_end,
248        });
249    }
250
251    /// Truncates the syntax-highlight cache so logical lines `>= line` are
252    /// re-highlighted on next access.
253    ///
254    /// Lines before the first edited line are unaffected, so the cached prefix
255    /// is preserved and edits never trigger a full re-parse from the top of the
256    /// file. Has no effect when the cache is empty.
257    ///
258    /// # Arguments
259    ///
260    /// * `line` - First logical line to invalidate.
261    pub(crate) fn invalidate_highlight_from(&self, line: usize) {
262        if let Some(cache) = self.highlight_cache.borrow_mut().as_mut() {
263            cache.truncate(line);
264        }
265    }
266
267    /// Performs common cleanup operations after navigation operations.
268    ///
269    /// This method should be called after cursor movement operations.
270    /// It resets the cursor blink animation and invalidates only the overlay
271    /// rendering cache. Cursor movement and selection changes do not modify the
272    /// buffer content, so keeping the content cache intact avoids unnecessary
273    /// re-rendering of syntax-highlighted text.
274    fn finish_navigation_operation(&mut self) {
275        self.sync_search_match_from_primary_cursor();
276        self.reset_cursor_blink();
277        self.overlay_cache.clear();
278    }
279
280    /// Starts command grouping with the given label if not already grouping.
281    ///
282    /// This is used for smart undo functionality, allowing multiple related
283    /// operations to be undone as a single unit.
284    ///
285    /// # Arguments
286    ///
287    /// * `label` - A descriptive label for the group of commands
288    fn ensure_grouping_started(&mut self, label: &str) {
289        if !self.is_grouping {
290            self.history.begin_group(label);
291            self.is_grouping = true;
292        }
293    }
294
295    /// Ends command grouping if currently active.
296    ///
297    /// This should be called when a series of related operations is complete,
298    /// or when starting a new type of operation that shouldn't be grouped
299    /// with previous operations.
300    fn end_grouping_if_active(&mut self) {
301        if self.is_grouping {
302            self.history.end_group();
303            self.is_grouping = false;
304        }
305    }
306
307    fn keep_vim_insert_group(&self) -> bool {
308        self.vim_enabled
309            && self.vim_state.mode() == VimMode::Insert
310            && self.is_grouping
311    }
312
313    /// Deletes all active selections across every cursor and performs cleanup.
314    ///
315    /// # Returns
316    ///
317    /// `true` if at least one selection was deleted, `false` if no cursor had a selection
318    fn delete_selection_if_present(&mut self) -> bool {
319        if self.cursors.iter().any(|c| c.has_selection()) {
320            self.delete_selection();
321            self.finish_edit_operation();
322            true
323        } else {
324            false
325        }
326    }
327
328    // =========================================================================
329    // Text Input Handlers
330    // =========================================================================
331
332    /// Handles character input message operations.
333    ///
334    /// Inserts a character at the current cursor position and adds it to the
335    /// undo history. Characters are grouped together for smart undo.
336    /// Only processes input when the editor has active focus and is not locked.
337    ///
338    /// # Arguments
339    ///
340    /// * `ch` - The character to insert
341    ///
342    /// # Returns
343    ///
344    /// A `Task<Message>` that scrolls to keep the cursor visible (including
345    /// horizontal scroll when wrap is disabled)
346    fn handle_character_input_msg(&mut self, ch: char) -> Task<Message> {
347        // Guard clause: only process character input if editor has focus and is not locked
348        if !self.has_focus() {
349            return Task::none();
350        }
351
352        // Start grouping if not already grouping (for smart undo)
353        self.ensure_grouping_started("Typing");
354
355        // Typing replaces active selections, matching paste and IME commit
356        // behavior. Keep the deletion and insertion in the same history group
357        // so a single undo restores the replaced text.
358        if self.cursors.iter().any(|cursor| cursor.has_selection()) {
359            self.delete_selection();
360        } else {
361            // A plain click leaves a zero-length anchor in place (see
362            // `handle_enter`); clear it so it isn't mistaken for a real
363            // selection by a later edit.
364            self.clear_selection();
365        }
366
367        // Multi-cursor: build a sorted index list (descending document order)
368        // so that edits at higher positions don't invalidate lower positions.
369        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
370        order.sort_by(|&a, &b| {
371            self.cursors.as_slice()[b]
372                .position
373                .cmp(&self.cursors.as_slice()[a].position)
374        });
375
376        for &idx in &order {
377            // Any active selection was deleted above, which also moves the
378            // cursor to the original selection start and clears its anchor.
379            // The current cursor position is therefore the insertion point.
380            let pos = self.cursors.as_slice()[idx].position;
381            let mut cmd = InsertCharCommand::new(pos.0, pos.1, ch, pos);
382            let mut cursor_pos = pos;
383            cmd.execute(&mut self.buffer, &mut cursor_pos);
384            self.cursors.as_mut_slice()[idx].position = cursor_pos;
385            adjust_other_cursors(
386                self.cursors.as_mut_slice(),
387                idx,
388                pos.0,
389                pos.1,
390                EditType::InsertChar,
391            );
392            self.history.push(Box::new(cmd));
393        }
394
395        self.finish_edit_operation();
396
397        // Auto-trigger LSP completion for identifier characters and trigger characters
398        if ch.is_alphanumeric() || ch == '_' || ch == '.' {
399            self.lsp_flush_pending_changes();
400            self.lsp_request_completion();
401        }
402
403        self.scroll_to_cursor()
404    }
405
406    /// Handles Tab key press (inserts 4 spaces).
407    ///
408    /// # Returns
409    ///
410    /// A `Task<Message>` that scrolls to keep the cursor visible (including
411    /// horizontal scroll when wrap is disabled)
412    fn handle_tab(&mut self) -> Task<Message> {
413        self.ensure_grouping_started("Tab");
414
415        // A plain click leaves a zero-length anchor in place (see
416        // `handle_enter`); clear it so it isn't mistaken for a real selection
417        // by a later edit. Tab only reaches here when there is no real
418        // selection to indent (see `IndentLines`).
419        self.clear_selection();
420
421        // Multi-cursor: process in descending document order
422        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
423        order.sort_by(|&a, &b| {
424            self.cursors.as_slice()[b]
425                .position
426                .cmp(&self.cursors.as_slice()[a].position)
427        });
428
429        for &idx in &order {
430            let pos = self.cursors.as_slice()[idx].position;
431            match self.indent_style {
432                IndentStyle::Spaces(n) => {
433                    let mut cursor_pos = pos;
434                    for _i in 0..n as usize {
435                        let current_col = cursor_pos.1;
436                        let mut cmd = InsertCharCommand::new(
437                            pos.0,
438                            current_col,
439                            ' ',
440                            cursor_pos,
441                        );
442                        cmd.execute(&mut self.buffer, &mut cursor_pos);
443                        adjust_other_cursors(
444                            self.cursors.as_mut_slice(),
445                            idx,
446                            pos.0,
447                            current_col,
448                            EditType::InsertChar,
449                        );
450                        self.history.push(Box::new(cmd));
451                    }
452                    self.cursors.as_mut_slice()[idx].position = cursor_pos;
453                }
454                IndentStyle::Tab => {
455                    let mut cmd =
456                        InsertCharCommand::new(pos.0, pos.1, '\t', pos);
457                    let mut cursor_pos = pos;
458                    cmd.execute(&mut self.buffer, &mut cursor_pos);
459                    adjust_other_cursors(
460                        self.cursors.as_mut_slice(),
461                        idx,
462                        pos.0,
463                        pos.1,
464                        EditType::InsertChar,
465                    );
466                    self.cursors.as_mut_slice()[idx].position = cursor_pos;
467                    self.history.push(Box::new(cmd));
468                }
469            }
470        }
471
472        self.finish_edit_operation();
473        self.scroll_to_cursor()
474    }
475
476    /// Handles Tab key press for focus navigation (when search dialog is not open).
477    ///
478    /// # Returns
479    ///
480    /// A `Task<Message>` that may navigate focus to another editor
481    fn handle_focus_navigation_tab(&mut self) -> Task<Message> {
482        // Only handle focus navigation if search dialog is not open
483        if !self.search_state.is_open {
484            // Lose focus from current editor
485            self.has_canvas_focus = false;
486            self.show_cursor = false;
487
488            // Return a task that could potentially focus another editor
489            // This implements focus chain management by allowing the parent application
490            // to handle focus navigation between multiple editors
491            Task::none()
492        } else {
493            Task::none()
494        }
495    }
496
497    /// Handles Shift+Tab key press for focus navigation (when search dialog is not open).
498    ///
499    /// # Returns
500    ///
501    /// A `Task<Message>` that may navigate focus to another editor
502    fn handle_focus_navigation_shift_tab(&mut self) -> Task<Message> {
503        // Only handle focus navigation if search dialog is not open
504        if !self.search_state.is_open {
505            // Lose focus from current editor
506            self.has_canvas_focus = false;
507            self.show_cursor = false;
508
509            // Return a task that could potentially focus another editor
510            // This implements focus chain management by allowing the parent application
511            // to handle focus navigation between multiple editors
512            Task::none()
513        } else {
514            Task::none()
515        }
516    }
517
518    /// Handles Enter key press (inserts newline).
519    ///
520    /// # Returns
521    ///
522    /// A `Task<Message>` that scrolls to keep the cursor visible
523    fn handle_enter(&mut self) -> Task<Message> {
524        // Standard editing treats Enter as a boundary. In Vim Insert mode the
525        // newline belongs to the same insertion session and is closed by Esc.
526        let keep_vim_group = self.keep_vim_insert_group();
527        if !keep_vim_group {
528            self.end_grouping_if_active();
529        }
530
531        // A mouse click leaves a zero-length anchor in place so a following
532        // drag can extend the selection. Enter must clear that anchor before
533        // moving the caret to the new line; otherwise the inserted newline
534        // becomes selected and the next typed character deletes it.
535        //
536        // For a real selection, Enter replaces the selected text with a
537        // newline. Group both commands so one undo restores the selection.
538        let replaces_selection =
539            self.cursors.iter().any(|cursor| cursor.has_selection());
540        if replaces_selection {
541            self.ensure_grouping_started("Enter");
542            self.delete_selection();
543        } else {
544            self.clear_selection();
545        }
546
547        // Multi-cursor: process in descending document order
548        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
549        order.sort_by(|&a, &b| {
550            self.cursors.as_slice()[b]
551                .position
552                .cmp(&self.cursors.as_slice()[a].position)
553        });
554
555        for &idx in &order {
556            let pos = self.cursors.as_slice()[idx].position;
557
558            // Copy leading whitespace of the current line to the new line (if enabled)
559            let indent: String = if self.auto_indent_enabled {
560                self.buffer
561                    .line(pos.0)
562                    .chars()
563                    .take_while(|c| c.is_whitespace())
564                    .collect()
565            } else {
566                String::new()
567            };
568            let indent_len = indent.chars().count();
569
570            let mut cmd =
571                InsertNewlineCommand::with_indent(pos.0, pos.1, pos, indent);
572            let mut cursor_pos = pos;
573            cmd.execute(&mut self.buffer, &mut cursor_pos);
574            self.cursors.as_mut_slice()[idx].position = cursor_pos;
575            adjust_other_cursors(
576                self.cursors.as_mut_slice(),
577                idx,
578                pos.0,
579                pos.1,
580                EditType::InsertNewline { indent_len },
581            );
582            self.history.push(Box::new(cmd));
583        }
584
585        if replaces_selection && !keep_vim_group {
586            self.end_grouping_if_active();
587        }
588
589        self.finish_edit_operation();
590        self.scroll_to_cursor()
591    }
592
593    // =========================================================================
594    // Line Manipulation Handlers
595    // =========================================================================
596
597    /// Returns the inclusive line range affected by the primary cursor.
598    ///
599    /// When the primary cursor has a selection, the range covers every line it
600    /// spans. A selection that ends at column 0 of a line does not include that
601    /// trailing line (VS Code convention). Without a selection, the range is the
602    /// single line the cursor sits on.
603    fn primary_line_range(&self) -> (usize, usize) {
604        let primary = self.cursors.primary();
605        match primary.selection_range() {
606            Some((sel_start, sel_end)) => {
607                let end_line = if sel_end.1 == 0 && sel_end.0 > sel_start.0 {
608                    sel_end.0 - 1
609                } else {
610                    sel_end.0
611                };
612                (sel_start.0, end_line)
613            }
614            None => {
615                let line = primary.position.0;
616                (line, line)
617            }
618        }
619    }
620
621    /// Shifts the primary cursor's position and selection anchor by `delta`
622    /// whole lines (positive moves downward) so the selection follows an edit.
623    fn shift_primary_cursor_lines(&mut self, delta: isize) {
624        let primary = self.cursors.primary_mut();
625        primary.position.0 = primary.position.0.saturating_add_signed(delta);
626        if let Some(anchor) = primary.anchor.as_mut() {
627            anchor.0 = anchor.0.saturating_add_signed(delta);
628        }
629    }
630
631    /// Moves the current line, or the lines spanned by the primary selection,
632    /// up or down by one line (Alt+Up / Alt+Down).
633    ///
634    /// Secondary cursors are collapsed onto the primary one. The move is a no-op
635    /// when the affected range is already at the corresponding edge of the
636    /// buffer.
637    ///
638    /// # Arguments
639    ///
640    /// * `down` - `true` to move the range down, `false` to move it up
641    ///
642    /// # Returns
643    ///
644    /// A `Task<Message>` that scrolls to keep the cursor visible
645    fn move_lines(&mut self, down: bool) -> Task<Message> {
646        self.end_grouping_if_active();
647        self.cursors.remove_all_but_primary();
648
649        let (start, end) = self.primary_line_range();
650
651        // Reject moves that would push the range past the buffer edges.
652        if down {
653            if end + 1 >= self.buffer.line_count() {
654                return Task::none();
655            }
656        } else if start == 0 {
657            return Task::none();
658        }
659
660        let pos = self.cursors.primary_position();
661        let mut cmd = MoveLinesCommand::new(start, end, down, pos);
662        let mut cursor_pos = pos;
663        cmd.execute(&mut self.buffer, &mut cursor_pos);
664        self.shift_primary_cursor_lines(if down { 1 } else { -1 });
665        self.history.push(Box::new(cmd));
666
667        self.finish_edit_operation();
668        self.scroll_to_cursor()
669    }
670
671    /// Duplicates the current line, or the lines spanned by the primary
672    /// selection, above or below (Shift+Alt+Up / Shift+Alt+Down).
673    ///
674    /// Secondary cursors are collapsed onto the primary one. A downward
675    /// duplication moves the cursor onto the new copy; an upward one leaves it
676    /// on the (upper) copy.
677    ///
678    /// # Arguments
679    ///
680    /// * `down` - `true` to insert the copy below, `false` to insert it above
681    ///
682    /// # Returns
683    ///
684    /// A `Task<Message>` that scrolls to keep the cursor visible
685    fn duplicate_lines(&mut self, down: bool) -> Task<Message> {
686        self.end_grouping_if_active();
687        self.cursors.remove_all_but_primary();
688
689        let (start, end) = self.primary_line_range();
690        let pos = self.cursors.primary_position();
691        let mut cmd = DuplicateLinesCommand::new(start, end, down, pos);
692        let mut cursor_pos = pos;
693        cmd.execute(&mut self.buffer, &mut cursor_pos);
694        if down {
695            let block_len = (end - start + 1) as isize;
696            self.shift_primary_cursor_lines(block_len);
697        }
698        self.history.push(Box::new(cmd));
699
700        self.finish_edit_operation();
701        self.scroll_to_cursor()
702    }
703
704    /// Toggles line comments on the current line, or the lines spanned by the
705    /// primary selection (Ctrl+/).
706    ///
707    /// Secondary cursors are collapsed onto the primary one. If every non-blank
708    /// line in the range is already commented, the range is uncommented;
709    /// otherwise every non-blank line is commented. The operation is a no-op
710    /// when the active syntax has no line-comment token (e.g. HTML) or the range
711    /// holds only blank lines.
712    ///
713    /// # Returns
714    ///
715    /// A `Task<Message>` that scrolls to keep the cursor visible
716    fn toggle_comment(&mut self) -> Task<Message> {
717        self.end_grouping_if_active();
718        self.cursors.remove_all_but_primary();
719
720        let Some(token) = line_comment_token(&self.syntax) else {
721            return Task::none();
722        };
723
724        let (start, end) = self.primary_line_range();
725        let pos = self.cursors.primary_position();
726        let mut cmd =
727            ToggleCommentCommand::new(&self.buffer, start, end, token, pos);
728        if cmd.is_noop() {
729            return Task::none();
730        }
731
732        // Track the selection anchor across the column shift before executing.
733        let new_anchor =
734            self.cursors.primary().anchor.map(|a| cmd.adjust_position(a));
735
736        let mut cursor_pos = pos;
737        cmd.execute(&mut self.buffer, &mut cursor_pos);
738        let primary = self.cursors.primary_mut();
739        primary.position = cursor_pos;
740        primary.anchor = new_anchor;
741        self.history.push(Box::new(cmd));
742
743        self.finish_edit_operation();
744        self.scroll_to_cursor()
745    }
746
747    // =========================================================================
748    // Deletion Handlers
749    // =========================================================================
750
751    /// Handles Backspace key press.
752    ///
753    /// If there's a selection, deletes the selection. Otherwise, deletes the
754    /// character before the cursor.
755    ///
756    /// # Returns
757    ///
758    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
759    fn handle_backspace(&mut self) -> Task<Message> {
760        // End grouping on backspace (separate from typing)
761        if !self.keep_vim_insert_group() {
762            self.end_grouping_if_active();
763        }
764
765        // If any cursor has a selection, delete all selections first
766        if self.delete_selection_if_present() {
767            return self.scroll_to_cursor();
768        }
769
770        // A mouse click leaves a zero-length anchor in place so a following
771        // drag can extend the selection (see `handle_enter`). Backspace must
772        // clear it before moving the caret; otherwise the anchor is left
773        // behind at the pre-edit position and a phantom one-character
774        // selection appears next to the cursor, which the next Backspace or
775        // Delete then eats instead of a single character.
776        self.clear_selection();
777
778        // Multi-cursor: process in descending document order
779        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
780        order.sort_by(|&a, &b| {
781            self.cursors.as_slice()[b]
782                .position
783                .cmp(&self.cursors.as_slice()[a].position)
784        });
785
786        for &idx in &order {
787            let pos = self.cursors.as_slice()[idx].position;
788            // Determine edit type for adjusting other cursors
789            let edit_kind = if pos.1 > 0 {
790                EditType::DeleteCharBack
791            } else if pos.0 > 0 {
792                let prev_line_len = self.buffer.line_len(pos.0 - 1);
793                EditType::MergePrev { prev_line_len }
794            } else {
795                // At very start of document: nothing to delete
796                continue;
797            };
798            let mut cmd =
799                DeleteCharCommand::new(&self.buffer, pos.0, pos.1, pos);
800            let mut cursor_pos = pos;
801            cmd.execute(&mut self.buffer, &mut cursor_pos);
802            self.cursors.as_mut_slice()[idx].position = cursor_pos;
803            adjust_other_cursors(
804                self.cursors.as_mut_slice(),
805                idx,
806                pos.0,
807                pos.1,
808                edit_kind,
809            );
810            self.history.push(Box::new(cmd));
811        }
812
813        self.finish_edit_operation();
814        self.scroll_to_cursor()
815    }
816
817    /// Handles Delete key press.
818    ///
819    /// If there's a selection, deletes the selection. Otherwise, deletes the
820    /// character after the cursor.
821    ///
822    /// # Returns
823    ///
824    /// A `Task<Message>` that scrolls to keep the cursor visible if selection was deleted
825    fn handle_delete(&mut self) -> Task<Message> {
826        // End grouping on delete
827        if !self.keep_vim_insert_group() {
828            self.end_grouping_if_active();
829        }
830
831        // If any cursor has a selection, delete all selections first
832        if self.delete_selection_if_present() {
833            return self.scroll_to_cursor();
834        }
835
836        // See the matching comment in `handle_backspace`: clear any
837        // zero-length anchor left by a plain click before editing, so it
838        // can't be mistaken for a real selection on a later edit.
839        self.clear_selection();
840
841        // Multi-cursor: process in descending document order
842        let mut order: Vec<usize> = (0..self.cursors.len()).collect();
843        order.sort_by(|&a, &b| {
844            self.cursors.as_slice()[b]
845                .position
846                .cmp(&self.cursors.as_slice()[a].position)
847        });
848
849        for &idx in &order {
850            let pos = self.cursors.as_slice()[idx].position;
851            let line_len = self.buffer.line_len(pos.0);
852            let edit_kind = if pos.1 < line_len {
853                EditType::DeleteCharForward
854            } else if pos.0 + 1 < self.buffer.line_count() {
855                EditType::MergeNext { edit_line_len: line_len }
856            } else {
857                // At very end of document: nothing to delete
858                continue;
859            };
860            let mut cmd =
861                DeleteForwardCommand::new(&self.buffer, pos.0, pos.1, pos);
862            let mut cursor_pos = pos;
863            cmd.execute(&mut self.buffer, &mut cursor_pos);
864            self.cursors.as_mut_slice()[idx].position = cursor_pos;
865            adjust_other_cursors(
866                self.cursors.as_mut_slice(),
867                idx,
868                pos.0,
869                pos.1,
870                edit_kind,
871            );
872            self.history.push(Box::new(cmd));
873        }
874
875        self.finish_edit_operation();
876        Task::none()
877    }
878
879    /// Handles explicit selection deletion (Shift+Delete).
880    ///
881    /// Deletes the selected text if a selection exists.
882    ///
883    /// # Returns
884    ///
885    /// A `Task<Message>` that scrolls to keep the cursor visible
886    fn handle_delete_selection(&mut self) -> Task<Message> {
887        // End grouping on delete selection
888        self.end_grouping_if_active();
889
890        if self.cursors.iter().any(|c| c.has_selection()) {
891            self.delete_selection();
892            self.finish_edit_operation();
893            self.scroll_to_cursor()
894        } else {
895            Task::none()
896        }
897    }
898
899    // =========================================================================
900    // Navigation Handlers
901    // =========================================================================
902
903    fn vim_accepts_insert_input(&self) -> bool {
904        !self.vim_enabled || self.vim_state.mode() == VimMode::Insert
905    }
906
907    fn handle_vim_key_msg(&mut self, key: char) -> Task<Message> {
908        if !self.vim_enabled {
909            return Task::none();
910        }
911
912        let previous_mode = self.vim_state.mode();
913        let action = self.vim_state.parse_key(key);
914        match action {
915            Some(VimAction::Mode(mode)) => {
916                self.handle_vim_mode(mode, previous_mode)
917            }
918            Some(VimAction::Motion { motion, count, explicit_count }) => {
919                self.handle_vim_motion(motion, count, explicit_count)
920            }
921            Some(VimAction::Insert { position, count }) => {
922                self.handle_vim_insert(position, count)
923            }
924            Some(VimAction::Operator {
925                operator,
926                motion,
927                count,
928                explicit_count,
929            }) => self.handle_vim_motion_operator(
930                operator,
931                motion,
932                count,
933                explicit_count,
934            ),
935            Some(VimAction::LineOperator { operator, count }) => {
936                let start_line = self.cursors.primary_position().0;
937                let end_line = start_line
938                    .saturating_add(count.saturating_sub(1))
939                    .min(self.buffer.line_count().saturating_sub(1));
940                self.handle_vim_line_operator(
941                    operator, start_line, end_line, false,
942                )
943            }
944            Some(VimAction::VisualOperator(operator)) => {
945                self.handle_vim_visual_operator(operator)
946            }
947            Some(VimAction::DeleteCharacters { count }) => {
948                self.handle_vim_delete_characters(count)
949            }
950            Some(VimAction::Paste { position, count }) => {
951                self.handle_vim_paste(position, count)
952            }
953            Some(VimAction::Undo { count }) => {
954                self.handle_vim_history(false, count)
955            }
956            Some(VimAction::Redo { count }) => {
957                self.handle_vim_history(true, count)
958            }
959            Some(VimAction::RepeatSearch { reverse }) => {
960                self.handle_vim_repeat_search(reverse)
961            }
962            Some(VimAction::SubmitSearch(query)) => {
963                self.handle_vim_search(&query)
964            }
965            Some(VimAction::SubmitGotoLine(line)) => {
966                self.handle_goto_position(line.saturating_sub(1), 0)
967            }
968            Some(VimAction::WriteFile { exit_vim }) => {
969                if exit_vim {
970                    self.set_vim_enabled(false);
971                }
972                Task::done(Message::WriteRequested)
973            }
974            Some(VimAction::ExitVimMode) => {
975                self.set_vim_enabled(false);
976                Task::none()
977            }
978            Some(VimAction::CommandLineChanged) => {
979                self.overlay_cache.clear();
980                Task::none()
981            }
982            None => Task::none(),
983        }
984    }
985
986    fn handle_vim_search(&mut self, query: &str) -> Task<Message> {
987        if !self.search_replace_enabled || query.is_empty() {
988            return Task::none();
989        }
990
991        self.search_state.close();
992        self.search_state.set_query(query.to_owned(), &self.buffer);
993        if self.search_state.matches.is_empty() {
994            self.overlay_cache.clear();
995            return Task::none();
996        }
997
998        let cursor = self.cursors.primary_position();
999        let next_index = self
1000            .search_state
1001            .matches
1002            .partition_point(|item| (item.line, item.col) <= cursor);
1003        self.search_state.current_match_index =
1004            Some(if next_index == self.search_state.matches.len() {
1005                0
1006            } else {
1007                next_index
1008            });
1009
1010        if let Some(search_match) = self.search_state.current_match() {
1011            self.cursors.set_single((search_match.line, search_match.col));
1012        }
1013        self.finish_navigation_operation();
1014        self.scroll_to_cursor()
1015    }
1016
1017    fn handle_vim_repeat_search(&mut self, reverse: bool) -> Task<Message> {
1018        let Some(last_search) = self.vim_state.last_search().map(str::to_owned)
1019        else {
1020            return Task::none();
1021        };
1022        if self.search_state.query != last_search {
1023            self.search_state.set_query(last_search, &self.buffer);
1024            self.search_state
1025                .select_match_near_cursor(self.cursors.primary_position());
1026        }
1027        if self.search_state.matches.is_empty() {
1028            return Task::none();
1029        }
1030
1031        if reverse {
1032            self.search_state.previous_match();
1033        } else {
1034            self.search_state.next_match();
1035        }
1036        if let Some(search_match) = self.search_state.current_match() {
1037            self.cursors.set_single((search_match.line, search_match.col));
1038        }
1039        self.finish_navigation_operation();
1040        self.scroll_to_cursor()
1041    }
1042
1043    fn handle_vim_delete_characters(&mut self, count: usize) -> Task<Message> {
1044        let start = self.vim_normal_position(self.cursors.primary_position());
1045        let line_len = self.buffer.line_len(start.0);
1046        let end = (start.0, start.1.saturating_add(count).min(line_len));
1047        self.handle_vim_character_operator(
1048            VimOperator::Delete,
1049            start,
1050            end,
1051            false,
1052        )
1053    }
1054
1055    fn handle_vim_motion_operator(
1056        &mut self,
1057        operator: VimOperator,
1058        motion: VimMotion,
1059        count: usize,
1060        explicit_count: bool,
1061    ) -> Task<Message> {
1062        let start = self.vim_normal_position(self.cursors.primary_position());
1063        if matches!(
1064            motion,
1065            VimMotion::Up
1066                | VimMotion::Down
1067                | VimMotion::DocumentStart
1068                | VimMotion::DocumentEnd
1069        ) {
1070            let target =
1071                self.vim_motion_target(start, motion, count, explicit_count);
1072            return self.handle_vim_line_operator(
1073                operator,
1074                start.0.min(target.0),
1075                start.0.max(target.0),
1076                false,
1077            );
1078        }
1079
1080        let target =
1081            self.vim_motion_target(start, motion, count, explicit_count);
1082        let (range_start, range_end) = match motion {
1083            VimMotion::Right => (
1084                start,
1085                (
1086                    start.0,
1087                    start
1088                        .1
1089                        .saturating_add(count)
1090                        .min(self.buffer.line_len(start.0)),
1091                ),
1092            ),
1093            VimMotion::Left => {
1094                ((start.0, start.1.saturating_sub(count)), start)
1095            }
1096            VimMotion::WordEnd | VimMotion::LineEnd => {
1097                let end = if motion == VimMotion::LineEnd {
1098                    (start.0, self.buffer.line_len(start.0))
1099                } else {
1100                    (
1101                        target.0,
1102                        target
1103                            .1
1104                            .saturating_add(1)
1105                            .min(self.buffer.line_len(target.0)),
1106                    )
1107                };
1108                (start.min(end), start.max(end))
1109            }
1110            VimMotion::WordForward => {
1111                let end = if target > start {
1112                    target
1113                } else {
1114                    (start.0, self.buffer.line_len(start.0))
1115                };
1116                (start.min(end), start.max(end))
1117            }
1118            VimMotion::WordBackward
1119            | VimMotion::LineStart
1120            | VimMotion::FirstNonBlank => {
1121                (start.min(target), start.max(target))
1122            }
1123            VimMotion::Up
1124            | VimMotion::Down
1125            | VimMotion::DocumentStart
1126            | VimMotion::DocumentEnd => return Task::none(),
1127        };
1128
1129        self.handle_vim_character_operator(
1130            operator,
1131            range_start,
1132            range_end,
1133            false,
1134        )
1135    }
1136
1137    fn handle_vim_visual_operator(
1138        &mut self,
1139        operator: VimOperator,
1140    ) -> Task<Message> {
1141        if self.vim_state.mode() == VimMode::VisualLine {
1142            let (anchor, active) =
1143                self.vim_state.visual_positions().unwrap_or_else(|| {
1144                    let position = self.cursors.primary_position();
1145                    (position, position)
1146                });
1147            self.handle_vim_line_operator(
1148                operator,
1149                anchor.0.min(active.0),
1150                anchor.0.max(active.0),
1151                true,
1152            )
1153        } else {
1154            let Some((start, end)) = self.cursors.primary().selection_range()
1155            else {
1156                return Task::none();
1157            };
1158            self.handle_vim_character_operator(operator, start, end, true)
1159        }
1160    }
1161
1162    fn handle_vim_character_operator(
1163        &mut self,
1164        operator: VimOperator,
1165        start: (usize, usize),
1166        end: (usize, usize),
1167        from_visual: bool,
1168    ) -> Task<Message> {
1169        if start == end {
1170            return Task::none();
1171        }
1172        let register = VimRegister {
1173            text: self.extract_text_range(start, end),
1174            kind: VimRegisterKind::Characterwise,
1175        };
1176        self.apply_vim_operator(operator, start, end, register, from_visual)
1177    }
1178
1179    fn handle_vim_line_operator(
1180        &mut self,
1181        operator: VimOperator,
1182        start_line: usize,
1183        end_line: usize,
1184        from_visual: bool,
1185    ) -> Task<Message> {
1186        let last_line = self.buffer.line_count().saturating_sub(1);
1187        let start_line = start_line.min(last_line);
1188        let end_line = end_line.min(last_line).max(start_line);
1189        let mut text = String::new();
1190        for line in start_line..=end_line {
1191            text.push_str(self.buffer.line(line));
1192            text.push('\n');
1193        }
1194
1195        let (start, end) = if end_line < last_line {
1196            ((start_line, 0), (end_line + 1, 0))
1197        } else if start_line > 0 {
1198            (
1199                (start_line - 1, self.buffer.line_len(start_line - 1)),
1200                (end_line, self.buffer.line_len(end_line)),
1201            )
1202        } else {
1203            ((0, 0), (end_line, self.buffer.line_len(end_line)))
1204        };
1205        self.apply_vim_operator(
1206            operator,
1207            start,
1208            end,
1209            VimRegister { text, kind: VimRegisterKind::Linewise },
1210            from_visual,
1211        )
1212    }
1213
1214    fn apply_vim_operator(
1215        &mut self,
1216        operator: VimOperator,
1217        start: (usize, usize),
1218        end: (usize, usize),
1219        register: VimRegister,
1220        from_visual: bool,
1221    ) -> Task<Message> {
1222        self.vim_state.register = register;
1223
1224        if operator == VimOperator::Yank {
1225            if from_visual {
1226                self.cursors.set_single(self.vim_normal_position(start));
1227            } else {
1228                let position =
1229                    self.vim_normal_position(self.cursors.primary_position());
1230                self.cursors.set_single(position);
1231            }
1232            self.vim_state.enter_clean_normal_mode();
1233            self.finish_navigation_operation();
1234            return self.scroll_to_cursor();
1235        }
1236
1237        self.end_grouping_if_active();
1238        if operator == VimOperator::Change {
1239            self.ensure_grouping_started("Vim change");
1240        }
1241
1242        self.pre_edit_line = start.0.min(end.0);
1243        self.pre_edit_last_line = start.0.max(end.0);
1244        self.capture_lsp_edit_snapshot(&Message::DeleteSelection);
1245
1246        let cursor_before = self.cursors.primary_position();
1247        let mut command =
1248            DeleteRangeCommand::new(&self.buffer, start, end, cursor_before);
1249        let mut cursor_after = cursor_before;
1250        command.execute(&mut self.buffer, &mut cursor_after);
1251        self.history.push(Box::new(command));
1252        self.cursors.set_single(self.vim_normal_position(cursor_after));
1253
1254        if operator == VimOperator::Change {
1255            self.vim_state.enter_insert_mode();
1256        } else {
1257            self.vim_state.enter_clean_normal_mode();
1258        }
1259        self.finish_edit_operation();
1260        self.scroll_to_cursor()
1261    }
1262
1263    fn handle_vim_paste(
1264        &mut self,
1265        position: VimPastePosition,
1266        count: usize,
1267    ) -> Task<Message> {
1268        let register = self.vim_state.register.clone();
1269        if register.text.is_empty() {
1270            return Task::none();
1271        }
1272        self.end_grouping_if_active();
1273
1274        let current = self.vim_normal_position(self.cursors.primary_position());
1275        let (insert_at, text, cursor_after) = match register.kind {
1276            VimRegisterKind::Characterwise => {
1277                let insert_at = match position {
1278                    VimPastePosition::BeforeCursor => current,
1279                    VimPastePosition::AfterCursor => (
1280                        current.0,
1281                        current
1282                            .1
1283                            .saturating_add(usize::from(
1284                                self.buffer.line_len(current.0) > 0,
1285                            ))
1286                            .min(self.buffer.line_len(current.0)),
1287                    ),
1288                };
1289                (insert_at, register.text.repeat(count.max(1)), insert_at)
1290            }
1291            VimRegisterKind::Linewise => {
1292                let repeated = register.text.repeat(count.max(1));
1293                match position {
1294                    VimPastePosition::BeforeCursor => {
1295                        ((current.0, 0), repeated, (current.0, 0))
1296                    }
1297                    VimPastePosition::AfterCursor
1298                        if current.0 + 1 < self.buffer.line_count() =>
1299                    {
1300                        ((current.0 + 1, 0), repeated, (current.0 + 1, 0))
1301                    }
1302                    VimPastePosition::AfterCursor => {
1303                        let text = format!(
1304                            "\n{}",
1305                            repeated.strip_suffix('\n').unwrap_or(&repeated)
1306                        );
1307                        (
1308                            (current.0, self.buffer.line_len(current.0)),
1309                            text,
1310                            (current.0 + 1, 0),
1311                        )
1312                    }
1313                }
1314            }
1315        };
1316
1317        self.pre_edit_line = insert_at.0;
1318        self.pre_edit_last_line = insert_at.0;
1319        self.capture_lsp_edit_snapshot(&Message::Paste(text.clone()));
1320        let mut command =
1321            InsertTextCommand::new(insert_at.0, insert_at.1, text, current)
1322                .with_cursor_after(cursor_after);
1323        let mut command_cursor = current;
1324        command.execute(&mut self.buffer, &mut command_cursor);
1325        self.history.push(Box::new(command));
1326        self.cursors.set_single(self.vim_normal_position(command_cursor));
1327        self.vim_state.enter_clean_normal_mode();
1328        self.finish_edit_operation();
1329        self.scroll_to_cursor()
1330    }
1331
1332    fn handle_vim_history(
1333        &mut self,
1334        redo: bool,
1335        count: usize,
1336    ) -> Task<Message> {
1337        self.end_grouping_if_active();
1338        self.pre_edit_line = 0;
1339        self.pre_edit_last_line = usize::MAX;
1340        self.capture_lsp_edit_snapshot(if redo {
1341            &Message::Redo
1342        } else {
1343            &Message::Undo
1344        });
1345
1346        let mut cursor = self.cursors.primary_position();
1347        let mut changed = false;
1348        for _ in 0..count.max(1) {
1349            let applied = if redo {
1350                self.history.redo(&mut self.buffer, &mut cursor)
1351            } else {
1352                self.history.undo(&mut self.buffer, &mut cursor)
1353            };
1354            if !applied {
1355                break;
1356            }
1357            changed = true;
1358        }
1359        if !changed {
1360            return Task::none();
1361        }
1362
1363        self.cursors.set_single(self.vim_normal_position(cursor));
1364        self.vim_state.enter_clean_normal_mode();
1365        self.finish_edit_operation();
1366        self.scroll_to_cursor()
1367    }
1368
1369    fn handle_vim_mode(
1370        &mut self,
1371        mode: VimMode,
1372        previous_mode: VimMode,
1373    ) -> Task<Message> {
1374        self.end_grouping_if_active();
1375        match mode {
1376            VimMode::Normal => {
1377                let mut active = self
1378                    .vim_state
1379                    .visual_positions()
1380                    .map(|(_, active)| active)
1381                    .unwrap_or_else(|| self.cursors.primary_position());
1382                if previous_mode == VimMode::Insert {
1383                    active.1 = active.1.saturating_sub(1);
1384                }
1385                self.vim_state.clear_visual();
1386                self.cursors.set_single(self.vim_normal_position(active));
1387            }
1388            VimMode::Visual | VimMode::VisualLine => {
1389                let position =
1390                    self.vim_normal_position(self.cursors.primary_position());
1391                self.vim_state.begin_visual(position);
1392                self.apply_vim_visual_selection(
1393                    position,
1394                    position,
1395                    mode == VimMode::VisualLine,
1396                );
1397            }
1398            VimMode::Insert => {}
1399        }
1400        self.finish_navigation_operation();
1401        self.scroll_to_cursor()
1402    }
1403
1404    fn handle_vim_motion(
1405        &mut self,
1406        motion: VimMotion,
1407        count: usize,
1408        explicit_count: bool,
1409    ) -> Task<Message> {
1410        self.end_grouping_if_active();
1411        match self.vim_state.mode() {
1412            VimMode::Visual | VimMode::VisualLine => {
1413                let (anchor, active) =
1414                    self.vim_state.visual_positions().unwrap_or_else(|| {
1415                        let position = self.vim_normal_position(
1416                            self.cursors.primary_position(),
1417                        );
1418                        (position, position)
1419                    });
1420                let target = self.vim_motion_target(
1421                    active,
1422                    motion,
1423                    count,
1424                    explicit_count,
1425                );
1426                self.vim_state.set_visual_active(target);
1427                self.apply_vim_visual_selection(
1428                    anchor,
1429                    target,
1430                    self.vim_state.mode() == VimMode::VisualLine,
1431                );
1432            }
1433            VimMode::Normal => {
1434                let target = self.vim_motion_target(
1435                    self.cursors.primary_position(),
1436                    motion,
1437                    count,
1438                    explicit_count,
1439                );
1440                self.cursors.set_single(target);
1441                self.overlay_cache.clear();
1442            }
1443            VimMode::Insert => return Task::none(),
1444        }
1445        self.finish_navigation_operation();
1446        self.scroll_to_cursor()
1447    }
1448
1449    fn handle_vim_insert(
1450        &mut self,
1451        position: VimInsertPosition,
1452        count: usize,
1453    ) -> Task<Message> {
1454        self.end_grouping_if_active();
1455        let current = self
1456            .vim_state
1457            .visual_positions()
1458            .map(|(_, active)| active)
1459            .unwrap_or_else(|| self.cursors.primary_position());
1460        self.vim_state.clear_visual();
1461        let current = self.vim_normal_position(current);
1462        self.cursors.set_single(current);
1463        self.ensure_grouping_started("Vim insert");
1464
1465        match position {
1466            VimInsertPosition::BeforeCursor => {}
1467            VimInsertPosition::AfterCursor => {
1468                let line_len = self.buffer.line_len(current.0);
1469                self.cursors.primary_mut().position.1 =
1470                    current.1.saturating_add(1).min(line_len);
1471            }
1472            VimInsertPosition::FirstNonBlank => {
1473                self.cursors.primary_mut().position.1 = self
1474                    .buffer
1475                    .line(current.0)
1476                    .chars()
1477                    .position(|ch| !ch.is_whitespace())
1478                    .unwrap_or(0);
1479            }
1480            VimInsertPosition::EndOfLine => {
1481                self.cursors.primary_mut().position.1 =
1482                    self.buffer.line_len(current.0);
1483            }
1484            VimInsertPosition::NewLineBelow => {
1485                self.cursors.primary_mut().position.1 =
1486                    self.buffer.line_len(current.0);
1487                for _ in 0..count.max(1) {
1488                    let _ = self.update(&Message::Enter);
1489                }
1490            }
1491            VimInsertPosition::NewLineAbove => {
1492                self.cursors.primary_mut().position.1 = 0;
1493                let line = current.0;
1494                for _ in 0..count.max(1) {
1495                    let _ = self.update(&Message::Enter);
1496                    self.cursors.set_single((line, 0));
1497                }
1498            }
1499        }
1500
1501        self.overlay_cache.clear();
1502        self.reset_cursor_blink();
1503        self.scroll_to_cursor()
1504    }
1505
1506    /// Handles arrow key navigation.
1507    ///
1508    /// # Arguments
1509    ///
1510    /// * `direction` - The direction of movement
1511    /// * `shift_pressed` - Whether Shift is held (for selection)
1512    ///
1513    /// # Returns
1514    ///
1515    /// A `Task<Message>` that scrolls to keep the cursor visible
1516    fn handle_arrow_key(
1517        &mut self,
1518        direction: ArrowDirection,
1519        shift_pressed: bool,
1520    ) -> Task<Message> {
1521        // End grouping on navigation
1522        self.end_grouping_if_active();
1523
1524        if shift_pressed {
1525            // Set anchor on ALL cursors that don't yet have one
1526            for cursor in self.cursors.as_mut_slice() {
1527                if cursor.anchor.is_none() {
1528                    cursor.set_anchor();
1529                }
1530            }
1531            self.move_cursor(direction);
1532        } else {
1533            // Clear all selections, then move all cursors
1534            self.clear_selection();
1535            self.move_cursor(direction);
1536        }
1537        self.finish_navigation_operation();
1538        self.scroll_to_cursor()
1539    }
1540
1541    /// Handles Home key press.
1542    ///
1543    /// Moves the cursor to the start of the current line.
1544    ///
1545    /// # Arguments
1546    ///
1547    /// * `shift_pressed` - Whether Shift is held (for selection)
1548    ///
1549    /// # Returns
1550    ///
1551    /// A `Task<Message>` that scrolls to keep the cursor visible (including
1552    /// horizontal scroll back to x=0 when wrap is disabled)
1553    fn handle_home(&mut self, shift_pressed: bool) -> Task<Message> {
1554        if shift_pressed {
1555            for cursor in self.cursors.as_mut_slice() {
1556                if cursor.anchor.is_none() {
1557                    cursor.set_anchor();
1558                }
1559                cursor.position.1 = 0;
1560            }
1561        } else {
1562            self.clear_selection();
1563            for cursor in self.cursors.as_mut_slice() {
1564                cursor.position.1 = 0;
1565            }
1566        }
1567        self.cursors.sort_and_merge();
1568        self.finish_navigation_operation();
1569        self.scroll_to_cursor()
1570    }
1571
1572    /// Handles End key press.
1573    ///
1574    /// Moves the cursor to the end of the current line.
1575    ///
1576    /// # Arguments
1577    ///
1578    /// * `shift_pressed` - Whether Shift is held (for selection)
1579    ///
1580    /// # Returns
1581    ///
1582    /// A `Task<Message>` that scrolls to keep the cursor visible (including
1583    /// horizontal scroll to end of line when wrap is disabled)
1584    fn handle_end(&mut self, shift_pressed: bool) -> Task<Message> {
1585        if shift_pressed {
1586            for cursor in self.cursors.as_mut_slice() {
1587                if cursor.anchor.is_none() {
1588                    cursor.set_anchor();
1589                }
1590                cursor.position.1 = self.buffer.line_len(cursor.position.0);
1591            }
1592        } else {
1593            self.clear_selection();
1594            for cursor in self.cursors.as_mut_slice() {
1595                cursor.position.1 = self.buffer.line_len(cursor.position.0);
1596            }
1597        }
1598        self.cursors.sort_and_merge();
1599        self.finish_navigation_operation();
1600        self.scroll_to_cursor()
1601    }
1602
1603    /// Handles Ctrl+Home key press.
1604    ///
1605    /// Moves the cursor to the beginning of the document.
1606    ///
1607    /// # Returns
1608    ///
1609    /// A `Task<Message>` that scrolls to keep the cursor visible
1610    fn handle_ctrl_home(&mut self) -> Task<Message> {
1611        // Move cursor to the beginning of the document
1612        self.clear_selection();
1613        self.cursors.set_single((0, 0));
1614        self.finish_navigation_operation();
1615        self.scroll_to_cursor()
1616    }
1617
1618    /// Handles Ctrl+End key press.
1619    ///
1620    /// Moves the cursor to the end of the document.
1621    ///
1622    /// # Returns
1623    ///
1624    /// A `Task<Message>` that scrolls to keep the cursor visible
1625    fn handle_ctrl_end(&mut self) -> Task<Message> {
1626        // Move cursor to the end of the document
1627        self.clear_selection();
1628        let last_line = self.buffer.line_count().saturating_sub(1);
1629        let last_col = self.buffer.line_len(last_line);
1630        self.cursors.set_single((last_line, last_col));
1631        self.finish_navigation_operation();
1632        self.scroll_to_cursor()
1633    }
1634
1635    /// Handles Page Up key press.
1636    ///
1637    /// Scrolls the view up by one page.
1638    ///
1639    /// # Returns
1640    ///
1641    /// A `Task<Message>` that scrolls to keep the cursor visible
1642    fn handle_page_up(&mut self) -> Task<Message> {
1643        self.page_up();
1644        self.finish_navigation_operation();
1645        self.scroll_to_cursor()
1646    }
1647
1648    /// Handles Page Down key press.
1649    ///
1650    /// Scrolls the view down by one page.
1651    ///
1652    /// # Returns
1653    ///
1654    /// A `Task<Message>` that scrolls to keep the cursor visible
1655    fn handle_page_down(&mut self) -> Task<Message> {
1656        self.page_down();
1657        self.finish_navigation_operation();
1658        self.scroll_to_cursor()
1659    }
1660
1661    /// Handles direct navigation to an explicit logical position.
1662    ///
1663    /// # Arguments
1664    ///
1665    /// * `line` - Target line index (0-based)
1666    /// * `col` - Target column index (0-based)
1667    ///
1668    /// # Returns
1669    ///
1670    /// A `Task<Message>` that scrolls to keep the cursor visible
1671    fn handle_goto_position(
1672        &mut self,
1673        line: usize,
1674        col: usize,
1675    ) -> Task<Message> {
1676        // End grouping on navigation command
1677        self.end_grouping_if_active();
1678        self.set_cursor(line, col)
1679    }
1680
1681    // =========================================================================
1682    // Mouse and Selection Handlers
1683    // =========================================================================
1684
1685    /// Synchronises the active search result with a manual primary-cursor
1686    /// position or selection.
1687    fn sync_search_match_from_primary_cursor(&mut self) {
1688        if !self.search_matches_visible() || self.search_state.query.is_empty()
1689        {
1690            return;
1691        }
1692
1693        let primary = self.cursors.primary();
1694        let cursor = primary.position;
1695        let selection = primary.selection_range();
1696        if self.search_state.select_match_at_cursor(cursor, selection) {
1697            self.overlay_cache.clear();
1698        }
1699    }
1700
1701    /// Handles mouse click operations.
1702    ///
1703    /// Sets focus, ends command grouping, positions cursor, starts selection tracking.
1704    ///
1705    /// # Arguments
1706    ///
1707    /// * `point` - The click position
1708    ///
1709    /// # Returns
1710    ///
1711    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
1712    fn handle_mouse_click_msg(&mut self, point: iced::Point) -> Task<Message> {
1713        // Capture focus when clicked using the new focus method
1714        self.request_focus();
1715
1716        // Set internal canvas focus state
1717        self.has_canvas_focus = true;
1718
1719        // End grouping on mouse click
1720        self.end_grouping_if_active();
1721
1722        // Regular click collapses any multi-cursor state to a single cursor
1723        // positioned at the click location.
1724        self.cursors.remove_all_but_primary();
1725
1726        self.handle_mouse_click(point);
1727        self.reset_cursor_blink();
1728        // Clear selection on click, then set anchor for potential drag selection
1729        self.clear_selection();
1730        self.is_dragging = true;
1731        self.cursors.primary_mut().set_anchor();
1732        self.sync_search_match_from_primary_cursor();
1733
1734        // Show cursor when focused
1735        self.show_cursor = true;
1736
1737        Task::none()
1738    }
1739
1740    /// Handles mouse drag operations for selection.
1741    ///
1742    /// # Arguments
1743    ///
1744    /// * `point` - The drag position
1745    ///
1746    /// # Returns
1747    ///
1748    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
1749    fn handle_mouse_drag_msg(&mut self, point: iced::Point) -> Task<Message> {
1750        if self.is_dragging {
1751            let before_pos = self.cursors.primary_position();
1752            self.handle_mouse_drag(point);
1753            if self.cursors.primary_position() != before_pos {
1754                // Mouse move events can be very frequent. Only invalidate the
1755                // overlay cache if the drag actually changed selection/cursor.
1756                self.overlay_cache.clear();
1757            }
1758        }
1759        Task::none()
1760    }
1761
1762    /// Handles mouse release operations.
1763    ///
1764    /// # Returns
1765    ///
1766    /// A `Task<Message>` (currently Task::none() as no scrolling is needed)
1767    fn handle_mouse_release_msg(&mut self) -> Task<Message> {
1768        self.is_dragging = false;
1769        if self.vim_enabled {
1770            if self.cursors.primary().has_selection() {
1771                let anchor = self.cursors.primary().anchor.unwrap_or_default();
1772                let position = self.cursors.primary_position();
1773                let active = if position >= anchor && position.1 > 0 {
1774                    (position.0, position.1 - 1)
1775                } else {
1776                    position
1777                };
1778                let anchor = self.vim_normal_position(anchor);
1779                let active = self.vim_normal_position(active);
1780                self.vim_state.set_mode_from_mouse(VimMode::Visual);
1781                self.vim_state.begin_visual(anchor);
1782                self.vim_state.set_visual_active(active);
1783            } else {
1784                let position =
1785                    self.vim_normal_position(self.cursors.primary_position());
1786                self.cursors.set_single(position);
1787            }
1788            self.overlay_cache.clear();
1789        }
1790        self.sync_search_match_from_primary_cursor();
1791        Task::none()
1792    }
1793
1794    /// Handles a double-click: selects the word under the cursor.
1795    ///
1796    /// If the click lands outside any word (e.g. on whitespace), the
1797    /// selection is cleared and the caret is simply placed there.
1798    fn handle_double_click_msg(&mut self, point: iced::Point) -> Task<Message> {
1799        self.request_focus();
1800        self.has_canvas_focus = true;
1801        self.end_grouping_if_active();
1802        self.cursors.remove_all_but_primary();
1803        if let Some((line, col)) = self.calculate_cursor_from_point(point) {
1804            let line_content = self.buffer.line(line);
1805            let start = Self::word_start_in_line(line_content, col);
1806            let end = Self::word_end_in_line(line_content, col);
1807            let cursor = self.cursors.primary_mut();
1808            if start < end {
1809                cursor.anchor = Some((line, start));
1810                cursor.position = (line, end);
1811            } else {
1812                cursor.anchor = None;
1813                cursor.position = (line, col);
1814            }
1815        }
1816        self.is_dragging = false;
1817        self.show_cursor = true;
1818        self.reset_cursor_blink();
1819        self.overlay_cache.clear();
1820        self.sync_search_match_from_primary_cursor();
1821        Task::none()
1822    }
1823
1824    /// Handles a triple-click: selects the whole line under the cursor.
1825    fn handle_triple_click_msg(&mut self, point: iced::Point) -> Task<Message> {
1826        self.request_focus();
1827        self.has_canvas_focus = true;
1828        self.end_grouping_if_active();
1829        self.cursors.remove_all_but_primary();
1830        if let Some((line, _col)) = self.calculate_cursor_from_point(point) {
1831            let line_len = self.buffer.line_len(line);
1832            let cursor = self.cursors.primary_mut();
1833            cursor.anchor = Some((line, 0));
1834            cursor.position = (line, line_len);
1835        }
1836        self.is_dragging = false;
1837        self.show_cursor = true;
1838        self.reset_cursor_blink();
1839        self.overlay_cache.clear();
1840        self.sync_search_match_from_primary_cursor();
1841        Task::none()
1842    }
1843
1844    /// Handles a right-click before the context menu is displayed.
1845    ///
1846    /// A click inside any existing selection preserves it so Cut and Copy act
1847    /// on the selected text. A click elsewhere collapses the selection and
1848    /// moves the caret to the clicked position.
1849    fn handle_context_menu_requested_msg(
1850        &mut self,
1851        point: iced::Point,
1852    ) -> Task<Message> {
1853        self.request_focus();
1854        self.has_canvas_focus = true;
1855        self.focus_locked = false;
1856        self.show_cursor = true;
1857        self.is_dragging = false;
1858        self.end_grouping_if_active();
1859
1860        if let Some(position) = self.calculate_cursor_from_point(point) {
1861            let inside_selection = self.cursors.iter().any(|cursor| {
1862                cursor.selection_range().is_some_and(|(start, end)| {
1863                    (start..=end).contains(&position)
1864                })
1865            });
1866
1867            if !inside_selection {
1868                self.cursors.set_single(position);
1869                self.overlay_cache.clear();
1870            }
1871        }
1872
1873        self.reset_cursor_blink();
1874        Task::none()
1875    }
1876
1877    // =========================================================================
1878    // Clipboard Handlers
1879    // =========================================================================
1880
1881    /// Cuts all selected ranges to the clipboard as a single undoable edit.
1882    fn handle_cut_msg(&mut self) -> Task<Message> {
1883        if !self.cursors.iter().any(|cursor| cursor.has_selection()) {
1884            return Task::none();
1885        }
1886
1887        self.end_grouping_if_active();
1888        self.ensure_grouping_started("Cut");
1889        let clipboard_task = self.copy_selection();
1890        self.delete_selection();
1891        self.end_grouping_if_active();
1892        self.finish_edit_operation();
1893
1894        Task::batch([clipboard_task, self.scroll_to_cursor()])
1895    }
1896
1897    /// Selects the complete document.
1898    fn handle_select_all_msg(&mut self) -> Task<Message> {
1899        self.end_grouping_if_active();
1900
1901        let last_line = self.buffer.line_count().saturating_sub(1);
1902        let end = (last_line, self.buffer.line_len(last_line));
1903        self.cursors.set_single(end);
1904        self.cursors.primary_mut().anchor = Some((0, 0));
1905        self.overlay_cache.clear();
1906        self.reset_cursor_blink();
1907
1908        self.scroll_to_cursor()
1909    }
1910
1911    /// Handles paste operations.
1912    ///
1913    /// If the provided text is empty, reads from clipboard. Otherwise pastes
1914    /// the provided text at the cursor position.
1915    ///
1916    /// # Arguments
1917    ///
1918    /// * `text` - The text to paste (empty string triggers clipboard read)
1919    ///
1920    /// # Returns
1921    ///
1922    /// A `Task<Message>` that may read clipboard or scroll to cursor
1923    fn handle_paste_msg(&mut self, text: &str) -> Task<Message> {
1924        // End grouping on paste
1925        self.end_grouping_if_active();
1926
1927        // If text is empty, we need to read from clipboard
1928        if text.is_empty() {
1929            // Return a task that reads clipboard and chains to paste
1930            iced::clipboard::read().and_then(|clipboard_text| {
1931                Task::done(Message::Paste(clipboard_text))
1932            })
1933        } else {
1934            // We have the text, paste it
1935            self.paste_text(text);
1936            self.finish_edit_operation();
1937            self.scroll_to_cursor()
1938        }
1939    }
1940
1941    // =========================================================================
1942    // History (Undo/Redo) Handlers
1943    // =========================================================================
1944
1945    /// Handles undo operations.
1946    ///
1947    /// # Returns
1948    ///
1949    /// A `Task<Message>` that scrolls to cursor if undo succeeded
1950    fn handle_undo_msg(&mut self) -> Task<Message> {
1951        // End any current grouping before undoing
1952        self.end_grouping_if_active();
1953
1954        let mut cursor_pos = self.cursors.primary_position();
1955        if self.history.undo(&mut self.buffer, &mut cursor_pos) {
1956            self.cursors.primary_mut().position = cursor_pos;
1957            self.clear_selection();
1958            // An undone command (especially a composite like "Replace All") may
1959            // touch lines anywhere in the document, so reset the highlight cache
1960            // entirely rather than trusting the cursor as the change origin.
1961            self.pre_edit_line = 0;
1962            self.pre_edit_last_line = usize::MAX;
1963            self.finish_edit_operation();
1964            self.scroll_to_cursor()
1965        } else {
1966            Task::none()
1967        }
1968    }
1969
1970    /// Handles redo operations.
1971    ///
1972    /// # Returns
1973    ///
1974    /// A `Task<Message>` that scrolls to cursor if redo succeeded
1975    fn handle_redo_msg(&mut self) -> Task<Message> {
1976        let mut cursor_pos = self.cursors.primary_position();
1977        if self.history.redo(&mut self.buffer, &mut cursor_pos) {
1978            self.cursors.primary_mut().position = cursor_pos;
1979            self.clear_selection();
1980            // A redone command may touch lines anywhere; reset the highlight
1981            // cache entirely (see `handle_undo_msg`).
1982            self.pre_edit_line = 0;
1983            self.pre_edit_last_line = usize::MAX;
1984            self.finish_edit_operation();
1985            self.scroll_to_cursor()
1986        } else {
1987            Task::none()
1988        }
1989    }
1990
1991    // =========================================================================
1992    // Search and Replace Handlers
1993    // =========================================================================
1994
1995    /// Handles opening the search dialog.
1996    ///
1997    /// # Returns
1998    ///
1999    /// A `Task<Message>` that focuses and selects all in the search input
2000    fn handle_open_search_msg(&mut self) -> Task<Message> {
2001        self.goto_line_state.close();
2002        self.search_state.open_search();
2003        if !self.search_state.query.is_empty() {
2004            self.search_state.update_matches(&self.buffer);
2005            self.search_state
2006                .select_match_near_cursor(self.cursors.primary_position());
2007        }
2008        self.overlay_cache.clear();
2009
2010        // Focus the search input and select all text if any
2011        Task::batch([
2012            focus(self.search_state.search_input_id.clone()),
2013            select_all(self.search_state.search_input_id.clone()),
2014        ])
2015    }
2016
2017    /// Handles opening the search and replace dialog.
2018    ///
2019    /// # Returns
2020    ///
2021    /// A `Task<Message>` that focuses and selects all in the search input
2022    fn handle_open_search_replace_msg(&mut self) -> Task<Message> {
2023        self.goto_line_state.close();
2024        self.search_state.open_replace();
2025        if !self.search_state.query.is_empty() {
2026            self.search_state.update_matches(&self.buffer);
2027            self.search_state
2028                .select_match_near_cursor(self.cursors.primary_position());
2029        }
2030        self.overlay_cache.clear();
2031
2032        // Focus the search input and select all text if any
2033        Task::batch([
2034            focus(self.search_state.search_input_id.clone()),
2035            select_all(self.search_state.search_input_id.clone()),
2036        ])
2037    }
2038
2039    /// Handles closing the search dialog.
2040    ///
2041    /// # Returns
2042    ///
2043    /// A `Task<Message>` (currently Task::none())
2044    fn handle_close_search_msg(&mut self) -> Task<Message> {
2045        // Escape with multiple cursors and no open search: collapse to primary cursor
2046        if self.cursors.is_multi() && !self.search_state.is_open {
2047            self.cursors.remove_all_but_primary();
2048            self.overlay_cache.clear();
2049            return Task::none();
2050        }
2051        self.search_state.close();
2052        self.overlay_cache.clear();
2053        Task::none()
2054    }
2055
2056    /// Opens the go-to-line input and selects the current one-based line.
2057    fn handle_open_goto_line_msg(&mut self) -> Task<Message> {
2058        self.search_state.close();
2059        self.goto_line_state.open(self.cursors.primary_position().0);
2060        self.overlay_cache.clear();
2061
2062        Task::batch([
2063            focus(self.goto_line_state.input_id.clone()),
2064            select_all(self.goto_line_state.input_id.clone()),
2065        ])
2066    }
2067
2068    /// Closes the go-to-line input without moving the cursor.
2069    fn handle_close_goto_line_msg(&mut self) -> Task<Message> {
2070        self.goto_line_state.close();
2071        self.overlay_cache.clear();
2072        Task::none()
2073    }
2074
2075    /// Updates the one-based line number entered by the user.
2076    fn handle_goto_line_changed_msg(&mut self, query: &str) -> Task<Message> {
2077        self.goto_line_state.query = query.to_string();
2078        Task::none()
2079    }
2080
2081    /// Moves to the submitted one-based line and closes the input.
2082    fn handle_submit_goto_line_msg(&mut self) -> Task<Message> {
2083        let Some(one_based_line) = self.goto_line_state.target_line() else {
2084            return Task::none();
2085        };
2086
2087        let target_line = one_based_line
2088            .saturating_sub(1)
2089            .min(self.buffer.line_count().saturating_sub(1));
2090        while self.hidden_lines_set().contains(&target_line) {
2091            let collapsed_count = self.collapsed_folds.len();
2092            self.unfold_at(target_line);
2093            if self.collapsed_folds.len() == collapsed_count {
2094                break;
2095            }
2096        }
2097
2098        self.goto_line_state.close();
2099        self.handle_goto_position(target_line, 0)
2100    }
2101
2102    /// Handles search query text changes.
2103    ///
2104    /// # Arguments
2105    ///
2106    /// * `query` - The new search query
2107    ///
2108    /// # Returns
2109    ///
2110    /// A `Task<Message>` that scrolls to first match if any
2111    fn handle_search_query_changed_msg(
2112        &mut self,
2113        query: &str,
2114    ) -> Task<Message> {
2115        self.search_state.set_query(query.to_string(), &self.buffer);
2116        self.overlay_cache.clear();
2117
2118        // Move cursor to first match if any
2119        if let Some(match_pos) = self.search_state.current_match() {
2120            self.cursors.primary_mut().position =
2121                (match_pos.line, match_pos.col);
2122            self.clear_selection();
2123            return self.scroll_to_cursor();
2124        }
2125        Task::none()
2126    }
2127
2128    /// Handles replace query text changes.
2129    ///
2130    /// # Arguments
2131    ///
2132    /// * `replace_text` - The new replacement text
2133    ///
2134    /// # Returns
2135    ///
2136    /// A `Task<Message>` (currently Task::none())
2137    fn handle_replace_query_changed_msg(
2138        &mut self,
2139        replace_text: &str,
2140    ) -> Task<Message> {
2141        self.search_state.set_replace_with(replace_text.to_string());
2142        Task::none()
2143    }
2144
2145    /// Handles toggling case-sensitive search.
2146    ///
2147    /// # Returns
2148    ///
2149    /// A `Task<Message>` that scrolls to first match if any
2150    fn handle_toggle_case_sensitive_msg(&mut self) -> Task<Message> {
2151        self.search_state.toggle_case_sensitive(&self.buffer);
2152        self.overlay_cache.clear();
2153
2154        // Move cursor to first match if any
2155        if let Some(match_pos) = self.search_state.current_match() {
2156            self.cursors.primary_mut().position =
2157                (match_pos.line, match_pos.col);
2158            self.clear_selection();
2159            return self.scroll_to_cursor();
2160        }
2161        Task::none()
2162    }
2163
2164    /// Handles finding the next match.
2165    ///
2166    /// # Returns
2167    ///
2168    /// A `Task<Message>` that scrolls to the next match if any
2169    fn handle_find_next_msg(&mut self) -> Task<Message> {
2170        if !self.search_state.matches.is_empty() {
2171            self.search_state.next_match();
2172            if let Some(match_pos) = self.search_state.current_match() {
2173                self.cursors.primary_mut().position =
2174                    (match_pos.line, match_pos.col);
2175                self.clear_selection();
2176                self.overlay_cache.clear();
2177                return self.scroll_to_cursor();
2178            }
2179        }
2180        Task::none()
2181    }
2182
2183    /// Handles finding the previous match.
2184    ///
2185    /// # Returns
2186    ///
2187    /// A `Task<Message>` that scrolls to the previous match if any
2188    fn handle_find_previous_msg(&mut self) -> Task<Message> {
2189        if !self.search_state.matches.is_empty() {
2190            self.search_state.previous_match();
2191            if let Some(match_pos) = self.search_state.current_match() {
2192                self.cursors.primary_mut().position =
2193                    (match_pos.line, match_pos.col);
2194                self.clear_selection();
2195                self.overlay_cache.clear();
2196                return self.scroll_to_cursor();
2197            }
2198        }
2199        Task::none()
2200    }
2201
2202    /// Handles replacing the current match and moving to the next.
2203    ///
2204    /// # Returns
2205    ///
2206    /// A `Task<Message>` that scrolls to the next match if any
2207    fn handle_replace_next_msg(&mut self) -> Task<Message> {
2208        // Replace current match and move to next
2209        if let Some(match_pos) = self.search_state.current_match() {
2210            let query_len = self.search_state.query.chars().count();
2211            let replace_text = self.search_state.replace_with.clone();
2212
2213            // Create and execute replace command
2214            let pos = self.cursors.primary_position();
2215            let mut cmd = ReplaceTextCommand::new(
2216                &self.buffer,
2217                (match_pos.line, match_pos.col),
2218                query_len,
2219                replace_text,
2220                pos,
2221            );
2222            let mut cursor_pos = pos;
2223            cmd.execute(&mut self.buffer, &mut cursor_pos);
2224            self.cursors.primary_mut().position = cursor_pos;
2225            self.history.push(Box::new(cmd));
2226
2227            // The replacement starts at the matched line; invalidate highlight
2228            // from there regardless of where the cursor moved next.
2229            self.pre_edit_line = self.pre_edit_line.min(match_pos.line);
2230            self.pre_edit_last_line =
2231                self.pre_edit_last_line.max(match_pos.line);
2232
2233            self.clear_selection();
2234            self.finish_edit_operation();
2235
2236            // Move to the closest remaining match after the replacement.
2237            if !self.search_state.matches.is_empty()
2238                && let Some(next_match) = self.search_state.current_match()
2239            {
2240                self.cursors.primary_mut().position =
2241                    (next_match.line, next_match.col);
2242            }
2243
2244            return self.scroll_to_cursor();
2245        }
2246        Task::none()
2247    }
2248
2249    /// Handles replacing all matches.
2250    ///
2251    /// # Returns
2252    ///
2253    /// A `Task<Message>` that scrolls to cursor after replacement
2254    fn handle_replace_all_msg(&mut self) -> Task<Message> {
2255        // Perform a fresh search to find ALL matches (ignoring the display limit)
2256        let all_matches = super::search::find_matches(
2257            &self.buffer,
2258            &self.search_state.query,
2259            self.search_state.case_sensitive,
2260            None, // No limit for Replace All
2261        );
2262
2263        if !all_matches.is_empty() {
2264            let query_len = self.search_state.query.chars().count();
2265            let replace_text = self.search_state.replace_with.clone();
2266
2267            // Create composite command for undo
2268            let mut composite =
2269                CompositeCommand::new("Replace All".to_string());
2270
2271            // Process matches in reverse order (to preserve positions)
2272            for match_pos in all_matches.iter().rev() {
2273                let pos = self.cursors.primary_position();
2274                let cmd = ReplaceTextCommand::new(
2275                    &self.buffer,
2276                    (match_pos.line, match_pos.col),
2277                    query_len,
2278                    replace_text.clone(),
2279                    pos,
2280                );
2281                composite.add(Box::new(cmd));
2282            }
2283
2284            // Execute all replacements
2285            let mut cursor_pos = self.cursors.primary_position();
2286            composite.execute(&mut self.buffer, &mut cursor_pos);
2287            self.cursors.primary_mut().position = cursor_pos;
2288            self.history.push(Box::new(composite));
2289
2290            // Replace All touches matches anywhere in the document, so reset
2291            // the highlight cache entirely.
2292            self.pre_edit_line = 0;
2293            self.pre_edit_last_line = usize::MAX;
2294
2295            self.clear_selection();
2296            self.finish_edit_operation();
2297            self.scroll_to_cursor()
2298        } else {
2299            Task::none()
2300        }
2301    }
2302
2303    /// Handles Tab key in search dialog (cycle forward).
2304    ///
2305    /// # Returns
2306    ///
2307    /// A `Task<Message>` that focuses the next field
2308    fn handle_search_dialog_tab_msg(&mut self) -> Task<Message> {
2309        // Cycle focus forward (Search → Replace → Search)
2310        self.search_state.focus_next_field();
2311
2312        // Focus the appropriate input based on new focused_field
2313        match self.search_state.focused_field {
2314            crate::canvas_editor::search::SearchFocusedField::Search => {
2315                focus(self.search_state.search_input_id.clone())
2316            }
2317            crate::canvas_editor::search::SearchFocusedField::Replace => {
2318                focus(self.search_state.replace_input_id.clone())
2319            }
2320        }
2321    }
2322
2323    /// Handles Shift+Tab key in search dialog (cycle backward).
2324    ///
2325    /// # Returns
2326    ///
2327    /// A `Task<Message>` that focuses the previous field
2328    fn handle_search_dialog_shift_tab_msg(&mut self) -> Task<Message> {
2329        // Cycle focus backward (Replace → Search → Replace)
2330        self.search_state.focus_previous_field();
2331
2332        // Focus the appropriate input based on new focused_field
2333        match self.search_state.focused_field {
2334            crate::canvas_editor::search::SearchFocusedField::Search => {
2335                focus(self.search_state.search_input_id.clone())
2336            }
2337            crate::canvas_editor::search::SearchFocusedField::Replace => {
2338                focus(self.search_state.replace_input_id.clone())
2339            }
2340        }
2341    }
2342
2343    // =========================================================================
2344    // Focus and IME Handlers
2345    // =========================================================================
2346
2347    /// Handles canvas focus gained event.
2348    ///
2349    /// # Returns
2350    ///
2351    /// A `Task<Message>` (currently Task::none())
2352    fn handle_canvas_focus_gained_msg(&mut self) -> Task<Message> {
2353        self.has_canvas_focus = true;
2354        self.focus_locked = false; // Unlock focus when gained
2355        self.show_cursor = true;
2356        self.reset_cursor_blink();
2357        self.overlay_cache.clear();
2358        Task::none()
2359    }
2360
2361    /// Handles canvas focus lost event.
2362    ///
2363    /// # Returns
2364    ///
2365    /// A `Task<Message>` (currently Task::none())
2366    fn handle_canvas_focus_lost_msg(&mut self) -> Task<Message> {
2367        self.has_canvas_focus = false;
2368        self.focus_locked = true; // Lock focus when lost to prevent focus stealing
2369        self.show_cursor = false;
2370        self.ime_preedit = None;
2371        self.overlay_cache.clear();
2372        Task::none()
2373    }
2374
2375    /// Handles IME opened event.
2376    ///
2377    /// Clears current preedit content to accept new input.
2378    ///
2379    /// # Returns
2380    ///
2381    /// A `Task<Message>` (currently Task::none())
2382    fn handle_ime_opened_msg(&mut self) -> Task<Message> {
2383        self.ime_preedit = None;
2384        self.overlay_cache.clear();
2385        Task::none()
2386    }
2387
2388    /// Handles IME preedit event.
2389    ///
2390    /// Updates the preedit text and selection while the user is composing.
2391    ///
2392    /// # Arguments
2393    ///
2394    /// * `content` - The preedit text content
2395    /// * `selection` - The selection range within the preedit text
2396    ///
2397    /// # Returns
2398    ///
2399    /// A `Task<Message>` (currently Task::none())
2400    fn handle_ime_preedit_msg(
2401        &mut self,
2402        content: &str,
2403        selection: &Option<std::ops::Range<usize>>,
2404    ) -> Task<Message> {
2405        if content.is_empty() {
2406            self.ime_preedit = None;
2407        } else {
2408            self.ime_preedit = Some(ImePreedit {
2409                content: content.to_string(),
2410                selection: selection.clone(),
2411            });
2412        }
2413
2414        self.overlay_cache.clear();
2415        Task::none()
2416    }
2417
2418    /// Handles IME commit event.
2419    ///
2420    /// Inserts the committed text at the cursor position.
2421    ///
2422    /// # Arguments
2423    ///
2424    /// * `text` - The committed text
2425    ///
2426    /// # Returns
2427    ///
2428    /// A `Task<Message>` that scrolls to cursor after insertion
2429    fn handle_ime_commit_msg(&mut self, text: &str) -> Task<Message> {
2430        self.ime_preedit = None;
2431
2432        if text.is_empty() || !self.vim_accepts_insert_input() {
2433            self.overlay_cache.clear();
2434            return Task::none();
2435        }
2436
2437        self.ensure_grouping_started("Typing");
2438
2439        self.paste_text(text);
2440        self.finish_edit_operation();
2441        self.scroll_to_cursor()
2442    }
2443
2444    /// Handles IME closed event.
2445    ///
2446    /// Clears preedit state to return to normal input mode.
2447    ///
2448    /// # Returns
2449    ///
2450    /// A `Task<Message>` (currently Task::none())
2451    fn handle_ime_closed_msg(&mut self) -> Task<Message> {
2452        self.ime_preedit = None;
2453        self.overlay_cache.clear();
2454        Task::none()
2455    }
2456
2457    // =========================================================================
2458    // Complex Standalone Handlers
2459    // =========================================================================
2460
2461    /// Handles cursor blink tick event.
2462    ///
2463    /// Updates cursor visibility for blinking animation.
2464    ///
2465    /// # Returns
2466    ///
2467    /// A `Task<Message>` (currently Task::none())
2468    fn handle_tick_msg(&mut self) -> Task<Message> {
2469        // Handle cursor blinking only if editor has focus
2470        if self.has_focus()
2471            && self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL
2472        {
2473            self.cursor_visible = !self.cursor_visible;
2474            self.last_blink = super::Instant::now();
2475            self.overlay_cache.clear();
2476        }
2477
2478        // Hide cursor if editor doesn't have focus
2479        if !self.has_focus() {
2480            self.show_cursor = false;
2481        }
2482
2483        Task::none()
2484    }
2485
2486    /// Handles viewport scrolled event.
2487    ///
2488    /// Manages the virtual scrolling cache window to optimize rendering
2489    /// for large files. Only clears the cache when scrolling crosses the
2490    /// cached window boundary or when viewport dimensions change.
2491    ///
2492    /// # Arguments
2493    ///
2494    /// * `viewport` - The viewport information after scrolling
2495    ///
2496    /// # Returns
2497    ///
2498    /// A `Task<Message>` (currently Task::none())
2499    fn handle_scrolled_msg(
2500        &mut self,
2501        viewport: iced::widget::scrollable::Viewport,
2502    ) -> Task<Message> {
2503        // Virtual-scrolling cache window:
2504        // Instead of clearing the canvas cache for every small scroll,
2505        // we maintain a larger "render window" of visual lines around
2506        // the visible range. We only clear the cache and re-window
2507        // when the scroll crosses the window boundary or the viewport
2508        // size changes significantly. This prevents frequent re-highlighting
2509        // and layout recomputation for very large files while ensuring
2510        // the first scroll renders correctly without requiring a click.
2511        let new_scroll = viewport.absolute_offset().y;
2512        let new_height = viewport.bounds().height;
2513        let new_width = viewport.bounds().width;
2514        let scroll_changed = (self.viewport_scroll - new_scroll).abs() > 0.1;
2515        let visible_lines_count =
2516            (new_height / self.line_height).ceil() as usize + 2;
2517        let first_visible_line =
2518            (new_scroll / self.line_height).floor() as usize;
2519        let last_visible_line = first_visible_line + visible_lines_count;
2520        let margin = visible_lines_count
2521            * crate::canvas_editor::CACHE_WINDOW_MARGIN_MULTIPLIER;
2522        let window_start = first_visible_line.saturating_sub(margin);
2523        let window_end = last_visible_line + margin;
2524        // Decide whether we need to re-window the cache.
2525        // Special-case top-of-file: when window_start == 0, allow small forward scrolls
2526        // without forcing a rewindow, to avoid thrashing when the visible range is near 0.
2527        let need_rewindow =
2528            if self.cache_window_end_line > self.cache_window_start_line {
2529                let lower_boundary_trigger = self.cache_window_start_line > 0
2530                    && first_visible_line
2531                        < self
2532                            .cache_window_start_line
2533                            .saturating_add(visible_lines_count / 2);
2534                let upper_boundary_trigger = last_visible_line
2535                    > self
2536                        .cache_window_end_line
2537                        .saturating_sub(visible_lines_count / 2);
2538                lower_boundary_trigger || upper_boundary_trigger
2539            } else {
2540                true
2541            };
2542        // Clear cache when viewport dimensions change significantly
2543        // to ensure proper redraw (e.g., window resize)
2544        if (self.viewport_height - new_height).abs() > 1.0
2545            || (self.viewport_width - new_width).abs() > 1.0
2546            || (scroll_changed && need_rewindow)
2547        {
2548            self.cache_window_start_line = window_start;
2549            self.cache_window_end_line = window_end;
2550            self.last_first_visible_line = first_visible_line;
2551            self.content_cache.clear();
2552            self.overlay_cache.clear();
2553        }
2554        self.viewport_scroll = new_scroll;
2555        self.viewport_height = new_height;
2556        self.viewport_width = new_width;
2557        Task::none()
2558    }
2559
2560    /// Handles horizontal scrollbar scrolled event (only active when wrap is disabled).
2561    ///
2562    /// Updates `horizontal_scroll_offset` and clears render caches when the offset
2563    /// changes by more than 0.1 pixels to avoid unnecessary redraws.
2564    ///
2565    /// # Arguments
2566    ///
2567    /// * `viewport` - The viewport information after scrolling
2568    ///
2569    /// # Returns
2570    ///
2571    /// A `Task<Message>` (currently `Task::none()`)
2572    fn handle_horizontal_scrolled_msg(
2573        &mut self,
2574        viewport: iced::widget::scrollable::Viewport,
2575    ) -> Task<Message> {
2576        let new_x = viewport.absolute_offset().x;
2577        if (self.horizontal_scroll_offset - new_x).abs() > 0.1 {
2578            self.horizontal_scroll_offset = new_x;
2579            self.content_cache.clear();
2580            self.overlay_cache.clear();
2581        }
2582        Task::none()
2583    }
2584
2585    // =========================================================================
2586    // Multi-cursor operations
2587    // =========================================================================
2588
2589    /// Handles Alt+Click: adds a new cursor at the clicked position without
2590    /// disturbing existing cursors.
2591    ///
2592    /// # Arguments
2593    ///
2594    /// * `point` - Canvas-local position of the click
2595    ///
2596    /// # Returns
2597    ///
2598    /// `Task::none()` — no async work needed
2599    fn handle_alt_click_msg(&mut self, point: iced::Point) -> Task<Message> {
2600        if self.vim_enabled {
2601            return Task::none();
2602        }
2603        if let Some(pos) = self.calculate_cursor_from_point(point) {
2604            self.cursors.add_cursor(pos);
2605            self.overlay_cache.clear();
2606            self.reset_cursor_blink();
2607        }
2608        Task::none()
2609    }
2610
2611    /// Handles Ctrl+Alt+Up: adds a cursor on the line above the primary cursor,
2612    /// at the same column (clamped to line length).
2613    ///
2614    /// # Returns
2615    ///
2616    /// `Task::none()`
2617    fn handle_add_cursor_above_msg(&mut self) -> Task<Message> {
2618        if self.vim_enabled {
2619            return Task::none();
2620        }
2621        let (line, col) = self.cursors.primary_position();
2622        if line == 0 {
2623            return Task::none();
2624        }
2625        let new_line = line - 1;
2626        let new_col = col.min(self.buffer.line_len(new_line));
2627        self.cursors.add_cursor((new_line, new_col));
2628        self.overlay_cache.clear();
2629        self.reset_cursor_blink();
2630        Task::none()
2631    }
2632
2633    /// Handles Ctrl+Alt+Down: adds a cursor on the line below the primary cursor,
2634    /// at the same column (clamped to line length).
2635    ///
2636    /// # Returns
2637    ///
2638    /// `Task::none()`
2639    fn handle_add_cursor_below_msg(&mut self) -> Task<Message> {
2640        if self.vim_enabled {
2641            return Task::none();
2642        }
2643        let (line, col) = self.cursors.primary_position();
2644        let last_line = self.buffer.line_count().saturating_sub(1);
2645        if line >= last_line {
2646            return Task::none();
2647        }
2648        let new_line = line + 1;
2649        let new_col = col.min(self.buffer.line_len(new_line));
2650        self.cursors.add_cursor((new_line, new_col));
2651        self.overlay_cache.clear();
2652        self.reset_cursor_blink();
2653        Task::none()
2654    }
2655
2656    /// Handles Ctrl+D: selects the next occurrence of the text currently selected
2657    /// by the primary cursor, or the word under the primary cursor if there is no
2658    /// selection. A new cursor with that selection is added.
2659    ///
2660    /// # Returns
2661    ///
2662    /// `Task::none()`
2663    fn handle_select_next_occurrence_msg(&mut self) -> Task<Message> {
2664        if self.vim_enabled {
2665            return Task::none();
2666        }
2667        // Determine the search text: selected text on primary cursor, or word under cursor
2668        let search_text = if let Some(text) = self.get_selected_text() {
2669            text
2670        } else {
2671            // Select word under primary cursor first
2672            let (line, col) = self.cursors.primary_position();
2673            let line_str = self.buffer.line(line).to_string();
2674            let word_start = Self::word_start_in_line(&line_str, col);
2675            let word_end = Self::word_end_in_line(&line_str, col);
2676            if word_start == word_end {
2677                return Task::none();
2678            }
2679            // Apply selection to primary cursor and stop: the next Ctrl+D call
2680            // will find the next occurrence (selection will be non-empty then).
2681            self.cursors.primary_mut().anchor = Some((line, word_start));
2682            self.cursors.primary_mut().position = (line, word_end);
2683            self.overlay_cache.clear();
2684            return Task::none();
2685        };
2686
2687        if search_text.is_empty() {
2688            return Task::none();
2689        }
2690
2691        // Find the search start position: just after the last cursor's selection end
2692        let search_start = self
2693            .cursors
2694            .as_slice()
2695            .last()
2696            .map(|last| {
2697                last.selection_range()
2698                    .map(|(_, end)| end)
2699                    .unwrap_or(last.position)
2700            })
2701            .unwrap_or((0, 0));
2702
2703        // Search forward from search_start for the next occurrence
2704        let (start_line, start_col) = search_start;
2705        let line_count = self.buffer.line_count();
2706        let search_char_len = search_text.chars().count();
2707
2708        for line_offset in 0..=line_count {
2709            let line_idx = (start_line + line_offset) % line_count;
2710            let line_str = self.buffer.line(line_idx);
2711
2712            // On the first iteration, start after start_col; on wrap-around, start from 0
2713            let search_col = if line_offset == 0 { start_col } else { 0 };
2714
2715            // Build substring from search_col onward (char-indexed)
2716            let prefix_bytes = char_to_byte_index(line_str, search_col);
2717            let haystack = &line_str[prefix_bytes..];
2718
2719            // The search_text is also char-based; find it as a substring
2720            if let Some(byte_offset) = haystack.find(search_text.as_str()) {
2721                // Convert byte_offset back to char offset
2722                let char_start =
2723                    search_col + haystack[..byte_offset].chars().count();
2724                let char_end = char_start + search_char_len;
2725
2726                // Build cursor with selection for the found occurrence
2727                let found_cursor = cursor_set::Cursor {
2728                    position: (line_idx, char_end),
2729                    anchor: Some((line_idx, char_start)),
2730                };
2731                self.cursors.add_cursor_with_selection(found_cursor);
2732                self.overlay_cache.clear();
2733                self.reset_cursor_blink();
2734                return self.scroll_to_cursor();
2735            }
2736        }
2737
2738        Task::none()
2739    }
2740
2741    // =========================================================================
2742    // Main Update Method
2743    // =========================================================================
2744
2745    /// Updates the editor state based on messages and returns scroll commands.
2746    ///
2747    /// # Arguments
2748    ///
2749    /// * `message` - The message to process for updating the editor state
2750    ///
2751    /// # Returns
2752    /// A `Task<Message>` for any asynchronous operations, such as scrolling to keep the cursor visible after state updates
2753    pub fn update(&mut self, message: &Message) -> Task<Message> {
2754        // Capture the topmost active line before any edit mutates the buffer,
2755        // so `finish_edit_operation` can truncate the highlight cache precisely.
2756        self.pre_edit_line = self.min_active_line();
2757        self.pre_edit_last_line = self.max_active_line();
2758        self.capture_lsp_edit_snapshot(message);
2759        match message {
2760            // Text input operations
2761            Message::CharacterInput(ch) if self.vim_accepts_insert_input() => {
2762                self.handle_character_input_msg(*ch)
2763            }
2764            Message::CharacterInput(_) => Task::none(),
2765            Message::VimKey(ch) => self.handle_vim_key_msg(*ch),
2766            Message::ToggleVimMode => {
2767                self.set_vim_enabled(!self.vim_enabled);
2768                Task::none()
2769            }
2770            Message::Tab if self.vim_accepts_insert_input() => {
2771                self.handle_tab()
2772            }
2773            Message::Enter if self.vim_accepts_insert_input() => {
2774                self.handle_enter()
2775            }
2776            Message::Tab | Message::Enter => Task::none(),
2777
2778            // Deletion operations
2779            Message::Backspace if self.vim_accepts_insert_input() => {
2780                self.handle_backspace()
2781            }
2782            Message::Delete if self.vim_accepts_insert_input() => {
2783                self.handle_delete()
2784            }
2785            Message::Backspace | Message::Delete => Task::none(),
2786            Message::DeleteSelection => self.handle_delete_selection(),
2787
2788            // Navigation operations
2789            Message::ArrowKey(direction, shift) => {
2790                self.handle_arrow_key(*direction, *shift)
2791            }
2792            Message::Home(shift) => self.handle_home(*shift),
2793            Message::End(shift) => self.handle_end(*shift),
2794            Message::CtrlHome => self.handle_ctrl_home(),
2795            Message::CtrlEnd => self.handle_ctrl_end(),
2796            Message::GotoPosition(line, col) => {
2797                self.handle_goto_position(*line, *col)
2798            }
2799            Message::OpenGotoLine => self.handle_open_goto_line_msg(),
2800            Message::CloseGotoLine => self.handle_close_goto_line_msg(),
2801            Message::GotoLineChanged(query) => {
2802                self.handle_goto_line_changed_msg(query)
2803            }
2804            Message::SubmitGotoLine => self.handle_submit_goto_line_msg(),
2805            Message::PageUp => self.handle_page_up(),
2806            Message::PageDown => self.handle_page_down(),
2807
2808            // Mouse and selection operations
2809            Message::MouseClick(point) => self.handle_mouse_click_msg(*point),
2810            Message::MouseDrag(point) => self.handle_mouse_drag_msg(*point),
2811            Message::MouseHover(point) => self.handle_mouse_drag_msg(*point),
2812            Message::MouseRelease => self.handle_mouse_release_msg(),
2813            Message::DoubleClick(point) => self.handle_double_click_msg(*point),
2814            Message::TripleClick(point) => self.handle_triple_click_msg(*point),
2815            Message::ContextMenuRequested(point) => {
2816                self.handle_context_menu_requested_msg(*point)
2817            }
2818            Message::WriteRequested
2819            | Message::CustomContextMenuAction(_)
2820            | Message::RevealInFileManager => Task::none(),
2821
2822            // Clipboard operations
2823            Message::Cut => self.handle_cut_msg(),
2824            Message::Copy => self.copy_selection(),
2825            Message::Paste(text) => self.handle_paste_msg(text),
2826            Message::SelectAll => self.handle_select_all_msg(),
2827
2828            // History operations
2829            Message::Undo => self.handle_undo_msg(),
2830            Message::Redo => self.handle_redo_msg(),
2831
2832            // Search and replace operations
2833            Message::OpenSearch => self.handle_open_search_msg(),
2834            Message::OpenSearchReplace => self.handle_open_search_replace_msg(),
2835            Message::CloseSearch => self.handle_close_search_msg(),
2836            Message::SearchQueryChanged(query) => {
2837                self.handle_search_query_changed_msg(query)
2838            }
2839            Message::ReplaceQueryChanged(text) => {
2840                self.handle_replace_query_changed_msg(text)
2841            }
2842            Message::ToggleCaseSensitive => {
2843                self.handle_toggle_case_sensitive_msg()
2844            }
2845            Message::FindNext => self.handle_find_next_msg(),
2846            Message::FindPrevious => self.handle_find_previous_msg(),
2847            Message::ReplaceNext => self.handle_replace_next_msg(),
2848            Message::ReplaceAll => self.handle_replace_all_msg(),
2849            Message::SearchDialogTab => self.handle_search_dialog_tab_msg(),
2850            Message::SearchDialogShiftTab => {
2851                self.handle_search_dialog_shift_tab_msg()
2852            }
2853            Message::FocusNavigationTab => self.handle_focus_navigation_tab(),
2854            Message::FocusNavigationShiftTab => {
2855                self.handle_focus_navigation_shift_tab()
2856            }
2857
2858            // Focus and IME operations
2859            Message::CanvasFocusGained => self.handle_canvas_focus_gained_msg(),
2860            Message::CanvasFocusLost => self.handle_canvas_focus_lost_msg(),
2861            Message::ImeOpened if self.vim_accepts_insert_input() => {
2862                self.handle_ime_opened_msg()
2863            }
2864            Message::ImeOpened => Task::none(),
2865            Message::ImePreedit(content, selection) => {
2866                if self.vim_accepts_insert_input() {
2867                    self.handle_ime_preedit_msg(content, selection)
2868                } else {
2869                    Task::none()
2870                }
2871            }
2872            Message::ImeCommit(text) => self.handle_ime_commit_msg(text),
2873            Message::ImeClosed => self.handle_ime_closed_msg(),
2874
2875            // UI update operations
2876            Message::Tick => self.handle_tick_msg(),
2877            Message::Scrolled(viewport) => self.handle_scrolled_msg(*viewport),
2878            Message::HorizontalScrolled(viewport) => {
2879                self.handle_horizontal_scrolled_msg(*viewport)
2880            }
2881
2882            // Handle the "Jump to Definition" action triggered by Ctrl+Click.
2883            // Currently, this returns `Task::none()` as the actual navigation logic
2884            // is delegated to the `LspClient` implementation or handled elsewhere.
2885            Message::JumpClick(_point) => Task::none(),
2886
2887            // Multi-cursor operations
2888            Message::AltClick(point) => self.handle_alt_click_msg(*point),
2889            Message::AddCursorAbove => self.handle_add_cursor_above_msg(),
2890            Message::AddCursorBelow => self.handle_add_cursor_below_msg(),
2891            Message::SelectNextOccurrence => {
2892                self.handle_select_next_occurrence_msg()
2893            }
2894            Message::ToggleFold(header_line) => {
2895                self.toggle_fold(*header_line);
2896                Task::none()
2897            }
2898            Message::ToggleFoldAtCursor => {
2899                self.toggle_fold_at(self.cursors.primary_position().0);
2900                Task::none()
2901            }
2902            Message::FoldAll => {
2903                self.fold_all();
2904                Task::none()
2905            }
2906            Message::UnfoldAll => {
2907                self.unfold_all();
2908                Task::none()
2909            }
2910
2911            // Line manipulation operations
2912            Message::MoveLineUp => self.move_lines(false),
2913            Message::MoveLineDown => self.move_lines(true),
2914            Message::DuplicateLineUp => self.duplicate_lines(false),
2915            Message::DuplicateLineDown => self.duplicate_lines(true),
2916            Message::ToggleComment => self.toggle_comment(),
2917        }
2918    }
2919}
2920
2921#[cfg(test)]
2922mod tests {
2923    use super::*;
2924    use crate::canvas_editor::{ArrowDirection, VimMode};
2925    use std::cell::RefCell;
2926    use std::rc::Rc;
2927
2928    fn vim_keys(editor: &mut CodeEditor, keys: &str) {
2929        for key in keys.chars() {
2930            let _ = editor.update(&Message::VimKey(key));
2931        }
2932    }
2933
2934    fn focus_editor(editor: &mut CodeEditor) {
2935        editor.request_focus();
2936        editor.has_canvas_focus = true;
2937        editor.focus_locked = false;
2938    }
2939
2940    fn assert_vim_delete(
2941        content: &str,
2942        cursor: (usize, usize),
2943        keys: &str,
2944        expected: &str,
2945        register: &str,
2946    ) {
2947        let mut editor = CodeEditor::new(content, "txt").with_vim_enabled(true);
2948        editor.cursors.set_single(cursor);
2949        vim_keys(&mut editor, keys);
2950        assert_eq!(editor.content(), expected, "keys: {keys}");
2951        assert_eq!(editor.vim_state.register.text, register, "keys: {keys}");
2952    }
2953
2954    #[derive(Default)]
2955    struct VimTestLspClient {
2956        changes: Rc<RefCell<Vec<Vec<lsp::LspTextChange>>>>,
2957    }
2958
2959    impl lsp::LspClient for VimTestLspClient {
2960        fn did_change(
2961            &mut self,
2962            _document: &lsp::LspDocument,
2963            changes: &[lsp::LspTextChange],
2964        ) {
2965            self.changes.borrow_mut().push(changes.to_vec());
2966        }
2967    }
2968
2969    #[test]
2970    fn test_vim_navigation_normal_key_does_not_insert() {
2971        let mut editor = CodeEditor::new("abc", "txt").with_vim_enabled(true);
2972
2973        vim_keys(&mut editor, "l");
2974
2975        assert_eq!(editor.content(), "abc");
2976        assert_eq!(editor.cursors.primary_position(), (0, 1));
2977
2978        let mut standard = CodeEditor::new("abc", "txt");
2979        focus_editor(&mut standard);
2980        let _ = standard.update(&Message::CharacterInput('l'));
2981        assert_eq!(standard.content(), "labc");
2982    }
2983
2984    #[test]
2985    fn test_vim_navigation_insert_and_escape_round_trip() {
2986        let mut editor = CodeEditor::new("abc", "txt").with_vim_enabled(true);
2987        focus_editor(&mut editor);
2988
2989        vim_keys(&mut editor, "i");
2990        assert_eq!(editor.vim_mode(), Some(VimMode::Insert));
2991        let _ = editor.update(&Message::CharacterInput('X'));
2992        assert_eq!(editor.content(), "Xabc");
2993
2994        let _ = editor.update(&Message::VimKey('\u{1b}'));
2995        assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
2996        vim_keys(&mut editor, "l");
2997        assert_eq!(editor.content(), "Xabc");
2998        assert_eq!(editor.cursors.primary_position(), (0, 1));
2999    }
3000
3001    #[test]
3002    fn test_vim_navigation_counted_word_and_line_motions() {
3003        let mut editor =
3004            CodeEditor::new("one two\nthree four\nfive six", "txt")
3005                .with_vim_enabled(true);
3006
3007        vim_keys(&mut editor, "2w");
3008        assert_eq!(editor.cursors.primary_position(), (1, 0));
3009        vim_keys(&mut editor, "e");
3010        assert_eq!(editor.cursors.primary_position(), (1, 4));
3011        vim_keys(&mut editor, "b");
3012        assert_eq!(editor.cursors.primary_position(), (1, 0));
3013        vim_keys(&mut editor, "G");
3014        assert_eq!(editor.cursors.primary_position(), (2, 0));
3015        vim_keys(&mut editor, "gg");
3016        assert_eq!(editor.cursors.primary_position(), (0, 0));
3017        vim_keys(&mut editor, "2j");
3018        assert_eq!(editor.cursors.primary_position(), (2, 0));
3019        vim_keys(&mut editor, "k$");
3020        assert_eq!(editor.cursors.primary_position(), (1, 9));
3021        vim_keys(&mut editor, "0");
3022        assert_eq!(editor.cursors.primary_position(), (1, 0));
3023
3024        let mut folded = CodeEditor::new(
3025            "fn main() {\n    let x = 1;\n    if x > 0 {\n        print();\n    }\n}",
3026            "rs",
3027        )
3028        .with_vim_enabled(true);
3029        folded.toggle_fold(0);
3030        vim_keys(&mut folded, "j");
3031        assert_eq!(folded.cursors.primary_position(), (5, 0));
3032    }
3033
3034    #[test]
3035    fn test_vim_navigation_visual_and_visual_line_ranges() {
3036        let mut editor = CodeEditor::new("abcd\nefgh\nijkl\nmnop", "txt")
3037            .with_vim_enabled(true);
3038        editor.cursors.set_single((0, 1));
3039
3040        vim_keys(&mut editor, "vl");
3041        assert_eq!(editor.vim_mode(), Some(VimMode::Visual));
3042        assert_eq!(
3043            editor.cursors.primary().selection_range(),
3044            Some(((0, 1), (0, 3)))
3045        );
3046
3047        let _ = editor.update(&Message::VimKey('\u{1b}'));
3048        assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
3049        assert!(editor.cursors.primary().anchor.is_none());
3050
3051        editor.cursors.set_single((1, 2));
3052        vim_keys(&mut editor, "Vj");
3053        assert_eq!(editor.vim_mode(), Some(VimMode::VisualLine));
3054        assert_eq!(
3055            editor.cursors.primary().selection_range(),
3056            Some(((1, 0), (3, 0)))
3057        );
3058    }
3059
3060    #[test]
3061    fn test_vim_navigation_unicode_and_empty_line_bounds() {
3062        let mut editor =
3063            CodeEditor::new("你🙂好\n\nz", "txt").with_vim_enabled(true);
3064
3065        vim_keys(&mut editor, "lll");
3066        assert_eq!(editor.cursors.primary_position(), (0, 2));
3067        vim_keys(&mut editor, "j");
3068        assert_eq!(editor.cursors.primary_position(), (1, 0));
3069        vim_keys(&mut editor, "j");
3070        assert_eq!(editor.cursors.primary_position(), (2, 0));
3071        vim_keys(&mut editor, "k");
3072        assert_eq!(editor.cursors.primary_position(), (1, 0));
3073        vim_keys(&mut editor, "k$");
3074        assert_eq!(editor.cursors.primary_position(), (0, 2));
3075        vim_keys(&mut editor, "0");
3076        assert_eq!(editor.cursors.primary_position(), (0, 0));
3077    }
3078
3079    #[test]
3080    fn test_vim_navigation_ime_only_commits_in_insert() {
3081        let mut editor = CodeEditor::new("abc", "txt").with_vim_enabled(true);
3082
3083        let _ = editor.update(&Message::ImeCommit("中".to_owned()));
3084        assert_eq!(editor.content(), "abc");
3085
3086        vim_keys(&mut editor, "i");
3087        let _ = editor.update(&Message::ImeCommit("中".to_owned()));
3088        assert_eq!(editor.content(), "中abc");
3089
3090        let mut standard = CodeEditor::new("abc", "txt");
3091        let _ = standard.update(&Message::ImeCommit("中".to_owned()));
3092        assert_eq!(standard.content(), "中abc");
3093    }
3094
3095    #[test]
3096    fn test_vim_navigation_collapses_and_blocks_extra_cursors() {
3097        let mut editor = CodeEditor::new("same\nsame\nsame", "txt");
3098        editor.cursors.add_cursor((1, 0));
3099        assert_eq!(editor.cursors.len(), 2);
3100
3101        editor.set_vim_enabled(true);
3102        assert_eq!(editor.cursors.len(), 1);
3103
3104        let _ = editor.update(&Message::AddCursorBelow);
3105        let _ = editor.update(&Message::SelectNextOccurrence);
3106        let _ = editor.update(&Message::SelectNextOccurrence);
3107        let _ = editor.update(&Message::AltClick(iced::Point::new(
3108            editor.gutter_width() + 5.0,
3109            editor.line_height,
3110        )));
3111        assert_eq!(editor.cursors.len(), 1);
3112    }
3113
3114    #[test]
3115    fn test_vim_editing_x_and_count() {
3116        let mut editor =
3117            CodeEditor::new("abcdef", "txt").with_vim_enabled(true);
3118
3119        vim_keys(&mut editor, "x");
3120        assert_eq!(editor.content(), "bcdef");
3121        assert_eq!(editor.vim_state.register.text, "a");
3122        assert_eq!(
3123            editor.vim_state.register.kind,
3124            super::super::vim::VimRegisterKind::Characterwise
3125        );
3126
3127        vim_keys(&mut editor, "2x");
3128        assert_eq!(editor.content(), "def");
3129        assert_eq!(editor.vim_state.register.text, "bc");
3130        assert_eq!(editor.cursors.primary_position(), (0, 0));
3131    }
3132
3133    #[test]
3134    fn test_vim_editing_delete_change_yank_motions() {
3135        let mut deleted =
3136            CodeEditor::new("one two three", "txt").with_vim_enabled(true);
3137        vim_keys(&mut deleted, "dw");
3138        assert_eq!(deleted.content(), "two three");
3139        assert_eq!(deleted.vim_state.register.text, "one ");
3140
3141        let mut yanked =
3142            CodeEditor::new("one two", "txt").with_vim_enabled(true);
3143        vim_keys(&mut yanked, "yw");
3144        assert_eq!(yanked.content(), "one two");
3145        assert_eq!(yanked.vim_state.register.text, "one ");
3146
3147        let mut changed =
3148            CodeEditor::new("one two", "txt").with_vim_enabled(true);
3149        focus_editor(&mut changed);
3150        vim_keys(&mut changed, "ce");
3151        assert_eq!(changed.content(), " two");
3152        assert_eq!(changed.vim_state.register.text, "one");
3153        assert_eq!(changed.vim_mode(), Some(VimMode::Insert));
3154        let _ = changed.update(&Message::CharacterInput('X'));
3155        vim_keys(&mut changed, "\u{1b}");
3156        assert_eq!(changed.content(), "X two");
3157
3158        assert_vim_delete("abc", (0, 2), "dh", "ac", "b");
3159        assert_vim_delete("abc", (0, 1), "dl", "ac", "b");
3160        assert_vim_delete("abc def", (0, 6), "db", "abc f", "de");
3161        assert_vim_delete("abcdef", (0, 3), "d0", "def", "abc");
3162        assert_vim_delete("  abc", (0, 4), "d^", "  c", "ab");
3163        assert_vim_delete("abcde", (0, 2), "d$", "ab", "cde");
3164        assert_vim_delete(
3165            "one\ntwo\nthree\nfour",
3166            (2, 0),
3167            "dgg",
3168            "four",
3169            "one\ntwo\nthree\n",
3170        );
3171        assert_vim_delete(
3172            "one\ntwo\nthree\nfour",
3173            (1, 0),
3174            "dG",
3175            "one",
3176            "two\nthree\nfour\n",
3177        );
3178    }
3179
3180    #[test]
3181    fn test_vim_editing_doubled_line_operators() {
3182        let mut deleted =
3183            CodeEditor::new("one\ntwo\nthree", "txt").with_vim_enabled(true);
3184        vim_keys(&mut deleted, "dd");
3185        assert_eq!(deleted.content(), "two\nthree");
3186        assert_eq!(deleted.vim_state.register.text, "one\n");
3187        assert_eq!(
3188            deleted.vim_state.register.kind,
3189            super::super::vim::VimRegisterKind::Linewise
3190        );
3191
3192        let mut yanked =
3193            CodeEditor::new("one\ntwo\nthree", "txt").with_vim_enabled(true);
3194        yanked.cursors.set_single((1, 1));
3195        vim_keys(&mut yanked, "yy");
3196        assert_eq!(yanked.content(), "one\ntwo\nthree");
3197        assert_eq!(yanked.vim_state.register.text, "two\n");
3198        assert_eq!(
3199            yanked.vim_state.register.kind,
3200            super::super::vim::VimRegisterKind::Linewise
3201        );
3202
3203        let mut changed =
3204            CodeEditor::new("one\ntwo\nthree", "txt").with_vim_enabled(true);
3205        changed.cursors.set_single((1, 1));
3206        vim_keys(&mut changed, "cc");
3207        assert_eq!(changed.content(), "one\nthree");
3208        assert_eq!(changed.vim_state.register.text, "two\n");
3209        assert_eq!(changed.vim_mode(), Some(VimMode::Insert));
3210
3211        assert_vim_delete(
3212            "one\ntwo\nthree\nfour",
3213            (1, 0),
3214            "dk",
3215            "three\nfour",
3216            "one\ntwo\n",
3217        );
3218        assert_vim_delete(
3219            "one\ntwo\nthree\nfour",
3220            (1, 0),
3221            "dj",
3222            "one\nfour",
3223            "two\nthree\n",
3224        );
3225        assert_vim_delete(
3226            "one\ntwo\nthree",
3227            (0, 0),
3228            "2dd",
3229            "three",
3230            "one\ntwo\n",
3231        );
3232    }
3233
3234    #[test]
3235    fn test_vim_editing_visual_operators() {
3236        let mut deleted =
3237            CodeEditor::new("abcd\nefgh", "txt").with_vim_enabled(true);
3238        deleted.cursors.set_single((0, 1));
3239        vim_keys(&mut deleted, "vld");
3240        assert_eq!(deleted.content(), "ad\nefgh");
3241        assert_eq!(deleted.vim_state.register.text, "bc");
3242        assert_eq!(
3243            deleted.vim_state.register.kind,
3244            super::super::vim::VimRegisterKind::Characterwise
3245        );
3246        assert_eq!(deleted.vim_mode(), Some(VimMode::Normal));
3247
3248        let mut yanked =
3249            CodeEditor::new("one\ntwo\nthree", "txt").with_vim_enabled(true);
3250        yanked.cursors.set_single((1, 1));
3251        vim_keys(&mut yanked, "Vjy");
3252        assert_eq!(yanked.content(), "one\ntwo\nthree");
3253        assert_eq!(yanked.vim_state.register.text, "two\nthree\n");
3254        assert_eq!(
3255            yanked.vim_state.register.kind,
3256            super::super::vim::VimRegisterKind::Linewise
3257        );
3258        assert_eq!(yanked.vim_mode(), Some(VimMode::Normal));
3259
3260        let mut changed = CodeEditor::new("abcd", "txt").with_vim_enabled(true);
3261        focus_editor(&mut changed);
3262        changed.cursors.set_single((0, 1));
3263        vim_keys(&mut changed, "vlc");
3264        let _ = changed.update(&Message::CharacterInput('X'));
3265        vim_keys(&mut changed, "\u{1b}");
3266        assert_eq!(changed.content(), "aXd");
3267        assert_eq!(changed.vim_state.register.text, "bc");
3268        assert_eq!(changed.history.undo_count(), 1);
3269    }
3270
3271    #[test]
3272    fn test_vim_editing_characterwise_and_linewise_paste() {
3273        let mut characterwise =
3274            CodeEditor::new("abc", "txt").with_vim_enabled(true);
3275        vim_keys(&mut characterwise, "yl2lp");
3276        assert_eq!(characterwise.content(), "abca");
3277        assert_eq!(characterwise.cursors.primary_position(), (0, 3));
3278
3279        let mut characterwise_before =
3280            CodeEditor::new("abc", "txt").with_vim_enabled(true);
3281        vim_keys(&mut characterwise_before, "yl2lP");
3282        assert_eq!(characterwise_before.content(), "abac");
3283        assert_eq!(characterwise_before.cursors.primary_position(), (0, 2));
3284
3285        let mut linewise =
3286            CodeEditor::new("one\ntwo", "txt").with_vim_enabled(true);
3287        vim_keys(&mut linewise, "yyp");
3288        assert_eq!(linewise.content(), "one\none\ntwo");
3289        assert_eq!(linewise.cursors.primary_position(), (1, 0));
3290
3291        let mut linewise_before =
3292            CodeEditor::new("one\ntwo", "txt").with_vim_enabled(true);
3293        linewise_before.cursors.set_single((1, 0));
3294        vim_keys(&mut linewise_before, "yyP");
3295        assert_eq!(linewise_before.content(), "one\ntwo\ntwo");
3296        assert_eq!(linewise_before.cursors.primary_position(), (1, 0));
3297    }
3298
3299    #[test]
3300    fn test_vim_editing_operator_counts_multiply() {
3301        let mut editor =
3302            CodeEditor::new("one two three four five six seven", "txt")
3303                .with_vim_enabled(true);
3304
3305        vim_keys(&mut editor, "2d3w");
3306
3307        assert_eq!(editor.content(), "seven");
3308        assert_eq!(
3309            editor.vim_state.register.text,
3310            "one two three four five six "
3311        );
3312    }
3313
3314    #[test]
3315    fn test_vim_editing_undo_redo_is_one_command() {
3316        let original = "one two three";
3317        let mut editor =
3318            CodeEditor::new(original, "txt").with_vim_enabled(true);
3319        focus_editor(&mut editor);
3320
3321        vim_keys(&mut editor, "cw");
3322        let _ = editor.update(&Message::CharacterInput('X'));
3323        let _ = editor.update(&Message::CharacterInput('Y'));
3324        vim_keys(&mut editor, "\u{1b}");
3325        assert_eq!(editor.content(), "XYtwo three");
3326        assert_eq!(editor.history.undo_count(), 1);
3327
3328        vim_keys(&mut editor, "u");
3329        assert_eq!(editor.content(), original);
3330        assert_eq!(editor.history.redo_count(), 1);
3331
3332        vim_keys(&mut editor, "\u{12}");
3333        assert_eq!(editor.content(), "XYtwo three");
3334        assert_eq!(editor.history.undo_count(), 1);
3335
3336        let mut opened = CodeEditor::new("one", "txt").with_vim_enabled(true);
3337        focus_editor(&mut opened);
3338        vim_keys(&mut opened, "o");
3339        let _ = opened.update(&Message::CharacterInput('X'));
3340        vim_keys(&mut opened, "\u{1b}");
3341        assert_eq!(opened.content(), "one\nX");
3342        assert_eq!(opened.history.undo_count(), 1);
3343        vim_keys(&mut opened, "u");
3344        assert_eq!(opened.content(), "one");
3345    }
3346
3347    #[test]
3348    fn test_vim_editing_emits_incremental_lsp_change() {
3349        let changes = Rc::new(RefCell::new(Vec::new()));
3350        let client = VimTestLspClient { changes: Rc::clone(&changes) };
3351        let content = (0..10)
3352            .map(|line| format!("line{line}"))
3353            .collect::<Vec<_>>()
3354            .join("\n");
3355        let mut editor = CodeEditor::new(&content, "rs").with_vim_enabled(true);
3356        editor.attach_lsp(
3357            Box::new(client),
3358            lsp::LspDocument::new("file:///vim.rs", "rust"),
3359        );
3360        editor.cursors.set_single((5, 2));
3361
3362        vim_keys(&mut editor, "x");
3363
3364        let changes = changes.borrow();
3365        assert_eq!(changes.len(), 1);
3366        assert_eq!(changes[0].len(), 1);
3367        let change = &changes[0][0];
3368        assert_eq!(change.range.start.line, 4);
3369        assert_eq!(change.range.start.character, 0);
3370        assert_eq!(change.range.end.line, 7);
3371        assert_eq!(change.range.end.character, 0);
3372        assert_eq!(change.text, "line4\nlie5\nline6\n");
3373    }
3374
3375    #[test]
3376    fn test_horizontal_scroll_initial_state() {
3377        let editor = CodeEditor::new("short line", "rs");
3378        assert!(
3379            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
3380            "Initial horizontal scroll offset should be 0"
3381        );
3382    }
3383
3384    #[test]
3385    fn test_set_wrap_enabled_resets_horizontal_offset() {
3386        let mut editor = CodeEditor::new("long line", "rs");
3387        editor.wrap_enabled = false;
3388        // Simulate a non-zero horizontal scroll
3389        editor.horizontal_scroll_offset = 100.0;
3390
3391        // Re-enabling wrap should reset horizontal offset
3392        editor.set_wrap_enabled(true);
3393        assert!(
3394            (editor.horizontal_scroll_offset - 0.0).abs() < f32::EPSILON,
3395            "Horizontal scroll offset should be reset when wrap is re-enabled"
3396        );
3397    }
3398
3399    #[test]
3400    fn test_canvas_focus_lost() {
3401        let mut editor = CodeEditor::new("test", "rs");
3402        editor.has_canvas_focus = true;
3403
3404        let _ = editor.update(&Message::CanvasFocusLost);
3405
3406        assert!(!editor.has_canvas_focus);
3407        assert!(!editor.show_cursor);
3408        assert!(editor.focus_locked, "Focus should be locked when lost");
3409    }
3410
3411    #[test]
3412    fn test_canvas_focus_gained_resets_lock() {
3413        let mut editor = CodeEditor::new("test", "rs");
3414        editor.has_canvas_focus = false;
3415        editor.focus_locked = true;
3416
3417        let _ = editor.update(&Message::CanvasFocusGained);
3418
3419        assert!(editor.has_canvas_focus);
3420        assert!(
3421            !editor.focus_locked,
3422            "Focus lock should be reset when focus is gained"
3423        );
3424    }
3425
3426    #[test]
3427    fn test_focus_lock_state() {
3428        let mut editor = CodeEditor::new("test", "rs");
3429
3430        // Initially, focus should not be locked
3431        assert!(!editor.focus_locked);
3432
3433        // When focus is lost, it should be locked
3434        let _ = editor.update(&Message::CanvasFocusLost);
3435        assert!(editor.focus_locked, "Focus should be locked when lost");
3436
3437        // When focus is regained, it should be unlocked
3438        editor.request_focus();
3439        let _ = editor.update(&Message::CanvasFocusGained);
3440        assert!(!editor.focus_locked, "Focus should be unlocked when regained");
3441
3442        // Can manually reset focus lock
3443        editor.focus_locked = true;
3444        editor.reset_focus_lock();
3445        assert!(!editor.focus_locked, "Focus lock should be resetable");
3446    }
3447
3448    #[test]
3449    fn test_reset_focus_lock() {
3450        let mut editor = CodeEditor::new("test", "rs");
3451        editor.focus_locked = true;
3452
3453        editor.reset_focus_lock();
3454
3455        assert!(!editor.focus_locked);
3456    }
3457
3458    #[test]
3459    fn test_home_key() {
3460        let mut editor = CodeEditor::new("hello world", "py");
3461        editor.cursors.primary_mut().position = (0, 5); // Move to middle of line
3462        let _ = editor.update(&Message::Home(false));
3463        assert_eq!(editor.cursors.primary_position(), (0, 0));
3464    }
3465
3466    #[test]
3467    fn test_end_key() {
3468        let mut editor = CodeEditor::new("hello world", "py");
3469        editor.cursors.primary_mut().position = (0, 0);
3470        let _ = editor.update(&Message::End(false));
3471        assert_eq!(editor.cursors.primary_position(), (0, 11)); // Length of "hello world"
3472    }
3473
3474    #[test]
3475    fn test_arrow_key_with_shift_creates_selection() {
3476        let mut editor = CodeEditor::new("hello world", "py");
3477        editor.cursors.primary_mut().position = (0, 0);
3478
3479        // Shift+Right should start selection
3480        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
3481        assert!(editor.cursors.primary().anchor.is_some());
3482        assert!(editor.cursors.primary().has_selection());
3483    }
3484
3485    #[test]
3486    fn test_arrow_key_without_shift_clears_selection() {
3487        let mut editor = CodeEditor::new("hello world", "py");
3488        editor.cursors.primary_mut().anchor = Some((0, 0));
3489        editor.cursors.primary_mut().position = (0, 5);
3490
3491        // Regular arrow key should clear selection
3492        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
3493        assert!(editor.cursors.primary().anchor.is_none());
3494        assert!(!editor.cursors.primary().has_selection());
3495    }
3496
3497    #[test]
3498    fn test_typing_with_selection() {
3499        let mut editor = CodeEditor::new("hello world", "py");
3500        // Ensure editor has focus for character input
3501        editor.request_focus();
3502        editor.has_canvas_focus = true;
3503        editor.focus_locked = false;
3504
3505        editor.cursors.primary_mut().anchor = Some((0, 0));
3506        editor.cursors.primary_mut().position = (0, 5);
3507
3508        let _ = editor.update(&Message::CharacterInput('X'));
3509        assert_eq!(editor.buffer.line(0), "X world");
3510        assert_eq!(editor.cursors.primary_position(), (0, 1));
3511        assert!(!editor.cursors.primary().has_selection());
3512    }
3513
3514    #[test]
3515    fn test_typing_digit_with_reversed_selection_replaces_selection() {
3516        let mut editor = CodeEditor::new("hello world", "py");
3517        editor.request_focus();
3518        editor.has_canvas_focus = true;
3519        editor.focus_locked = false;
3520
3521        editor.cursors.primary_mut().anchor = Some((0, 5));
3522        editor.cursors.primary_mut().position = (0, 0);
3523
3524        let _ = editor.update(&Message::CharacterInput('7'));
3525        assert_eq!(editor.buffer.line(0), "7 world");
3526        assert_eq!(editor.cursors.primary_position(), (0, 1));
3527        assert!(!editor.cursors.primary().has_selection());
3528    }
3529
3530    #[test]
3531    fn test_typing_with_selection_undoes_as_single_edit() {
3532        let mut editor = CodeEditor::new("hello world", "py");
3533        editor.request_focus();
3534        editor.has_canvas_focus = true;
3535        editor.focus_locked = false;
3536
3537        editor.cursors.primary_mut().anchor = Some((0, 0));
3538        editor.cursors.primary_mut().position = (0, 5);
3539
3540        let _ = editor.update(&Message::CharacterInput('X'));
3541        let _ = editor.update(&Message::Undo);
3542        assert_eq!(editor.buffer.line(0), "hello world");
3543    }
3544
3545    #[test]
3546    fn test_ctrl_home() {
3547        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3548        editor.cursors.primary_mut().position = (2, 5); // Start at line 3, column 5
3549        let _ = editor.update(&Message::CtrlHome);
3550        assert_eq!(editor.cursors.primary_position(), (0, 0)); // Should move to beginning of document
3551    }
3552
3553    #[test]
3554    fn test_ctrl_end() {
3555        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3556        editor.cursors.primary_mut().position = (0, 0); // Start at beginning
3557        let _ = editor.update(&Message::CtrlEnd);
3558        assert_eq!(editor.cursors.primary_position(), (2, 5)); // Should move to end of last line (line3 has 5 chars)
3559    }
3560
3561    #[test]
3562    fn test_ctrl_home_clears_selection() {
3563        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3564        editor.cursors.primary_mut().position = (2, 5);
3565        editor.cursors.primary_mut().anchor = Some((0, 0));
3566        editor.cursors.primary_mut().position = (2, 5);
3567
3568        let _ = editor.update(&Message::CtrlHome);
3569        assert_eq!(editor.cursors.primary_position(), (0, 0));
3570        assert!(editor.cursors.primary().anchor.is_none());
3571        assert!(!editor.cursors.primary().has_selection());
3572    }
3573
3574    #[test]
3575    fn test_ctrl_end_clears_selection() {
3576        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3577        editor.cursors.primary_mut().position = (0, 0);
3578        editor.cursors.primary_mut().anchor = Some((0, 0));
3579        editor.cursors.primary_mut().position = (1, 3);
3580
3581        let _ = editor.update(&Message::CtrlEnd);
3582        assert_eq!(editor.cursors.primary_position(), (2, 5));
3583        assert!(editor.cursors.primary().anchor.is_none());
3584        assert!(!editor.cursors.primary().has_selection());
3585    }
3586
3587    #[test]
3588    fn test_goto_position_sets_cursor_and_clears_selection() {
3589        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3590        editor.cursors.primary_mut().anchor = Some((0, 0));
3591        editor.cursors.primary_mut().position = (1, 2);
3592
3593        let _ = editor.update(&Message::GotoPosition(1, 3));
3594
3595        assert_eq!(editor.cursors.primary_position(), (1, 3));
3596        assert!(editor.cursors.primary().anchor.is_none());
3597        assert!(!editor.cursors.primary().has_selection());
3598    }
3599
3600    #[test]
3601    fn test_goto_position_clamps_out_of_range() {
3602        let mut editor = CodeEditor::new("a\nbb", "py");
3603
3604        let _ = editor.update(&Message::GotoPosition(99, 99));
3605
3606        // Clamped to last line (index 1) and end of that line (len = 2)
3607        assert_eq!(editor.cursors.primary_position(), (1, 2));
3608    }
3609
3610    #[test]
3611    fn test_scroll_sets_initial_cache_window() {
3612        let content =
3613            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
3614        let mut editor = CodeEditor::new(&content, "py");
3615
3616        // Simulate initial viewport
3617        let height = 400.0;
3618        let width = 800.0;
3619        let scroll = 0.0;
3620
3621        // Expected derived ranges
3622        let visible_lines_count =
3623            (height / editor.line_height).ceil() as usize + 2;
3624        let first_visible_line = (scroll / editor.line_height).floor() as usize;
3625        let last_visible_line = first_visible_line + visible_lines_count;
3626        let margin = visible_lines_count * 2;
3627        let window_start = first_visible_line.saturating_sub(margin);
3628        let window_end = last_visible_line + margin;
3629
3630        // Apply logic similar to Message::Scrolled branch
3631        editor.viewport_height = height;
3632        editor.viewport_width = width;
3633        editor.viewport_scroll = -1.0;
3634        let scroll_changed = (editor.viewport_scroll - scroll).abs() > 0.1;
3635        let need_rewindow = true;
3636        if (editor.viewport_height - height).abs() > 1.0
3637            || (editor.viewport_width - width).abs() > 1.0
3638            || (scroll_changed && need_rewindow)
3639        {
3640            editor.cache_window_start_line = window_start;
3641            editor.cache_window_end_line = window_end;
3642            editor.last_first_visible_line = first_visible_line;
3643        }
3644        editor.viewport_scroll = scroll;
3645
3646        assert_eq!(editor.last_first_visible_line, first_visible_line);
3647        assert!(editor.cache_window_end_line > editor.cache_window_start_line);
3648        assert_eq!(editor.cache_window_start_line, window_start);
3649        assert_eq!(editor.cache_window_end_line, window_end);
3650    }
3651
3652    #[test]
3653    fn test_small_scroll_keeps_window() {
3654        let content =
3655            (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
3656        let mut editor = CodeEditor::new(&content, "py");
3657        let height = 400.0;
3658        let width = 800.0;
3659        let initial_scroll = 0.0;
3660        let visible_lines_count =
3661            (height / editor.line_height).ceil() as usize + 2;
3662        let first_visible_line =
3663            (initial_scroll / editor.line_height).floor() as usize;
3664        let last_visible_line = first_visible_line + visible_lines_count;
3665        let margin = visible_lines_count * 2;
3666        let window_start = first_visible_line.saturating_sub(margin);
3667        let window_end = last_visible_line + margin;
3668        editor.cache_window_start_line = window_start;
3669        editor.cache_window_end_line = window_end;
3670        editor.viewport_height = height;
3671        editor.viewport_width = width;
3672        editor.viewport_scroll = initial_scroll;
3673
3674        // Small scroll inside window
3675        let small_scroll =
3676            editor.line_height * (visible_lines_count as f32 / 4.0);
3677        let first_visible_line2 =
3678            (small_scroll / editor.line_height).floor() as usize;
3679        let last_visible_line2 = first_visible_line2 + visible_lines_count;
3680        let lower_boundary_trigger = editor.cache_window_start_line > 0
3681            && first_visible_line2
3682                < editor
3683                    .cache_window_start_line
3684                    .saturating_add(visible_lines_count / 2);
3685        let upper_boundary_trigger = last_visible_line2
3686            > editor
3687                .cache_window_end_line
3688                .saturating_sub(visible_lines_count / 2);
3689        let need_rewindow = lower_boundary_trigger || upper_boundary_trigger;
3690
3691        assert!(!need_rewindow, "Small scroll should be inside the window");
3692        // Window remains unchanged
3693        assert_eq!(editor.cache_window_start_line, window_start);
3694        assert_eq!(editor.cache_window_end_line, window_end);
3695    }
3696
3697    #[test]
3698    fn test_large_scroll_rewindows() {
3699        let content =
3700            (0..1000).map(|i| format!("line{}\n", i)).collect::<String>();
3701        let mut editor = CodeEditor::new(&content, "py");
3702        let height = 400.0;
3703        let width = 800.0;
3704        let initial_scroll = 0.0;
3705        let visible_lines_count =
3706            (height / editor.line_height).ceil() as usize + 2;
3707        let first_visible_line =
3708            (initial_scroll / editor.line_height).floor() as usize;
3709        let last_visible_line = first_visible_line + visible_lines_count;
3710        let margin = visible_lines_count * 2;
3711        editor.cache_window_start_line =
3712            first_visible_line.saturating_sub(margin);
3713        editor.cache_window_end_line = last_visible_line + margin;
3714        editor.viewport_height = height;
3715        editor.viewport_width = width;
3716        editor.viewport_scroll = initial_scroll;
3717
3718        // Large scroll beyond window boundary
3719        let large_scroll =
3720            editor.line_height * ((visible_lines_count * 4) as f32);
3721        let first_visible_line2 =
3722            (large_scroll / editor.line_height).floor() as usize;
3723        let last_visible_line2 = first_visible_line2 + visible_lines_count;
3724        let window_start2 = first_visible_line2.saturating_sub(margin);
3725        let window_end2 = last_visible_line2 + margin;
3726        let need_rewindow = first_visible_line2
3727            < editor
3728                .cache_window_start_line
3729                .saturating_add(visible_lines_count / 2)
3730            || last_visible_line2
3731                > editor
3732                    .cache_window_end_line
3733                    .saturating_sub(visible_lines_count / 2);
3734        assert!(need_rewindow, "Large scroll should trigger window update");
3735
3736        // Apply rewindow
3737        editor.cache_window_start_line = window_start2;
3738        editor.cache_window_end_line = window_end2;
3739        editor.last_first_visible_line = first_visible_line2;
3740
3741        assert_eq!(editor.cache_window_start_line, window_start2);
3742        assert_eq!(editor.cache_window_end_line, window_end2);
3743        assert_eq!(editor.last_first_visible_line, first_visible_line2);
3744    }
3745
3746    #[test]
3747    fn test_delete_selection_message() {
3748        let mut editor = CodeEditor::new("hello world", "py");
3749        editor.cursors.primary_mut().position = (0, 0);
3750        editor.cursors.primary_mut().anchor = Some((0, 0));
3751        editor.cursors.primary_mut().position = (0, 5);
3752
3753        let _ = editor.update(&Message::DeleteSelection);
3754        assert_eq!(editor.buffer.line(0), " world");
3755        assert_eq!(editor.cursors.primary_position(), (0, 0));
3756        assert!(editor.cursors.primary().anchor.is_none());
3757        assert!(!editor.cursors.primary().has_selection());
3758    }
3759
3760    #[test]
3761    fn test_delete_selection_multiline() {
3762        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
3763        editor.cursors.primary_mut().position = (0, 2);
3764        editor.cursors.primary_mut().anchor = Some((0, 2));
3765        editor.cursors.primary_mut().position = (2, 2);
3766
3767        let _ = editor.update(&Message::DeleteSelection);
3768        assert_eq!(editor.buffer.line(0), "line3");
3769        assert_eq!(editor.cursors.primary_position(), (0, 2));
3770        assert!(editor.cursors.primary().anchor.is_none());
3771    }
3772
3773    #[test]
3774    fn test_delete_selection_no_selection() {
3775        let mut editor = CodeEditor::new("hello world", "py");
3776        editor.cursors.primary_mut().position = (0, 5);
3777
3778        let _ = editor.update(&Message::DeleteSelection);
3779        // Should do nothing if there's no selection
3780        assert_eq!(editor.buffer.line(0), "hello world");
3781        assert_eq!(editor.cursors.primary_position(), (0, 5));
3782    }
3783
3784    #[test]
3785    #[allow(clippy::unwrap_used)]
3786    fn test_ime_preedit_and_commit_chinese() {
3787        let mut editor = CodeEditor::new("", "py");
3788        // Simulate IME opened
3789        let _ = editor.update(&Message::ImeOpened);
3790        assert!(editor.ime_preedit.is_none());
3791
3792        // Preedit with Chinese content and a selection range
3793        let content = "安全与合规".to_string();
3794        let selection = Some(0..3); // range aligned to UTF-8 character boundary
3795        let _ = editor
3796            .update(&Message::ImePreedit(content.clone(), selection.clone()));
3797
3798        assert!(editor.ime_preedit.is_some());
3799        assert_eq!(
3800            editor.ime_preedit.as_ref().unwrap().content.clone(),
3801            content
3802        );
3803        assert_eq!(
3804            editor.ime_preedit.as_ref().unwrap().selection.clone(),
3805            selection
3806        );
3807
3808        // Commit should insert the text and clear preedit
3809        let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
3810        assert!(editor.ime_preedit.is_none());
3811        assert_eq!(editor.buffer.line(0), "安全与合规");
3812        assert_eq!(
3813            editor.cursors.primary_position(),
3814            (0, "安全与合规".chars().count())
3815        );
3816    }
3817
3818    #[test]
3819    fn test_undo_char_insert() {
3820        let mut editor = CodeEditor::new("hello", "py");
3821        // Ensure editor has focus for character input
3822        editor.request_focus();
3823        editor.has_canvas_focus = true;
3824        editor.focus_locked = false;
3825
3826        editor.cursors.primary_mut().position = (0, 5);
3827
3828        // Type a character
3829        let _ = editor.update(&Message::CharacterInput('!'));
3830        assert_eq!(editor.buffer.line(0), "hello!");
3831        assert_eq!(editor.cursors.primary_position(), (0, 6));
3832
3833        // Undo should remove it (but first end the grouping)
3834        editor.history.end_group();
3835        let _ = editor.update(&Message::Undo);
3836        assert_eq!(editor.buffer.line(0), "hello");
3837        assert_eq!(editor.cursors.primary_position(), (0, 5));
3838    }
3839
3840    #[test]
3841    fn test_undo_redo_char_insert() {
3842        let mut editor = CodeEditor::new("hello", "py");
3843        // Ensure editor has focus for character input
3844        editor.request_focus();
3845        editor.has_canvas_focus = true;
3846        editor.focus_locked = false;
3847
3848        editor.cursors.primary_mut().position = (0, 5);
3849
3850        // Type a character
3851        let _ = editor.update(&Message::CharacterInput('!'));
3852        editor.history.end_group();
3853
3854        // Undo
3855        let _ = editor.update(&Message::Undo);
3856        assert_eq!(editor.buffer.line(0), "hello");
3857
3858        // Redo
3859        let _ = editor.update(&Message::Redo);
3860        assert_eq!(editor.buffer.line(0), "hello!");
3861        assert_eq!(editor.cursors.primary_position(), (0, 6));
3862    }
3863
3864    #[test]
3865    fn test_undo_backspace() {
3866        let mut editor = CodeEditor::new("hello", "py");
3867        editor.cursors.primary_mut().position = (0, 5);
3868
3869        // Backspace
3870        let _ = editor.update(&Message::Backspace);
3871        assert_eq!(editor.buffer.line(0), "hell");
3872        assert_eq!(editor.cursors.primary_position(), (0, 4));
3873
3874        // Undo
3875        let _ = editor.update(&Message::Undo);
3876        assert_eq!(editor.buffer.line(0), "hello");
3877        assert_eq!(editor.cursors.primary_position(), (0, 5));
3878    }
3879
3880    #[test]
3881    fn test_undo_newline() {
3882        let mut editor = CodeEditor::new("hello world", "py");
3883        editor.cursors.primary_mut().position = (0, 5);
3884
3885        // Insert newline
3886        let _ = editor.update(&Message::Enter);
3887        assert_eq!(editor.buffer.line(0), "hello");
3888        assert_eq!(editor.buffer.line(1), " world");
3889        assert_eq!(editor.cursors.primary_position(), (1, 0));
3890
3891        // Undo
3892        let _ = editor.update(&Message::Undo);
3893        assert_eq!(editor.buffer.line(0), "hello world");
3894        assert_eq!(editor.cursors.primary_position(), (0, 5));
3895    }
3896
3897    #[test]
3898    fn test_undo_grouped_typing() {
3899        let mut editor = CodeEditor::new("hello", "py");
3900        // Ensure editor has focus for character input
3901        editor.request_focus();
3902        editor.has_canvas_focus = true;
3903        editor.focus_locked = false;
3904
3905        editor.cursors.primary_mut().position = (0, 5);
3906
3907        // Type multiple characters (they should be grouped)
3908        let _ = editor.update(&Message::CharacterInput(' '));
3909        let _ = editor.update(&Message::CharacterInput('w'));
3910        let _ = editor.update(&Message::CharacterInput('o'));
3911        let _ = editor.update(&Message::CharacterInput('r'));
3912        let _ = editor.update(&Message::CharacterInput('l'));
3913        let _ = editor.update(&Message::CharacterInput('d'));
3914
3915        assert_eq!(editor.buffer.line(0), "hello world");
3916
3917        // End the group
3918        editor.history.end_group();
3919
3920        // Single undo should remove all grouped characters
3921        let _ = editor.update(&Message::Undo);
3922        assert_eq!(editor.buffer.line(0), "hello");
3923        assert_eq!(editor.cursors.primary_position(), (0, 5));
3924    }
3925
3926    #[test]
3927    fn test_navigation_ends_grouping() {
3928        let mut editor = CodeEditor::new("hello", "py");
3929        // Ensure editor has focus for character input
3930        editor.request_focus();
3931        editor.has_canvas_focus = true;
3932        editor.focus_locked = false;
3933
3934        editor.cursors.primary_mut().position = (0, 5);
3935
3936        // Type a character (starts grouping)
3937        let _ = editor.update(&Message::CharacterInput('!'));
3938        assert!(editor.is_grouping);
3939
3940        // Move cursor (ends grouping)
3941        let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
3942        assert!(!editor.is_grouping);
3943
3944        // Type another character (starts new group)
3945        let _ = editor.update(&Message::CharacterInput('?'));
3946        assert!(editor.is_grouping);
3947
3948        editor.history.end_group();
3949
3950        // Two separate undo operations
3951        let _ = editor.update(&Message::Undo);
3952        assert_eq!(editor.buffer.line(0), "hello!");
3953
3954        let _ = editor.update(&Message::Undo);
3955        assert_eq!(editor.buffer.line(0), "hello");
3956    }
3957
3958    #[test]
3959    fn test_edit_increments_revision_and_clears_visual_lines_cache() {
3960        let mut editor = CodeEditor::new("hello", "rs");
3961        editor.request_focus();
3962        editor.has_canvas_focus = true;
3963        editor.focus_locked = false;
3964        editor.cursors.primary_mut().position = (0, 5);
3965
3966        let _ = editor.visual_lines_cached(800.0);
3967        assert!(
3968            editor.visual_lines_cache.borrow().is_some(),
3969            "visual_lines_cached should populate the cache"
3970        );
3971
3972        let previous_revision = editor.buffer_revision;
3973
3974        let _ = editor.update(&Message::CharacterInput('!'));
3975        assert_eq!(
3976            editor.buffer_revision,
3977            previous_revision.wrapping_add(1),
3978            "buffer_revision should change on buffer edits"
3979        );
3980        // `scroll_to_cursor` repopulates the cache after the edit with the new
3981        // revision, so the cache may be `Some`.  What must never happen is that
3982        // stale data (an old revision) survives an edit.
3983        assert!(
3984            editor
3985                .visual_lines_cache
3986                .borrow()
3987                .as_ref()
3988                .is_none_or(|c| c.key.buffer_revision == editor.buffer_revision),
3989            "buffer edits should not leave stale data in the visual lines cache"
3990        );
3991    }
3992
3993    #[test]
3994    fn test_edit_refreshes_only_affected_search_matches() {
3995        let mut editor = CodeEditor::new("foo\nfoo\nfoo", "rs");
3996        editor.request_focus();
3997        editor.has_canvas_focus = true;
3998        editor.focus_locked = false;
3999        editor.search_state.open_search();
4000        editor.search_state.set_query("foo".to_owned(), &editor.buffer);
4001        editor.cursors.primary_mut().position = (1, 1);
4002
4003        let _ = editor.update(&Message::CharacterInput('x'));
4004
4005        let match_lines: Vec<usize> =
4006            editor.search_state.matches.iter().map(|item| item.line).collect();
4007        assert_eq!(match_lines, vec![0, 2]);
4008    }
4009
4010    #[test]
4011    fn test_manual_search_match_selection_updates_current_index() {
4012        let mut editor =
4013            CodeEditor::new("foo bar foo baz foo\nno result", "txt");
4014        editor.search_state.open_search();
4015        editor.search_state.set_query("foo".to_owned(), &editor.buffer);
4016        assert_eq!(editor.search_state.current_match_index, Some(0));
4017
4018        let text_start = editor.gutter_width() + 5.0;
4019        let char_width = editor.char_width;
4020        let line_y = editor.line_height / 2.0;
4021        let point_at_col = |col: usize| {
4022            iced::Point::new(text_start + char_width * col as f32, line_y)
4023        };
4024
4025        let _ = editor.update(&Message::MouseClick(point_at_col(8)));
4026        let _ = editor.update(&Message::MouseDrag(point_at_col(11)));
4027        let _ = editor.update(&Message::MouseRelease);
4028
4029        assert_eq!(
4030            editor.cursors.primary().selection_range(),
4031            Some(((0, 8), (0, 11)))
4032        );
4033        assert_eq!(editor.search_state.current_match_index, Some(1));
4034
4035        let no_match_line = iced::Point::new(
4036            text_start + char_width * 4.0,
4037            editor.line_height * 1.5,
4038        );
4039        let _ = editor.update(&Message::MouseClick(no_match_line));
4040        let _ = editor.update(&Message::MouseRelease);
4041        assert_eq!(editor.search_state.current_match_index, Some(1));
4042
4043        let _ = editor.update(&Message::FindNext);
4044        assert_eq!(editor.search_state.current_match_index, Some(2));
4045        let _ = editor.update(&Message::FindPrevious);
4046        assert_eq!(editor.search_state.current_match_index, Some(1));
4047    }
4048
4049    #[test]
4050    fn test_manual_line_selection_updates_current_search_index() {
4051        let mut editor =
4052            CodeEditor::new("foo\nprefix foo suffix\nlast foo", "txt");
4053        editor.search_state.open_search();
4054        editor.search_state.set_query("foo".to_owned(), &editor.buffer);
4055        assert_eq!(editor.search_state.current_match_index, Some(0));
4056
4057        let line_start = iced::Point::new(
4058            editor.gutter_width() + 5.0,
4059            editor.line_height * 1.5,
4060        );
4061        let _ = editor.update(&Message::MouseClick(line_start));
4062        let _ = editor.update(&Message::MouseRelease);
4063
4064        assert_eq!(editor.cursors.primary_position(), (1, 0));
4065        assert_eq!(editor.search_state.current_match_index, Some(1));
4066
4067        let _ = editor.update(&Message::FindNext);
4068        assert_eq!(editor.search_state.current_match_index, Some(2));
4069
4070        let mut keyboard_editor =
4071            CodeEditor::new("foo\nprefix foo suffix\nlast foo", "txt");
4072        keyboard_editor.search_state.open_search();
4073        keyboard_editor
4074            .search_state
4075            .set_query("foo".to_owned(), &keyboard_editor.buffer);
4076
4077        let _ = keyboard_editor
4078            .update(&Message::ArrowKey(ArrowDirection::Down, false));
4079
4080        assert_eq!(keyboard_editor.cursors.primary_position(), (1, 0));
4081        assert_eq!(keyboard_editor.search_state.current_match_index, Some(1));
4082    }
4083
4084    #[test]
4085    fn test_incremental_visual_lines_match_full_recalculation_after_newline() {
4086        use std::collections::HashSet;
4087
4088        let mut editor = CodeEditor::new("zero\nabcdefgh\nlast", "rs")
4089            .with_wrap_column(Some(4));
4090        editor.request_focus();
4091        editor.has_canvas_focus = true;
4092        editor.focus_locked = false;
4093        editor.cursors.primary_mut().position = (1, 4);
4094
4095        let _ = editor.visual_lines_cached(800.0);
4096        let _ = editor.update(&Message::Enter);
4097        let incremental = editor.visual_lines_cached(800.0);
4098
4099        let calculator = super::super::wrapping::WrappingCalculator::new(
4100            editor.wrap_enabled,
4101            editor.wrap_column,
4102            editor.full_char_width,
4103            editor.char_width,
4104        );
4105        let expected = calculator.calculate_visual_lines(
4106            &editor.buffer,
4107            800.0,
4108            editor.gutter_width(),
4109            &HashSet::new(),
4110        );
4111
4112        assert_eq!(incremental.as_ref(), &expected);
4113    }
4114
4115    #[test]
4116    fn test_multiple_undo_redo() {
4117        let mut editor = CodeEditor::new("a", "py");
4118        // Ensure editor has focus for character input
4119        editor.request_focus();
4120        editor.has_canvas_focus = true;
4121        editor.focus_locked = false;
4122
4123        editor.cursors.primary_mut().position = (0, 1);
4124
4125        // Make several changes
4126        let _ = editor.update(&Message::CharacterInput('b'));
4127        editor.history.end_group();
4128
4129        let _ = editor.update(&Message::CharacterInput('c'));
4130        editor.history.end_group();
4131
4132        let _ = editor.update(&Message::CharacterInput('d'));
4133        editor.history.end_group();
4134
4135        assert_eq!(editor.buffer.line(0), "abcd");
4136
4137        // Undo all
4138        let _ = editor.update(&Message::Undo);
4139        assert_eq!(editor.buffer.line(0), "abc");
4140
4141        let _ = editor.update(&Message::Undo);
4142        assert_eq!(editor.buffer.line(0), "ab");
4143
4144        let _ = editor.update(&Message::Undo);
4145        assert_eq!(editor.buffer.line(0), "a");
4146
4147        // Redo all
4148        let _ = editor.update(&Message::Redo);
4149        assert_eq!(editor.buffer.line(0), "ab");
4150
4151        let _ = editor.update(&Message::Redo);
4152        assert_eq!(editor.buffer.line(0), "abc");
4153
4154        let _ = editor.update(&Message::Redo);
4155        assert_eq!(editor.buffer.line(0), "abcd");
4156    }
4157
4158    #[test]
4159    fn test_delete_key_with_selection() {
4160        let mut editor = CodeEditor::new("hello world", "py");
4161        editor.cursors.primary_mut().anchor = Some((0, 0));
4162        editor.cursors.primary_mut().position = (0, 5);
4163        editor.cursors.primary_mut().position = (0, 5);
4164
4165        let _ = editor.update(&Message::Delete);
4166
4167        assert_eq!(editor.buffer.line(0), " world");
4168        assert_eq!(editor.cursors.primary_position(), (0, 0));
4169        assert!(editor.cursors.primary().anchor.is_none());
4170        assert!(!editor.cursors.primary().has_selection());
4171    }
4172
4173    #[test]
4174    fn test_delete_key_without_selection() {
4175        let mut editor = CodeEditor::new("hello", "py");
4176        editor.cursors.primary_mut().position = (0, 0);
4177
4178        let _ = editor.update(&Message::Delete);
4179
4180        // Should delete the 'h'
4181        assert_eq!(editor.buffer.line(0), "ello");
4182        assert_eq!(editor.cursors.primary_position(), (0, 0));
4183    }
4184
4185    #[test]
4186    fn test_backspace_with_selection() {
4187        let mut editor = CodeEditor::new("hello world", "py");
4188        editor.cursors.primary_mut().anchor = Some((0, 6));
4189        editor.cursors.primary_mut().position = (0, 11);
4190        editor.cursors.primary_mut().position = (0, 11);
4191
4192        let _ = editor.update(&Message::Backspace);
4193
4194        assert_eq!(editor.buffer.line(0), "hello ");
4195        assert_eq!(editor.cursors.primary_position(), (0, 6));
4196        assert!(editor.cursors.primary().anchor.is_none());
4197        assert!(!editor.cursors.primary().has_selection());
4198    }
4199
4200    #[test]
4201    fn test_backspace_without_selection() {
4202        let mut editor = CodeEditor::new("hello", "py");
4203        editor.cursors.primary_mut().position = (0, 5);
4204
4205        let _ = editor.update(&Message::Backspace);
4206
4207        // Should delete the 'o'
4208        assert_eq!(editor.buffer.line(0), "hell");
4209        assert_eq!(editor.cursors.primary_position(), (0, 4));
4210    }
4211
4212    #[test]
4213    fn test_delete_multiline_selection() {
4214        let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
4215        editor.cursors.primary_mut().anchor = Some((0, 2));
4216        editor.cursors.primary_mut().position = (2, 2);
4217        editor.cursors.primary_mut().position = (2, 2);
4218
4219        let _ = editor.update(&Message::Delete);
4220
4221        assert_eq!(editor.buffer.line(0), "line3");
4222        assert_eq!(editor.cursors.primary_position(), (0, 2));
4223        assert!(editor.cursors.primary().anchor.is_none());
4224    }
4225
4226    #[test]
4227    fn test_canvas_focus_gained() {
4228        let mut editor = CodeEditor::new("hello world", "py");
4229        assert!(!editor.has_canvas_focus);
4230        assert!(!editor.show_cursor);
4231
4232        let _ = editor.update(&Message::CanvasFocusGained);
4233
4234        assert!(editor.has_canvas_focus);
4235        assert!(editor.show_cursor);
4236    }
4237
4238    #[test]
4239    fn test_mouse_click_gains_focus() {
4240        let mut editor = CodeEditor::new("hello world", "py");
4241        editor.has_canvas_focus = false;
4242        editor.show_cursor = false;
4243
4244        let _ =
4245            editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
4246
4247        assert!(editor.has_canvas_focus);
4248        assert!(editor.show_cursor);
4249    }
4250
4251    #[test]
4252    fn test_context_click_inside_selection_preserves_selection() {
4253        let mut editor = CodeEditor::new("hello world", "py");
4254        editor.cursors.primary_mut().anchor = Some((0, 0));
4255        editor.cursors.primary_mut().position = (0, 5);
4256        let point = iced::Point::new(
4257            editor.gutter_width() + 5.0 + editor.char_width * 2.0,
4258            editor.line_height / 2.0,
4259        );
4260
4261        let _ = editor.update(&Message::ContextMenuRequested(point));
4262
4263        assert_eq!(
4264            editor.cursors.primary().selection_range(),
4265            Some(((0, 0), (0, 5)))
4266        );
4267    }
4268
4269    #[test]
4270    fn test_context_click_outside_selection_moves_caret() {
4271        let mut editor = CodeEditor::new("hello world", "py");
4272        editor.cursors.primary_mut().anchor = Some((0, 0));
4273        editor.cursors.primary_mut().position = (0, 5);
4274        let point = iced::Point::new(
4275            editor.gutter_width() + 5.0 + editor.char_width * 8.0,
4276            editor.line_height / 2.0,
4277        );
4278
4279        let _ = editor.update(&Message::ContextMenuRequested(point));
4280
4281        assert_eq!(editor.cursors.primary_position(), (0, 8));
4282        assert!(!editor.cursors.primary().has_selection());
4283    }
4284
4285    #[test]
4286    fn test_context_menu_cut_and_select_all() {
4287        let mut editor = CodeEditor::new("hello world", "py");
4288        editor.cursors.primary_mut().anchor = Some((0, 0));
4289        editor.cursors.primary_mut().position = (0, 5);
4290
4291        let _ = editor.update(&Message::Cut);
4292        assert_eq!(editor.content(), " world");
4293
4294        let _ = editor.update(&Message::SelectAll);
4295        assert_eq!(editor.get_selected_text(), Some(" world".to_string()));
4296
4297        let _ = editor.update(&Message::Undo);
4298        assert_eq!(editor.content(), "hello world");
4299    }
4300
4301    #[test]
4302    fn test_enter_no_indent() {
4303        let mut editor = CodeEditor::new("hello", "rs");
4304        editor.cursors.primary_mut().position = (0, 5);
4305        let _ = editor.update(&Message::Enter);
4306        assert_eq!(editor.buffer.line(0), "hello");
4307        assert_eq!(editor.buffer.line(1), "");
4308        assert_eq!(editor.cursors.primary_position(), (1, 0));
4309    }
4310
4311    #[test]
4312    fn test_typing_after_enter_does_not_delete_newline_from_click_anchor() {
4313        let mut editor = CodeEditor::new("hello", "rs");
4314        editor.request_focus();
4315        editor.has_canvas_focus = true;
4316        editor.focus_locked = false;
4317
4318        // A regular click starts drag tracking with a zero-length anchor.
4319        editor.cursors.primary_mut().position = (0, 5);
4320        editor.cursors.primary_mut().anchor = Some((0, 5));
4321
4322        let _ = editor.update(&Message::Enter);
4323        let _ = editor.update(&Message::CharacterInput('X'));
4324
4325        assert_eq!(editor.buffer.line_count(), 2);
4326        assert_eq!(editor.buffer.line(0), "hello");
4327        assert_eq!(editor.buffer.line(1), "X");
4328        assert_eq!(editor.cursors.primary_position(), (1, 1));
4329        assert!(!editor.cursors.primary().has_selection());
4330    }
4331
4332    #[test]
4333    fn test_enter_replaces_selection_and_undo_restores_text() {
4334        let mut editor = CodeEditor::new("hello world", "rs");
4335        editor.set_auto_indent_enabled(false);
4336        editor.cursors.primary_mut().anchor = Some((0, 0));
4337        editor.cursors.primary_mut().position = (0, 5);
4338
4339        let _ = editor.update(&Message::Enter);
4340
4341        assert_eq!(editor.buffer.line(0), "");
4342        assert_eq!(editor.buffer.line(1), " world");
4343        assert_eq!(editor.cursors.primary_position(), (1, 0));
4344        assert!(!editor.cursors.primary().has_selection());
4345
4346        let _ = editor.update(&Message::Undo);
4347        assert_eq!(editor.content(), "hello world");
4348    }
4349
4350    #[test]
4351    fn test_enter_auto_indent_spaces() {
4352        let mut editor = CodeEditor::new("    hello", "rs");
4353        editor.cursors.primary_mut().position = (0, 9);
4354        let _ = editor.update(&Message::Enter);
4355        assert_eq!(editor.buffer.line(0), "    hello");
4356        assert_eq!(editor.buffer.line(1), "    ");
4357        assert_eq!(editor.cursors.primary_position(), (1, 4));
4358    }
4359
4360    #[test]
4361    fn test_enter_auto_indent_tab() {
4362        let mut editor = CodeEditor::new("\thello", "rs");
4363        editor.cursors.primary_mut().position = (0, 6);
4364        let _ = editor.update(&Message::Enter);
4365        assert_eq!(editor.buffer.line(0), "\thello");
4366        assert_eq!(editor.buffer.line(1), "\t");
4367        assert_eq!(editor.cursors.primary_position(), (1, 1));
4368    }
4369
4370    #[test]
4371    fn test_enter_auto_indent_undo() {
4372        let mut editor = CodeEditor::new("    hello", "rs");
4373        editor.cursors.primary_mut().position = (0, 9);
4374        let _ = editor.update(&Message::Enter);
4375        assert_eq!(editor.buffer.line_count(), 2);
4376
4377        let _ = editor.update(&Message::Undo);
4378        assert_eq!(editor.buffer.line_count(), 1);
4379        assert_eq!(editor.buffer.line(0), "    hello");
4380        assert_eq!(editor.cursors.primary_position(), (0, 9));
4381    }
4382
4383    // =========================================================================
4384    // Multi-cursor tests
4385    // =========================================================================
4386
4387    #[test]
4388    fn test_multi_cursor_char_input_different_lines() {
4389        let mut editor = CodeEditor::new("aaa\nbbb", "rs");
4390        editor.request_focus();
4391        editor.has_canvas_focus = true;
4392        editor.focus_locked = false;
4393        // Place cursors at (0, 1) and (1, 1)
4394        editor.cursors.primary_mut().position = (0, 1);
4395        editor.cursors.add_cursor((1, 1));
4396
4397        let _ = editor.update(&Message::CharacterInput('X'));
4398
4399        // Both lines should have 'X' inserted at col 1
4400        assert_eq!(editor.buffer.line(0), "aXaa");
4401        assert_eq!(editor.buffer.line(1), "bXbb");
4402    }
4403
4404    #[test]
4405    fn test_multi_cursor_char_input_same_line() {
4406        let mut editor = CodeEditor::new("abcd", "rs");
4407        editor.request_focus();
4408        editor.has_canvas_focus = true;
4409        editor.focus_locked = false;
4410        // Place cursors at col 1 and col 3 (same line)
4411        editor.cursors.primary_mut().position = (0, 1);
4412        editor.cursors.add_cursor((0, 3));
4413
4414        let _ = editor.update(&Message::CharacterInput('X'));
4415
4416        // Process descending: col 3 first → "abcXd"; then col 1 → "aXbcXd"
4417        // Col 1 cursor adjustment: insert at col 3 does not affect col 1 (col 1 < 3)
4418        assert_eq!(editor.buffer.line(0), "aXbcXd");
4419    }
4420
4421    #[test]
4422    fn test_add_cursor_above() {
4423        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
4424        editor.cursors.primary_mut().position = (1, 3);
4425
4426        let _ = editor.update(&Message::AddCursorAbove);
4427
4428        assert!(editor.cursors.is_multi());
4429        // New cursor should be at line 0, col 3
4430        assert_eq!(editor.cursors.as_slice()[0].position, (0, 3));
4431    }
4432
4433    #[test]
4434    fn test_add_cursor_below() {
4435        let mut editor = CodeEditor::new("line0\nline1\nline2", "rs");
4436        editor.cursors.primary_mut().position = (1, 3);
4437
4438        let _ = editor.update(&Message::AddCursorBelow);
4439
4440        assert!(editor.cursors.is_multi());
4441        // New cursor should be at line 2, col 3
4442        assert_eq!(
4443            editor
4444                .cursors
4445                .as_slice()
4446                .iter()
4447                .find(|c| c.position.0 == 2)
4448                .map(|c| c.position),
4449            Some((2, 3))
4450        );
4451    }
4452
4453    #[test]
4454    fn test_escape_collapses_multi_cursor() {
4455        let mut editor = CodeEditor::new("line0\nline1", "rs");
4456        editor.cursors.primary_mut().position = (0, 0);
4457        editor.cursors.add_cursor((1, 0));
4458        assert!(editor.cursors.is_multi());
4459
4460        let _ = editor.update(&Message::CloseSearch);
4461
4462        assert!(!editor.cursors.is_multi());
4463    }
4464
4465    #[test]
4466    fn test_select_next_occurrence_selects_word() {
4467        let mut editor = CodeEditor::new("foo bar foo", "rs");
4468        editor.cursors.primary_mut().position = (0, 1); // inside "foo"
4469
4470        let _ = editor.update(&Message::SelectNextOccurrence);
4471
4472        // Primary cursor should now have "foo" selected
4473        let range = editor.cursors.primary().selection_range();
4474        assert_eq!(range, Some(((0, 0), (0, 3))));
4475    }
4476
4477    #[test]
4478    fn test_select_next_occurrence_adds_cursor_for_second_occurrence() {
4479        let mut editor = CodeEditor::new("foo bar foo", "rs");
4480        // Set up primary cursor with "foo" selected
4481        editor.cursors.primary_mut().anchor = Some((0, 0));
4482        editor.cursors.primary_mut().position = (0, 3);
4483
4484        let _ = editor.update(&Message::SelectNextOccurrence);
4485
4486        // Should now have 2 cursors: primary at "foo" (0..3) and new at "foo" (8..11)
4487        assert_eq!(editor.cursors.len(), 2);
4488    }
4489
4490    #[test]
4491    fn test_multi_cursor_backspace() {
4492        let mut editor = CodeEditor::new("abc\ndef", "rs");
4493        editor.cursors.primary_mut().position = (0, 2);
4494        editor.cursors.add_cursor((1, 2));
4495
4496        let _ = editor.update(&Message::Backspace);
4497
4498        assert_eq!(editor.buffer.line(0), "ac");
4499        assert_eq!(editor.buffer.line(1), "df");
4500    }
4501
4502    #[test]
4503    fn test_toggle_comment_selection() {
4504        let mut editor = CodeEditor::new("a\nb\nc", "rs");
4505        // Select lines 0..=2.
4506        editor.cursors.primary_mut().anchor = Some((0, 0));
4507        editor.cursors.primary_mut().position = (2, 1);
4508
4509        let _ = editor.update(&Message::ToggleComment);
4510        assert_eq!(editor.buffer.to_string(), "// a\n// b\n// c");
4511        assert_eq!(editor.cursors.primary_position(), (2, 4));
4512
4513        // Toggling again uncomments the whole range.
4514        let _ = editor.update(&Message::ToggleComment);
4515        assert_eq!(editor.buffer.to_string(), "a\nb\nc");
4516    }
4517
4518    #[test]
4519    fn test_toggle_comment_noop_without_token() {
4520        let mut editor = CodeEditor::new("<div>", "html");
4521        let _ = editor.update(&Message::ToggleComment);
4522        // HTML has no line-comment token, so the buffer is unchanged.
4523        assert_eq!(editor.buffer.line(0), "<div>");
4524    }
4525
4526    #[test]
4527    fn test_toggle_comment_undo() {
4528        let mut editor = CodeEditor::new("    let x = 1;", "rs");
4529        editor.cursors.primary_mut().position = (0, 8);
4530
4531        let _ = editor.update(&Message::ToggleComment);
4532        assert_eq!(editor.buffer.line(0), "    // let x = 1;");
4533
4534        let _ = editor.update(&Message::Undo);
4535        assert_eq!(editor.buffer.line(0), "    let x = 1;");
4536        assert_eq!(editor.cursors.primary_position(), (0, 8));
4537    }
4538
4539    #[test]
4540    fn test_open_goto_line_prefills_current_one_based_line() {
4541        let mut editor = CodeEditor::new("one\ntwo\nthree", "rs");
4542        editor.cursors.primary_mut().position = (1, 2);
4543        editor.search_state.open_search();
4544
4545        let _ = editor.update(&Message::OpenGotoLine);
4546
4547        assert!(editor.goto_line_state.is_open);
4548        assert_eq!(editor.goto_line_state.query, "2");
4549        assert!(!editor.search_state.is_open);
4550    }
4551
4552    #[test]
4553    fn test_submit_goto_line_moves_to_one_based_line_and_closes_dialog() {
4554        let mut editor = CodeEditor::new("one\ntwo\nthree", "rs");
4555        let _ = editor.update(&Message::OpenGotoLine);
4556        let _ = editor.update(&Message::GotoLineChanged("3".to_string()));
4557
4558        let _ = editor.update(&Message::SubmitGotoLine);
4559
4560        assert_eq!(editor.cursors.primary_position(), (2, 0));
4561        assert!(!editor.goto_line_state.is_open);
4562    }
4563
4564    #[test]
4565    fn test_submit_goto_line_clamps_to_last_line() {
4566        let mut editor = CodeEditor::new("one\ntwo\nthree", "rs");
4567        let _ = editor.update(&Message::OpenGotoLine);
4568        let _ = editor.update(&Message::GotoLineChanged("99".to_string()));
4569
4570        let _ = editor.update(&Message::SubmitGotoLine);
4571
4572        assert_eq!(editor.cursors.primary_position(), (2, 0));
4573        assert!(!editor.goto_line_state.is_open);
4574    }
4575
4576    #[test]
4577    fn test_submit_goto_line_reveals_folded_target() {
4578        let mut editor =
4579            CodeEditor::new("root\n    child\n        nested\ntail", "rs");
4580        editor.fold_all();
4581        assert!(editor.hidden_lines_set().contains(&1));
4582        let _ = editor.update(&Message::OpenGotoLine);
4583        let _ = editor.update(&Message::GotoLineChanged("2".to_string()));
4584
4585        let _ = editor.update(&Message::SubmitGotoLine);
4586
4587        assert_eq!(editor.cursors.primary_position(), (1, 0));
4588        assert!(!editor.hidden_lines_set().contains(&1));
4589    }
4590
4591    #[test]
4592    fn test_submit_goto_line_keeps_dialog_open_for_invalid_input() {
4593        let mut editor = CodeEditor::new("one\ntwo\nthree", "rs");
4594        editor.cursors.primary_mut().position = (1, 1);
4595        let _ = editor.update(&Message::OpenGotoLine);
4596        let _ = editor.update(&Message::GotoLineChanged("invalid".to_string()));
4597
4598        let _ = editor.update(&Message::SubmitGotoLine);
4599
4600        assert_eq!(editor.cursors.primary_position(), (1, 1));
4601        assert!(editor.goto_line_state.is_open);
4602    }
4603}