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