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};
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::{Cell, HeadlessGrid, HeadlessLine};
47use crate::{
48    FOLD_PLACEHOLDER_STYLE_ID, FoldingManager, IntervalTree, LayoutEngine, LineIndex, PieceTable,
49    SnapshotGenerator,
50};
51use regex::RegexBuilder;
52use std::cmp::Ordering;
53use std::collections::BTreeMap;
54use unicode_segmentation::UnicodeSegmentation;
55
56/// Position coordinates (line and column numbers)
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Position {
59    /// Zero-based logical line index.
60    pub line: usize,
61    /// Zero-based column in characters within the logical line.
62    pub column: usize,
63}
64
65impl Position {
66    /// Create a new logical position.
67    pub fn new(line: usize, column: usize) -> Self {
68        Self { line, column }
69    }
70}
71
72impl Ord for Position {
73    fn cmp(&self, other: &Self) -> Ordering {
74        self.line
75            .cmp(&other.line)
76            .then_with(|| self.column.cmp(&other.column))
77    }
78}
79
80impl PartialOrd for Position {
81    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
82        Some(self.cmp(other))
83    }
84}
85
86/// Selection range
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Selection {
89    /// Selection start position
90    pub start: Position,
91    /// Selection end position
92    pub end: Position,
93    /// Selection direction
94    pub direction: SelectionDirection,
95}
96
97/// Selection direction
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum SelectionDirection {
100    /// Forward selection (from start to end)
101    Forward,
102    /// Backward selection (from end to start)
103    Backward,
104}
105
106/// Controls how a Tab key press is handled by the editor when using [`EditCommand::InsertTab`].
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum TabKeyBehavior {
109    /// Insert a literal tab character (`'\t'`).
110    Tab,
111    /// Insert spaces up to the next tab stop (based on the current `tab_width` setting).
112    Spaces,
113}
114
115/// Text editing commands
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum EditCommand {
118    /// Insert text at the specified position
119    Insert {
120        /// Character offset to insert at.
121        offset: usize,
122        /// Text to insert.
123        text: String,
124    },
125    /// Delete text in specified range
126    Delete {
127        /// Character offset of the deletion start.
128        start: usize,
129        /// Length of the deletion in characters.
130        length: usize,
131    },
132    /// Replace text in specified range
133    Replace {
134        /// Character offset of the replacement start.
135        start: usize,
136        /// Length of the replaced range in characters.
137        length: usize,
138        /// Replacement text.
139        text: String,
140    },
141    /// VSCode-like typing/paste: apply to all carets/selections (primary + secondary)
142    InsertText {
143        /// Text to insert/replace at each selection/caret.
144        text: String,
145    },
146    /// Insert a tab at each caret (or replace each selection), using the current tab settings.
147    ///
148    /// - If `TabKeyBehavior::Tab`, inserts `'\t'`.
149    /// - If `TabKeyBehavior::Spaces`, inserts spaces up to the next tab stop.
150    InsertTab,
151    /// Insert a newline at each caret (or replace each selection).
152    ///
153    /// If `auto_indent` is true, the inserted newline is followed by the leading whitespace
154    /// prefix of the current logical line.
155    InsertNewline {
156        /// Whether to auto-indent the new line.
157        auto_indent: bool,
158    },
159    /// Indent the selected lines (or the current line for an empty selection).
160    Indent,
161    /// Outdent the selected lines (or the current line for an empty selection).
162    Outdent,
163    /// Smart backspace: if the caret is in leading whitespace, delete back to the previous tab stop.
164    ///
165    /// Otherwise, behaves like [`EditCommand::Backspace`].
166    DeleteToPrevTabStop,
167    /// Delete the previous Unicode grapheme cluster (UAX #29) for each caret/selection.
168    DeleteGraphemeBack,
169    /// Delete the next Unicode grapheme cluster (UAX #29) for each caret/selection.
170    DeleteGraphemeForward,
171    /// Delete back to the previous Unicode word boundary (UAX #29) for each caret/selection.
172    DeleteWordBack,
173    /// Delete forward to the next Unicode word boundary (UAX #29) for each caret/selection.
174    DeleteWordForward,
175    /// Backspace-like deletion: delete selection(s) if any, otherwise delete 1 char before each caret.
176    Backspace,
177    /// Delete key-like deletion: delete selection(s) if any, otherwise delete 1 char after each caret.
178    DeleteForward,
179    /// Undo last edit operation (supports grouping)
180    Undo,
181    /// Redo last undone operation (supports grouping)
182    Redo,
183    /// Explicitly end the current undo group (for idle or external boundaries)
184    EndUndoGroup,
185    /// Replace the current occurrence of `query` (based on selection/caret) with `replacement`.
186    ///
187    /// - Honors `options` (case sensitivity / whole-word / regex).
188    /// - Treated as a single undoable edit.
189    ReplaceCurrent {
190        /// Search query.
191        query: String,
192        /// Replacement text.
193        replacement: String,
194        /// Search options (case sensitivity, whole-word, regex).
195        options: SearchOptions,
196    },
197    /// Replace all occurrences of `query` with `replacement`.
198    ///
199    /// - Honors `options` (case sensitivity / whole-word / regex).
200    /// - Treated as a single undoable edit.
201    ReplaceAll {
202        /// Search query.
203        query: String,
204        /// Replacement text.
205        replacement: String,
206        /// Search options (case sensitivity, whole-word, regex).
207        options: SearchOptions,
208    },
209}
210
211/// Cursor & selection commands
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum CursorCommand {
214    /// Move cursor to the specified position
215    MoveTo {
216        /// Target logical line index.
217        line: usize,
218        /// Target column in characters (will be clamped to line length).
219        column: usize,
220    },
221    /// Move cursor relatively
222    MoveBy {
223        /// Delta in logical lines.
224        delta_line: isize,
225        /// Delta in columns (characters).
226        delta_column: isize,
227    },
228    /// Move cursor by visual rows (soft wrap + folding aware).
229    ///
230    /// This uses a "preferred x" in **cells** (sticky column) similar to many editors:
231    /// horizontal moves update preferred x, while vertical visual moves try to preserve it.
232    MoveVisualBy {
233        /// Delta in global visual rows (after wrapping/folding).
234        delta_rows: isize,
235    },
236    /// Move cursor to a visual position (global visual row + x in cells).
237    MoveToVisual {
238        /// Target global visual row (after wrapping/folding).
239        row: usize,
240        /// Target x offset in cells within that visual row.
241        x_cells: usize,
242    },
243    /// Move cursor to the start of the current logical line.
244    MoveToLineStart,
245    /// Move cursor to the end of the current logical line.
246    MoveToLineEnd,
247    /// Move cursor to the start of the current visual line segment (wrap-aware).
248    MoveToVisualLineStart,
249    /// Move cursor to the end of the current visual line segment (wrap-aware).
250    MoveToVisualLineEnd,
251    /// Move cursor left by one Unicode grapheme cluster (UAX #29).
252    MoveGraphemeLeft,
253    /// Move cursor right by one Unicode grapheme cluster (UAX #29).
254    MoveGraphemeRight,
255    /// Move cursor left to the previous Unicode word boundary (UAX #29).
256    MoveWordLeft,
257    /// Move cursor right to the next Unicode word boundary (UAX #29).
258    MoveWordRight,
259    /// Set selection range
260    SetSelection {
261        /// Selection start position.
262        start: Position,
263        /// Selection end position.
264        end: Position,
265    },
266    /// Extend selection range
267    ExtendSelection {
268        /// New active end position.
269        to: Position,
270    },
271    /// Clear selection
272    ClearSelection,
273    /// Set multiple selections/multi-cursor (including primary)
274    SetSelections {
275        /// All selections (including primary).
276        selections: Vec<Selection>,
277        /// Index of the primary selection in `selections`.
278        primary_index: usize,
279    },
280    /// Clear secondary selections/cursors, keeping only primary
281    ClearSecondarySelections,
282    /// Set rectangular selection (box/column selection), which expands into one Selection per line
283    SetRectSelection {
284        /// Anchor position (fixed corner).
285        anchor: Position,
286        /// Active position (moving corner).
287        active: Position,
288    },
289    /// Find the next occurrence of `query` and select it (primary selection only).
290    FindNext {
291        /// Search query.
292        query: String,
293        /// Search options (case sensitivity, whole-word, regex).
294        options: SearchOptions,
295    },
296    /// Find the previous occurrence of `query` and select it (primary selection only).
297    FindPrev {
298        /// Search query.
299        query: String,
300        /// Search options (case sensitivity, whole-word, regex).
301        options: SearchOptions,
302    },
303}
304
305/// View commands
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub enum ViewCommand {
308    /// Set viewport width
309    SetViewportWidth {
310        /// Width in character cells.
311        width: usize,
312    },
313    /// Set soft wrap mode.
314    SetWrapMode {
315        /// Wrap mode.
316        mode: WrapMode,
317    },
318    /// Set wrapped-line indentation policy.
319    SetWrapIndent {
320        /// Wrap indent policy.
321        indent: WrapIndent,
322    },
323    /// Set tab width (in character cells) used for measuring `'\t'` and tab stops.
324    SetTabWidth {
325        /// Tab width in character cells (must be greater than 0).
326        width: usize,
327    },
328    /// Configure how [`EditCommand::InsertTab`] inserts text.
329    SetTabKeyBehavior {
330        /// Tab key behavior.
331        behavior: TabKeyBehavior,
332    },
333    /// Scroll to specified line
334    ScrollTo {
335        /// Logical line index to scroll to.
336        line: usize,
337    },
338    /// Get current viewport content
339    GetViewport {
340        /// Starting visual row.
341        start_row: usize,
342        /// Number of visual rows requested.
343        count: usize,
344    },
345}
346
347/// Style and folding commands
348#[derive(Debug, Clone, PartialEq, Eq)]
349pub enum StyleCommand {
350    /// Add style interval
351    AddStyle {
352        /// Interval start offset in characters.
353        start: usize,
354        /// Interval end offset in characters (exclusive).
355        end: usize,
356        /// Style identifier.
357        style_id: StyleId,
358    },
359    /// Remove style interval
360    RemoveStyle {
361        /// Interval start offset in characters.
362        start: usize,
363        /// Interval end offset in characters (exclusive).
364        end: usize,
365        /// Style identifier.
366        style_id: StyleId,
367    },
368    /// Fold code block
369    Fold {
370        /// Start logical line (inclusive).
371        start_line: usize,
372        /// End logical line (inclusive).
373        end_line: usize,
374    },
375    /// Unfold code block
376    Unfold {
377        /// Start logical line (inclusive) of the fold region to unfold.
378        start_line: usize,
379    },
380    /// Unfold all folds
381    UnfoldAll,
382}
383
384/// Unified command enum
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub enum Command {
387    /// Text editing commands
388    Edit(EditCommand),
389    /// Cursor command
390    Cursor(CursorCommand),
391    /// View commands
392    View(ViewCommand),
393    /// Style command
394    Style(StyleCommand),
395}
396
397/// Command execution result
398#[derive(Debug, Clone)]
399pub enum CommandResult {
400    /// Success, no return value
401    Success,
402    /// Success, returns text
403    Text(String),
404    /// Success, returns position
405    Position(Position),
406    /// Success, returns offset
407    Offset(usize),
408    /// Success, returns viewport content
409    Viewport(HeadlessGrid),
410    /// Find/search result: a match in char offsets (half-open).
411    SearchMatch {
412        /// Inclusive start character offset.
413        start: usize,
414        /// Exclusive end character offset.
415        end: usize,
416    },
417    /// Find/search result: no match found.
418    SearchNotFound,
419    /// Replace result: how many occurrences were replaced.
420    ReplaceResult {
421        /// Number of occurrences replaced.
422        replaced: usize,
423    },
424}
425
426/// Command error type
427#[derive(Debug, Clone, PartialEq, Eq)]
428pub enum CommandError {
429    /// Invalid offset
430    InvalidOffset(usize),
431    /// Invalid position
432    InvalidPosition {
433        /// Logical line index.
434        line: usize,
435        /// Column in characters.
436        column: usize,
437    },
438    /// Invalid range
439    InvalidRange {
440        /// Inclusive start character offset.
441        start: usize,
442        /// Exclusive end character offset.
443        end: usize,
444    },
445    /// Empty text
446    EmptyText,
447    /// Other error
448    Other(String),
449}
450
451impl std::fmt::Display for CommandError {
452    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453        match self {
454            CommandError::InvalidOffset(offset) => {
455                write!(f, "Invalid offset: {}", offset)
456            }
457            CommandError::InvalidPosition { line, column } => {
458                write!(f, "Invalid position: line {}, column {}", line, column)
459            }
460            CommandError::InvalidRange { start, end } => {
461                write!(f, "Invalid range: {}..{}", start, end)
462            }
463            CommandError::EmptyText => {
464                write!(f, "Text cannot be empty")
465            }
466            CommandError::Other(msg) => {
467                write!(f, "{}", msg)
468            }
469        }
470    }
471}
472
473impl std::error::Error for CommandError {}
474
475#[derive(Debug, Clone)]
476struct SelectionSetSnapshot {
477    selections: Vec<Selection>,
478    primary_index: usize,
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482enum TextBoundary {
483    Grapheme,
484    Word,
485}
486
487fn byte_offset_for_char_column(text: &str, column: usize) -> usize {
488    if column == 0 {
489        return 0;
490    }
491
492    text.char_indices()
493        .nth(column)
494        .map(|(byte, _)| byte)
495        .unwrap_or_else(|| text.len())
496}
497
498fn char_column_for_byte_offset(text: &str, byte_offset: usize) -> usize {
499    text.get(..byte_offset).unwrap_or(text).chars().count()
500}
501
502fn prev_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
503    let byte_pos = byte_offset_for_char_column(text, column);
504
505    let mut prev = 0usize;
506    match boundary {
507        TextBoundary::Grapheme => {
508            for (b, _) in text.grapheme_indices(true) {
509                if b >= byte_pos {
510                    break;
511                }
512                prev = b;
513            }
514        }
515        TextBoundary::Word => {
516            for (b, _) in text.split_word_bound_indices() {
517                if b >= byte_pos {
518                    break;
519                }
520                prev = b;
521            }
522        }
523    }
524
525    char_column_for_byte_offset(text, prev)
526}
527
528fn next_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
529    let byte_pos = byte_offset_for_char_column(text, column);
530
531    let mut next = text.len();
532    match boundary {
533        TextBoundary::Grapheme => {
534            for (b, _) in text.grapheme_indices(true) {
535                if b > byte_pos {
536                    next = b;
537                    break;
538                }
539            }
540        }
541        TextBoundary::Word => {
542            for (b, _) in text.split_word_bound_indices() {
543                if b > byte_pos {
544                    next = b;
545                    break;
546                }
547            }
548        }
549    }
550
551    char_column_for_byte_offset(text, next)
552}
553
554#[derive(Debug, Clone)]
555struct TextEdit {
556    start_before: usize,
557    start_after: usize,
558    deleted_text: String,
559    inserted_text: String,
560}
561
562impl TextEdit {
563    fn deleted_len(&self) -> usize {
564        self.deleted_text.chars().count()
565    }
566
567    fn inserted_len(&self) -> usize {
568        self.inserted_text.chars().count()
569    }
570}
571
572#[derive(Debug, Clone)]
573struct UndoStep {
574    group_id: usize,
575    edits: Vec<TextEdit>,
576    before_selection: SelectionSetSnapshot,
577    after_selection: SelectionSetSnapshot,
578}
579
580#[derive(Debug)]
581struct UndoRedoManager {
582    undo_stack: Vec<UndoStep>,
583    redo_stack: Vec<UndoStep>,
584    max_undo: usize,
585    /// Clean point tracking. Uses `undo_stack.len()` as the saved position in the linear history.
586    /// When `redo_stack` is non-empty, `clean_index` may be greater than `undo_stack.len()`.
587    clean_index: Option<usize>,
588    next_group_id: usize,
589    open_group_id: Option<usize>,
590}
591
592impl UndoRedoManager {
593    fn new(max_undo: usize) -> Self {
594        Self {
595            undo_stack: Vec::new(),
596            redo_stack: Vec::new(),
597            max_undo,
598            clean_index: Some(0),
599            next_group_id: 0,
600            open_group_id: None,
601        }
602    }
603
604    fn can_undo(&self) -> bool {
605        !self.undo_stack.is_empty()
606    }
607
608    fn can_redo(&self) -> bool {
609        !self.redo_stack.is_empty()
610    }
611
612    fn undo_depth(&self) -> usize {
613        self.undo_stack.len()
614    }
615
616    fn redo_depth(&self) -> usize {
617        self.redo_stack.len()
618    }
619
620    fn current_group_id(&self) -> Option<usize> {
621        self.open_group_id
622    }
623
624    fn is_clean(&self) -> bool {
625        self.clean_index == Some(self.undo_stack.len())
626    }
627
628    fn mark_clean(&mut self) {
629        self.clean_index = Some(self.undo_stack.len());
630        self.end_group();
631    }
632
633    fn end_group(&mut self) {
634        self.open_group_id = None;
635    }
636
637    fn clear_redo_and_adjust_clean(&mut self) {
638        if self.redo_stack.is_empty() {
639            return;
640        }
641
642        // If clean point is in redo area, it becomes unreachable after clearing redo.
643        if let Some(clean_index) = self.clean_index
644            && clean_index > self.undo_stack.len()
645        {
646            self.clean_index = None;
647        }
648
649        self.redo_stack.clear();
650    }
651
652    fn push_step(&mut self, mut step: UndoStep, coalescible_insert: bool) -> usize {
653        self.clear_redo_and_adjust_clean();
654
655        if self.undo_stack.len() >= self.max_undo {
656            self.undo_stack.remove(0);
657            if let Some(clean_index) = self.clean_index {
658                if clean_index == 0 {
659                    self.clean_index = None;
660                } else {
661                    self.clean_index = Some(clean_index - 1);
662                }
663            }
664        }
665
666        let reuse_open_group = coalescible_insert
667            && self.open_group_id.is_some()
668            && self.clean_index != Some(self.undo_stack.len());
669
670        if reuse_open_group {
671            step.group_id = self.open_group_id.expect("checked");
672        } else {
673            step.group_id = self.next_group_id;
674            self.next_group_id = self.next_group_id.wrapping_add(1);
675        }
676
677        if coalescible_insert {
678            self.open_group_id = Some(step.group_id);
679        } else {
680            self.open_group_id = None;
681        }
682
683        let group_id = step.group_id;
684        self.undo_stack.push(step);
685        group_id
686    }
687
688    fn pop_undo_group(&mut self) -> Option<Vec<UndoStep>> {
689        let last_group_id = self.undo_stack.last().map(|s| s.group_id)?;
690        let mut steps: Vec<UndoStep> = Vec::new();
691
692        while let Some(step) = self.undo_stack.last() {
693            if step.group_id != last_group_id {
694                break;
695            }
696            steps.push(self.undo_stack.pop().expect("checked"));
697        }
698
699        Some(steps)
700    }
701
702    fn pop_redo_group(&mut self) -> Option<Vec<UndoStep>> {
703        let last_group_id = self.redo_stack.last().map(|s| s.group_id)?;
704        let mut steps: Vec<UndoStep> = Vec::new();
705
706        while let Some(step) = self.redo_stack.last() {
707            if step.group_id != last_group_id {
708                break;
709            }
710            steps.push(self.redo_stack.pop().expect("checked"));
711        }
712
713        Some(steps)
714    }
715}
716
717/// Editor Core state
718///
719/// `EditorCore` aggregates all underlying editor components, including:
720///
721/// - **PieceTable**: Efficient text storage and modification
722/// - **LineIndex**: Rope-based line index, supporting fast line access
723/// - **LayoutEngine**: Soft wrapping and text layout calculation
724/// - **IntervalTree**: Style interval management
725/// - **FoldingManager**: Code folding management
726/// - **Cursor & Selection**: Cursor and selection state
727///
728/// # Example
729///
730/// ```rust
731/// use editor_core::EditorCore;
732///
733/// let mut core = EditorCore::new("Hello\nWorld", 80);
734/// assert_eq!(core.line_count(), 2);
735/// assert_eq!(core.get_text(), "Hello\nWorld");
736/// ```
737pub struct EditorCore {
738    /// Piece Table storage layer
739    pub piece_table: PieceTable,
740    /// Line index
741    pub line_index: LineIndex,
742    /// Layout engine
743    pub layout_engine: LayoutEngine,
744    /// Interval tree (style management)
745    pub interval_tree: IntervalTree,
746    /// Layered styles (for semantic highlighting/simple syntax highlighting, etc.)
747    pub style_layers: BTreeMap<StyleLayerId, IntervalTree>,
748    /// Derived diagnostics for this document (character-offset ranges + metadata).
749    pub diagnostics: Vec<Diagnostic>,
750    /// Derived decorations for this document (virtual text, links, etc.).
751    pub decorations: BTreeMap<DecorationLayerId, Vec<Decoration>>,
752    /// Folding manager
753    pub folding_manager: FoldingManager,
754    /// Current cursor position
755    pub cursor_position: Position,
756    /// Current selection range
757    pub selection: Option<Selection>,
758    /// Secondary selections/cursors (multi-cursor). Each Selection can be empty (start==end), representing a caret.
759    pub secondary_selections: Vec<Selection>,
760    /// Viewport width
761    pub viewport_width: usize,
762}
763
764impl EditorCore {
765    /// Create a new Editor Core
766    pub fn new(text: &str, viewport_width: usize) -> Self {
767        let normalized = crate::text::normalize_crlf_to_lf(text);
768        let text = normalized.as_ref();
769
770        let piece_table = PieceTable::new(text);
771        let line_index = LineIndex::from_text(text);
772        let mut layout_engine = LayoutEngine::new(viewport_width);
773
774        // Initialize layout engine to be consistent with initial text (including trailing empty line).
775        let lines = crate::text::split_lines_preserve_trailing(text);
776        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
777        layout_engine.from_lines(&line_refs);
778
779        Self {
780            piece_table,
781            line_index,
782            layout_engine,
783            interval_tree: IntervalTree::new(),
784            style_layers: BTreeMap::new(),
785            diagnostics: Vec::new(),
786            decorations: BTreeMap::new(),
787            folding_manager: FoldingManager::new(),
788            cursor_position: Position::new(0, 0),
789            selection: None,
790            secondary_selections: Vec::new(),
791            viewport_width,
792        }
793    }
794
795    /// Create an empty Editor Core
796    pub fn empty(viewport_width: usize) -> Self {
797        Self::new("", viewport_width)
798    }
799
800    /// Get text content
801    pub fn get_text(&self) -> String {
802        self.piece_table.get_text()
803    }
804
805    /// Get total line count
806    pub fn line_count(&self) -> usize {
807        self.line_index.line_count()
808    }
809
810    /// Get total character count
811    pub fn char_count(&self) -> usize {
812        self.piece_table.char_count()
813    }
814
815    /// Get cursor position
816    pub fn cursor_position(&self) -> Position {
817        self.cursor_position
818    }
819
820    /// Get selection range
821    pub fn selection(&self) -> Option<&Selection> {
822        self.selection.as_ref()
823    }
824
825    /// Get secondary selections/cursors (multi-cursor)
826    pub fn secondary_selections(&self) -> &[Selection] {
827        &self.secondary_selections
828    }
829
830    /// Get the current diagnostics list.
831    pub fn diagnostics(&self) -> &[Diagnostic] {
832        &self.diagnostics
833    }
834
835    /// Get all decorations for a given layer.
836    pub fn decorations_for_layer(&self, layer: DecorationLayerId) -> &[Decoration] {
837        self.decorations
838            .get(&layer)
839            .map(Vec::as_slice)
840            .unwrap_or(&[])
841    }
842
843    /// Get styled headless grid snapshot (by visual line).
844    ///
845    /// - Supportsoft wrapping (based `layout_engine`)
846    /// - `Cell.styles` will `interval_tree` + `style_layers` merged from
847    /// - Supportcode folding (based `folding_manager`)
848    ///
849    /// Note: This API is not responsible for mapping `StyleId` to specific colors.
850    pub fn get_headless_grid_styled(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
851        let mut grid = HeadlessGrid::new(start_visual_row, count);
852        if count == 0 {
853            return grid;
854        }
855
856        let tab_width = self.layout_engine.tab_width();
857
858        let total_visual = self.visual_line_count();
859        if start_visual_row >= total_visual {
860            return grid;
861        }
862
863        let end_visual = start_visual_row.saturating_add(count).min(total_visual);
864
865        let mut current_visual = 0usize;
866        let logical_line_count = self.layout_engine.logical_line_count();
867        let regions = self.folding_manager.regions();
868
869        'outer: for logical_line in 0..logical_line_count {
870            if Self::is_logical_line_hidden(regions, logical_line) {
871                continue;
872            }
873
874            let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
875                continue;
876            };
877
878            let line_text = self
879                .line_index
880                .get_line_text(logical_line)
881                .unwrap_or_default();
882            let line_char_len = line_text.chars().count();
883            let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
884
885            for visual_in_line in 0..layout.visual_line_count {
886                if current_visual >= end_visual {
887                    break 'outer;
888                }
889
890                if current_visual >= start_visual_row {
891                    let segment_start_col = if visual_in_line == 0 {
892                        0
893                    } else {
894                        layout
895                            .wrap_points
896                            .get(visual_in_line - 1)
897                            .map(|wp| wp.char_index)
898                            .unwrap_or(0)
899                            .min(line_char_len)
900                    };
901
902                    let segment_end_col = if visual_in_line < layout.wrap_points.len() {
903                        layout.wrap_points[visual_in_line]
904                            .char_index
905                            .min(line_char_len)
906                    } else {
907                        line_char_len
908                    };
909
910                    let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
911                    if visual_in_line > 0 {
912                        let indent_cells = wrap_indent_cells_for_line_text(
913                            &line_text,
914                            self.layout_engine.wrap_indent(),
915                            self.viewport_width,
916                            tab_width,
917                        );
918                        for _ in 0..indent_cells {
919                            headless_line.add_cell(Cell::new(' ', 1));
920                        }
921                    }
922                    let mut x_in_line =
923                        visual_x_for_column(&line_text, segment_start_col, tab_width);
924
925                    for (col, ch) in line_text
926                        .chars()
927                        .enumerate()
928                        .skip(segment_start_col)
929                        .take(segment_end_col.saturating_sub(segment_start_col))
930                    {
931                        let offset = line_start_offset + col;
932                        let styles = self.styles_at_offset(offset);
933                        let w = cell_width_at(ch, x_in_line, tab_width);
934                        x_in_line = x_in_line.saturating_add(w);
935                        headless_line.add_cell(Cell::with_styles(ch, w, styles));
936                    }
937
938                    // For collapsed folding start line, append placeholder to the last segment.
939                    if visual_in_line + 1 == layout.visual_line_count
940                        && let Some(region) =
941                            Self::collapsed_region_starting_at(regions, logical_line)
942                        && !region.placeholder.is_empty()
943                    {
944                        if !headless_line.cells.is_empty() {
945                            x_in_line = x_in_line.saturating_add(char_width(' '));
946                            headless_line.add_cell(Cell::with_styles(
947                                ' ',
948                                char_width(' '),
949                                vec![FOLD_PLACEHOLDER_STYLE_ID],
950                            ));
951                        }
952                        for ch in region.placeholder.chars() {
953                            let w = cell_width_at(ch, x_in_line, tab_width);
954                            x_in_line = x_in_line.saturating_add(w);
955                            headless_line.add_cell(Cell::with_styles(
956                                ch,
957                                w,
958                                vec![FOLD_PLACEHOLDER_STYLE_ID],
959                            ));
960                        }
961                    }
962
963                    grid.add_line(headless_line);
964                }
965
966                current_visual = current_visual.saturating_add(1);
967            }
968        }
969
970        grid
971    }
972
973    /// Get total visual line count (considering soft wrapping + folding).
974    pub fn visual_line_count(&self) -> usize {
975        let regions = self.folding_manager.regions();
976        let mut total = 0usize;
977
978        for logical_line in 0..self.layout_engine.logical_line_count() {
979            if Self::is_logical_line_hidden(regions, logical_line) {
980                continue;
981            }
982
983            total = total.saturating_add(
984                self.layout_engine
985                    .get_line_layout(logical_line)
986                    .map(|l| l.visual_line_count)
987                    .unwrap_or(1),
988            );
989        }
990
991        total
992    }
993
994    /// Map visual line number back to (logical_line, visual_in_logical), considering folding.
995    pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
996        let regions = self.folding_manager.regions();
997        let mut cumulative_visual = 0usize;
998        let mut last_visible = (0usize, 0usize);
999
1000        for logical_line in 0..self.layout_engine.logical_line_count() {
1001            if Self::is_logical_line_hidden(regions, logical_line) {
1002                continue;
1003            }
1004
1005            let visual_count = self
1006                .layout_engine
1007                .get_line_layout(logical_line)
1008                .map(|l| l.visual_line_count)
1009                .unwrap_or(1);
1010
1011            if cumulative_visual + visual_count > visual_line {
1012                return (logical_line, visual_line - cumulative_visual);
1013            }
1014
1015            cumulative_visual = cumulative_visual.saturating_add(visual_count);
1016            last_visible = (logical_line, visual_count.saturating_sub(1));
1017        }
1018
1019        last_visible
1020    }
1021
1022    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
1023    pub fn logical_position_to_visual(
1024        &self,
1025        logical_line: usize,
1026        column: usize,
1027    ) -> Option<(usize, usize)> {
1028        let regions = self.folding_manager.regions();
1029        let logical_line = Self::closest_visible_line(regions, logical_line)?;
1030        let visual_start = self.visual_start_for_logical_line(logical_line)?;
1031
1032        let tab_width = self.layout_engine.tab_width();
1033
1034        let layout = self.layout_engine.get_line_layout(logical_line)?;
1035        let line_text = self
1036            .line_index
1037            .get_line_text(logical_line)
1038            .unwrap_or_default();
1039
1040        let line_char_len = line_text.chars().count();
1041        let column = column.min(line_char_len);
1042
1043        let mut wrapped_offset = 0usize;
1044        let mut segment_start_col = 0usize;
1045        for wrap_point in &layout.wrap_points {
1046            if column >= wrap_point.char_index {
1047                wrapped_offset = wrapped_offset.saturating_add(1);
1048                segment_start_col = wrap_point.char_index;
1049            } else {
1050                break;
1051            }
1052        }
1053
1054        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1055        let mut x_in_line = seg_start_x_in_line;
1056        let mut x_in_segment = 0usize;
1057        for ch in line_text
1058            .chars()
1059            .skip(segment_start_col)
1060            .take(column.saturating_sub(segment_start_col))
1061        {
1062            let w = cell_width_at(ch, x_in_line, tab_width);
1063            x_in_line = x_in_line.saturating_add(w);
1064            x_in_segment = x_in_segment.saturating_add(w);
1065        }
1066
1067        let indent = if wrapped_offset == 0 {
1068            0
1069        } else {
1070            wrap_indent_cells_for_line_text(
1071                &line_text,
1072                self.layout_engine.wrap_indent(),
1073                self.viewport_width,
1074                tab_width,
1075            )
1076        };
1077
1078        Some((
1079            visual_start.saturating_add(wrapped_offset),
1080            indent.saturating_add(x_in_segment),
1081        ))
1082    }
1083
1084    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
1085    ///
1086    /// Difference from [`logical_position_to_visual`](Self::logical_position_to_visual) is that it allows `column`
1087    /// to exceed the line end: the exceeding part is treated as `' '` (width=1) virtual spaces, suitable for rectangular selection / column editing.
1088    pub fn logical_position_to_visual_allow_virtual(
1089        &self,
1090        logical_line: usize,
1091        column: usize,
1092    ) -> Option<(usize, usize)> {
1093        let regions = self.folding_manager.regions();
1094        let logical_line = Self::closest_visible_line(regions, logical_line)?;
1095        let visual_start = self.visual_start_for_logical_line(logical_line)?;
1096
1097        let tab_width = self.layout_engine.tab_width();
1098
1099        let layout = self.layout_engine.get_line_layout(logical_line)?;
1100        let line_text = self
1101            .line_index
1102            .get_line_text(logical_line)
1103            .unwrap_or_default();
1104
1105        let line_char_len = line_text.chars().count();
1106        let clamped_column = column.min(line_char_len);
1107
1108        let mut wrapped_offset = 0usize;
1109        let mut segment_start_col = 0usize;
1110        for wrap_point in &layout.wrap_points {
1111            if clamped_column >= wrap_point.char_index {
1112                wrapped_offset = wrapped_offset.saturating_add(1);
1113                segment_start_col = wrap_point.char_index;
1114            } else {
1115                break;
1116            }
1117        }
1118
1119        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1120        let mut x_in_line = seg_start_x_in_line;
1121        let mut x_in_segment = 0usize;
1122        for ch in line_text
1123            .chars()
1124            .skip(segment_start_col)
1125            .take(clamped_column.saturating_sub(segment_start_col))
1126        {
1127            let w = cell_width_at(ch, x_in_line, tab_width);
1128            x_in_line = x_in_line.saturating_add(w);
1129            x_in_segment = x_in_segment.saturating_add(w);
1130        }
1131
1132        let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
1133
1134        let indent = if wrapped_offset == 0 {
1135            0
1136        } else {
1137            wrap_indent_cells_for_line_text(
1138                &line_text,
1139                self.layout_engine.wrap_indent(),
1140                self.viewport_width,
1141                tab_width,
1142            )
1143        };
1144
1145        Some((
1146            visual_start.saturating_add(wrapped_offset),
1147            indent.saturating_add(x_in_segment),
1148        ))
1149    }
1150
1151    /// Convert visual coordinates (global visual row + x in cells) back to logical `(line, column)`.
1152    ///
1153    /// - `visual_row` is the global visual row (after soft wrapping and folding).
1154    /// - `x_in_cells` is the cell offset within that visual row (0-based).
1155    ///
1156    /// Returns `None` if layout information is unavailable.
1157    pub fn visual_position_to_logical(
1158        &self,
1159        visual_row: usize,
1160        x_in_cells: usize,
1161    ) -> Option<Position> {
1162        let total_visual = self.visual_line_count();
1163        if total_visual == 0 {
1164            return Some(Position::new(0, 0));
1165        }
1166
1167        let clamped_row = visual_row.min(total_visual.saturating_sub(1));
1168        let (logical_line, visual_in_logical) = self.visual_to_logical_line(clamped_row);
1169
1170        let layout = self.layout_engine.get_line_layout(logical_line)?;
1171        let line_text = self
1172            .line_index
1173            .get_line_text(logical_line)
1174            .unwrap_or_default();
1175        let line_char_len = line_text.chars().count();
1176
1177        let segment_start_col = if visual_in_logical == 0 {
1178            0
1179        } else {
1180            layout
1181                .wrap_points
1182                .get(visual_in_logical - 1)
1183                .map(|wp| wp.char_index)
1184                .unwrap_or(0)
1185        };
1186
1187        let segment_end_col = layout
1188            .wrap_points
1189            .get(visual_in_logical)
1190            .map(|wp| wp.char_index)
1191            .unwrap_or(line_char_len)
1192            .max(segment_start_col)
1193            .min(line_char_len);
1194
1195        let tab_width = self.layout_engine.tab_width();
1196        let x_in_cells = if visual_in_logical == 0 {
1197            x_in_cells
1198        } else {
1199            let indent = wrap_indent_cells_for_line_text(
1200                &line_text,
1201                self.layout_engine.wrap_indent(),
1202                self.viewport_width,
1203                tab_width,
1204            );
1205            x_in_cells.saturating_sub(indent)
1206        };
1207        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
1208        let mut x_in_line = seg_start_x_in_line;
1209        let mut x_in_segment = 0usize;
1210        let mut column = segment_start_col;
1211
1212        for (char_idx, ch) in line_text.chars().enumerate().skip(segment_start_col) {
1213            if char_idx >= segment_end_col {
1214                break;
1215            }
1216
1217            let w = cell_width_at(ch, x_in_line, tab_width);
1218            if x_in_segment.saturating_add(w) > x_in_cells {
1219                break;
1220            }
1221
1222            x_in_line = x_in_line.saturating_add(w);
1223            x_in_segment = x_in_segment.saturating_add(w);
1224            column = column.saturating_add(1);
1225        }
1226
1227        Some(Position::new(logical_line, column))
1228    }
1229
1230    fn visual_start_for_logical_line(&self, logical_line: usize) -> Option<usize> {
1231        if logical_line >= self.layout_engine.logical_line_count() {
1232            return None;
1233        }
1234
1235        let regions = self.folding_manager.regions();
1236        if Self::is_logical_line_hidden(regions, logical_line) {
1237            return None;
1238        }
1239
1240        let mut start = 0usize;
1241        for line in 0..logical_line {
1242            if Self::is_logical_line_hidden(regions, line) {
1243                continue;
1244            }
1245            start = start.saturating_add(
1246                self.layout_engine
1247                    .get_line_layout(line)
1248                    .map(|l| l.visual_line_count)
1249                    .unwrap_or(1),
1250            );
1251        }
1252        Some(start)
1253    }
1254
1255    fn is_logical_line_hidden(regions: &[FoldRegion], logical_line: usize) -> bool {
1256        regions.iter().any(|region| {
1257            region.is_collapsed
1258                && logical_line > region.start_line
1259                && logical_line <= region.end_line
1260        })
1261    }
1262
1263    fn collapsed_region_starting_at(
1264        regions: &[FoldRegion],
1265        start_line: usize,
1266    ) -> Option<&FoldRegion> {
1267        regions
1268            .iter()
1269            .filter(|region| {
1270                region.is_collapsed
1271                    && region.start_line == start_line
1272                    && region.end_line > start_line
1273            })
1274            .min_by_key(|region| region.end_line)
1275    }
1276
1277    fn closest_visible_line(regions: &[FoldRegion], logical_line: usize) -> Option<usize> {
1278        let mut line = logical_line;
1279        if regions.is_empty() {
1280            return Some(line);
1281        }
1282
1283        while Self::is_logical_line_hidden(regions, line) {
1284            let Some(start) = regions
1285                .iter()
1286                .filter(|region| {
1287                    region.is_collapsed && line > region.start_line && line <= region.end_line
1288                })
1289                .map(|region| region.start_line)
1290                .max()
1291            else {
1292                break;
1293            };
1294            line = start;
1295        }
1296
1297        if Self::is_logical_line_hidden(regions, line) {
1298            None
1299        } else {
1300            Some(line)
1301        }
1302    }
1303
1304    fn styles_at_offset(&self, offset: usize) -> Vec<StyleId> {
1305        let mut styles: Vec<StyleId> = self
1306            .interval_tree
1307            .query_point(offset)
1308            .iter()
1309            .map(|interval| interval.style_id)
1310            .collect();
1311
1312        for tree in self.style_layers.values() {
1313            styles.extend(
1314                tree.query_point(offset)
1315                    .iter()
1316                    .map(|interval| interval.style_id),
1317            );
1318        }
1319
1320        styles.sort_unstable();
1321        styles.dedup();
1322        styles
1323    }
1324}
1325
1326/// Command executor
1327///
1328/// `CommandExecutor` is the main interface for the editor, responsible for:
1329///
1330/// - Execute various editor commands
1331/// - Maintain command history
1332/// - Handle errors and exceptions
1333/// - Ensure editor state consistency
1334///
1335/// # Command Types
1336///
1337/// - [`EditCommand`] - Text insertion, deletion, replacement
1338/// - [`CursorCommand`] - Cursor movement, selection operations
1339/// - [`ViewCommand`] - Viewport management and scroll control
1340/// - [`StyleCommand`] - Style and folding management
1341///
1342/// # Example
1343///
1344/// ```rust
1345/// use editor_core::{CommandExecutor, Command, EditCommand, CursorCommand, Position};
1346///
1347/// let mut executor = CommandExecutor::empty(80);
1348///
1349/// // Insert text
1350/// executor.execute(Command::Edit(EditCommand::Insert {
1351///     offset: 0,
1352///     text: "fn main() {}".to_string(),
1353/// })).unwrap();
1354///
1355/// // Move cursor
1356/// executor.execute(Command::Cursor(CursorCommand::MoveTo {
1357///     line: 0,
1358///     column: 3,
1359/// })).unwrap();
1360///
1361/// assert_eq!(executor.editor().cursor_position(), Position::new(0, 3));
1362/// ```
1363pub struct CommandExecutor {
1364    /// Editor Core
1365    editor: EditorCore,
1366    /// Command history
1367    command_history: Vec<Command>,
1368    /// Undo/redo manager (only records CommandExecutor edit commands executed via)
1369    undo_redo: UndoRedoManager,
1370    /// Controls how [`EditCommand::InsertTab`] behaves.
1371    tab_key_behavior: TabKeyBehavior,
1372    /// Preferred line ending for saving (internal storage is always LF).
1373    line_ending: LineEnding,
1374    /// Sticky x position for visual-row cursor movement (in cells).
1375    preferred_x_cells: Option<usize>,
1376    /// Structured delta for the last executed text modification (cleared on each `execute()` call).
1377    last_text_delta: Option<TextDelta>,
1378}
1379
1380impl CommandExecutor {
1381    /// Create a new command executor
1382    pub fn new(text: &str, viewport_width: usize) -> Self {
1383        Self {
1384            editor: EditorCore::new(text, viewport_width),
1385            command_history: Vec::new(),
1386            undo_redo: UndoRedoManager::new(1000),
1387            tab_key_behavior: TabKeyBehavior::Tab,
1388            line_ending: LineEnding::detect_in_text(text),
1389            preferred_x_cells: None,
1390            last_text_delta: None,
1391        }
1392    }
1393
1394    /// Create an empty command executor
1395    pub fn empty(viewport_width: usize) -> Self {
1396        Self::new("", viewport_width)
1397    }
1398
1399    /// Execute command
1400    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
1401        self.last_text_delta = None;
1402
1403        // Save command to history
1404        self.command_history.push(command.clone());
1405
1406        // Undo grouping: any non-edit command ends the current coalescing group.
1407        if !matches!(command, Command::Edit(_)) {
1408            self.undo_redo.end_group();
1409        }
1410
1411        // Execute command
1412        match command {
1413            Command::Edit(edit_cmd) => self.execute_edit(edit_cmd),
1414            Command::Cursor(cursor_cmd) => self.execute_cursor(cursor_cmd),
1415            Command::View(view_cmd) => self.execute_view(view_cmd),
1416            Command::Style(style_cmd) => self.execute_style(style_cmd),
1417        }
1418    }
1419
1420    /// Get the structured text delta produced by the last successful `execute()` call, if any.
1421    pub fn last_text_delta(&self) -> Option<&TextDelta> {
1422        self.last_text_delta.as_ref()
1423    }
1424
1425    /// Take the structured text delta produced by the last successful `execute()` call, if any.
1426    pub fn take_last_text_delta(&mut self) -> Option<TextDelta> {
1427        self.last_text_delta.take()
1428    }
1429
1430    /// Batch execute commands (transactional)
1431    pub fn execute_batch(
1432        &mut self,
1433        commands: Vec<Command>,
1434    ) -> Result<Vec<CommandResult>, CommandError> {
1435        let mut results = Vec::new();
1436
1437        for command in commands {
1438            let result = self.execute(command)?;
1439            results.push(result);
1440        }
1441
1442        Ok(results)
1443    }
1444
1445    /// Get command history
1446    pub fn get_command_history(&self) -> &[Command] {
1447        &self.command_history
1448    }
1449
1450    /// Can undo
1451    pub fn can_undo(&self) -> bool {
1452        self.undo_redo.can_undo()
1453    }
1454
1455    /// Can redo
1456    pub fn can_redo(&self) -> bool {
1457        self.undo_redo.can_redo()
1458    }
1459
1460    /// Undo stack depth (counted by undo steps; grouped undo may pop multiple steps at once)
1461    pub fn undo_depth(&self) -> usize {
1462        self.undo_redo.undo_depth()
1463    }
1464
1465    /// Redo stack depth (counted by undo steps)
1466    pub fn redo_depth(&self) -> usize {
1467        self.undo_redo.redo_depth()
1468    }
1469
1470    /// Currently open undo group ID (for insert coalescing only)
1471    pub fn current_change_group(&self) -> Option<usize> {
1472        self.undo_redo.current_group_id()
1473    }
1474
1475    /// Whether current state is at clean point (for dirty tracking)
1476    pub fn is_clean(&self) -> bool {
1477        self.undo_redo.is_clean()
1478    }
1479
1480    /// Mark current state as clean point (call after saving file)
1481    pub fn mark_clean(&mut self) {
1482        self.undo_redo.mark_clean();
1483    }
1484
1485    /// Get a reference to the Editor Core
1486    pub fn editor(&self) -> &EditorCore {
1487        &self.editor
1488    }
1489
1490    /// Get a mutable reference to the Editor Core
1491    pub fn editor_mut(&mut self) -> &mut EditorCore {
1492        &mut self.editor
1493    }
1494
1495    /// Get current tab key behavior used by [`EditCommand::InsertTab`].
1496    pub fn tab_key_behavior(&self) -> TabKeyBehavior {
1497        self.tab_key_behavior
1498    }
1499
1500    /// Set tab key behavior used by [`EditCommand::InsertTab`].
1501    pub fn set_tab_key_behavior(&mut self, behavior: TabKeyBehavior) {
1502        self.tab_key_behavior = behavior;
1503    }
1504
1505    /// Get the preferred line ending for saving this document.
1506    pub fn line_ending(&self) -> LineEnding {
1507        self.line_ending
1508    }
1509
1510    /// Override the preferred line ending for saving this document.
1511    pub fn set_line_ending(&mut self, line_ending: LineEnding) {
1512        self.line_ending = line_ending;
1513    }
1514
1515    // Private method: execute edit command
1516    fn execute_edit(&mut self, command: EditCommand) -> Result<CommandResult, CommandError> {
1517        match command {
1518            EditCommand::Undo => self.execute_undo_command(),
1519            EditCommand::Redo => self.execute_redo_command(),
1520            EditCommand::EndUndoGroup => {
1521                self.undo_redo.end_group();
1522                Ok(CommandResult::Success)
1523            }
1524            EditCommand::ReplaceCurrent {
1525                query,
1526                replacement,
1527                options,
1528            } => self.execute_replace_current_command(query, replacement, options),
1529            EditCommand::ReplaceAll {
1530                query,
1531                replacement,
1532                options,
1533            } => self.execute_replace_all_command(query, replacement, options),
1534            EditCommand::DeleteToPrevTabStop => self.execute_delete_to_prev_tab_stop_command(),
1535            EditCommand::DeleteGraphemeBack => {
1536                self.execute_delete_by_boundary_command(false, TextBoundary::Grapheme)
1537            }
1538            EditCommand::DeleteGraphemeForward => {
1539                self.execute_delete_by_boundary_command(true, TextBoundary::Grapheme)
1540            }
1541            EditCommand::DeleteWordBack => {
1542                self.execute_delete_by_boundary_command(false, TextBoundary::Word)
1543            }
1544            EditCommand::DeleteWordForward => {
1545                self.execute_delete_by_boundary_command(true, TextBoundary::Word)
1546            }
1547            EditCommand::Backspace => self.execute_backspace_command(),
1548            EditCommand::DeleteForward => self.execute_delete_forward_command(),
1549            EditCommand::InsertText { text } => self.execute_insert_text_command(text),
1550            EditCommand::InsertTab => self.execute_insert_tab_command(),
1551            EditCommand::InsertNewline { auto_indent } => {
1552                self.execute_insert_newline_command(auto_indent)
1553            }
1554            EditCommand::Indent => self.execute_indent_command(false),
1555            EditCommand::Outdent => self.execute_indent_command(true),
1556            EditCommand::Insert { offset, text } => self.execute_insert_command(offset, text),
1557            EditCommand::Delete { start, length } => self.execute_delete_command(start, length),
1558            EditCommand::Replace {
1559                start,
1560                length,
1561                text,
1562            } => self.execute_replace_command(start, length, text),
1563        }
1564    }
1565
1566    fn execute_undo_command(&mut self) -> Result<CommandResult, CommandError> {
1567        self.undo_redo.end_group();
1568        if !self.undo_redo.can_undo() {
1569            return Err(CommandError::Other("Nothing to undo".to_string()));
1570        }
1571
1572        let before_char_count = self.editor.piece_table.char_count();
1573        let steps = self
1574            .undo_redo
1575            .pop_undo_group()
1576            .ok_or_else(|| CommandError::Other("Nothing to undo".to_string()))?;
1577
1578        let undo_group_id = steps.first().map(|s| s.group_id);
1579        let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
1580
1581        for step in &steps {
1582            let mut step_edits: Vec<TextDeltaEdit> = step
1583                .edits
1584                .iter()
1585                .map(|edit| TextDeltaEdit {
1586                    start: edit.start_after,
1587                    deleted_text: edit.inserted_text.clone(),
1588                    inserted_text: edit.deleted_text.clone(),
1589                })
1590                .collect();
1591            step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
1592            delta_edits.extend(step_edits);
1593
1594            self.apply_undo_edits(&step.edits)?;
1595            self.restore_selection_set(step.before_selection.clone());
1596        }
1597
1598        // Move steps to redo stack in the same pop order (newest->oldest) so redo pops oldest first.
1599        for step in steps {
1600            self.undo_redo.redo_stack.push(step);
1601        }
1602
1603        self.last_text_delta = Some(TextDelta {
1604            before_char_count,
1605            after_char_count: self.editor.piece_table.char_count(),
1606            edits: delta_edits,
1607            undo_group_id,
1608        });
1609
1610        Ok(CommandResult::Success)
1611    }
1612
1613    fn execute_redo_command(&mut self) -> Result<CommandResult, CommandError> {
1614        self.undo_redo.end_group();
1615        if !self.undo_redo.can_redo() {
1616            return Err(CommandError::Other("Nothing to redo".to_string()));
1617        }
1618
1619        let before_char_count = self.editor.piece_table.char_count();
1620        let steps = self
1621            .undo_redo
1622            .pop_redo_group()
1623            .ok_or_else(|| CommandError::Other("Nothing to redo".to_string()))?;
1624
1625        let undo_group_id = steps.first().map(|s| s.group_id);
1626        let mut delta_edits: Vec<TextDeltaEdit> = Vec::new();
1627
1628        for step in &steps {
1629            let mut step_edits: Vec<TextDeltaEdit> = step
1630                .edits
1631                .iter()
1632                .map(|edit| TextDeltaEdit {
1633                    start: edit.start_before,
1634                    deleted_text: edit.deleted_text.clone(),
1635                    inserted_text: edit.inserted_text.clone(),
1636                })
1637                .collect();
1638            step_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
1639            delta_edits.extend(step_edits);
1640
1641            self.apply_redo_edits(&step.edits)?;
1642            self.restore_selection_set(step.after_selection.clone());
1643        }
1644
1645        // Reapplied steps return to undo stack in the same order (oldest->newest).
1646        for step in steps {
1647            self.undo_redo.undo_stack.push(step);
1648        }
1649
1650        self.last_text_delta = Some(TextDelta {
1651            before_char_count,
1652            after_char_count: self.editor.piece_table.char_count(),
1653            edits: delta_edits,
1654            undo_group_id,
1655        });
1656
1657        Ok(CommandResult::Success)
1658    }
1659
1660    fn execute_insert_text_command(&mut self, text: String) -> Result<CommandResult, CommandError> {
1661        if text.is_empty() {
1662            return Ok(CommandResult::Success);
1663        }
1664
1665        let text = crate::text::normalize_crlf_to_lf_string(text);
1666        let before_char_count = self.editor.piece_table.char_count();
1667        let before_selection = self.snapshot_selection_set();
1668
1669        // Build canonical selection set (primary + secondary), VSCode-like: edits are applied
1670        // "simultaneously" by computing ranges in the original document and mutating in
1671        // descending offset order.
1672        let mut selections: Vec<Selection> =
1673            Vec::with_capacity(1 + self.editor.secondary_selections.len());
1674        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
1675            start: self.editor.cursor_position,
1676            end: self.editor.cursor_position,
1677            direction: SelectionDirection::Forward,
1678        });
1679        selections.push(primary_selection);
1680        selections.extend(self.editor.secondary_selections.iter().cloned());
1681
1682        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
1683
1684        let text_char_len = text.chars().count();
1685
1686        struct Op {
1687            selection_index: usize,
1688            start_offset: usize,
1689            start_after: usize,
1690            delete_len: usize,
1691            deleted_text: String,
1692            insert_text: String,
1693            insert_char_len: usize,
1694        }
1695
1696        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
1697
1698        for (selection_index, selection) in selections.iter().enumerate() {
1699            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
1700                (selection.start, selection.end)
1701            } else {
1702                (selection.end, selection.start)
1703            };
1704
1705            let (start_offset, start_pad) =
1706                self.position_to_char_offset_and_virtual_pad(range_start_pos);
1707            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
1708
1709            let delete_len = end_offset.saturating_sub(start_offset);
1710            let insert_char_len = start_pad + text_char_len;
1711
1712            let deleted_text = if delete_len == 0 {
1713                String::new()
1714            } else {
1715                self.editor.piece_table.get_range(start_offset, delete_len)
1716            };
1717
1718            let mut insert_text = String::with_capacity(text.len() + start_pad);
1719            for _ in 0..start_pad {
1720                insert_text.push(' ');
1721            }
1722            insert_text.push_str(&text);
1723
1724            ops.push(Op {
1725                selection_index,
1726                start_offset,
1727                start_after: start_offset,
1728                delete_len,
1729                deleted_text,
1730                insert_text,
1731                insert_char_len,
1732            });
1733        }
1734
1735        // Compute final caret offsets in the post-edit document (ascending order with delta),
1736        // while also recording each operation's start offset in the post-edit document.
1737        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
1738        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
1739
1740        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
1741        let mut delta: i64 = 0;
1742        for &idx in &asc_indices {
1743            let op = &mut ops[idx];
1744            let effective_start = (op.start_offset as i64 + delta) as usize;
1745            op.start_after = effective_start;
1746            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
1747            delta += op.insert_char_len as i64 - op.delete_len as i64;
1748        }
1749
1750        // Apply edits safely (descending offsets).
1751        let mut desc_indices = asc_indices;
1752        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
1753
1754        for &idx in &desc_indices {
1755            let op = &ops[idx];
1756
1757            if op.delete_len > 0 {
1758                self.editor
1759                    .piece_table
1760                    .delete(op.start_offset, op.delete_len);
1761                self.editor
1762                    .interval_tree
1763                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1764                for layer_tree in self.editor.style_layers.values_mut() {
1765                    layer_tree
1766                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1767                }
1768            }
1769
1770            if !op.insert_text.is_empty() {
1771                self.editor
1772                    .piece_table
1773                    .insert(op.start_offset, &op.insert_text);
1774                self.editor
1775                    .interval_tree
1776                    .update_for_insertion(op.start_offset, op.insert_char_len);
1777                for layer_tree in self.editor.style_layers.values_mut() {
1778                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
1779                }
1780            }
1781        }
1782
1783        // Rebuild derived structures once.
1784        let updated_text = self.editor.piece_table.get_text();
1785        self.editor.line_index = LineIndex::from_text(&updated_text);
1786        self.rebuild_layout_engine_from_text(&updated_text);
1787
1788        // Update selection state: collapse to carets after typing.
1789        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
1790        for offset in &caret_offsets {
1791            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
1792            let pos = Position::new(line, column);
1793            new_carets.push(Selection {
1794                start: pos,
1795                end: pos,
1796                direction: SelectionDirection::Forward,
1797            });
1798        }
1799
1800        let (new_carets, new_primary_index) =
1801            crate::selection_set::normalize_selections(new_carets, primary_index);
1802        let primary = new_carets
1803            .get(new_primary_index)
1804            .cloned()
1805            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
1806
1807        self.editor.cursor_position = primary.end;
1808        self.editor.selection = None;
1809        self.editor.secondary_selections = new_carets
1810            .into_iter()
1811            .enumerate()
1812            .filter_map(|(idx, sel)| {
1813                if idx == new_primary_index {
1814                    None
1815                } else {
1816                    Some(sel)
1817                }
1818            })
1819            .collect();
1820
1821        let after_selection = self.snapshot_selection_set();
1822
1823        let edits: Vec<TextEdit> = ops
1824            .into_iter()
1825            .map(|op| TextEdit {
1826                start_before: op.start_offset,
1827                start_after: op.start_after,
1828                deleted_text: op.deleted_text,
1829                inserted_text: op.insert_text,
1830            })
1831            .collect();
1832
1833        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
1834        let coalescible_insert = is_pure_insert && !text.contains('\n');
1835
1836        let mut delta_edits: Vec<TextDeltaEdit> = edits
1837            .iter()
1838            .map(|e| TextDeltaEdit {
1839                start: e.start_before,
1840                deleted_text: e.deleted_text.clone(),
1841                inserted_text: e.inserted_text.clone(),
1842            })
1843            .collect();
1844        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
1845
1846        let step = UndoStep {
1847            group_id: 0,
1848            edits,
1849            before_selection,
1850            after_selection,
1851        };
1852        let group_id = self.undo_redo.push_step(step, coalescible_insert);
1853
1854        self.last_text_delta = Some(TextDelta {
1855            before_char_count,
1856            after_char_count: self.editor.piece_table.char_count(),
1857            edits: delta_edits,
1858            undo_group_id: Some(group_id),
1859        });
1860
1861        Ok(CommandResult::Success)
1862    }
1863
1864    fn execute_insert_tab_command(&mut self) -> Result<CommandResult, CommandError> {
1865        let before_char_count = self.editor.piece_table.char_count();
1866        let before_selection = self.snapshot_selection_set();
1867
1868        let mut selections: Vec<Selection> =
1869            Vec::with_capacity(1 + self.editor.secondary_selections.len());
1870        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
1871            start: self.editor.cursor_position,
1872            end: self.editor.cursor_position,
1873            direction: SelectionDirection::Forward,
1874        });
1875        selections.push(primary_selection);
1876        selections.extend(self.editor.secondary_selections.iter().cloned());
1877
1878        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
1879
1880        let tab_width = self.editor.layout_engine.tab_width();
1881
1882        struct Op {
1883            selection_index: usize,
1884            start_offset: usize,
1885            start_after: usize,
1886            delete_len: usize,
1887            deleted_text: String,
1888            insert_text: String,
1889            insert_char_len: usize,
1890        }
1891
1892        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
1893
1894        for (selection_index, selection) in selections.iter().enumerate() {
1895            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
1896                (selection.start, selection.end)
1897            } else {
1898                (selection.end, selection.start)
1899            };
1900
1901            let (start_offset, start_pad) =
1902                self.position_to_char_offset_and_virtual_pad(range_start_pos);
1903            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
1904
1905            let delete_len = end_offset.saturating_sub(start_offset);
1906
1907            let deleted_text = if delete_len == 0 {
1908                String::new()
1909            } else {
1910                self.editor.piece_table.get_range(start_offset, delete_len)
1911            };
1912
1913            // Compute cell X within the logical line at the insertion position (including virtual pad).
1914            let line_text = self
1915                .editor
1916                .line_index
1917                .get_line_text(range_start_pos.line)
1918                .unwrap_or_default();
1919            let line_char_len = line_text.chars().count();
1920            let clamped_col = range_start_pos.column.min(line_char_len);
1921            let x_in_line =
1922                visual_x_for_column(&line_text, clamped_col, tab_width).saturating_add(start_pad);
1923
1924            let mut insert_text = String::new();
1925            for _ in 0..start_pad {
1926                insert_text.push(' ');
1927            }
1928
1929            match self.tab_key_behavior {
1930                TabKeyBehavior::Tab => {
1931                    insert_text.push('\t');
1932                    ops.push(Op {
1933                        selection_index,
1934                        start_offset,
1935                        start_after: start_offset,
1936                        delete_len,
1937                        deleted_text,
1938                        insert_text,
1939                        insert_char_len: start_pad + 1,
1940                    });
1941                }
1942                TabKeyBehavior::Spaces => {
1943                    let tab_width = tab_width.max(1);
1944                    let rem = x_in_line % tab_width;
1945                    let spaces = tab_width - rem;
1946                    for _ in 0..spaces {
1947                        insert_text.push(' ');
1948                    }
1949
1950                    ops.push(Op {
1951                        selection_index,
1952                        start_offset,
1953                        start_after: start_offset,
1954                        delete_len,
1955                        deleted_text,
1956                        insert_text,
1957                        insert_char_len: start_pad + spaces,
1958                    });
1959                }
1960            }
1961        }
1962
1963        // Compute final caret offsets in the post-edit document (ascending order with delta),
1964        // while also recording each operation's start offset in the post-edit document.
1965        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
1966        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
1967
1968        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
1969        let mut delta: i64 = 0;
1970        for &idx in &asc_indices {
1971            let op = &mut ops[idx];
1972            let effective_start = (op.start_offset as i64 + delta) as usize;
1973            op.start_after = effective_start;
1974            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
1975            delta += op.insert_char_len as i64 - op.delete_len as i64;
1976        }
1977
1978        // Apply edits safely (descending offsets).
1979        let mut desc_indices = asc_indices;
1980        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
1981
1982        for &idx in &desc_indices {
1983            let op = &ops[idx];
1984
1985            if op.delete_len > 0 {
1986                self.editor
1987                    .piece_table
1988                    .delete(op.start_offset, op.delete_len);
1989                self.editor
1990                    .interval_tree
1991                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1992                for layer_tree in self.editor.style_layers.values_mut() {
1993                    layer_tree
1994                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1995                }
1996            }
1997
1998            if !op.insert_text.is_empty() {
1999                self.editor
2000                    .piece_table
2001                    .insert(op.start_offset, &op.insert_text);
2002                self.editor
2003                    .interval_tree
2004                    .update_for_insertion(op.start_offset, op.insert_char_len);
2005                for layer_tree in self.editor.style_layers.values_mut() {
2006                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
2007                }
2008            }
2009        }
2010
2011        // Rebuild derived structures once.
2012        let updated_text = self.editor.piece_table.get_text();
2013        self.editor.line_index = LineIndex::from_text(&updated_text);
2014        self.rebuild_layout_engine_from_text(&updated_text);
2015
2016        // Update selection state: collapse to carets after insertion.
2017        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2018        for offset in &caret_offsets {
2019            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2020            let pos = Position::new(line, column);
2021            new_carets.push(Selection {
2022                start: pos,
2023                end: pos,
2024                direction: SelectionDirection::Forward,
2025            });
2026        }
2027
2028        let (new_carets, new_primary_index) =
2029            crate::selection_set::normalize_selections(new_carets, primary_index);
2030        let primary = new_carets
2031            .get(new_primary_index)
2032            .cloned()
2033            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2034
2035        self.editor.cursor_position = primary.end;
2036        self.editor.selection = None;
2037        self.editor.secondary_selections = new_carets
2038            .into_iter()
2039            .enumerate()
2040            .filter_map(|(idx, sel)| {
2041                if idx == new_primary_index {
2042                    None
2043                } else {
2044                    Some(sel)
2045                }
2046            })
2047            .collect();
2048
2049        let after_selection = self.snapshot_selection_set();
2050
2051        let edits: Vec<TextEdit> = ops
2052            .into_iter()
2053            .map(|op| TextEdit {
2054                start_before: op.start_offset,
2055                start_after: op.start_after,
2056                deleted_text: op.deleted_text,
2057                inserted_text: op.insert_text,
2058            })
2059            .collect();
2060
2061        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
2062        let coalescible_insert = is_pure_insert;
2063
2064        let mut delta_edits: Vec<TextDeltaEdit> = edits
2065            .iter()
2066            .map(|e| TextDeltaEdit {
2067                start: e.start_before,
2068                deleted_text: e.deleted_text.clone(),
2069                inserted_text: e.inserted_text.clone(),
2070            })
2071            .collect();
2072        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2073
2074        let step = UndoStep {
2075            group_id: 0,
2076            edits,
2077            before_selection,
2078            after_selection,
2079        };
2080        let group_id = self.undo_redo.push_step(step, coalescible_insert);
2081
2082        self.last_text_delta = Some(TextDelta {
2083            before_char_count,
2084            after_char_count: self.editor.piece_table.char_count(),
2085            edits: delta_edits,
2086            undo_group_id: Some(group_id),
2087        });
2088
2089        Ok(CommandResult::Success)
2090    }
2091
2092    fn leading_whitespace_prefix(line_text: &str) -> String {
2093        line_text
2094            .chars()
2095            .take_while(|ch| *ch == ' ' || *ch == '\t')
2096            .collect()
2097    }
2098
2099    fn indent_unit(&self) -> String {
2100        match self.tab_key_behavior {
2101            TabKeyBehavior::Tab => "\t".to_string(),
2102            TabKeyBehavior::Spaces => " ".repeat(self.editor.layout_engine.tab_width().max(1)),
2103        }
2104    }
2105
2106    fn execute_insert_newline_command(
2107        &mut self,
2108        auto_indent: bool,
2109    ) -> Result<CommandResult, CommandError> {
2110        // Newline insertion should not coalesce into a typing group.
2111        self.undo_redo.end_group();
2112
2113        let before_char_count = self.editor.piece_table.char_count();
2114        let before_selection = self.snapshot_selection_set();
2115
2116        // Canonical selection set (primary + secondary).
2117        let mut selections: Vec<Selection> =
2118            Vec::with_capacity(1 + self.editor.secondary_selections.len());
2119        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
2120            start: self.editor.cursor_position,
2121            end: self.editor.cursor_position,
2122            direction: SelectionDirection::Forward,
2123        });
2124        selections.push(primary_selection);
2125        selections.extend(self.editor.secondary_selections.iter().cloned());
2126
2127        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
2128
2129        struct Op {
2130            selection_index: usize,
2131            start_offset: usize,
2132            start_after: usize,
2133            delete_len: usize,
2134            deleted_text: String,
2135            insert_text: String,
2136            insert_char_len: usize,
2137        }
2138
2139        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
2140
2141        for (selection_index, selection) in selections.iter().enumerate() {
2142            let (range_start_pos, range_end_pos) =
2143                crate::selection_set::selection_min_max(selection);
2144
2145            let start_offset = self.position_to_char_offset_clamped(range_start_pos);
2146            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
2147
2148            let delete_len = end_offset.saturating_sub(start_offset);
2149            let deleted_text = if delete_len == 0 {
2150                String::new()
2151            } else {
2152                self.editor.piece_table.get_range(start_offset, delete_len)
2153            };
2154
2155            let indent = if auto_indent {
2156                let line_text = self
2157                    .editor
2158                    .line_index
2159                    .get_line_text(range_start_pos.line)
2160                    .unwrap_or_default();
2161                Self::leading_whitespace_prefix(&line_text)
2162            } else {
2163                String::new()
2164            };
2165
2166            let insert_text = format!("\n{}", indent);
2167            let insert_char_len = insert_text.chars().count();
2168
2169            ops.push(Op {
2170                selection_index,
2171                start_offset,
2172                start_after: start_offset,
2173                delete_len,
2174                deleted_text,
2175                insert_text,
2176                insert_char_len,
2177            });
2178        }
2179
2180        // Compute final caret offsets in the post-edit document (ascending order with delta),
2181        // while also recording each operation's start offset in the post-edit document.
2182        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2183        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2184
2185        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
2186        let mut delta: i64 = 0;
2187        for &idx in &asc_indices {
2188            let op = &mut ops[idx];
2189            let effective_start = (op.start_offset as i64 + delta) as usize;
2190            op.start_after = effective_start;
2191            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
2192            delta += op.insert_char_len as i64 - op.delete_len as i64;
2193        }
2194
2195        // Apply edits safely (descending offsets).
2196        let mut desc_indices = asc_indices;
2197        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2198
2199        for &idx in &desc_indices {
2200            let op = &ops[idx];
2201
2202            if op.delete_len > 0 {
2203                self.editor
2204                    .piece_table
2205                    .delete(op.start_offset, op.delete_len);
2206                self.editor
2207                    .interval_tree
2208                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2209                for layer_tree in self.editor.style_layers.values_mut() {
2210                    layer_tree
2211                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2212                }
2213            }
2214
2215            if !op.insert_text.is_empty() {
2216                self.editor
2217                    .piece_table
2218                    .insert(op.start_offset, &op.insert_text);
2219                self.editor
2220                    .interval_tree
2221                    .update_for_insertion(op.start_offset, op.insert_char_len);
2222                for layer_tree in self.editor.style_layers.values_mut() {
2223                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
2224                }
2225            }
2226        }
2227
2228        // Rebuild derived structures once.
2229        let updated_text = self.editor.piece_table.get_text();
2230        self.editor.line_index = LineIndex::from_text(&updated_text);
2231        self.rebuild_layout_engine_from_text(&updated_text);
2232
2233        // Update selection state: collapse to carets after insertion.
2234        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2235        for offset in &caret_offsets {
2236            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2237            let pos = Position::new(line, column);
2238            new_carets.push(Selection {
2239                start: pos,
2240                end: pos,
2241                direction: SelectionDirection::Forward,
2242            });
2243        }
2244
2245        let (new_carets, new_primary_index) =
2246            crate::selection_set::normalize_selections(new_carets, primary_index);
2247        let primary = new_carets
2248            .get(new_primary_index)
2249            .cloned()
2250            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2251
2252        self.editor.cursor_position = primary.end;
2253        self.editor.selection = None;
2254        self.editor.secondary_selections = new_carets
2255            .into_iter()
2256            .enumerate()
2257            .filter_map(|(idx, sel)| {
2258                if idx == new_primary_index {
2259                    None
2260                } else {
2261                    Some(sel)
2262                }
2263            })
2264            .collect();
2265
2266        let after_selection = self.snapshot_selection_set();
2267
2268        let edits: Vec<TextEdit> = ops
2269            .into_iter()
2270            .map(|op| TextEdit {
2271                start_before: op.start_offset,
2272                start_after: op.start_after,
2273                deleted_text: op.deleted_text,
2274                inserted_text: op.insert_text,
2275            })
2276            .collect();
2277
2278        let mut delta_edits: Vec<TextDeltaEdit> = edits
2279            .iter()
2280            .map(|e| TextDeltaEdit {
2281                start: e.start_before,
2282                deleted_text: e.deleted_text.clone(),
2283                inserted_text: e.inserted_text.clone(),
2284            })
2285            .collect();
2286        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2287
2288        let step = UndoStep {
2289            group_id: 0,
2290            edits,
2291            before_selection,
2292            after_selection,
2293        };
2294        let group_id = self.undo_redo.push_step(step, false);
2295
2296        self.last_text_delta = Some(TextDelta {
2297            before_char_count,
2298            after_char_count: self.editor.piece_table.char_count(),
2299            edits: delta_edits,
2300            undo_group_id: Some(group_id),
2301        });
2302
2303        Ok(CommandResult::Success)
2304    }
2305
2306    fn execute_indent_command(&mut self, outdent: bool) -> Result<CommandResult, CommandError> {
2307        self.undo_redo.end_group();
2308
2309        let before_char_count = self.editor.piece_table.char_count();
2310        let before_selection = self.snapshot_selection_set();
2311        let selections = before_selection.selections.clone();
2312
2313        let mut lines: Vec<usize> = Vec::new();
2314        for sel in &selections {
2315            let (min_pos, max_pos) = crate::selection_set::selection_min_max(sel);
2316            for line in min_pos.line..=max_pos.line {
2317                lines.push(line);
2318            }
2319        }
2320        lines.sort_unstable();
2321        lines.dedup();
2322
2323        if lines.is_empty() {
2324            return Ok(CommandResult::Success);
2325        }
2326
2327        let tab_width = self.editor.layout_engine.tab_width().max(1);
2328        let indent_unit = self.indent_unit();
2329        let indent_chars = indent_unit.chars().count();
2330
2331        #[derive(Debug)]
2332        struct Op {
2333            start_offset: usize,
2334            start_after: usize,
2335            delete_len: usize,
2336            deleted_text: String,
2337            insert_text: String,
2338            insert_len: usize,
2339        }
2340
2341        let mut ops: Vec<Op> = Vec::new();
2342        let mut line_deltas: std::collections::HashMap<usize, isize> =
2343            std::collections::HashMap::new();
2344
2345        for line in lines {
2346            if line >= self.editor.line_index.line_count() {
2347                continue;
2348            }
2349
2350            let start_offset = self.editor.line_index.position_to_char_offset(line, 0);
2351            let line_text = self
2352                .editor
2353                .line_index
2354                .get_line_text(line)
2355                .unwrap_or_default();
2356
2357            if outdent {
2358                let mut remove_len = 0usize;
2359                if let Some(first) = line_text.chars().next() {
2360                    if first == '\t' {
2361                        remove_len = 1;
2362                    } else if first == ' ' {
2363                        let leading_spaces = line_text.chars().take_while(|c| *c == ' ').count();
2364                        remove_len = leading_spaces.min(tab_width);
2365                    }
2366                }
2367
2368                if remove_len == 0 {
2369                    continue;
2370                }
2371
2372                let deleted_text = self.editor.piece_table.get_range(start_offset, remove_len);
2373                ops.push(Op {
2374                    start_offset,
2375                    start_after: start_offset,
2376                    delete_len: remove_len,
2377                    deleted_text,
2378                    insert_text: String::new(),
2379                    insert_len: 0,
2380                });
2381                line_deltas.insert(line, -(remove_len as isize));
2382            } else {
2383                if indent_chars == 0 {
2384                    continue;
2385                }
2386
2387                ops.push(Op {
2388                    start_offset,
2389                    start_after: start_offset,
2390                    delete_len: 0,
2391                    deleted_text: String::new(),
2392                    insert_text: indent_unit.clone(),
2393                    insert_len: indent_chars,
2394                });
2395                line_deltas.insert(line, indent_chars as isize);
2396            }
2397        }
2398
2399        if ops.is_empty() {
2400            return Ok(CommandResult::Success);
2401        }
2402
2403        // Compute start_after using ascending order and delta accumulation.
2404        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2405        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2406
2407        let mut delta: i64 = 0;
2408        for &idx in &asc_indices {
2409            let op = &mut ops[idx];
2410            let effective_start = (op.start_offset as i64 + delta) as usize;
2411            op.start_after = effective_start;
2412            delta += op.insert_len as i64 - op.delete_len as i64;
2413        }
2414
2415        // Apply ops descending so offsets remain valid.
2416        let mut desc_indices = asc_indices;
2417        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2418
2419        for &idx in &desc_indices {
2420            let op = &ops[idx];
2421
2422            if op.delete_len > 0 {
2423                self.editor
2424                    .piece_table
2425                    .delete(op.start_offset, op.delete_len);
2426                self.editor
2427                    .interval_tree
2428                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2429                for layer_tree in self.editor.style_layers.values_mut() {
2430                    layer_tree
2431                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2432                }
2433            }
2434
2435            if op.insert_len > 0 {
2436                self.editor
2437                    .piece_table
2438                    .insert(op.start_offset, &op.insert_text);
2439                self.editor
2440                    .interval_tree
2441                    .update_for_insertion(op.start_offset, op.insert_len);
2442                for layer_tree in self.editor.style_layers.values_mut() {
2443                    layer_tree.update_for_insertion(op.start_offset, op.insert_len);
2444                }
2445            }
2446        }
2447
2448        // Rebuild derived structures.
2449        let updated_text = self.editor.piece_table.get_text();
2450        self.editor.line_index = LineIndex::from_text(&updated_text);
2451        self.rebuild_layout_engine_from_text(&updated_text);
2452
2453        // Shift cursor/selections for touched lines.
2454        let line_index = &self.editor.line_index;
2455        let apply_delta = |pos: &mut Position, deltas: &std::collections::HashMap<usize, isize>| {
2456            let Some(delta) = deltas.get(&pos.line) else {
2457                return;
2458            };
2459
2460            let new_col = if *delta >= 0 {
2461                pos.column.saturating_add(*delta as usize)
2462            } else {
2463                pos.column.saturating_sub((-*delta) as usize)
2464            };
2465
2466            pos.column = Self::clamp_column_for_line_with_index(line_index, pos.line, new_col);
2467        };
2468
2469        apply_delta(&mut self.editor.cursor_position, &line_deltas);
2470        if let Some(sel) = &mut self.editor.selection {
2471            apply_delta(&mut sel.start, &line_deltas);
2472            apply_delta(&mut sel.end, &line_deltas);
2473        }
2474        for sel in &mut self.editor.secondary_selections {
2475            apply_delta(&mut sel.start, &line_deltas);
2476            apply_delta(&mut sel.end, &line_deltas);
2477        }
2478
2479        self.normalize_cursor_and_selection();
2480        self.preferred_x_cells = self
2481            .editor
2482            .logical_position_to_visual(
2483                self.editor.cursor_position.line,
2484                self.editor.cursor_position.column,
2485            )
2486            .map(|(_, x)| x);
2487
2488        let after_selection = self.snapshot_selection_set();
2489
2490        let edits: Vec<TextEdit> = ops
2491            .into_iter()
2492            .map(|op| TextEdit {
2493                start_before: op.start_offset,
2494                start_after: op.start_after,
2495                deleted_text: op.deleted_text,
2496                inserted_text: op.insert_text,
2497            })
2498            .collect();
2499
2500        let mut delta_edits: Vec<TextDeltaEdit> = edits
2501            .iter()
2502            .map(|e| TextDeltaEdit {
2503                start: e.start_before,
2504                deleted_text: e.deleted_text.clone(),
2505                inserted_text: e.inserted_text.clone(),
2506            })
2507            .collect();
2508        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
2509
2510        let step = UndoStep {
2511            group_id: 0,
2512            edits,
2513            before_selection,
2514            after_selection,
2515        };
2516        let group_id = self.undo_redo.push_step(step, false);
2517
2518        self.last_text_delta = Some(TextDelta {
2519            before_char_count,
2520            after_char_count: self.editor.piece_table.char_count(),
2521            edits: delta_edits,
2522            undo_group_id: Some(group_id),
2523        });
2524
2525        Ok(CommandResult::Success)
2526    }
2527
2528    fn execute_insert_command(
2529        &mut self,
2530        offset: usize,
2531        text: String,
2532    ) -> Result<CommandResult, CommandError> {
2533        if text.is_empty() {
2534            return Err(CommandError::EmptyText);
2535        }
2536
2537        let text = crate::text::normalize_crlf_to_lf_string(text);
2538        let max_offset = self.editor.piece_table.char_count();
2539        if offset > max_offset {
2540            return Err(CommandError::InvalidOffset(offset));
2541        }
2542
2543        let before_char_count = self.editor.piece_table.char_count();
2544        let before_selection = self.snapshot_selection_set();
2545
2546        let affected_line = self.editor.line_index.char_offset_to_position(offset).0;
2547        let inserts_newline = text.contains('\n');
2548
2549        // Execute insertion
2550        self.editor.piece_table.insert(offset, &text);
2551
2552        // Update line index
2553        let updated_text = self.editor.piece_table.get_text();
2554        self.editor.line_index = LineIndex::from_text(&updated_text);
2555
2556        // Update layout engine (soft wrappingneeds to stay consistent with text)
2557        if inserts_newline {
2558            self.rebuild_layout_engine_from_text(&updated_text);
2559        } else {
2560            let line_text = self
2561                .editor
2562                .line_index
2563                .get_line_text(affected_line)
2564                .unwrap_or_default();
2565            self.editor
2566                .layout_engine
2567                .update_line(affected_line, &line_text);
2568        }
2569
2570        let inserted_len = text.chars().count();
2571
2572        // Update interval tree offsets
2573        self.editor
2574            .interval_tree
2575            .update_for_insertion(offset, inserted_len);
2576        for layer_tree in self.editor.style_layers.values_mut() {
2577            layer_tree.update_for_insertion(offset, inserted_len);
2578        }
2579
2580        // Ensure cursor/selection still within valid range
2581        self.normalize_cursor_and_selection();
2582
2583        let after_selection = self.snapshot_selection_set();
2584
2585        let step = UndoStep {
2586            group_id: 0,
2587            edits: vec![TextEdit {
2588                start_before: offset,
2589                start_after: offset,
2590                deleted_text: String::new(),
2591                inserted_text: text.clone(),
2592            }],
2593            before_selection,
2594            after_selection,
2595        };
2596
2597        let coalescible_insert = !text.contains('\n');
2598        let group_id = self.undo_redo.push_step(step, coalescible_insert);
2599
2600        self.last_text_delta = Some(TextDelta {
2601            before_char_count,
2602            after_char_count: self.editor.piece_table.char_count(),
2603            edits: vec![TextDeltaEdit {
2604                start: offset,
2605                deleted_text: String::new(),
2606                inserted_text: text,
2607            }],
2608            undo_group_id: Some(group_id),
2609        });
2610
2611        Ok(CommandResult::Success)
2612    }
2613
2614    fn execute_delete_command(
2615        &mut self,
2616        start: usize,
2617        length: usize,
2618    ) -> Result<CommandResult, CommandError> {
2619        if length == 0 {
2620            return Ok(CommandResult::Success);
2621        }
2622
2623        let before_char_count = self.editor.piece_table.char_count();
2624        let max_offset = self.editor.piece_table.char_count();
2625        if start > max_offset {
2626            return Err(CommandError::InvalidOffset(start));
2627        }
2628        if start + length > max_offset {
2629            return Err(CommandError::InvalidRange {
2630                start,
2631                end: start + length,
2632            });
2633        }
2634
2635        let before_selection = self.snapshot_selection_set();
2636
2637        let deleted_text = self.editor.piece_table.get_range(start, length);
2638        let delta_deleted_text = deleted_text.clone();
2639        let deletes_newline = deleted_text.contains('\n');
2640        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
2641
2642        // Execute deletion
2643        self.editor.piece_table.delete(start, length);
2644
2645        // Update line index
2646        let updated_text = self.editor.piece_table.get_text();
2647        self.editor.line_index = LineIndex::from_text(&updated_text);
2648
2649        // Update layout engine (soft wrappingneeds to stay consistent with text)
2650        if deletes_newline {
2651            self.rebuild_layout_engine_from_text(&updated_text);
2652        } else {
2653            let line_text = self
2654                .editor
2655                .line_index
2656                .get_line_text(affected_line)
2657                .unwrap_or_default();
2658            self.editor
2659                .layout_engine
2660                .update_line(affected_line, &line_text);
2661        }
2662
2663        // Update interval tree offsets
2664        self.editor
2665            .interval_tree
2666            .update_for_deletion(start, start + length);
2667        for layer_tree in self.editor.style_layers.values_mut() {
2668            layer_tree.update_for_deletion(start, start + length);
2669        }
2670
2671        // Ensure cursor/selection still within valid range
2672        self.normalize_cursor_and_selection();
2673
2674        let after_selection = self.snapshot_selection_set();
2675
2676        let step = UndoStep {
2677            group_id: 0,
2678            edits: vec![TextEdit {
2679                start_before: start,
2680                start_after: start,
2681                deleted_text,
2682                inserted_text: String::new(),
2683            }],
2684            before_selection,
2685            after_selection,
2686        };
2687        let group_id = self.undo_redo.push_step(step, false);
2688
2689        self.last_text_delta = Some(TextDelta {
2690            before_char_count,
2691            after_char_count: self.editor.piece_table.char_count(),
2692            edits: vec![TextDeltaEdit {
2693                start,
2694                deleted_text: delta_deleted_text,
2695                inserted_text: String::new(),
2696            }],
2697            undo_group_id: Some(group_id),
2698        });
2699
2700        Ok(CommandResult::Success)
2701    }
2702
2703    fn execute_replace_command(
2704        &mut self,
2705        start: usize,
2706        length: usize,
2707        text: String,
2708    ) -> Result<CommandResult, CommandError> {
2709        let before_char_count = self.editor.piece_table.char_count();
2710        let max_offset = self.editor.piece_table.char_count();
2711        if start > max_offset {
2712            return Err(CommandError::InvalidOffset(start));
2713        }
2714        if start + length > max_offset {
2715            return Err(CommandError::InvalidRange {
2716                start,
2717                end: start + length,
2718            });
2719        }
2720
2721        if length == 0 && text.is_empty() {
2722            return Ok(CommandResult::Success);
2723        }
2724
2725        let text = crate::text::normalize_crlf_to_lf_string(text);
2726        let before_selection = self.snapshot_selection_set();
2727
2728        let deleted_text = if length == 0 {
2729            String::new()
2730        } else {
2731            self.editor.piece_table.get_range(start, length)
2732        };
2733        let delta_deleted_text = deleted_text.clone();
2734        let delta_inserted_text = text.clone();
2735
2736        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
2737        let replace_affects_layout = deleted_text.contains('\n') || text.contains('\n');
2738
2739        // Apply as a single operation (delete then insert at the same offset).
2740        if length > 0 {
2741            self.editor.piece_table.delete(start, length);
2742            self.editor
2743                .interval_tree
2744                .update_for_deletion(start, start + length);
2745            for layer_tree in self.editor.style_layers.values_mut() {
2746                layer_tree.update_for_deletion(start, start + length);
2747            }
2748        }
2749
2750        let inserted_len = text.chars().count();
2751        if inserted_len > 0 {
2752            self.editor.piece_table.insert(start, &text);
2753            self.editor
2754                .interval_tree
2755                .update_for_insertion(start, inserted_len);
2756            for layer_tree in self.editor.style_layers.values_mut() {
2757                layer_tree.update_for_insertion(start, inserted_len);
2758            }
2759        }
2760
2761        // Rebuild derived structures.
2762        let updated_text = self.editor.piece_table.get_text();
2763        self.editor.line_index = LineIndex::from_text(&updated_text);
2764
2765        if replace_affects_layout {
2766            self.rebuild_layout_engine_from_text(&updated_text);
2767        } else {
2768            let line_text = self
2769                .editor
2770                .line_index
2771                .get_line_text(affected_line)
2772                .unwrap_or_default();
2773            self.editor
2774                .layout_engine
2775                .update_line(affected_line, &line_text);
2776        }
2777
2778        // Ensure cursor/selection still valid.
2779        self.normalize_cursor_and_selection();
2780
2781        let after_selection = self.snapshot_selection_set();
2782
2783        let step = UndoStep {
2784            group_id: 0,
2785            edits: vec![TextEdit {
2786                start_before: start,
2787                start_after: start,
2788                deleted_text,
2789                inserted_text: text,
2790            }],
2791            before_selection,
2792            after_selection,
2793        };
2794        let group_id = self.undo_redo.push_step(step, false);
2795
2796        self.last_text_delta = Some(TextDelta {
2797            before_char_count,
2798            after_char_count: self.editor.piece_table.char_count(),
2799            edits: vec![TextDeltaEdit {
2800                start,
2801                deleted_text: delta_deleted_text,
2802                inserted_text: delta_inserted_text,
2803            }],
2804            undo_group_id: Some(group_id),
2805        });
2806
2807        Ok(CommandResult::Success)
2808    }
2809
2810    fn cursor_char_offset(&self) -> usize {
2811        self.position_to_char_offset_clamped(self.editor.cursor_position)
2812    }
2813
2814    fn primary_selection_char_range(&self) -> Option<SearchMatch> {
2815        let selection = self.editor.selection.as_ref()?;
2816        let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
2817        let start = self.position_to_char_offset_clamped(min_pos);
2818        let end = self.position_to_char_offset_clamped(max_pos);
2819        if start == end {
2820            None
2821        } else {
2822            Some(SearchMatch { start, end })
2823        }
2824    }
2825
2826    fn set_primary_selection_by_char_range(&mut self, range: SearchMatch) {
2827        let (start_line, start_col) = self.editor.line_index.char_offset_to_position(range.start);
2828        let (end_line, end_col) = self.editor.line_index.char_offset_to_position(range.end);
2829
2830        self.editor.cursor_position = Position::new(end_line, end_col);
2831        self.editor.secondary_selections.clear();
2832
2833        if range.start == range.end {
2834            self.editor.selection = None;
2835        } else {
2836            self.editor.selection = Some(Selection {
2837                start: Position::new(start_line, start_col),
2838                end: Position::new(end_line, end_col),
2839                direction: SelectionDirection::Forward,
2840            });
2841        }
2842    }
2843
2844    fn execute_find_command(
2845        &mut self,
2846        query: String,
2847        options: SearchOptions,
2848        forward: bool,
2849    ) -> Result<CommandResult, CommandError> {
2850        if query.is_empty() {
2851            return Ok(CommandResult::SearchNotFound);
2852        }
2853
2854        let text = self.editor.piece_table.get_text();
2855        let from = if let Some(selection) = self.primary_selection_char_range() {
2856            if forward {
2857                selection.end
2858            } else {
2859                selection.start
2860            }
2861        } else {
2862            self.cursor_char_offset()
2863        };
2864
2865        let found = if forward {
2866            find_next(&text, &query, options, from)
2867        } else {
2868            find_prev(&text, &query, options, from)
2869        }
2870        .map_err(|err| CommandError::Other(err.to_string()))?;
2871
2872        let Some(m) = found else {
2873            return Ok(CommandResult::SearchNotFound);
2874        };
2875
2876        self.set_primary_selection_by_char_range(m);
2877
2878        Ok(CommandResult::SearchMatch {
2879            start: m.start,
2880            end: m.end,
2881        })
2882    }
2883
2884    fn compile_user_regex(
2885        query: &str,
2886        options: SearchOptions,
2887    ) -> Result<regex::Regex, CommandError> {
2888        RegexBuilder::new(query)
2889            .case_insensitive(!options.case_sensitive)
2890            .multi_line(true)
2891            .build()
2892            .map_err(|err| CommandError::Other(format!("Invalid regex: {}", err)))
2893    }
2894
2895    fn regex_expand_replacement(
2896        re: &regex::Regex,
2897        text: &str,
2898        index: &CharIndex,
2899        range: SearchMatch,
2900        replacement: &str,
2901    ) -> Result<String, CommandError> {
2902        let start_byte = index.char_to_byte(range.start);
2903        let end_byte = index.char_to_byte(range.end);
2904
2905        let caps = re
2906            .captures_at(text, start_byte)
2907            .ok_or_else(|| CommandError::Other("Regex match not found".to_string()))?;
2908        let whole = caps
2909            .get(0)
2910            .ok_or_else(|| CommandError::Other("Regex match missing capture 0".to_string()))?;
2911        if whole.start() != start_byte || whole.end() != end_byte {
2912            return Err(CommandError::Other(
2913                "Regex match did not align with the selected range".to_string(),
2914            ));
2915        }
2916
2917        let mut expanded = String::new();
2918        caps.expand(replacement, &mut expanded);
2919        Ok(expanded)
2920    }
2921
2922    fn execute_replace_current_command(
2923        &mut self,
2924        query: String,
2925        replacement: String,
2926        options: SearchOptions,
2927    ) -> Result<CommandResult, CommandError> {
2928        if query.is_empty() {
2929            return Err(CommandError::Other("Search query is empty".to_string()));
2930        }
2931
2932        let text = self.editor.piece_table.get_text();
2933        let selection_range = self.primary_selection_char_range();
2934
2935        let mut target = None::<SearchMatch>;
2936        if let Some(range) = selection_range {
2937            let is_match = crate::search::is_match_exact(&text, &query, options, range)
2938                .map_err(|err| CommandError::Other(err.to_string()))?;
2939            if is_match {
2940                target = Some(range);
2941            }
2942        }
2943
2944        if target.is_none() {
2945            let from = self.cursor_char_offset();
2946            target = find_next(&text, &query, options, from)
2947                .map_err(|err| CommandError::Other(err.to_string()))?;
2948        }
2949
2950        let Some(target) = target else {
2951            return Err(CommandError::Other("No match found".to_string()));
2952        };
2953
2954        let index = CharIndex::new(&text);
2955        let inserted_text = if options.regex {
2956            let re = Self::compile_user_regex(&query, options)?;
2957            Self::regex_expand_replacement(&re, &text, &index, target, &replacement)?
2958        } else {
2959            replacement
2960        };
2961        let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
2962
2963        let deleted_text = self
2964            .editor
2965            .piece_table
2966            .get_range(target.start, target.len());
2967        let before_char_count = self.editor.piece_table.char_count();
2968        let delta_deleted_text = deleted_text.clone();
2969
2970        let before_selection = self.snapshot_selection_set();
2971        self.apply_text_ops(vec![(target.start, target.len(), inserted_text.as_str())])?;
2972
2973        let inserted_len = inserted_text.chars().count();
2974        let new_range = SearchMatch {
2975            start: target.start,
2976            end: target.start + inserted_len,
2977        };
2978        self.set_primary_selection_by_char_range(new_range);
2979        let after_selection = self.snapshot_selection_set();
2980
2981        let step = UndoStep {
2982            group_id: 0,
2983            edits: vec![TextEdit {
2984                start_before: target.start,
2985                start_after: target.start,
2986                deleted_text,
2987                inserted_text: inserted_text.clone(),
2988            }],
2989            before_selection,
2990            after_selection,
2991        };
2992        let group_id = self.undo_redo.push_step(step, false);
2993
2994        self.last_text_delta = Some(TextDelta {
2995            before_char_count,
2996            after_char_count: self.editor.piece_table.char_count(),
2997            edits: vec![TextDeltaEdit {
2998                start: target.start,
2999                deleted_text: delta_deleted_text,
3000                inserted_text,
3001            }],
3002            undo_group_id: Some(group_id),
3003        });
3004
3005        Ok(CommandResult::ReplaceResult { replaced: 1 })
3006    }
3007
3008    fn execute_replace_all_command(
3009        &mut self,
3010        query: String,
3011        replacement: String,
3012        options: SearchOptions,
3013    ) -> Result<CommandResult, CommandError> {
3014        if query.is_empty() {
3015            return Err(CommandError::Other("Search query is empty".to_string()));
3016        }
3017
3018        let replacement = crate::text::normalize_crlf_to_lf_string(replacement);
3019        let text = self.editor.piece_table.get_text();
3020        let matches =
3021            find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
3022        if matches.is_empty() {
3023            return Err(CommandError::Other("No match found".to_string()));
3024        }
3025        let match_count = matches.len();
3026
3027        let index = CharIndex::new(&text);
3028
3029        struct Op {
3030            start_before: usize,
3031            start_after: usize,
3032            delete_len: usize,
3033            deleted_text: String,
3034            inserted_text: String,
3035            inserted_len: usize,
3036        }
3037
3038        let mut ops: Vec<Op> = Vec::with_capacity(match_count);
3039        if options.regex {
3040            let re = Self::compile_user_regex(&query, options)?;
3041            for m in matches {
3042                let deleted_text = {
3043                    let start_byte = index.char_to_byte(m.start);
3044                    let end_byte = index.char_to_byte(m.end);
3045                    text.get(start_byte..end_byte)
3046                        .unwrap_or_default()
3047                        .to_string()
3048                };
3049                let inserted_text =
3050                    Self::regex_expand_replacement(&re, &text, &index, m, &replacement)?;
3051                let inserted_text = crate::text::normalize_crlf_to_lf_string(inserted_text);
3052                let inserted_len = inserted_text.chars().count();
3053                ops.push(Op {
3054                    start_before: m.start,
3055                    start_after: m.start,
3056                    delete_len: m.len(),
3057                    deleted_text,
3058                    inserted_text,
3059                    inserted_len,
3060                });
3061            }
3062        } else {
3063            let inserted_len = replacement.chars().count();
3064            for m in matches {
3065                let deleted_text = {
3066                    let start_byte = index.char_to_byte(m.start);
3067                    let end_byte = index.char_to_byte(m.end);
3068                    text.get(start_byte..end_byte)
3069                        .unwrap_or_default()
3070                        .to_string()
3071                };
3072                ops.push(Op {
3073                    start_before: m.start,
3074                    start_after: m.start,
3075                    delete_len: m.len(),
3076                    deleted_text,
3077                    inserted_text: replacement.clone(),
3078                    inserted_len,
3079                });
3080            }
3081        }
3082
3083        ops.sort_by_key(|op| op.start_before);
3084
3085        let mut delta: i64 = 0;
3086        for op in &mut ops {
3087            let effective_start = op.start_before as i64 + delta;
3088            if effective_start < 0 {
3089                return Err(CommandError::Other(
3090                    "ReplaceAll produced an invalid intermediate offset".to_string(),
3091                ));
3092            }
3093            op.start_after = effective_start as usize;
3094            delta += op.inserted_len as i64 - op.delete_len as i64;
3095        }
3096
3097        let before_char_count = self.editor.piece_table.char_count();
3098        let before_selection = self.snapshot_selection_set();
3099        let apply_ops: Vec<(usize, usize, &str)> = ops
3100            .iter()
3101            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
3102            .collect();
3103        self.apply_text_ops(apply_ops)?;
3104
3105        if let Some(first) = ops.first() {
3106            let caret_end = first.start_after + first.inserted_len;
3107            let select_end = if first.inserted_len == 0 {
3108                first.start_after
3109            } else {
3110                caret_end
3111            };
3112            self.set_primary_selection_by_char_range(SearchMatch {
3113                start: first.start_after,
3114                end: select_end,
3115            });
3116        } else {
3117            self.editor.selection = None;
3118            self.editor.secondary_selections.clear();
3119        }
3120
3121        let after_selection = self.snapshot_selection_set();
3122
3123        let edits: Vec<TextEdit> = ops
3124            .into_iter()
3125            .map(|op| TextEdit {
3126                start_before: op.start_before,
3127                start_after: op.start_after,
3128                deleted_text: op.deleted_text,
3129                inserted_text: op.inserted_text,
3130            })
3131            .collect();
3132
3133        let mut delta_edits: Vec<TextDeltaEdit> = edits
3134            .iter()
3135            .map(|e| TextDeltaEdit {
3136                start: e.start_before,
3137                deleted_text: e.deleted_text.clone(),
3138                inserted_text: e.inserted_text.clone(),
3139            })
3140            .collect();
3141        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3142
3143        let step = UndoStep {
3144            group_id: 0,
3145            edits,
3146            before_selection,
3147            after_selection,
3148        };
3149        let group_id = self.undo_redo.push_step(step, false);
3150
3151        self.last_text_delta = Some(TextDelta {
3152            before_char_count,
3153            after_char_count: self.editor.piece_table.char_count(),
3154            edits: delta_edits,
3155            undo_group_id: Some(group_id),
3156        });
3157
3158        Ok(CommandResult::ReplaceResult {
3159            replaced: match_count,
3160        })
3161    }
3162
3163    fn execute_backspace_command(&mut self) -> Result<CommandResult, CommandError> {
3164        self.execute_delete_like_command(false)
3165    }
3166
3167    fn execute_delete_forward_command(&mut self) -> Result<CommandResult, CommandError> {
3168        self.execute_delete_like_command(true)
3169    }
3170
3171    fn execute_delete_to_prev_tab_stop_command(&mut self) -> Result<CommandResult, CommandError> {
3172        // Treat like a delete-like action: end any open insert coalescing group, even if it turns out
3173        // to be a no-op.
3174        self.undo_redo.end_group();
3175
3176        let before_selection = self.snapshot_selection_set();
3177        let selections = before_selection.selections.clone();
3178        let primary_index = before_selection.primary_index;
3179
3180        let tab_width = self.editor.layout_engine.tab_width().max(1);
3181
3182        #[derive(Debug)]
3183        struct Op {
3184            selection_index: usize,
3185            start_offset: usize,
3186            delete_len: usize,
3187            deleted_text: String,
3188            start_after: usize,
3189        }
3190
3191        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
3192
3193        for (selection_index, selection) in selections.iter().enumerate() {
3194            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
3195                (selection.start, selection.end)
3196            } else {
3197                (selection.end, selection.start)
3198            };
3199
3200            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
3201                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
3202                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
3203                if start_offset <= end_offset {
3204                    (start_offset, end_offset)
3205                } else {
3206                    (end_offset, start_offset)
3207                }
3208            } else {
3209                let caret = selection.end;
3210                let caret_offset = self.position_to_char_offset_clamped(caret);
3211                if caret_offset == 0 {
3212                    (0, 0)
3213                } else {
3214                    let line_text = self
3215                        .editor
3216                        .line_index
3217                        .get_line_text(caret.line)
3218                        .unwrap_or_default();
3219                    let line_char_len = line_text.chars().count();
3220                    let col = caret.column.min(line_char_len);
3221
3222                    let in_leading_whitespace = line_text
3223                        .chars()
3224                        .take(col)
3225                        .all(|ch| ch == ' ' || ch == '\t');
3226
3227                    if !in_leading_whitespace {
3228                        (caret_offset - 1, caret_offset)
3229                    } else {
3230                        let x_in_line = visual_x_for_column(&line_text, col, tab_width);
3231                        let back = if x_in_line == 0 {
3232                            0
3233                        } else {
3234                            let rem = x_in_line % tab_width;
3235                            if rem == 0 { tab_width } else { rem }
3236                        };
3237                        let target_x = x_in_line.saturating_sub(back);
3238
3239                        let mut target_col = col;
3240                        while target_col > 0 {
3241                            let prev_col = target_col - 1;
3242                            let prev_x = visual_x_for_column(&line_text, prev_col, tab_width);
3243                            if prev_x < target_x {
3244                                break;
3245                            }
3246                            target_col = prev_col;
3247                            if prev_x == target_x {
3248                                break;
3249                            }
3250                        }
3251
3252                        let target_offset = self
3253                            .editor
3254                            .line_index
3255                            .position_to_char_offset(caret.line, target_col);
3256                        (target_offset, caret_offset)
3257                    }
3258                }
3259            };
3260
3261            let delete_len = end_offset.saturating_sub(start_offset);
3262            let deleted_text = if delete_len == 0 {
3263                String::new()
3264            } else {
3265                self.editor.piece_table.get_range(start_offset, delete_len)
3266            };
3267
3268            ops.push(Op {
3269                selection_index,
3270                start_offset,
3271                delete_len,
3272                deleted_text,
3273                start_after: start_offset,
3274            });
3275        }
3276
3277        if !ops.iter().any(|op| op.delete_len > 0) {
3278            return Ok(CommandResult::Success);
3279        }
3280
3281        let before_char_count = self.editor.piece_table.char_count();
3282
3283        // Compute caret offsets in the post-delete document (ascending order with delta).
3284        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3285        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
3286
3287        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
3288        let mut delta: i64 = 0;
3289        for &idx in &asc_indices {
3290            let op = &mut ops[idx];
3291            let effective_start = (op.start_offset as i64 + delta) as usize;
3292            op.start_after = effective_start;
3293            caret_offsets[op.selection_index] = effective_start;
3294            delta -= op.delete_len as i64;
3295        }
3296
3297        // Apply deletes descending to keep offsets valid.
3298        let mut desc_indices = asc_indices;
3299        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
3300
3301        for &idx in &desc_indices {
3302            let op = &ops[idx];
3303            if op.delete_len == 0 {
3304                continue;
3305            }
3306
3307            self.editor
3308                .piece_table
3309                .delete(op.start_offset, op.delete_len);
3310            self.editor
3311                .interval_tree
3312                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3313            for layer_tree in self.editor.style_layers.values_mut() {
3314                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3315            }
3316        }
3317
3318        // Rebuild derived structures once.
3319        let updated_text = self.editor.piece_table.get_text();
3320        self.editor.line_index = LineIndex::from_text(&updated_text);
3321        self.rebuild_layout_engine_from_text(&updated_text);
3322
3323        // Collapse selection state to carets at the start of deleted ranges.
3324        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
3325        for offset in &caret_offsets {
3326            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
3327            let pos = Position::new(line, column);
3328            new_carets.push(Selection {
3329                start: pos,
3330                end: pos,
3331                direction: SelectionDirection::Forward,
3332            });
3333        }
3334
3335        let (new_carets, new_primary_index) =
3336            crate::selection_set::normalize_selections(new_carets, primary_index);
3337        let primary = new_carets
3338            .get(new_primary_index)
3339            .cloned()
3340            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
3341
3342        self.editor.cursor_position = primary.end;
3343        self.editor.selection = None;
3344        self.editor.secondary_selections = new_carets
3345            .into_iter()
3346            .enumerate()
3347            .filter_map(|(idx, sel)| {
3348                if idx == new_primary_index {
3349                    None
3350                } else {
3351                    Some(sel)
3352                }
3353            })
3354            .collect();
3355
3356        let after_selection = self.snapshot_selection_set();
3357
3358        let edits: Vec<TextEdit> = ops
3359            .into_iter()
3360            .map(|op| TextEdit {
3361                start_before: op.start_offset,
3362                start_after: op.start_after,
3363                deleted_text: op.deleted_text,
3364                inserted_text: String::new(),
3365            })
3366            .collect();
3367
3368        let mut delta_edits: Vec<TextDeltaEdit> = edits
3369            .iter()
3370            .map(|e| TextDeltaEdit {
3371                start: e.start_before,
3372                deleted_text: e.deleted_text.clone(),
3373                inserted_text: e.inserted_text.clone(),
3374            })
3375            .collect();
3376        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3377
3378        let step = UndoStep {
3379            group_id: 0,
3380            edits,
3381            before_selection,
3382            after_selection,
3383        };
3384        let group_id = self.undo_redo.push_step(step, false);
3385
3386        self.last_text_delta = Some(TextDelta {
3387            before_char_count,
3388            after_char_count: self.editor.piece_table.char_count(),
3389            edits: delta_edits,
3390            undo_group_id: Some(group_id),
3391        });
3392
3393        Ok(CommandResult::Success)
3394    }
3395
3396    fn execute_delete_by_boundary_command(
3397        &mut self,
3398        forward: bool,
3399        boundary: TextBoundary,
3400    ) -> Result<CommandResult, CommandError> {
3401        // Any delete-like action should end an open insert coalescing group, even if it turns out
3402        // to be a no-op.
3403        self.undo_redo.end_group();
3404
3405        let before_selection = self.snapshot_selection_set();
3406        let selections = before_selection.selections.clone();
3407        let primary_index = before_selection.primary_index;
3408
3409        let doc_char_count = self.editor.piece_table.char_count();
3410
3411        #[derive(Debug)]
3412        struct Op {
3413            selection_index: usize,
3414            start_offset: usize,
3415            delete_len: usize,
3416            deleted_text: String,
3417            start_after: usize,
3418        }
3419
3420        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
3421
3422        for (selection_index, selection) in selections.iter().enumerate() {
3423            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
3424                (selection.start, selection.end)
3425            } else {
3426                (selection.end, selection.start)
3427            };
3428
3429            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
3430                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
3431                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
3432                if start_offset <= end_offset {
3433                    (start_offset, end_offset)
3434                } else {
3435                    (end_offset, start_offset)
3436                }
3437            } else {
3438                let caret = selection.end;
3439                let caret_offset = self.position_to_char_offset_clamped(caret);
3440                let line_count = self.editor.line_index.line_count();
3441                let line = caret.line.min(line_count.saturating_sub(1));
3442                let line_text = self
3443                    .editor
3444                    .line_index
3445                    .get_line_text(line)
3446                    .unwrap_or_default();
3447                let line_char_len = line_text.chars().count();
3448                let col = caret.column.min(line_char_len);
3449
3450                if forward {
3451                    if caret_offset >= doc_char_count {
3452                        (caret_offset, caret_offset)
3453                    } else if col >= line_char_len {
3454                        (caret_offset, (caret_offset + 1).min(doc_char_count))
3455                    } else {
3456                        let next_col = next_boundary_column(&line_text, col, boundary);
3457                        let start_offset =
3458                            self.editor.line_index.position_to_char_offset(line, col);
3459                        let end_offset = self
3460                            .editor
3461                            .line_index
3462                            .position_to_char_offset(line, next_col);
3463                        (start_offset, end_offset)
3464                    }
3465                } else if caret_offset == 0 {
3466                    (0, 0)
3467                } else if col == 0 {
3468                    (caret_offset - 1, caret_offset)
3469                } else {
3470                    let prev_col = prev_boundary_column(&line_text, col, boundary);
3471                    let start_offset = self
3472                        .editor
3473                        .line_index
3474                        .position_to_char_offset(line, prev_col);
3475                    let end_offset = self.editor.line_index.position_to_char_offset(line, col);
3476                    (start_offset, end_offset)
3477                }
3478            };
3479
3480            let delete_len = end_offset.saturating_sub(start_offset);
3481            let deleted_text = if delete_len == 0 {
3482                String::new()
3483            } else {
3484                self.editor.piece_table.get_range(start_offset, delete_len)
3485            };
3486
3487            ops.push(Op {
3488                selection_index,
3489                start_offset,
3490                delete_len,
3491                deleted_text,
3492                start_after: start_offset,
3493            });
3494        }
3495
3496        if !ops.iter().any(|op| op.delete_len > 0) {
3497            return Ok(CommandResult::Success);
3498        }
3499
3500        let before_char_count = self.editor.piece_table.char_count();
3501
3502        // Compute caret offsets in the post-delete document (ascending order with delta).
3503        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3504        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
3505
3506        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
3507        let mut delta: i64 = 0;
3508        for &idx in &asc_indices {
3509            let op = &mut ops[idx];
3510            let effective_start = (op.start_offset as i64 + delta) as usize;
3511            op.start_after = effective_start;
3512            caret_offsets[op.selection_index] = effective_start;
3513            delta -= op.delete_len as i64;
3514        }
3515
3516        // Apply deletes descending to keep offsets valid.
3517        let mut desc_indices = asc_indices;
3518        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
3519
3520        for &idx in &desc_indices {
3521            let op = &ops[idx];
3522            if op.delete_len == 0 {
3523                continue;
3524            }
3525
3526            self.editor
3527                .piece_table
3528                .delete(op.start_offset, op.delete_len);
3529            self.editor
3530                .interval_tree
3531                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3532            for layer_tree in self.editor.style_layers.values_mut() {
3533                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3534            }
3535        }
3536
3537        // Rebuild derived structures once.
3538        let updated_text = self.editor.piece_table.get_text();
3539        self.editor.line_index = LineIndex::from_text(&updated_text);
3540        self.rebuild_layout_engine_from_text(&updated_text);
3541
3542        // Collapse selection state to carets at the start of deleted ranges.
3543        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
3544        for offset in &caret_offsets {
3545            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
3546            let pos = Position::new(line, column);
3547            new_carets.push(Selection {
3548                start: pos,
3549                end: pos,
3550                direction: SelectionDirection::Forward,
3551            });
3552        }
3553
3554        let (new_carets, new_primary_index) =
3555            crate::selection_set::normalize_selections(new_carets, primary_index);
3556        let primary = new_carets
3557            .get(new_primary_index)
3558            .cloned()
3559            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
3560
3561        self.editor.cursor_position = primary.end;
3562        self.editor.selection = None;
3563        self.editor.secondary_selections = new_carets
3564            .into_iter()
3565            .enumerate()
3566            .filter_map(|(idx, sel)| {
3567                if idx == new_primary_index {
3568                    None
3569                } else {
3570                    Some(sel)
3571                }
3572            })
3573            .collect();
3574
3575        let after_selection = self.snapshot_selection_set();
3576
3577        let edits: Vec<TextEdit> = ops
3578            .into_iter()
3579            .map(|op| TextEdit {
3580                start_before: op.start_offset,
3581                start_after: op.start_after,
3582                deleted_text: op.deleted_text,
3583                inserted_text: String::new(),
3584            })
3585            .collect();
3586
3587        let mut delta_edits: Vec<TextDeltaEdit> = edits
3588            .iter()
3589            .map(|e| TextDeltaEdit {
3590                start: e.start_before,
3591                deleted_text: e.deleted_text.clone(),
3592                inserted_text: e.inserted_text.clone(),
3593            })
3594            .collect();
3595        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3596
3597        let step = UndoStep {
3598            group_id: 0,
3599            edits,
3600            before_selection,
3601            after_selection,
3602        };
3603        let group_id = self.undo_redo.push_step(step, false);
3604
3605        self.last_text_delta = Some(TextDelta {
3606            before_char_count,
3607            after_char_count: self.editor.piece_table.char_count(),
3608            edits: delta_edits,
3609            undo_group_id: Some(group_id),
3610        });
3611
3612        Ok(CommandResult::Success)
3613    }
3614
3615    fn execute_delete_like_command(
3616        &mut self,
3617        forward: bool,
3618    ) -> Result<CommandResult, CommandError> {
3619        // Any delete-like action should end an open insert coalescing group, even if it turns out
3620        // to be a no-op (e.g. backspace at the beginning of the document).
3621        self.undo_redo.end_group();
3622
3623        let before_selection = self.snapshot_selection_set();
3624        let selections = before_selection.selections.clone();
3625        let primary_index = before_selection.primary_index;
3626
3627        let doc_char_count = self.editor.piece_table.char_count();
3628
3629        #[derive(Debug)]
3630        struct Op {
3631            selection_index: usize,
3632            start_offset: usize,
3633            delete_len: usize,
3634            deleted_text: String,
3635            start_after: usize,
3636        }
3637
3638        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
3639
3640        for (selection_index, selection) in selections.iter().enumerate() {
3641            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
3642                (selection.start, selection.end)
3643            } else {
3644                (selection.end, selection.start)
3645            };
3646
3647            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
3648                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
3649                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
3650                if start_offset <= end_offset {
3651                    (start_offset, end_offset)
3652                } else {
3653                    (end_offset, start_offset)
3654                }
3655            } else {
3656                let caret_offset = self.position_to_char_offset_clamped(selection.end);
3657                if forward {
3658                    if caret_offset >= doc_char_count {
3659                        (caret_offset, caret_offset)
3660                    } else {
3661                        (caret_offset, (caret_offset + 1).min(doc_char_count))
3662                    }
3663                } else if caret_offset == 0 {
3664                    (0, 0)
3665                } else {
3666                    (caret_offset - 1, caret_offset)
3667                }
3668            };
3669
3670            let delete_len = end_offset.saturating_sub(start_offset);
3671            let deleted_text = if delete_len == 0 {
3672                String::new()
3673            } else {
3674                self.editor.piece_table.get_range(start_offset, delete_len)
3675            };
3676
3677            ops.push(Op {
3678                selection_index,
3679                start_offset,
3680                delete_len,
3681                deleted_text,
3682                start_after: start_offset,
3683            });
3684        }
3685
3686        if !ops.iter().any(|op| op.delete_len > 0) {
3687            return Ok(CommandResult::Success);
3688        }
3689
3690        let before_char_count = self.editor.piece_table.char_count();
3691
3692        // Compute caret offsets in the post-delete document (ascending order with delta).
3693        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
3694        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
3695
3696        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
3697        let mut delta: i64 = 0;
3698        for &idx in &asc_indices {
3699            let op = &mut ops[idx];
3700            let effective_start = (op.start_offset as i64 + delta) as usize;
3701            op.start_after = effective_start;
3702            caret_offsets[op.selection_index] = effective_start;
3703            delta -= op.delete_len as i64;
3704        }
3705
3706        // Apply deletes descending to keep offsets valid.
3707        let mut desc_indices = asc_indices;
3708        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
3709
3710        for &idx in &desc_indices {
3711            let op = &ops[idx];
3712            if op.delete_len == 0 {
3713                continue;
3714            }
3715
3716            self.editor
3717                .piece_table
3718                .delete(op.start_offset, op.delete_len);
3719            self.editor
3720                .interval_tree
3721                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3722            for layer_tree in self.editor.style_layers.values_mut() {
3723                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
3724            }
3725        }
3726
3727        // Rebuild derived structures once.
3728        let updated_text = self.editor.piece_table.get_text();
3729        self.editor.line_index = LineIndex::from_text(&updated_text);
3730        self.rebuild_layout_engine_from_text(&updated_text);
3731
3732        // Collapse selection state to carets at the start of deleted ranges.
3733        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
3734        for offset in &caret_offsets {
3735            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
3736            let pos = Position::new(line, column);
3737            new_carets.push(Selection {
3738                start: pos,
3739                end: pos,
3740                direction: SelectionDirection::Forward,
3741            });
3742        }
3743
3744        let (new_carets, new_primary_index) =
3745            crate::selection_set::normalize_selections(new_carets, primary_index);
3746        let primary = new_carets
3747            .get(new_primary_index)
3748            .cloned()
3749            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
3750
3751        self.editor.cursor_position = primary.end;
3752        self.editor.selection = None;
3753        self.editor.secondary_selections = new_carets
3754            .into_iter()
3755            .enumerate()
3756            .filter_map(|(idx, sel)| {
3757                if idx == new_primary_index {
3758                    None
3759                } else {
3760                    Some(sel)
3761                }
3762            })
3763            .collect();
3764
3765        let after_selection = self.snapshot_selection_set();
3766
3767        let edits: Vec<TextEdit> = ops
3768            .into_iter()
3769            .map(|op| TextEdit {
3770                start_before: op.start_offset,
3771                start_after: op.start_after,
3772                deleted_text: op.deleted_text,
3773                inserted_text: String::new(),
3774            })
3775            .collect();
3776
3777        let mut delta_edits: Vec<TextDeltaEdit> = edits
3778            .iter()
3779            .map(|e| TextDeltaEdit {
3780                start: e.start_before,
3781                deleted_text: e.deleted_text.clone(),
3782                inserted_text: String::new(),
3783            })
3784            .collect();
3785        delta_edits.sort_by_key(|e| std::cmp::Reverse(e.start));
3786
3787        let step = UndoStep {
3788            group_id: 0,
3789            edits,
3790            before_selection,
3791            after_selection,
3792        };
3793        let group_id = self.undo_redo.push_step(step, false);
3794
3795        self.last_text_delta = Some(TextDelta {
3796            before_char_count,
3797            after_char_count: self.editor.piece_table.char_count(),
3798            edits: delta_edits,
3799            undo_group_id: Some(group_id),
3800        });
3801
3802        Ok(CommandResult::Success)
3803    }
3804
3805    fn snapshot_selection_set(&self) -> SelectionSetSnapshot {
3806        let mut selections: Vec<Selection> =
3807            Vec::with_capacity(1 + self.editor.secondary_selections.len());
3808
3809        let primary = self.editor.selection.clone().unwrap_or(Selection {
3810            start: self.editor.cursor_position,
3811            end: self.editor.cursor_position,
3812            direction: SelectionDirection::Forward,
3813        });
3814        selections.push(primary);
3815        selections.extend(self.editor.secondary_selections.iter().cloned());
3816
3817        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
3818        SelectionSetSnapshot {
3819            selections,
3820            primary_index,
3821        }
3822    }
3823
3824    fn restore_selection_set(&mut self, snapshot: SelectionSetSnapshot) {
3825        if snapshot.selections.is_empty() {
3826            self.editor.cursor_position = Position::new(0, 0);
3827            self.editor.selection = None;
3828            self.editor.secondary_selections.clear();
3829            return;
3830        }
3831
3832        let primary = snapshot
3833            .selections
3834            .get(snapshot.primary_index)
3835            .cloned()
3836            .unwrap_or_else(|| snapshot.selections[0].clone());
3837
3838        self.editor.cursor_position = primary.end;
3839        self.editor.selection = if primary.start == primary.end {
3840            None
3841        } else {
3842            Some(primary.clone())
3843        };
3844
3845        self.editor.secondary_selections = snapshot
3846            .selections
3847            .into_iter()
3848            .enumerate()
3849            .filter_map(|(idx, sel)| {
3850                if idx == snapshot.primary_index {
3851                    None
3852                } else {
3853                    Some(sel)
3854                }
3855            })
3856            .collect();
3857
3858        self.normalize_cursor_and_selection();
3859    }
3860
3861    fn apply_undo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
3862        // Apply inverse: delete inserted text, then reinsert deleted text.
3863        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
3864        for edit in edits {
3865            let start = edit.start_after;
3866            let delete_len = edit.inserted_len();
3867            let insert_text = edit.deleted_text.as_str();
3868            ops.push((start, delete_len, insert_text));
3869        }
3870        self.apply_text_ops(ops)
3871    }
3872
3873    fn apply_redo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
3874        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
3875        for edit in edits {
3876            let start = edit.start_before;
3877            let delete_len = edit.deleted_len();
3878            let insert_text = edit.inserted_text.as_str();
3879            ops.push((start, delete_len, insert_text));
3880        }
3881        self.apply_text_ops(ops)
3882    }
3883
3884    fn apply_text_ops(&mut self, mut ops: Vec<(usize, usize, &str)>) -> Result<(), CommandError> {
3885        // Sort descending by start offset to make offsets stable while mutating.
3886        ops.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
3887
3888        for (start, delete_len, insert_text) in ops {
3889            let max_offset = self.editor.piece_table.char_count();
3890            if start > max_offset {
3891                return Err(CommandError::InvalidOffset(start));
3892            }
3893            if start + delete_len > max_offset {
3894                return Err(CommandError::InvalidRange {
3895                    start,
3896                    end: start + delete_len,
3897                });
3898            }
3899
3900            if delete_len > 0 {
3901                self.editor.piece_table.delete(start, delete_len);
3902                self.editor
3903                    .interval_tree
3904                    .update_for_deletion(start, start + delete_len);
3905                for layer_tree in self.editor.style_layers.values_mut() {
3906                    layer_tree.update_for_deletion(start, start + delete_len);
3907                }
3908            }
3909
3910            let insert_len = insert_text.chars().count();
3911            if insert_len > 0 {
3912                self.editor.piece_table.insert(start, insert_text);
3913                self.editor
3914                    .interval_tree
3915                    .update_for_insertion(start, insert_len);
3916                for layer_tree in self.editor.style_layers.values_mut() {
3917                    layer_tree.update_for_insertion(start, insert_len);
3918                }
3919            }
3920        }
3921
3922        // Rebuild derived structures.
3923        let updated_text = self.editor.piece_table.get_text();
3924        self.editor.line_index = LineIndex::from_text(&updated_text);
3925        self.rebuild_layout_engine_from_text(&updated_text);
3926        self.normalize_cursor_and_selection();
3927
3928        Ok(())
3929    }
3930
3931    // Private method: execute cursor command
3932    fn execute_cursor(&mut self, command: CursorCommand) -> Result<CommandResult, CommandError> {
3933        match command {
3934            CursorCommand::MoveTo { line, column } => {
3935                if line >= self.editor.line_index.line_count() {
3936                    return Err(CommandError::InvalidPosition { line, column });
3937                }
3938
3939                let clamped_column = self.clamp_column_for_line(line, column);
3940                self.editor.cursor_position = Position::new(line, clamped_column);
3941                self.preferred_x_cells = self
3942                    .editor
3943                    .logical_position_to_visual(line, clamped_column)
3944                    .map(|(_, x)| x);
3945                // VSCode-like: moving the primary caret to an absolute position collapses multi-cursor.
3946                self.editor.secondary_selections.clear();
3947                Ok(CommandResult::Success)
3948            }
3949            CursorCommand::MoveBy {
3950                delta_line,
3951                delta_column,
3952            } => {
3953                let new_line = if delta_line >= 0 {
3954                    self.editor.cursor_position.line + delta_line as usize
3955                } else {
3956                    self.editor
3957                        .cursor_position
3958                        .line
3959                        .saturating_sub((-delta_line) as usize)
3960                };
3961
3962                let new_column = if delta_column >= 0 {
3963                    self.editor.cursor_position.column + delta_column as usize
3964                } else {
3965                    self.editor
3966                        .cursor_position
3967                        .column
3968                        .saturating_sub((-delta_column) as usize)
3969                };
3970
3971                if new_line >= self.editor.line_index.line_count() {
3972                    return Err(CommandError::InvalidPosition {
3973                        line: new_line,
3974                        column: new_column,
3975                    });
3976                }
3977
3978                let clamped_column = self.clamp_column_for_line(new_line, new_column);
3979                self.editor.cursor_position = Position::new(new_line, clamped_column);
3980                self.preferred_x_cells = self
3981                    .editor
3982                    .logical_position_to_visual(new_line, clamped_column)
3983                    .map(|(_, x)| x);
3984                Ok(CommandResult::Success)
3985            }
3986            CursorCommand::MoveGraphemeLeft => {
3987                let line_count = self.editor.line_index.line_count();
3988                if line_count == 0 {
3989                    return Ok(CommandResult::Success);
3990                }
3991
3992                let mut line = self
3993                    .editor
3994                    .cursor_position
3995                    .line
3996                    .min(line_count.saturating_sub(1));
3997                let mut line_text = self
3998                    .editor
3999                    .line_index
4000                    .get_line_text(line)
4001                    .unwrap_or_default();
4002                let mut line_char_len = line_text.chars().count();
4003                let mut col = self.editor.cursor_position.column.min(line_char_len);
4004
4005                if col == 0 {
4006                    if line == 0 {
4007                        return Ok(CommandResult::Success);
4008                    }
4009                    line = line.saturating_sub(1);
4010                    line_text = self
4011                        .editor
4012                        .line_index
4013                        .get_line_text(line)
4014                        .unwrap_or_default();
4015                    line_char_len = line_text.chars().count();
4016                    col = line_char_len;
4017                } else {
4018                    col = prev_boundary_column(&line_text, col, TextBoundary::Grapheme);
4019                }
4020
4021                self.editor.cursor_position = Position::new(line, col);
4022                self.preferred_x_cells = self
4023                    .editor
4024                    .logical_position_to_visual(line, col)
4025                    .map(|(_, x)| x);
4026                Ok(CommandResult::Success)
4027            }
4028            CursorCommand::MoveGraphemeRight => {
4029                let line_count = self.editor.line_index.line_count();
4030                if line_count == 0 {
4031                    return Ok(CommandResult::Success);
4032                }
4033
4034                let line = self
4035                    .editor
4036                    .cursor_position
4037                    .line
4038                    .min(line_count.saturating_sub(1));
4039                let line_text = self
4040                    .editor
4041                    .line_index
4042                    .get_line_text(line)
4043                    .unwrap_or_default();
4044                let line_char_len = line_text.chars().count();
4045                let col = self.editor.cursor_position.column.min(line_char_len);
4046
4047                let (line, col) = if col >= line_char_len {
4048                    if line + 1 >= line_count {
4049                        return Ok(CommandResult::Success);
4050                    }
4051                    (line + 1, 0)
4052                } else {
4053                    (
4054                        line,
4055                        next_boundary_column(&line_text, col, TextBoundary::Grapheme),
4056                    )
4057                };
4058
4059                self.editor.cursor_position = Position::new(line, col);
4060                self.preferred_x_cells = self
4061                    .editor
4062                    .logical_position_to_visual(line, col)
4063                    .map(|(_, x)| x);
4064                Ok(CommandResult::Success)
4065            }
4066            CursorCommand::MoveWordLeft => {
4067                let line_count = self.editor.line_index.line_count();
4068                if line_count == 0 {
4069                    return Ok(CommandResult::Success);
4070                }
4071
4072                let mut line = self
4073                    .editor
4074                    .cursor_position
4075                    .line
4076                    .min(line_count.saturating_sub(1));
4077                let mut line_text = self
4078                    .editor
4079                    .line_index
4080                    .get_line_text(line)
4081                    .unwrap_or_default();
4082                let mut line_char_len = line_text.chars().count();
4083                let mut col = self.editor.cursor_position.column.min(line_char_len);
4084
4085                if col == 0 {
4086                    if line == 0 {
4087                        return Ok(CommandResult::Success);
4088                    }
4089                    line = line.saturating_sub(1);
4090                    line_text = self
4091                        .editor
4092                        .line_index
4093                        .get_line_text(line)
4094                        .unwrap_or_default();
4095                    line_char_len = line_text.chars().count();
4096                    col = line_char_len;
4097                } else {
4098                    col = prev_boundary_column(&line_text, col, TextBoundary::Word);
4099                }
4100
4101                self.editor.cursor_position = Position::new(line, col);
4102                self.preferred_x_cells = self
4103                    .editor
4104                    .logical_position_to_visual(line, col)
4105                    .map(|(_, x)| x);
4106                Ok(CommandResult::Success)
4107            }
4108            CursorCommand::MoveWordRight => {
4109                let line_count = self.editor.line_index.line_count();
4110                if line_count == 0 {
4111                    return Ok(CommandResult::Success);
4112                }
4113
4114                let line = self
4115                    .editor
4116                    .cursor_position
4117                    .line
4118                    .min(line_count.saturating_sub(1));
4119                let line_text = self
4120                    .editor
4121                    .line_index
4122                    .get_line_text(line)
4123                    .unwrap_or_default();
4124                let line_char_len = line_text.chars().count();
4125                let col = self.editor.cursor_position.column.min(line_char_len);
4126
4127                let (line, col) = if col >= line_char_len {
4128                    if line + 1 >= line_count {
4129                        return Ok(CommandResult::Success);
4130                    }
4131                    (line + 1, 0)
4132                } else {
4133                    (
4134                        line,
4135                        next_boundary_column(&line_text, col, TextBoundary::Word),
4136                    )
4137                };
4138
4139                self.editor.cursor_position = Position::new(line, col);
4140                self.preferred_x_cells = self
4141                    .editor
4142                    .logical_position_to_visual(line, col)
4143                    .map(|(_, x)| x);
4144                Ok(CommandResult::Success)
4145            }
4146            CursorCommand::MoveVisualBy { delta_rows } => {
4147                let Some((current_row, current_x)) = self.editor.logical_position_to_visual(
4148                    self.editor.cursor_position.line,
4149                    self.editor.cursor_position.column,
4150                ) else {
4151                    return Ok(CommandResult::Success);
4152                };
4153
4154                let preferred_x = self.preferred_x_cells.unwrap_or(current_x);
4155                self.preferred_x_cells = Some(preferred_x);
4156
4157                let total_visual = self.editor.visual_line_count();
4158                if total_visual == 0 {
4159                    return Ok(CommandResult::Success);
4160                }
4161
4162                let target_row = if delta_rows >= 0 {
4163                    current_row.saturating_add(delta_rows as usize)
4164                } else {
4165                    current_row.saturating_sub((-delta_rows) as usize)
4166                }
4167                .min(total_visual.saturating_sub(1));
4168
4169                let Some(pos) = self
4170                    .editor
4171                    .visual_position_to_logical(target_row, preferred_x)
4172                else {
4173                    return Ok(CommandResult::Success);
4174                };
4175
4176                self.editor.cursor_position = pos;
4177                Ok(CommandResult::Success)
4178            }
4179            CursorCommand::MoveToVisual { row, x_cells } => {
4180                let Some(pos) = self.editor.visual_position_to_logical(row, x_cells) else {
4181                    return Ok(CommandResult::Success);
4182                };
4183
4184                self.editor.cursor_position = pos;
4185                self.preferred_x_cells = Some(x_cells);
4186                // Treat as an absolute move (similar to `MoveTo`).
4187                self.editor.secondary_selections.clear();
4188                Ok(CommandResult::Success)
4189            }
4190            CursorCommand::MoveToLineStart => {
4191                let line = self.editor.cursor_position.line;
4192                self.editor.cursor_position = Position::new(line, 0);
4193                self.preferred_x_cells = Some(0);
4194                self.editor.secondary_selections.clear();
4195                Ok(CommandResult::Success)
4196            }
4197            CursorCommand::MoveToLineEnd => {
4198                let line = self.editor.cursor_position.line;
4199                let end_col = self.clamp_column_for_line(line, usize::MAX);
4200                self.editor.cursor_position = Position::new(line, end_col);
4201                self.preferred_x_cells = self
4202                    .editor
4203                    .logical_position_to_visual(line, end_col)
4204                    .map(|(_, x)| x);
4205                self.editor.secondary_selections.clear();
4206                Ok(CommandResult::Success)
4207            }
4208            CursorCommand::MoveToVisualLineStart => {
4209                let line = self.editor.cursor_position.line;
4210                let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
4211                    return Ok(CommandResult::Success);
4212                };
4213
4214                let line_text = self
4215                    .editor
4216                    .line_index
4217                    .get_line_text(line)
4218                    .unwrap_or_default();
4219                let line_char_len = line_text.chars().count();
4220                let column = self.editor.cursor_position.column.min(line_char_len);
4221
4222                let mut seg_start = 0usize;
4223                for wp in &layout.wrap_points {
4224                    if column >= wp.char_index {
4225                        seg_start = wp.char_index;
4226                    } else {
4227                        break;
4228                    }
4229                }
4230
4231                self.editor.cursor_position = Position::new(line, seg_start);
4232                self.preferred_x_cells = self
4233                    .editor
4234                    .logical_position_to_visual(line, seg_start)
4235                    .map(|(_, x)| x);
4236                self.editor.secondary_selections.clear();
4237                Ok(CommandResult::Success)
4238            }
4239            CursorCommand::MoveToVisualLineEnd => {
4240                let line = self.editor.cursor_position.line;
4241                let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
4242                    return Ok(CommandResult::Success);
4243                };
4244
4245                let line_text = self
4246                    .editor
4247                    .line_index
4248                    .get_line_text(line)
4249                    .unwrap_or_default();
4250                let line_char_len = line_text.chars().count();
4251                let column = self.editor.cursor_position.column.min(line_char_len);
4252
4253                let mut seg_end = line_char_len;
4254                for wp in &layout.wrap_points {
4255                    if column < wp.char_index {
4256                        seg_end = wp.char_index;
4257                        break;
4258                    }
4259                }
4260
4261                self.editor.cursor_position = Position::new(line, seg_end);
4262                self.preferred_x_cells = self
4263                    .editor
4264                    .logical_position_to_visual(line, seg_end)
4265                    .map(|(_, x)| x);
4266                self.editor.secondary_selections.clear();
4267                Ok(CommandResult::Success)
4268            }
4269            CursorCommand::SetSelection { start, end } => {
4270                if start.line >= self.editor.line_index.line_count()
4271                    || end.line >= self.editor.line_index.line_count()
4272                {
4273                    return Err(CommandError::InvalidPosition {
4274                        line: start.line.max(end.line),
4275                        column: start.column.max(end.column),
4276                    });
4277                }
4278
4279                let start = Position::new(
4280                    start.line,
4281                    self.clamp_column_for_line(start.line, start.column),
4282                );
4283                let end = Position::new(end.line, self.clamp_column_for_line(end.line, end.column));
4284
4285                let direction = if start.line < end.line
4286                    || (start.line == end.line && start.column <= end.column)
4287                {
4288                    SelectionDirection::Forward
4289                } else {
4290                    SelectionDirection::Backward
4291                };
4292
4293                self.editor.selection = Some(Selection {
4294                    start,
4295                    end,
4296                    direction,
4297                });
4298                Ok(CommandResult::Success)
4299            }
4300            CursorCommand::ExtendSelection { to } => {
4301                if to.line >= self.editor.line_index.line_count() {
4302                    return Err(CommandError::InvalidPosition {
4303                        line: to.line,
4304                        column: to.column,
4305                    });
4306                }
4307
4308                let to = Position::new(to.line, self.clamp_column_for_line(to.line, to.column));
4309
4310                if let Some(ref mut selection) = self.editor.selection {
4311                    selection.end = to;
4312                    selection.direction = if selection.start.line < to.line
4313                        || (selection.start.line == to.line && selection.start.column <= to.column)
4314                    {
4315                        SelectionDirection::Forward
4316                    } else {
4317                        SelectionDirection::Backward
4318                    };
4319                } else {
4320                    // If no selection, create selection from current cursor
4321                    self.editor.selection = Some(Selection {
4322                        start: self.editor.cursor_position,
4323                        end: to,
4324                        direction: if self.editor.cursor_position.line < to.line
4325                            || (self.editor.cursor_position.line == to.line
4326                                && self.editor.cursor_position.column <= to.column)
4327                        {
4328                            SelectionDirection::Forward
4329                        } else {
4330                            SelectionDirection::Backward
4331                        },
4332                    });
4333                }
4334                Ok(CommandResult::Success)
4335            }
4336            CursorCommand::ClearSelection => {
4337                self.editor.selection = None;
4338                Ok(CommandResult::Success)
4339            }
4340            CursorCommand::SetSelections {
4341                selections,
4342                primary_index,
4343            } => {
4344                let line_count = self.editor.line_index.line_count();
4345                if selections.is_empty() {
4346                    return Err(CommandError::Other(
4347                        "SetSelections requires a non-empty selection list".to_string(),
4348                    ));
4349                }
4350                if primary_index >= selections.len() {
4351                    return Err(CommandError::Other(format!(
4352                        "Invalid primary_index {} for {} selections",
4353                        primary_index,
4354                        selections.len()
4355                    )));
4356                }
4357
4358                for sel in &selections {
4359                    if sel.start.line >= line_count || sel.end.line >= line_count {
4360                        return Err(CommandError::InvalidPosition {
4361                            line: sel.start.line.max(sel.end.line),
4362                            column: sel.start.column.max(sel.end.column),
4363                        });
4364                    }
4365                }
4366
4367                let (selections, primary_index) =
4368                    crate::selection_set::normalize_selections(selections, primary_index);
4369
4370                let primary = selections
4371                    .get(primary_index)
4372                    .cloned()
4373                    .ok_or_else(|| CommandError::Other("Invalid primary selection".to_string()))?;
4374
4375                self.editor.cursor_position = primary.end;
4376                self.editor.selection = if primary.start == primary.end {
4377                    None
4378                } else {
4379                    Some(primary.clone())
4380                };
4381
4382                self.editor.secondary_selections = selections
4383                    .into_iter()
4384                    .enumerate()
4385                    .filter_map(|(idx, sel)| {
4386                        if idx == primary_index {
4387                            None
4388                        } else {
4389                            Some(sel)
4390                        }
4391                    })
4392                    .collect();
4393
4394                Ok(CommandResult::Success)
4395            }
4396            CursorCommand::ClearSecondarySelections => {
4397                self.editor.secondary_selections.clear();
4398                Ok(CommandResult::Success)
4399            }
4400            CursorCommand::SetRectSelection { anchor, active } => {
4401                let line_count = self.editor.line_index.line_count();
4402                if anchor.line >= line_count || active.line >= line_count {
4403                    return Err(CommandError::InvalidPosition {
4404                        line: anchor.line.max(active.line),
4405                        column: anchor.column.max(active.column),
4406                    });
4407                }
4408
4409                let (selections, primary_index) =
4410                    crate::selection_set::rect_selections(anchor, active);
4411
4412                // Delegate to SetSelections so normalization rules are shared.
4413                self.execute_cursor(CursorCommand::SetSelections {
4414                    selections,
4415                    primary_index,
4416                })?;
4417                Ok(CommandResult::Success)
4418            }
4419            CursorCommand::FindNext { query, options } => {
4420                self.execute_find_command(query, options, true)
4421            }
4422            CursorCommand::FindPrev { query, options } => {
4423                self.execute_find_command(query, options, false)
4424            }
4425        }
4426    }
4427
4428    // Private method: execute view command
4429    fn execute_view(&mut self, command: ViewCommand) -> Result<CommandResult, CommandError> {
4430        match command {
4431            ViewCommand::SetViewportWidth { width } => {
4432                if width == 0 {
4433                    return Err(CommandError::Other(
4434                        "Viewport width must be greater than 0".to_string(),
4435                    ));
4436                }
4437
4438                self.editor.viewport_width = width;
4439                self.editor.layout_engine.set_viewport_width(width);
4440                Ok(CommandResult::Success)
4441            }
4442            ViewCommand::SetWrapMode { mode } => {
4443                self.editor.layout_engine.set_wrap_mode(mode);
4444                Ok(CommandResult::Success)
4445            }
4446            ViewCommand::SetWrapIndent { indent } => {
4447                self.editor.layout_engine.set_wrap_indent(indent);
4448                Ok(CommandResult::Success)
4449            }
4450            ViewCommand::SetTabWidth { width } => {
4451                if width == 0 {
4452                    return Err(CommandError::Other(
4453                        "Tab width must be greater than 0".to_string(),
4454                    ));
4455                }
4456
4457                self.editor.layout_engine.set_tab_width(width);
4458                Ok(CommandResult::Success)
4459            }
4460            ViewCommand::SetTabKeyBehavior { behavior } => {
4461                self.tab_key_behavior = behavior;
4462                Ok(CommandResult::Success)
4463            }
4464            ViewCommand::ScrollTo { line } => {
4465                if line >= self.editor.line_index.line_count() {
4466                    return Err(CommandError::InvalidPosition { line, column: 0 });
4467                }
4468
4469                // Scroll operation only validates line number validity
4470                // Actual scrolling handled by frontend
4471                Ok(CommandResult::Success)
4472            }
4473            ViewCommand::GetViewport { start_row, count } => {
4474                let text = self.editor.piece_table.get_text();
4475                let generator = SnapshotGenerator::from_text_with_layout_options(
4476                    &text,
4477                    self.editor.viewport_width,
4478                    self.editor.layout_engine.tab_width(),
4479                    self.editor.layout_engine.wrap_mode(),
4480                    self.editor.layout_engine.wrap_indent(),
4481                );
4482                let grid = generator.get_headless_grid(start_row, count);
4483                Ok(CommandResult::Viewport(grid))
4484            }
4485        }
4486    }
4487
4488    // Private method: execute style command
4489    fn execute_style(&mut self, command: StyleCommand) -> Result<CommandResult, CommandError> {
4490        match command {
4491            StyleCommand::AddStyle {
4492                start,
4493                end,
4494                style_id,
4495            } => {
4496                if start >= end {
4497                    return Err(CommandError::InvalidRange { start, end });
4498                }
4499
4500                let interval = crate::intervals::Interval::new(start, end, style_id);
4501                self.editor.interval_tree.insert(interval);
4502                Ok(CommandResult::Success)
4503            }
4504            StyleCommand::RemoveStyle {
4505                start,
4506                end,
4507                style_id,
4508            } => {
4509                self.editor.interval_tree.remove(start, end, style_id);
4510                Ok(CommandResult::Success)
4511            }
4512            StyleCommand::Fold {
4513                start_line,
4514                end_line,
4515            } => {
4516                if start_line >= end_line {
4517                    return Err(CommandError::InvalidRange {
4518                        start: start_line,
4519                        end: end_line,
4520                    });
4521                }
4522
4523                let mut region = crate::intervals::FoldRegion::new(start_line, end_line);
4524                region.collapse();
4525                self.editor.folding_manager.add_region(region);
4526                Ok(CommandResult::Success)
4527            }
4528            StyleCommand::Unfold { start_line } => {
4529                self.editor.folding_manager.expand_line(start_line);
4530                Ok(CommandResult::Success)
4531            }
4532            StyleCommand::UnfoldAll => {
4533                self.editor.folding_manager.expand_all();
4534                Ok(CommandResult::Success)
4535            }
4536        }
4537    }
4538
4539    fn rebuild_layout_engine_from_text(&mut self, text: &str) {
4540        let lines = crate::text::split_lines_preserve_trailing(text);
4541        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
4542        self.editor.layout_engine.from_lines(&line_refs);
4543    }
4544
4545    fn position_to_char_offset_clamped(&self, pos: Position) -> usize {
4546        let line_count = self.editor.line_index.line_count();
4547        if line_count == 0 {
4548            return 0;
4549        }
4550
4551        let line = pos.line.min(line_count.saturating_sub(1));
4552        let line_text = self
4553            .editor
4554            .line_index
4555            .get_line_text(line)
4556            .unwrap_or_default();
4557        let line_char_len = line_text.chars().count();
4558        let column = pos.column.min(line_char_len);
4559        self.editor.line_index.position_to_char_offset(line, column)
4560    }
4561
4562    fn position_to_char_offset_and_virtual_pad(&self, pos: Position) -> (usize, usize) {
4563        let line_count = self.editor.line_index.line_count();
4564        if line_count == 0 {
4565            return (0, 0);
4566        }
4567
4568        let line = pos.line.min(line_count.saturating_sub(1));
4569        let line_text = self
4570            .editor
4571            .line_index
4572            .get_line_text(line)
4573            .unwrap_or_default();
4574        let line_char_len = line_text.chars().count();
4575        let clamped_col = pos.column.min(line_char_len);
4576        let offset = self
4577            .editor
4578            .line_index
4579            .position_to_char_offset(line, clamped_col);
4580        let pad = pos.column.saturating_sub(clamped_col);
4581        (offset, pad)
4582    }
4583
4584    fn normalize_cursor_and_selection(&mut self) {
4585        let line_index = &self.editor.line_index;
4586        let line_count = line_index.line_count();
4587        if line_count == 0 {
4588            self.editor.cursor_position = Position::new(0, 0);
4589            self.editor.selection = None;
4590            self.editor.secondary_selections.clear();
4591            return;
4592        }
4593
4594        self.editor.cursor_position =
4595            Self::clamp_position_lenient_with_index(line_index, self.editor.cursor_position);
4596
4597        if let Some(ref mut selection) = self.editor.selection {
4598            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
4599            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
4600            selection.direction = if selection.start.line < selection.end.line
4601                || (selection.start.line == selection.end.line
4602                    && selection.start.column <= selection.end.column)
4603            {
4604                SelectionDirection::Forward
4605            } else {
4606                SelectionDirection::Backward
4607            };
4608        }
4609
4610        for selection in &mut self.editor.secondary_selections {
4611            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
4612            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
4613            selection.direction = if selection.start.line < selection.end.line
4614                || (selection.start.line == selection.end.line
4615                    && selection.start.column <= selection.end.column)
4616            {
4617                SelectionDirection::Forward
4618            } else {
4619                SelectionDirection::Backward
4620            };
4621        }
4622    }
4623
4624    fn clamp_column_for_line(&self, line: usize, column: usize) -> usize {
4625        Self::clamp_column_for_line_with_index(&self.editor.line_index, line, column)
4626    }
4627
4628    fn clamp_position_lenient_with_index(line_index: &LineIndex, pos: Position) -> Position {
4629        let line_count = line_index.line_count();
4630        if line_count == 0 {
4631            return Position::new(0, 0);
4632        }
4633
4634        let clamped_line = pos.line.min(line_count.saturating_sub(1));
4635        // Note: do NOT clamp column here. Virtual columns (box selection) are allowed.
4636        Position::new(clamped_line, pos.column)
4637    }
4638
4639    fn clamp_column_for_line_with_index(
4640        line_index: &LineIndex,
4641        line: usize,
4642        column: usize,
4643    ) -> usize {
4644        let line_start = line_index.position_to_char_offset(line, 0);
4645        let line_end = line_index.position_to_char_offset(line, usize::MAX);
4646        let line_len = line_end.saturating_sub(line_start);
4647        column.min(line_len)
4648    }
4649}
4650
4651#[cfg(test)]
4652mod tests {
4653    use super::*;
4654
4655    #[test]
4656    fn test_edit_insert() {
4657        let mut executor = CommandExecutor::new("Hello", 80);
4658
4659        let result = executor.execute(Command::Edit(EditCommand::Insert {
4660            offset: 5,
4661            text: " World".to_string(),
4662        }));
4663
4664        assert!(result.is_ok());
4665        assert_eq!(executor.editor().get_text(), "Hello World");
4666    }
4667
4668    #[test]
4669    fn test_edit_delete() {
4670        let mut executor = CommandExecutor::new("Hello World", 80);
4671
4672        let result = executor.execute(Command::Edit(EditCommand::Delete {
4673            start: 5,
4674            length: 6,
4675        }));
4676
4677        assert!(result.is_ok());
4678        assert_eq!(executor.editor().get_text(), "Hello");
4679    }
4680
4681    #[test]
4682    fn test_edit_replace() {
4683        let mut executor = CommandExecutor::new("Hello World", 80);
4684
4685        let result = executor.execute(Command::Edit(EditCommand::Replace {
4686            start: 6,
4687            length: 5,
4688            text: "Rust".to_string(),
4689        }));
4690
4691        assert!(result.is_ok());
4692        assert_eq!(executor.editor().get_text(), "Hello Rust");
4693    }
4694
4695    #[test]
4696    fn test_cursor_move_to() {
4697        let mut executor = CommandExecutor::new("Line 1\nLine 2\nLine 3", 80);
4698
4699        let result = executor.execute(Command::Cursor(CursorCommand::MoveTo {
4700            line: 1,
4701            column: 3,
4702        }));
4703
4704        assert!(result.is_ok());
4705        assert_eq!(executor.editor().cursor_position(), Position::new(1, 3));
4706    }
4707
4708    #[test]
4709    fn test_cursor_selection() {
4710        let mut executor = CommandExecutor::new("Hello World", 80);
4711
4712        let result = executor.execute(Command::Cursor(CursorCommand::SetSelection {
4713            start: Position::new(0, 0),
4714            end: Position::new(0, 5),
4715        }));
4716
4717        assert!(result.is_ok());
4718        assert!(executor.editor().selection().is_some());
4719    }
4720
4721    #[test]
4722    fn test_view_set_width() {
4723        let mut executor = CommandExecutor::new("Test", 80);
4724
4725        let result = executor.execute(Command::View(ViewCommand::SetViewportWidth { width: 40 }));
4726
4727        assert!(result.is_ok());
4728        assert_eq!(executor.editor().viewport_width, 40);
4729    }
4730
4731    #[test]
4732    fn test_style_add_remove() {
4733        let mut executor = CommandExecutor::new("Hello World", 80);
4734
4735        // Add style
4736        let result = executor.execute(Command::Style(StyleCommand::AddStyle {
4737            start: 0,
4738            end: 5,
4739            style_id: 1,
4740        }));
4741        assert!(result.is_ok());
4742
4743        // Remove style
4744        let result = executor.execute(Command::Style(StyleCommand::RemoveStyle {
4745            start: 0,
4746            end: 5,
4747            style_id: 1,
4748        }));
4749        assert!(result.is_ok());
4750    }
4751
4752    #[test]
4753    fn test_batch_execution() {
4754        let mut executor = CommandExecutor::new("", 80);
4755
4756        let commands = vec![
4757            Command::Edit(EditCommand::Insert {
4758                offset: 0,
4759                text: "Hello".to_string(),
4760            }),
4761            Command::Edit(EditCommand::Insert {
4762                offset: 5,
4763                text: " World".to_string(),
4764            }),
4765        ];
4766
4767        let results = executor.execute_batch(commands);
4768        assert!(results.is_ok());
4769        assert_eq!(executor.editor().get_text(), "Hello World");
4770    }
4771
4772    #[test]
4773    fn test_error_invalid_offset() {
4774        let mut executor = CommandExecutor::new("Hello", 80);
4775
4776        let result = executor.execute(Command::Edit(EditCommand::Insert {
4777            offset: 100,
4778            text: "X".to_string(),
4779        }));
4780
4781        assert!(result.is_err());
4782        assert!(matches!(
4783            result.unwrap_err(),
4784            CommandError::InvalidOffset(_)
4785        ));
4786    }
4787}