Skip to main content

editor_core/
commands.rs

1//! Command Interface Layer
2//!
3//! Provides a unified command interface for convenient frontend integration.
4//!
5//! # Overview
6//!
7//! The Command Interface Layer is the primary entry point for Editor Core, wrapping all underlying components in a unified command pattern.
8//! It supports the following types of operations:
9//!
10//! - **Text Editing**: Insert, delete, and replace text
11//! - **Cursor Operations**: Move cursor and set selection range
12//! - **View Management**: Set viewport, scroll, and get visible content
13//! - **Style Control**: Add/remove styles and code folding
14//!
15//! # Example
16//!
17//! ```rust
18//! use editor_core::{CommandExecutor, Command, EditCommand};
19//!
20//! let mut executor = CommandExecutor::empty(80);
21//!
22//! // Insert text
23//! executor.execute(Command::Edit(EditCommand::Insert {
24//!     offset: 0,
25//!     text: "Hello, World!".to_string(),
26//! })).unwrap();
27//!
28//! // Batch execute commands
29//! let commands = vec![
30//!     Command::Edit(EditCommand::Insert { offset: 0, text: "Line 1\n".to_string() }),
31//!     Command::Edit(EditCommand::Insert { offset: 7, text: "Line 2\n".to_string() }),
32//! ];
33//! executor.execute_batch(commands).unwrap();
34//! ```
35
36use crate::decorations::{Decoration, DecorationLayerId, DecorationPlacement};
37use crate::delta::{TextDelta, TextDeltaEdit};
38use crate::diagnostics::Diagnostic;
39use crate::intervals::{FoldRegion, StyleId, StyleLayerId};
40use crate::layout::{
41    WrapIndent, WrapMode, cell_width_at, char_width, visual_x_for_column,
42    wrap_indent_cells_for_line_text,
43};
44use crate::line_ending::LineEnding;
45use crate::search::{CharIndex, SearchMatch, SearchOptions, find_all, find_next, find_prev};
46use crate::snapshot::{
47    Cell, ComposedCell, ComposedCellSource, ComposedGrid, ComposedLine, ComposedLineKind,
48    HeadlessGrid, HeadlessLine, MinimapGrid, MinimapLine,
49};
50use crate::{
51    FOLD_PLACEHOLDER_STYLE_ID, FoldingManager, IntervalTree, LayoutEngine, LineIndex, PieceTable,
52};
53use editor_core_lang::CommentConfig;
54use regex::RegexBuilder;
55use std::cell::RefCell;
56use std::cmp::Ordering;
57use std::collections::{BTreeMap, HashMap};
58use unicode_segmentation::UnicodeSegmentation;
59
60/// Position coordinates (line and column numbers)
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct Position {
63    /// Zero-based logical line index.
64    pub line: usize,
65    /// Zero-based column in characters within the logical line.
66    pub column: usize,
67}
68
69impl Position {
70    /// Create a new logical position.
71    pub fn new(line: usize, column: usize) -> Self {
72        Self { line, column }
73    }
74}
75
76impl Ord for Position {
77    fn cmp(&self, other: &Self) -> Ordering {
78        self.line
79            .cmp(&other.line)
80            .then_with(|| self.column.cmp(&other.column))
81    }
82}
83
84impl PartialOrd for Position {
85    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
86        Some(self.cmp(other))
87    }
88}
89
90/// Selection range
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Selection {
93    /// Selection start position
94    pub start: Position,
95    /// Selection end position
96    pub end: Position,
97    /// Selection direction
98    pub direction: SelectionDirection,
99}
100
101/// Selection direction
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum SelectionDirection {
104    /// Forward selection (from start to end)
105    Forward,
106    /// Backward selection (from end to start)
107    Backward,
108}
109
110/// Controls how a Tab key press is handled by the editor when using [`EditCommand::InsertTab`].
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum TabKeyBehavior {
113    /// Insert a literal tab character (`'\t'`).
114    Tab,
115    /// Insert spaces up to the next tab stop (based on the current `tab_width` setting).
116    Spaces,
117}
118
119/// A simple document text edit (character offsets, half-open).
120///
121/// This is commonly used for applying a batch of "simultaneous" edits (e.g. rename, refactor, or
122/// workspace-wide search/replace), where the edit list is expressed in **pre-edit** coordinates.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct TextEditSpec {
125    /// Inclusive start character offset.
126    pub start: usize,
127    /// Exclusive end character offset.
128    pub end: usize,
129    /// Replacement text.
130    pub text: String,
131}
132
133/// Text editing commands
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum EditCommand {
136    /// Insert text at the specified position
137    Insert {
138        /// Character offset to insert at.
139        offset: usize,
140        /// Text to insert.
141        text: String,
142    },
143    /// Delete text in specified range
144    Delete {
145        /// Character offset of the deletion start.
146        start: usize,
147        /// Length of the deletion in characters.
148        length: usize,
149    },
150    /// Replace text in specified range
151    Replace {
152        /// Character offset of the replacement start.
153        start: usize,
154        /// Length of the replaced range in characters.
155        length: usize,
156        /// Replacement text.
157        text: String,
158    },
159    /// VSCode-like typing/paste: apply to all carets/selections (primary + secondary)
160    InsertText {
161        /// Text to insert/replace at each selection/caret.
162        text: String,
163    },
164    /// Insert a tab at each caret (or replace each selection), using the current tab settings.
165    ///
166    /// - If `TabKeyBehavior::Tab`, inserts `'\t'`.
167    /// - If `TabKeyBehavior::Spaces`, inserts spaces up to the next tab stop.
168    InsertTab,
169    /// Insert a newline at each caret (or replace each selection).
170    ///
171    /// If `auto_indent` is true, the inserted newline is followed by the leading whitespace
172    /// prefix of the current logical line.
173    InsertNewline {
174        /// Whether to auto-indent the new line.
175        auto_indent: bool,
176    },
177    /// Indent the selected lines (or the current line for an empty selection).
178    Indent,
179    /// Outdent the selected lines (or the current line for an empty selection).
180    Outdent,
181    /// Duplicate the selected line(s) (or the current line for an empty selection).
182    ///
183    /// This is a line-based operation and will act on all carets/selections (primary + secondary),
184    /// including rectangular selections.
185    DuplicateLines,
186    /// Delete the selected line(s) (or the current line for an empty selection).
187    ///
188    /// This is a line-based operation and will act on all carets/selections (primary + secondary),
189    /// including rectangular selections.
190    DeleteLines,
191    /// Move the selected line(s) up by one line.
192    ///
193    /// This is a line-based operation and will act on all carets/selections (primary + secondary),
194    /// including rectangular selections.
195    MoveLinesUp,
196    /// Move the selected line(s) down by one line.
197    ///
198    /// This is a line-based operation and will act on all carets/selections (primary + secondary),
199    /// including rectangular selections.
200    MoveLinesDown,
201    /// Join the current line with the next line (for each caret/selection).
202    ///
203    /// If multiple carets/selections exist, joins are applied from bottom to top to keep offsets stable.
204    JoinLines,
205    /// Split the current line at each caret (or replace each selection) by inserting a newline.
206    ///
207    /// This is a convenience alias for [`EditCommand::InsertNewline`] with `auto_indent: false`.
208    SplitLine,
209    /// Toggle comments for the selected line(s) or selection ranges, using a language-provided
210    /// comment configuration.
211    ToggleComment {
212        /// Comment tokens/config for the current language (data-driven).
213        config: CommentConfig,
214    },
215    /// Apply a batch of text edits as a single undoable step.
216    ///
217    /// - Edits are interpreted in **pre-edit** character offsets.
218    /// - Edits must be non-overlapping; they are applied in descending offset order internally.
219    ApplyTextEdits {
220        /// The edit list (character offsets, half-open).
221        edits: Vec<TextEditSpec>,
222    },
223    /// Smart backspace: if the caret is in leading whitespace, delete back to the previous tab stop.
224    ///
225    /// Otherwise, behaves like [`EditCommand::Backspace`].
226    DeleteToPrevTabStop,
227    /// Delete the previous Unicode grapheme cluster (UAX #29) for each caret/selection.
228    DeleteGraphemeBack,
229    /// Delete the next Unicode grapheme cluster (UAX #29) for each caret/selection.
230    DeleteGraphemeForward,
231    /// Delete back to the previous Unicode word boundary (UAX #29) for each caret/selection.
232    DeleteWordBack,
233    /// Delete forward to the next Unicode word boundary (UAX #29) for each caret/selection.
234    DeleteWordForward,
235    /// Backspace-like deletion: delete selection(s) if any, otherwise delete 1 char before each caret.
236    Backspace,
237    /// Delete key-like deletion: delete selection(s) if any, otherwise delete 1 char after each caret.
238    DeleteForward,
239    /// Undo last edit operation (supports grouping)
240    Undo,
241    /// Redo last undone operation (supports grouping)
242    Redo,
243    /// Explicitly end the current undo group (for idle or external boundaries)
244    EndUndoGroup,
245    /// Replace the current occurrence of `query` (based on selection/caret) with `replacement`.
246    ///
247    /// - Honors `options` (case sensitivity / whole-word / regex).
248    /// - Treated as a single undoable edit.
249    ReplaceCurrent {
250        /// Search query.
251        query: String,
252        /// Replacement text.
253        replacement: String,
254        /// Search options (case sensitivity, whole-word, regex).
255        options: SearchOptions,
256    },
257    /// Replace all occurrences of `query` with `replacement`.
258    ///
259    /// - Honors `options` (case sensitivity / whole-word / regex).
260    /// - Treated as a single undoable edit.
261    ReplaceAll {
262        /// Search query.
263        query: String,
264        /// Replacement text.
265        replacement: String,
266        /// Search options (case sensitivity, whole-word, regex).
267        options: SearchOptions,
268    },
269}
270
271/// Cursor & selection commands
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub enum CursorCommand {
274    /// Move cursor to the specified position
275    MoveTo {
276        /// Target logical line index.
277        line: usize,
278        /// Target column in characters (will be clamped to line length).
279        column: usize,
280    },
281    /// Move cursor relatively
282    MoveBy {
283        /// Delta in logical lines.
284        delta_line: isize,
285        /// Delta in columns (characters).
286        delta_column: isize,
287    },
288    /// Move cursor by visual rows (soft wrap + folding aware).
289    ///
290    /// This uses a "preferred x" in **cells** (sticky column) similar to many editors:
291    /// horizontal moves update preferred x, while vertical visual moves try to preserve it.
292    MoveVisualBy {
293        /// Delta in global visual rows (after wrapping/folding).
294        delta_rows: isize,
295    },
296    /// Move cursor to a visual position (global visual row + x in cells).
297    MoveToVisual {
298        /// Target global visual row (after wrapping/folding).
299        row: usize,
300        /// Target x offset in cells within that visual row.
301        x_cells: usize,
302    },
303    /// Move cursor to the start of the current logical line.
304    MoveToLineStart,
305    /// Move cursor to the end of the current logical line.
306    MoveToLineEnd,
307    /// Move cursor to the start of the current visual line segment (wrap-aware).
308    MoveToVisualLineStart,
309    /// Move cursor to the end of the current visual line segment (wrap-aware).
310    MoveToVisualLineEnd,
311    /// Move cursor left by one Unicode grapheme cluster (UAX #29).
312    MoveGraphemeLeft,
313    /// Move cursor right by one Unicode grapheme cluster (UAX #29).
314    MoveGraphemeRight,
315    /// Move cursor left to the previous Unicode word boundary (UAX #29).
316    MoveWordLeft,
317    /// Move cursor right to the next Unicode word boundary (UAX #29).
318    MoveWordRight,
319    /// Set selection range
320    SetSelection {
321        /// Selection start position.
322        start: Position,
323        /// Selection end position.
324        end: Position,
325    },
326    /// Extend selection range
327    ExtendSelection {
328        /// New active end position.
329        to: Position,
330    },
331    /// Clear selection
332    ClearSelection,
333    /// Set multiple selections/multi-cursor (including primary)
334    SetSelections {
335        /// All selections (including primary).
336        selections: Vec<Selection>,
337        /// Index of the primary selection in `selections`.
338        primary_index: usize,
339    },
340    /// Clear secondary selections/cursors, keeping only primary
341    ClearSecondarySelections,
342    /// Set rectangular selection (box/column selection), which expands into one Selection per line
343    SetRectSelection {
344        /// Anchor position (fixed corner).
345        anchor: Position,
346        /// Active position (moving corner).
347        active: Position,
348    },
349    /// Select the entire current line (or the set of lines covered by the selection), for all carets.
350    SelectLine,
351    /// Select the word under each caret (or keep existing selections if already non-empty).
352    SelectWord,
353    /// Expand selection in a basic, editor-friendly way.
354    ///
355    /// - If the selection is empty, expands to the word under the caret.
356    /// - If the selection is non-empty, expands to full line(s).
357    ExpandSelection,
358    /// Add a new caret above each existing caret/selection (at the same column, clamped to line length).
359    AddCursorAbove,
360    /// Add a new caret below each existing caret/selection (at the same column, clamped to line length).
361    AddCursorBelow,
362    /// Multi-cursor match op: add the next occurrence of the current selection/word as a new selection.
363    AddNextOccurrence {
364        /// Search options (case sensitivity, whole-word, regex).
365        options: SearchOptions,
366    },
367    /// Multi-cursor match op: select all occurrences of the current selection/word.
368    AddAllOccurrences {
369        /// Search options (case sensitivity, whole-word, regex).
370        options: SearchOptions,
371    },
372    /// Find the next occurrence of `query` and select it (primary selection only).
373    FindNext {
374        /// Search query.
375        query: String,
376        /// Search options (case sensitivity, whole-word, regex).
377        options: SearchOptions,
378    },
379    /// Find the previous occurrence of `query` and select it (primary selection only).
380    FindPrev {
381        /// Search query.
382        query: String,
383        /// Search options (case sensitivity, whole-word, regex).
384        options: SearchOptions,
385    },
386}
387
388/// View commands
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub enum ViewCommand {
391    /// Set viewport width
392    SetViewportWidth {
393        /// Width in character cells.
394        width: usize,
395    },
396    /// Set soft wrap mode.
397    SetWrapMode {
398        /// Wrap mode.
399        mode: WrapMode,
400    },
401    /// Set wrapped-line indentation policy.
402    SetWrapIndent {
403        /// Wrap indent policy.
404        indent: WrapIndent,
405    },
406    /// Set tab width (in character cells) used for measuring `'\t'` and tab stops.
407    SetTabWidth {
408        /// Tab width in character cells (must be greater than 0).
409        width: usize,
410    },
411    /// Configure how [`EditCommand::InsertTab`] inserts text.
412    SetTabKeyBehavior {
413        /// Tab key behavior.
414        behavior: TabKeyBehavior,
415    },
416    /// Scroll to specified line
417    ScrollTo {
418        /// Logical line index to scroll to.
419        line: usize,
420    },
421    /// Get current viewport content
422    GetViewport {
423        /// Starting visual row.
424        start_row: usize,
425        /// Number of visual rows requested.
426        count: usize,
427    },
428}
429
430/// Style and folding commands
431#[derive(Debug, Clone, PartialEq, Eq)]
432pub enum StyleCommand {
433    /// Add style interval
434    AddStyle {
435        /// Interval start offset in characters.
436        start: usize,
437        /// Interval end offset in characters (exclusive).
438        end: usize,
439        /// Style identifier.
440        style_id: StyleId,
441    },
442    /// Remove style interval
443    RemoveStyle {
444        /// Interval start offset in characters.
445        start: usize,
446        /// Interval end offset in characters (exclusive).
447        end: usize,
448        /// Style identifier.
449        style_id: StyleId,
450    },
451    /// Fold code block
452    Fold {
453        /// Start logical line (inclusive).
454        start_line: usize,
455        /// End logical line (inclusive).
456        end_line: usize,
457    },
458    /// Unfold code block
459    Unfold {
460        /// Start logical line (inclusive) of the fold region to unfold.
461        start_line: usize,
462    },
463    /// Unfold all folds
464    UnfoldAll,
465}
466
467/// Unified command enum
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum Command {
470    /// Text editing commands
471    Edit(EditCommand),
472    /// Cursor command
473    Cursor(CursorCommand),
474    /// View commands
475    View(ViewCommand),
476    /// Style command
477    Style(StyleCommand),
478}
479
480/// Command execution result
481#[derive(Debug, Clone)]
482pub enum CommandResult {
483    /// Success, no return value
484    Success,
485    /// Success, returns text
486    Text(String),
487    /// Success, returns position
488    Position(Position),
489    /// Success, returns offset
490    Offset(usize),
491    /// Success, returns viewport content
492    Viewport(HeadlessGrid),
493    /// Find/search result: a match in char offsets (half-open).
494    SearchMatch {
495        /// Inclusive start character offset.
496        start: usize,
497        /// Exclusive end character offset.
498        end: usize,
499    },
500    /// Find/search result: no match found.
501    SearchNotFound,
502    /// Replace result: how many occurrences were replaced.
503    ReplaceResult {
504        /// Number of occurrences replaced.
505        replaced: usize,
506    },
507}
508
509/// Command error type
510#[derive(Debug, Clone, PartialEq, Eq)]
511pub enum CommandError {
512    /// Invalid offset
513    InvalidOffset(usize),
514    /// Invalid position
515    InvalidPosition {
516        /// Logical line index.
517        line: usize,
518        /// Column in characters.
519        column: usize,
520    },
521    /// Invalid range
522    InvalidRange {
523        /// Inclusive start character offset.
524        start: usize,
525        /// Exclusive end character offset.
526        end: usize,
527    },
528    /// Empty text
529    EmptyText,
530    /// Other error
531    Other(String),
532}
533
534impl std::fmt::Display for CommandError {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        match self {
537            CommandError::InvalidOffset(offset) => {
538                write!(f, "Invalid offset: {}", offset)
539            }
540            CommandError::InvalidPosition { line, column } => {
541                write!(f, "Invalid position: line {}, column {}", line, column)
542            }
543            CommandError::InvalidRange { start, end } => {
544                write!(f, "Invalid range: {}..{}", start, end)
545            }
546            CommandError::EmptyText => {
547                write!(f, "Text cannot be empty")
548            }
549            CommandError::Other(msg) => {
550                write!(f, "{}", msg)
551            }
552        }
553    }
554}
555
556impl std::error::Error for CommandError {}
557
558#[derive(Debug, Clone)]
559struct SelectionSetSnapshot {
560    selections: Vec<Selection>,
561    primary_index: usize,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565enum TextBoundary {
566    Grapheme,
567    Word,
568}
569
570fn byte_offset_for_char_column(text: &str, column: usize) -> usize {
571    if column == 0 {
572        return 0;
573    }
574
575    text.char_indices()
576        .nth(column)
577        .map(|(byte, _)| byte)
578        .unwrap_or_else(|| text.len())
579}
580
581fn char_column_for_byte_offset(text: &str, byte_offset: usize) -> usize {
582    text.get(..byte_offset).unwrap_or(text).chars().count()
583}
584
585fn prev_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
586    let byte_pos = byte_offset_for_char_column(text, column);
587
588    let mut prev = 0usize;
589    match boundary {
590        TextBoundary::Grapheme => {
591            for (b, _) in text.grapheme_indices(true) {
592                if b >= byte_pos {
593                    break;
594                }
595                prev = b;
596            }
597        }
598        TextBoundary::Word => {
599            for (b, _) in text.split_word_bound_indices() {
600                if b >= byte_pos {
601                    break;
602                }
603                prev = b;
604            }
605        }
606    }
607
608    char_column_for_byte_offset(text, prev)
609}
610
611fn next_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
612    let byte_pos = byte_offset_for_char_column(text, column);
613
614    let mut next = text.len();
615    match boundary {
616        TextBoundary::Grapheme => {
617            for (b, _) in text.grapheme_indices(true) {
618                if b > byte_pos {
619                    next = b;
620                    break;
621                }
622            }
623        }
624        TextBoundary::Word => {
625            for (b, _) in text.split_word_bound_indices() {
626                if b > byte_pos {
627                    next = b;
628                    break;
629                }
630            }
631        }
632    }
633
634    char_column_for_byte_offset(text, next)
635}
636
637#[derive(Debug, Clone)]
638struct TextEdit {
639    start_before: usize,
640    start_after: usize,
641    deleted_text: String,
642    inserted_text: String,
643}
644
645impl TextEdit {
646    fn deleted_len(&self) -> usize {
647        self.deleted_text.chars().count()
648    }
649
650    fn inserted_len(&self) -> usize {
651        self.inserted_text.chars().count()
652    }
653}
654
655#[derive(Debug, Clone)]
656struct UndoStep {
657    group_id: usize,
658    edits: Vec<TextEdit>,
659    before_selection: SelectionSetSnapshot,
660    after_selection: SelectionSetSnapshot,
661}
662
663#[derive(Debug)]
664struct UndoRedoManager {
665    undo_stack: Vec<UndoStep>,
666    redo_stack: Vec<UndoStep>,
667    max_undo: usize,
668    /// Clean point tracking. Uses `undo_stack.len()` as the saved position in the linear history.
669    /// When `redo_stack` is non-empty, `clean_index` may be greater than `undo_stack.len()`.
670    clean_index: Option<usize>,
671    next_group_id: usize,
672    open_group_id: Option<usize>,
673}
674
675impl UndoRedoManager {
676    fn new(max_undo: usize) -> Self {
677        Self {
678            undo_stack: Vec::new(),
679            redo_stack: Vec::new(),
680            max_undo,
681            clean_index: Some(0),
682            next_group_id: 0,
683            open_group_id: None,
684        }
685    }
686
687    fn can_undo(&self) -> bool {
688        !self.undo_stack.is_empty()
689    }
690
691    fn can_redo(&self) -> bool {
692        !self.redo_stack.is_empty()
693    }
694
695    fn undo_depth(&self) -> usize {
696        self.undo_stack.len()
697    }
698
699    fn redo_depth(&self) -> usize {
700        self.redo_stack.len()
701    }
702
703    fn current_group_id(&self) -> Option<usize> {
704        self.open_group_id
705    }
706
707    fn is_clean(&self) -> bool {
708        self.clean_index == Some(self.undo_stack.len())
709    }
710
711    fn mark_clean(&mut self) {
712        self.clean_index = Some(self.undo_stack.len());
713        self.end_group();
714    }
715
716    fn end_group(&mut self) {
717        self.open_group_id = None;
718    }
719
720    fn clear_redo_and_adjust_clean(&mut self) {
721        if self.redo_stack.is_empty() {
722            return;
723        }
724
725        // If clean point is in redo area, it becomes unreachable after clearing redo.
726        if let Some(clean_index) = self.clean_index
727            && clean_index > self.undo_stack.len()
728        {
729            self.clean_index = None;
730        }
731
732        self.redo_stack.clear();
733    }
734
735    fn push_step(&mut self, mut step: UndoStep, coalescible_insert: bool) -> usize {
736        self.clear_redo_and_adjust_clean();
737
738        if self.undo_stack.len() >= self.max_undo {
739            self.undo_stack.remove(0);
740            if let Some(clean_index) = self.clean_index {
741                if clean_index == 0 {
742                    self.clean_index = None;
743                } else {
744                    self.clean_index = Some(clean_index - 1);
745                }
746            }
747        }
748
749        let reuse_open_group = coalescible_insert
750            && self.open_group_id.is_some()
751            && self.clean_index != Some(self.undo_stack.len());
752
753        if reuse_open_group {
754            step.group_id = self.open_group_id.expect("checked");
755        } else {
756            step.group_id = self.next_group_id;
757            self.next_group_id = self.next_group_id.wrapping_add(1);
758        }
759
760        if coalescible_insert {
761            self.open_group_id = Some(step.group_id);
762        } else {
763            self.open_group_id = None;
764        }
765
766        let group_id = step.group_id;
767        self.undo_stack.push(step);
768        group_id
769    }
770
771    fn pop_undo_group(&mut self) -> Option<Vec<UndoStep>> {
772        let last_group_id = self.undo_stack.last().map(|s| s.group_id)?;
773        let mut steps: Vec<UndoStep> = Vec::new();
774
775        while let Some(step) = self.undo_stack.last() {
776            if step.group_id != last_group_id {
777                break;
778            }
779            steps.push(self.undo_stack.pop().expect("checked"));
780        }
781
782        Some(steps)
783    }
784
785    fn pop_redo_group(&mut self) -> Option<Vec<UndoStep>> {
786        let last_group_id = self.redo_stack.last().map(|s| s.group_id)?;
787        let mut steps: Vec<UndoStep> = Vec::new();
788
789        while let Some(step) = self.redo_stack.last() {
790            if step.group_id != last_group_id {
791                break;
792            }
793            steps.push(self.redo_stack.pop().expect("checked"));
794        }
795
796        Some(steps)
797    }
798}
799
800#[derive(Debug, Clone, Copy)]
801struct VisualRowSpan {
802    logical_line: usize,
803    start_visual_row: usize,
804    visual_line_count: usize,
805}
806
807#[derive(Debug, Clone, Default)]
808struct VisualRowIndex {
809    spans: Vec<VisualRowSpan>,
810    total_visual_lines: usize,
811}
812
813impl VisualRowIndex {
814    fn total_visual_lines(&self) -> usize {
815        self.total_visual_lines
816    }
817
818    fn span_index_for_visual_row(&self, visual_row: usize) -> Option<usize> {
819        let idx = self
820            .spans
821            .partition_point(|span| span.start_visual_row + span.visual_line_count <= visual_row);
822        (idx < self.spans.len()).then_some(idx)
823    }
824
825    fn span_for_visual_row(&self, visual_row: usize) -> Option<(VisualRowSpan, usize)> {
826        let idx = self.span_index_for_visual_row(visual_row)?;
827        let span = self.spans[idx];
828        Some((span, visual_row.saturating_sub(span.start_visual_row)))
829    }
830
831    fn span_for_logical_line(&self, logical_line: usize) -> Option<VisualRowSpan> {
832        let idx = self
833            .spans
834            .binary_search_by_key(&logical_line, |span| span.logical_line)
835            .ok()?;
836        Some(self.spans[idx])
837    }
838}
839
840/// Editor Core state
841///
842/// `EditorCore` aggregates all underlying editor components, including:
843///
844/// - **PieceTable**: Efficient text storage and modification
845/// - **LineIndex**: Rope-based line index, supporting fast line access
846/// - **LayoutEngine**: Soft wrapping and text layout calculation
847/// - **IntervalTree**: Style interval management
848/// - **FoldingManager**: Code folding management
849/// - **Cursor & Selection**: Cursor and selection state
850///
851/// # Example
852///
853/// ```rust
854/// use editor_core::EditorCore;
855///
856/// let mut core = EditorCore::new("Hello\nWorld", 80);
857/// assert_eq!(core.line_count(), 2);
858/// assert_eq!(core.get_text(), "Hello\nWorld");
859/// ```
860pub struct EditorCore {
861    /// Piece Table storage layer
862    pub piece_table: PieceTable,
863    /// Line index
864    pub line_index: LineIndex,
865    /// Layout engine
866    pub layout_engine: LayoutEngine,
867    /// Interval tree (style management)
868    pub interval_tree: IntervalTree,
869    /// Layered styles (for semantic highlighting/simple syntax highlighting, etc.)
870    pub style_layers: BTreeMap<StyleLayerId, IntervalTree>,
871    /// Derived diagnostics for this document (character-offset ranges + metadata).
872    pub diagnostics: Vec<Diagnostic>,
873    /// Derived decorations for this document (virtual text, links, etc.).
874    pub decorations: BTreeMap<DecorationLayerId, Vec<Decoration>>,
875    /// Derived document symbols / outline for this document.
876    pub document_symbols: crate::DocumentOutline,
877    /// Folding manager
878    pub folding_manager: FoldingManager,
879    /// Current cursor position
880    pub cursor_position: Position,
881    /// Current selection range
882    pub selection: Option<Selection>,
883    /// Secondary selections/cursors (multi-cursor). Each Selection can be empty (start==end), representing a caret.
884    pub secondary_selections: Vec<Selection>,
885    /// Viewport width
886    pub viewport_width: usize,
887    visual_row_index_cache: RefCell<Option<VisualRowIndex>>,
888}
889
890impl EditorCore {
891    /// Create a new Editor Core
892    pub fn new(text: &str, viewport_width: usize) -> Self {
893        let normalized = crate::text::normalize_crlf_to_lf(text);
894        let text = normalized.as_ref();
895
896        let piece_table = PieceTable::new(text);
897        let line_index = LineIndex::from_text(text);
898        let mut layout_engine = LayoutEngine::new(viewport_width);
899
900        // Initialize layout engine to be consistent with initial text (including trailing empty line).
901        let lines = crate::text::split_lines_preserve_trailing(text);
902        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
903        layout_engine.from_lines(&line_refs);
904
905        Self {
906            piece_table,
907            line_index,
908            layout_engine,
909            interval_tree: IntervalTree::new(),
910            style_layers: BTreeMap::new(),
911            diagnostics: Vec::new(),
912            decorations: BTreeMap::new(),
913            document_symbols: crate::DocumentOutline::default(),
914            folding_manager: FoldingManager::new(),
915            cursor_position: Position::new(0, 0),
916            selection: None,
917            secondary_selections: Vec::new(),
918            viewport_width,
919            visual_row_index_cache: RefCell::new(None),
920        }
921    }
922
923    /// Create an empty Editor Core
924    pub fn empty(viewport_width: usize) -> Self {
925        Self::new("", viewport_width)
926    }
927
928    /// Get text content
929    pub fn get_text(&self) -> String {
930        self.piece_table.get_text()
931    }
932
933    /// Get total line count
934    pub fn line_count(&self) -> usize {
935        self.line_index.line_count()
936    }
937
938    /// Get total character count
939    pub fn char_count(&self) -> usize {
940        self.piece_table.char_count()
941    }
942
943    /// Get cursor position
944    pub fn cursor_position(&self) -> Position {
945        self.cursor_position
946    }
947
948    /// Get selection range
949    pub fn selection(&self) -> Option<&Selection> {
950        self.selection.as_ref()
951    }
952
953    /// Get secondary selections/cursors (multi-cursor)
954    pub fn secondary_selections(&self) -> &[Selection] {
955        &self.secondary_selections
956    }
957
958    /// Get the current diagnostics list.
959    pub fn diagnostics(&self) -> &[Diagnostic] {
960        &self.diagnostics
961    }
962
963    /// Get all decorations for a given layer.
964    pub fn decorations_for_layer(&self, layer: DecorationLayerId) -> &[Decoration] {
965        self.decorations
966            .get(&layer)
967            .map(Vec::as_slice)
968            .unwrap_or(&[])
969    }
970
971    /// Invalidate cached visual-row index (wrap/folding derived mapping).
972    pub fn invalidate_visual_row_index_cache(&mut self) {
973        *self.visual_row_index_cache.borrow_mut() = None;
974    }
975
976    fn with_visual_row_index<R>(&self, f: impl FnOnce(&VisualRowIndex) -> R) -> R {
977        if self.visual_row_index_cache.borrow().is_none() {
978            let index = self.build_visual_row_index();
979            *self.visual_row_index_cache.borrow_mut() = Some(index);
980        }
981        let cache = self.visual_row_index_cache.borrow();
982        let index = cache
983            .as_ref()
984            .expect("visual-row cache should be initialized");
985        f(index)
986    }
987
988    fn build_visual_row_index(&self) -> VisualRowIndex {
989        let regions = self.folding_manager.regions();
990        let mut spans = Vec::new();
991        let mut total_visual = 0usize;
992
993        for logical_line in 0..self.layout_engine.logical_line_count() {
994            if Self::is_logical_line_hidden(regions, logical_line) {
995                continue;
996            }
997
998            let visual_line_count = self
999                .layout_engine
1000                .get_line_layout(logical_line)
1001                .map(|l| l.visual_line_count)
1002                .unwrap_or(1)
1003                .max(1);
1004
1005            spans.push(VisualRowSpan {
1006                logical_line,
1007                start_visual_row: total_visual,
1008                visual_line_count,
1009            });
1010            total_visual = total_visual.saturating_add(visual_line_count);
1011        }
1012
1013        VisualRowIndex {
1014            spans,
1015            total_visual_lines: total_visual,
1016        }
1017    }
1018
1019    /// Get styled headless grid snapshot (by visual line).
1020    ///
1021    /// - Supportsoft wrapping (based `layout_engine`)
1022    /// - `Cell.styles` will `interval_tree` + `style_layers` merged from
1023    /// - Supportcode folding (based `folding_manager`)
1024    ///
1025    /// Note: This API is not responsible for mapping `StyleId` to specific colors.
1026    pub fn get_headless_grid_styled(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
1027        self.with_visual_row_index(|index| {
1028            let mut grid = HeadlessGrid::new(start_visual_row, count);
1029            if count == 0 {
1030                return grid;
1031            }
1032
1033            let total_visual = index.total_visual_lines();
1034            if start_visual_row >= total_visual {
1035                return grid;
1036            }
1037
1038            let tab_width = self.layout_engine.tab_width();
1039            let end_visual = start_visual_row.saturating_add(count).min(total_visual);
1040            let regions = self.folding_manager.regions();
1041
1042            let Some(mut span_idx) = index.span_index_for_visual_row(start_visual_row) else {
1043                return grid;
1044            };
1045            let mut current_visual = start_visual_row;
1046            let mut visual_in_line =
1047                start_visual_row.saturating_sub(index.spans[span_idx].start_visual_row);
1048
1049            while current_visual < end_visual && span_idx < index.spans.len() {
1050                let span = index.spans[span_idx];
1051                let logical_line = span.logical_line;
1052
1053                let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
1054                    let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
1055                    current_visual = current_visual.saturating_add(remaining_in_span);
1056                    span_idx = span_idx.saturating_add(1);
1057                    visual_in_line = 0;
1058                    continue;
1059                };
1060
1061                let line_text = self
1062                    .line_index
1063                    .get_line_text(logical_line)
1064                    .unwrap_or_default();
1065                let line_char_len = line_text.chars().count();
1066                let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
1067
1068                let segment_start_col = if visual_in_line == 0 {
1069                    0
1070                } else {
1071                    layout
1072                        .wrap_points
1073                        .get(visual_in_line - 1)
1074                        .map(|wp| wp.char_index)
1075                        .unwrap_or(0)
1076                        .min(line_char_len)
1077                };
1078
1079                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
1080                    layout.wrap_points[visual_in_line]
1081                        .char_index
1082                        .min(line_char_len)
1083                } else {
1084                    line_char_len
1085                };
1086
1087                let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
1088                let mut segment_x_start_cells = 0usize;
1089                if visual_in_line > 0 {
1090                    let indent_cells = wrap_indent_cells_for_line_text(
1091                        &line_text,
1092                        self.layout_engine.wrap_indent(),
1093                        self.viewport_width,
1094                        tab_width,
1095                    );
1096                    segment_x_start_cells = indent_cells;
1097                    for _ in 0..indent_cells {
1098                        headless_line.add_cell(Cell::new(' ', 1));
1099                    }
1100                }
1101                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1102
1103                for (col, ch) in line_text
1104                    .chars()
1105                    .enumerate()
1106                    .skip(segment_start_col)
1107                    .take(segment_end_col.saturating_sub(segment_start_col))
1108                {
1109                    let offset = line_start_offset + col;
1110                    let styles = self.styles_at_offset(offset);
1111                    let w = cell_width_at(ch, x_in_line, tab_width);
1112                    x_in_line = x_in_line.saturating_add(w);
1113                    headless_line.add_cell(Cell::with_styles(ch, w, styles));
1114                }
1115
1116                headless_line.set_visual_metadata(
1117                    visual_in_line,
1118                    line_start_offset.saturating_add(segment_start_col),
1119                    line_start_offset.saturating_add(segment_end_col),
1120                    segment_x_start_cells,
1121                );
1122                headless_line.set_fold_placeholder_appended(false);
1123
1124                // For collapsed folding start line, append placeholder to the last segment.
1125                if visual_in_line + 1 == layout.visual_line_count
1126                    && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
1127                    && !region.placeholder.is_empty()
1128                {
1129                    if !headless_line.cells.is_empty() {
1130                        x_in_line = x_in_line.saturating_add(char_width(' '));
1131                        headless_line.add_cell(Cell::with_styles(
1132                            ' ',
1133                            char_width(' '),
1134                            vec![FOLD_PLACEHOLDER_STYLE_ID],
1135                        ));
1136                    }
1137                    for ch in region.placeholder.chars() {
1138                        let w = cell_width_at(ch, x_in_line, tab_width);
1139                        x_in_line = x_in_line.saturating_add(w);
1140                        headless_line.add_cell(Cell::with_styles(
1141                            ch,
1142                            w,
1143                            vec![FOLD_PLACEHOLDER_STYLE_ID],
1144                        ));
1145                    }
1146                    headless_line.set_fold_placeholder_appended(true);
1147                }
1148
1149                grid.add_line(headless_line);
1150                current_visual = current_visual.saturating_add(1);
1151                visual_in_line = visual_in_line.saturating_add(1);
1152                if visual_in_line >= span.visual_line_count {
1153                    span_idx = span_idx.saturating_add(1);
1154                    visual_in_line = 0;
1155                }
1156            }
1157
1158            grid
1159        })
1160    }
1161
1162    /// Get a lightweight minimap snapshot (by visual line).
1163    ///
1164    /// Compared with [`Self::get_headless_grid_styled`], this API returns aggregated per-line
1165    /// summaries instead of per-character cells, which is intended for minimap-like overviews.
1166    pub fn get_minimap_grid(&self, start_visual_row: usize, count: usize) -> MinimapGrid {
1167        self.with_visual_row_index(|index| {
1168            let mut grid = MinimapGrid::new(start_visual_row, count);
1169            if count == 0 {
1170                return grid;
1171            }
1172
1173            let total_visual = index.total_visual_lines();
1174            if start_visual_row >= total_visual {
1175                return grid;
1176            }
1177
1178            let tab_width = self.layout_engine.tab_width();
1179            let end_visual = start_visual_row.saturating_add(count).min(total_visual);
1180            let regions = self.folding_manager.regions();
1181
1182            let Some(mut span_idx) = index.span_index_for_visual_row(start_visual_row) else {
1183                return grid;
1184            };
1185            let mut current_visual = start_visual_row;
1186            let mut visual_in_line =
1187                start_visual_row.saturating_sub(index.spans[span_idx].start_visual_row);
1188
1189            while current_visual < end_visual && span_idx < index.spans.len() {
1190                let span = index.spans[span_idx];
1191                let logical_line = span.logical_line;
1192
1193                let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
1194                    let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
1195                    current_visual = current_visual.saturating_add(remaining_in_span);
1196                    span_idx = span_idx.saturating_add(1);
1197                    visual_in_line = 0;
1198                    continue;
1199                };
1200
1201                let line_text = self
1202                    .line_index
1203                    .get_line_text(logical_line)
1204                    .unwrap_or_default();
1205                let line_char_len = line_text.chars().count();
1206                let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
1207
1208                let segment_start_col = if visual_in_line == 0 {
1209                    0
1210                } else {
1211                    layout
1212                        .wrap_points
1213                        .get(visual_in_line - 1)
1214                        .map(|wp| wp.char_index)
1215                        .unwrap_or(0)
1216                        .min(line_char_len)
1217                };
1218
1219                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
1220                    layout.wrap_points[visual_in_line]
1221                        .char_index
1222                        .min(line_char_len)
1223                } else {
1224                    line_char_len
1225                };
1226
1227                let mut total_cells = 0usize;
1228                let mut non_whitespace_cells = 0usize;
1229                let mut dominant_style_counts: HashMap<StyleId, usize> = HashMap::new();
1230                if visual_in_line > 0 {
1231                    let indent_cells = wrap_indent_cells_for_line_text(
1232                        &line_text,
1233                        self.layout_engine.wrap_indent(),
1234                        self.viewport_width,
1235                        tab_width,
1236                    );
1237                    total_cells = total_cells.saturating_add(indent_cells);
1238                }
1239                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1240
1241                for (col, ch) in line_text
1242                    .chars()
1243                    .enumerate()
1244                    .skip(segment_start_col)
1245                    .take(segment_end_col.saturating_sub(segment_start_col))
1246                {
1247                    let offset = line_start_offset + col;
1248                    let styles = self.styles_at_offset(offset);
1249                    let w = cell_width_at(ch, x_in_line, tab_width);
1250                    x_in_line = x_in_line.saturating_add(w);
1251                    total_cells = total_cells.saturating_add(w);
1252                    if !ch.is_whitespace() {
1253                        non_whitespace_cells = non_whitespace_cells.saturating_add(w);
1254                    }
1255                    if let Some(style) = styles.first().copied() {
1256                        let entry = dominant_style_counts.entry(style).or_insert(0);
1257                        *entry = entry.saturating_add(w);
1258                    }
1259                }
1260
1261                let mut placeholder_appended = false;
1262                if visual_in_line + 1 == layout.visual_line_count
1263                    && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
1264                    && !region.placeholder.is_empty()
1265                {
1266                    placeholder_appended = true;
1267                    if total_cells > 0 {
1268                        total_cells = total_cells.saturating_add(char_width(' '));
1269                    }
1270                    for ch in region.placeholder.chars() {
1271                        let w = cell_width_at(ch, x_in_line, tab_width);
1272                        x_in_line = x_in_line.saturating_add(w);
1273                        total_cells = total_cells.saturating_add(w);
1274                        if !ch.is_whitespace() {
1275                            non_whitespace_cells = non_whitespace_cells.saturating_add(w);
1276                        }
1277                        let entry = dominant_style_counts
1278                            .entry(FOLD_PLACEHOLDER_STYLE_ID)
1279                            .or_insert(0);
1280                        *entry = entry.saturating_add(w);
1281                    }
1282                }
1283
1284                let dominant_style = dominant_style_counts
1285                    .into_iter()
1286                    .max_by(|a, b| a.1.cmp(&b.1).then_with(|| b.0.cmp(&a.0)))
1287                    .map(|(style, _)| style);
1288
1289                grid.lines.push(MinimapLine {
1290                    logical_line_index: logical_line,
1291                    visual_in_logical: visual_in_line,
1292                    char_offset_start: line_start_offset.saturating_add(segment_start_col),
1293                    char_offset_end: line_start_offset.saturating_add(segment_end_col),
1294                    total_cells,
1295                    non_whitespace_cells,
1296                    dominant_style,
1297                    is_fold_placeholder_appended: placeholder_appended,
1298                });
1299
1300                current_visual = current_visual.saturating_add(1);
1301                visual_in_line = visual_in_line.saturating_add(1);
1302                if visual_in_line >= span.visual_line_count {
1303                    span_idx = span_idx.saturating_add(1);
1304                    visual_in_line = 0;
1305                }
1306            }
1307
1308            grid
1309        })
1310    }
1311
1312    /// Get a decoration-aware composed grid snapshot (by composed visual line).
1313    ///
1314    /// This is an **optional** snapshot path that injects:
1315    /// - inline virtual text (`DecorationPlacement::{Before,After}`), e.g. inlay hints
1316    /// - above-line virtual text (`DecorationPlacement::AboveLine`), e.g. code lens
1317    ///
1318    /// Notes:
1319    /// - Wrapping is still computed from the underlying document text only.
1320    /// - Virtual text can therefore extend past the viewport width; hosts may clip.
1321    /// - Each [`ComposedCell`] carries its origin (`Document` vs `Virtual`) so hosts can map
1322    ///   interactions back to document offsets without re-implementing layout.
1323    pub fn get_headless_grid_composed(
1324        &self,
1325        start_visual_row: usize,
1326        count: usize,
1327    ) -> ComposedGrid {
1328        let mut grid = ComposedGrid::new(start_visual_row, count);
1329        if count == 0 {
1330            return grid;
1331        }
1332
1333        #[derive(Debug, Clone)]
1334        struct VirtualText {
1335            anchor: usize,
1336            text: String,
1337            styles: Vec<StyleId>,
1338        }
1339
1340        // Collect virtual text decorations from all layers.
1341        let mut inline_before: HashMap<usize, Vec<VirtualText>> = HashMap::new();
1342        let mut inline_after: HashMap<usize, Vec<VirtualText>> = HashMap::new();
1343        let mut above_by_line: BTreeMap<usize, Vec<VirtualText>> = BTreeMap::new();
1344
1345        for decorations in self.decorations.values() {
1346            for deco in decorations {
1347                let Some(text) = deco.text.as_ref() else {
1348                    continue;
1349                };
1350                if text.is_empty() {
1351                    continue;
1352                }
1353
1354                let anchor = match deco.placement {
1355                    DecorationPlacement::After => deco.range.end,
1356                    DecorationPlacement::Before | DecorationPlacement::AboveLine => {
1357                        deco.range.start
1358                    }
1359                };
1360                let vt = VirtualText {
1361                    anchor,
1362                    text: text.clone(),
1363                    styles: deco.styles.clone(),
1364                };
1365
1366                match deco.placement {
1367                    DecorationPlacement::Before => {
1368                        inline_before.entry(anchor).or_default().push(vt);
1369                    }
1370                    DecorationPlacement::After => {
1371                        inline_after.entry(anchor).or_default().push(vt);
1372                    }
1373                    DecorationPlacement::AboveLine => {
1374                        let line = self.line_index.char_offset_to_position(anchor).0;
1375                        above_by_line.entry(line).or_default().push(vt);
1376                    }
1377                }
1378            }
1379        }
1380
1381        // Compute the total composed visual line count for bounds checking.
1382        let regions = self.folding_manager.regions();
1383        let mut total_composed = 0usize;
1384        for logical_line in 0..self.layout_engine.logical_line_count() {
1385            if Self::is_logical_line_hidden(regions, logical_line) {
1386                continue;
1387            }
1388
1389            if let Some(above) = above_by_line.get(&logical_line) {
1390                total_composed = total_composed.saturating_add(above.len());
1391            }
1392
1393            total_composed = total_composed.saturating_add(
1394                self.layout_engine
1395                    .get_line_layout(logical_line)
1396                    .map(|l| l.visual_line_count)
1397                    .unwrap_or(1),
1398            );
1399        }
1400
1401        if start_visual_row >= total_composed {
1402            return grid;
1403        }
1404
1405        let end_visual = start_visual_row.saturating_add(count).min(total_composed);
1406        let tab_width = self.layout_engine.tab_width();
1407
1408        let mut current_visual = 0usize;
1409
1410        for logical_line in 0..self.layout_engine.logical_line_count() {
1411            if Self::is_logical_line_hidden(regions, logical_line) {
1412                continue;
1413            }
1414
1415            // Above-line virtual text (e.g. code lens).
1416            if let Some(above) = above_by_line.get(&logical_line) {
1417                for vt in above {
1418                    if current_visual >= end_visual {
1419                        return grid;
1420                    }
1421
1422                    if current_visual >= start_visual_row {
1423                        let mut x_render = 0usize;
1424                        let mut cells: Vec<ComposedCell> = Vec::new();
1425                        for ch in vt.text.chars() {
1426                            let w = cell_width_at(ch, x_render, tab_width);
1427                            x_render = x_render.saturating_add(w);
1428                            cells.push(ComposedCell {
1429                                ch,
1430                                width: w,
1431                                styles: vt.styles.clone(),
1432                                source: ComposedCellSource::Virtual {
1433                                    anchor_offset: vt.anchor,
1434                                },
1435                            });
1436                        }
1437
1438                        grid.lines.push(ComposedLine {
1439                            kind: ComposedLineKind::VirtualAboveLine { logical_line },
1440                            cells,
1441                        });
1442                    }
1443
1444                    current_visual = current_visual.saturating_add(1);
1445                }
1446            }
1447
1448            let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
1449                continue;
1450            };
1451
1452            let line_text = self
1453                .line_index
1454                .get_line_text(logical_line)
1455                .unwrap_or_default();
1456            let line_char_len = line_text.chars().count();
1457            let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
1458
1459            for visual_in_line in 0..layout.visual_line_count {
1460                if current_visual >= end_visual {
1461                    return grid;
1462                }
1463
1464                if current_visual < start_visual_row {
1465                    current_visual = current_visual.saturating_add(1);
1466                    continue;
1467                }
1468
1469                let segment_start_col = if visual_in_line == 0 {
1470                    0
1471                } else {
1472                    layout
1473                        .wrap_points
1474                        .get(visual_in_line - 1)
1475                        .map(|wp| wp.char_index)
1476                        .unwrap_or(0)
1477                        .min(line_char_len)
1478                };
1479
1480                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
1481                    layout.wrap_points[visual_in_line]
1482                        .char_index
1483                        .min(line_char_len)
1484                } else {
1485                    line_char_len
1486                };
1487
1488                let segment_start_offset = line_start_offset + segment_start_col;
1489
1490                let mut cells: Vec<ComposedCell> = Vec::new();
1491
1492                let mut x_render = 0usize;
1493                if visual_in_line > 0 {
1494                    let indent_cells = wrap_indent_cells_for_line_text(
1495                        &line_text,
1496                        self.layout_engine.wrap_indent(),
1497                        self.viewport_width,
1498                        tab_width,
1499                    );
1500                    x_render = x_render.saturating_add(indent_cells);
1501                    for _ in 0..indent_cells {
1502                        cells.push(ComposedCell {
1503                            ch: ' ',
1504                            width: 1,
1505                            styles: Vec::new(),
1506                            source: ComposedCellSource::Virtual {
1507                                anchor_offset: segment_start_offset,
1508                            },
1509                        });
1510                    }
1511                }
1512
1513                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1514
1515                let push_virtual = |anchor: usize,
1516                                    list: &[VirtualText],
1517                                    cells: &mut Vec<ComposedCell>,
1518                                    x_render: &mut usize| {
1519                    for vt in list {
1520                        for ch in vt.text.chars() {
1521                            let w = cell_width_at(ch, *x_render, tab_width);
1522                            *x_render = x_render.saturating_add(w);
1523                            cells.push(ComposedCell {
1524                                ch,
1525                                width: w,
1526                                styles: vt.styles.clone(),
1527                                source: ComposedCellSource::Virtual {
1528                                    anchor_offset: anchor,
1529                                },
1530                            });
1531                        }
1532                    }
1533                };
1534
1535                for (col, ch) in line_text
1536                    .chars()
1537                    .enumerate()
1538                    .skip(segment_start_col)
1539                    .take(segment_end_col.saturating_sub(segment_start_col))
1540                {
1541                    let offset = line_start_offset + col;
1542
1543                    if let Some(list) = inline_before.get(&offset) {
1544                        push_virtual(offset, list, &mut cells, &mut x_render);
1545                    }
1546                    if let Some(list) = inline_after.get(&offset) {
1547                        push_virtual(offset, list, &mut cells, &mut x_render);
1548                    }
1549
1550                    let styles = self.styles_at_offset(offset);
1551                    let w = cell_width_at(ch, x_in_line, tab_width);
1552                    x_in_line = x_in_line.saturating_add(w);
1553                    x_render = x_render.saturating_add(w);
1554                    cells.push(ComposedCell {
1555                        ch,
1556                        width: w,
1557                        styles,
1558                        source: ComposedCellSource::Document { offset },
1559                    });
1560                }
1561
1562                // End-of-line inline virtual text (only on the last visual segment).
1563                if visual_in_line + 1 == layout.visual_line_count {
1564                    let eol_offset = line_start_offset + line_char_len;
1565                    if let Some(list) = inline_before.get(&eol_offset) {
1566                        push_virtual(eol_offset, list, &mut cells, &mut x_render);
1567                    }
1568                    if let Some(list) = inline_after.get(&eol_offset) {
1569                        push_virtual(eol_offset, list, &mut cells, &mut x_render);
1570                    }
1571
1572                    // For collapsed folding start line, append placeholder to the last segment.
1573                    if let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
1574                        && !region.placeholder.is_empty()
1575                    {
1576                        if !cells.is_empty() {
1577                            x_render = x_render.saturating_add(char_width(' '));
1578                            cells.push(ComposedCell {
1579                                ch: ' ',
1580                                width: char_width(' '),
1581                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
1582                                source: ComposedCellSource::Virtual {
1583                                    anchor_offset: eol_offset,
1584                                },
1585                            });
1586                        }
1587                        for ch in region.placeholder.chars() {
1588                            let w = cell_width_at(ch, x_render, tab_width);
1589                            x_render = x_render.saturating_add(w);
1590                            cells.push(ComposedCell {
1591                                ch,
1592                                width: w,
1593                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
1594                                source: ComposedCellSource::Virtual {
1595                                    anchor_offset: eol_offset,
1596                                },
1597                            });
1598                        }
1599                    }
1600                }
1601
1602                grid.lines.push(ComposedLine {
1603                    kind: ComposedLineKind::Document {
1604                        logical_line,
1605                        visual_in_logical: visual_in_line,
1606                    },
1607                    cells,
1608                });
1609
1610                current_visual = current_visual.saturating_add(1);
1611            }
1612        }
1613
1614        grid
1615    }
1616
1617    /// Get total visual line count (considering soft wrapping + folding).
1618    pub fn visual_line_count(&self) -> usize {
1619        self.with_visual_row_index(|index| index.total_visual_lines())
1620    }
1621
1622    /// Map visual line number back to (logical_line, visual_in_logical), considering folding.
1623    pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
1624        self.with_visual_row_index(|index| {
1625            if index.total_visual_lines() == 0 {
1626                return (0, 0);
1627            }
1628            let clamped_visual = visual_line.min(index.total_visual_lines().saturating_sub(1));
1629            index
1630                .span_for_visual_row(clamped_visual)
1631                .map(|(span, visual_in_logical)| (span.logical_line, visual_in_logical))
1632                .unwrap_or((0, 0))
1633        })
1634    }
1635
1636    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
1637    pub fn logical_position_to_visual(
1638        &self,
1639        logical_line: usize,
1640        column: usize,
1641    ) -> Option<(usize, usize)> {
1642        let regions = self.folding_manager.regions();
1643        let logical_line = Self::closest_visible_line(regions, logical_line)?;
1644        let visual_start = self.visual_start_for_logical_line(logical_line)?;
1645
1646        let tab_width = self.layout_engine.tab_width();
1647
1648        let layout = self.layout_engine.get_line_layout(logical_line)?;
1649        let line_text = self
1650            .line_index
1651            .get_line_text(logical_line)
1652            .unwrap_or_default();
1653
1654        let line_char_len = line_text.chars().count();
1655        let column = column.min(line_char_len);
1656
1657        let mut wrapped_offset = 0usize;
1658        let mut segment_start_col = 0usize;
1659        for wrap_point in &layout.wrap_points {
1660            if column >= wrap_point.char_index {
1661                wrapped_offset = wrapped_offset.saturating_add(1);
1662                segment_start_col = wrap_point.char_index;
1663            } else {
1664                break;
1665            }
1666        }
1667
1668        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1669        let mut x_in_line = seg_start_x_in_line;
1670        let mut x_in_segment = 0usize;
1671        for ch in line_text
1672            .chars()
1673            .skip(segment_start_col)
1674            .take(column.saturating_sub(segment_start_col))
1675        {
1676            let w = cell_width_at(ch, x_in_line, tab_width);
1677            x_in_line = x_in_line.saturating_add(w);
1678            x_in_segment = x_in_segment.saturating_add(w);
1679        }
1680
1681        let indent = if wrapped_offset == 0 {
1682            0
1683        } else {
1684            wrap_indent_cells_for_line_text(
1685                &line_text,
1686                self.layout_engine.wrap_indent(),
1687                self.viewport_width,
1688                tab_width,
1689            )
1690        };
1691
1692        Some((
1693            visual_start.saturating_add(wrapped_offset),
1694            indent.saturating_add(x_in_segment),
1695        ))
1696    }
1697
1698    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
1699    ///
1700    /// Difference from [`logical_position_to_visual`](Self::logical_position_to_visual) is that it allows `column`
1701    /// to exceed the line end: the exceeding part is treated as `' '` (width=1) virtual spaces, suitable for rectangular selection / column editing.
1702    pub fn logical_position_to_visual_allow_virtual(
1703        &self,
1704        logical_line: usize,
1705        column: usize,
1706    ) -> Option<(usize, usize)> {
1707        let regions = self.folding_manager.regions();
1708        let logical_line = Self::closest_visible_line(regions, logical_line)?;
1709        let visual_start = self.visual_start_for_logical_line(logical_line)?;
1710
1711        let tab_width = self.layout_engine.tab_width();
1712
1713        let layout = self.layout_engine.get_line_layout(logical_line)?;
1714        let line_text = self
1715            .line_index
1716            .get_line_text(logical_line)
1717            .unwrap_or_default();
1718
1719        let line_char_len = line_text.chars().count();
1720        let clamped_column = column.min(line_char_len);
1721
1722        let mut wrapped_offset = 0usize;
1723        let mut segment_start_col = 0usize;
1724        for wrap_point in &layout.wrap_points {
1725            if clamped_column >= wrap_point.char_index {
1726                wrapped_offset = wrapped_offset.saturating_add(1);
1727                segment_start_col = wrap_point.char_index;
1728            } else {
1729                break;
1730            }
1731        }
1732
1733        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1734        let mut x_in_line = seg_start_x_in_line;
1735        let mut x_in_segment = 0usize;
1736        for ch in line_text
1737            .chars()
1738            .skip(segment_start_col)
1739            .take(clamped_column.saturating_sub(segment_start_col))
1740        {
1741            let w = cell_width_at(ch, x_in_line, tab_width);
1742            x_in_line = x_in_line.saturating_add(w);
1743            x_in_segment = x_in_segment.saturating_add(w);
1744        }
1745
1746        let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
1747
1748        let indent = if wrapped_offset == 0 {
1749            0
1750        } else {
1751            wrap_indent_cells_for_line_text(
1752                &line_text,
1753                self.layout_engine.wrap_indent(),
1754                self.viewport_width,
1755                tab_width,
1756            )
1757        };
1758
1759        Some((
1760            visual_start.saturating_add(wrapped_offset),
1761            indent.saturating_add(x_in_segment),
1762        ))
1763    }
1764
1765    /// Convert visual coordinates (global visual row + x in cells) back to logical `(line, column)`.
1766    ///
1767    /// - `visual_row` is the global visual row (after soft wrapping and folding).
1768    /// - `x_in_cells` is the cell offset within that visual row (0-based).
1769    ///
1770    /// Returns `None` if layout information is unavailable.
1771    pub fn visual_position_to_logical(
1772        &self,
1773        visual_row: usize,
1774        x_in_cells: usize,
1775    ) -> Option<Position> {
1776        let total_visual = self.visual_line_count();
1777        if total_visual == 0 {
1778            return Some(Position::new(0, 0));
1779        }
1780
1781        let clamped_row = visual_row.min(total_visual.saturating_sub(1));
1782        let (logical_line, visual_in_logical) = self.visual_to_logical_line(clamped_row);
1783
1784        let layout = self.layout_engine.get_line_layout(logical_line)?;
1785        let line_text = self
1786            .line_index
1787            .get_line_text(logical_line)
1788            .unwrap_or_default();
1789        let line_char_len = line_text.chars().count();
1790
1791        let segment_start_col = if visual_in_logical == 0 {
1792            0
1793        } else {
1794            layout
1795                .wrap_points
1796                .get(visual_in_logical - 1)
1797                .map(|wp| wp.char_index)
1798                .unwrap_or(0)
1799        };
1800
1801        let segment_end_col = layout
1802            .wrap_points
1803            .get(visual_in_logical)
1804            .map(|wp| wp.char_index)
1805            .unwrap_or(line_char_len)
1806            .max(segment_start_col)
1807            .min(line_char_len);
1808
1809        let tab_width = self.layout_engine.tab_width();
1810        let x_in_cells = if visual_in_logical == 0 {
1811            x_in_cells
1812        } else {
1813            let indent = wrap_indent_cells_for_line_text(
1814                &line_text,
1815                self.layout_engine.wrap_indent(),
1816                self.viewport_width,
1817                tab_width,
1818            );
1819            x_in_cells.saturating_sub(indent)
1820        };
1821        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1822        let mut x_in_line = seg_start_x_in_line;
1823        let mut x_in_segment = 0usize;
1824        let mut column = segment_start_col;
1825
1826        for (char_idx, ch) in line_text.chars().enumerate().skip(segment_start_col) {
1827            if char_idx >= segment_end_col {
1828                break;
1829            }
1830
1831            let w = cell_width_at(ch, x_in_line, tab_width);
1832            if x_in_segment.saturating_add(w) > x_in_cells {
1833                break;
1834            }
1835
1836            x_in_line = x_in_line.saturating_add(w);
1837            x_in_segment = x_in_segment.saturating_add(w);
1838            column = column.saturating_add(1);
1839        }
1840
1841        Some(Position::new(logical_line, column))
1842    }
1843
1844    fn visual_start_for_logical_line(&self, logical_line: usize) -> Option<usize> {
1845        if logical_line >= self.layout_engine.logical_line_count() {
1846            return None;
1847        }
1848        self.with_visual_row_index(|index| {
1849            index
1850                .span_for_logical_line(logical_line)
1851                .map(|span| span.start_visual_row)
1852        })
1853    }
1854
1855    fn is_logical_line_hidden(regions: &[FoldRegion], logical_line: usize) -> bool {
1856        regions.iter().any(|region| {
1857            region.is_collapsed
1858                && logical_line > region.start_line
1859                && logical_line <= region.end_line
1860        })
1861    }
1862
1863    fn collapsed_region_starting_at(
1864        regions: &[FoldRegion],
1865        start_line: usize,
1866    ) -> Option<&FoldRegion> {
1867        regions
1868            .iter()
1869            .filter(|region| {
1870                region.is_collapsed
1871                    && region.start_line == start_line
1872                    && region.end_line > start_line
1873            })
1874            .min_by_key(|region| region.end_line)
1875    }
1876
1877    fn closest_visible_line(regions: &[FoldRegion], logical_line: usize) -> Option<usize> {
1878        let mut line = logical_line;
1879        if regions.is_empty() {
1880            return Some(line);
1881        }
1882
1883        while Self::is_logical_line_hidden(regions, line) {
1884            let Some(start) = regions
1885                .iter()
1886                .filter(|region| {
1887                    region.is_collapsed && line > region.start_line && line <= region.end_line
1888                })
1889                .map(|region| region.start_line)
1890                .max()
1891            else {
1892                break;
1893            };
1894            line = start;
1895        }
1896
1897        if Self::is_logical_line_hidden(regions, line) {
1898            None
1899        } else {
1900            Some(line)
1901        }
1902    }
1903
1904    fn styles_at_offset(&self, offset: usize) -> Vec<StyleId> {
1905        let mut styles: Vec<StyleId> = self
1906            .interval_tree
1907            .query_point(offset)
1908            .iter()
1909            .map(|interval| interval.style_id)
1910            .collect();
1911
1912        for tree in self.style_layers.values() {
1913            styles.extend(
1914                tree.query_point(offset)
1915                    .iter()
1916                    .map(|interval| interval.style_id),
1917            );
1918        }
1919
1920        styles.sort_unstable();
1921        styles.dedup();
1922        styles
1923    }
1924}
1925
1926/// Command executor
1927///
1928/// `CommandExecutor` is the main interface for the editor, responsible for:
1929///
1930/// - Execute various editor commands
1931/// - Maintain command history
1932/// - Handle errors and exceptions
1933/// - Ensure editor state consistency
1934///
1935/// # Command Types
1936///
1937/// - [`EditCommand`] - Text insertion, deletion, replacement
1938/// - [`CursorCommand`] - Cursor movement, selection operations
1939/// - [`ViewCommand`] - Viewport management and scroll control
1940/// - [`StyleCommand`] - Style and folding management
1941///
1942/// # Example
1943///
1944/// ```rust
1945/// use editor_core::{CommandExecutor, Command, EditCommand, CursorCommand, Position};
1946///
1947/// let mut executor = CommandExecutor::empty(80);
1948///
1949/// // Insert text
1950/// executor.execute(Command::Edit(EditCommand::Insert {
1951///     offset: 0,
1952///     text: "fn main() {}".to_string(),
1953/// })).unwrap();
1954///
1955/// // Move cursor
1956/// executor.execute(Command::Cursor(CursorCommand::MoveTo {
1957///     line: 0,
1958///     column: 3,
1959/// })).unwrap();
1960///
1961/// assert_eq!(executor.editor().cursor_position(), Position::new(0, 3));
1962/// ```
1963pub struct CommandExecutor {
1964    /// Editor Core
1965    editor: EditorCore,
1966    /// Command history
1967    command_history: Vec<Command>,
1968    /// Undo/redo manager (only records CommandExecutor edit commands executed via)
1969    undo_redo: UndoRedoManager,
1970    /// Controls how [`EditCommand::InsertTab`] behaves.
1971    tab_key_behavior: TabKeyBehavior,
1972    /// Preferred line ending for saving (internal storage is always LF).
1973    line_ending: LineEnding,
1974    /// Sticky x position for visual-row cursor movement (in cells).
1975    preferred_x_cells: Option<usize>,
1976    /// Structured delta for the last executed text modification (cleared on each `execute()` call).
1977    last_text_delta: Option<TextDelta>,
1978}
1979
1980impl CommandExecutor {
1981    /// Create a new command executor
1982    pub fn new(text: &str, viewport_width: usize) -> Self {
1983        Self {
1984            editor: EditorCore::new(text, viewport_width),
1985            command_history: Vec::new(),
1986            undo_redo: UndoRedoManager::new(1000),
1987            tab_key_behavior: TabKeyBehavior::Tab,
1988            line_ending: LineEnding::detect_in_text(text),
1989            preferred_x_cells: None,
1990            last_text_delta: None,
1991        }
1992    }
1993
1994    /// Create an empty command executor
1995    pub fn empty(viewport_width: usize) -> Self {
1996        Self::new("", viewport_width)
1997    }
1998
1999    /// Execute command
2000    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
2001        self.last_text_delta = None;
2002
2003        // Save command to history
2004        self.command_history.push(command.clone());
2005
2006        let affects_visual_rows = matches!(
2007            &command,
2008            Command::Edit(_)
2009                | Command::View(
2010                    ViewCommand::SetViewportWidth { .. }
2011                        | ViewCommand::SetWrapMode { .. }
2012                        | ViewCommand::SetWrapIndent { .. }
2013                        | ViewCommand::SetTabWidth { .. }
2014                )
2015                | Command::Style(
2016                    StyleCommand::Fold { .. }
2017                        | StyleCommand::Unfold { .. }
2018                        | StyleCommand::UnfoldAll
2019                )
2020        );
2021        if affects_visual_rows {
2022            self.editor.invalidate_visual_row_index_cache();
2023        }
2024
2025        // Undo grouping: any non-edit command ends the current coalescing group.
2026        if !matches!(command, Command::Edit(_)) {
2027            self.undo_redo.end_group();
2028        }
2029
2030        // Execute command
2031        match command {
2032            Command::Edit(edit_cmd) => self.execute_edit(edit_cmd),
2033            Command::Cursor(cursor_cmd) => self.execute_cursor(cursor_cmd),
2034            Command::View(view_cmd) => self.execute_view(view_cmd),
2035            Command::Style(style_cmd) => self.execute_style(style_cmd),
2036        }
2037    }
2038
2039    /// Get the structured text delta produced by the last successful `execute()` call, if any.
2040    pub fn last_text_delta(&self) -> Option<&TextDelta> {
2041        self.last_text_delta.as_ref()
2042    }
2043
2044    /// Take the structured text delta produced by the last successful `execute()` call, if any.
2045    pub fn take_last_text_delta(&mut self) -> Option<TextDelta> {
2046        self.last_text_delta.take()
2047    }
2048
2049    /// Batch execute commands (transactional)
2050    pub fn execute_batch(
2051        &mut self,
2052        commands: Vec<Command>,
2053    ) -> Result<Vec<CommandResult>, CommandError> {
2054        let mut results = Vec::new();
2055
2056        for command in commands {
2057            let result = self.execute(command)?;
2058            results.push(result);
2059        }
2060
2061        Ok(results)
2062    }
2063
2064    /// Get command history
2065    pub fn get_command_history(&self) -> &[Command] {
2066        &self.command_history
2067    }
2068
2069    /// Can undo
2070    pub fn can_undo(&self) -> bool {
2071        self.undo_redo.can_undo()
2072    }
2073
2074    /// Can redo
2075    pub fn can_redo(&self) -> bool {
2076        self.undo_redo.can_redo()
2077    }
2078
2079    /// Undo stack depth (counted by undo steps; grouped undo may pop multiple steps at once)
2080    pub fn undo_depth(&self) -> usize {
2081        self.undo_redo.undo_depth()
2082    }
2083
2084    /// Redo stack depth (counted by undo steps)
2085    pub fn redo_depth(&self) -> usize {
2086        self.undo_redo.redo_depth()
2087    }
2088
2089    /// Currently open undo group ID (for insert coalescing only)
2090    pub fn current_change_group(&self) -> Option<usize> {
2091        self.undo_redo.current_group_id()
2092    }
2093
2094    /// Whether current state is at clean point (for dirty tracking)
2095    pub fn is_clean(&self) -> bool {
2096        self.undo_redo.is_clean()
2097    }
2098
2099    /// Mark current state as clean point (call after saving file)
2100    pub fn mark_clean(&mut self) {
2101        self.undo_redo.mark_clean();
2102    }
2103
2104    /// Get a reference to the Editor Core
2105    pub fn editor(&self) -> &EditorCore {
2106        &self.editor
2107    }
2108
2109    /// Get a mutable reference to the Editor Core
2110    pub fn editor_mut(&mut self) -> &mut EditorCore {
2111        &mut self.editor
2112    }
2113
2114    /// Get current tab key behavior used by [`EditCommand::InsertTab`].
2115    pub fn tab_key_behavior(&self) -> TabKeyBehavior {
2116        self.tab_key_behavior
2117    }
2118
2119    /// Set tab key behavior used by [`EditCommand::InsertTab`].
2120    pub fn set_tab_key_behavior(&mut self, behavior: TabKeyBehavior) {
2121        self.tab_key_behavior = behavior;
2122    }
2123
2124    /// Get the sticky x position (in cells) used by visual-row cursor movement.
2125    pub fn preferred_x_cells(&self) -> Option<usize> {
2126        self.preferred_x_cells
2127    }
2128
2129    /// Set the sticky x position (in cells) used by visual-row cursor movement.
2130    pub fn set_preferred_x_cells(&mut self, preferred_x_cells: Option<usize>) {
2131        self.preferred_x_cells = preferred_x_cells;
2132    }
2133
2134    /// Get the preferred line ending for saving this document.
2135    pub fn line_ending(&self) -> LineEnding {
2136        self.line_ending
2137    }
2138
2139    /// Override the preferred line ending for saving this document.
2140    pub fn set_line_ending(&mut self, line_ending: LineEnding) {
2141        self.line_ending = line_ending;
2142    }
2143
2144    // Private method: execute edit command
2145    fn execute_edit(&mut self, command: EditCommand) -> Result<CommandResult, CommandError> {
2146        match command {
2147            EditCommand::Undo => self.execute_undo_command(),
2148            EditCommand::Redo => self.execute_redo_command(),
2149            EditCommand::EndUndoGroup => {
2150                self.undo_redo.end_group();
2151                Ok(CommandResult::Success)
2152            }
2153            EditCommand::ReplaceCurrent {
2154                query,
2155                replacement,
2156                options,
2157            } => self.execute_replace_current_command(query, replacement, options),
2158            EditCommand::ReplaceAll {
2159                query,
2160                replacement,
2161                options,
2162            } => self.execute_replace_all_command(query, replacement, options),
2163            EditCommand::DeleteToPrevTabStop => self.execute_delete_to_prev_tab_stop_command(),
2164            EditCommand::DeleteGraphemeBack => {
2165                self.execute_delete_by_boundary_command(false, TextBoundary::Grapheme)
2166            }
2167            EditCommand::DeleteGraphemeForward => {
2168                self.execute_delete_by_boundary_command(true, TextBoundary::Grapheme)
2169            }
2170            EditCommand::DeleteWordBack => {
2171                self.execute_delete_by_boundary_command(false, TextBoundary::Word)
2172            }
2173            EditCommand::DeleteWordForward => {
2174                self.execute_delete_by_boundary_command(true, TextBoundary::Word)
2175            }
2176            EditCommand::Backspace => self.execute_backspace_command(),
2177            EditCommand::DeleteForward => self.execute_delete_forward_command(),
2178            EditCommand::InsertText { text } => self.execute_insert_text_command(text),
2179            EditCommand::InsertTab => self.execute_insert_tab_command(),
2180            EditCommand::InsertNewline { auto_indent } => {
2181                self.execute_insert_newline_command(auto_indent)
2182            }
2183            EditCommand::Indent => self.execute_indent_command(false),
2184            EditCommand::Outdent => self.execute_indent_command(true),
2185            EditCommand::DuplicateLines => self.execute_duplicate_lines_command(),
2186            EditCommand::DeleteLines => self.execute_delete_lines_command(),
2187            EditCommand::MoveLinesUp => self.execute_move_lines_command(true),
2188            EditCommand::MoveLinesDown => self.execute_move_lines_command(false),
2189            EditCommand::JoinLines => self.execute_join_lines_command(),
2190            EditCommand::SplitLine => self.execute_insert_newline_command(false),
2191            EditCommand::ToggleComment { config } => self.execute_toggle_comment_command(config),
2192            EditCommand::ApplyTextEdits { edits } => self.execute_apply_text_edits_command(edits),
2193            EditCommand::Insert { offset, text } => self.execute_insert_command(offset, text),
2194            EditCommand::Delete { start, length } => self.execute_delete_command(start, length),
2195            EditCommand::Replace {
2196                start,
2197                length,
2198                text,
2199            } => self.execute_replace_command(start, length, text),
2200        }
2201    }
2202
2203    fn execute_undo_command(&mut self) -> Result<CommandResult, CommandError> {
2204        self.undo_redo.end_group();
2205        if !self.undo_redo.can_undo() {
2206            return Err(CommandError::Other("Nothing to undo".to_string()));
2207        }
2208
2209        let before_char_count = self.editor.piece_table.char_count();
2210        let steps = self
2211            .undo_redo
2212            .pop_undo_group()
2213            .ok_or_else(|| CommandError::Other("Nothing to undo".to_string()))?;
2214
2215        let undo_group_id = steps.first().map(|s| s.group_id);
2216        let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
2217
2218        for step in &steps {
2219            let mut step_edits: Vec<TextDeltaEdit> = step
2220                .edits
2221                .iter()
2222                .map(|edit| TextDeltaEdit {
2223                    start: edit.start_after,
2224                    deleted_text: edit.inserted_text.clone(),
2225                    inserted_text: edit.deleted_text.clone(),
2226                })
2227                .collect();
2228            step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2229            delta_edits.extend(step_edits);
2230
2231            self.apply_undo_edits(&step.edits)?;
2232            self.restore_selection_set(step.before_selection.clone());
2233        }
2234
2235        // Move steps to redo stack in the same pop order (newest->oldest) so redo pops oldest first.
2236        for step in steps {
2237            self.undo_redo.redo_stack.push(step);
2238        }
2239
2240        self.last_text_delta = Some(TextDelta {
2241            before_char_count,
2242            after_char_count: self.editor.piece_table.char_count(),
2243            edits: delta_edits,
2244            undo_group_id,
2245        });
2246
2247        Ok(CommandResult::Success)
2248    }
2249
2250    fn execute_redo_command(&mut self) -> Result<CommandResult, CommandError> {
2251        self.undo_redo.end_group();
2252        if !self.undo_redo.can_redo() {
2253            return Err(CommandError::Other("Nothing to redo".to_string()));
2254        }
2255
2256        let before_char_count = self.editor.piece_table.char_count();
2257        let steps = self
2258            .undo_redo
2259            .pop_redo_group()
2260            .ok_or_else(|| CommandError::Other("Nothing to redo".to_string()))?;
2261
2262        let undo_group_id = steps.first().map(|s| s.group_id);
2263        let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
2264
2265        for step in &steps {
2266            let mut step_edits: Vec<TextDeltaEdit> = step
2267                .edits
2268                .iter()
2269                .map(|edit| TextDeltaEdit {
2270                    start: edit.start_before,
2271                    deleted_text: edit.deleted_text.clone(),
2272                    inserted_text: edit.inserted_text.clone(),
2273                })
2274                .collect();
2275            step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2276            delta_edits.extend(step_edits);
2277
2278            self.apply_redo_edits(&step.edits)?;
2279            self.restore_selection_set(step.after_selection.clone());
2280        }
2281
2282        // Reapplied steps return to undo stack in the same order (oldest->newest).
2283        for step in steps {
2284            self.undo_redo.undo_stack.push(step);
2285        }
2286
2287        self.last_text_delta = Some(TextDelta {
2288            before_char_count,
2289            after_char_count: self.editor.piece_table.char_count(),
2290            edits: delta_edits,
2291            undo_group_id,
2292        });
2293
2294        Ok(CommandResult::Success)
2295    }
2296
2297    fn execute_insert_text_command(&mut self, text: String) -> Result<CommandResult, CommandError> {
2298        if text.is_empty() {
2299            return Ok(CommandResult::Success);
2300        }
2301
2302        let text = crate::text::normalize_crlf_to_lf_string(text);
2303        let before_char_count = self.editor.piece_table.char_count();
2304        let before_selection = self.snapshot_selection_set();
2305
2306        // Build canonical selection set (primary + secondary), VSCode-like: edits are applied
2307        // "simultaneously" by computing ranges in the original document and mutating in
2308        // descending offset order.
2309        let mut selections: Vec<Selection> =
2310            Vec::with_capacity(1 + self.editor.secondary_selections.len());
2311        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
2312            start: self.editor.cursor_position,
2313            end: self.editor.cursor_position,
2314            direction: SelectionDirection::Forward,
2315        });
2316        selections.push(primary_selection);
2317        selections.extend(self.editor.secondary_selections.iter().cloned());
2318
2319        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
2320
2321        let text_char_len = text.chars().count();
2322
2323        struct Op {
2324            selection_index: usize,
2325            start_offset: usize,
2326            start_after: usize,
2327            delete_len: usize,
2328            deleted_text: String,
2329            insert_text: String,
2330            insert_char_len: usize,
2331        }
2332
2333        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
2334
2335        for (selection_index, selection) in selections.iter().enumerate() {
2336            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
2337                (selection.start, selection.end)
2338            } else {
2339                (selection.end, selection.start)
2340            };
2341
2342            let (start_offset, start_pad) =
2343                self.position_to_char_offset_and_virtual_pad(range_start_pos);
2344            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
2345
2346            let delete_len = end_offset.saturating_sub(start_offset);
2347            let insert_char_len = start_pad + text_char_len;
2348
2349            let deleted_text = if delete_len == 0 {
2350                String::new()
2351            } else {
2352                self.editor.piece_table.get_range(start_offset, delete_len)
2353            };
2354
2355            let mut insert_text = String::with_capacity(text.len() + start_pad);
2356            for _ in 0..start_pad {
2357                insert_text.push(' ');
2358            }
2359            insert_text.push_str(&text);
2360
2361            ops.push(Op {
2362                selection_index,
2363                start_offset,
2364                start_after: start_offset,
2365                delete_len,
2366                deleted_text,
2367                insert_text,
2368                insert_char_len,
2369            });
2370        }
2371
2372        // Compute final caret offsets in the post-edit document (ascending order with delta),
2373        // while also recording each operation's start offset in the post-edit document.
2374        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2375        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2376
2377        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
2378        let mut delta: i64 = 0;
2379        for &idx in &asc_indices {
2380            let op = &mut ops[idx];
2381            let effective_start = (op.start_offset as i64 + delta) as usize;
2382            op.start_after = effective_start;
2383            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
2384            delta += op.insert_char_len as i64 - op.delete_len as i64;
2385        }
2386
2387        // Apply edits safely (descending offsets).
2388        let mut desc_indices = asc_indices;
2389        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2390
2391        for &idx in &desc_indices {
2392            let op = &ops[idx];
2393
2394            let edit_line = self
2395                .editor
2396                .line_index
2397                .char_offset_to_position(op.start_offset)
2398                .0;
2399            let deleted_newlines = op
2400                .deleted_text
2401                .as_bytes()
2402                .iter()
2403                .filter(|b| **b == b'\n')
2404                .count();
2405            let inserted_newlines = op
2406                .insert_text
2407                .as_bytes()
2408                .iter()
2409                .filter(|b| **b == b'\n')
2410                .count();
2411            let line_delta = inserted_newlines as isize - deleted_newlines as isize;
2412            if line_delta != 0 {
2413                self.editor
2414                    .folding_manager
2415                    .apply_line_delta(edit_line, line_delta);
2416            }
2417
2418            if op.delete_len > 0 {
2419                self.editor
2420                    .piece_table
2421                    .delete(op.start_offset, op.delete_len);
2422                self.editor
2423                    .interval_tree
2424                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2425                for layer_tree in self.editor.style_layers.values_mut() {
2426                    layer_tree
2427                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2428                }
2429            }
2430
2431            if !op.insert_text.is_empty() {
2432                self.editor
2433                    .piece_table
2434                    .insert(op.start_offset, &op.insert_text);
2435                self.editor
2436                    .interval_tree
2437                    .update_for_insertion(op.start_offset, op.insert_char_len);
2438                for layer_tree in self.editor.style_layers.values_mut() {
2439                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
2440                }
2441            }
2442
2443            self.apply_text_change_to_line_index_and_layout(
2444                op.start_offset,
2445                &op.deleted_text,
2446                &op.insert_text,
2447            );
2448        }
2449
2450        self.editor
2451            .folding_manager
2452            .clamp_to_line_count(self.editor.line_index.line_count());
2453
2454        // Update selection state: collapse to carets after typing.
2455        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2456        for offset in &caret_offsets {
2457            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2458            let pos = Position::new(line, column);
2459            new_carets.push(Selection {
2460                start: pos,
2461                end: pos,
2462                direction: SelectionDirection::Forward,
2463            });
2464        }
2465
2466        let (new_carets, new_primary_index) =
2467            crate::selection_set::normalize_selections(new_carets, primary_index);
2468        let primary = new_carets
2469            .get(new_primary_index)
2470            .cloned()
2471            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2472
2473        self.editor.cursor_position = primary.end;
2474        self.editor.selection = None;
2475        self.editor.secondary_selections = new_carets
2476            .into_iter()
2477            .enumerate()
2478            .filter_map(|(idx, sel)| {
2479                if idx == new_primary_index {
2480                    None
2481                } else {
2482                    Some(sel)
2483                }
2484            })
2485            .collect();
2486
2487        let after_selection = self.snapshot_selection_set();
2488
2489        let edits: Vec<TextEdit> = ops
2490            .into_iter()
2491            .map(|op| TextEdit {
2492                start_before: op.start_offset,
2493                start_after: op.start_after,
2494                deleted_text: op.deleted_text,
2495                inserted_text: op.insert_text,
2496            })
2497            .collect();
2498
2499        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
2500        let coalescible_insert = is_pure_insert && !text.contains('\n');
2501
2502        let mut delta_edits: Vec<TextDeltaEdit> = edits
2503            .iter()
2504            .map(|e| TextDeltaEdit {
2505                start: e.start_before,
2506                deleted_text: e.deleted_text.clone(),
2507                inserted_text: e.inserted_text.clone(),
2508            })
2509            .collect();
2510        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2511
2512        let step = UndoStep {
2513            group_id: 0,
2514            edits,
2515            before_selection,
2516            after_selection,
2517        };
2518        let group_id = self.undo_redo.push_step(step, coalescible_insert);
2519
2520        self.last_text_delta = Some(TextDelta {
2521            before_char_count,
2522            after_char_count: self.editor.piece_table.char_count(),
2523            edits: delta_edits,
2524            undo_group_id: Some(group_id),
2525        });
2526
2527        Ok(CommandResult::Success)
2528    }
2529
2530    fn execute_insert_tab_command(&mut self) -> Result<CommandResult, CommandError> {
2531        let before_char_count = self.editor.piece_table.char_count();
2532        let before_selection = self.snapshot_selection_set();
2533
2534        let mut selections: Vec<Selection> =
2535            Vec::with_capacity(1 + self.editor.secondary_selections.len());
2536        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
2537            start: self.editor.cursor_position,
2538            end: self.editor.cursor_position,
2539            direction: SelectionDirection::Forward,
2540        });
2541        selections.push(primary_selection);
2542        selections.extend(self.editor.secondary_selections.iter().cloned());
2543
2544        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
2545
2546        let tab_width = self.editor.layout_engine.tab_width();
2547
2548        struct Op {
2549            selection_index: usize,
2550            start_offset: usize,
2551            start_after: usize,
2552            delete_len: usize,
2553            deleted_text: String,
2554            insert_text: String,
2555            insert_char_len: usize,
2556        }
2557
2558        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
2559
2560        for (selection_index, selection) in selections.iter().enumerate() {
2561            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
2562                (selection.start, selection.end)
2563            } else {
2564                (selection.end, selection.start)
2565            };
2566
2567            let (start_offset, start_pad) =
2568                self.position_to_char_offset_and_virtual_pad(range_start_pos);
2569            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
2570
2571            let delete_len = end_offset.saturating_sub(start_offset);
2572
2573            let deleted_text = if delete_len == 0 {
2574                String::new()
2575            } else {
2576                self.editor.piece_table.get_range(start_offset, delete_len)
2577            };
2578
2579            // Compute cell X within the logical line at the insertion position (including virtual pad).
2580            let line_text = self
2581                .editor
2582                .line_index
2583                .get_line_text(range_start_pos.line)
2584                .unwrap_or_default();
2585            let line_char_len = line_text.chars().count();
2586            let clamped_col = range_start_pos.column.min(line_char_len);
2587            let x_in_line =
2588                visual_x_for_column(&line_text, clamped_col, tab_width).saturating_add(start_pad);
2589
2590            let mut insert_text = String::new();
2591            for _ in 0..start_pad {
2592                insert_text.push(' ');
2593            }
2594
2595            match self.tab_key_behavior {
2596                TabKeyBehavior::Tab => {
2597                    insert_text.push('\t');
2598                    ops.push(Op {
2599                        selection_index,
2600                        start_offset,
2601                        start_after: start_offset,
2602                        delete_len,
2603                        deleted_text,
2604                        insert_text,
2605                        insert_char_len: start_pad + 1,
2606                    });
2607                }
2608                TabKeyBehavior::Spaces => {
2609                    let tab_width = tab_width.max(1);
2610                    let rem = x_in_line % tab_width;
2611                    let spaces = tab_width - rem;
2612                    for _ in 0..spaces {
2613                        insert_text.push(' ');
2614                    }
2615
2616                    ops.push(Op {
2617                        selection_index,
2618                        start_offset,
2619                        start_after: start_offset,
2620                        delete_len,
2621                        deleted_text,
2622                        insert_text,
2623                        insert_char_len: start_pad + spaces,
2624                    });
2625                }
2626            }
2627        }
2628
2629        // Compute final caret offsets in the post-edit document (ascending order with delta),
2630        // while also recording each operation's start offset in the post-edit document.
2631        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2632        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2633
2634        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
2635        let mut delta: i64 = 0;
2636        for &idx in &asc_indices {
2637            let op = &mut ops[idx];
2638            let effective_start = (op.start_offset as i64 + delta) as usize;
2639            op.start_after = effective_start;
2640            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
2641            delta += op.insert_char_len as i64 - op.delete_len as i64;
2642        }
2643
2644        // Apply edits safely (descending offsets).
2645        let mut desc_indices = asc_indices;
2646        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2647
2648        for &idx in &desc_indices {
2649            let op = &ops[idx];
2650
2651            let edit_line = self
2652                .editor
2653                .line_index
2654                .char_offset_to_position(op.start_offset)
2655                .0;
2656            let deleted_newlines = op
2657                .deleted_text
2658                .as_bytes()
2659                .iter()
2660                .filter(|b| **b == b'\n')
2661                .count();
2662            let inserted_newlines = op
2663                .insert_text
2664                .as_bytes()
2665                .iter()
2666                .filter(|b| **b == b'\n')
2667                .count();
2668            let line_delta = inserted_newlines as isize - deleted_newlines as isize;
2669            if line_delta != 0 {
2670                self.editor
2671                    .folding_manager
2672                    .apply_line_delta(edit_line, line_delta);
2673            }
2674
2675            if op.delete_len > 0 {
2676                self.editor
2677                    .piece_table
2678                    .delete(op.start_offset, op.delete_len);
2679                self.editor
2680                    .interval_tree
2681                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2682                for layer_tree in self.editor.style_layers.values_mut() {
2683                    layer_tree
2684                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2685                }
2686            }
2687
2688            if !op.insert_text.is_empty() {
2689                self.editor
2690                    .piece_table
2691                    .insert(op.start_offset, &op.insert_text);
2692                self.editor
2693                    .interval_tree
2694                    .update_for_insertion(op.start_offset, op.insert_char_len);
2695                for layer_tree in self.editor.style_layers.values_mut() {
2696                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
2697                }
2698            }
2699
2700            self.apply_text_change_to_line_index_and_layout(
2701                op.start_offset,
2702                &op.deleted_text,
2703                &op.insert_text,
2704            );
2705        }
2706
2707        self.editor
2708            .folding_manager
2709            .clamp_to_line_count(self.editor.line_index.line_count());
2710
2711        // Update selection state: collapse to carets after insertion.
2712        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2713        for offset in &caret_offsets {
2714            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2715            let pos = Position::new(line, column);
2716            new_carets.push(Selection {
2717                start: pos,
2718                end: pos,
2719                direction: SelectionDirection::Forward,
2720            });
2721        }
2722
2723        let (new_carets, new_primary_index) =
2724            crate::selection_set::normalize_selections(new_carets, primary_index);
2725        let primary = new_carets
2726            .get(new_primary_index)
2727            .cloned()
2728            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2729
2730        self.editor.cursor_position = primary.end;
2731        self.editor.selection = None;
2732        self.editor.secondary_selections = new_carets
2733            .into_iter()
2734            .enumerate()
2735            .filter_map(|(idx, sel)| {
2736                if idx == new_primary_index {
2737                    None
2738                } else {
2739                    Some(sel)
2740                }
2741            })
2742            .collect();
2743
2744        let after_selection = self.snapshot_selection_set();
2745
2746        let edits: Vec<TextEdit> = ops
2747            .into_iter()
2748            .map(|op| TextEdit {
2749                start_before: op.start_offset,
2750                start_after: op.start_after,
2751                deleted_text: op.deleted_text,
2752                inserted_text: op.insert_text,
2753            })
2754            .collect();
2755
2756        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
2757        let coalescible_insert = is_pure_insert;
2758
2759        let mut delta_edits: Vec<TextDeltaEdit> = edits
2760            .iter()
2761            .map(|e| TextDeltaEdit {
2762                start: e.start_before,
2763                deleted_text: e.deleted_text.clone(),
2764                inserted_text: e.inserted_text.clone(),
2765            })
2766            .collect();
2767        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2768
2769        let step = UndoStep {
2770            group_id: 0,
2771            edits,
2772            before_selection,
2773            after_selection,
2774        };
2775        let group_id = self.undo_redo.push_step(step, coalescible_insert);
2776
2777        self.last_text_delta = Some(TextDelta {
2778            before_char_count,
2779            after_char_count: self.editor.piece_table.char_count(),
2780            edits: delta_edits,
2781            undo_group_id: Some(group_id),
2782        });
2783
2784        Ok(CommandResult::Success)
2785    }
2786
2787    fn leading_whitespace_prefix(line_text: &str) -> String {
2788        line_text
2789            .chars()
2790            .take_while(|ch| *ch == ' ' || *ch == '\t')
2791            .collect()
2792    }
2793
2794    fn indent_unit(&self) -> String {
2795        match self.tab_key_behavior {
2796            TabKeyBehavior::Tab => "\t".to_string(),
2797            TabKeyBehavior::Spaces => " ".repeat(self.editor.layout_engine.tab_width().max(1)),
2798        }
2799    }
2800
2801    fn execute_insert_newline_command(
2802        &mut self,
2803        auto_indent: bool,
2804    ) -> Result<CommandResult, CommandError> {
2805        // Newline insertion should not coalesce into a typing group.
2806        self.undo_redo.end_group();
2807
2808        let before_char_count = self.editor.piece_table.char_count();
2809        let before_selection = self.snapshot_selection_set();
2810
2811        // Canonical selection set (primary + secondary).
2812        let mut selections: Vec<Selection> =
2813            Vec::with_capacity(1 + self.editor.secondary_selections.len());
2814        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
2815            start: self.editor.cursor_position,
2816            end: self.editor.cursor_position,
2817            direction: SelectionDirection::Forward,
2818        });
2819        selections.push(primary_selection);
2820        selections.extend(self.editor.secondary_selections.iter().cloned());
2821
2822        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
2823
2824        struct Op {
2825            selection_index: usize,
2826            start_offset: usize,
2827            start_after: usize,
2828            delete_len: usize,
2829            deleted_text: String,
2830            insert_text: String,
2831            insert_char_len: usize,
2832        }
2833
2834        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
2835
2836        for (selection_index, selection) in selections.iter().enumerate() {
2837            let (range_start_pos, range_end_pos) =
2838                crate::selection_set::selection_min_max(selection);
2839
2840            let start_offset = self.position_to_char_offset_clamped(range_start_pos);
2841            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
2842
2843            let delete_len = end_offset.saturating_sub(start_offset);
2844            let deleted_text = if delete_len == 0 {
2845                String::new()
2846            } else {
2847                self.editor.piece_table.get_range(start_offset, delete_len)
2848            };
2849
2850            let indent = if auto_indent {
2851                let line_text = self
2852                    .editor
2853                    .line_index
2854                    .get_line_text(range_start_pos.line)
2855                    .unwrap_or_default();
2856                Self::leading_whitespace_prefix(&line_text)
2857            } else {
2858                String::new()
2859            };
2860
2861            let insert_text = format!("\n{}", indent);
2862            let insert_char_len = insert_text.chars().count();
2863
2864            ops.push(Op {
2865                selection_index,
2866                start_offset,
2867                start_after: start_offset,
2868                delete_len,
2869                deleted_text,
2870                insert_text,
2871                insert_char_len,
2872            });
2873        }
2874
2875        // Compute final caret offsets in the post-edit document (ascending order with delta),
2876        // while also recording each operation's start offset in the post-edit document.
2877        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2878        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2879
2880        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
2881        let mut delta: i64 = 0;
2882        for &idx in &asc_indices {
2883            let op = &mut ops[idx];
2884            let effective_start = (op.start_offset as i64 + delta) as usize;
2885            op.start_after = effective_start;
2886            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
2887            delta += op.insert_char_len as i64 - op.delete_len as i64;
2888        }
2889
2890        // Apply edits safely (descending offsets).
2891        let mut desc_indices = asc_indices;
2892        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2893
2894        for &idx in &desc_indices {
2895            let op = &ops[idx];
2896
2897            if op.delete_len > 0 {
2898                self.editor
2899                    .piece_table
2900                    .delete(op.start_offset, op.delete_len);
2901                self.editor
2902                    .interval_tree
2903                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2904                for layer_tree in self.editor.style_layers.values_mut() {
2905                    layer_tree
2906                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2907                }
2908            }
2909
2910            if !op.insert_text.is_empty() {
2911                self.editor
2912                    .piece_table
2913                    .insert(op.start_offset, &op.insert_text);
2914                self.editor
2915                    .interval_tree
2916                    .update_for_insertion(op.start_offset, op.insert_char_len);
2917                for layer_tree in self.editor.style_layers.values_mut() {
2918                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
2919                }
2920            }
2921
2922            self.apply_text_change_to_line_index_and_layout(
2923                op.start_offset,
2924                &op.deleted_text,
2925                &op.insert_text,
2926            );
2927        }
2928
2929        // Update selection state: collapse to carets after insertion.
2930        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2931        for offset in &caret_offsets {
2932            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2933            let pos = Position::new(line, column);
2934            new_carets.push(Selection {
2935                start: pos,
2936                end: pos,
2937                direction: SelectionDirection::Forward,
2938            });
2939        }
2940
2941        let (new_carets, new_primary_index) =
2942            crate::selection_set::normalize_selections(new_carets, primary_index);
2943        let primary = new_carets
2944            .get(new_primary_index)
2945            .cloned()
2946            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2947
2948        self.editor.cursor_position = primary.end;
2949        self.editor.selection = None;
2950        self.editor.secondary_selections = new_carets
2951            .into_iter()
2952            .enumerate()
2953            .filter_map(|(idx, sel)| {
2954                if idx == new_primary_index {
2955                    None
2956                } else {
2957                    Some(sel)
2958                }
2959            })
2960            .collect();
2961
2962        let after_selection = self.snapshot_selection_set();
2963
2964        let edits: Vec<TextEdit> = ops
2965            .into_iter()
2966            .map(|op| TextEdit {
2967                start_before: op.start_offset,
2968                start_after: op.start_after,
2969                deleted_text: op.deleted_text,
2970                inserted_text: op.insert_text,
2971            })
2972            .collect();
2973
2974        let mut delta_edits: Vec<TextDeltaEdit> = edits
2975            .iter()
2976            .map(|e| TextDeltaEdit {
2977                start: e.start_before,
2978                deleted_text: e.deleted_text.clone(),
2979                inserted_text: e.inserted_text.clone(),
2980            })
2981            .collect();
2982        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2983
2984        let step = UndoStep {
2985            group_id: 0,
2986            edits,
2987            before_selection,
2988            after_selection,
2989        };
2990        let group_id = self.undo_redo.push_step(step, false);
2991
2992        self.last_text_delta = Some(TextDelta {
2993            before_char_count,
2994            after_char_count: self.editor.piece_table.char_count(),
2995            edits: delta_edits,
2996            undo_group_id: Some(group_id),
2997        });
2998
2999        Ok(CommandResult::Success)
3000    }
3001
3002    fn execute_indent_command(&mut self, outdent: bool) -> Result<CommandResult, CommandError> {
3003        self.undo_redo.end_group();
3004
3005        let before_char_count = self.editor.piece_table.char_count();
3006        let before_selection = self.snapshot_selection_set();
3007        let selections = before_selection.selections.clone();
3008
3009        let mut lines: Vec<usize> = Vec::new();
3010        for sel in &selections {
3011            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
3012            for line in min_pos.line..=max_pos.line {
3013                lines.push(line);
3014            }
3015        }
3016        lines.sort_unstable();
3017        lines.dedup();
3018
3019        if lines.is_empty() {
3020            return Ok(CommandResult::Success);
3021        }
3022
3023        let tab_width = self.editor.layout_engine.tab_width().max(1);
3024        let indent_unit = self.indent_unit();
3025        let indent_chars = indent_unit.chars().count();
3026
3027        #[derive(Debug)]
3028        struct Op {
3029            start_offset: usize,
3030            start_after: usize,
3031            delete_len: usize,
3032            deleted_text: String,
3033            insert_text: String,
3034            insert_len: usize,
3035        }
3036
3037        let mut ops: Vec<Op> = Vec::new();
3038        let mut line_deltas: std::collections::HashMap<usize, isize> =
3039            std::collections::HashMap::new();
3040
3041        for line in lines {
3042            if line >= self.editor.line_index.line_count() {
3043                continue;
3044            }
3045
3046            let start_offset = self.editor.line_index.position_to_char_offset(line, 0);
3047            let line_text = self
3048                .editor
3049                .line_index
3050                .get_line_text(line)
3051                .unwrap_or_default();
3052
3053            if outdent {
3054                let mut remove_len = 0usize;
3055                if let Some(first) = line_text.chars().next() {
3056                    if first == '\t' {
3057                        remove_len = 1;
3058                    } else if first == ' ' {
3059                        let leading_spaces = line_text.chars().take_while(|c| *c == ' ').count();
3060                        remove_len = leading_spaces.min(tab_width);
3061                    }
3062                }
3063
3064                if remove_len == 0 {
3065                    continue;
3066                }
3067
3068                let deleted_text = self.editor.piece_table.get_range(start_offset, remove_len);
3069                ops.push(Op {
3070                    start_offset,
3071                    start_after: start_offset,
3072                    delete_len: remove_len,
3073                    deleted_text,
3074                    insert_text: String::new(),
3075                    insert_len: 0,
3076                });
3077                line_deltas.insert(line, -(remove_len as isize));
3078            } else {
3079                if indent_chars == 0 {
3080                    continue;
3081                }
3082
3083                ops.push(Op {
3084                    start_offset,
3085                    start_after: start_offset,
3086                    delete_len: 0,
3087                    deleted_text: String::new(),
3088                    insert_text: indent_unit.clone(),
3089                    insert_len: indent_chars,
3090                });
3091                line_deltas.insert(line, indent_chars as isize);
3092            }
3093        }
3094
3095        if ops.is_empty() {
3096            return Ok(CommandResult::Success);
3097        }
3098
3099        // Compute start_after using ascending order and delta accumulation.
3100        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3101        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
3102
3103        let mut delta: i64 = 0;
3104        for &idx in &asc_indices {
3105            let op = &mut ops[idx];
3106            let effective_start = (op.start_offset as i64 + delta) as usize;
3107            op.start_after = effective_start;
3108            delta += op.insert_len as i64 - op.delete_len as i64;
3109        }
3110
3111        // Apply ops descending so offsets remain valid.
3112        let mut desc_indices = asc_indices;
3113        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
3114
3115        for &idx in &desc_indices {
3116            let op = &ops[idx];
3117
3118            if op.delete_len > 0 {
3119                self.editor
3120                    .piece_table
3121                    .delete(op.start_offset, op.delete_len);
3122                self.editor
3123                    .interval_tree
3124                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3125                for layer_tree in self.editor.style_layers.values_mut() {
3126                    layer_tree
3127                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3128                }
3129            }
3130
3131            if op.insert_len > 0 {
3132                self.editor
3133                    .piece_table
3134                    .insert(op.start_offset, &op.insert_text);
3135                self.editor
3136                    .interval_tree
3137                    .update_for_insertion(op.start_offset, op.insert_len);
3138                for layer_tree in self.editor.style_layers.values_mut() {
3139                    layer_tree.update_for_insertion(op.start_offset, op.insert_len);
3140                }
3141            }
3142
3143            self.apply_text_change_to_line_index_and_layout(
3144                op.start_offset,
3145                &op.deleted_text,
3146                &op.insert_text,
3147            );
3148        }
3149
3150        // Shift cursor/selections for touched lines.
3151        let line_index = &self.editor.line_index;
3152        let apply_delta = |pos: &mut Position, deltas: &std::collections::HashMap<usize, isize>| {
3153            let Some(delta) = deltas.get(&pos.line) else {
3154                return;
3155            };
3156
3157            let new_col = if *delta >= 0 {
3158                pos.column.saturating_add(*delta as usize)
3159            } else {
3160                pos.column.saturating_sub((-*delta) as usize)
3161            };
3162
3163            pos.column = Self::clamp_column_for_line_with_index(line_index, pos.line, new_col);
3164        };
3165
3166        apply_delta(&mut self.editor.cursor_position, &line_deltas);
3167        if let Some(sel) = &mut self.editor.selection {
3168            apply_delta(&mut sel.start, &line_deltas);
3169            apply_delta(&mut sel.end, &line_deltas);
3170        }
3171        for sel in &mut self.editor.secondary_selections {
3172            apply_delta(&mut sel.start, &line_deltas);
3173            apply_delta(&mut sel.end, &line_deltas);
3174        }
3175
3176        self.normalize_cursor_and_selection();
3177        self.preferred_x_cells = self
3178            .editor
3179            .logical_position_to_visual(
3180                self.editor.cursor_position.line,
3181                self.editor.cursor_position.column,
3182            )
3183            .map(|(_, x)| x);
3184
3185        let after_selection = self.snapshot_selection_set();
3186
3187        let edits: Vec<TextEdit> = ops
3188            .into_iter()
3189            .map(|op| TextEdit {
3190                start_before: op.start_offset,
3191                start_after: op.start_after,
3192                deleted_text: op.deleted_text,
3193                inserted_text: op.insert_text,
3194            })
3195            .collect();
3196
3197        let mut delta_edits: Vec<TextDeltaEdit> = edits
3198            .iter()
3199            .map(|e| TextDeltaEdit {
3200                start: e.start_before,
3201                deleted_text: e.deleted_text.clone(),
3202                inserted_text: e.inserted_text.clone(),
3203            })
3204            .collect();
3205        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3206
3207        let step = UndoStep {
3208            group_id: 0,
3209            edits,
3210            before_selection,
3211            after_selection,
3212        };
3213        let group_id = self.undo_redo.push_step(step, false);
3214
3215        self.last_text_delta = Some(TextDelta {
3216            before_char_count,
3217            after_char_count: self.editor.piece_table.char_count(),
3218            edits: delta_edits,
3219            undo_group_id: Some(group_id),
3220        });
3221
3222        Ok(CommandResult::Success)
3223    }
3224
3225    fn selection_char_range(&self, selection: &Selection) -> SearchMatch {
3226        let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
3227        let start = self.position_to_char_offset_clamped(min_pos);
3228        let end = self.position_to_char_offset_clamped(max_pos);
3229        SearchMatch {
3230            start: start.min(end),
3231            end: start.max(end),
3232        }
3233    }
3234
3235    fn selected_line_blocks(selections: &[Selection]) -> Vec<(usize, usize)> {
3236        let mut lines: Vec<usize> = Vec::new();
3237        for sel in selections {
3238            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
3239            for line in min_pos.line..=max_pos.line {
3240                lines.push(line);
3241            }
3242        }
3243
3244        lines.sort_unstable();
3245        lines.dedup();
3246
3247        let mut blocks: Vec<(usize, usize)> = Vec::new();
3248        for line in lines {
3249            if let Some((_, end)) = blocks.last_mut()
3250                && *end + 1 == line
3251            {
3252                *end = line;
3253                continue;
3254            }
3255            blocks.push((line, line));
3256        }
3257        blocks
3258    }
3259
3260    fn slice_text_for_lines(&self, start_line: usize, end_line: usize) -> String {
3261        let line_count = self.editor.line_index.line_count();
3262        if line_count == 0 || start_line >= line_count || start_line > end_line {
3263            return String::new();
3264        }
3265
3266        let mut out = String::new();
3267        for line in start_line..=end_line.min(line_count - 1) {
3268            let text = self
3269                .editor
3270                .line_index
3271                .get_line_text(line)
3272                .unwrap_or_default();
3273            out.push_str(&text);
3274            // In the stored document, every line except the last has a trailing '\n'.
3275            if line + 1 < line_count {
3276                out.push('\n');
3277            }
3278        }
3279        out
3280    }
3281
3282    fn execute_duplicate_lines_command(&mut self) -> Result<CommandResult, CommandError> {
3283        self.undo_redo.end_group();
3284
3285        let before_char_count = self.editor.piece_table.char_count();
3286        let before_selection = self.snapshot_selection_set();
3287        let selections = before_selection.selections.clone();
3288        let primary_index = before_selection.primary_index;
3289
3290        let line_count = self.editor.line_index.line_count();
3291        if line_count == 0 {
3292            return Ok(CommandResult::Success);
3293        }
3294
3295        let blocks = Self::selected_line_blocks(&selections);
3296        if blocks.is_empty() {
3297            return Ok(CommandResult::Success);
3298        }
3299
3300        let doc_text = self.editor.piece_table.get_text();
3301        let doc_ends_with_newline = doc_text.ends_with('\n');
3302
3303        struct Op {
3304            start_before: usize,
3305            start_after: usize,
3306            deleted_text: String,
3307            inserted_text: String,
3308            inserted_len: usize,
3309        }
3310
3311        let mut ops: Vec<Op> = Vec::new();
3312
3313        for (start_line, end_line) in blocks {
3314            if start_line >= line_count {
3315                continue;
3316            }
3317            let end_line = end_line.min(line_count - 1);
3318
3319            let insertion_offset = if end_line + 1 < line_count {
3320                self.editor
3321                    .line_index
3322                    .position_to_char_offset(end_line + 1, 0)
3323            } else {
3324                before_char_count
3325            };
3326
3327            let block_text = self.slice_text_for_lines(start_line, end_line);
3328            if block_text.is_empty() && before_char_count == 0 {
3329                continue;
3330            }
3331
3332            let mut inserted_text = block_text;
3333            if insertion_offset == before_char_count
3334                && !doc_ends_with_newline
3335                && before_char_count > 0
3336            {
3337                inserted_text.insert(0, '\n');
3338            }
3339
3340            let inserted_len = inserted_text.chars().count();
3341            if inserted_len == 0 {
3342                continue;
3343            }
3344
3345            ops.push(Op {
3346                start_before: insertion_offset,
3347                start_after: insertion_offset,
3348                deleted_text: String::new(),
3349                inserted_text,
3350                inserted_len,
3351            });
3352        }
3353
3354        if ops.is_empty() {
3355            return Ok(CommandResult::Success);
3356        }
3357
3358        // Compute start_after using ascending order and delta accumulation.
3359        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3360        asc_indices.sort_by_key(|&idx| ops[idx].start_before);
3361
3362        let mut delta: i64 = 0;
3363        for &idx in &asc_indices {
3364            let op = &mut ops[idx];
3365            let effective_start = op.start_before as i64 + delta;
3366            if effective_start < 0 {
3367                return Err(CommandError::Other(
3368                    "DuplicateLines produced an invalid intermediate offset".to_string(),
3369                ));
3370            }
3371            op.start_after = effective_start as usize;
3372            delta += op.inserted_len as i64;
3373        }
3374
3375        let apply_ops: Vec<(usize, usize, &str)> = ops
3376            .iter()
3377            .map(|op| (op.start_before, 0usize, op.inserted_text.as_str()))
3378            .collect();
3379        self.apply_text_ops(apply_ops)?;
3380
3381        // Move selections/carets to the duplicated lines, VSCode-style.
3382        let mut mapped: Vec<Selection> = Vec::with_capacity(selections.len());
3383
3384        // Precompute per-block cumulative shift (in lines) for blocks above.
3385        let mut block_info: Vec<(usize, usize, usize, usize)> = Vec::new(); // (start,end,size,shift_before)
3386        let mut cumulative = 0usize;
3387        let mut blocks = Self::selected_line_blocks(&selections);
3388        blocks.sort_by_key(|(s, _)| *s);
3389        for (s, e) in blocks {
3390            let size = e.saturating_sub(s) + 1;
3391            block_info.push((s, e, size, cumulative));
3392            cumulative = cumulative.saturating_add(size);
3393        }
3394
3395        let line_index = &self.editor.line_index;
3396        for sel in selections {
3397            let mut start = sel.start;
3398            let mut end = sel.end;
3399
3400            let map_line = |line: usize, info: &[(usize, usize, usize, usize)]| -> usize {
3401                // If inside a duplicated block, map to the duplicate copy (shift by block_size).
3402                for (s, e, size, shift_before) in info {
3403                    if line >= *s && line <= *e {
3404                        return line + *shift_before + *size;
3405                    }
3406                    if line < *s {
3407                        break;
3408                    }
3409                }
3410
3411                // Otherwise, shift down by the number of duplicated lines above this line.
3412                let mut shift = 0usize;
3413                for (s, e, size, shift_before) in info {
3414                    let _ = shift_before;
3415                    if *e < line {
3416                        shift = shift.saturating_add(*size);
3417                    } else if line < *s {
3418                        break;
3419                    }
3420                }
3421                line + shift
3422            };
3423
3424            start.line = map_line(start.line, &block_info);
3425            end.line = map_line(end.line, &block_info);
3426
3427            start.column =
3428                Self::clamp_column_for_line_with_index(line_index, start.line, start.column);
3429            end.column = Self::clamp_column_for_line_with_index(line_index, end.line, end.column);
3430
3431            mapped.push(Selection {
3432                start,
3433                end,
3434                direction: crate::selection_set::selection_direction(start, end),
3435            });
3436        }
3437
3438        let (mapped, mapped_primary) =
3439            crate::selection_set::normalize_selections(mapped, primary_index);
3440        self.execute_cursor(CursorCommand::SetSelections {
3441            selections: mapped,
3442            primary_index: mapped_primary,
3443        })?;
3444
3445        let after_selection = self.snapshot_selection_set();
3446
3447        let edits: Vec<TextEdit> = ops
3448            .into_iter()
3449            .map(|op| TextEdit {
3450                start_before: op.start_before,
3451                start_after: op.start_after,
3452                deleted_text: op.deleted_text,
3453                inserted_text: op.inserted_text,
3454            })
3455            .collect();
3456
3457        let mut delta_edits: Vec<TextDeltaEdit> = edits
3458            .iter()
3459            .map(|e| TextDeltaEdit {
3460                start: e.start_before,
3461                deleted_text: e.deleted_text.clone(),
3462                inserted_text: e.inserted_text.clone(),
3463            })
3464            .collect();
3465        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3466
3467        let step = UndoStep {
3468            group_id: 0,
3469            edits,
3470            before_selection,
3471            after_selection,
3472        };
3473        let group_id = self.undo_redo.push_step(step, false);
3474
3475        self.last_text_delta = Some(TextDelta {
3476            before_char_count,
3477            after_char_count: self.editor.piece_table.char_count(),
3478            edits: delta_edits,
3479            undo_group_id: Some(group_id),
3480        });
3481
3482        Ok(CommandResult::Success)
3483    }
3484
3485    fn execute_delete_lines_command(&mut self) -> Result<CommandResult, CommandError> {
3486        self.undo_redo.end_group();
3487
3488        let before_char_count = self.editor.piece_table.char_count();
3489        let before_selection = self.snapshot_selection_set();
3490        let selections = before_selection.selections.clone();
3491        let primary_selection = selections
3492            .get(before_selection.primary_index)
3493            .cloned()
3494            .unwrap_or_else(|| selections[0].clone());
3495
3496        let line_count = self.editor.line_index.line_count();
3497        if line_count == 0 {
3498            return Ok(CommandResult::Success);
3499        }
3500
3501        let blocks = Self::selected_line_blocks(&selections);
3502        if blocks.is_empty() {
3503            return Ok(CommandResult::Success);
3504        }
3505
3506        struct Op {
3507            start_before: usize,
3508            start_after: usize,
3509            delete_len: usize,
3510            deleted_text: String,
3511        }
3512
3513        let mut ops: Vec<Op> = Vec::new();
3514        let mut primary_op_index = 0usize;
3515
3516        for (idx, (start_line, end_line)) in blocks.into_iter().enumerate() {
3517            if start_line >= line_count {
3518                continue;
3519            }
3520
3521            let end_line = end_line.min(line_count - 1);
3522            let mut start_offset = self
3523                .editor
3524                .line_index
3525                .position_to_char_offset(start_line, 0);
3526            let end_offset = if end_line + 1 < line_count {
3527                self.editor
3528                    .line_index
3529                    .position_to_char_offset(end_line + 1, 0)
3530            } else {
3531                before_char_count
3532            };
3533
3534            if end_line + 1 >= line_count && start_offset > 0 {
3535                // Deleting the last line: also remove the newline before it, if any.
3536                start_offset = start_offset.saturating_sub(1);
3537            }
3538
3539            if end_offset <= start_offset {
3540                continue;
3541            }
3542
3543            let delete_len = end_offset - start_offset;
3544            let deleted_text = self.editor.piece_table.get_range(start_offset, delete_len);
3545
3546            if crate::selection_set::selection_contains_position_inclusive(
3547                &primary_selection,
3548                Position::new(start_line, 0),
3549            ) {
3550                primary_op_index = idx;
3551            }
3552
3553            ops.push(Op {
3554                start_before: start_offset,
3555                start_after: start_offset,
3556                delete_len,
3557                deleted_text,
3558            });
3559        }
3560
3561        if ops.is_empty() {
3562            return Ok(CommandResult::Success);
3563        }
3564
3565        // Compute start_after using ascending order and delta accumulation.
3566        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3567        asc_indices.sort_by_key(|&idx| ops[idx].start_before);
3568
3569        let mut delta: i64 = 0;
3570        for &idx in &asc_indices {
3571            let op = &mut ops[idx];
3572            let effective_start = op.start_before as i64 + delta;
3573            if effective_start < 0 {
3574                return Err(CommandError::Other(
3575                    "DeleteLines produced an invalid intermediate offset".to_string(),
3576                ));
3577            }
3578            op.start_after = effective_start as usize;
3579            delta -= op.delete_len as i64;
3580        }
3581
3582        let apply_ops: Vec<(usize, usize, &str)> = ops
3583            .iter()
3584            .map(|op| (op.start_before, op.delete_len, ""))
3585            .collect();
3586        self.apply_text_ops(apply_ops)?;
3587
3588        // Collapse selection state to carets at the start of each deleted block.
3589        let mut new_carets: Vec<Selection> = Vec::with_capacity(ops.len());
3590        for op in &ops {
3591            let (line, column) = self
3592                .editor
3593                .line_index
3594                .char_offset_to_position(op.start_after);
3595            let pos = Position::new(line, column);
3596            new_carets.push(Selection {
3597                start: pos,
3598                end: pos,
3599                direction: SelectionDirection::Forward,
3600            });
3601        }
3602
3603        let primary_index = primary_op_index.min(new_carets.len().saturating_sub(1));
3604        self.execute_cursor(CursorCommand::SetSelections {
3605            selections: new_carets,
3606            primary_index,
3607        })?;
3608
3609        let after_selection = self.snapshot_selection_set();
3610
3611        let edits: Vec<TextEdit> = ops
3612            .into_iter()
3613            .map(|op| TextEdit {
3614                start_before: op.start_before,
3615                start_after: op.start_after,
3616                deleted_text: op.deleted_text,
3617                inserted_text: String::new(),
3618            })
3619            .collect();
3620
3621        let mut delta_edits: Vec<TextDeltaEdit> = edits
3622            .iter()
3623            .map(|e| TextDeltaEdit {
3624                start: e.start_before,
3625                deleted_text: e.deleted_text.clone(),
3626                inserted_text: String::new(),
3627            })
3628            .collect();
3629        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3630
3631        let step = UndoStep {
3632            group_id: 0,
3633            edits,
3634            before_selection,
3635            after_selection,
3636        };
3637        let group_id = self.undo_redo.push_step(step, false);
3638
3639        self.last_text_delta = Some(TextDelta {
3640            before_char_count,
3641            after_char_count: self.editor.piece_table.char_count(),
3642            edits: delta_edits,
3643            undo_group_id: Some(group_id),
3644        });
3645
3646        Ok(CommandResult::Success)
3647    }
3648
3649    fn execute_move_lines_command(&mut self, up: bool) -> Result<CommandResult, CommandError> {
3650        self.undo_redo.end_group();
3651
3652        let before_char_count = self.editor.piece_table.char_count();
3653        let before_selection = self.snapshot_selection_set();
3654        let selections = before_selection.selections.clone();
3655        let primary_index = before_selection.primary_index;
3656
3657        let line_count = self.editor.line_index.line_count();
3658        if line_count <= 1 {
3659            return Ok(CommandResult::Success);
3660        }
3661
3662        let blocks = Self::selected_line_blocks(&selections);
3663        if blocks.is_empty() {
3664            return Ok(CommandResult::Success);
3665        }
3666
3667        #[derive(Debug, Clone, Copy)]
3668        struct Block {
3669            start: usize,
3670            end: usize,
3671        }
3672
3673        let mut moved_blocks: Vec<Block> = Vec::new();
3674        for (start, end) in blocks {
3675            let start = start.min(line_count - 1);
3676            let end = end.min(line_count - 1);
3677            if up {
3678                if start == 0 {
3679                    continue;
3680                }
3681            } else if end + 1 >= line_count {
3682                continue;
3683            }
3684            moved_blocks.push(Block { start, end });
3685        }
3686
3687        if moved_blocks.is_empty() {
3688            return Ok(CommandResult::Success);
3689        }
3690
3691        struct Op {
3692            start_before: usize,
3693            start_after: usize,
3694            delete_len: usize,
3695            deleted_text: String,
3696            inserted_text: String,
3697        }
3698
3699        let mut ops: Vec<Op> = Vec::with_capacity(moved_blocks.len());
3700
3701        for block in &moved_blocks {
3702            let (range_start_line, range_end_line) = if up {
3703                (block.start - 1, block.end)
3704            } else {
3705                (block.start, block.end + 1)
3706            };
3707
3708            let start_offset = self
3709                .editor
3710                .line_index
3711                .position_to_char_offset(range_start_line, 0);
3712            let end_offset = if range_end_line + 1 < line_count {
3713                self.editor
3714                    .line_index
3715                    .position_to_char_offset(range_end_line + 1, 0)
3716            } else {
3717                before_char_count
3718            };
3719
3720            if end_offset <= start_offset {
3721                continue;
3722            }
3723
3724            let deleted_text = self
3725                .editor
3726                .piece_table
3727                .get_range(start_offset, end_offset - start_offset);
3728
3729            let block_text = self.slice_text_for_lines(block.start, block.end);
3730
3731            let inserted_text = if up {
3732                let above_text = self.slice_text_for_lines(block.start - 1, block.start - 1);
3733                format!("{}{}", block_text, above_text)
3734            } else {
3735                let below_text = self.slice_text_for_lines(block.end + 1, block.end + 1);
3736                format!("{}{}", below_text, block_text)
3737            };
3738
3739            ops.push(Op {
3740                start_before: start_offset,
3741                start_after: start_offset,
3742                delete_len: end_offset - start_offset,
3743                deleted_text,
3744                inserted_text,
3745            });
3746        }
3747
3748        if ops.is_empty() {
3749            return Ok(CommandResult::Success);
3750        }
3751
3752        // start_after is stable here (equal-length replacements), but compute for consistency.
3753        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3754        asc_indices.sort_by_key(|&idx| ops[idx].start_before);
3755
3756        let mut delta: i64 = 0;
3757        for &idx in &asc_indices {
3758            let op = &mut ops[idx];
3759            let effective_start = op.start_before as i64 + delta;
3760            if effective_start < 0 {
3761                return Err(CommandError::Other(
3762                    "MoveLines produced an invalid intermediate offset".to_string(),
3763                ));
3764            }
3765            op.start_after = effective_start as usize;
3766            let inserted_len = op.inserted_text.chars().count() as i64;
3767            delta += inserted_len - op.delete_len as i64;
3768        }
3769
3770        let apply_ops: Vec<(usize, usize, &str)> = ops
3771            .iter()
3772            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
3773            .collect();
3774        self.apply_text_ops(apply_ops)?;
3775
3776        // Move selections with their line blocks (and adjust displaced neighbor line).
3777        let line_index = &self.editor.line_index;
3778        let mut mapped: Vec<Selection> = Vec::with_capacity(selections.len());
3779
3780        for sel in selections {
3781            let mut start = sel.start;
3782            let mut end = sel.end;
3783
3784            let map_line = |line: usize, moved_blocks: &[Block], up: bool| -> usize {
3785                for block in moved_blocks {
3786                    let size = block.end.saturating_sub(block.start) + 1;
3787                    if line >= block.start && line <= block.end {
3788                        return if up { line - 1 } else { line + 1 };
3789                    }
3790                    if up && line == block.start - 1 {
3791                        return line + size;
3792                    }
3793                    if !up && line == block.end + 1 {
3794                        return line.saturating_sub(size);
3795                    }
3796                }
3797                line
3798            };
3799
3800            start.line = map_line(start.line, &moved_blocks, up);
3801            end.line = map_line(end.line, &moved_blocks, up);
3802
3803            start.column =
3804                Self::clamp_column_for_line_with_index(line_index, start.line, start.column);
3805            end.column = Self::clamp_column_for_line_with_index(line_index, end.line, end.column);
3806
3807            mapped.push(Selection {
3808                start,
3809                end,
3810                direction: crate::selection_set::selection_direction(start, end),
3811            });
3812        }
3813
3814        let (mapped, mapped_primary) =
3815            crate::selection_set::normalize_selections(mapped, primary_index);
3816        self.execute_cursor(CursorCommand::SetSelections {
3817            selections: mapped,
3818            primary_index: mapped_primary,
3819        })?;
3820
3821        let after_selection = self.snapshot_selection_set();
3822
3823        let edits: Vec<TextEdit> = ops
3824            .into_iter()
3825            .map(|op| TextEdit {
3826                start_before: op.start_before,
3827                start_after: op.start_after,
3828                deleted_text: op.deleted_text,
3829                inserted_text: op.inserted_text,
3830            })
3831            .collect();
3832
3833        let mut delta_edits: Vec<TextDeltaEdit> = edits
3834            .iter()
3835            .map(|e| TextDeltaEdit {
3836                start: e.start_before,
3837                deleted_text: e.deleted_text.clone(),
3838                inserted_text: e.inserted_text.clone(),
3839            })
3840            .collect();
3841        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3842
3843        let step = UndoStep {
3844            group_id: 0,
3845            edits,
3846            before_selection,
3847            after_selection,
3848        };
3849        let group_id = self.undo_redo.push_step(step, false);
3850
3851        self.last_text_delta = Some(TextDelta {
3852            before_char_count,
3853            after_char_count: self.editor.piece_table.char_count(),
3854            edits: delta_edits,
3855            undo_group_id: Some(group_id),
3856        });
3857
3858        Ok(CommandResult::Success)
3859    }
3860
3861    fn execute_join_lines_command(&mut self) -> Result<CommandResult, CommandError> {
3862        self.undo_redo.end_group();
3863
3864        let before_char_count = self.editor.piece_table.char_count();
3865        let before_selection = self.snapshot_selection_set();
3866        let selections = before_selection.selections.clone();
3867
3868        let line_count = self.editor.line_index.line_count();
3869        if line_count <= 1 {
3870            return Ok(CommandResult::Success);
3871        }
3872
3873        let mut join_lines: Vec<usize> = Vec::new();
3874        for sel in &selections {
3875            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
3876            if min_pos.line >= line_count {
3877                continue;
3878            }
3879            let last = max_pos.line.min(line_count - 1);
3880            if min_pos.line == last {
3881                join_lines.push(last);
3882            } else {
3883                for line in min_pos.line..last {
3884                    join_lines.push(line);
3885                }
3886            }
3887        }
3888
3889        join_lines.sort_unstable();
3890        join_lines.dedup();
3891        join_lines.retain(|l| *l + 1 < line_count);
3892
3893        if join_lines.is_empty() {
3894            return Ok(CommandResult::Success);
3895        }
3896
3897        struct Op {
3898            start_before: usize,
3899            start_after: usize,
3900            delete_len: usize,
3901            deleted_text: String,
3902            inserted_text: String,
3903            inserted_len: usize,
3904        }
3905
3906        let mut ops: Vec<Op> = Vec::with_capacity(join_lines.len());
3907
3908        // Process from bottom to top to keep (line->offset) stable in the pre-edit document.
3909        join_lines.sort_by_key(|l| std::cmp::Reverse(*l));
3910
3911        for line in join_lines {
3912            let line_text = self
3913                .editor
3914                .line_index
3915                .get_line_text(line)
3916                .unwrap_or_default();
3917            let next_text = self
3918                .editor
3919                .line_index
3920                .get_line_text(line + 1)
3921                .unwrap_or_default();
3922
3923            let line_len = line_text.chars().count();
3924            let join_offset = self
3925                .editor
3926                .line_index
3927                .position_to_char_offset(line, line_len);
3928            let leading_ws = next_text
3929                .chars()
3930                .take_while(|c| *c == ' ' || *c == '\t')
3931                .count();
3932            let end_offset = self
3933                .editor
3934                .line_index
3935                .position_to_char_offset(line + 1, leading_ws);
3936
3937            if end_offset <= join_offset {
3938                continue;
3939            }
3940
3941            let left_ends_with_ws = line_text
3942                .chars()
3943                .last()
3944                .is_some_and(|c| c == ' ' || c == '\t');
3945            let right_trimmed_empty = next_text.chars().nth(leading_ws).is_none();
3946            let insert_space = !left_ends_with_ws && !line_text.is_empty() && !right_trimmed_empty;
3947
3948            let inserted_text = if insert_space {
3949                " ".to_string()
3950            } else {
3951                String::new()
3952            };
3953            let inserted_len = inserted_text.chars().count();
3954            let delete_len = end_offset - join_offset;
3955            let deleted_text = self.editor.piece_table.get_range(join_offset, delete_len);
3956
3957            ops.push(Op {
3958                start_before: join_offset,
3959                start_after: join_offset,
3960                delete_len,
3961                deleted_text,
3962                inserted_text,
3963                inserted_len,
3964            });
3965        }
3966
3967        if ops.is_empty() {
3968            return Ok(CommandResult::Success);
3969        }
3970
3971        ops.sort_by_key(|op| op.start_before);
3972
3973        let mut delta: i64 = 0;
3974        for op in &mut ops {
3975            let effective_start = op.start_before as i64 + delta;
3976            if effective_start < 0 {
3977                return Err(CommandError::Other(
3978                    "JoinLines produced an invalid intermediate offset".to_string(),
3979                ));
3980            }
3981            op.start_after = effective_start as usize;
3982            delta += op.inserted_len as i64 - op.delete_len as i64;
3983        }
3984
3985        let apply_ops: Vec<(usize, usize, &str)> = ops
3986            .iter()
3987            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
3988            .collect();
3989        self.apply_text_ops(apply_ops)?;
3990
3991        // Collapse selection state to carets at each join point.
3992        let mut new_carets: Vec<Selection> = Vec::with_capacity(ops.len());
3993        for op in &ops {
3994            let caret_offset = op.start_after + op.inserted_len;
3995            let (line, column) = self.editor.line_index.char_offset_to_position(caret_offset);
3996            let pos = Position::new(line, column);
3997            new_carets.push(Selection {
3998                start: pos,
3999                end: pos,
4000                direction: SelectionDirection::Forward,
4001            });
4002        }
4003
4004        let (new_carets, primary_index) = crate::selection_set::normalize_selections(new_carets, 0);
4005        self.execute_cursor(CursorCommand::SetSelections {
4006            selections: new_carets,
4007            primary_index,
4008        })?;
4009
4010        let after_selection = self.snapshot_selection_set();
4011
4012        let edits: Vec<TextEdit> = ops
4013            .into_iter()
4014            .map(|op| TextEdit {
4015                start_before: op.start_before,
4016                start_after: op.start_after,
4017                deleted_text: op.deleted_text,
4018                inserted_text: op.inserted_text,
4019            })
4020            .collect();
4021
4022        let mut delta_edits: Vec<TextDeltaEdit> = edits
4023            .iter()
4024            .map(|e| TextDeltaEdit {
4025                start: e.start_before,
4026                deleted_text: e.deleted_text.clone(),
4027                inserted_text: e.inserted_text.clone(),
4028            })
4029            .collect();
4030        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
4031
4032        let step = UndoStep {
4033            group_id: 0,
4034            edits,
4035            before_selection,
4036            after_selection,
4037        };
4038        let group_id = self.undo_redo.push_step(step, false);
4039
4040        self.last_text_delta = Some(TextDelta {
4041            before_char_count,
4042            after_char_count: self.editor.piece_table.char_count(),
4043            edits: delta_edits,
4044            undo_group_id: Some(group_id),
4045        });
4046
4047        Ok(CommandResult::Success)
4048    }
4049
4050    fn execute_toggle_comment_command(
4051        &mut self,
4052        config: CommentConfig,
4053    ) -> Result<CommandResult, CommandError> {
4054        if !config.has_line() && !config.has_block() {
4055            return Err(CommandError::Other(
4056                "ToggleComment requires at least one comment token".to_string(),
4057            ));
4058        }
4059
4060        self.undo_redo.end_group();
4061
4062        let before_char_count = self.editor.piece_table.char_count();
4063        let before_selection = self.snapshot_selection_set();
4064        let selections = before_selection.selections.clone();
4065        let primary_index = before_selection.primary_index;
4066
4067        let line_count = self.editor.line_index.line_count();
4068        if line_count == 0 {
4069            return Ok(CommandResult::Success);
4070        }
4071
4072        let all_single_line_selections = selections.iter().all(|sel| {
4073            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
4074            min_pos.line == max_pos.line && min_pos != max_pos
4075        });
4076
4077        if config.has_block()
4078            && all_single_line_selections
4079            && let (Some(block_start), Some(block_end)) =
4080                (config.block_start.as_deref(), config.block_end.as_deref())
4081        {
4082            return self.execute_toggle_block_comment_inline(
4083                block_start,
4084                block_end,
4085                before_char_count,
4086                before_selection,
4087                selections,
4088                primary_index,
4089            );
4090        }
4091
4092        if config.has_line()
4093            && let Some(token) = config.line.as_deref()
4094        {
4095            return self.execute_toggle_line_comment(
4096                token,
4097                before_char_count,
4098                before_selection,
4099                selections,
4100                primary_index,
4101            );
4102        }
4103
4104        if config.has_block()
4105            && let (Some(block_start), Some(block_end)) =
4106                (config.block_start.as_deref(), config.block_end.as_deref())
4107        {
4108            return self.execute_toggle_block_comment_lines(
4109                block_start,
4110                block_end,
4111                before_char_count,
4112                before_selection,
4113                selections,
4114                primary_index,
4115            );
4116        }
4117
4118        Ok(CommandResult::Success)
4119    }
4120
4121    fn execute_apply_text_edits_command(
4122        &mut self,
4123        mut edits: Vec<TextEditSpec>,
4124    ) -> Result<CommandResult, CommandError> {
4125        self.undo_redo.end_group();
4126
4127        if edits.is_empty() {
4128            return Ok(CommandResult::Success);
4129        }
4130
4131        let before_char_count = self.editor.piece_table.char_count();
4132        let before_selection = self.snapshot_selection_set();
4133
4134        let max_offset = before_char_count;
4135
4136        for edit in &mut edits {
4137            if edit.start > edit.end {
4138                return Err(CommandError::InvalidRange {
4139                    start: edit.start,
4140                    end: edit.end,
4141                });
4142            }
4143            if edit.end > max_offset {
4144                return Err(CommandError::InvalidRange {
4145                    start: edit.start,
4146                    end: edit.end,
4147                });
4148            }
4149            edit.text = crate::text::normalize_crlf_to_lf_string(edit.text.clone());
4150        }
4151
4152        edits.sort_by_key(|e| (e.start, e.end));
4153
4154        // Validate non-overlap (pre-edit coordinates).
4155        let mut prev_end = 0usize;
4156        for (idx, edit) in edits.iter().enumerate() {
4157            if idx > 0 && edit.start < prev_end {
4158                return Err(CommandError::Other(
4159                    "ApplyTextEdits requires non-overlapping edits".to_string(),
4160                ));
4161            }
4162            prev_end = prev_end.max(edit.end);
4163        }
4164
4165        struct Op {
4166            start_before: usize,
4167            start_after: usize,
4168            delete_len: usize,
4169            deleted_text: String,
4170            inserted_text: String,
4171            inserted_len: usize,
4172        }
4173
4174        let mut ops: Vec<Op> = Vec::with_capacity(edits.len());
4175        for edit in edits {
4176            let delete_len = edit.end.saturating_sub(edit.start);
4177            let deleted_text = if delete_len == 0 {
4178                String::new()
4179            } else {
4180                self.editor.piece_table.get_range(edit.start, delete_len)
4181            };
4182
4183            let inserted_text = edit.text;
4184            let inserted_len = inserted_text.chars().count();
4185
4186            ops.push(Op {
4187                start_before: edit.start,
4188                start_after: edit.start,
4189                delete_len,
4190                deleted_text,
4191                inserted_text,
4192                inserted_len,
4193            });
4194        }
4195
4196        // Compute start_after using ascending order and delta accumulation.
4197        let mut delta: i64 = 0;
4198        for op in &mut ops {
4199            let effective_start = op.start_before as i64 + delta;
4200            if effective_start < 0 {
4201                return Err(CommandError::Other(
4202                    "ApplyTextEdits produced an invalid intermediate offset".to_string(),
4203                ));
4204            }
4205            op.start_after = effective_start as usize;
4206            delta += op.inserted_len as i64 - op.delete_len as i64;
4207        }
4208
4209        let apply_ops: Vec<(usize, usize, &str)> = ops
4210            .iter()
4211            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
4212            .collect();
4213        self.apply_text_ops(apply_ops)?;
4214
4215        let after_selection = self.snapshot_selection_set();
4216
4217        let edits: Vec<TextEdit> = ops
4218            .into_iter()
4219            .map(|op| TextEdit {
4220                start_before: op.start_before,
4221                start_after: op.start_after,
4222                deleted_text: op.deleted_text,
4223                inserted_text: op.inserted_text,
4224            })
4225            .collect();
4226
4227        let mut delta_edits: Vec<TextDeltaEdit> = edits
4228            .iter()
4229            .map(|e| TextDeltaEdit {
4230                start: e.start_before,
4231                deleted_text: e.deleted_text.clone(),
4232                inserted_text: e.inserted_text.clone(),
4233            })
4234            .collect();
4235        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
4236
4237        let step = UndoStep {
4238            group_id: 0,
4239            edits,
4240            before_selection,
4241            after_selection,
4242        };
4243        let group_id = self.undo_redo.push_step(step, false);
4244
4245        self.last_text_delta = Some(TextDelta {
4246            before_char_count,
4247            after_char_count: self.editor.piece_table.char_count(),
4248            edits: delta_edits,
4249            undo_group_id: Some(group_id),
4250        });
4251
4252        Ok(CommandResult::Success)
4253    }
4254
4255    fn execute_toggle_line_comment(
4256        &mut self,
4257        token: &str,
4258        before_char_count: usize,
4259        before_selection: SelectionSetSnapshot,
4260        selections: Vec<Selection>,
4261        _primary_index: usize,
4262    ) -> Result<CommandResult, CommandError> {
4263        let token = token.trim_end();
4264        if token.is_empty() {
4265            return Ok(CommandResult::Success);
4266        }
4267
4268        let token_len = token.chars().count();
4269        let insert_text = format!("{} ", token);
4270        let insert_len = insert_text.chars().count();
4271
4272        // Collect unique target lines.
4273        let mut lines: Vec<usize> = Vec::new();
4274        for sel in &selections {
4275            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
4276            for line in min_pos.line..=max_pos.line {
4277                lines.push(line);
4278            }
4279        }
4280        lines.sort_unstable();
4281        lines.dedup();
4282        lines.retain(|l| *l < self.editor.line_index.line_count());
4283
4284        if lines.is_empty() {
4285            return Ok(CommandResult::Success);
4286        }
4287
4288        // Decide whether to comment or uncomment.
4289        let mut non_empty = 0usize;
4290        let mut all_commented = true;
4291        for line in &lines {
4292            let line_text = self
4293                .editor
4294                .line_index
4295                .get_line_text(*line)
4296                .unwrap_or_default();
4297            let indent = line_text
4298                .chars()
4299                .take_while(|c| *c == ' ' || *c == '\t')
4300                .count();
4301            let indent_byte = byte_offset_for_char_column(&line_text, indent);
4302            let rest = line_text.get(indent_byte..).unwrap_or("");
4303            if rest.is_empty() {
4304                continue;
4305            }
4306            non_empty += 1;
4307            if !rest.starts_with(token) {
4308                all_commented = false;
4309                break;
4310            }
4311        }
4312
4313        let should_uncomment = non_empty > 0 && all_commented;
4314
4315        struct Op {
4316            start_before: usize,
4317            start_after: usize,
4318            delete_len: usize,
4319            deleted_text: String,
4320            inserted_text: String,
4321            inserted_len: usize,
4322            line: usize,
4323            indent_col: usize,
4324            col_delta: isize,
4325        }
4326
4327        let mut ops: Vec<Op> = Vec::new();
4328
4329        for line in lines {
4330            let line_text = self
4331                .editor
4332                .line_index
4333                .get_line_text(line)
4334                .unwrap_or_default();
4335            let indent = line_text
4336                .chars()
4337                .take_while(|c| *c == ' ' || *c == '\t')
4338                .count();
4339            let indent_byte = byte_offset_for_char_column(&line_text, indent);
4340            let rest = line_text.get(indent_byte..).unwrap_or("");
4341
4342            let start_offset = self.editor.line_index.position_to_char_offset(line, indent);
4343
4344            if should_uncomment {
4345                if rest.is_empty() || !rest.starts_with(token) {
4346                    continue;
4347                }
4348
4349                let mut remove_len = token_len;
4350                if let Some(ch) = line_text.chars().nth(indent + token_len)
4351                    && ch == ' '
4352                {
4353                    remove_len += 1;
4354                }
4355
4356                if remove_len == 0 {
4357                    continue;
4358                }
4359
4360                let deleted_text = self.editor.piece_table.get_range(start_offset, remove_len);
4361                ops.push(Op {
4362                    start_before: start_offset,
4363                    start_after: start_offset,
4364                    delete_len: remove_len,
4365                    deleted_text,
4366                    inserted_text: String::new(),
4367                    inserted_len: 0,
4368                    line,
4369                    indent_col: indent,
4370                    col_delta: -(remove_len as isize),
4371                });
4372            } else {
4373                ops.push(Op {
4374                    start_before: start_offset,
4375                    start_after: start_offset,
4376                    delete_len: 0,
4377                    deleted_text: String::new(),
4378                    inserted_text: insert_text.clone(),
4379                    inserted_len: insert_len,
4380                    line,
4381                    indent_col: indent,
4382                    col_delta: insert_len as isize,
4383                });
4384            }
4385        }
4386
4387        if ops.is_empty() {
4388            return Ok(CommandResult::Success);
4389        }
4390
4391        // Compute start_after.
4392        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
4393        asc_indices.sort_by_key(|&idx| ops[idx].start_before);
4394
4395        let mut delta: i64 = 0;
4396        for &idx in &asc_indices {
4397            let op = &mut ops[idx];
4398            let effective_start = op.start_before as i64 + delta;
4399            if effective_start < 0 {
4400                return Err(CommandError::Other(
4401                    "ToggleComment produced an invalid intermediate offset".to_string(),
4402                ));
4403            }
4404            op.start_after = effective_start as usize;
4405            delta += op.inserted_len as i64 - op.delete_len as i64;
4406        }
4407
4408        let apply_ops: Vec<(usize, usize, &str)> = ops
4409            .iter()
4410            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
4411            .collect();
4412        self.apply_text_ops(apply_ops)?;
4413
4414        // Shift cursor/selections for touched lines, but only for columns at/after the insertion point.
4415        use std::collections::HashMap;
4416        let mut line_deltas: HashMap<usize, (usize, isize)> = HashMap::new();
4417        for op in &ops {
4418            line_deltas.insert(op.line, (op.indent_col, op.col_delta));
4419        }
4420
4421        let line_index = &self.editor.line_index;
4422        let apply_delta = |pos: &mut Position, deltas: &HashMap<usize, (usize, isize)>| {
4423            let Some((indent_col, delta)) = deltas.get(&pos.line) else {
4424                return;
4425            };
4426            if pos.column < *indent_col {
4427                return;
4428            }
4429
4430            let new_col = if *delta >= 0 {
4431                pos.column.saturating_add(*delta as usize)
4432            } else {
4433                pos.column.saturating_sub((-*delta) as usize)
4434            };
4435
4436            pos.column = Self::clamp_column_for_line_with_index(line_index, pos.line, new_col);
4437        };
4438
4439        apply_delta(&mut self.editor.cursor_position, &line_deltas);
4440        if let Some(sel) = &mut self.editor.selection {
4441            apply_delta(&mut sel.start, &line_deltas);
4442            apply_delta(&mut sel.end, &line_deltas);
4443        }
4444        for sel in &mut self.editor.secondary_selections {
4445            apply_delta(&mut sel.start, &line_deltas);
4446            apply_delta(&mut sel.end, &line_deltas);
4447        }
4448
4449        self.normalize_cursor_and_selection();
4450        let after_selection = self.snapshot_selection_set();
4451
4452        let edits: Vec<TextEdit> = ops
4453            .into_iter()
4454            .map(|op| TextEdit {
4455                start_before: op.start_before,
4456                start_after: op.start_after,
4457                deleted_text: op.deleted_text,
4458                inserted_text: op.inserted_text,
4459            })
4460            .collect();
4461
4462        let mut delta_edits: Vec<TextDeltaEdit> = edits
4463            .iter()
4464            .map(|e| TextDeltaEdit {
4465                start: e.start_before,
4466                deleted_text: e.deleted_text.clone(),
4467                inserted_text: e.inserted_text.clone(),
4468            })
4469            .collect();
4470        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
4471
4472        let step = UndoStep {
4473            group_id: 0,
4474            edits,
4475            before_selection,
4476            after_selection,
4477        };
4478        let group_id = self.undo_redo.push_step(step, false);
4479
4480        self.last_text_delta = Some(TextDelta {
4481            before_char_count,
4482            after_char_count: self.editor.piece_table.char_count(),
4483            edits: delta_edits,
4484            undo_group_id: Some(group_id),
4485        });
4486
4487        Ok(CommandResult::Success)
4488    }
4489
4490    fn execute_toggle_block_comment_inline(
4491        &mut self,
4492        block_start: &str,
4493        block_end: &str,
4494        before_char_count: usize,
4495        before_selection: SelectionSetSnapshot,
4496        selections: Vec<Selection>,
4497        primary_index: usize,
4498    ) -> Result<CommandResult, CommandError> {
4499        let start_len = block_start.chars().count();
4500        let end_len = block_end.chars().count();
4501        if start_len == 0 || end_len == 0 {
4502            return Ok(CommandResult::Success);
4503        }
4504
4505        let mut selection_ranges: Vec<SearchMatch> = selections
4506            .iter()
4507            .map(|s| self.selection_char_range(s))
4508            .filter(|r| r.start < r.end)
4509            .collect();
4510
4511        if selection_ranges.is_empty() {
4512            return Ok(CommandResult::Success);
4513        }
4514
4515        selection_ranges.sort_by_key(|r| (r.start, r.end));
4516
4517        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4518        enum TokenOpKind {
4519            Start,
4520            End,
4521        }
4522
4523        // Decide per-selection whether it is already wrapped; then build ops.
4524        struct Op {
4525            start_before: usize,
4526            start_after: usize,
4527            delete_len: usize,
4528            deleted_text: String,
4529            inserted_text: String,
4530            inserted_len: usize,
4531            sel_id: usize,
4532            kind: TokenOpKind,
4533        }
4534
4535        let mut ops: Vec<Op> = Vec::new();
4536
4537        for (sel_id, range) in selection_ranges.iter().enumerate() {
4538            let start = range.start;
4539            let end = range.end;
4540
4541            let already_wrapped = start >= start_len
4542                && end + end_len <= before_char_count
4543                && self
4544                    .editor
4545                    .piece_table
4546                    .get_range(start - start_len, start_len)
4547                    == block_start
4548                && self.editor.piece_table.get_range(end, end_len) == block_end;
4549
4550            if already_wrapped {
4551                // Delete end token first (higher offset), then start token.
4552                let deleted_end = self.editor.piece_table.get_range(end, end_len);
4553                ops.push(Op {
4554                    start_before: end,
4555                    start_after: end,
4556                    delete_len: end_len,
4557                    deleted_text: deleted_end,
4558                    inserted_text: String::new(),
4559                    inserted_len: 0,
4560                    sel_id,
4561                    kind: TokenOpKind::End,
4562                });
4563
4564                let start_token_offset = start - start_len;
4565                let deleted_start = self
4566                    .editor
4567                    .piece_table
4568                    .get_range(start_token_offset, start_len);
4569                ops.push(Op {
4570                    start_before: start_token_offset,
4571                    start_after: start_token_offset,
4572                    delete_len: start_len,
4573                    deleted_text: deleted_start,
4574                    inserted_text: String::new(),
4575                    inserted_len: 0,
4576                    sel_id,
4577                    kind: TokenOpKind::Start,
4578                });
4579            } else {
4580                // Insert end token first (higher offset), then start token.
4581                ops.push(Op {
4582                    start_before: end,
4583                    start_after: end,
4584                    delete_len: 0,
4585                    deleted_text: String::new(),
4586                    inserted_text: block_end.to_string(),
4587                    inserted_len: end_len,
4588                    sel_id,
4589                    kind: TokenOpKind::End,
4590                });
4591                ops.push(Op {
4592                    start_before: start,
4593                    start_after: start,
4594                    delete_len: 0,
4595                    deleted_text: String::new(),
4596                    inserted_text: block_start.to_string(),
4597                    inserted_len: start_len,
4598                    sel_id,
4599                    kind: TokenOpKind::Start,
4600                });
4601            }
4602        }
4603
4604        if ops.is_empty() {
4605            return Ok(CommandResult::Success);
4606        }
4607
4608        ops.sort_by_key(|op| op.start_before);
4609
4610        let mut delta: i64 = 0;
4611        for op in &mut ops {
4612            let effective_start = op.start_before as i64 + delta;
4613            if effective_start < 0 {
4614                return Err(CommandError::Other(
4615                    "ToggleComment produced an invalid intermediate offset".to_string(),
4616                ));
4617            }
4618            op.start_after = effective_start as usize;
4619            delta += op.inserted_len as i64 - op.delete_len as i64;
4620        }
4621
4622        let apply_ops: Vec<(usize, usize, &str)> = ops
4623            .iter()
4624            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
4625            .collect();
4626        self.apply_text_ops(apply_ops)?;
4627
4628        // Keep selections around the inner text between tokens so toggling is repeatable.
4629        let mut new_starts: Vec<usize> = vec![0; selection_ranges.len()];
4630        let mut new_ends: Vec<usize> = vec![0; selection_ranges.len()];
4631
4632        for op in &ops {
4633            match op.kind {
4634                TokenOpKind::Start => {
4635                    new_starts[op.sel_id] = if op.inserted_len > 0 {
4636                        op.start_after + start_len
4637                    } else {
4638                        op.start_after
4639                    };
4640                }
4641                TokenOpKind::End => {
4642                    new_ends[op.sel_id] = op.start_after;
4643                }
4644            }
4645        }
4646
4647        let mut next_selections: Vec<Selection> = Vec::with_capacity(selection_ranges.len());
4648        for i in 0..selection_ranges.len() {
4649            let start = new_starts[i].min(new_ends[i]);
4650            let end = new_starts[i].max(new_ends[i]);
4651            let (start_line, start_col) = self.editor.line_index.char_offset_to_position(start);
4652            let (end_line, end_col) = self.editor.line_index.char_offset_to_position(end);
4653            next_selections.push(Selection {
4654                start: Position::new(start_line, start_col),
4655                end: Position::new(end_line, end_col),
4656                direction: SelectionDirection::Forward,
4657            });
4658        }
4659
4660        self.execute_cursor(CursorCommand::SetSelections {
4661            selections: next_selections,
4662            primary_index: primary_index.min(selection_ranges.len().saturating_sub(1)),
4663        })?;
4664
4665        let after_selection = self.snapshot_selection_set();
4666
4667        let edits: Vec<TextEdit> = ops
4668            .into_iter()
4669            .map(|op| TextEdit {
4670                start_before: op.start_before,
4671                start_after: op.start_after,
4672                deleted_text: op.deleted_text,
4673                inserted_text: op.inserted_text,
4674            })
4675            .collect();
4676
4677        let mut delta_edits: Vec<TextDeltaEdit> = edits
4678            .iter()
4679            .map(|e| TextDeltaEdit {
4680                start: e.start_before,
4681                deleted_text: e.deleted_text.clone(),
4682                inserted_text: e.inserted_text.clone(),
4683            })
4684            .collect();
4685        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
4686
4687        let step = UndoStep {
4688            group_id: 0,
4689            edits,
4690            before_selection,
4691            after_selection,
4692        };
4693        let group_id = self.undo_redo.push_step(step, false);
4694
4695        self.last_text_delta = Some(TextDelta {
4696            before_char_count,
4697            after_char_count: self.editor.piece_table.char_count(),
4698            edits: delta_edits,
4699            undo_group_id: Some(group_id),
4700        });
4701
4702        Ok(CommandResult::Success)
4703    }
4704
4705    fn execute_toggle_block_comment_lines(
4706        &mut self,
4707        block_start: &str,
4708        block_end: &str,
4709        before_char_count: usize,
4710        before_selection: SelectionSetSnapshot,
4711        selections: Vec<Selection>,
4712        primary_index: usize,
4713    ) -> Result<CommandResult, CommandError> {
4714        let start_len = block_start.chars().count();
4715        let end_len = block_end.chars().count();
4716        if start_len == 0 || end_len == 0 {
4717            return Ok(CommandResult::Success);
4718        }
4719
4720        let mut ranges: Vec<(usize, usize)> = Vec::new();
4721        for sel in &selections {
4722            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
4723            let start_line = min_pos.line.min(self.editor.line_index.line_count() - 1);
4724            let end_line = max_pos.line.min(self.editor.line_index.line_count() - 1);
4725
4726            let start = self
4727                .editor
4728                .line_index
4729                .position_to_char_offset(start_line, 0);
4730            let end_line_text = self
4731                .editor
4732                .line_index
4733                .get_line_text(end_line)
4734                .unwrap_or_default();
4735            let end = self
4736                .editor
4737                .line_index
4738                .position_to_char_offset(end_line, end_line_text.chars().count());
4739            if start < end {
4740                ranges.push((start, end));
4741            }
4742        }
4743
4744        ranges.sort_unstable();
4745        ranges.dedup();
4746
4747        if ranges.is_empty() {
4748            return Ok(CommandResult::Success);
4749        }
4750
4751        // Unwrap if every range already starts/ends with the tokens.
4752        let mut all_wrapped = true;
4753        for (start, end) in &ranges {
4754            if *end < *start + start_len + end_len {
4755                all_wrapped = false;
4756                break;
4757            }
4758            let text = self.editor.piece_table.get_range(*start, end - start);
4759            if !text.starts_with(block_start) || !text.ends_with(block_end) {
4760                all_wrapped = false;
4761                break;
4762            }
4763        }
4764
4765        struct Op {
4766            start_before: usize,
4767            start_after: usize,
4768            delete_len: usize,
4769            deleted_text: String,
4770            inserted_text: String,
4771            inserted_len: usize,
4772        }
4773
4774        let mut ops: Vec<Op> = Vec::new();
4775
4776        for (start, end) in &ranges {
4777            if all_wrapped {
4778                // Remove end token (at end-end_len) and start token (at start).
4779                let end_token_start = end.saturating_sub(end_len);
4780                let deleted_end = self.editor.piece_table.get_range(end_token_start, end_len);
4781                ops.push(Op {
4782                    start_before: end_token_start,
4783                    start_after: end_token_start,
4784                    delete_len: end_len,
4785                    deleted_text: deleted_end,
4786                    inserted_text: String::new(),
4787                    inserted_len: 0,
4788                });
4789
4790                let deleted_start = self.editor.piece_table.get_range(*start, start_len);
4791                ops.push(Op {
4792                    start_before: *start,
4793                    start_after: *start,
4794                    delete_len: start_len,
4795                    deleted_text: deleted_start,
4796                    inserted_text: String::new(),
4797                    inserted_len: 0,
4798                });
4799            } else {
4800                // Insert end token then start token.
4801                ops.push(Op {
4802                    start_before: *end,
4803                    start_after: *end,
4804                    delete_len: 0,
4805                    deleted_text: String::new(),
4806                    inserted_text: block_end.to_string(),
4807                    inserted_len: end_len,
4808                });
4809                ops.push(Op {
4810                    start_before: *start,
4811                    start_after: *start,
4812                    delete_len: 0,
4813                    deleted_text: String::new(),
4814                    inserted_text: block_start.to_string(),
4815                    inserted_len: start_len,
4816                });
4817            }
4818        }
4819
4820        ops.sort_by_key(|op| op.start_before);
4821        let mut delta: i64 = 0;
4822        for op in &mut ops {
4823            let effective_start = op.start_before as i64 + delta;
4824            if effective_start < 0 {
4825                return Err(CommandError::Other(
4826                    "ToggleComment produced an invalid intermediate offset".to_string(),
4827                ));
4828            }
4829            op.start_after = effective_start as usize;
4830            delta += op.inserted_len as i64 - op.delete_len as i64;
4831        }
4832
4833        let apply_ops: Vec<(usize, usize, &str)> = ops
4834            .iter()
4835            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
4836            .collect();
4837        self.apply_text_ops(apply_ops)?;
4838
4839        // Keep a single caret at the end of the primary range.
4840        let (primary_start, primary_end) = ranges
4841            .get(primary_index.min(ranges.len().saturating_sub(1)))
4842            .copied()
4843            .unwrap_or((0, 0));
4844        let caret_offset = primary_end.max(primary_start);
4845        let (line, column) = self.editor.line_index.char_offset_to_position(caret_offset);
4846        let pos = Position::new(line, column);
4847        self.execute_cursor(CursorCommand::SetSelections {
4848            selections: vec![Selection {
4849                start: pos,
4850                end: pos,
4851                direction: SelectionDirection::Forward,
4852            }],
4853            primary_index: 0,
4854        })?;
4855
4856        let after_selection = self.snapshot_selection_set();
4857
4858        let edits: Vec<TextEdit> = ops
4859            .into_iter()
4860            .map(|op| TextEdit {
4861                start_before: op.start_before,
4862                start_after: op.start_after,
4863                deleted_text: op.deleted_text,
4864                inserted_text: op.inserted_text,
4865            })
4866            .collect();
4867
4868        let mut delta_edits: Vec<TextDeltaEdit> = edits
4869            .iter()
4870            .map(|e| TextDeltaEdit {
4871                start: e.start_before,
4872                deleted_text: e.deleted_text.clone(),
4873                inserted_text: e.inserted_text.clone(),
4874            })
4875            .collect();
4876        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
4877
4878        let step = UndoStep {
4879            group_id: 0,
4880            edits,
4881            before_selection,
4882            after_selection,
4883        };
4884        let group_id = self.undo_redo.push_step(step, false);
4885
4886        self.last_text_delta = Some(TextDelta {
4887            before_char_count,
4888            after_char_count: self.editor.piece_table.char_count(),
4889            edits: delta_edits,
4890            undo_group_id: Some(group_id),
4891        });
4892
4893        Ok(CommandResult::Success)
4894    }
4895
4896    fn is_word_char(ch: char) -> bool {
4897        ch == '_' || ch.is_alphanumeric()
4898    }
4899
4900    fn word_range_in_line(line_text: &str, column: usize) -> Option<(usize, usize)> {
4901        if line_text.is_empty() {
4902            return None;
4903        }
4904
4905        let mut parts: Vec<(usize, usize, &str)> = Vec::new();
4906        for (start, part) in line_text.split_word_bound_indices() {
4907            let end = start + part.len();
4908            parts.push((start, end, part));
4909        }
4910        if parts.is_empty() {
4911            return None;
4912        }
4913
4914        let byte_pos =
4915            byte_offset_for_char_column(line_text, column.min(line_text.chars().count()));
4916
4917        let mut part_idx = parts
4918            .iter()
4919            .position(|(s, e, _)| *s <= byte_pos && byte_pos < *e)
4920            .or_else(|| parts.iter().position(|(s, _, _)| *s == byte_pos))
4921            .unwrap_or_else(|| parts.len().saturating_sub(1));
4922
4923        let pick_part = |idx: usize, parts: &[(usize, usize, &str)]| -> Option<(usize, usize)> {
4924            let (s, e, text) = parts.get(idx)?;
4925            if text.chars().any(Self::is_word_char) {
4926                Some((*s, *e))
4927            } else {
4928                None
4929            }
4930        };
4931
4932        // Prefer the part under the caret.
4933        if let Some((s, e)) = pick_part(part_idx, &parts) {
4934            return Some((
4935                char_column_for_byte_offset(line_text, s),
4936                char_column_for_byte_offset(line_text, e),
4937            ));
4938        }
4939
4940        // Search to the right.
4941        for idx in part_idx + 1..parts.len() {
4942            if let Some((s, e)) = pick_part(idx, &parts) {
4943                return Some((
4944                    char_column_for_byte_offset(line_text, s),
4945                    char_column_for_byte_offset(line_text, e),
4946                ));
4947            }
4948        }
4949
4950        // Search to the left.
4951        while part_idx > 0 {
4952            part_idx -= 1;
4953            if let Some((s, e)) = pick_part(part_idx, &parts) {
4954                return Some((
4955                    char_column_for_byte_offset(line_text, s),
4956                    char_column_for_byte_offset(line_text, e),
4957                ));
4958            }
4959        }
4960
4961        None
4962    }
4963
4964    fn execute_select_line_command(&mut self) -> Result<CommandResult, CommandError> {
4965        let snapshot = self.snapshot_selection_set();
4966        let selections = snapshot.selections;
4967        let primary_index = snapshot.primary_index;
4968
4969        let line_count = self.editor.line_index.line_count();
4970        if line_count == 0 {
4971            return Ok(CommandResult::Success);
4972        }
4973
4974        let mut next: Vec<Selection> = Vec::with_capacity(selections.len());
4975        for sel in selections {
4976            let (min_pos, max_pos) = crate::selection_set::selection_min_max(&sel);
4977            let start_line = min_pos.line.min(line_count.saturating_sub(1));
4978            let end_line = max_pos.line.min(line_count.saturating_sub(1));
4979
4980            let start = Position::new(start_line, 0);
4981            let end = if end_line + 1 < line_count {
4982                Position::new(end_line + 1, 0)
4983            } else {
4984                let line_text = self
4985                    .editor
4986                    .line_index
4987                    .get_line_text(end_line)
4988                    .unwrap_or_default();
4989                Position::new(end_line, line_text.chars().count())
4990            };
4991
4992            next.push(Selection {
4993                start,
4994                end,
4995                direction: SelectionDirection::Forward,
4996            });
4997        }
4998
4999        self.execute_cursor(CursorCommand::SetSelections {
5000            selections: next,
5001            primary_index,
5002        })?;
5003        Ok(CommandResult::Success)
5004    }
5005
5006    fn execute_select_word_command(&mut self) -> Result<CommandResult, CommandError> {
5007        let snapshot = self.snapshot_selection_set();
5008        let selections = snapshot.selections;
5009        let primary_index = snapshot.primary_index;
5010
5011        let line_count = self.editor.line_index.line_count();
5012        if line_count == 0 {
5013            return Ok(CommandResult::Success);
5014        }
5015
5016        let mut next: Vec<Selection> = Vec::with_capacity(selections.len());
5017
5018        for sel in selections {
5019            // If already a non-empty selection, keep it.
5020            if sel.start != sel.end {
5021                next.push(sel);
5022                continue;
5023            }
5024
5025            let caret = sel.end;
5026            let line = caret.line.min(line_count.saturating_sub(1));
5027            let line_text = self
5028                .editor
5029                .line_index
5030                .get_line_text(line)
5031                .unwrap_or_default();
5032            let col = caret.column.min(line_text.chars().count());
5033
5034            let Some((start_col, end_col)) = Self::word_range_in_line(&line_text, col) else {
5035                next.push(sel);
5036                continue;
5037            };
5038
5039            let start = Position::new(line, start_col);
5040            let end = Position::new(line, end_col);
5041
5042            next.push(Selection {
5043                start,
5044                end,
5045                direction: SelectionDirection::Forward,
5046            });
5047        }
5048
5049        self.execute_cursor(CursorCommand::SetSelections {
5050            selections: next,
5051            primary_index,
5052        })?;
5053        Ok(CommandResult::Success)
5054    }
5055
5056    fn execute_expand_selection_command(&mut self) -> Result<CommandResult, CommandError> {
5057        // Basic expand policy:
5058        // - empty selection => select word
5059        // - non-empty selection => select line(s)
5060        let snapshot = self.snapshot_selection_set();
5061        if snapshot.selections.iter().any(|s| s.start != s.end) {
5062            self.execute_select_line_command()
5063        } else {
5064            self.execute_select_word_command()
5065        }
5066    }
5067
5068    fn execute_add_cursor_vertical_command(
5069        &mut self,
5070        above: bool,
5071    ) -> Result<CommandResult, CommandError> {
5072        let snapshot = self.snapshot_selection_set();
5073        let mut selections = snapshot.selections;
5074        let primary_index = snapshot.primary_index;
5075
5076        let line_count = self.editor.line_index.line_count();
5077        if line_count == 0 {
5078            return Ok(CommandResult::Success);
5079        }
5080
5081        let mut extra: Vec<Selection> = Vec::new();
5082        for sel in &selections {
5083            let caret = sel.end;
5084            let target_line = if above {
5085                if caret.line == 0 {
5086                    continue;
5087                }
5088                caret.line - 1
5089            } else {
5090                let next = caret.line + 1;
5091                if next >= line_count {
5092                    continue;
5093                }
5094                next
5095            };
5096
5097            let col = self.clamp_column_for_line(target_line, caret.column);
5098            let pos = Position::new(target_line, col);
5099            extra.push(Selection {
5100                start: pos,
5101                end: pos,
5102                direction: SelectionDirection::Forward,
5103            });
5104        }
5105
5106        if extra.is_empty() {
5107            return Ok(CommandResult::Success);
5108        }
5109
5110        selections.extend(extra);
5111
5112        self.execute_cursor(CursorCommand::SetSelections {
5113            selections,
5114            primary_index,
5115        })?;
5116        Ok(CommandResult::Success)
5117    }
5118
5119    fn selection_query(
5120        &self,
5121        selections: &[Selection],
5122        primary_index: usize,
5123    ) -> Option<(String, Option<SearchMatch>)> {
5124        let primary = selections.get(primary_index)?;
5125        let range = self.selection_char_range(primary);
5126
5127        if range.start != range.end {
5128            let len = range.end - range.start;
5129            return Some((
5130                self.editor.piece_table.get_range(range.start, len),
5131                Some(range),
5132            ));
5133        }
5134
5135        let caret = primary.end;
5136        let line_text = self
5137            .editor
5138            .line_index
5139            .get_line_text(caret.line)
5140            .unwrap_or_default();
5141        let col = caret.column.min(line_text.chars().count());
5142        let (start_col, end_col) = Self::word_range_in_line(&line_text, col)?;
5143        if start_col == end_col {
5144            return None;
5145        }
5146
5147        let start = self
5148            .editor
5149            .line_index
5150            .position_to_char_offset(caret.line, start_col);
5151        let end = self
5152            .editor
5153            .line_index
5154            .position_to_char_offset(caret.line, end_col);
5155        let range = SearchMatch {
5156            start,
5157            end: end.max(start),
5158        };
5159        Some((
5160            self.editor
5161                .piece_table
5162                .get_range(range.start, range.end.saturating_sub(range.start)),
5163            Some(range),
5164        ))
5165    }
5166
5167    fn execute_add_next_occurrence_command(
5168        &mut self,
5169        options: SearchOptions,
5170    ) -> Result<CommandResult, CommandError> {
5171        let snapshot = self.snapshot_selection_set();
5172        let mut selections = snapshot.selections;
5173        let primary_index = snapshot.primary_index;
5174
5175        let Some((query, primary_range)) = self.selection_query(&selections, primary_index) else {
5176            return Ok(CommandResult::Success);
5177        };
5178        if query.is_empty() {
5179            return Ok(CommandResult::Success);
5180        }
5181
5182        // VSCode-like: if there is no active selection, first select the current word occurrence.
5183        if let Some(primary_range) = primary_range
5184            && primary_range.start != primary_range.end
5185        {
5186            let current = selections
5187                .get(primary_index)
5188                .map(|s| self.selection_char_range(s))
5189                .unwrap_or(SearchMatch { start: 0, end: 0 });
5190            if current.start == current.end {
5191                let (start_line, start_col) = self
5192                    .editor
5193                    .line_index
5194                    .char_offset_to_position(primary_range.start);
5195                let (end_line, end_col) = self
5196                    .editor
5197                    .line_index
5198                    .char_offset_to_position(primary_range.end);
5199                if let Some(sel) = selections.get_mut(primary_index) {
5200                    *sel = Selection {
5201                        start: Position::new(start_line, start_col),
5202                        end: Position::new(end_line, end_col),
5203                        direction: SelectionDirection::Forward,
5204                    };
5205                }
5206            }
5207        }
5208
5209        let text = self.editor.piece_table.get_text();
5210
5211        let mut ranges: Vec<SearchMatch> = selections
5212            .iter()
5213            .map(|s| self.selection_char_range(s))
5214            .filter(|r| r.start != r.end)
5215            .collect();
5216
5217        if let Some(primary_range) = primary_range
5218            && primary_range.start != primary_range.end
5219            && !ranges
5220                .iter()
5221                .any(|r| r.start == primary_range.start && r.end == primary_range.end)
5222        {
5223            ranges.push(primary_range);
5224        }
5225
5226        let mut existing: Vec<(usize, usize)> = ranges
5227            .iter()
5228            .map(|r| (r.start.min(r.end), r.end.max(r.start)))
5229            .collect();
5230        existing.sort_unstable();
5231
5232        let from = existing.iter().map(|(_, end)| *end).max().unwrap_or(0);
5233
5234        let mut search_from = from;
5235        let mut wrapped = false;
5236        let mut found: Option<SearchMatch> = None;
5237
5238        loop {
5239            let next = find_next(&text, &query, options, search_from)
5240                .map_err(|err| CommandError::Other(err.to_string()))?;
5241
5242            let Some(m) = next else {
5243                if wrapped {
5244                    break;
5245                }
5246                wrapped = true;
5247                search_from = 0;
5248                continue;
5249            };
5250
5251            let overlaps = existing.iter().any(|(s, e)| m.start < *e && m.end > *s);
5252
5253            if overlaps {
5254                if m.end >= text.chars().count() {
5255                    break;
5256                }
5257                search_from = m.end + 1;
5258                continue;
5259            }
5260
5261            found = Some(m);
5262            break;
5263        }
5264
5265        let Some(m) = found else {
5266            return Ok(CommandResult::Success);
5267        };
5268
5269        let (start_line, start_col) = self.editor.line_index.char_offset_to_position(m.start);
5270        let (end_line, end_col) = self.editor.line_index.char_offset_to_position(m.end);
5271
5272        selections.push(Selection {
5273            start: Position::new(start_line, start_col),
5274            end: Position::new(end_line, end_col),
5275            direction: SelectionDirection::Forward,
5276        });
5277
5278        let new_primary_index = selections.len().saturating_sub(1);
5279        self.execute_cursor(CursorCommand::SetSelections {
5280            selections,
5281            primary_index: new_primary_index,
5282        })?;
5283
5284        Ok(CommandResult::Success)
5285    }
5286
5287    fn execute_add_all_occurrences_command(
5288        &mut self,
5289        options: SearchOptions,
5290    ) -> Result<CommandResult, CommandError> {
5291        let snapshot = self.snapshot_selection_set();
5292        let selections = snapshot.selections;
5293        let primary_index = snapshot.primary_index;
5294
5295        let Some((query, primary_range)) = self.selection_query(&selections, primary_index) else {
5296            return Ok(CommandResult::Success);
5297        };
5298        if query.is_empty() {
5299            return Ok(CommandResult::Success);
5300        }
5301
5302        let text = self.editor.piece_table.get_text();
5303        let matches =
5304            find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
5305
5306        if matches.is_empty() {
5307            return Ok(CommandResult::Success);
5308        }
5309
5310        let mut out: Vec<Selection> = Vec::with_capacity(matches.len());
5311        let mut next_primary = 0usize;
5312        let primary_range = primary_range.filter(|r| r.start != r.end);
5313
5314        for (idx, m) in matches.iter().enumerate() {
5315            let (start_line, start_col) = self.editor.line_index.char_offset_to_position(m.start);
5316            let (end_line, end_col) = self.editor.line_index.char_offset_to_position(m.end);
5317            out.push(Selection {
5318                start: Position::new(start_line, start_col),
5319                end: Position::new(end_line, end_col),
5320                direction: SelectionDirection::Forward,
5321            });
5322
5323            if let Some(pr) = primary_range
5324                && pr.start == m.start
5325                && pr.end == m.end
5326            {
5327                next_primary = idx;
5328            }
5329        }
5330
5331        self.execute_cursor(CursorCommand::SetSelections {
5332            selections: out,
5333            primary_index: next_primary,
5334        })?;
5335
5336        Ok(CommandResult::Success)
5337    }
5338
5339    fn execute_insert_command(
5340        &mut self,
5341        offset: usize,
5342        text: String,
5343    ) -> Result<CommandResult, CommandError> {
5344        if text.is_empty() {
5345            return Err(CommandError::EmptyText);
5346        }
5347
5348        let text = crate::text::normalize_crlf_to_lf_string(text);
5349        let max_offset = self.editor.piece_table.char_count();
5350        if offset > max_offset {
5351            return Err(CommandError::InvalidOffset(offset));
5352        }
5353
5354        let before_char_count = self.editor.piece_table.char_count();
5355        let before_selection = self.snapshot_selection_set();
5356
5357        let affected_line = self.editor.line_index.char_offset_to_position(offset).0;
5358        let inserted_newlines = text.as_bytes().iter().filter(|b| **b == b'\n').count();
5359
5360        // Execute insertion
5361        self.editor.piece_table.insert(offset, &text);
5362
5363        // Update line index + layout engine incrementally.
5364        self.apply_text_change_to_line_index_and_layout(offset, "", &text);
5365
5366        if inserted_newlines > 0 {
5367            self.editor
5368                .folding_manager
5369                .apply_line_delta(affected_line, inserted_newlines as isize);
5370            self.editor
5371                .folding_manager
5372                .clamp_to_line_count(self.editor.line_index.line_count());
5373        }
5374
5375        let inserted_len = text.chars().count();
5376
5377        // Update interval tree offsets
5378        self.editor
5379            .interval_tree
5380            .update_for_insertion(offset, inserted_len);
5381        for layer_tree in self.editor.style_layers.values_mut() {
5382            layer_tree.update_for_insertion(offset, inserted_len);
5383        }
5384
5385        // Ensure cursor/selection still within valid range
5386        self.normalize_cursor_and_selection();
5387
5388        let after_selection = self.snapshot_selection_set();
5389
5390        let step = UndoStep {
5391            group_id: 0,
5392            edits: vec![TextEdit {
5393                start_before: offset,
5394                start_after: offset,
5395                deleted_text: String::new(),
5396                inserted_text: text.clone(),
5397            }],
5398            before_selection,
5399            after_selection,
5400        };
5401
5402        let coalescible_insert = !text.contains('\n');
5403        let group_id = self.undo_redo.push_step(step, coalescible_insert);
5404
5405        self.last_text_delta = Some(TextDelta {
5406            before_char_count,
5407            after_char_count: self.editor.piece_table.char_count(),
5408            edits: vec![TextDeltaEdit {
5409                start: offset,
5410                deleted_text: String::new(),
5411                inserted_text: text,
5412            }],
5413            undo_group_id: Some(group_id),
5414        });
5415
5416        Ok(CommandResult::Success)
5417    }
5418
5419    fn execute_delete_command(
5420        &mut self,
5421        start: usize,
5422        length: usize,
5423    ) -> Result<CommandResult, CommandError> {
5424        if length == 0 {
5425            return Ok(CommandResult::Success);
5426        }
5427
5428        let before_char_count = self.editor.piece_table.char_count();
5429        let max_offset = self.editor.piece_table.char_count();
5430        if start > max_offset {
5431            return Err(CommandError::InvalidOffset(start));
5432        }
5433        if start + length > max_offset {
5434            return Err(CommandError::InvalidRange {
5435                start,
5436                end: start + length,
5437            });
5438        }
5439
5440        let before_selection = self.snapshot_selection_set();
5441
5442        let deleted_text = self.editor.piece_table.get_range(start, length);
5443        let delta_deleted_text = deleted_text.clone();
5444        let deleted_newlines = deleted_text
5445            .as_bytes()
5446            .iter()
5447            .filter(|b| **b == b'\n')
5448            .count();
5449        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
5450
5451        // Execute deletion
5452        self.editor.piece_table.delete(start, length);
5453
5454        // Update line index + layout engine incrementally.
5455        self.apply_text_change_to_line_index_and_layout(start, &delta_deleted_text, "");
5456
5457        if deleted_newlines > 0 {
5458            self.editor
5459                .folding_manager
5460                .apply_line_delta(affected_line, -(deleted_newlines as isize));
5461            self.editor
5462                .folding_manager
5463                .clamp_to_line_count(self.editor.line_index.line_count());
5464        }
5465
5466        // Update interval tree offsets
5467        self.editor
5468            .interval_tree
5469            .update_for_deletion(start, start + length);
5470        for layer_tree in self.editor.style_layers.values_mut() {
5471            layer_tree.update_for_deletion(start, start + length);
5472        }
5473
5474        // Ensure cursor/selection still within valid range
5475        self.normalize_cursor_and_selection();
5476
5477        let after_selection = self.snapshot_selection_set();
5478
5479        let step = UndoStep {
5480            group_id: 0,
5481            edits: vec![TextEdit {
5482                start_before: start,
5483                start_after: start,
5484                deleted_text,
5485                inserted_text: String::new(),
5486            }],
5487            before_selection,
5488            after_selection,
5489        };
5490        let group_id = self.undo_redo.push_step(step, false);
5491
5492        self.last_text_delta = Some(TextDelta {
5493            before_char_count,
5494            after_char_count: self.editor.piece_table.char_count(),
5495            edits: vec![TextDeltaEdit {
5496                start,
5497                deleted_text: delta_deleted_text,
5498                inserted_text: String::new(),
5499            }],
5500            undo_group_id: Some(group_id),
5501        });
5502
5503        Ok(CommandResult::Success)
5504    }
5505
5506    fn execute_replace_command(
5507        &mut self,
5508        start: usize,
5509        length: usize,
5510        text: String,
5511    ) -> Result<CommandResult, CommandError> {
5512        let before_char_count = self.editor.piece_table.char_count();
5513        let max_offset = self.editor.piece_table.char_count();
5514        if start > max_offset {
5515            return Err(CommandError::InvalidOffset(start));
5516        }
5517        if start + length > max_offset {
5518            return Err(CommandError::InvalidRange {
5519                start,
5520                end: start + length,
5521            });
5522        }
5523
5524        if length == 0 && text.is_empty() {
5525            return Ok(CommandResult::Success);
5526        }
5527
5528        let text = crate::text::normalize_crlf_to_lf_string(text);
5529        let before_selection = self.snapshot_selection_set();
5530
5531        let deleted_text = if length == 0 {
5532            String::new()
5533        } else {
5534            self.editor.piece_table.get_range(start, length)
5535        };
5536        let delta_deleted_text = deleted_text.clone();
5537        let delta_inserted_text = text.clone();
5538
5539        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
5540        let deleted_newlines = deleted_text
5541            .as_bytes()
5542            .iter()
5543            .filter(|b| **b == b'\n')
5544            .count();
5545        let inserted_newlines = text.as_bytes().iter().filter(|b| **b == b'\n').count();
5546        let line_delta = inserted_newlines as isize - deleted_newlines as isize;
5547
5548        // Apply as a single operation (delete then insert at the same offset).
5549        if length > 0 {
5550            self.editor.piece_table.delete(start, length);
5551            self.editor
5552                .interval_tree
5553                .update_for_deletion(start, start + length);
5554            for layer_tree in self.editor.style_layers.values_mut() {
5555                layer_tree.update_for_deletion(start, start + length);
5556            }
5557        }
5558
5559        let inserted_len = text.chars().count();
5560        if inserted_len > 0 {
5561            self.editor.piece_table.insert(start, &text);
5562            self.editor
5563                .interval_tree
5564                .update_for_insertion(start, inserted_len);
5565            for layer_tree in self.editor.style_layers.values_mut() {
5566                layer_tree.update_for_insertion(start, inserted_len);
5567            }
5568        }
5569
5570        // Update line index + layout engine incrementally.
5571        self.apply_text_change_to_line_index_and_layout(start, &deleted_text, &text);
5572
5573        if line_delta != 0 {
5574            self.editor
5575                .folding_manager
5576                .apply_line_delta(affected_line, line_delta);
5577            self.editor
5578                .folding_manager
5579                .clamp_to_line_count(self.editor.line_index.line_count());
5580        }
5581
5582        // Ensure cursor/selection still valid.
5583        self.normalize_cursor_and_selection();
5584
5585        let after_selection = self.snapshot_selection_set();
5586
5587        let step = UndoStep {
5588            group_id: 0,
5589            edits: vec![TextEdit {
5590                start_before: start,
5591                start_after: start,
5592                deleted_text,
5593                inserted_text: text,
5594            }],
5595            before_selection,
5596            after_selection,
5597        };
5598        let group_id = self.undo_redo.push_step(step, false);
5599
5600        self.last_text_delta = Some(TextDelta {
5601            before_char_count,
5602            after_char_count: self.editor.piece_table.char_count(),
5603            edits: vec![TextDeltaEdit {
5604                start,
5605                deleted_text: delta_deleted_text,
5606                inserted_text: delta_inserted_text,
5607            }],
5608            undo_group_id: Some(group_id),
5609        });
5610
5611        Ok(CommandResult::Success)
5612    }
5613
5614    fn cursor_char_offset(&self) -> usize {
5615        self.position_to_char_offset_clamped(self.editor.cursor_position)
5616    }
5617
5618    fn primary_selection_char_range(&self) -> Option<SearchMatch> {
5619        let selection = self.editor.selection.as_ref()?;
5620        let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
5621        let start = self.position_to_char_offset_clamped(min_pos);
5622        let end = self.position_to_char_offset_clamped(max_pos);
5623        if start == end {
5624            None
5625        } else {
5626            Some(SearchMatch { start, end })
5627        }
5628    }
5629
5630    fn set_primary_selection_by_char_range(&mut self, range: SearchMatch) {
5631        let (start_line, start_col) = self.editor.line_index.char_offset_to_position(range.start);
5632        let (end_line, end_col) = self.editor.line_index.char_offset_to_position(range.end);
5633
5634        self.editor.cursor_position = Position::new(end_line, end_col);
5635        self.editor.secondary_selections.clear();
5636
5637        if range.start == range.end {
5638            self.editor.selection = None;
5639        } else {
5640            self.editor.selection = Some(Selection {
5641                start: Position::new(start_line, start_col),
5642                end: Position::new(end_line, end_col),
5643                direction: SelectionDirection::Forward,
5644            });
5645        }
5646    }
5647
5648    fn execute_find_command(
5649        &mut self,
5650        query: String,
5651        options: SearchOptions,
5652        forward: bool,
5653    ) -> Result<CommandResult, CommandError> {
5654        if query.is_empty() {
5655            return Ok(CommandResult::SearchNotFound);
5656        }
5657
5658        let text = self.editor.piece_table.get_text();
5659        let from = if let Some(selection) = self.primary_selection_char_range() {
5660            if forward {
5661                selection.end
5662            } else {
5663                selection.start
5664            }
5665        } else {
5666            self.cursor_char_offset()
5667        };
5668
5669        let found = if forward {
5670            find_next(&text, &query, options, from)
5671        } else {
5672            find_prev(&text, &query, options, from)
5673        }
5674        .map_err(|err| CommandError::Other(err.to_string()))?;
5675
5676        let Some(m) = found else {
5677            return Ok(CommandResult::SearchNotFound);
5678        };
5679
5680        self.set_primary_selection_by_char_range(m);
5681
5682        Ok(CommandResult::SearchMatch {
5683            start: m.start,
5684            end: m.end,
5685        })
5686    }
5687
5688    fn compile_user_regex(
5689        query: &str,
5690        options: SearchOptions,
5691    ) -> Result<regex::Regex, CommandError> {
5692        RegexBuilder::new(query)
5693            .case_insensitive(!options.case_sensitive)
5694            .multi_line(true)
5695            .build()
5696            .map_err(|err| CommandError::Other(format!("Invalid regex: {}", err)))
5697    }
5698
5699    fn regex_expand_replacement(
5700        re: &regex::Regex,
5701        text: &str,
5702        index: &CharIndex,
5703        range: SearchMatch,
5704        replacement: &str,
5705    ) -> Result<String, CommandError> {
5706        let start_byte = index.char_to_byte(range.start);
5707        let end_byte = index.char_to_byte(range.end);
5708
5709        let caps = re
5710            .captures_at(text, start_byte)
5711            .ok_or_else(|| CommandError::Other("Regex match not found".to_string()))?;
5712        let whole = caps
5713            .get(0)
5714            .ok_or_else(|| CommandError::Other("Regex match missing capture 0".to_string()))?;
5715        if whole.start() != start_byte || whole.end() != end_byte {
5716            return Err(CommandError::Other(
5717                "Regex match did not align with the selected range".to_string(),
5718            ));
5719        }
5720
5721        let mut expanded = String::new();
5722        caps.expand(replacement, &mut expanded);
5723        Ok(expanded)
5724    }
5725
5726    fn execute_replace_current_command(
5727        &mut self,
5728        query: String,
5729        replacement: String,
5730        options: SearchOptions,
5731    ) -> Result<CommandResult, CommandError> {
5732        if query.is_empty() {
5733            return Err(CommandError::Other("Search query is empty".to_string()));
5734        }
5735
5736        let text = self.editor.piece_table.get_text();
5737        let selection_range = self.primary_selection_char_range();
5738
5739        let mut target = None::<SearchMatch>;
5740        if let Some(range) = selection_range {
5741            let is_match = crate::search::is_match_exact(&text, &query, options, range)
5742                .map_err(|err| CommandError::Other(err.to_string()))?;
5743            if is_match {
5744                target = Some(range);
5745            }
5746        }
5747
5748        if target.is_none() {
5749            let from = self.cursor_char_offset();
5750            target = find_next(&text, &query, options, from)
5751                .map_err(|err| CommandError::Other(err.to_string()))?;
5752        }
5753
5754        let Some(target) = target else {
5755            return Err(CommandError::Other("No match found".to_string()));
5756        };
5757
5758        let index = CharIndex::new(&text);
5759        let inserted_text = if options.regex {
5760            let re = Self::compile_user_regex(&query, options)?;
5761            Self::regex_expand_replacement(&re, &text, &index, target, &replacement)?
5762        } else {
5763            replacement
5764        };
5765        let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
5766
5767        let deleted_text = self
5768            .editor
5769            .piece_table
5770            .get_range(target.start, target.len());
5771        let before_char_count = self.editor.piece_table.char_count();
5772        let delta_deleted_text = deleted_text.clone();
5773
5774        let before_selection = self.snapshot_selection_set();
5775        self.apply_text_ops(vec![(target.start, target.len(), inserted_text.as_str())])?;
5776
5777        let inserted_len = inserted_text.chars().count();
5778        let new_range = SearchMatch {
5779            start: target.start,
5780            end: target.start + inserted_len,
5781        };
5782        self.set_primary_selection_by_char_range(new_range);
5783        let after_selection = self.snapshot_selection_set();
5784
5785        let step = UndoStep {
5786            group_id: 0,
5787            edits: vec![TextEdit {
5788                start_before: target.start,
5789                start_after: target.start,
5790                deleted_text,
5791                inserted_text: inserted_text.clone(),
5792            }],
5793            before_selection,
5794            after_selection,
5795        };
5796        let group_id = self.undo_redo.push_step(step, false);
5797
5798        self.last_text_delta = Some(TextDelta {
5799            before_char_count,
5800            after_char_count: self.editor.piece_table.char_count(),
5801            edits: vec![TextDeltaEdit {
5802                start: target.start,
5803                deleted_text: delta_deleted_text,
5804                inserted_text,
5805            }],
5806            undo_group_id: Some(group_id),
5807        });
5808
5809        Ok(CommandResult::ReplaceResult { replaced: 1 })
5810    }
5811
5812    fn execute_replace_all_command(
5813        &mut self,
5814        query: String,
5815        replacement: String,
5816        options: SearchOptions,
5817    ) -> Result<CommandResult, CommandError> {
5818        if query.is_empty() {
5819            return Err(CommandError::Other("Search query is empty".to_string()));
5820        }
5821
5822        let replacement = crate::text::normalize_crlf_to_lf_string(replacement);
5823        let text = self.editor.piece_table.get_text();
5824        let matches =
5825            find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
5826        if matches.is_empty() {
5827            return Err(CommandError::Other("No match found".to_string()));
5828        }
5829        let match_count = matches.len();
5830
5831        let index = CharIndex::new(&text);
5832
5833        struct Op {
5834            start_before: usize,
5835            start_after: usize,
5836            delete_len: usize,
5837            deleted_text: String,
5838            inserted_text: String,
5839            inserted_len: usize,
5840        }
5841
5842        let mut ops: Vec<Op> = Vec::with_capacity(match_count);
5843        if options.regex {
5844            let re = Self::compile_user_regex(&query, options)?;
5845            for m in matches {
5846                let deleted_text = {
5847                    let start_byte = index.char_to_byte(m.start);
5848                    let end_byte = index.char_to_byte(m.end);
5849                    text.get(start_byte..end_byte)
5850                        .unwrap_or_default()
5851                        .to_string()
5852                };
5853                let inserted_text =
5854                    Self::regex_expand_replacement(&re, &text, &index, m, &replacement)?;
5855                let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
5856                let inserted_len = inserted_text.chars().count();
5857                ops.push(Op {
5858                    start_before: m.start,
5859                    start_after: m.start,
5860                    delete_len: m.len(),
5861                    deleted_text,
5862                    inserted_text,
5863                    inserted_len,
5864                });
5865            }
5866        } else {
5867            let inserted_len = replacement.chars().count();
5868            for m in matches {
5869                let deleted_text = {
5870                    let start_byte = index.char_to_byte(m.start);
5871                    let end_byte = index.char_to_byte(m.end);
5872                    text.get(start_byte..end_byte)
5873                        .unwrap_or_default()
5874                        .to_string()
5875                };
5876                ops.push(Op {
5877                    start_before: m.start,
5878                    start_after: m.start,
5879                    delete_len: m.len(),
5880                    deleted_text,
5881                    inserted_text: replacement.clone(),
5882                    inserted_len,
5883                });
5884            }
5885        }
5886
5887        ops.sort_by_key(|op| op.start_before);
5888
5889        let mut delta: i64 = 0;
5890        for op in &mut ops {
5891            let effective_start = op.start_before as i64 + delta;
5892            if effective_start < 0 {
5893                return Err(CommandError::Other(
5894                    "ReplaceAll produced an invalid intermediate offset".to_string(),
5895                ));
5896            }
5897            op.start_after = effective_start as usize;
5898            delta += op.inserted_len as i64 - op.delete_len as i64;
5899        }
5900
5901        let before_char_count = self.editor.piece_table.char_count();
5902        let before_selection = self.snapshot_selection_set();
5903        let apply_ops: Vec<(usize, usize, &str)> = ops
5904            .iter()
5905            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
5906            .collect();
5907        self.apply_text_ops(apply_ops)?;
5908
5909        if let Some(first) = ops.first() {
5910            let caret_end = first.start_after + first.inserted_len;
5911            let select_end = if first.inserted_len == 0 {
5912                first.start_after
5913            } else {
5914                caret_end
5915            };
5916            self.set_primary_selection_by_char_range(SearchMatch {
5917                start: first.start_after,
5918                end: select_end,
5919            });
5920        } else {
5921            self.editor.selection = None;
5922            self.editor.secondary_selections.clear();
5923        }
5924
5925        let after_selection = self.snapshot_selection_set();
5926
5927        let edits: Vec<TextEdit> = ops
5928            .into_iter()
5929            .map(|op| TextEdit {
5930                start_before: op.start_before,
5931                start_after: op.start_after,
5932                deleted_text: op.deleted_text,
5933                inserted_text: op.inserted_text,
5934            })
5935            .collect();
5936
5937        let mut delta_edits: Vec<TextDeltaEdit> = edits
5938            .iter()
5939            .map(|e| TextDeltaEdit {
5940                start: e.start_before,
5941                deleted_text: e.deleted_text.clone(),
5942                inserted_text: e.inserted_text.clone(),
5943            })
5944            .collect();
5945        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
5946
5947        let step = UndoStep {
5948            group_id: 0,
5949            edits,
5950            before_selection,
5951            after_selection,
5952        };
5953        let group_id = self.undo_redo.push_step(step, false);
5954
5955        self.last_text_delta = Some(TextDelta {
5956            before_char_count,
5957            after_char_count: self.editor.piece_table.char_count(),
5958            edits: delta_edits,
5959            undo_group_id: Some(group_id),
5960        });
5961
5962        Ok(CommandResult::ReplaceResult {
5963            replaced: match_count,
5964        })
5965    }
5966
5967    fn execute_backspace_command(&mut self) -> Result<CommandResult, CommandError> {
5968        self.execute_delete_like_command(false)
5969    }
5970
5971    fn execute_delete_forward_command(&mut self) -> Result<CommandResult, CommandError> {
5972        self.execute_delete_like_command(true)
5973    }
5974
5975    fn execute_delete_to_prev_tab_stop_command(&mut self) -> Result<CommandResult, CommandError> {
5976        // Treat like a delete-like action: end any open insert coalescing group, even if it turns out
5977        // to be a no-op.
5978        self.undo_redo.end_group();
5979
5980        let before_selection = self.snapshot_selection_set();
5981        let selections = before_selection.selections.clone();
5982        let primary_index = before_selection.primary_index;
5983
5984        let tab_width = self.editor.layout_engine.tab_width().max(1);
5985
5986        #[derive(Debug)]
5987        struct Op {
5988            selection_index: usize,
5989            start_offset: usize,
5990            delete_len: usize,
5991            deleted_text: String,
5992            start_after: usize,
5993        }
5994
5995        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
5996
5997        for (selection_index, selection) in selections.iter().enumerate() {
5998            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
5999                (selection.start, selection.end)
6000            } else {
6001                (selection.end, selection.start)
6002            };
6003
6004            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
6005                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
6006                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
6007                if start_offset <= end_offset {
6008                    (start_offset, end_offset)
6009                } else {
6010                    (end_offset, start_offset)
6011                }
6012            } else {
6013                let caret = selection.end;
6014                let caret_offset = self.position_to_char_offset_clamped(caret);
6015                if caret_offset == 0 {
6016                    (0, 0)
6017                } else {
6018                    let line_text = self
6019                        .editor
6020                        .line_index
6021                        .get_line_text(caret.line)
6022                        .unwrap_or_default();
6023                    let line_char_len = line_text.chars().count();
6024                    let col = caret.column.min(line_char_len);
6025
6026                    let in_leading_whitespace = line_text
6027                        .chars()
6028                        .take(col)
6029                        .all(|ch| ch == ' ' || ch == '\t');
6030
6031                    if !in_leading_whitespace {
6032                        (caret_offset - 1, caret_offset)
6033                    } else {
6034                        let x_in_line = visual_x_for_column(&line_text, col, tab_width);
6035                        let back = if x_in_line == 0 {
6036                            0
6037                        } else {
6038                            let rem = x_in_line % tab_width;
6039                            if rem == 0 { tab_width } else { rem }
6040                        };
6041                        let target_x = x_in_line.saturating_sub(back);
6042
6043                        let mut target_col = col;
6044                        while target_col > 0 {
6045                            let prev_col = target_col - 1;
6046                            let prev_x = visual_x_for_column(&line_text, prev_col, tab_width);
6047                            if prev_x < target_x {
6048                                break;
6049                            }
6050                            target_col = prev_col;
6051                            if prev_x == target_x {
6052                                break;
6053                            }
6054                        }
6055
6056                        let target_offset = self
6057                            .editor
6058                            .line_index
6059                            .position_to_char_offset(caret.line, target_col);
6060                        (target_offset, caret_offset)
6061                    }
6062                }
6063            };
6064
6065            let delete_len = end_offset.saturating_sub(start_offset);
6066            let deleted_text = if delete_len == 0 {
6067                String::new()
6068            } else {
6069                self.editor.piece_table.get_range(start_offset, delete_len)
6070            };
6071
6072            ops.push(Op {
6073                selection_index,
6074                start_offset,
6075                delete_len,
6076                deleted_text,
6077                start_after: start_offset,
6078            });
6079        }
6080
6081        if !ops.iter().any(|op| op.delete_len > 0) {
6082            return Ok(CommandResult::Success);
6083        }
6084
6085        let before_char_count = self.editor.piece_table.char_count();
6086
6087        // Compute caret offsets in the post-delete document (ascending order with delta).
6088        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
6089        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
6090
6091        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
6092        let mut delta: i64 = 0;
6093        for &idx in &asc_indices {
6094            let op = &mut ops[idx];
6095            let effective_start = (op.start_offset as i64 + delta) as usize;
6096            op.start_after = effective_start;
6097            caret_offsets[op.selection_index] = effective_start;
6098            delta -= op.delete_len as i64;
6099        }
6100
6101        // Apply deletes descending to keep offsets valid.
6102        let mut desc_indices = asc_indices;
6103        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
6104
6105        for &idx in &desc_indices {
6106            let op = &ops[idx];
6107            if op.delete_len == 0 {
6108                continue;
6109            }
6110
6111            let edit_line = self
6112                .editor
6113                .line_index
6114                .char_offset_to_position(op.start_offset)
6115                .0;
6116            let deleted_newlines = op
6117                .deleted_text
6118                .as_bytes()
6119                .iter()
6120                .filter(|b| **b == b'\n')
6121                .count();
6122            if deleted_newlines > 0 {
6123                self.editor
6124                    .folding_manager
6125                    .apply_line_delta(edit_line, -(deleted_newlines as isize));
6126            }
6127
6128            self.editor
6129                .piece_table
6130                .delete(op.start_offset, op.delete_len);
6131            self.editor
6132                .interval_tree
6133                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6134            for layer_tree in self.editor.style_layers.values_mut() {
6135                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6136            }
6137
6138            self.apply_text_change_to_line_index_and_layout(op.start_offset, &op.deleted_text, "");
6139        }
6140
6141        self.editor
6142            .folding_manager
6143            .clamp_to_line_count(self.editor.line_index.line_count());
6144
6145        // Collapse selection state to carets at the start of deleted ranges.
6146        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
6147        for offset in &caret_offsets {
6148            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
6149            let pos = Position::new(line, column);
6150            new_carets.push(Selection {
6151                start: pos,
6152                end: pos,
6153                direction: SelectionDirection::Forward,
6154            });
6155        }
6156
6157        let (new_carets, new_primary_index) =
6158            crate::selection_set::normalize_selections(new_carets, primary_index);
6159        let primary = new_carets
6160            .get(new_primary_index)
6161            .cloned()
6162            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
6163
6164        self.editor.cursor_position = primary.end;
6165        self.editor.selection = None;
6166        self.editor.secondary_selections = new_carets
6167            .into_iter()
6168            .enumerate()
6169            .filter_map(|(idx, sel)| {
6170                if idx == new_primary_index {
6171                    None
6172                } else {
6173                    Some(sel)
6174                }
6175            })
6176            .collect();
6177
6178        let after_selection = self.snapshot_selection_set();
6179
6180        let edits: Vec<TextEdit> = ops
6181            .into_iter()
6182            .map(|op| TextEdit {
6183                start_before: op.start_offset,
6184                start_after: op.start_after,
6185                deleted_text: op.deleted_text,
6186                inserted_text: String::new(),
6187            })
6188            .collect();
6189
6190        let mut delta_edits: Vec<TextDeltaEdit> = edits
6191            .iter()
6192            .map(|e| TextDeltaEdit {
6193                start: e.start_before,
6194                deleted_text: e.deleted_text.clone(),
6195                inserted_text: e.inserted_text.clone(),
6196            })
6197            .collect();
6198        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
6199
6200        let step = UndoStep {
6201            group_id: 0,
6202            edits,
6203            before_selection,
6204            after_selection,
6205        };
6206        let group_id = self.undo_redo.push_step(step, false);
6207
6208        self.last_text_delta = Some(TextDelta {
6209            before_char_count,
6210            after_char_count: self.editor.piece_table.char_count(),
6211            edits: delta_edits,
6212            undo_group_id: Some(group_id),
6213        });
6214
6215        Ok(CommandResult::Success)
6216    }
6217
6218    fn execute_delete_by_boundary_command(
6219        &mut self,
6220        forward: bool,
6221        boundary: TextBoundary,
6222    ) -> Result<CommandResult, CommandError> {
6223        // Any delete-like action should end an open insert coalescing group, even if it turns out
6224        // to be a no-op.
6225        self.undo_redo.end_group();
6226
6227        let before_selection = self.snapshot_selection_set();
6228        let selections = before_selection.selections.clone();
6229        let primary_index = before_selection.primary_index;
6230
6231        let doc_char_count = self.editor.piece_table.char_count();
6232
6233        #[derive(Debug)]
6234        struct Op {
6235            selection_index: usize,
6236            start_offset: usize,
6237            delete_len: usize,
6238            deleted_text: String,
6239            start_after: usize,
6240        }
6241
6242        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
6243
6244        for (selection_index, selection) in selections.iter().enumerate() {
6245            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
6246                (selection.start, selection.end)
6247            } else {
6248                (selection.end, selection.start)
6249            };
6250
6251            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
6252                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
6253                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
6254                if start_offset <= end_offset {
6255                    (start_offset, end_offset)
6256                } else {
6257                    (end_offset, start_offset)
6258                }
6259            } else {
6260                let caret = selection.end;
6261                let caret_offset = self.position_to_char_offset_clamped(caret);
6262                let line_count = self.editor.line_index.line_count();
6263                let line = caret.line.min(line_count.saturating_sub(1));
6264                let line_text = self
6265                    .editor
6266                    .line_index
6267                    .get_line_text(line)
6268                    .unwrap_or_default();
6269                let line_char_len = line_text.chars().count();
6270                let col = caret.column.min(line_char_len);
6271
6272                if forward {
6273                    if caret_offset >= doc_char_count {
6274                        (caret_offset, caret_offset)
6275                    } else if col >= line_char_len {
6276                        (caret_offset, (caret_offset + 1).min(doc_char_count))
6277                    } else {
6278                        let next_col = next_boundary_column(&line_text, col, boundary);
6279                        let start_offset =
6280                            self.editor.line_index.position_to_char_offset(line, col);
6281                        let end_offset = self
6282                            .editor
6283                            .line_index
6284                            .position_to_char_offset(line, next_col);
6285                        (start_offset, end_offset)
6286                    }
6287                } else if caret_offset == 0 {
6288                    (0, 0)
6289                } else if col == 0 {
6290                    (caret_offset - 1, caret_offset)
6291                } else {
6292                    let prev_col = prev_boundary_column(&line_text, col, boundary);
6293                    let start_offset = self
6294                        .editor
6295                        .line_index
6296                        .position_to_char_offset(line, prev_col);
6297                    let end_offset = self.editor.line_index.position_to_char_offset(line, col);
6298                    (start_offset, end_offset)
6299                }
6300            };
6301
6302            let delete_len = end_offset.saturating_sub(start_offset);
6303            let deleted_text = if delete_len == 0 {
6304                String::new()
6305            } else {
6306                self.editor.piece_table.get_range(start_offset, delete_len)
6307            };
6308
6309            ops.push(Op {
6310                selection_index,
6311                start_offset,
6312                delete_len,
6313                deleted_text,
6314                start_after: start_offset,
6315            });
6316        }
6317
6318        if !ops.iter().any(|op| op.delete_len > 0) {
6319            return Ok(CommandResult::Success);
6320        }
6321
6322        let before_char_count = self.editor.piece_table.char_count();
6323
6324        // Compute caret offsets in the post-delete document (ascending order with delta).
6325        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
6326        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
6327
6328        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
6329        let mut delta: i64 = 0;
6330        for &idx in &asc_indices {
6331            let op = &mut ops[idx];
6332            let effective_start = (op.start_offset as i64 + delta) as usize;
6333            op.start_after = effective_start;
6334            caret_offsets[op.selection_index] = effective_start;
6335            delta -= op.delete_len as i64;
6336        }
6337
6338        // Apply deletes descending to keep offsets valid.
6339        let mut desc_indices = asc_indices;
6340        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
6341
6342        for &idx in &desc_indices {
6343            let op = &ops[idx];
6344            if op.delete_len == 0 {
6345                continue;
6346            }
6347
6348            self.editor
6349                .piece_table
6350                .delete(op.start_offset, op.delete_len);
6351            self.editor
6352                .interval_tree
6353                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6354            for layer_tree in self.editor.style_layers.values_mut() {
6355                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6356            }
6357
6358            self.apply_text_change_to_line_index_and_layout(op.start_offset, &op.deleted_text, "");
6359        }
6360
6361        // Collapse selection state to carets at the start of deleted ranges.
6362        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
6363        for offset in &caret_offsets {
6364            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
6365            let pos = Position::new(line, column);
6366            new_carets.push(Selection {
6367                start: pos,
6368                end: pos,
6369                direction: SelectionDirection::Forward,
6370            });
6371        }
6372
6373        let (new_carets, new_primary_index) =
6374            crate::selection_set::normalize_selections(new_carets, primary_index);
6375        let primary = new_carets
6376            .get(new_primary_index)
6377            .cloned()
6378            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
6379
6380        self.editor.cursor_position = primary.end;
6381        self.editor.selection = None;
6382        self.editor.secondary_selections = new_carets
6383            .into_iter()
6384            .enumerate()
6385            .filter_map(|(idx, sel)| {
6386                if idx == new_primary_index {
6387                    None
6388                } else {
6389                    Some(sel)
6390                }
6391            })
6392            .collect();
6393
6394        let after_selection = self.snapshot_selection_set();
6395
6396        let edits: Vec<TextEdit> = ops
6397            .into_iter()
6398            .map(|op| TextEdit {
6399                start_before: op.start_offset,
6400                start_after: op.start_after,
6401                deleted_text: op.deleted_text,
6402                inserted_text: String::new(),
6403            })
6404            .collect();
6405
6406        let mut delta_edits: Vec<TextDeltaEdit> = edits
6407            .iter()
6408            .map(|e| TextDeltaEdit {
6409                start: e.start_before,
6410                deleted_text: e.deleted_text.clone(),
6411                inserted_text: e.inserted_text.clone(),
6412            })
6413            .collect();
6414        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
6415
6416        let step = UndoStep {
6417            group_id: 0,
6418            edits,
6419            before_selection,
6420            after_selection,
6421        };
6422        let group_id = self.undo_redo.push_step(step, false);
6423
6424        self.last_text_delta = Some(TextDelta {
6425            before_char_count,
6426            after_char_count: self.editor.piece_table.char_count(),
6427            edits: delta_edits,
6428            undo_group_id: Some(group_id),
6429        });
6430
6431        Ok(CommandResult::Success)
6432    }
6433
6434    fn execute_delete_like_command(
6435        &mut self,
6436        forward: bool,
6437    ) -> Result<CommandResult, CommandError> {
6438        // Any delete-like action should end an open insert coalescing group, even if it turns out
6439        // to be a no-op (e.g. backspace at the beginning of the document).
6440        self.undo_redo.end_group();
6441
6442        let before_selection = self.snapshot_selection_set();
6443        let selections = before_selection.selections.clone();
6444        let primary_index = before_selection.primary_index;
6445
6446        let doc_char_count = self.editor.piece_table.char_count();
6447
6448        #[derive(Debug)]
6449        struct Op {
6450            selection_index: usize,
6451            start_offset: usize,
6452            delete_len: usize,
6453            deleted_text: String,
6454            start_after: usize,
6455        }
6456
6457        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
6458
6459        for (selection_index, selection) in selections.iter().enumerate() {
6460            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
6461                (selection.start, selection.end)
6462            } else {
6463                (selection.end, selection.start)
6464            };
6465
6466            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
6467                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
6468                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
6469                if start_offset <= end_offset {
6470                    (start_offset, end_offset)
6471                } else {
6472                    (end_offset, start_offset)
6473                }
6474            } else {
6475                let caret_offset = self.position_to_char_offset_clamped(selection.end);
6476                if forward {
6477                    if caret_offset >= doc_char_count {
6478                        (caret_offset, caret_offset)
6479                    } else {
6480                        (caret_offset, (caret_offset + 1).min(doc_char_count))
6481                    }
6482                } else if caret_offset == 0 {
6483                    (0, 0)
6484                } else {
6485                    (caret_offset - 1, caret_offset)
6486                }
6487            };
6488
6489            let delete_len = end_offset.saturating_sub(start_offset);
6490            let deleted_text = if delete_len == 0 {
6491                String::new()
6492            } else {
6493                self.editor.piece_table.get_range(start_offset, delete_len)
6494            };
6495
6496            ops.push(Op {
6497                selection_index,
6498                start_offset,
6499                delete_len,
6500                deleted_text,
6501                start_after: start_offset,
6502            });
6503        }
6504
6505        if !ops.iter().any(|op| op.delete_len > 0) {
6506            return Ok(CommandResult::Success);
6507        }
6508
6509        let before_char_count = self.editor.piece_table.char_count();
6510
6511        // Compute caret offsets in the post-delete document (ascending order with delta).
6512        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
6513        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
6514
6515        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
6516        let mut delta: i64 = 0;
6517        for &idx in &asc_indices {
6518            let op = &mut ops[idx];
6519            let effective_start = (op.start_offset as i64 + delta) as usize;
6520            op.start_after = effective_start;
6521            caret_offsets[op.selection_index] = effective_start;
6522            delta -= op.delete_len as i64;
6523        }
6524
6525        // Apply deletes descending to keep offsets valid.
6526        let mut desc_indices = asc_indices;
6527        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
6528
6529        for &idx in &desc_indices {
6530            let op = &ops[idx];
6531            if op.delete_len == 0 {
6532                continue;
6533            }
6534
6535            self.editor
6536                .piece_table
6537                .delete(op.start_offset, op.delete_len);
6538            self.editor
6539                .interval_tree
6540                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6541            for layer_tree in self.editor.style_layers.values_mut() {
6542                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
6543            }
6544
6545            self.apply_text_change_to_line_index_and_layout(op.start_offset, &op.deleted_text, "");
6546        }
6547
6548        // Collapse selection state to carets at the start of deleted ranges.
6549        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
6550        for offset in &caret_offsets {
6551            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
6552            let pos = Position::new(line, column);
6553            new_carets.push(Selection {
6554                start: pos,
6555                end: pos,
6556                direction: SelectionDirection::Forward,
6557            });
6558        }
6559
6560        let (new_carets, new_primary_index) =
6561            crate::selection_set::normalize_selections(new_carets, primary_index);
6562        let primary = new_carets
6563            .get(new_primary_index)
6564            .cloned()
6565            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
6566
6567        self.editor.cursor_position = primary.end;
6568        self.editor.selection = None;
6569        self.editor.secondary_selections = new_carets
6570            .into_iter()
6571            .enumerate()
6572            .filter_map(|(idx, sel)| {
6573                if idx == new_primary_index {
6574                    None
6575                } else {
6576                    Some(sel)
6577                }
6578            })
6579            .collect();
6580
6581        let after_selection = self.snapshot_selection_set();
6582
6583        let edits: Vec<TextEdit> = ops
6584            .into_iter()
6585            .map(|op| TextEdit {
6586                start_before: op.start_offset,
6587                start_after: op.start_after,
6588                deleted_text: op.deleted_text,
6589                inserted_text: String::new(),
6590            })
6591            .collect();
6592
6593        let mut delta_edits: Vec<TextDeltaEdit> = edits
6594            .iter()
6595            .map(|e| TextDeltaEdit {
6596                start: e.start_before,
6597                deleted_text: e.deleted_text.clone(),
6598                inserted_text: String::new(),
6599            })
6600            .collect();
6601        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
6602
6603        let step = UndoStep {
6604            group_id: 0,
6605            edits,
6606            before_selection,
6607            after_selection,
6608        };
6609        let group_id = self.undo_redo.push_step(step, false);
6610
6611        self.last_text_delta = Some(TextDelta {
6612            before_char_count,
6613            after_char_count: self.editor.piece_table.char_count(),
6614            edits: delta_edits,
6615            undo_group_id: Some(group_id),
6616        });
6617
6618        Ok(CommandResult::Success)
6619    }
6620
6621    fn snapshot_selection_set(&self) -> SelectionSetSnapshot {
6622        let mut selections: Vec<Selection> =
6623            Vec::with_capacity(1 + self.editor.secondary_selections.len());
6624
6625        let primary = self.editor.selection.clone().unwrap_or(Selection {
6626            start: self.editor.cursor_position,
6627            end: self.editor.cursor_position,
6628            direction: SelectionDirection::Forward,
6629        });
6630        selections.push(primary);
6631        selections.extend(self.editor.secondary_selections.iter().cloned());
6632
6633        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
6634        SelectionSetSnapshot {
6635            selections,
6636            primary_index,
6637        }
6638    }
6639
6640    fn restore_selection_set(&mut self, snapshot: SelectionSetSnapshot) {
6641        if snapshot.selections.is_empty() {
6642            self.editor.cursor_position = Position::new(0, 0);
6643            self.editor.selection = None;
6644            self.editor.secondary_selections.clear();
6645            return;
6646        }
6647
6648        let primary = snapshot
6649            .selections
6650            .get(snapshot.primary_index)
6651            .cloned()
6652            .unwrap_or_else(|| snapshot.selections[0].clone());
6653
6654        self.editor.cursor_position = primary.end;
6655        self.editor.selection = if primary.start == primary.end {
6656            None
6657        } else {
6658            Some(primary.clone())
6659        };
6660
6661        self.editor.secondary_selections = snapshot
6662            .selections
6663            .into_iter()
6664            .enumerate()
6665            .filter_map(|(idx, sel)| {
6666                if idx == snapshot.primary_index {
6667                    None
6668                } else {
6669                    Some(sel)
6670                }
6671            })
6672            .collect();
6673
6674        self.normalize_cursor_and_selection();
6675    }
6676
6677    fn apply_undo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
6678        // Apply inverse: delete inserted text, then reinsert deleted text.
6679        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
6680        for edit in edits {
6681            let start = edit.start_after;
6682            let delete_len = edit.inserted_len();
6683            let insert_text = edit.deleted_text.as_str();
6684            ops.push((start, delete_len, insert_text));
6685        }
6686        self.apply_text_ops(ops)
6687    }
6688
6689    fn apply_redo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
6690        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
6691        for edit in edits {
6692            let start = edit.start_before;
6693            let delete_len = edit.deleted_len();
6694            let insert_text = edit.inserted_text.as_str();
6695            ops.push((start, delete_len, insert_text));
6696        }
6697        self.apply_text_ops(ops)
6698    }
6699
6700    fn apply_text_ops(&mut self, mut ops: Vec<(usize, usize, &str)>) -> Result<(), CommandError> {
6701        // Sort descending by start offset to make offsets stable while mutating.
6702        ops.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
6703
6704        for (start, delete_len, insert_text) in ops {
6705            let max_offset = self.editor.piece_table.char_count();
6706            if start > max_offset {
6707                return Err(CommandError::InvalidOffset(start));
6708            }
6709            if start + delete_len > max_offset {
6710                return Err(CommandError::InvalidRange {
6711                    start,
6712                    end: start + delete_len,
6713                });
6714            }
6715
6716            let edit_line = self.editor.line_index.char_offset_to_position(start).0;
6717            let deleted_text = if delete_len > 0 {
6718                self.editor.piece_table.get_range(start, delete_len)
6719            } else {
6720                String::new()
6721            };
6722            let deleted_newlines = deleted_text
6723                .as_bytes()
6724                .iter()
6725                .filter(|b| **b == b'\n')
6726                .count();
6727            let inserted_newlines = insert_text
6728                .as_bytes()
6729                .iter()
6730                .filter(|b| **b == b'\n')
6731                .count();
6732            let line_delta = inserted_newlines as isize - deleted_newlines as isize;
6733            if line_delta != 0 {
6734                self.editor
6735                    .folding_manager
6736                    .apply_line_delta(edit_line, line_delta);
6737            }
6738
6739            if delete_len > 0 {
6740                self.editor.piece_table.delete(start, delete_len);
6741                self.editor
6742                    .interval_tree
6743                    .update_for_deletion(start, start + delete_len);
6744                for layer_tree in self.editor.style_layers.values_mut() {
6745                    layer_tree.update_for_deletion(start, start + delete_len);
6746                }
6747            }
6748
6749            let insert_len = insert_text.chars().count();
6750            if insert_len > 0 {
6751                self.editor.piece_table.insert(start, insert_text);
6752                self.editor
6753                    .interval_tree
6754                    .update_for_insertion(start, insert_len);
6755                for layer_tree in self.editor.style_layers.values_mut() {
6756                    layer_tree.update_for_insertion(start, insert_len);
6757                }
6758            }
6759
6760            self.apply_text_change_to_line_index_and_layout(start, &deleted_text, insert_text);
6761        }
6762
6763        self.editor
6764            .folding_manager
6765            .clamp_to_line_count(self.editor.line_index.line_count());
6766        self.normalize_cursor_and_selection();
6767
6768        Ok(())
6769    }
6770
6771    // Private method: execute cursor command
6772    fn execute_cursor(&mut self, command: CursorCommand) -> Result<CommandResult, CommandError> {
6773        match command {
6774            CursorCommand::MoveTo { line, column } => {
6775                if line >= self.editor.line_index.line_count() {
6776                    return Err(CommandError::InvalidPosition { line, column });
6777                }
6778
6779                let clamped_column = self.clamp_column_for_line(line, column);
6780                self.editor.cursor_position = Position::new(line, clamped_column);
6781                self.preferred_x_cells = self
6782                    .editor
6783                    .logical_position_to_visual(line, clamped_column)
6784                    .map(|(_, x)| x);
6785                // VSCode-like: moving the primary caret to an absolute position collapses multi-cursor.
6786                self.editor.secondary_selections.clear();
6787                Ok(CommandResult::Success)
6788            }
6789            CursorCommand::MoveBy {
6790                delta_line,
6791                delta_column,
6792            } => {
6793                let new_line = if delta_line >= 0 {
6794                    self.editor.cursor_position.line + delta_line as usize
6795                } else {
6796                    self.editor
6797                        .cursor_position
6798                        .line
6799                        .saturating_sub((-delta_line) as usize)
6800                };
6801
6802                let new_column = if delta_column >= 0 {
6803                    self.editor.cursor_position.column + delta_column as usize
6804                } else {
6805                    self.editor
6806                        .cursor_position
6807                        .column
6808                        .saturating_sub((-delta_column) as usize)
6809                };
6810
6811                if new_line >= self.editor.line_index.line_count() {
6812                    return Err(CommandError::InvalidPosition {
6813                        line: new_line,
6814                        column: new_column,
6815                    });
6816                }
6817
6818                let clamped_column = self.clamp_column_for_line(new_line, new_column);
6819                self.editor.cursor_position = Position::new(new_line, clamped_column);
6820                self.preferred_x_cells = self
6821                    .editor
6822                    .logical_position_to_visual(new_line, clamped_column)
6823                    .map(|(_, x)| x);
6824                Ok(CommandResult::Success)
6825            }
6826            CursorCommand::MoveGraphemeLeft => {
6827                let line_count = self.editor.line_index.line_count();
6828                if line_count == 0 {
6829                    return Ok(CommandResult::Success);
6830                }
6831
6832                let mut line = self
6833                    .editor
6834                    .cursor_position
6835                    .line
6836                    .min(line_count.saturating_sub(1));
6837                let mut line_text = self
6838                    .editor
6839                    .line_index
6840                    .get_line_text(line)
6841                    .unwrap_or_default();
6842                let mut line_char_len = line_text.chars().count();
6843                let mut col = self.editor.cursor_position.column.min(line_char_len);
6844
6845                if col == 0 {
6846                    if line == 0 {
6847                        return Ok(CommandResult::Success);
6848                    }
6849                    line = line.saturating_sub(1);
6850                    line_text = self
6851                        .editor
6852                        .line_index
6853                        .get_line_text(line)
6854                        .unwrap_or_default();
6855                    line_char_len = line_text.chars().count();
6856                    col = line_char_len;
6857                } else {
6858                    col = prev_boundary_column(&line_text, col, TextBoundary::Grapheme);
6859                }
6860
6861                self.editor.cursor_position = Position::new(line, col);
6862                self.preferred_x_cells = self
6863                    .editor
6864                    .logical_position_to_visual(line, col)
6865                    .map(|(_, x)| x);
6866                Ok(CommandResult::Success)
6867            }
6868            CursorCommand::MoveGraphemeRight => {
6869                let line_count = self.editor.line_index.line_count();
6870                if line_count == 0 {
6871                    return Ok(CommandResult::Success);
6872                }
6873
6874                let line = self
6875                    .editor
6876                    .cursor_position
6877                    .line
6878                    .min(line_count.saturating_sub(1));
6879                let line_text = self
6880                    .editor
6881                    .line_index
6882                    .get_line_text(line)
6883                    .unwrap_or_default();
6884                let line_char_len = line_text.chars().count();
6885                let col = self.editor.cursor_position.column.min(line_char_len);
6886
6887                let (line, col) = if col >= line_char_len {
6888                    if line + 1 >= line_count {
6889                        return Ok(CommandResult::Success);
6890                    }
6891                    (line + 1, 0)
6892                } else {
6893                    (
6894                        line,
6895                        next_boundary_column(&line_text, col, TextBoundary::Grapheme),
6896                    )
6897                };
6898
6899                self.editor.cursor_position = Position::new(line, col);
6900                self.preferred_x_cells = self
6901                    .editor
6902                    .logical_position_to_visual(line, col)
6903                    .map(|(_, x)| x);
6904                Ok(CommandResult::Success)
6905            }
6906            CursorCommand::MoveWordLeft => {
6907                let line_count = self.editor.line_index.line_count();
6908                if line_count == 0 {
6909                    return Ok(CommandResult::Success);
6910                }
6911
6912                let mut line = self
6913                    .editor
6914                    .cursor_position
6915                    .line
6916                    .min(line_count.saturating_sub(1));
6917                let mut line_text = self
6918                    .editor
6919                    .line_index
6920                    .get_line_text(line)
6921                    .unwrap_or_default();
6922                let mut line_char_len = line_text.chars().count();
6923                let mut col = self.editor.cursor_position.column.min(line_char_len);
6924
6925                if col == 0 {
6926                    if line == 0 {
6927                        return Ok(CommandResult::Success);
6928                    }
6929                    line = line.saturating_sub(1);
6930                    line_text = self
6931                        .editor
6932                        .line_index
6933                        .get_line_text(line)
6934                        .unwrap_or_default();
6935                    line_char_len = line_text.chars().count();
6936                    col = line_char_len;
6937                } else {
6938                    col = prev_boundary_column(&line_text, col, TextBoundary::Word);
6939                }
6940
6941                self.editor.cursor_position = Position::new(line, col);
6942                self.preferred_x_cells = self
6943                    .editor
6944                    .logical_position_to_visual(line, col)
6945                    .map(|(_, x)| x);
6946                Ok(CommandResult::Success)
6947            }
6948            CursorCommand::MoveWordRight => {
6949                let line_count = self.editor.line_index.line_count();
6950                if line_count == 0 {
6951                    return Ok(CommandResult::Success);
6952                }
6953
6954                let line = self
6955                    .editor
6956                    .cursor_position
6957                    .line
6958                    .min(line_count.saturating_sub(1));
6959                let line_text = self
6960                    .editor
6961                    .line_index
6962                    .get_line_text(line)
6963                    .unwrap_or_default();
6964                let line_char_len = line_text.chars().count();
6965                let col = self.editor.cursor_position.column.min(line_char_len);
6966
6967                let (line, col) = if col >= line_char_len {
6968                    if line + 1 >= line_count {
6969                        return Ok(CommandResult::Success);
6970                    }
6971                    (line + 1, 0)
6972                } else {
6973                    (
6974                        line,
6975                        next_boundary_column(&line_text, col, TextBoundary::Word),
6976                    )
6977                };
6978
6979                self.editor.cursor_position = Position::new(line, col);
6980                self.preferred_x_cells = self
6981                    .editor
6982                    .logical_position_to_visual(line, col)
6983                    .map(|(_, x)| x);
6984                Ok(CommandResult::Success)
6985            }
6986            CursorCommand::MoveVisualBy { delta_rows } => {
6987                let Some((current_row, current_x)) = self.editor.logical_position_to_visual(
6988                    self.editor.cursor_position.line,
6989                    self.editor.cursor_position.column,
6990                ) else {
6991                    return Ok(CommandResult::Success);
6992                };
6993
6994                let preferred_x = self.preferred_x_cells.unwrap_or(current_x);
6995                self.preferred_x_cells = Some(preferred_x);
6996
6997                let total_visual = self.editor.visual_line_count();
6998                if total_visual == 0 {
6999                    return Ok(CommandResult::Success);
7000                }
7001
7002                let target_row = if delta_rows >= 0 {
7003                    current_row.saturating_add(delta_rows as usize)
7004                } else {
7005                    current_row.saturating_sub((-delta_rows) as usize)
7006                }
7007                .min(total_visual.saturating_sub(1));
7008
7009                let Some(pos) = self
7010                    .editor
7011                    .visual_position_to_logical(target_row, preferred_x)
7012                else {
7013                    return Ok(CommandResult::Success);
7014                };
7015
7016                self.editor.cursor_position = pos;
7017                Ok(CommandResult::Success)
7018            }
7019            CursorCommand::MoveToVisual { row, x_cells } => {
7020                let Some(pos) = self.editor.visual_position_to_logical(row, x_cells) else {
7021                    return Ok(CommandResult::Success);
7022                };
7023
7024                self.editor.cursor_position = pos;
7025                self.preferred_x_cells = Some(x_cells);
7026                // Treat as an absolute move (similar to `MoveTo`).
7027                self.editor.secondary_selections.clear();
7028                Ok(CommandResult::Success)
7029            }
7030            CursorCommand::MoveToLineStart => {
7031                let line = self.editor.cursor_position.line;
7032                self.editor.cursor_position = Position::new(line, 0);
7033                self.preferred_x_cells = Some(0);
7034                self.editor.secondary_selections.clear();
7035                Ok(CommandResult::Success)
7036            }
7037            CursorCommand::MoveToLineEnd => {
7038                let line = self.editor.cursor_position.line;
7039                let end_col = self.clamp_column_for_line(line, usize::MAX);
7040                self.editor.cursor_position = Position::new(line, end_col);
7041                self.preferred_x_cells = self
7042                    .editor
7043                    .logical_position_to_visual(line, end_col)
7044                    .map(|(_, x)| x);
7045                self.editor.secondary_selections.clear();
7046                Ok(CommandResult::Success)
7047            }
7048            CursorCommand::MoveToVisualLineStart => {
7049                let line = self.editor.cursor_position.line;
7050                let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
7051                    return Ok(CommandResult::Success);
7052                };
7053
7054                let line_text = self
7055                    .editor
7056                    .line_index
7057                    .get_line_text(line)
7058                    .unwrap_or_default();
7059                let line_char_len = line_text.chars().count();
7060                let column = self.editor.cursor_position.column.min(line_char_len);
7061
7062                let mut seg_start = 0usize;
7063                for wp in &layout.wrap_points {
7064                    if column >= wp.char_index {
7065                        seg_start = wp.char_index;
7066                    } else {
7067                        break;
7068                    }
7069                }
7070
7071                self.editor.cursor_position = Position::new(line, seg_start);
7072                self.preferred_x_cells = self
7073                    .editor
7074                    .logical_position_to_visual(line, seg_start)
7075                    .map(|(_, x)| x);
7076                self.editor.secondary_selections.clear();
7077                Ok(CommandResult::Success)
7078            }
7079            CursorCommand::MoveToVisualLineEnd => {
7080                let line = self.editor.cursor_position.line;
7081                let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
7082                    return Ok(CommandResult::Success);
7083                };
7084
7085                let line_text = self
7086                    .editor
7087                    .line_index
7088                    .get_line_text(line)
7089                    .unwrap_or_default();
7090                let line_char_len = line_text.chars().count();
7091                let column = self.editor.cursor_position.column.min(line_char_len);
7092
7093                let mut seg_end = line_char_len;
7094                for wp in &layout.wrap_points {
7095                    if column < wp.char_index {
7096                        seg_end = wp.char_index;
7097                        break;
7098                    }
7099                }
7100
7101                self.editor.cursor_position = Position::new(line, seg_end);
7102                self.preferred_x_cells = self
7103                    .editor
7104                    .logical_position_to_visual(line, seg_end)
7105                    .map(|(_, x)| x);
7106                self.editor.secondary_selections.clear();
7107                Ok(CommandResult::Success)
7108            }
7109            CursorCommand::SetSelection { start, end } => {
7110                if start.line >= self.editor.line_index.line_count()
7111                    || end.line >= self.editor.line_index.line_count()
7112                {
7113                    return Err(CommandError::InvalidPosition {
7114                        line: start.line.max(end.line),
7115                        column: start.column.max(end.column),
7116                    });
7117                }
7118
7119                let start = Position::new(
7120                    start.line,
7121                    self.clamp_column_for_line(start.line, start.column),
7122                );
7123                let end = Position::new(end.line, self.clamp_column_for_line(end.line, end.column));
7124
7125                let direction = if start.line < end.line
7126                    || (start.line == end.line && start.column <= end.column)
7127                {
7128                    SelectionDirection::Forward
7129                } else {
7130                    SelectionDirection::Backward
7131                };
7132
7133                self.editor.selection = Some(Selection {
7134                    start,
7135                    end,
7136                    direction,
7137                });
7138                Ok(CommandResult::Success)
7139            }
7140            CursorCommand::ExtendSelection { to } => {
7141                if to.line >= self.editor.line_index.line_count() {
7142                    return Err(CommandError::InvalidPosition {
7143                        line: to.line,
7144                        column: to.column,
7145                    });
7146                }
7147
7148                let to = Position::new(to.line, self.clamp_column_for_line(to.line, to.column));
7149
7150                if let Some(ref mut selection) = self.editor.selection {
7151                    selection.end = to;
7152                    selection.direction = if selection.start.line < to.line
7153                        || (selection.start.line == to.line && selection.start.column <= to.column)
7154                    {
7155                        SelectionDirection::Forward
7156                    } else {
7157                        SelectionDirection::Backward
7158                    };
7159                } else {
7160                    // If no selection, create selection from current cursor
7161                    self.editor.selection = Some(Selection {
7162                        start: self.editor.cursor_position,
7163                        end: to,
7164                        direction: if self.editor.cursor_position.line < to.line
7165                            || (self.editor.cursor_position.line == to.line
7166                                && self.editor.cursor_position.column <= to.column)
7167                        {
7168                            SelectionDirection::Forward
7169                        } else {
7170                            SelectionDirection::Backward
7171                        },
7172                    });
7173                }
7174                Ok(CommandResult::Success)
7175            }
7176            CursorCommand::ClearSelection => {
7177                self.editor.selection = None;
7178                Ok(CommandResult::Success)
7179            }
7180            CursorCommand::SetSelections {
7181                selections,
7182                primary_index,
7183            } => {
7184                let line_count = self.editor.line_index.line_count();
7185                if selections.is_empty() {
7186                    return Err(CommandError::Other(
7187                        "SetSelections requires a non-empty selection list".to_string(),
7188                    ));
7189                }
7190                if primary_index >= selections.len() {
7191                    return Err(CommandError::Other(format!(
7192                        "Invalid primary_index {} for {} selections",
7193                        primary_index,
7194                        selections.len()
7195                    )));
7196                }
7197
7198                for sel in &selections {
7199                    if sel.start.line >= line_count || sel.end.line >= line_count {
7200                        return Err(CommandError::InvalidPosition {
7201                            line: sel.start.line.max(sel.end.line),
7202                            column: sel.start.column.max(sel.end.column),
7203                        });
7204                    }
7205                }
7206
7207                let (selections, primary_index) =
7208                    crate::selection_set::normalize_selections(selections, primary_index);
7209
7210                let primary = selections
7211                    .get(primary_index)
7212                    .cloned()
7213                    .ok_or_else(|| CommandError::Other("Invalid primary selection".to_string()))?;
7214
7215                self.editor.cursor_position = primary.end;
7216                self.editor.selection = if primary.start == primary.end {
7217                    None
7218                } else {
7219                    Some(primary.clone())
7220                };
7221
7222                self.editor.secondary_selections = selections
7223                    .into_iter()
7224                    .enumerate()
7225                    .filter_map(|(idx, sel)| {
7226                        if idx == primary_index {
7227                            None
7228                        } else {
7229                            Some(sel)
7230                        }
7231                    })
7232                    .collect();
7233
7234                Ok(CommandResult::Success)
7235            }
7236            CursorCommand::ClearSecondarySelections => {
7237                self.editor.secondary_selections.clear();
7238                Ok(CommandResult::Success)
7239            }
7240            CursorCommand::SetRectSelection { anchor, active } => {
7241                let line_count = self.editor.line_index.line_count();
7242                if anchor.line >= line_count || active.line >= line_count {
7243                    return Err(CommandError::InvalidPosition {
7244                        line: anchor.line.max(active.line),
7245                        column: anchor.column.max(active.column),
7246                    });
7247                }
7248
7249                let (selections, primary_index) =
7250                    crate::selection_set::rect_selections(anchor, active);
7251
7252                // Delegate to SetSelections so normalization rules are shared.
7253                self.execute_cursor(CursorCommand::SetSelections {
7254                    selections,
7255                    primary_index,
7256                })?;
7257                Ok(CommandResult::Success)
7258            }
7259            CursorCommand::SelectLine => self.execute_select_line_command(),
7260            CursorCommand::SelectWord => self.execute_select_word_command(),
7261            CursorCommand::ExpandSelection => self.execute_expand_selection_command(),
7262            CursorCommand::AddCursorAbove => self.execute_add_cursor_vertical_command(true),
7263            CursorCommand::AddCursorBelow => self.execute_add_cursor_vertical_command(false),
7264            CursorCommand::AddNextOccurrence { options } => {
7265                self.execute_add_next_occurrence_command(options)
7266            }
7267            CursorCommand::AddAllOccurrences { options } => {
7268                self.execute_add_all_occurrences_command(options)
7269            }
7270            CursorCommand::FindNext { query, options } => {
7271                self.execute_find_command(query, options, true)
7272            }
7273            CursorCommand::FindPrev { query, options } => {
7274                self.execute_find_command(query, options, false)
7275            }
7276        }
7277    }
7278
7279    // Private method: execute view command
7280    fn execute_view(&mut self, command: ViewCommand) -> Result<CommandResult, CommandError> {
7281        match command {
7282            ViewCommand::SetViewportWidth { width } => {
7283                if width == 0 {
7284                    return Err(CommandError::Other(
7285                        "Viewport width must be greater than 0".to_string(),
7286                    ));
7287                }
7288
7289                self.editor.viewport_width = width;
7290                self.editor.layout_engine.set_viewport_width(width);
7291                Ok(CommandResult::Success)
7292            }
7293            ViewCommand::SetWrapMode { mode } => {
7294                self.editor.layout_engine.set_wrap_mode(mode);
7295                Ok(CommandResult::Success)
7296            }
7297            ViewCommand::SetWrapIndent { indent } => {
7298                self.editor.layout_engine.set_wrap_indent(indent);
7299                Ok(CommandResult::Success)
7300            }
7301            ViewCommand::SetTabWidth { width } => {
7302                if width == 0 {
7303                    return Err(CommandError::Other(
7304                        "Tab width must be greater than 0".to_string(),
7305                    ));
7306                }
7307
7308                self.editor.layout_engine.set_tab_width(width);
7309                Ok(CommandResult::Success)
7310            }
7311            ViewCommand::SetTabKeyBehavior { behavior } => {
7312                self.tab_key_behavior = behavior;
7313                Ok(CommandResult::Success)
7314            }
7315            ViewCommand::ScrollTo { line } => {
7316                if line >= self.editor.line_index.line_count() {
7317                    return Err(CommandError::InvalidPosition { line, column: 0 });
7318                }
7319
7320                // Scroll operation only validates line number validity
7321                // Actual scrolling handled by frontend
7322                Ok(CommandResult::Success)
7323            }
7324            ViewCommand::GetViewport { start_row, count } => {
7325                let grid = self.editor.get_headless_grid_styled(start_row, count);
7326                Ok(CommandResult::Viewport(grid))
7327            }
7328        }
7329    }
7330
7331    // Private method: execute style command
7332    fn execute_style(&mut self, command: StyleCommand) -> Result<CommandResult, CommandError> {
7333        match command {
7334            StyleCommand::AddStyle {
7335                start,
7336                end,
7337                style_id,
7338            } => {
7339                if start >= end {
7340                    return Err(CommandError::InvalidRange { start, end });
7341                }
7342
7343                let interval = crate::intervals::Interval::new(start, end, style_id);
7344                self.editor.interval_tree.insert(interval);
7345                Ok(CommandResult::Success)
7346            }
7347            StyleCommand::RemoveStyle {
7348                start,
7349                end,
7350                style_id,
7351            } => {
7352                self.editor.interval_tree.remove(start, end, style_id);
7353                Ok(CommandResult::Success)
7354            }
7355            StyleCommand::Fold {
7356                start_line,
7357                end_line,
7358            } => {
7359                if start_line >= end_line {
7360                    return Err(CommandError::InvalidRange {
7361                        start: start_line,
7362                        end: end_line,
7363                    });
7364                }
7365
7366                let mut region = crate::intervals::FoldRegion::new(start_line, end_line);
7367                region.collapse();
7368                self.editor.folding_manager.add_region(region);
7369                Ok(CommandResult::Success)
7370            }
7371            StyleCommand::Unfold { start_line } => {
7372                self.editor.folding_manager.expand_line(start_line);
7373                Ok(CommandResult::Success)
7374            }
7375            StyleCommand::UnfoldAll => {
7376                self.editor.folding_manager.expand_all();
7377                Ok(CommandResult::Success)
7378            }
7379        }
7380    }
7381
7382    fn apply_text_change_to_line_index_and_layout(
7383        &mut self,
7384        start_offset: usize,
7385        deleted_text: &str,
7386        inserted_text: &str,
7387    ) {
7388        let start_line = self
7389            .editor
7390            .line_index
7391            .char_offset_to_position(start_offset)
7392            .0;
7393
7394        let deleted_chars = deleted_text.chars().count();
7395        if deleted_chars > 0 {
7396            self.editor.line_index.delete(start_offset, deleted_chars);
7397        }
7398        if !inserted_text.is_empty() {
7399            self.editor.line_index.insert(start_offset, inserted_text);
7400        }
7401
7402        let deleted_newlines = deleted_text
7403            .as_bytes()
7404            .iter()
7405            .filter(|b| **b == b'\n')
7406            .count();
7407        let inserted_newlines = inserted_text
7408            .as_bytes()
7409            .iter()
7410            .filter(|b| **b == b'\n')
7411            .count();
7412
7413        let line_delta = inserted_newlines as isize - deleted_newlines as isize;
7414        if line_delta > 0 {
7415            for i in 0..(line_delta as usize) {
7416                let line = start_line.saturating_add(1).saturating_add(i);
7417                let line_text = self
7418                    .editor
7419                    .line_index
7420                    .get_line_text(line)
7421                    .unwrap_or_default();
7422                self.editor.layout_engine.insert_line(line, &line_text);
7423            }
7424        } else if line_delta < 0 {
7425            for _ in 0..((-line_delta) as usize) {
7426                self.editor
7427                    .layout_engine
7428                    .delete_line(start_line.saturating_add(1));
7429            }
7430        }
7431
7432        // Update a small window around the edit site. This keeps the layout engine incremental
7433        // while still handling multi-line inserts/deletes deterministically.
7434        let touch_lines = deleted_newlines.max(inserted_newlines).saturating_add(1);
7435        let end_line = start_line.saturating_add(touch_lines);
7436
7437        let line_count = self.editor.line_index.line_count();
7438        if line_count == 0 {
7439            return;
7440        }
7441        let last_line = line_count.saturating_sub(1);
7442        for line in start_line..=end_line.min(last_line) {
7443            let line_text = self
7444                .editor
7445                .line_index
7446                .get_line_text(line)
7447                .unwrap_or_default();
7448            self.editor.layout_engine.update_line(line, &line_text);
7449        }
7450    }
7451
7452    fn position_to_char_offset_clamped(&self, pos: Position) -> usize {
7453        let line_count = self.editor.line_index.line_count();
7454        if line_count == 0 {
7455            return 0;
7456        }
7457
7458        let line = pos.line.min(line_count.saturating_sub(1));
7459        let line_text = self
7460            .editor
7461            .line_index
7462            .get_line_text(line)
7463            .unwrap_or_default();
7464        let line_char_len = line_text.chars().count();
7465        let column = pos.column.min(line_char_len);
7466        self.editor.line_index.position_to_char_offset(line, column)
7467    }
7468
7469    fn position_to_char_offset_and_virtual_pad(&self, pos: Position) -> (usize, usize) {
7470        let line_count = self.editor.line_index.line_count();
7471        if line_count == 0 {
7472            return (0, 0);
7473        }
7474
7475        let line = pos.line.min(line_count.saturating_sub(1));
7476        let line_text = self
7477            .editor
7478            .line_index
7479            .get_line_text(line)
7480            .unwrap_or_default();
7481        let line_char_len = line_text.chars().count();
7482        let clamped_col = pos.column.min(line_char_len);
7483        let offset = self
7484            .editor
7485            .line_index
7486            .position_to_char_offset(line, clamped_col);
7487        let pad = pos.column.saturating_sub(clamped_col);
7488        (offset, pad)
7489    }
7490
7491    fn normalize_cursor_and_selection(&mut self) {
7492        let line_index = &self.editor.line_index;
7493        let line_count = line_index.line_count();
7494        if line_count == 0 {
7495            self.editor.cursor_position = Position::new(0, 0);
7496            self.editor.selection = None;
7497            self.editor.secondary_selections.clear();
7498            return;
7499        }
7500
7501        self.editor.cursor_position =
7502            Self::clamp_position_lenient_with_index(line_index, self.editor.cursor_position);
7503
7504        if let Some(ref mut selection) = self.editor.selection {
7505            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
7506            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
7507            selection.direction = if selection.start.line < selection.end.line
7508                || (selection.start.line == selection.end.line
7509                    && selection.start.column <= selection.end.column)
7510            {
7511                SelectionDirection::Forward
7512            } else {
7513                SelectionDirection::Backward
7514            };
7515        }
7516
7517        for selection in &mut self.editor.secondary_selections {
7518            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
7519            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
7520            selection.direction = if selection.start.line < selection.end.line
7521                || (selection.start.line == selection.end.line
7522                    && selection.start.column <= selection.end.column)
7523            {
7524                SelectionDirection::Forward
7525            } else {
7526                SelectionDirection::Backward
7527            };
7528        }
7529    }
7530
7531    fn clamp_column_for_line(&self, line: usize, column: usize) -> usize {
7532        Self::clamp_column_for_line_with_index(&self.editor.line_index, line, column)
7533    }
7534
7535    fn clamp_position_lenient_with_index(line_index: &LineIndex, pos: Position) -> Position {
7536        let line_count = line_index.line_count();
7537        if line_count == 0 {
7538            return Position::new(0, 0);
7539        }
7540
7541        let clamped_line = pos.line.min(line_count.saturating_sub(1));
7542        // Note: do NOT clamp column here. Virtual columns (box selection) are allowed.
7543        Position::new(clamped_line, pos.column)
7544    }
7545
7546    fn clamp_column_for_line_with_index(
7547        line_index: &LineIndex,
7548        line: usize,
7549        column: usize,
7550    ) -> usize {
7551        let line_start = line_index.position_to_char_offset(line, 0);
7552        let line_end = line_index.position_to_char_offset(line, usize::MAX);
7553        let line_len = line_end.saturating_sub(line_start);
7554        column.min(line_len)
7555    }
7556}
7557
7558#[cfg(test)]
7559mod tests {
7560    use super::*;
7561
7562    #[test]
7563    fn test_edit_insert() {
7564        let mut executor = CommandExecutor::new("Hello", 80);
7565
7566        let result = executor.execute(Command::Edit(EditCommand::Insert {
7567            offset: 5,
7568            text: " World".to_string(),
7569        }));
7570
7571        assert!(result.is_ok());
7572        assert_eq!(executor.editor().get_text(), "Hello World");
7573    }
7574
7575    #[test]
7576    fn test_edit_delete() {
7577        let mut executor = CommandExecutor::new("Hello World", 80);
7578
7579        let result = executor.execute(Command::Edit(EditCommand::Delete {
7580            start: 5,
7581            length: 6,
7582        }));
7583
7584        assert!(result.is_ok());
7585        assert_eq!(executor.editor().get_text(), "Hello");
7586    }
7587
7588    #[test]
7589    fn test_edit_replace() {
7590        let mut executor = CommandExecutor::new("Hello World", 80);
7591
7592        let result = executor.execute(Command::Edit(EditCommand::Replace {
7593            start: 6,
7594            length: 5,
7595            text: "Rust".to_string(),
7596        }));
7597
7598        assert!(result.is_ok());
7599        assert_eq!(executor.editor().get_text(), "Hello Rust");
7600    }
7601
7602    #[test]
7603    fn test_cursor_move_to() {
7604        let mut executor = CommandExecutor::new("Line 1\nLine 2\nLine 3", 80);
7605
7606        let result = executor.execute(Command::Cursor(CursorCommand::MoveTo {
7607            line: 1,
7608            column: 3,
7609        }));
7610
7611        assert!(result.is_ok());
7612        assert_eq!(executor.editor().cursor_position(), Position::new(1, 3));
7613    }
7614
7615    #[test]
7616    fn test_cursor_selection() {
7617        let mut executor = CommandExecutor::new("Hello World", 80);
7618
7619        let result = executor.execute(Command::Cursor(CursorCommand::SetSelection {
7620            start: Position::new(0, 0),
7621            end: Position::new(0, 5),
7622        }));
7623
7624        assert!(result.is_ok());
7625        assert!(executor.editor().selection().is_some());
7626    }
7627
7628    #[test]
7629    fn test_view_set_width() {
7630        let mut executor = CommandExecutor::new("Test", 80);
7631
7632        let result = executor.execute(Command::View(ViewCommand::SetViewportWidth { width: 40 }));
7633
7634        assert!(result.is_ok());
7635        assert_eq!(executor.editor().viewport_width, 40);
7636    }
7637
7638    #[test]
7639    fn test_style_add_remove() {
7640        let mut executor = CommandExecutor::new("Hello World", 80);
7641
7642        // Add style
7643        let result = executor.execute(Command::Style(StyleCommand::AddStyle {
7644            start: 0,
7645            end: 5,
7646            style_id: 1,
7647        }));
7648        assert!(result.is_ok());
7649
7650        // Remove style
7651        let result = executor.execute(Command::Style(StyleCommand::RemoveStyle {
7652            start: 0,
7653            end: 5,
7654            style_id: 1,
7655        }));
7656        assert!(result.is_ok());
7657    }
7658
7659    #[test]
7660    fn test_batch_execution() {
7661        let mut executor = CommandExecutor::new("", 80);
7662
7663        let commands = vec![
7664            Command::Edit(EditCommand::Insert {
7665                offset: 0,
7666                text: "Hello".to_string(),
7667            }),
7668            Command::Edit(EditCommand::Insert {
7669                offset: 5,
7670                text: " World".to_string(),
7671            }),
7672        ];
7673
7674        let results = executor.execute_batch(commands);
7675        assert!(results.is_ok());
7676        assert_eq!(executor.editor().get_text(), "Hello World");
7677    }
7678
7679    #[test]
7680    fn test_error_invalid_offset() {
7681        let mut executor = CommandExecutor::new("Hello", 80);
7682
7683        let result = executor.execute(Command::Edit(EditCommand::Insert {
7684            offset: 100,
7685            text: "X".to_string(),
7686        }));
7687
7688        assert!(result.is_err());
7689        assert!(matches!(
7690            result.unwrap_err(),
7691            CommandError::InvalidOffset(_)
7692        ));
7693    }
7694}