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::intervals::{FoldRegion, StyleId, StyleLayerId};
37use crate::layout::{cell_width_at, char_width, visual_x_for_column};
38use crate::search::{CharIndex, SearchMatch, SearchOptions, find_all, find_next, find_prev};
39use crate::snapshot::{Cell, HeadlessGrid, HeadlessLine};
40use crate::{
41    FOLD_PLACEHOLDER_STYLE_ID, FoldingManager, IntervalTree, LayoutEngine, LineIndex, PieceTable,
42    SnapshotGenerator,
43};
44use regex::RegexBuilder;
45use std::cmp::Ordering;
46use std::collections::BTreeMap;
47
48/// Position coordinates (line and column numbers)
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Position {
51    /// Zero-based logical line index.
52    pub line: usize,
53    /// Zero-based column in characters within the logical line.
54    pub column: usize,
55}
56
57impl Position {
58    /// Create a new logical position.
59    pub fn new(line: usize, column: usize) -> Self {
60        Self { line, column }
61    }
62}
63
64impl Ord for Position {
65    fn cmp(&self, other: &Self) -> Ordering {
66        self.line
67            .cmp(&other.line)
68            .then_with(|| self.column.cmp(&other.column))
69    }
70}
71
72impl PartialOrd for Position {
73    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
74        Some(self.cmp(other))
75    }
76}
77
78/// Selection range
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Selection {
81    /// Selection start position
82    pub start: Position,
83    /// Selection end position
84    pub end: Position,
85    /// Selection direction
86    pub direction: SelectionDirection,
87}
88
89/// Selection direction
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum SelectionDirection {
92    /// Forward selection (from start to end)
93    Forward,
94    /// Backward selection (from end to start)
95    Backward,
96}
97
98/// Controls how a Tab key press is handled by the editor when using [`EditCommand::InsertTab`].
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum TabKeyBehavior {
101    /// Insert a literal tab character (`'\t'`).
102    Tab,
103    /// Insert spaces up to the next tab stop (based on the current `tab_width` setting).
104    Spaces,
105}
106
107/// Text editing commands
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum EditCommand {
110    /// Insert text at the specified position
111    Insert {
112        /// Character offset to insert at.
113        offset: usize,
114        /// Text to insert.
115        text: String,
116    },
117    /// Delete text in specified range
118    Delete {
119        /// Character offset of the deletion start.
120        start: usize,
121        /// Length of the deletion in characters.
122        length: usize,
123    },
124    /// Replace text in specified range
125    Replace {
126        /// Character offset of the replacement start.
127        start: usize,
128        /// Length of the replaced range in characters.
129        length: usize,
130        /// Replacement text.
131        text: String,
132    },
133    /// VSCode-like typing/paste: apply to all carets/selections (primary + secondary)
134    InsertText {
135        /// Text to insert/replace at each selection/caret.
136        text: String,
137    },
138    /// Insert a tab at each caret (or replace each selection), using the current tab settings.
139    ///
140    /// - If `TabKeyBehavior::Tab`, inserts `'\t'`.
141    /// - If `TabKeyBehavior::Spaces`, inserts spaces up to the next tab stop.
142    InsertTab,
143    /// Backspace-like deletion: delete selection(s) if any, otherwise delete 1 char before each caret.
144    Backspace,
145    /// Delete key-like deletion: delete selection(s) if any, otherwise delete 1 char after each caret.
146    DeleteForward,
147    /// Undo last edit operation (supports grouping)
148    Undo,
149    /// Redo last undone operation (supports grouping)
150    Redo,
151    /// Explicitly end the current undo group (for idle or external boundaries)
152    EndUndoGroup,
153    /// Replace the current occurrence of `query` (based on selection/caret) with `replacement`.
154    ///
155    /// - Honors `options` (case sensitivity / whole-word / regex).
156    /// - Treated as a single undoable edit.
157    ReplaceCurrent {
158        /// Search query.
159        query: String,
160        /// Replacement text.
161        replacement: String,
162        /// Search options (case sensitivity, whole-word, regex).
163        options: SearchOptions,
164    },
165    /// Replace all occurrences of `query` with `replacement`.
166    ///
167    /// - Honors `options` (case sensitivity / whole-word / regex).
168    /// - Treated as a single undoable edit.
169    ReplaceAll {
170        /// Search query.
171        query: String,
172        /// Replacement text.
173        replacement: String,
174        /// Search options (case sensitivity, whole-word, regex).
175        options: SearchOptions,
176    },
177}
178
179/// Cursor & selection commands
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum CursorCommand {
182    /// Move cursor to the specified position
183    MoveTo {
184        /// Target logical line index.
185        line: usize,
186        /// Target column in characters (will be clamped to line length).
187        column: usize,
188    },
189    /// Move cursor relatively
190    MoveBy {
191        /// Delta in logical lines.
192        delta_line: isize,
193        /// Delta in columns (characters).
194        delta_column: isize,
195    },
196    /// Set selection range
197    SetSelection {
198        /// Selection start position.
199        start: Position,
200        /// Selection end position.
201        end: Position,
202    },
203    /// Extend selection range
204    ExtendSelection {
205        /// New active end position.
206        to: Position,
207    },
208    /// Clear selection
209    ClearSelection,
210    /// Set multiple selections/multi-cursor (including primary)
211    SetSelections {
212        /// All selections (including primary).
213        selections: Vec<Selection>,
214        /// Index of the primary selection in `selections`.
215        primary_index: usize,
216    },
217    /// Clear secondary selections/cursors, keeping only primary
218    ClearSecondarySelections,
219    /// Set rectangular selection (box/column selection), which expands into one Selection per line
220    SetRectSelection {
221        /// Anchor position (fixed corner).
222        anchor: Position,
223        /// Active position (moving corner).
224        active: Position,
225    },
226    /// Find the next occurrence of `query` and select it (primary selection only).
227    FindNext {
228        /// Search query.
229        query: String,
230        /// Search options (case sensitivity, whole-word, regex).
231        options: SearchOptions,
232    },
233    /// Find the previous occurrence of `query` and select it (primary selection only).
234    FindPrev {
235        /// Search query.
236        query: String,
237        /// Search options (case sensitivity, whole-word, regex).
238        options: SearchOptions,
239    },
240}
241
242/// View commands
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum ViewCommand {
245    /// Set viewport width
246    SetViewportWidth {
247        /// Width in character cells.
248        width: usize,
249    },
250    /// Set tab width (in character cells) used for measuring `'\t'` and tab stops.
251    SetTabWidth {
252        /// Tab width in character cells (must be greater than 0).
253        width: usize,
254    },
255    /// Configure how [`EditCommand::InsertTab`] inserts text.
256    SetTabKeyBehavior {
257        /// Tab key behavior.
258        behavior: TabKeyBehavior,
259    },
260    /// Scroll to specified line
261    ScrollTo {
262        /// Logical line index to scroll to.
263        line: usize,
264    },
265    /// Get current viewport content
266    GetViewport {
267        /// Starting visual row.
268        start_row: usize,
269        /// Number of visual rows requested.
270        count: usize,
271    },
272}
273
274/// Style and folding commands
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum StyleCommand {
277    /// Add style interval
278    AddStyle {
279        /// Interval start offset in characters.
280        start: usize,
281        /// Interval end offset in characters (exclusive).
282        end: usize,
283        /// Style identifier.
284        style_id: StyleId,
285    },
286    /// Remove style interval
287    RemoveStyle {
288        /// Interval start offset in characters.
289        start: usize,
290        /// Interval end offset in characters (exclusive).
291        end: usize,
292        /// Style identifier.
293        style_id: StyleId,
294    },
295    /// Fold code block
296    Fold {
297        /// Start logical line (inclusive).
298        start_line: usize,
299        /// End logical line (inclusive).
300        end_line: usize,
301    },
302    /// Unfold code block
303    Unfold {
304        /// Start logical line (inclusive) of the fold region to unfold.
305        start_line: usize,
306    },
307    /// Unfold all folds
308    UnfoldAll,
309}
310
311/// Unified command enum
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum Command {
314    /// Text editing commands
315    Edit(EditCommand),
316    /// Cursor command
317    Cursor(CursorCommand),
318    /// View commands
319    View(ViewCommand),
320    /// Style command
321    Style(StyleCommand),
322}
323
324/// Command execution result
325#[derive(Debug, Clone)]
326pub enum CommandResult {
327    /// Success, no return value
328    Success,
329    /// Success, returns text
330    Text(String),
331    /// Success, returns position
332    Position(Position),
333    /// Success, returns offset
334    Offset(usize),
335    /// Success, returns viewport content
336    Viewport(HeadlessGrid),
337    /// Find/search result: a match in char offsets (half-open).
338    SearchMatch {
339        /// Inclusive start character offset.
340        start: usize,
341        /// Exclusive end character offset.
342        end: usize,
343    },
344    /// Find/search result: no match found.
345    SearchNotFound,
346    /// Replace result: how many occurrences were replaced.
347    ReplaceResult {
348        /// Number of occurrences replaced.
349        replaced: usize,
350    },
351}
352
353/// Command error type
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub enum CommandError {
356    /// Invalid offset
357    InvalidOffset(usize),
358    /// Invalid position
359    InvalidPosition {
360        /// Logical line index.
361        line: usize,
362        /// Column in characters.
363        column: usize,
364    },
365    /// Invalid range
366    InvalidRange {
367        /// Inclusive start character offset.
368        start: usize,
369        /// Exclusive end character offset.
370        end: usize,
371    },
372    /// Empty text
373    EmptyText,
374    /// Other error
375    Other(String),
376}
377
378impl std::fmt::Display for CommandError {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        match self {
381            CommandError::InvalidOffset(offset) => {
382                write!(f, "Invalid offset: {}", offset)
383            }
384            CommandError::InvalidPosition { line, column } => {
385                write!(f, "Invalid position: line {}, column {}", line, column)
386            }
387            CommandError::InvalidRange { start, end } => {
388                write!(f, "Invalid range: {}..{}", start, end)
389            }
390            CommandError::EmptyText => {
391                write!(f, "Text cannot be empty")
392            }
393            CommandError::Other(msg) => {
394                write!(f, "{}", msg)
395            }
396        }
397    }
398}
399
400impl std::error::Error for CommandError {}
401
402#[derive(Debug, Clone)]
403struct SelectionSetSnapshot {
404    selections: Vec<Selection>,
405    primary_index: usize,
406}
407
408#[derive(Debug, Clone)]
409struct TextEdit {
410    start_before: usize,
411    start_after: usize,
412    deleted_text: String,
413    inserted_text: String,
414}
415
416impl TextEdit {
417    fn deleted_len(&self) -> usize {
418        self.deleted_text.chars().count()
419    }
420
421    fn inserted_len(&self) -> usize {
422        self.inserted_text.chars().count()
423    }
424}
425
426#[derive(Debug, Clone)]
427struct UndoStep {
428    group_id: usize,
429    edits: Vec<TextEdit>,
430    before_selection: SelectionSetSnapshot,
431    after_selection: SelectionSetSnapshot,
432}
433
434#[derive(Debug)]
435struct UndoRedoManager {
436    undo_stack: Vec<UndoStep>,
437    redo_stack: Vec<UndoStep>,
438    max_undo: usize,
439    /// Clean point tracking. Uses `undo_stack.len()` as the saved position in the linear history.
440    /// When `redo_stack` is non-empty, `clean_index` may be greater than `undo_stack.len()`.
441    clean_index: Option<usize>,
442    next_group_id: usize,
443    open_group_id: Option<usize>,
444}
445
446impl UndoRedoManager {
447    fn new(max_undo: usize) -> Self {
448        Self {
449            undo_stack: Vec::new(),
450            redo_stack: Vec::new(),
451            max_undo,
452            clean_index: Some(0),
453            next_group_id: 0,
454            open_group_id: None,
455        }
456    }
457
458    fn can_undo(&self) -> bool {
459        !self.undo_stack.is_empty()
460    }
461
462    fn can_redo(&self) -> bool {
463        !self.redo_stack.is_empty()
464    }
465
466    fn undo_depth(&self) -> usize {
467        self.undo_stack.len()
468    }
469
470    fn redo_depth(&self) -> usize {
471        self.redo_stack.len()
472    }
473
474    fn current_group_id(&self) -> Option<usize> {
475        self.open_group_id
476    }
477
478    fn is_clean(&self) -> bool {
479        self.clean_index == Some(self.undo_stack.len())
480    }
481
482    fn mark_clean(&mut self) {
483        self.clean_index = Some(self.undo_stack.len());
484        self.end_group();
485    }
486
487    fn end_group(&mut self) {
488        self.open_group_id = None;
489    }
490
491    fn clear_redo_and_adjust_clean(&mut self) {
492        if self.redo_stack.is_empty() {
493            return;
494        }
495
496        // If clean point is in redo area, it becomes unreachable after clearing redo.
497        if let Some(clean_index) = self.clean_index
498            && clean_index > self.undo_stack.len()
499        {
500            self.clean_index = None;
501        }
502
503        self.redo_stack.clear();
504    }
505
506    fn push_step(&mut self, mut step: UndoStep, coalescible_insert: bool) {
507        self.clear_redo_and_adjust_clean();
508
509        if self.undo_stack.len() >= self.max_undo {
510            self.undo_stack.remove(0);
511            if let Some(clean_index) = self.clean_index {
512                if clean_index == 0 {
513                    self.clean_index = None;
514                } else {
515                    self.clean_index = Some(clean_index - 1);
516                }
517            }
518        }
519
520        let reuse_open_group = coalescible_insert
521            && self.open_group_id.is_some()
522            && self.clean_index != Some(self.undo_stack.len());
523
524        if reuse_open_group {
525            step.group_id = self.open_group_id.expect("checked");
526        } else {
527            step.group_id = self.next_group_id;
528            self.next_group_id = self.next_group_id.wrapping_add(1);
529        }
530
531        if coalescible_insert {
532            self.open_group_id = Some(step.group_id);
533        } else {
534            self.open_group_id = None;
535        }
536
537        self.undo_stack.push(step);
538    }
539
540    fn pop_undo_group(&mut self) -> Option<Vec<UndoStep>> {
541        let last_group_id = self.undo_stack.last().map(|s| s.group_id)?;
542        let mut steps: Vec<UndoStep> = Vec::new();
543
544        while let Some(step) = self.undo_stack.last() {
545            if step.group_id != last_group_id {
546                break;
547            }
548            steps.push(self.undo_stack.pop().expect("checked"));
549        }
550
551        Some(steps)
552    }
553
554    fn pop_redo_group(&mut self) -> Option<Vec<UndoStep>> {
555        let last_group_id = self.redo_stack.last().map(|s| s.group_id)?;
556        let mut steps: Vec<UndoStep> = Vec::new();
557
558        while let Some(step) = self.redo_stack.last() {
559            if step.group_id != last_group_id {
560                break;
561            }
562            steps.push(self.redo_stack.pop().expect("checked"));
563        }
564
565        Some(steps)
566    }
567}
568
569/// Editor Core state
570///
571/// `EditorCore` aggregates all underlying editor components, including:
572///
573/// - **PieceTable**: Efficient text storage and modification
574/// - **LineIndex**: Rope-based line index, supporting fast line access
575/// - **LayoutEngine**: Soft wrapping and text layout calculation
576/// - **IntervalTree**: Style interval management
577/// - **FoldingManager**: Code folding management
578/// - **Cursor & Selection**: Cursor and selection state
579///
580/// # Example
581///
582/// ```rust
583/// use editor_core::EditorCore;
584///
585/// let mut core = EditorCore::new("Hello\nWorld", 80);
586/// assert_eq!(core.line_count(), 2);
587/// assert_eq!(core.get_text(), "Hello\nWorld");
588/// ```
589pub struct EditorCore {
590    /// Piece Table storage layer
591    pub piece_table: PieceTable,
592    /// Line index
593    pub line_index: LineIndex,
594    /// Layout engine
595    pub layout_engine: LayoutEngine,
596    /// Interval tree (style management)
597    pub interval_tree: IntervalTree,
598    /// Layered styles (for semantic highlighting/simple syntax highlighting, etc.)
599    pub style_layers: BTreeMap<StyleLayerId, IntervalTree>,
600    /// Folding manager
601    pub folding_manager: FoldingManager,
602    /// Current cursor position
603    pub cursor_position: Position,
604    /// Current selection range
605    pub selection: Option<Selection>,
606    /// Secondary selections/cursors (multi-cursor). Each Selection can be empty (start==end), representing a caret.
607    pub secondary_selections: Vec<Selection>,
608    /// Viewport width
609    pub viewport_width: usize,
610}
611
612impl EditorCore {
613    /// Create a new Editor Core
614    pub fn new(text: &str, viewport_width: usize) -> Self {
615        let piece_table = PieceTable::new(text);
616        let line_index = LineIndex::from_text(text);
617        let mut layout_engine = LayoutEngine::new(viewport_width);
618
619        // Initialize layout engine to be consistent with initial text (including trailing empty line).
620        let lines = crate::text::split_lines_preserve_trailing(text);
621        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
622        layout_engine.from_lines(&line_refs);
623
624        Self {
625            piece_table,
626            line_index,
627            layout_engine,
628            interval_tree: IntervalTree::new(),
629            style_layers: BTreeMap::new(),
630            folding_manager: FoldingManager::new(),
631            cursor_position: Position::new(0, 0),
632            selection: None,
633            secondary_selections: Vec::new(),
634            viewport_width,
635        }
636    }
637
638    /// Create an empty Editor Core
639    pub fn empty(viewport_width: usize) -> Self {
640        Self::new("", viewport_width)
641    }
642
643    /// Get text content
644    pub fn get_text(&self) -> String {
645        self.piece_table.get_text()
646    }
647
648    /// Get total line count
649    pub fn line_count(&self) -> usize {
650        self.line_index.line_count()
651    }
652
653    /// Get total character count
654    pub fn char_count(&self) -> usize {
655        self.piece_table.char_count()
656    }
657
658    /// Get cursor position
659    pub fn cursor_position(&self) -> Position {
660        self.cursor_position
661    }
662
663    /// Get selection range
664    pub fn selection(&self) -> Option<&Selection> {
665        self.selection.as_ref()
666    }
667
668    /// Get secondary selections/cursors (multi-cursor)
669    pub fn secondary_selections(&self) -> &[Selection] {
670        &self.secondary_selections
671    }
672
673    /// Get styled headless grid snapshot (by visual line).
674    ///
675    /// - Supportsoft wrapping (based `layout_engine`)
676    /// - `Cell.styles` will `interval_tree` + `style_layers` merged from
677    /// - Supportcode folding (based `folding_manager`)
678    ///
679    /// Note: This API is not responsible for mapping `StyleId` to specific colors.
680    pub fn get_headless_grid_styled(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
681        let mut grid = HeadlessGrid::new(start_visual_row, count);
682        if count == 0 {
683            return grid;
684        }
685
686        let tab_width = self.layout_engine.tab_width();
687
688        let total_visual = self.visual_line_count();
689        if start_visual_row >= total_visual {
690            return grid;
691        }
692
693        let end_visual = start_visual_row.saturating_add(count).min(total_visual);
694
695        let mut current_visual = 0usize;
696        let logical_line_count = self.layout_engine.logical_line_count();
697        let regions = self.folding_manager.regions();
698
699        'outer: for logical_line in 0..logical_line_count {
700            if Self::is_logical_line_hidden(regions, logical_line) {
701                continue;
702            }
703
704            let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
705                continue;
706            };
707
708            let line_text = self
709                .line_index
710                .get_line_text(logical_line)
711                .unwrap_or_default();
712            let line_char_len = line_text.chars().count();
713            let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
714
715            for visual_in_line in 0..layout.visual_line_count {
716                if current_visual >= end_visual {
717                    break 'outer;
718                }
719
720                if current_visual >= start_visual_row {
721                    let segment_start_col = if visual_in_line == 0 {
722                        0
723                    } else {
724                        layout
725                            .wrap_points
726                            .get(visual_in_line - 1)
727                            .map(|wp| wp.char_index)
728                            .unwrap_or(0)
729                            .min(line_char_len)
730                    };
731
732                    let segment_end_col = if visual_in_line < layout.wrap_points.len() {
733                        layout.wrap_points[visual_in_line]
734                            .char_index
735                            .min(line_char_len)
736                    } else {
737                        line_char_len
738                    };
739
740                    let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
741                    let mut x_in_line =
742                        visual_x_for_column(&line_text, segment_start_col, tab_width);
743
744                    for (col, ch) in line_text
745                        .chars()
746                        .enumerate()
747                        .skip(segment_start_col)
748                        .take(segment_end_col.saturating_sub(segment_start_col))
749                    {
750                        let offset = line_start_offset + col;
751                        let styles = self.styles_at_offset(offset);
752                        let w = cell_width_at(ch, x_in_line, tab_width);
753                        x_in_line = x_in_line.saturating_add(w);
754                        headless_line.add_cell(Cell::with_styles(ch, w, styles));
755                    }
756
757                    // For collapsed folding start line, append placeholder to the last segment.
758                    if visual_in_line + 1 == layout.visual_line_count
759                        && let Some(region) =
760                            Self::collapsed_region_starting_at(regions, logical_line)
761                        && !region.placeholder.is_empty()
762                    {
763                        if !headless_line.cells.is_empty() {
764                            x_in_line = x_in_line.saturating_add(char_width(' '));
765                            headless_line.add_cell(Cell::with_styles(
766                                ' ',
767                                char_width(' '),
768                                vec![FOLD_PLACEHOLDER_STYLE_ID],
769                            ));
770                        }
771                        for ch in region.placeholder.chars() {
772                            let w = cell_width_at(ch, x_in_line, tab_width);
773                            x_in_line = x_in_line.saturating_add(w);
774                            headless_line.add_cell(Cell::with_styles(
775                                ch,
776                                w,
777                                vec![FOLD_PLACEHOLDER_STYLE_ID],
778                            ));
779                        }
780                    }
781
782                    grid.add_line(headless_line);
783                }
784
785                current_visual = current_visual.saturating_add(1);
786            }
787        }
788
789        grid
790    }
791
792    /// Get total visual line count (considering soft wrapping + folding).
793    pub fn visual_line_count(&self) -> usize {
794        let regions = self.folding_manager.regions();
795        let mut total = 0usize;
796
797        for logical_line in 0..self.layout_engine.logical_line_count() {
798            if Self::is_logical_line_hidden(regions, logical_line) {
799                continue;
800            }
801
802            total = total.saturating_add(
803                self.layout_engine
804                    .get_line_layout(logical_line)
805                    .map(|l| l.visual_line_count)
806                    .unwrap_or(1),
807            );
808        }
809
810        total
811    }
812
813    /// Map visual line number back to (logical_line, visual_in_logical), considering folding.
814    pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
815        let regions = self.folding_manager.regions();
816        let mut cumulative_visual = 0usize;
817        let mut last_visible = (0usize, 0usize);
818
819        for logical_line in 0..self.layout_engine.logical_line_count() {
820            if Self::is_logical_line_hidden(regions, logical_line) {
821                continue;
822            }
823
824            let visual_count = self
825                .layout_engine
826                .get_line_layout(logical_line)
827                .map(|l| l.visual_line_count)
828                .unwrap_or(1);
829
830            if cumulative_visual + visual_count > visual_line {
831                return (logical_line, visual_line - cumulative_visual);
832            }
833
834            cumulative_visual = cumulative_visual.saturating_add(visual_count);
835            last_visible = (logical_line, visual_count.saturating_sub(1));
836        }
837
838        last_visible
839    }
840
841    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
842    pub fn logical_position_to_visual(
843        &self,
844        logical_line: usize,
845        column: usize,
846    ) -> Option<(usize, usize)> {
847        let regions = self.folding_manager.regions();
848        let logical_line = Self::closest_visible_line(regions, logical_line)?;
849        let visual_start = self.visual_start_for_logical_line(logical_line)?;
850
851        let tab_width = self.layout_engine.tab_width();
852
853        let layout = self.layout_engine.get_line_layout(logical_line)?;
854        let line_text = self
855            .line_index
856            .get_line_text(logical_line)
857            .unwrap_or_default();
858
859        let line_char_len = line_text.chars().count();
860        let column = column.min(line_char_len);
861
862        let mut wrapped_offset = 0usize;
863        let mut segment_start_col = 0usize;
864        for wrap_point in &layout.wrap_points {
865            if column >= wrap_point.char_index {
866                wrapped_offset = wrapped_offset.saturating_add(1);
867                segment_start_col = wrap_point.char_index;
868            } else {
869                break;
870            }
871        }
872
873        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
874        let mut x_in_line = seg_start_x_in_line;
875        let mut x_in_segment = 0usize;
876        for ch in line_text
877            .chars()
878            .skip(segment_start_col)
879            .take(column.saturating_sub(segment_start_col))
880        {
881            let w = cell_width_at(ch, x_in_line, tab_width);
882            x_in_line = x_in_line.saturating_add(w);
883            x_in_segment = x_in_segment.saturating_add(w);
884        }
885
886        Some((visual_start.saturating_add(wrapped_offset), x_in_segment))
887    }
888
889    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
890    ///
891    /// Difference from [`logical_position_to_visual`](Self::logical_position_to_visual) is that it allows `column`
892    /// to exceed the line end: the exceeding part is treated as `' '` (width=1) virtual spaces, suitable for rectangular selection / column editing.
893    pub fn logical_position_to_visual_allow_virtual(
894        &self,
895        logical_line: usize,
896        column: usize,
897    ) -> Option<(usize, usize)> {
898        let regions = self.folding_manager.regions();
899        let logical_line = Self::closest_visible_line(regions, logical_line)?;
900        let visual_start = self.visual_start_for_logical_line(logical_line)?;
901
902        let tab_width = self.layout_engine.tab_width();
903
904        let layout = self.layout_engine.get_line_layout(logical_line)?;
905        let line_text = self
906            .line_index
907            .get_line_text(logical_line)
908            .unwrap_or_default();
909
910        let line_char_len = line_text.chars().count();
911        let clamped_column = column.min(line_char_len);
912
913        let mut wrapped_offset = 0usize;
914        let mut segment_start_col = 0usize;
915        for wrap_point in &layout.wrap_points {
916            if clamped_column >= wrap_point.char_index {
917                wrapped_offset = wrapped_offset.saturating_add(1);
918                segment_start_col = wrap_point.char_index;
919            } else {
920                break;
921            }
922        }
923
924        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
925        let mut x_in_line = seg_start_x_in_line;
926        let mut x_in_segment = 0usize;
927        for ch in line_text
928            .chars()
929            .skip(segment_start_col)
930            .take(clamped_column.saturating_sub(segment_start_col))
931        {
932            let w = cell_width_at(ch, x_in_line, tab_width);
933            x_in_line = x_in_line.saturating_add(w);
934            x_in_segment = x_in_segment.saturating_add(w);
935        }
936
937        let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
938
939        Some((visual_start.saturating_add(wrapped_offset), x_in_segment))
940    }
941
942    fn visual_start_for_logical_line(&self, logical_line: usize) -> Option<usize> {
943        if logical_line >= self.layout_engine.logical_line_count() {
944            return None;
945        }
946
947        let regions = self.folding_manager.regions();
948        if Self::is_logical_line_hidden(regions, logical_line) {
949            return None;
950        }
951
952        let mut start = 0usize;
953        for line in 0..logical_line {
954            if Self::is_logical_line_hidden(regions, line) {
955                continue;
956            }
957            start = start.saturating_add(
958                self.layout_engine
959                    .get_line_layout(line)
960                    .map(|l| l.visual_line_count)
961                    .unwrap_or(1),
962            );
963        }
964        Some(start)
965    }
966
967    fn is_logical_line_hidden(regions: &[FoldRegion], logical_line: usize) -> bool {
968        regions.iter().any(|region| {
969            region.is_collapsed
970                && logical_line > region.start_line
971                && logical_line <= region.end_line
972        })
973    }
974
975    fn collapsed_region_starting_at(
976        regions: &[FoldRegion],
977        start_line: usize,
978    ) -> Option<&FoldRegion> {
979        regions
980            .iter()
981            .filter(|region| {
982                region.is_collapsed
983                    && region.start_line == start_line
984                    && region.end_line > start_line
985            })
986            .min_by_key(|region| region.end_line)
987    }
988
989    fn closest_visible_line(regions: &[FoldRegion], logical_line: usize) -> Option<usize> {
990        let mut line = logical_line;
991        if regions.is_empty() {
992            return Some(line);
993        }
994
995        while Self::is_logical_line_hidden(regions, line) {
996            let Some(start) = regions
997                .iter()
998                .filter(|region| {
999                    region.is_collapsed && line > region.start_line && line <= region.end_line
1000                })
1001                .map(|region| region.start_line)
1002                .max()
1003            else {
1004                break;
1005            };
1006            line = start;
1007        }
1008
1009        if Self::is_logical_line_hidden(regions, line) {
1010            None
1011        } else {
1012            Some(line)
1013        }
1014    }
1015
1016    fn styles_at_offset(&self, offset: usize) -> Vec<StyleId> {
1017        let mut styles: Vec<StyleId> = self
1018            .interval_tree
1019            .query_point(offset)
1020            .iter()
1021            .map(|interval| interval.style_id)
1022            .collect();
1023
1024        for tree in self.style_layers.values() {
1025            styles.extend(
1026                tree.query_point(offset)
1027                    .iter()
1028                    .map(|interval| interval.style_id),
1029            );
1030        }
1031
1032        styles.sort_unstable();
1033        styles.dedup();
1034        styles
1035    }
1036}
1037
1038/// Command executor
1039///
1040/// `CommandExecutor` is the main interface for the editor, responsible for:
1041///
1042/// - Execute various editor commands
1043/// - Maintain command history
1044/// - Handle errors and exceptions
1045/// - Ensure editor state consistency
1046///
1047/// # Command Types
1048///
1049/// - [`EditCommand`] - Text insertion, deletion, replacement
1050/// - [`CursorCommand`] - Cursor movement, selection operations
1051/// - [`ViewCommand`] - Viewport management and scroll control
1052/// - [`StyleCommand`] - Style and folding management
1053///
1054/// # Example
1055///
1056/// ```rust
1057/// use editor_core::{CommandExecutor, Command, EditCommand, CursorCommand, Position};
1058///
1059/// let mut executor = CommandExecutor::empty(80);
1060///
1061/// // Insert text
1062/// executor.execute(Command::Edit(EditCommand::Insert {
1063///     offset: 0,
1064///     text: "fn main() {}".to_string(),
1065/// })).unwrap();
1066///
1067/// // Move cursor
1068/// executor.execute(Command::Cursor(CursorCommand::MoveTo {
1069///     line: 0,
1070///     column: 3,
1071/// })).unwrap();
1072///
1073/// assert_eq!(executor.editor().cursor_position(), Position::new(0, 3));
1074/// ```
1075pub struct CommandExecutor {
1076    /// Editor Core
1077    editor: EditorCore,
1078    /// Command history
1079    command_history: Vec<Command>,
1080    /// Undo/redo manager (only records CommandExecutor edit commands executed via)
1081    undo_redo: UndoRedoManager,
1082    /// Controls how [`EditCommand::InsertTab`] behaves.
1083    tab_key_behavior: TabKeyBehavior,
1084}
1085
1086impl CommandExecutor {
1087    /// Create a new command executor
1088    pub fn new(text: &str, viewport_width: usize) -> Self {
1089        Self {
1090            editor: EditorCore::new(text, viewport_width),
1091            command_history: Vec::new(),
1092            undo_redo: UndoRedoManager::new(1000),
1093            tab_key_behavior: TabKeyBehavior::Tab,
1094        }
1095    }
1096
1097    /// Create an empty command executor
1098    pub fn empty(viewport_width: usize) -> Self {
1099        Self::new("", viewport_width)
1100    }
1101
1102    /// Execute command
1103    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
1104        // Save command to history
1105        self.command_history.push(command.clone());
1106
1107        // Undo grouping: any non-edit command ends the current coalescing group.
1108        if !matches!(command, Command::Edit(_)) {
1109            self.undo_redo.end_group();
1110        }
1111
1112        // Execute command
1113        match command {
1114            Command::Edit(edit_cmd) => self.execute_edit(edit_cmd),
1115            Command::Cursor(cursor_cmd) => self.execute_cursor(cursor_cmd),
1116            Command::View(view_cmd) => self.execute_view(view_cmd),
1117            Command::Style(style_cmd) => self.execute_style(style_cmd),
1118        }
1119    }
1120
1121    /// Batch execute commands (transactional)
1122    pub fn execute_batch(
1123        &mut self,
1124        commands: Vec<Command>,
1125    ) -> Result<Vec<CommandResult>, CommandError> {
1126        let mut results = Vec::new();
1127
1128        for command in commands {
1129            let result = self.execute(command)?;
1130            results.push(result);
1131        }
1132
1133        Ok(results)
1134    }
1135
1136    /// Get command history
1137    pub fn get_command_history(&self) -> &[Command] {
1138        &self.command_history
1139    }
1140
1141    /// Can undo
1142    pub fn can_undo(&self) -> bool {
1143        self.undo_redo.can_undo()
1144    }
1145
1146    /// Can redo
1147    pub fn can_redo(&self) -> bool {
1148        self.undo_redo.can_redo()
1149    }
1150
1151    /// Undo stack depth (counted by undo steps; grouped undo may pop multiple steps at once)
1152    pub fn undo_depth(&self) -> usize {
1153        self.undo_redo.undo_depth()
1154    }
1155
1156    /// Redo stack depth (counted by undo steps)
1157    pub fn redo_depth(&self) -> usize {
1158        self.undo_redo.redo_depth()
1159    }
1160
1161    /// Currently open undo group ID (for insert coalescing only)
1162    pub fn current_change_group(&self) -> Option<usize> {
1163        self.undo_redo.current_group_id()
1164    }
1165
1166    /// Whether current state is at clean point (for dirty tracking)
1167    pub fn is_clean(&self) -> bool {
1168        self.undo_redo.is_clean()
1169    }
1170
1171    /// Mark current state as clean point (call after saving file)
1172    pub fn mark_clean(&mut self) {
1173        self.undo_redo.mark_clean();
1174    }
1175
1176    /// Get a reference to the Editor Core
1177    pub fn editor(&self) -> &EditorCore {
1178        &self.editor
1179    }
1180
1181    /// Get a mutable reference to the Editor Core
1182    pub fn editor_mut(&mut self) -> &mut EditorCore {
1183        &mut self.editor
1184    }
1185
1186    /// Get current tab key behavior used by [`EditCommand::InsertTab`].
1187    pub fn tab_key_behavior(&self) -> TabKeyBehavior {
1188        self.tab_key_behavior
1189    }
1190
1191    /// Set tab key behavior used by [`EditCommand::InsertTab`].
1192    pub fn set_tab_key_behavior(&mut self, behavior: TabKeyBehavior) {
1193        self.tab_key_behavior = behavior;
1194    }
1195
1196    // Private method: execute edit command
1197    fn execute_edit(&mut self, command: EditCommand) -> Result<CommandResult, CommandError> {
1198        match command {
1199            EditCommand::Undo => self.execute_undo_command(),
1200            EditCommand::Redo => self.execute_redo_command(),
1201            EditCommand::EndUndoGroup => {
1202                self.undo_redo.end_group();
1203                Ok(CommandResult::Success)
1204            }
1205            EditCommand::ReplaceCurrent {
1206                query,
1207                replacement,
1208                options,
1209            } => self.execute_replace_current_command(query, replacement, options),
1210            EditCommand::ReplaceAll {
1211                query,
1212                replacement,
1213                options,
1214            } => self.execute_replace_all_command(query, replacement, options),
1215            EditCommand::Backspace => self.execute_backspace_command(),
1216            EditCommand::DeleteForward => self.execute_delete_forward_command(),
1217            EditCommand::InsertText { text } => self.execute_insert_text_command(text),
1218            EditCommand::InsertTab => self.execute_insert_tab_command(),
1219            EditCommand::Insert { offset, text } => self.execute_insert_command(offset, text),
1220            EditCommand::Delete { start, length } => self.execute_delete_command(start, length),
1221            EditCommand::Replace {
1222                start,
1223                length,
1224                text,
1225            } => self.execute_replace_command(start, length, text),
1226        }
1227    }
1228
1229    fn execute_undo_command(&mut self) -> Result<CommandResult, CommandError> {
1230        self.undo_redo.end_group();
1231        if !self.undo_redo.can_undo() {
1232            return Err(CommandError::Other("Nothing to undo".to_string()));
1233        }
1234
1235        let steps = self
1236            .undo_redo
1237            .pop_undo_group()
1238            .ok_or_else(|| CommandError::Other("Nothing to undo".to_string()))?;
1239
1240        for step in &steps {
1241            self.apply_undo_edits(&step.edits)?;
1242            self.restore_selection_set(step.before_selection.clone());
1243        }
1244
1245        // Move steps to redo stack in the same pop order (newest->oldest) so redo pops oldest first.
1246        for step in steps {
1247            self.undo_redo.redo_stack.push(step);
1248        }
1249
1250        Ok(CommandResult::Success)
1251    }
1252
1253    fn execute_redo_command(&mut self) -> Result<CommandResult, CommandError> {
1254        self.undo_redo.end_group();
1255        if !self.undo_redo.can_redo() {
1256            return Err(CommandError::Other("Nothing to redo".to_string()));
1257        }
1258
1259        let steps = self
1260            .undo_redo
1261            .pop_redo_group()
1262            .ok_or_else(|| CommandError::Other("Nothing to redo".to_string()))?;
1263
1264        for step in &steps {
1265            self.apply_redo_edits(&step.edits)?;
1266            self.restore_selection_set(step.after_selection.clone());
1267        }
1268
1269        // Reapplied steps return to undo stack in the same order (oldest->newest).
1270        for step in steps {
1271            self.undo_redo.undo_stack.push(step);
1272        }
1273
1274        Ok(CommandResult::Success)
1275    }
1276
1277    fn execute_insert_text_command(&mut self, text: String) -> Result<CommandResult, CommandError> {
1278        if text.is_empty() {
1279            return Ok(CommandResult::Success);
1280        }
1281
1282        let before_selection = self.snapshot_selection_set();
1283
1284        // Build canonical selection set (primary + secondary), VSCode-like: edits are applied
1285        // "simultaneously" by computing ranges in the original document and mutating in
1286        // descending offset order.
1287        let mut selections: Vec<Selection> =
1288            Vec::with_capacity(1 + self.editor.secondary_selections.len());
1289        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
1290            start: self.editor.cursor_position,
1291            end: self.editor.cursor_position,
1292            direction: SelectionDirection::Forward,
1293        });
1294        selections.push(primary_selection);
1295        selections.extend(self.editor.secondary_selections.iter().cloned());
1296
1297        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
1298
1299        let text_char_len = text.chars().count();
1300
1301        struct Op {
1302            selection_index: usize,
1303            start_offset: usize,
1304            start_after: usize,
1305            delete_len: usize,
1306            deleted_text: String,
1307            insert_text: String,
1308            insert_char_len: usize,
1309        }
1310
1311        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
1312
1313        for (selection_index, selection) in selections.iter().enumerate() {
1314            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
1315                (selection.start, selection.end)
1316            } else {
1317                (selection.end, selection.start)
1318            };
1319
1320            let (start_offset, start_pad) =
1321                self.position_to_char_offset_and_virtual_pad(range_start_pos);
1322            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
1323
1324            let delete_len = end_offset.saturating_sub(start_offset);
1325            let insert_char_len = start_pad + text_char_len;
1326
1327            let deleted_text = if delete_len == 0 {
1328                String::new()
1329            } else {
1330                self.editor.piece_table.get_range(start_offset, delete_len)
1331            };
1332
1333            let mut insert_text = String::with_capacity(text.len() + start_pad);
1334            for _ in 0..start_pad {
1335                insert_text.push(' ');
1336            }
1337            insert_text.push_str(&text);
1338
1339            ops.push(Op {
1340                selection_index,
1341                start_offset,
1342                start_after: start_offset,
1343                delete_len,
1344                deleted_text,
1345                insert_text,
1346                insert_char_len,
1347            });
1348        }
1349
1350        // Compute final caret offsets in the post-edit document (ascending order with delta),
1351        // while also recording each operation's start offset in the post-edit document.
1352        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
1353        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
1354
1355        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
1356        let mut delta: i64 = 0;
1357        for &idx in &asc_indices {
1358            let op = &mut ops[idx];
1359            let effective_start = (op.start_offset as i64 + delta) as usize;
1360            op.start_after = effective_start;
1361            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
1362            delta += op.insert_char_len as i64 - op.delete_len as i64;
1363        }
1364
1365        // Apply edits safely (descending offsets).
1366        let mut desc_indices = asc_indices;
1367        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
1368
1369        for &idx in &desc_indices {
1370            let op = &ops[idx];
1371
1372            if op.delete_len > 0 {
1373                self.editor
1374                    .piece_table
1375                    .delete(op.start_offset, op.delete_len);
1376                self.editor
1377                    .interval_tree
1378                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1379                for layer_tree in self.editor.style_layers.values_mut() {
1380                    layer_tree
1381                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1382                }
1383            }
1384
1385            if !op.insert_text.is_empty() {
1386                self.editor
1387                    .piece_table
1388                    .insert(op.start_offset, &op.insert_text);
1389                self.editor
1390                    .interval_tree
1391                    .update_for_insertion(op.start_offset, op.insert_char_len);
1392                for layer_tree in self.editor.style_layers.values_mut() {
1393                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
1394                }
1395            }
1396        }
1397
1398        // Rebuild derived structures once.
1399        let updated_text = self.editor.piece_table.get_text();
1400        self.editor.line_index = LineIndex::from_text(&updated_text);
1401        self.rebuild_layout_engine_from_text(&updated_text);
1402
1403        // Update selection state: collapse to carets after typing.
1404        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
1405        for offset in &caret_offsets {
1406            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
1407            let pos = Position::new(line, column);
1408            new_carets.push(Selection {
1409                start: pos,
1410                end: pos,
1411                direction: SelectionDirection::Forward,
1412            });
1413        }
1414
1415        let (new_carets, new_primary_index) =
1416            crate::selection_set::normalize_selections(new_carets, primary_index);
1417        let primary = new_carets
1418            .get(new_primary_index)
1419            .cloned()
1420            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
1421
1422        self.editor.cursor_position = primary.end;
1423        self.editor.selection = None;
1424        self.editor.secondary_selections = new_carets
1425            .into_iter()
1426            .enumerate()
1427            .filter_map(|(idx, sel)| {
1428                if idx == new_primary_index {
1429                    None
1430                } else {
1431                    Some(sel)
1432                }
1433            })
1434            .collect();
1435
1436        let after_selection = self.snapshot_selection_set();
1437
1438        let edits: Vec<TextEdit> = ops
1439            .into_iter()
1440            .map(|op| TextEdit {
1441                start_before: op.start_offset,
1442                start_after: op.start_after,
1443                deleted_text: op.deleted_text,
1444                inserted_text: op.insert_text,
1445            })
1446            .collect();
1447
1448        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
1449        let coalescible_insert = is_pure_insert && !text.contains('\n');
1450
1451        let step = UndoStep {
1452            group_id: 0,
1453            edits,
1454            before_selection,
1455            after_selection,
1456        };
1457        self.undo_redo.push_step(step, coalescible_insert);
1458
1459        Ok(CommandResult::Success)
1460    }
1461
1462    fn execute_insert_tab_command(&mut self) -> Result<CommandResult, CommandError> {
1463        let before_selection = self.snapshot_selection_set();
1464
1465        let mut selections: Vec<Selection> =
1466            Vec::with_capacity(1 + self.editor.secondary_selections.len());
1467        let primary_selection = self.editor.selection.clone().unwrap_or(Selection {
1468            start: self.editor.cursor_position,
1469            end: self.editor.cursor_position,
1470            direction: SelectionDirection::Forward,
1471        });
1472        selections.push(primary_selection);
1473        selections.extend(self.editor.secondary_selections.iter().cloned());
1474
1475        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
1476
1477        let tab_width = self.editor.layout_engine.tab_width();
1478
1479        struct Op {
1480            selection_index: usize,
1481            start_offset: usize,
1482            start_after: usize,
1483            delete_len: usize,
1484            deleted_text: String,
1485            insert_text: String,
1486            insert_char_len: usize,
1487        }
1488
1489        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
1490
1491        for (selection_index, selection) in selections.iter().enumerate() {
1492            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
1493                (selection.start, selection.end)
1494            } else {
1495                (selection.end, selection.start)
1496            };
1497
1498            let (start_offset, start_pad) =
1499                self.position_to_char_offset_and_virtual_pad(range_start_pos);
1500            let end_offset = self.position_to_char_offset_clamped(range_end_pos);
1501
1502            let delete_len = end_offset.saturating_sub(start_offset);
1503
1504            let deleted_text = if delete_len == 0 {
1505                String::new()
1506            } else {
1507                self.editor.piece_table.get_range(start_offset, delete_len)
1508            };
1509
1510            // Compute cell X within the logical line at the insertion position (including virtual pad).
1511            let line_text = self
1512                .editor
1513                .line_index
1514                .get_line_text(range_start_pos.line)
1515                .unwrap_or_default();
1516            let line_char_len = line_text.chars().count();
1517            let clamped_col = range_start_pos.column.min(line_char_len);
1518            let x_in_line =
1519                visual_x_for_column(&line_text, clamped_col, tab_width).saturating_add(start_pad);
1520
1521            let mut insert_text = String::new();
1522            for _ in 0..start_pad {
1523                insert_text.push(' ');
1524            }
1525
1526            match self.tab_key_behavior {
1527                TabKeyBehavior::Tab => {
1528                    insert_text.push('\t');
1529                    ops.push(Op {
1530                        selection_index,
1531                        start_offset,
1532                        start_after: start_offset,
1533                        delete_len,
1534                        deleted_text,
1535                        insert_text,
1536                        insert_char_len: start_pad + 1,
1537                    });
1538                }
1539                TabKeyBehavior::Spaces => {
1540                    let tab_width = tab_width.max(1);
1541                    let rem = x_in_line % tab_width;
1542                    let spaces = tab_width - rem;
1543                    for _ in 0..spaces {
1544                        insert_text.push(' ');
1545                    }
1546
1547                    ops.push(Op {
1548                        selection_index,
1549                        start_offset,
1550                        start_after: start_offset,
1551                        delete_len,
1552                        deleted_text,
1553                        insert_text,
1554                        insert_char_len: start_pad + spaces,
1555                    });
1556                }
1557            }
1558        }
1559
1560        // Compute final caret offsets in the post-edit document (ascending order with delta),
1561        // while also recording each operation's start offset in the post-edit document.
1562        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
1563        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
1564
1565        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
1566        let mut delta: i64 = 0;
1567        for &idx in &asc_indices {
1568            let op = &mut ops[idx];
1569            let effective_start = (op.start_offset as i64 + delta) as usize;
1570            op.start_after = effective_start;
1571            caret_offsets[op.selection_index] = effective_start + op.insert_char_len;
1572            delta += op.insert_char_len as i64 - op.delete_len as i64;
1573        }
1574
1575        // Apply edits safely (descending offsets).
1576        let mut desc_indices = asc_indices;
1577        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
1578
1579        for &idx in &desc_indices {
1580            let op = &ops[idx];
1581
1582            if op.delete_len > 0 {
1583                self.editor
1584                    .piece_table
1585                    .delete(op.start_offset, op.delete_len);
1586                self.editor
1587                    .interval_tree
1588                    .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1589                for layer_tree in self.editor.style_layers.values_mut() {
1590                    layer_tree
1591                        .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
1592                }
1593            }
1594
1595            if !op.insert_text.is_empty() {
1596                self.editor
1597                    .piece_table
1598                    .insert(op.start_offset, &op.insert_text);
1599                self.editor
1600                    .interval_tree
1601                    .update_for_insertion(op.start_offset, op.insert_char_len);
1602                for layer_tree in self.editor.style_layers.values_mut() {
1603                    layer_tree.update_for_insertion(op.start_offset, op.insert_char_len);
1604                }
1605            }
1606        }
1607
1608        // Rebuild derived structures once.
1609        let updated_text = self.editor.piece_table.get_text();
1610        self.editor.line_index = LineIndex::from_text(&updated_text);
1611        self.rebuild_layout_engine_from_text(&updated_text);
1612
1613        // Update selection state: collapse to carets after insertion.
1614        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
1615        for offset in &caret_offsets {
1616            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
1617            let pos = Position::new(line, column);
1618            new_carets.push(Selection {
1619                start: pos,
1620                end: pos,
1621                direction: SelectionDirection::Forward,
1622            });
1623        }
1624
1625        let (new_carets, new_primary_index) =
1626            crate::selection_set::normalize_selections(new_carets, primary_index);
1627        let primary = new_carets
1628            .get(new_primary_index)
1629            .cloned()
1630            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
1631
1632        self.editor.cursor_position = primary.end;
1633        self.editor.selection = None;
1634        self.editor.secondary_selections = new_carets
1635            .into_iter()
1636            .enumerate()
1637            .filter_map(|(idx, sel)| {
1638                if idx == new_primary_index {
1639                    None
1640                } else {
1641                    Some(sel)
1642                }
1643            })
1644            .collect();
1645
1646        let after_selection = self.snapshot_selection_set();
1647
1648        let edits: Vec<TextEdit> = ops
1649            .into_iter()
1650            .map(|op| TextEdit {
1651                start_before: op.start_offset,
1652                start_after: op.start_after,
1653                deleted_text: op.deleted_text,
1654                inserted_text: op.insert_text,
1655            })
1656            .collect();
1657
1658        let is_pure_insert = edits.iter().all(|e| e.deleted_text.is_empty());
1659        let coalescible_insert = is_pure_insert;
1660
1661        let step = UndoStep {
1662            group_id: 0,
1663            edits,
1664            before_selection,
1665            after_selection,
1666        };
1667        self.undo_redo.push_step(step, coalescible_insert);
1668
1669        Ok(CommandResult::Success)
1670    }
1671
1672    fn execute_insert_command(
1673        &mut self,
1674        offset: usize,
1675        text: String,
1676    ) -> Result<CommandResult, CommandError> {
1677        if text.is_empty() {
1678            return Err(CommandError::EmptyText);
1679        }
1680
1681        let max_offset = self.editor.piece_table.char_count();
1682        if offset > max_offset {
1683            return Err(CommandError::InvalidOffset(offset));
1684        }
1685
1686        let before_selection = self.snapshot_selection_set();
1687
1688        let affected_line = self.editor.line_index.char_offset_to_position(offset).0;
1689        let inserts_newline = text.contains('\n');
1690
1691        // Execute insertion
1692        self.editor.piece_table.insert(offset, &text);
1693
1694        // Update line index
1695        let updated_text = self.editor.piece_table.get_text();
1696        self.editor.line_index = LineIndex::from_text(&updated_text);
1697
1698        // Update layout engine (soft wrappingneeds to stay consistent with text)
1699        if inserts_newline {
1700            self.rebuild_layout_engine_from_text(&updated_text);
1701        } else {
1702            let line_text = self
1703                .editor
1704                .line_index
1705                .get_line_text(affected_line)
1706                .unwrap_or_default();
1707            self.editor
1708                .layout_engine
1709                .update_line(affected_line, &line_text);
1710        }
1711
1712        let inserted_len = text.chars().count();
1713
1714        // Update interval tree offsets
1715        self.editor
1716            .interval_tree
1717            .update_for_insertion(offset, inserted_len);
1718        for layer_tree in self.editor.style_layers.values_mut() {
1719            layer_tree.update_for_insertion(offset, inserted_len);
1720        }
1721
1722        // Ensure cursor/selection still within valid range
1723        self.normalize_cursor_and_selection();
1724
1725        let after_selection = self.snapshot_selection_set();
1726
1727        let step = UndoStep {
1728            group_id: 0,
1729            edits: vec![TextEdit {
1730                start_before: offset,
1731                start_after: offset,
1732                deleted_text: String::new(),
1733                inserted_text: text.clone(),
1734            }],
1735            before_selection,
1736            after_selection,
1737        };
1738
1739        let coalescible_insert = !text.contains('\n');
1740        self.undo_redo.push_step(step, coalescible_insert);
1741
1742        Ok(CommandResult::Success)
1743    }
1744
1745    fn execute_delete_command(
1746        &mut self,
1747        start: usize,
1748        length: usize,
1749    ) -> Result<CommandResult, CommandError> {
1750        if length == 0 {
1751            return Ok(CommandResult::Success);
1752        }
1753
1754        let max_offset = self.editor.piece_table.char_count();
1755        if start > max_offset {
1756            return Err(CommandError::InvalidOffset(start));
1757        }
1758        if start + length > max_offset {
1759            return Err(CommandError::InvalidRange {
1760                start,
1761                end: start + length,
1762            });
1763        }
1764
1765        let before_selection = self.snapshot_selection_set();
1766
1767        let deleted_text = self.editor.piece_table.get_range(start, length);
1768        let deletes_newline = deleted_text.contains('\n');
1769        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
1770
1771        // Execute deletion
1772        self.editor.piece_table.delete(start, length);
1773
1774        // Update line index
1775        let updated_text = self.editor.piece_table.get_text();
1776        self.editor.line_index = LineIndex::from_text(&updated_text);
1777
1778        // Update layout engine (soft wrappingneeds to stay consistent with text)
1779        if deletes_newline {
1780            self.rebuild_layout_engine_from_text(&updated_text);
1781        } else {
1782            let line_text = self
1783                .editor
1784                .line_index
1785                .get_line_text(affected_line)
1786                .unwrap_or_default();
1787            self.editor
1788                .layout_engine
1789                .update_line(affected_line, &line_text);
1790        }
1791
1792        // Update interval tree offsets
1793        self.editor
1794            .interval_tree
1795            .update_for_deletion(start, start + length);
1796        for layer_tree in self.editor.style_layers.values_mut() {
1797            layer_tree.update_for_deletion(start, start + length);
1798        }
1799
1800        // Ensure cursor/selection still within valid range
1801        self.normalize_cursor_and_selection();
1802
1803        let after_selection = self.snapshot_selection_set();
1804
1805        let step = UndoStep {
1806            group_id: 0,
1807            edits: vec![TextEdit {
1808                start_before: start,
1809                start_after: start,
1810                deleted_text,
1811                inserted_text: String::new(),
1812            }],
1813            before_selection,
1814            after_selection,
1815        };
1816        self.undo_redo.push_step(step, false);
1817
1818        Ok(CommandResult::Success)
1819    }
1820
1821    fn execute_replace_command(
1822        &mut self,
1823        start: usize,
1824        length: usize,
1825        text: String,
1826    ) -> Result<CommandResult, CommandError> {
1827        let max_offset = self.editor.piece_table.char_count();
1828        if start > max_offset {
1829            return Err(CommandError::InvalidOffset(start));
1830        }
1831        if start + length > max_offset {
1832            return Err(CommandError::InvalidRange {
1833                start,
1834                end: start + length,
1835            });
1836        }
1837
1838        if length == 0 && text.is_empty() {
1839            return Ok(CommandResult::Success);
1840        }
1841
1842        let before_selection = self.snapshot_selection_set();
1843
1844        let deleted_text = if length == 0 {
1845            String::new()
1846        } else {
1847            self.editor.piece_table.get_range(start, length)
1848        };
1849
1850        let affected_line = self.editor.line_index.char_offset_to_position(start).0;
1851        let replace_affects_layout = deleted_text.contains('\n') || text.contains('\n');
1852
1853        // Apply as a single operation (delete then insert at the same offset).
1854        if length > 0 {
1855            self.editor.piece_table.delete(start, length);
1856            self.editor
1857                .interval_tree
1858                .update_for_deletion(start, start + length);
1859            for layer_tree in self.editor.style_layers.values_mut() {
1860                layer_tree.update_for_deletion(start, start + length);
1861            }
1862        }
1863
1864        let inserted_len = text.chars().count();
1865        if inserted_len > 0 {
1866            self.editor.piece_table.insert(start, &text);
1867            self.editor
1868                .interval_tree
1869                .update_for_insertion(start, inserted_len);
1870            for layer_tree in self.editor.style_layers.values_mut() {
1871                layer_tree.update_for_insertion(start, inserted_len);
1872            }
1873        }
1874
1875        // Rebuild derived structures.
1876        let updated_text = self.editor.piece_table.get_text();
1877        self.editor.line_index = LineIndex::from_text(&updated_text);
1878
1879        if replace_affects_layout {
1880            self.rebuild_layout_engine_from_text(&updated_text);
1881        } else {
1882            let line_text = self
1883                .editor
1884                .line_index
1885                .get_line_text(affected_line)
1886                .unwrap_or_default();
1887            self.editor
1888                .layout_engine
1889                .update_line(affected_line, &line_text);
1890        }
1891
1892        // Ensure cursor/selection still valid.
1893        self.normalize_cursor_and_selection();
1894
1895        let after_selection = self.snapshot_selection_set();
1896
1897        let step = UndoStep {
1898            group_id: 0,
1899            edits: vec![TextEdit {
1900                start_before: start,
1901                start_after: start,
1902                deleted_text,
1903                inserted_text: text,
1904            }],
1905            before_selection,
1906            after_selection,
1907        };
1908        self.undo_redo.push_step(step, false);
1909
1910        Ok(CommandResult::Success)
1911    }
1912
1913    fn cursor_char_offset(&self) -> usize {
1914        self.position_to_char_offset_clamped(self.editor.cursor_position)
1915    }
1916
1917    fn primary_selection_char_range(&self) -> Option<SearchMatch> {
1918        let selection = self.editor.selection.as_ref()?;
1919        let (min_pos, max_pos) = crate::selection_set::selection_min_max(selection);
1920        let start = self.position_to_char_offset_clamped(min_pos);
1921        let end = self.position_to_char_offset_clamped(max_pos);
1922        if start == end {
1923            None
1924        } else {
1925            Some(SearchMatch { start, end })
1926        }
1927    }
1928
1929    fn set_primary_selection_by_char_range(&mut self, range: SearchMatch) {
1930        let (start_line, start_col) = self.editor.line_index.char_offset_to_position(range.start);
1931        let (end_line, end_col) = self.editor.line_index.char_offset_to_position(range.end);
1932
1933        self.editor.cursor_position = Position::new(end_line, end_col);
1934        self.editor.secondary_selections.clear();
1935
1936        if range.start == range.end {
1937            self.editor.selection = None;
1938        } else {
1939            self.editor.selection = Some(Selection {
1940                start: Position::new(start_line, start_col),
1941                end: Position::new(end_line, end_col),
1942                direction: SelectionDirection::Forward,
1943            });
1944        }
1945    }
1946
1947    fn execute_find_command(
1948        &mut self,
1949        query: String,
1950        options: SearchOptions,
1951        forward: bool,
1952    ) -> Result<CommandResult, CommandError> {
1953        if query.is_empty() {
1954            return Ok(CommandResult::SearchNotFound);
1955        }
1956
1957        let text = self.editor.piece_table.get_text();
1958        let from = if let Some(selection) = self.primary_selection_char_range() {
1959            if forward {
1960                selection.end
1961            } else {
1962                selection.start
1963            }
1964        } else {
1965            self.cursor_char_offset()
1966        };
1967
1968        let found = if forward {
1969            find_next(&text, &query, options, from)
1970        } else {
1971            find_prev(&text, &query, options, from)
1972        }
1973        .map_err(|err| CommandError::Other(err.to_string()))?;
1974
1975        let Some(m) = found else {
1976            return Ok(CommandResult::SearchNotFound);
1977        };
1978
1979        self.set_primary_selection_by_char_range(m);
1980
1981        Ok(CommandResult::SearchMatch {
1982            start: m.start,
1983            end: m.end,
1984        })
1985    }
1986
1987    fn compile_user_regex(
1988        query: &str,
1989        options: SearchOptions,
1990    ) -> Result<regex::Regex, CommandError> {
1991        RegexBuilder::new(query)
1992            .case_insensitive(!options.case_sensitive)
1993            .multi_line(true)
1994            .build()
1995            .map_err(|err| CommandError::Other(format!("Invalid regex: {}", err)))
1996    }
1997
1998    fn regex_expand_replacement(
1999        re: &regex::Regex,
2000        text: &str,
2001        index: &CharIndex,
2002        range: SearchMatch,
2003        replacement: &str,
2004    ) -> Result<String, CommandError> {
2005        let start_byte = index.char_to_byte(range.start);
2006        let end_byte = index.char_to_byte(range.end);
2007
2008        let caps = re
2009            .captures_at(text, start_byte)
2010            .ok_or_else(|| CommandError::Other("Regex match not found".to_string()))?;
2011        let whole = caps
2012            .get(0)
2013            .ok_or_else(|| CommandError::Other("Regex match missing capture 0".to_string()))?;
2014        if whole.start() != start_byte || whole.end() != end_byte {
2015            return Err(CommandError::Other(
2016                "Regex match did not align with the selected range".to_string(),
2017            ));
2018        }
2019
2020        let mut expanded = String::new();
2021        caps.expand(replacement, &mut expanded);
2022        Ok(expanded)
2023    }
2024
2025    fn execute_replace_current_command(
2026        &mut self,
2027        query: String,
2028        replacement: String,
2029        options: SearchOptions,
2030    ) -> Result<CommandResult, CommandError> {
2031        if query.is_empty() {
2032            return Err(CommandError::Other("Search query is empty".to_string()));
2033        }
2034
2035        let text = self.editor.piece_table.get_text();
2036        let selection_range = self.primary_selection_char_range();
2037
2038        let mut target = None::<SearchMatch>;
2039        if let Some(range) = selection_range {
2040            let is_match = crate::search::is_match_exact(&text, &query, options, range)
2041                .map_err(|err| CommandError::Other(err.to_string()))?;
2042            if is_match {
2043                target = Some(range);
2044            }
2045        }
2046
2047        if target.is_none() {
2048            let from = self.cursor_char_offset();
2049            target = find_next(&text, &query, options, from)
2050                .map_err(|err| CommandError::Other(err.to_string()))?;
2051        }
2052
2053        let Some(target) = target else {
2054            return Err(CommandError::Other("No match found".to_string()));
2055        };
2056
2057        let index = CharIndex::new(&text);
2058        let inserted_text = if options.regex {
2059            let re = Self::compile_user_regex(&query, options)?;
2060            Self::regex_expand_replacement(&re, &text, &index, target, &replacement)?
2061        } else {
2062            replacement
2063        };
2064
2065        let deleted_text = self
2066            .editor
2067            .piece_table
2068            .get_range(target.start, target.len());
2069
2070        let before_selection = self.snapshot_selection_set();
2071        self.apply_text_ops(vec![(target.start, target.len(), inserted_text.as_str())])?;
2072
2073        let inserted_len = inserted_text.chars().count();
2074        let new_range = SearchMatch {
2075            start: target.start,
2076            end: target.start + inserted_len,
2077        };
2078        self.set_primary_selection_by_char_range(new_range);
2079        let after_selection = self.snapshot_selection_set();
2080
2081        let step = UndoStep {
2082            group_id: 0,
2083            edits: vec![TextEdit {
2084                start_before: target.start,
2085                start_after: target.start,
2086                deleted_text,
2087                inserted_text: inserted_text.clone(),
2088            }],
2089            before_selection,
2090            after_selection,
2091        };
2092        self.undo_redo.push_step(step, false);
2093
2094        Ok(CommandResult::ReplaceResult { replaced: 1 })
2095    }
2096
2097    fn execute_replace_all_command(
2098        &mut self,
2099        query: String,
2100        replacement: String,
2101        options: SearchOptions,
2102    ) -> Result<CommandResult, CommandError> {
2103        if query.is_empty() {
2104            return Err(CommandError::Other("Search query is empty".to_string()));
2105        }
2106
2107        let text = self.editor.piece_table.get_text();
2108        let matches =
2109            find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
2110        if matches.is_empty() {
2111            return Err(CommandError::Other("No match found".to_string()));
2112        }
2113        let match_count = matches.len();
2114
2115        let index = CharIndex::new(&text);
2116
2117        struct Op {
2118            start_before: usize,
2119            start_after: usize,
2120            delete_len: usize,
2121            deleted_text: String,
2122            inserted_text: String,
2123            inserted_len: usize,
2124        }
2125
2126        let mut ops: Vec<Op> = Vec::with_capacity(match_count);
2127        if options.regex {
2128            let re = Self::compile_user_regex(&query, options)?;
2129            for m in matches {
2130                let deleted_text = {
2131                    let start_byte = index.char_to_byte(m.start);
2132                    let end_byte = index.char_to_byte(m.end);
2133                    text.get(start_byte..end_byte)
2134                        .unwrap_or_default()
2135                        .to_string()
2136                };
2137                let inserted_text =
2138                    Self::regex_expand_replacement(&re, &text, &index, m, &replacement)?;
2139                let inserted_len = inserted_text.chars().count();
2140                ops.push(Op {
2141                    start_before: m.start,
2142                    start_after: m.start,
2143                    delete_len: m.len(),
2144                    deleted_text,
2145                    inserted_text,
2146                    inserted_len,
2147                });
2148            }
2149        } else {
2150            let inserted_len = replacement.chars().count();
2151            for m in matches {
2152                let deleted_text = {
2153                    let start_byte = index.char_to_byte(m.start);
2154                    let end_byte = index.char_to_byte(m.end);
2155                    text.get(start_byte..end_byte)
2156                        .unwrap_or_default()
2157                        .to_string()
2158                };
2159                ops.push(Op {
2160                    start_before: m.start,
2161                    start_after: m.start,
2162                    delete_len: m.len(),
2163                    deleted_text,
2164                    inserted_text: replacement.clone(),
2165                    inserted_len,
2166                });
2167            }
2168        }
2169
2170        ops.sort_by_key(|op| op.start_before);
2171
2172        let mut delta: i64 = 0;
2173        for op in &mut ops {
2174            let effective_start = op.start_before as i64 + delta;
2175            if effective_start < 0 {
2176                return Err(CommandError::Other(
2177                    "ReplaceAll produced an invalid intermediate offset".to_string(),
2178                ));
2179            }
2180            op.start_after = effective_start as usize;
2181            delta += op.inserted_len as i64 - op.delete_len as i64;
2182        }
2183
2184        let before_selection = self.snapshot_selection_set();
2185        let apply_ops: Vec<(usize, usize, &str)> = ops
2186            .iter()
2187            .map(|op| (op.start_before, op.delete_len, op.inserted_text.as_str()))
2188            .collect();
2189        self.apply_text_ops(apply_ops)?;
2190
2191        if let Some(first) = ops.first() {
2192            let caret_end = first.start_after + first.inserted_len;
2193            let select_end = if first.inserted_len == 0 {
2194                first.start_after
2195            } else {
2196                caret_end
2197            };
2198            self.set_primary_selection_by_char_range(SearchMatch {
2199                start: first.start_after,
2200                end: select_end,
2201            });
2202        } else {
2203            self.editor.selection = None;
2204            self.editor.secondary_selections.clear();
2205        }
2206
2207        let after_selection = self.snapshot_selection_set();
2208
2209        let edits: Vec<TextEdit> = ops
2210            .into_iter()
2211            .map(|op| TextEdit {
2212                start_before: op.start_before,
2213                start_after: op.start_after,
2214                deleted_text: op.deleted_text,
2215                inserted_text: op.inserted_text,
2216            })
2217            .collect();
2218
2219        let step = UndoStep {
2220            group_id: 0,
2221            edits,
2222            before_selection,
2223            after_selection,
2224        };
2225        self.undo_redo.push_step(step, false);
2226
2227        Ok(CommandResult::ReplaceResult {
2228            replaced: match_count,
2229        })
2230    }
2231
2232    fn execute_backspace_command(&mut self) -> Result<CommandResult, CommandError> {
2233        self.execute_delete_like_command(false)
2234    }
2235
2236    fn execute_delete_forward_command(&mut self) -> Result<CommandResult, CommandError> {
2237        self.execute_delete_like_command(true)
2238    }
2239
2240    fn execute_delete_like_command(
2241        &mut self,
2242        forward: bool,
2243    ) -> Result<CommandResult, CommandError> {
2244        // Any delete-like action should end an open insert coalescing group, even if it turns out
2245        // to be a no-op (e.g. backspace at the beginning of the document).
2246        self.undo_redo.end_group();
2247
2248        let before_selection = self.snapshot_selection_set();
2249        let selections = before_selection.selections.clone();
2250        let primary_index = before_selection.primary_index;
2251
2252        let doc_char_count = self.editor.piece_table.char_count();
2253
2254        #[derive(Debug)]
2255        struct Op {
2256            selection_index: usize,
2257            start_offset: usize,
2258            delete_len: usize,
2259            deleted_text: String,
2260            start_after: usize,
2261        }
2262
2263        let mut ops: Vec<Op> = Vec::with_capacity(selections.len());
2264
2265        for (selection_index, selection) in selections.iter().enumerate() {
2266            let (range_start_pos, range_end_pos) = if selection.start <= selection.end {
2267                (selection.start, selection.end)
2268            } else {
2269                (selection.end, selection.start)
2270            };
2271
2272            let (start_offset, end_offset) = if range_start_pos != range_end_pos {
2273                let start_offset = self.position_to_char_offset_clamped(range_start_pos);
2274                let end_offset = self.position_to_char_offset_clamped(range_end_pos);
2275                if start_offset <= end_offset {
2276                    (start_offset, end_offset)
2277                } else {
2278                    (end_offset, start_offset)
2279                }
2280            } else {
2281                let caret_offset = self.position_to_char_offset_clamped(selection.end);
2282                if forward {
2283                    if caret_offset >= doc_char_count {
2284                        (caret_offset, caret_offset)
2285                    } else {
2286                        (caret_offset, (caret_offset + 1).min(doc_char_count))
2287                    }
2288                } else if caret_offset == 0 {
2289                    (0, 0)
2290                } else {
2291                    (caret_offset - 1, caret_offset)
2292                }
2293            };
2294
2295            let delete_len = end_offset.saturating_sub(start_offset);
2296            let deleted_text = if delete_len == 0 {
2297                String::new()
2298            } else {
2299                self.editor.piece_table.get_range(start_offset, delete_len)
2300            };
2301
2302            ops.push(Op {
2303                selection_index,
2304                start_offset,
2305                delete_len,
2306                deleted_text,
2307                start_after: start_offset,
2308            });
2309        }
2310
2311        if !ops.iter().any(|op| op.delete_len > 0) {
2312            return Ok(CommandResult::Success);
2313        }
2314
2315        // Compute caret offsets in the post-delete document (ascending order with delta).
2316        let mut asc_indices: Vec<usize> = (0..ops.len()).collect();
2317        asc_indices.sort_by_key(|&idx| ops[idx].start_offset);
2318
2319        let mut caret_offsets: Vec<usize> = vec![0; ops.len()];
2320        let mut delta: i64 = 0;
2321        for &idx in &asc_indices {
2322            let op = &mut ops[idx];
2323            let effective_start = (op.start_offset as i64 + delta) as usize;
2324            op.start_after = effective_start;
2325            caret_offsets[op.selection_index] = effective_start;
2326            delta -= op.delete_len as i64;
2327        }
2328
2329        // Apply deletes descending to keep offsets valid.
2330        let mut desc_indices = asc_indices;
2331        desc_indices.sort_by_key(|&idx| std::cmp::Reverse(ops[idx].start_offset));
2332
2333        for &idx in &desc_indices {
2334            let op = &ops[idx];
2335            if op.delete_len == 0 {
2336                continue;
2337            }
2338
2339            self.editor
2340                .piece_table
2341                .delete(op.start_offset, op.delete_len);
2342            self.editor
2343                .interval_tree
2344                .update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2345            for layer_tree in self.editor.style_layers.values_mut() {
2346                layer_tree.update_for_deletion(op.start_offset, op.start_offset + op.delete_len);
2347            }
2348        }
2349
2350        // Rebuild derived structures once.
2351        let updated_text = self.editor.piece_table.get_text();
2352        self.editor.line_index = LineIndex::from_text(&updated_text);
2353        self.rebuild_layout_engine_from_text(&updated_text);
2354
2355        // Collapse selection state to carets at the start of deleted ranges.
2356        let mut new_carets: Vec<Selection> = Vec::with_capacity(caret_offsets.len());
2357        for offset in &caret_offsets {
2358            let (line, column) = self.editor.line_index.char_offset_to_position(*offset);
2359            let pos = Position::new(line, column);
2360            new_carets.push(Selection {
2361                start: pos,
2362                end: pos,
2363                direction: SelectionDirection::Forward,
2364            });
2365        }
2366
2367        let (new_carets, new_primary_index) =
2368            crate::selection_set::normalize_selections(new_carets, primary_index);
2369        let primary = new_carets
2370            .get(new_primary_index)
2371            .cloned()
2372            .ok_or_else(|| CommandError::Other("Invalid primary caret".to_string()))?;
2373
2374        self.editor.cursor_position = primary.end;
2375        self.editor.selection = None;
2376        self.editor.secondary_selections = new_carets
2377            .into_iter()
2378            .enumerate()
2379            .filter_map(|(idx, sel)| {
2380                if idx == new_primary_index {
2381                    None
2382                } else {
2383                    Some(sel)
2384                }
2385            })
2386            .collect();
2387
2388        let after_selection = self.snapshot_selection_set();
2389
2390        let edits: Vec<TextEdit> = ops
2391            .into_iter()
2392            .map(|op| TextEdit {
2393                start_before: op.start_offset,
2394                start_after: op.start_after,
2395                deleted_text: op.deleted_text,
2396                inserted_text: String::new(),
2397            })
2398            .collect();
2399
2400        let step = UndoStep {
2401            group_id: 0,
2402            edits,
2403            before_selection,
2404            after_selection,
2405        };
2406        self.undo_redo.push_step(step, false);
2407
2408        Ok(CommandResult::Success)
2409    }
2410
2411    fn snapshot_selection_set(&self) -> SelectionSetSnapshot {
2412        let mut selections: Vec<Selection> =
2413            Vec::with_capacity(1 + self.editor.secondary_selections.len());
2414
2415        let primary = self.editor.selection.clone().unwrap_or(Selection {
2416            start: self.editor.cursor_position,
2417            end: self.editor.cursor_position,
2418            direction: SelectionDirection::Forward,
2419        });
2420        selections.push(primary);
2421        selections.extend(self.editor.secondary_selections.iter().cloned());
2422
2423        let (selections, primary_index) = crate::selection_set::normalize_selections(selections, 0);
2424        SelectionSetSnapshot {
2425            selections,
2426            primary_index,
2427        }
2428    }
2429
2430    fn restore_selection_set(&mut self, snapshot: SelectionSetSnapshot) {
2431        if snapshot.selections.is_empty() {
2432            self.editor.cursor_position = Position::new(0, 0);
2433            self.editor.selection = None;
2434            self.editor.secondary_selections.clear();
2435            return;
2436        }
2437
2438        let primary = snapshot
2439            .selections
2440            .get(snapshot.primary_index)
2441            .cloned()
2442            .unwrap_or_else(|| snapshot.selections[0].clone());
2443
2444        self.editor.cursor_position = primary.end;
2445        self.editor.selection = if primary.start == primary.end {
2446            None
2447        } else {
2448            Some(primary.clone())
2449        };
2450
2451        self.editor.secondary_selections = snapshot
2452            .selections
2453            .into_iter()
2454            .enumerate()
2455            .filter_map(|(idx, sel)| {
2456                if idx == snapshot.primary_index {
2457                    None
2458                } else {
2459                    Some(sel)
2460                }
2461            })
2462            .collect();
2463
2464        self.normalize_cursor_and_selection();
2465    }
2466
2467    fn apply_undo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
2468        // Apply inverse: delete inserted text, then reinsert deleted text.
2469        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
2470        for edit in edits {
2471            let start = edit.start_after;
2472            let delete_len = edit.inserted_len();
2473            let insert_text = edit.deleted_text.as_str();
2474            ops.push((start, delete_len, insert_text));
2475        }
2476        self.apply_text_ops(ops)
2477    }
2478
2479    fn apply_redo_edits(&mut self, edits: &[TextEdit]) -> Result<(), CommandError> {
2480        let mut ops: Vec<(usize, usize, &str)> = Vec::with_capacity(edits.len());
2481        for edit in edits {
2482            let start = edit.start_before;
2483            let delete_len = edit.deleted_len();
2484            let insert_text = edit.inserted_text.as_str();
2485            ops.push((start, delete_len, insert_text));
2486        }
2487        self.apply_text_ops(ops)
2488    }
2489
2490    fn apply_text_ops(&mut self, mut ops: Vec<(usize, usize, &str)>) -> Result<(), CommandError> {
2491        // Sort descending by start offset to make offsets stable while mutating.
2492        ops.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
2493
2494        for (start, delete_len, insert_text) in ops {
2495            let max_offset = self.editor.piece_table.char_count();
2496            if start > max_offset {
2497                return Err(CommandError::InvalidOffset(start));
2498            }
2499            if start + delete_len > max_offset {
2500                return Err(CommandError::InvalidRange {
2501                    start,
2502                    end: start + delete_len,
2503                });
2504            }
2505
2506            if delete_len > 0 {
2507                self.editor.piece_table.delete(start, delete_len);
2508                self.editor
2509                    .interval_tree
2510                    .update_for_deletion(start, start + delete_len);
2511                for layer_tree in self.editor.style_layers.values_mut() {
2512                    layer_tree.update_for_deletion(start, start + delete_len);
2513                }
2514            }
2515
2516            let insert_len = insert_text.chars().count();
2517            if insert_len > 0 {
2518                self.editor.piece_table.insert(start, insert_text);
2519                self.editor
2520                    .interval_tree
2521                    .update_for_insertion(start, insert_len);
2522                for layer_tree in self.editor.style_layers.values_mut() {
2523                    layer_tree.update_for_insertion(start, insert_len);
2524                }
2525            }
2526        }
2527
2528        // Rebuild derived structures.
2529        let updated_text = self.editor.piece_table.get_text();
2530        self.editor.line_index = LineIndex::from_text(&updated_text);
2531        self.rebuild_layout_engine_from_text(&updated_text);
2532        self.normalize_cursor_and_selection();
2533
2534        Ok(())
2535    }
2536
2537    // Private method: execute cursor command
2538    fn execute_cursor(&mut self, command: CursorCommand) -> Result<CommandResult, CommandError> {
2539        match command {
2540            CursorCommand::MoveTo { line, column } => {
2541                if line >= self.editor.line_index.line_count() {
2542                    return Err(CommandError::InvalidPosition { line, column });
2543                }
2544
2545                let clamped_column = self.clamp_column_for_line(line, column);
2546                self.editor.cursor_position = Position::new(line, clamped_column);
2547                // VSCode-like: moving the primary caret to an absolute position collapses multi-cursor.
2548                self.editor.secondary_selections.clear();
2549                Ok(CommandResult::Success)
2550            }
2551            CursorCommand::MoveBy {
2552                delta_line,
2553                delta_column,
2554            } => {
2555                let new_line = if delta_line >= 0 {
2556                    self.editor.cursor_position.line + delta_line as usize
2557                } else {
2558                    self.editor
2559                        .cursor_position
2560                        .line
2561                        .saturating_sub((-delta_line) as usize)
2562                };
2563
2564                let new_column = if delta_column >= 0 {
2565                    self.editor.cursor_position.column + delta_column as usize
2566                } else {
2567                    self.editor
2568                        .cursor_position
2569                        .column
2570                        .saturating_sub((-delta_column) as usize)
2571                };
2572
2573                if new_line >= self.editor.line_index.line_count() {
2574                    return Err(CommandError::InvalidPosition {
2575                        line: new_line,
2576                        column: new_column,
2577                    });
2578                }
2579
2580                let clamped_column = self.clamp_column_for_line(new_line, new_column);
2581                self.editor.cursor_position = Position::new(new_line, clamped_column);
2582                Ok(CommandResult::Success)
2583            }
2584            CursorCommand::SetSelection { start, end } => {
2585                if start.line >= self.editor.line_index.line_count()
2586                    || end.line >= self.editor.line_index.line_count()
2587                {
2588                    return Err(CommandError::InvalidPosition {
2589                        line: start.line.max(end.line),
2590                        column: start.column.max(end.column),
2591                    });
2592                }
2593
2594                let start = Position::new(
2595                    start.line,
2596                    self.clamp_column_for_line(start.line, start.column),
2597                );
2598                let end = Position::new(end.line, self.clamp_column_for_line(end.line, end.column));
2599
2600                let direction = if start.line < end.line
2601                    || (start.line == end.line && start.column <= end.column)
2602                {
2603                    SelectionDirection::Forward
2604                } else {
2605                    SelectionDirection::Backward
2606                };
2607
2608                self.editor.selection = Some(Selection {
2609                    start,
2610                    end,
2611                    direction,
2612                });
2613                Ok(CommandResult::Success)
2614            }
2615            CursorCommand::ExtendSelection { to } => {
2616                if to.line >= self.editor.line_index.line_count() {
2617                    return Err(CommandError::InvalidPosition {
2618                        line: to.line,
2619                        column: to.column,
2620                    });
2621                }
2622
2623                let to = Position::new(to.line, self.clamp_column_for_line(to.line, to.column));
2624
2625                if let Some(ref mut selection) = self.editor.selection {
2626                    selection.end = to;
2627                    selection.direction = if selection.start.line < to.line
2628                        || (selection.start.line == to.line && selection.start.column <= to.column)
2629                    {
2630                        SelectionDirection::Forward
2631                    } else {
2632                        SelectionDirection::Backward
2633                    };
2634                } else {
2635                    // If no selection, create selection from current cursor
2636                    self.editor.selection = Some(Selection {
2637                        start: self.editor.cursor_position,
2638                        end: to,
2639                        direction: if self.editor.cursor_position.line < to.line
2640                            || (self.editor.cursor_position.line == to.line
2641                                && self.editor.cursor_position.column <= to.column)
2642                        {
2643                            SelectionDirection::Forward
2644                        } else {
2645                            SelectionDirection::Backward
2646                        },
2647                    });
2648                }
2649                Ok(CommandResult::Success)
2650            }
2651            CursorCommand::ClearSelection => {
2652                self.editor.selection = None;
2653                Ok(CommandResult::Success)
2654            }
2655            CursorCommand::SetSelections {
2656                selections,
2657                primary_index,
2658            } => {
2659                let line_count = self.editor.line_index.line_count();
2660                if selections.is_empty() {
2661                    return Err(CommandError::Other(
2662                        "SetSelections requires a non-empty selection list".to_string(),
2663                    ));
2664                }
2665                if primary_index >= selections.len() {
2666                    return Err(CommandError::Other(format!(
2667                        "Invalid primary_index {} for {} selections",
2668                        primary_index,
2669                        selections.len()
2670                    )));
2671                }
2672
2673                for sel in &selections {
2674                    if sel.start.line >= line_count || sel.end.line >= line_count {
2675                        return Err(CommandError::InvalidPosition {
2676                            line: sel.start.line.max(sel.end.line),
2677                            column: sel.start.column.max(sel.end.column),
2678                        });
2679                    }
2680                }
2681
2682                let (selections, primary_index) =
2683                    crate::selection_set::normalize_selections(selections, primary_index);
2684
2685                let primary = selections
2686                    .get(primary_index)
2687                    .cloned()
2688                    .ok_or_else(|| CommandError::Other("Invalid primary selection".to_string()))?;
2689
2690                self.editor.cursor_position = primary.end;
2691                self.editor.selection = if primary.start == primary.end {
2692                    None
2693                } else {
2694                    Some(primary.clone())
2695                };
2696
2697                self.editor.secondary_selections = selections
2698                    .into_iter()
2699                    .enumerate()
2700                    .filter_map(|(idx, sel)| {
2701                        if idx == primary_index {
2702                            None
2703                        } else {
2704                            Some(sel)
2705                        }
2706                    })
2707                    .collect();
2708
2709                Ok(CommandResult::Success)
2710            }
2711            CursorCommand::ClearSecondarySelections => {
2712                self.editor.secondary_selections.clear();
2713                Ok(CommandResult::Success)
2714            }
2715            CursorCommand::SetRectSelection { anchor, active } => {
2716                let line_count = self.editor.line_index.line_count();
2717                if anchor.line >= line_count || active.line >= line_count {
2718                    return Err(CommandError::InvalidPosition {
2719                        line: anchor.line.max(active.line),
2720                        column: anchor.column.max(active.column),
2721                    });
2722                }
2723
2724                let (selections, primary_index) =
2725                    crate::selection_set::rect_selections(anchor, active);
2726
2727                // Delegate to SetSelections so normalization rules are shared.
2728                self.execute_cursor(CursorCommand::SetSelections {
2729                    selections,
2730                    primary_index,
2731                })?;
2732                Ok(CommandResult::Success)
2733            }
2734            CursorCommand::FindNext { query, options } => {
2735                self.execute_find_command(query, options, true)
2736            }
2737            CursorCommand::FindPrev { query, options } => {
2738                self.execute_find_command(query, options, false)
2739            }
2740        }
2741    }
2742
2743    // Private method: execute view command
2744    fn execute_view(&mut self, command: ViewCommand) -> Result<CommandResult, CommandError> {
2745        match command {
2746            ViewCommand::SetViewportWidth { width } => {
2747                if width == 0 {
2748                    return Err(CommandError::Other(
2749                        "Viewport width must be greater than 0".to_string(),
2750                    ));
2751                }
2752
2753                self.editor.viewport_width = width;
2754                self.editor.layout_engine.set_viewport_width(width);
2755                Ok(CommandResult::Success)
2756            }
2757            ViewCommand::SetTabWidth { width } => {
2758                if width == 0 {
2759                    return Err(CommandError::Other(
2760                        "Tab width must be greater than 0".to_string(),
2761                    ));
2762                }
2763
2764                self.editor.layout_engine.set_tab_width(width);
2765                Ok(CommandResult::Success)
2766            }
2767            ViewCommand::SetTabKeyBehavior { behavior } => {
2768                self.tab_key_behavior = behavior;
2769                Ok(CommandResult::Success)
2770            }
2771            ViewCommand::ScrollTo { line } => {
2772                if line >= self.editor.line_index.line_count() {
2773                    return Err(CommandError::InvalidPosition { line, column: 0 });
2774                }
2775
2776                // Scroll operation only validates line number validity
2777                // Actual scrolling handled by frontend
2778                Ok(CommandResult::Success)
2779            }
2780            ViewCommand::GetViewport { start_row, count } => {
2781                let text = self.editor.piece_table.get_text();
2782                let generator = SnapshotGenerator::from_text_with_tab_width(
2783                    &text,
2784                    self.editor.viewport_width,
2785                    self.editor.layout_engine.tab_width(),
2786                );
2787                let grid = generator.get_headless_grid(start_row, count);
2788                Ok(CommandResult::Viewport(grid))
2789            }
2790        }
2791    }
2792
2793    // Private method: execute style command
2794    fn execute_style(&mut self, command: StyleCommand) -> Result<CommandResult, CommandError> {
2795        match command {
2796            StyleCommand::AddStyle {
2797                start,
2798                end,
2799                style_id,
2800            } => {
2801                if start >= end {
2802                    return Err(CommandError::InvalidRange { start, end });
2803                }
2804
2805                let interval = crate::intervals::Interval::new(start, end, style_id);
2806                self.editor.interval_tree.insert(interval);
2807                Ok(CommandResult::Success)
2808            }
2809            StyleCommand::RemoveStyle {
2810                start,
2811                end,
2812                style_id,
2813            } => {
2814                self.editor.interval_tree.remove(start, end, style_id);
2815                Ok(CommandResult::Success)
2816            }
2817            StyleCommand::Fold {
2818                start_line,
2819                end_line,
2820            } => {
2821                if start_line >= end_line {
2822                    return Err(CommandError::InvalidRange {
2823                        start: start_line,
2824                        end: end_line,
2825                    });
2826                }
2827
2828                let mut region = crate::intervals::FoldRegion::new(start_line, end_line);
2829                region.collapse();
2830                self.editor.folding_manager.add_region(region);
2831                Ok(CommandResult::Success)
2832            }
2833            StyleCommand::Unfold { start_line } => {
2834                self.editor.folding_manager.expand_line(start_line);
2835                Ok(CommandResult::Success)
2836            }
2837            StyleCommand::UnfoldAll => {
2838                self.editor.folding_manager.expand_all();
2839                Ok(CommandResult::Success)
2840            }
2841        }
2842    }
2843
2844    fn rebuild_layout_engine_from_text(&mut self, text: &str) {
2845        let lines = crate::text::split_lines_preserve_trailing(text);
2846        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
2847        self.editor.layout_engine.from_lines(&line_refs);
2848    }
2849
2850    fn position_to_char_offset_clamped(&self, pos: Position) -> usize {
2851        let line_count = self.editor.line_index.line_count();
2852        if line_count == 0 {
2853            return 0;
2854        }
2855
2856        let line = pos.line.min(line_count.saturating_sub(1));
2857        let line_text = self
2858            .editor
2859            .line_index
2860            .get_line_text(line)
2861            .unwrap_or_default();
2862        let line_char_len = line_text.chars().count();
2863        let column = pos.column.min(line_char_len);
2864        self.editor.line_index.position_to_char_offset(line, column)
2865    }
2866
2867    fn position_to_char_offset_and_virtual_pad(&self, pos: Position) -> (usize, usize) {
2868        let line_count = self.editor.line_index.line_count();
2869        if line_count == 0 {
2870            return (0, 0);
2871        }
2872
2873        let line = pos.line.min(line_count.saturating_sub(1));
2874        let line_text = self
2875            .editor
2876            .line_index
2877            .get_line_text(line)
2878            .unwrap_or_default();
2879        let line_char_len = line_text.chars().count();
2880        let clamped_col = pos.column.min(line_char_len);
2881        let offset = self
2882            .editor
2883            .line_index
2884            .position_to_char_offset(line, clamped_col);
2885        let pad = pos.column.saturating_sub(clamped_col);
2886        (offset, pad)
2887    }
2888
2889    fn normalize_cursor_and_selection(&mut self) {
2890        let line_index = &self.editor.line_index;
2891        let line_count = line_index.line_count();
2892        if line_count == 0 {
2893            self.editor.cursor_position = Position::new(0, 0);
2894            self.editor.selection = None;
2895            self.editor.secondary_selections.clear();
2896            return;
2897        }
2898
2899        self.editor.cursor_position =
2900            Self::clamp_position_lenient_with_index(line_index, self.editor.cursor_position);
2901
2902        if let Some(ref mut selection) = self.editor.selection {
2903            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
2904            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
2905            selection.direction = if selection.start.line < selection.end.line
2906                || (selection.start.line == selection.end.line
2907                    && selection.start.column <= selection.end.column)
2908            {
2909                SelectionDirection::Forward
2910            } else {
2911                SelectionDirection::Backward
2912            };
2913        }
2914
2915        for selection in &mut self.editor.secondary_selections {
2916            selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
2917            selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
2918            selection.direction = if selection.start.line < selection.end.line
2919                || (selection.start.line == selection.end.line
2920                    && selection.start.column <= selection.end.column)
2921            {
2922                SelectionDirection::Forward
2923            } else {
2924                SelectionDirection::Backward
2925            };
2926        }
2927    }
2928
2929    fn clamp_column_for_line(&self, line: usize, column: usize) -> usize {
2930        Self::clamp_column_for_line_with_index(&self.editor.line_index, line, column)
2931    }
2932
2933    fn clamp_position_lenient_with_index(line_index: &LineIndex, pos: Position) -> Position {
2934        let line_count = line_index.line_count();
2935        if line_count == 0 {
2936            return Position::new(0, 0);
2937        }
2938
2939        let clamped_line = pos.line.min(line_count.saturating_sub(1));
2940        // Note: do NOT clamp column here. Virtual columns (box selection) are allowed.
2941        Position::new(clamped_line, pos.column)
2942    }
2943
2944    fn clamp_column_for_line_with_index(
2945        line_index: &LineIndex,
2946        line: usize,
2947        column: usize,
2948    ) -> usize {
2949        let line_start = line_index.position_to_char_offset(line, 0);
2950        let line_end = line_index.position_to_char_offset(line, usize::MAX);
2951        let line_len = line_end.saturating_sub(line_start);
2952        column.min(line_len)
2953    }
2954}
2955
2956#[cfg(test)]
2957mod tests {
2958    use super::*;
2959
2960    #[test]
2961    fn test_edit_insert() {
2962        let mut executor = CommandExecutor::new("Hello", 80);
2963
2964        let result = executor.execute(Command::Edit(EditCommand::Insert {
2965            offset: 5,
2966            text: " World".to_string(),
2967        }));
2968
2969        assert!(result.is_ok());
2970        assert_eq!(executor.editor().get_text(), "Hello World");
2971    }
2972
2973    #[test]
2974    fn test_edit_delete() {
2975        let mut executor = CommandExecutor::new("Hello World", 80);
2976
2977        let result = executor.execute(Command::Edit(EditCommand::Delete {
2978            start: 5,
2979            length: 6,
2980        }));
2981
2982        assert!(result.is_ok());
2983        assert_eq!(executor.editor().get_text(), "Hello");
2984    }
2985
2986    #[test]
2987    fn test_edit_replace() {
2988        let mut executor = CommandExecutor::new("Hello World", 80);
2989
2990        let result = executor.execute(Command::Edit(EditCommand::Replace {
2991            start: 6,
2992            length: 5,
2993            text: "Rust".to_string(),
2994        }));
2995
2996        assert!(result.is_ok());
2997        assert_eq!(executor.editor().get_text(), "Hello Rust");
2998    }
2999
3000    #[test]
3001    fn test_cursor_move_to() {
3002        let mut executor = CommandExecutor::new("Line 1\nLine 2\nLine 3", 80);
3003
3004        let result = executor.execute(Command::Cursor(CursorCommand::MoveTo {
3005            line: 1,
3006            column: 3,
3007        }));
3008
3009        assert!(result.is_ok());
3010        assert_eq!(executor.editor().cursor_position(), Position::new(1, 3));
3011    }
3012
3013    #[test]
3014    fn test_cursor_selection() {
3015        let mut executor = CommandExecutor::new("Hello World", 80);
3016
3017        let result = executor.execute(Command::Cursor(CursorCommand::SetSelection {
3018            start: Position::new(0, 0),
3019            end: Position::new(0, 5),
3020        }));
3021
3022        assert!(result.is_ok());
3023        assert!(executor.editor().selection().is_some());
3024    }
3025
3026    #[test]
3027    fn test_view_set_width() {
3028        let mut executor = CommandExecutor::new("Test", 80);
3029
3030        let result = executor.execute(Command::View(ViewCommand::SetViewportWidth { width: 40 }));
3031
3032        assert!(result.is_ok());
3033        assert_eq!(executor.editor().viewport_width, 40);
3034    }
3035
3036    #[test]
3037    fn test_style_add_remove() {
3038        let mut executor = CommandExecutor::new("Hello World", 80);
3039
3040        // Add style
3041        let result = executor.execute(Command::Style(StyleCommand::AddStyle {
3042            start: 0,
3043            end: 5,
3044            style_id: 1,
3045        }));
3046        assert!(result.is_ok());
3047
3048        // Remove style
3049        let result = executor.execute(Command::Style(StyleCommand::RemoveStyle {
3050            start: 0,
3051            end: 5,
3052            style_id: 1,
3053        }));
3054        assert!(result.is_ok());
3055    }
3056
3057    #[test]
3058    fn test_batch_execution() {
3059        let mut executor = CommandExecutor::new("", 80);
3060
3061        let commands = vec![
3062            Command::Edit(EditCommand::Insert {
3063                offset: 0,
3064                text: "Hello".to_string(),
3065            }),
3066            Command::Edit(EditCommand::Insert {
3067                offset: 5,
3068                text: " World".to_string(),
3069            }),
3070        ];
3071
3072        let results = executor.execute_batch(commands);
3073        assert!(results.is_ok());
3074        assert_eq!(executor.editor().get_text(), "Hello World");
3075    }
3076
3077    #[test]
3078    fn test_error_invalid_offset() {
3079        let mut executor = CommandExecutor::new("Hello", 80);
3080
3081        let result = executor.execute(Command::Edit(EditCommand::Insert {
3082            offset: 100,
3083            text: "X".to_string(),
3084        }));
3085
3086        assert!(result.is_err());
3087        assert!(matches!(
3088            result.unwrap_err(),
3089            CommandError::InvalidOffset(_)
3090        ));
3091    }
3092}