Skip to main content

editor_core/
commands.rs

1//! Command Interface Layer
2//!
3//! Provides a unified command interface for convenient frontend integration.
4//!
5//! # Overview
6//!
7//! The Command Interface Layer is the primary entry point for Editor Core, wrapping all underlying components in a unified command pattern.
8//! It supports the following types of operations:
9//!
10//! - **Text Editing**: Insert, delete, and replace text
11//! - **Cursor Operations**: Move cursor and set selection range
12//! - **View Management**: Set viewport, scroll, and get visible content
13//! - **Style Control**: Add/remove styles and code folding
14//!
15//! # Example
16//!
17//! ```rust
18//! use editor_core::{CommandExecutor, Command, EditCommand};
19//!
20//! let mut executor = CommandExecutor::empty(80);
21//!
22//! // Insert text
23//! executor.execute(Command::Edit(EditCommand::Insert {
24//!     offset: 0,
25//!     text: "Hello, World!".to_string(),
26//! })).unwrap();
27//!
28//! // Batch execute commands
29//! let commands = vec![
30//!     Command::Edit(EditCommand::Insert { offset: 0, text: "Line 1\n".to_string() }),
31//!     Command::Edit(EditCommand::Insert { offset: 7, text: "Line 2\n".to_string() }),
32//! ];
33//! executor.execute_batch(commands).unwrap();
34//! ```
35
36use crate::decorations::{Decoration, DecorationLayerId, DecorationPlacement};
37use crate::delta::{TextDelta, TextDeltaEdit};
38use crate::diagnostics::Diagnostic;
39use crate::intervals::{FoldRegion, IntervalTextEdit, StyleId, StyleLayerId};
40use crate::layout::{
41    cell_width_at, char_width, visual_x_for_column, wrap_indent_cells_for_line_text,
42};
43use crate::line_ending::LineEnding;
44use crate::search::{CharIndex, SearchMatch, SearchOptions, find_all, find_next, find_prev};
45use crate::snapshot::{
46    Cell, ComposedCell, ComposedCellSource, ComposedGrid, ComposedLine, ComposedLineKind,
47    HeadlessGrid, HeadlessLine, MinimapGrid, MinimapLine,
48};
49use crate::snippets::{SnippetNavigation, SnippetSession, parse_snippet};
50#[cfg(debug_assertions)]
51use crate::storage::PieceTable;
52use crate::visual_rows::VisualRowIndex;
53use crate::{FOLD_PLACEHOLDER_STYLE_ID, FoldingManager, IntervalTree, LayoutEngine, LineIndex};
54use editor_core_lang::{CommentConfig, IndentStyle, IndentationConfig};
55use regex::RegexBuilder;
56use std::cell::RefCell;
57use std::collections::{BTreeMap, HashMap};
58use std::time::Duration;
59use unicode_segmentation::UnicodeSegmentation;
60
61const DEFAULT_COMMAND_HISTORY_LIMIT: usize = 1000;
62#[path = "model.rs"]
63mod model;
64pub use self::model::{
65    AutoPair, AutoPairsConfig, Command, CommandError, CommandResult, CursorCommand, EditCommand,
66    ExpandSelectionDirection, ExpandSelectionUnit, Position, Selection, SelectionDirection,
67    StyleCommand, TabKeyBehavior, TextEditSpec, ViewCommand,
68};
69
70#[path = "undo.rs"]
71mod undo;
72use self::undo::{TextEdit, UndoRedoManager, UndoStep};
73pub use self::undo::{
74    UndoHistoryRestoreError, UndoHistorySelectionSet, UndoHistorySnapshot, UndoHistoryStep,
75    UndoHistoryTextEdit,
76};
77
78#[path = "render_grid.rs"]
79mod render_grid;
80
81#[path = "cursor_ops.rs"]
82mod cursor_ops;
83pub use self::cursor_ops::WordBoundaryConfig;
84use self::cursor_ops::{
85    TextBoundary, leading_horizontal_whitespace, next_boundary_column, prev_boundary_column,
86};
87
88#[path = "line_ops.rs"]
89mod line_ops;
90
91#[path = "edit_ops.rs"]
92mod edit_ops;
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95struct SelectionSetSnapshot {
96    selections: Vec<Selection>,
97    primary_index: usize,
98}
99
100/// Editor Core state
101///
102/// `EditorCore` aggregates all underlying editor components, including:
103///
104/// - **LineIndex**: Rope-backed canonical text buffer with fast line access
105/// - **LayoutEngine**: Soft wrapping and text layout calculation
106/// - **IntervalTree**: Style interval management
107/// - **FoldingManager**: Code folding management
108/// - **Cursor & Selection**: Cursor and selection state
109///
110/// # Example
111///
112/// ```rust
113/// use editor_core::EditorCore;
114///
115/// let mut core = EditorCore::new("Hello\nWorld", 80);
116/// assert_eq!(core.line_count(), 2);
117/// assert_eq!(core.get_text(), "Hello\nWorld");
118/// ```
119pub struct EditorCore {
120    /// Debug-only deprecated PieceTable shadow used to catch migration regressions.
121    #[cfg(debug_assertions)]
122    piece_table_shadow: PieceTable,
123    /// Line index
124    line_index: LineIndex,
125    /// Layout engine
126    layout_engine: LayoutEngine,
127    /// Interval tree (style management)
128    interval_tree: IntervalTree,
129    /// Layered styles (for semantic highlighting/simple syntax highlighting, etc.)
130    style_layers: BTreeMap<StyleLayerId, IntervalTree>,
131    /// Derived diagnostics for this document (character-offset ranges + metadata).
132    diagnostics: Vec<Diagnostic>,
133    /// Derived decorations for this document (virtual text, links, etc.).
134    decorations: BTreeMap<DecorationLayerId, Vec<Decoration>>,
135    /// Derived document symbols / outline for this document.
136    document_symbols: crate::DocumentOutline,
137    /// Folding manager
138    folding_manager: FoldingManager,
139    /// Current cursor position
140    cursor_position: Position,
141    /// Current selection range
142    selection: Option<Selection>,
143    /// Secondary selections/cursors (multi-cursor). Each Selection can be empty (start==end), representing a caret.
144    secondary_selections: Vec<Selection>,
145    /// Viewport width
146    viewport_width: usize,
147    word_boundary: WordBoundaryConfig,
148    visual_row_index_cache: RefCell<Option<VisualRowIndex>>,
149}
150
151impl EditorCore {
152    /// Create a new Editor Core
153    pub fn new(text: &str, viewport_width: usize) -> Self {
154        let normalized = crate::text::normalize_crlf_to_lf(text);
155        let text = normalized.as_ref();
156
157        #[cfg(debug_assertions)]
158        let piece_table_shadow = PieceTable::new(text);
159        let line_index = LineIndex::from_text(text);
160        let mut layout_engine = LayoutEngine::new(viewport_width);
161
162        // Initialize layout engine to be consistent with initial text (including trailing empty line).
163        let lines = crate::text::split_lines_preserve_trailing(text);
164        let line_refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
165        layout_engine.from_lines(&line_refs);
166
167        Self {
168            #[cfg(debug_assertions)]
169            piece_table_shadow,
170            line_index,
171            layout_engine,
172            interval_tree: IntervalTree::new(),
173            style_layers: BTreeMap::new(),
174            diagnostics: Vec::new(),
175            decorations: BTreeMap::new(),
176            document_symbols: crate::DocumentOutline::default(),
177            folding_manager: FoldingManager::new(),
178            cursor_position: Position::new(0, 0),
179            selection: None,
180            secondary_selections: Vec::new(),
181            viewport_width,
182            word_boundary: WordBoundaryConfig::default(),
183            visual_row_index_cache: RefCell::new(None),
184        }
185    }
186
187    /// Create an empty Editor Core
188    pub fn empty(viewport_width: usize) -> Self {
189        Self::new("", viewport_width)
190    }
191
192    /// Get text content
193    pub fn get_text(&self) -> String {
194        self.line_index.text_buffer().get_text()
195    }
196
197    /// Get a text range by character offset and length.
198    pub fn text_range(&self, start: usize, len: usize) -> String {
199        self.line_index.text_buffer().get_range(start, len)
200    }
201
202    /// Get total line count
203    pub fn line_count(&self) -> usize {
204        self.line_index.line_count()
205    }
206
207    /// Get total character count
208    pub fn char_count(&self) -> usize {
209        self.line_index.text_buffer().len_chars()
210    }
211
212    /// Override the ASCII word-boundary character set used by editor-friendly "word" operations.
213    ///
214    /// See [`WordBoundaryConfig::set_ascii_boundary_chars`].
215    pub fn set_word_boundary_ascii_boundary_chars(&mut self, boundary_chars: &str) {
216        self.word_boundary.set_ascii_boundary_chars(boundary_chars);
217    }
218
219    /// Reset word-boundary configuration to the default (ASCII identifier-like words).
220    pub fn reset_word_boundary_defaults(&mut self) {
221        self.word_boundary = WordBoundaryConfig::default();
222    }
223
224    /// Get cursor position
225    pub fn cursor_position(&self) -> Position {
226        self.cursor_position
227    }
228
229    /// Get selection range
230    pub fn selection(&self) -> Option<&Selection> {
231        self.selection.as_ref()
232    }
233
234    /// Get secondary selections/cursors (multi-cursor)
235    pub fn secondary_selections(&self) -> &[Selection] {
236        &self.secondary_selections
237    }
238
239    /// Replace cursor and selection state as one view-local snapshot.
240    pub(crate) fn set_cursor_state(
241        &mut self,
242        cursor_position: Position,
243        selection: Option<Selection>,
244        secondary_selections: Vec<Selection>,
245    ) {
246        self.cursor_position = cursor_position;
247        self.selection = selection;
248        self.secondary_selections = secondary_selections;
249    }
250
251    /// Get the canonical line index and text access facade.
252    pub fn line_index(&self) -> &LineIndex {
253        &self.line_index
254    }
255
256    /// Get the current layout engine state.
257    pub fn layout_engine(&self) -> &LayoutEngine {
258        &self.layout_engine
259    }
260
261    /// Get the base style interval tree.
262    pub fn interval_tree(&self) -> &IntervalTree {
263        &self.interval_tree
264    }
265
266    /// Get all style layers.
267    pub fn style_layers(&self) -> &BTreeMap<StyleLayerId, IntervalTree> {
268        &self.style_layers
269    }
270
271    /// Get one style layer by id.
272    pub fn style_layer(&self, layer: StyleLayerId) -> Option<&IntervalTree> {
273        self.style_layers.get(&layer)
274    }
275
276    /// Get the current diagnostics list.
277    pub fn diagnostics(&self) -> &[Diagnostic] {
278        &self.diagnostics
279    }
280
281    /// Get all decoration layers.
282    pub fn decorations(&self) -> &BTreeMap<DecorationLayerId, Vec<Decoration>> {
283        &self.decorations
284    }
285
286    /// Get all decorations for a given layer.
287    pub fn decorations_for_layer(&self, layer: DecorationLayerId) -> &[Decoration] {
288        self.decorations
289            .get(&layer)
290            .map(Vec::as_slice)
291            .unwrap_or(&[])
292    }
293
294    /// Get the current document outline.
295    pub fn document_symbols(&self) -> &crate::DocumentOutline {
296        &self.document_symbols
297    }
298
299    /// Get the folding manager as a read-only view.
300    pub fn folding_manager(&self) -> &FoldingManager {
301        &self.folding_manager
302    }
303
304    /// Get the current viewport width in cells.
305    pub fn viewport_width(&self) -> usize {
306        self.viewport_width
307    }
308
309    /// Replace view layout options and reflow from the canonical text source when needed.
310    pub(crate) fn set_view_options(
311        &mut self,
312        viewport_width: usize,
313        wrap_mode: crate::WrapMode,
314        wrap_indent: crate::WrapIndent,
315        tab_width: usize,
316    ) {
317        let viewport_width = viewport_width.max(1);
318        let tab_width = tab_width.max(1);
319
320        let changed = self.viewport_width != viewport_width
321            || self.layout_engine.viewport_width() != viewport_width
322            || self.layout_engine.wrap_mode() != wrap_mode
323            || self.layout_engine.wrap_indent() != wrap_indent
324            || self.layout_engine.tab_width() != tab_width;
325
326        self.viewport_width = viewport_width;
327        self.layout_engine.set_viewport_width(viewport_width);
328        self.layout_engine.set_wrap_mode(wrap_mode);
329        self.layout_engine.set_wrap_indent(wrap_indent);
330        self.layout_engine.set_tab_width(tab_width);
331
332        if changed {
333            self.reflow_layout_from_line_index();
334        }
335    }
336
337    /// Insert a base style interval through the controlled style path.
338    pub(crate) fn insert_style_interval(&mut self, interval: crate::intervals::Interval) {
339        self.interval_tree.insert(interval);
340    }
341
342    /// Remove a base style interval through the controlled style path.
343    pub(crate) fn remove_style_interval(&mut self, start: usize, end: usize, style_id: StyleId) {
344        self.interval_tree.remove(start, end, style_id);
345    }
346
347    /// Replace one derived style layer; empty intervals clear the layer.
348    pub(crate) fn replace_style_layer(
349        &mut self,
350        layer: StyleLayerId,
351        intervals: Vec<crate::intervals::Interval>,
352    ) {
353        if intervals.is_empty() {
354            self.style_layers.remove(&layer);
355            return;
356        }
357
358        let tree = self.style_layers.entry(layer).or_default();
359        tree.clear();
360        for interval in intervals {
361            if interval.start < interval.end {
362                tree.insert(interval);
363            }
364        }
365    }
366
367    /// Clear one derived style layer.
368    pub(crate) fn clear_style_layer(&mut self, layer: StyleLayerId) {
369        self.style_layers.remove(&layer);
370    }
371
372    /// Replace diagnostics wholesale.
373    pub(crate) fn replace_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
374        self.diagnostics = diagnostics;
375    }
376
377    /// Clear all diagnostics.
378    pub(crate) fn clear_diagnostics(&mut self) {
379        self.diagnostics.clear();
380    }
381
382    /// Replace a decoration layer, sorting it into deterministic range order.
383    pub(crate) fn replace_decorations(
384        &mut self,
385        layer: DecorationLayerId,
386        mut decorations: Vec<Decoration>,
387    ) {
388        decorations.sort_unstable_by_key(|d| (d.range.start, d.range.end));
389        self.decorations.insert(layer, decorations);
390    }
391
392    /// Clear one decoration layer.
393    pub(crate) fn clear_decorations(&mut self, layer: DecorationLayerId) {
394        self.decorations.remove(&layer);
395    }
396
397    /// Replace document symbols / outline wholesale.
398    pub(crate) fn replace_document_symbols(&mut self, symbols: crate::DocumentOutline) {
399        self.document_symbols = symbols;
400    }
401
402    /// Clear document symbols / outline.
403    pub(crate) fn clear_document_symbols(&mut self) {
404        self.document_symbols = crate::DocumentOutline::default();
405    }
406
407    /// Replace derived folding regions and invalidate visual-row mappings.
408    pub(crate) fn replace_folding_regions(
409        &mut self,
410        regions: Vec<FoldRegion>,
411        preserve_collapsed: bool,
412    ) {
413        if preserve_collapsed {
414            self.folding_manager
415                .replace_derived_regions_preserving_collapsed(regions);
416        } else {
417            self.folding_manager.replace_derived_regions(regions);
418        }
419        self.invalidate_visual_row_index_cache();
420    }
421
422    /// Clear derived folding regions and invalidate visual-row mappings.
423    pub(crate) fn clear_derived_folding_regions(&mut self) {
424        self.folding_manager.clear_derived_regions();
425        self.invalidate_visual_row_index_cache();
426    }
427
428    /// Toggle a fold starting at `line` and refresh affected visual-row mappings.
429    pub(crate) fn toggle_fold_at_line(&mut self, line: usize) -> bool {
430        let affected = self
431            .folding_manager
432            .regions()
433            .iter()
434            .filter(|region| region.start_line == line && region.end_line > region.start_line)
435            .min_by_key(|region| region.end_line)
436            .map(|region| (region.start_line, region.end_line));
437        let toggled = self.folding_manager.toggle_region_starting_at_line(line);
438        if toggled {
439            if let Some((start, end)) = affected {
440                self.sync_visual_row_index_for_logical_range(start, end);
441            } else {
442                self.invalidate_visual_row_index_cache();
443            }
444        }
445        toggled
446    }
447
448    /// Expand all folds and refresh visual-row mappings.
449    pub(crate) fn expand_all_folds(&mut self) {
450        let had_collapsed = self
451            .folding_manager
452            .regions()
453            .iter()
454            .any(|region| region.is_collapsed);
455        self.folding_manager.expand_all();
456        if had_collapsed {
457            self.invalidate_visual_row_index_cache();
458        }
459    }
460
461    /// Invalidate cached visual-row index (wrap/folding derived mapping).
462    pub fn invalidate_visual_row_index_cache(&mut self) {
463        *self.visual_row_index_cache.borrow_mut() = None;
464    }
465
466    fn visual_row_count_for_logical_line(&self, logical_line: usize) -> usize {
467        if logical_line >= self.layout_engine.logical_line_count() {
468            return 0;
469        }
470        if Self::is_logical_line_hidden(self.folding_manager.regions(), logical_line) {
471            return 0;
472        }
473
474        self.layout_engine
475            .get_line_layout(logical_line)
476            .map(|layout| layout.visual_line_count)
477            .unwrap_or(1)
478            .max(1)
479    }
480
481    fn sync_visual_row_index_for_logical_range(&mut self, start_line: usize, end_line: usize) {
482        if self.visual_row_index_cache.borrow().is_none() {
483            return;
484        }
485
486        let line_count = self.layout_engine.logical_line_count();
487        if line_count == 0 || start_line >= line_count {
488            return;
489        }
490
491        let end_line = end_line.min(line_count.saturating_sub(1));
492        let counts = (start_line..=end_line)
493            .map(|line| (line, self.visual_row_count_for_logical_line(line)))
494            .collect::<Vec<_>>();
495
496        let mut cache = self.visual_row_index_cache.borrow_mut();
497        let Some(index) = cache.as_mut() else {
498            return;
499        };
500
501        if index.logical_line_count() != line_count {
502            *cache = None;
503            return;
504        }
505
506        for (line, count) in counts {
507            if !index.set_line_visual_count(line, count) {
508                *cache = None;
509                return;
510            }
511        }
512    }
513
514    fn sync_visual_row_index_after_text_change(
515        &mut self,
516        start_line: usize,
517        deleted_newlines: usize,
518        inserted_newlines: usize,
519    ) {
520        if self.visual_row_index_cache.borrow().is_none() {
521            return;
522        }
523
524        let line_delta = inserted_newlines as isize - deleted_newlines as isize;
525        if line_delta != 0 {
526            let line_count = self.layout_engine.logical_line_count();
527            let mut cache = self.visual_row_index_cache.borrow_mut();
528            let Some(index) = cache.as_mut() else {
529                return;
530            };
531
532            if line_delta > 0 {
533                let inserted = line_delta as usize;
534                if index.logical_line_count().saturating_add(inserted) != line_count {
535                    *cache = None;
536                    return;
537                }
538                index.insert_lines(
539                    start_line.saturating_add(1),
540                    std::iter::repeat_n(0, inserted),
541                );
542            } else {
543                let removed = (-line_delta) as usize;
544                if index.logical_line_count().saturating_sub(removed) != line_count
545                    || !index.remove_lines(start_line.saturating_add(1), removed)
546                {
547                    *cache = None;
548                    return;
549                }
550            }
551        }
552
553        let touch_lines = deleted_newlines.max(inserted_newlines).saturating_add(1);
554        self.sync_visual_row_index_for_logical_range(
555            start_line,
556            start_line.saturating_add(touch_lines),
557        );
558    }
559
560    /// Reflow every logical line from the canonical line index after layout options change.
561    pub(crate) fn reflow_layout_from_line_index(&mut self) {
562        let lines: Vec<String> = (0..self.line_index.line_count())
563            .map(|line| self.line_index.get_line_text(line).unwrap_or_default())
564            .collect();
565        self.layout_engine
566            .recalculate_all_from_lines(lines.iter().map(String::as_str));
567        self.invalidate_visual_row_index_cache();
568    }
569
570    fn with_visual_row_index<R>(&self, f: impl FnOnce(&VisualRowIndex) -> R) -> R {
571        if self.visual_row_index_cache.borrow().is_none() {
572            let index = self.build_visual_row_index();
573            *self.visual_row_index_cache.borrow_mut() = Some(index);
574        }
575        let cache = self.visual_row_index_cache.borrow();
576        let index = cache
577            .as_ref()
578            .expect("visual-row cache should be initialized");
579        f(index)
580    }
581
582    fn build_visual_row_index(&self) -> VisualRowIndex {
583        let counts = (0..self.layout_engine.logical_line_count())
584            .map(|logical_line| self.visual_row_count_for_logical_line(logical_line))
585            .collect();
586        VisualRowIndex::from_line_visual_counts(counts)
587    }
588
589    /// Get total visual line count (considering soft wrapping + folding).
590    pub fn visual_line_count(&self) -> usize {
591        self.with_visual_row_index(|index| index.total_visual_lines())
592    }
593
594    /// Map visual line number back to (logical_line, visual_in_logical), considering folding.
595    pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
596        self.with_visual_row_index(|index| {
597            if index.total_visual_lines() == 0 {
598                return (0, 0);
599            }
600            let clamped_visual = visual_line.min(index.total_visual_lines().saturating_sub(1));
601            index
602                .span_for_visual_row(clamped_visual)
603                .map(|(span, visual_in_logical)| (span.logical_line, visual_in_logical))
604                .unwrap_or((0, 0))
605        })
606    }
607
608    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
609    pub fn logical_position_to_visual(
610        &self,
611        logical_line: usize,
612        column: usize,
613    ) -> Option<(usize, usize)> {
614        let regions = self.folding_manager.regions();
615        let logical_line = Self::closest_visible_line(regions, logical_line)?;
616        let visual_start = self.visual_start_for_logical_line(logical_line)?;
617
618        let tab_width = self.layout_engine.tab_width();
619
620        let layout = self.layout_engine.get_line_layout(logical_line)?;
621        let line_text = self
622            .line_index
623            .get_line_text(logical_line)
624            .unwrap_or_default();
625
626        let line_char_len = line_text.chars().count();
627        let column = column.min(line_char_len);
628
629        let mut wrapped_offset = 0usize;
630        let mut segment_start_col = 0usize;
631        for wrap_point in &layout.wrap_points {
632            if column >= wrap_point.char_index {
633                wrapped_offset = wrapped_offset.saturating_add(1);
634                segment_start_col = wrap_point.char_index;
635            } else {
636                break;
637            }
638        }
639
640        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
641        let mut x_in_line = seg_start_x_in_line;
642        let mut x_in_segment = 0usize;
643        for ch in line_text
644            .chars()
645            .skip(segment_start_col)
646            .take(column.saturating_sub(segment_start_col))
647        {
648            let w = cell_width_at(ch, x_in_line, tab_width);
649            x_in_line = x_in_line.saturating_add(w);
650            x_in_segment = x_in_segment.saturating_add(w);
651        }
652
653        let indent = if wrapped_offset == 0 {
654            0
655        } else {
656            wrap_indent_cells_for_line_text(
657                &line_text,
658                self.layout_engine.wrap_indent(),
659                self.viewport_width,
660                tab_width,
661            )
662        };
663
664        Some((
665            visual_start.saturating_add(wrapped_offset),
666            indent.saturating_add(x_in_segment),
667        ))
668    }
669
670    /// Convert logical coordinates (line, column) to visual coordinates (visual line number, in-line x cell offset), considering folding.
671    ///
672    /// Difference from [`logical_position_to_visual`](Self::logical_position_to_visual) is that it allows `column`
673    /// to exceed the line end: the exceeding part is treated as `' '` (width=1) virtual spaces, suitable for rectangular selection / column editing.
674    pub fn logical_position_to_visual_allow_virtual(
675        &self,
676        logical_line: usize,
677        column: usize,
678    ) -> Option<(usize, usize)> {
679        let regions = self.folding_manager.regions();
680        let logical_line = Self::closest_visible_line(regions, logical_line)?;
681        let visual_start = self.visual_start_for_logical_line(logical_line)?;
682
683        let tab_width = self.layout_engine.tab_width();
684
685        let layout = self.layout_engine.get_line_layout(logical_line)?;
686        let line_text = self
687            .line_index
688            .get_line_text(logical_line)
689            .unwrap_or_default();
690
691        let line_char_len = line_text.chars().count();
692        let clamped_column = column.min(line_char_len);
693
694        let mut wrapped_offset = 0usize;
695        let mut segment_start_col = 0usize;
696        for wrap_point in &layout.wrap_points {
697            if clamped_column >= wrap_point.char_index {
698                wrapped_offset = wrapped_offset.saturating_add(1);
699                segment_start_col = wrap_point.char_index;
700            } else {
701                break;
702            }
703        }
704
705        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
706        let mut x_in_line = seg_start_x_in_line;
707        let mut x_in_segment = 0usize;
708        for ch in line_text
709            .chars()
710            .skip(segment_start_col)
711            .take(clamped_column.saturating_sub(segment_start_col))
712        {
713            let w = cell_width_at(ch, x_in_line, tab_width);
714            x_in_line = x_in_line.saturating_add(w);
715            x_in_segment = x_in_segment.saturating_add(w);
716        }
717
718        let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
719
720        let indent = if wrapped_offset == 0 {
721            0
722        } else {
723            wrap_indent_cells_for_line_text(
724                &line_text,
725                self.layout_engine.wrap_indent(),
726                self.viewport_width,
727                tab_width,
728            )
729        };
730
731        Some((
732            visual_start.saturating_add(wrapped_offset),
733            indent.saturating_add(x_in_segment),
734        ))
735    }
736
737    /// Convert visual coordinates (global visual row + x in cells) back to logical `(line, column)`.
738    ///
739    /// - `visual_row` is the global visual row (after soft wrapping and folding).
740    /// - `x_in_cells` is the cell offset within that visual row (0-based).
741    ///
742    /// Returns `None` if layout information is unavailable.
743    pub fn visual_position_to_logical(
744        &self,
745        visual_row: usize,
746        x_in_cells: usize,
747    ) -> Option<Position> {
748        let total_visual = self.visual_line_count();
749        if total_visual == 0 {
750            return Some(Position::new(0, 0));
751        }
752
753        let clamped_row = visual_row.min(total_visual.saturating_sub(1));
754        let (logical_line, visual_in_logical) = self.visual_to_logical_line(clamped_row);
755
756        let layout = self.layout_engine.get_line_layout(logical_line)?;
757        let line_text = self
758            .line_index
759            .get_line_text(logical_line)
760            .unwrap_or_default();
761        let line_char_len = line_text.chars().count();
762
763        let segment_start_col = if visual_in_logical == 0 {
764            0
765        } else {
766            layout
767                .wrap_points
768                .get(visual_in_logical - 1)
769                .map(|wp| wp.char_index)
770                .unwrap_or(0)
771        };
772
773        let segment_end_col = layout
774            .wrap_points
775            .get(visual_in_logical)
776            .map(|wp| wp.char_index)
777            .unwrap_or(line_char_len)
778            .max(segment_start_col)
779            .min(line_char_len);
780
781        let tab_width = self.layout_engine.tab_width();
782        let x_in_cells = if visual_in_logical == 0 {
783            x_in_cells
784        } else {
785            let indent = wrap_indent_cells_for_line_text(
786                &line_text,
787                self.layout_engine.wrap_indent(),
788                self.viewport_width,
789                tab_width,
790            );
791            x_in_cells.saturating_sub(indent)
792        };
793        let seg_start_x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
794        let mut x_in_line = seg_start_x_in_line;
795        let mut x_in_segment = 0usize;
796        let mut column = segment_start_col;
797
798        for (char_idx, ch) in line_text.chars().enumerate().skip(segment_start_col) {
799            if char_idx >= segment_end_col {
800                break;
801            }
802
803            let w = cell_width_at(ch, x_in_line, tab_width);
804            if x_in_segment.saturating_add(w) > x_in_cells {
805                break;
806            }
807
808            x_in_line = x_in_line.saturating_add(w);
809            x_in_segment = x_in_segment.saturating_add(w);
810            column = column.saturating_add(1);
811        }
812
813        Some(Position::new(logical_line, column))
814    }
815
816    fn visual_start_for_logical_line(&self, logical_line: usize) -> Option<usize> {
817        if logical_line >= self.layout_engine.logical_line_count() {
818            return None;
819        }
820        self.with_visual_row_index(|index| {
821            index
822                .span_for_logical_line(logical_line)
823                .map(|span| span.start_visual_row)
824        })
825    }
826
827    fn is_logical_line_hidden(regions: &[FoldRegion], logical_line: usize) -> bool {
828        regions.iter().any(|region| {
829            region.is_collapsed
830                && logical_line > region.start_line
831                && logical_line <= region.end_line
832        })
833    }
834
835    fn collapsed_region_starting_at(
836        regions: &[FoldRegion],
837        start_line: usize,
838    ) -> Option<&FoldRegion> {
839        regions
840            .iter()
841            .filter(|region| {
842                region.is_collapsed
843                    && region.start_line == start_line
844                    && region.end_line > start_line
845            })
846            .min_by_key(|region| region.end_line)
847    }
848
849    fn closest_visible_line(regions: &[FoldRegion], logical_line: usize) -> Option<usize> {
850        let mut line = logical_line;
851        if regions.is_empty() {
852            return Some(line);
853        }
854
855        while Self::is_logical_line_hidden(regions, line) {
856            let Some(start) = regions
857                .iter()
858                .filter(|region| {
859                    region.is_collapsed && line > region.start_line && line <= region.end_line
860                })
861                .map(|region| region.start_line)
862                .max()
863            else {
864                break;
865            };
866            line = start;
867        }
868
869        if Self::is_logical_line_hidden(regions, line) {
870            None
871        } else {
872            Some(line)
873        }
874    }
875
876    fn fold_right_boundary_bracket_char(&self, region: &FoldRegion) -> Option<char> {
877        let end_line_text = self.line_index.get_line_text(region.end_line)?;
878
879        // Common formatting: closing brace is the first non-whitespace char on the end line.
880        if let Some(ch) = end_line_text.chars().find(|c| !c.is_whitespace())
881            && matches!(ch, '}' | ')' | ']')
882        {
883            return Some(ch);
884        }
885
886        // Fallback: scan from the end, skipping common trailing punctuation.
887        for ch in end_line_text.chars().rev() {
888            if ch.is_whitespace() {
889                continue;
890            }
891            if matches!(ch, '}' | ')' | ']') {
892                return Some(ch);
893            }
894            if matches!(ch, ';' | ',') {
895                continue;
896            }
897            break;
898        }
899
900        None
901    }
902
903    fn styles_at_offset(&self, offset: usize) -> Vec<StyleId> {
904        let mut styles: Vec<StyleId> = self
905            .interval_tree
906            .query_point(offset)
907            .iter()
908            .map(|interval| interval.style_id)
909            .collect();
910
911        for tree in self.style_layers.values() {
912            styles.extend(
913                tree.query_point(offset)
914                    .iter()
915                    .map(|interval| interval.style_id),
916            );
917        }
918
919        styles.sort_unstable();
920        styles.dedup();
921        styles
922    }
923}
924
925/// Command executor
926///
927/// `CommandExecutor` is the main interface for the editor, responsible for:
928///
929/// - Execute various editor commands
930/// - Maintain command history
931/// - Handle errors and exceptions
932/// - Ensure editor state consistency
933///
934/// # Command Types
935///
936/// - [`EditCommand`] - Text insertion, deletion, replacement
937/// - [`CursorCommand`] - Cursor movement, selection operations
938/// - [`ViewCommand`] - Viewport management and scroll control
939/// - [`StyleCommand`] - Style and folding management
940///
941/// # Example
942///
943/// ```rust
944/// use editor_core::{CommandExecutor, Command, EditCommand, CursorCommand, Position};
945///
946/// let mut executor = CommandExecutor::empty(80);
947///
948/// // Insert text
949/// executor.execute(Command::Edit(EditCommand::Insert {
950///     offset: 0,
951///     text: "fn main() {}".to_string(),
952/// })).unwrap();
953///
954/// // Move cursor
955/// executor.execute(Command::Cursor(CursorCommand::MoveTo {
956///     line: 0,
957///     column: 3,
958/// })).unwrap();
959///
960/// assert_eq!(executor.editor().cursor_position(), Position::new(0, 3));
961/// ```
962pub struct CommandExecutor {
963    /// Editor Core
964    editor: EditorCore,
965    /// Bounded command history for debug/inspection APIs.
966    command_history: Vec<Command>,
967    /// Maximum number of commands retained in `command_history`; zero disables history.
968    command_history_limit: usize,
969    /// Undo/redo manager (only records CommandExecutor edit commands executed via)
970    undo_redo: UndoRedoManager,
971    /// Controls how [`EditCommand::InsertTab`] behaves.
972    tab_key_behavior: TabKeyBehavior,
973    /// Language-aware indentation config used by [`EditCommand::InsertNewline`] when `auto_indent=true`.
974    indentation_config: IndentationConfig,
975    /// Auto-pairs configuration used by [`EditCommand::TypeChar`] and delete-pair behavior.
976    auto_pairs: AutoPairsConfig,
977    /// Active snippet session (placeholders + navigation), if any.
978    snippet_session: Option<SnippetSession>,
979    /// Preferred line ending for saving (internal storage is always LF).
980    line_ending: LineEnding,
981    /// Sticky x position for visual-row cursor movement (in cells).
982    preferred_x_cells: Option<usize>,
983    /// Structured delta for the last executed text modification (cleared on each `execute()` call).
984    last_text_delta: Option<TextDelta>,
985}
986
987impl CommandExecutor {
988    /// Create a new command executor
989    pub fn new(text: &str, viewport_width: usize) -> Self {
990        Self {
991            editor: EditorCore::new(text, viewport_width),
992            command_history: Vec::with_capacity(DEFAULT_COMMAND_HISTORY_LIMIT),
993            command_history_limit: DEFAULT_COMMAND_HISTORY_LIMIT,
994            undo_redo: UndoRedoManager::new(1000),
995            tab_key_behavior: TabKeyBehavior::Spaces,
996            indentation_config: IndentationConfig::default(),
997            auto_pairs: AutoPairsConfig::default(),
998            snippet_session: None,
999            line_ending: LineEnding::detect_in_text(text),
1000            preferred_x_cells: None,
1001            last_text_delta: None,
1002        }
1003    }
1004
1005    /// Create an empty command executor
1006    pub fn empty(viewport_width: usize) -> Self {
1007        Self::new("", viewport_width)
1008    }
1009
1010    fn update_interval_trees_for_text_edits(&mut self, edits: &[IntervalTextEdit]) {
1011        if edits.is_empty() {
1012            return;
1013        }
1014
1015        self.editor.interval_tree.update_for_text_edits(edits);
1016        for layer_tree in self.editor.style_layers.values_mut() {
1017            layer_tree.update_for_text_edits(edits);
1018        }
1019    }
1020
1021    fn record_command_history(&mut self, command: &Command) {
1022        if self.command_history_limit == 0 {
1023            return;
1024        }
1025
1026        self.command_history.push(command.history_summary());
1027        self.trim_command_history_to_limit();
1028    }
1029
1030    fn trim_command_history_to_limit(&mut self) {
1031        if self.command_history_limit == 0 {
1032            self.command_history.clear();
1033            return;
1034        }
1035
1036        let excess = self
1037            .command_history
1038            .len()
1039            .saturating_sub(self.command_history_limit);
1040        if excess > 0 {
1041            self.command_history.drain(..excess);
1042        }
1043    }
1044
1045    /// Execute command
1046    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
1047        self.last_text_delta = None;
1048
1049        // Snippet sessions are view-local and should generally end when the user performs an
1050        // explicit navigation outside snippet tabstop traversal, or when history/programmatic
1051        // edits occur (undo/redo, bulk apply edits, ...).
1052        if matches!(
1053            &command,
1054            Command::Cursor(
1055                CursorCommand::SnippetNextPlaceholder | CursorCommand::SnippetPrevPlaceholder
1056            )
1057        ) {
1058            // keep session
1059        } else if matches!(&command, Command::Cursor(_))
1060            || matches!(
1061                &command,
1062                Command::Edit(
1063                    EditCommand::Undo | EditCommand::Redo | EditCommand::ApplyTextEdits { .. }
1064                )
1065            )
1066        {
1067            self.snippet_session = None;
1068        }
1069
1070        // Save a bounded summary before execution so failed commands remain observable.
1071        self.record_command_history(&command);
1072
1073        let skip_snippet_delta =
1074            matches!(&command, Command::Edit(EditCommand::ApplySnippet { .. }));
1075
1076        // Undo grouping:
1077        //
1078        // Coalescing groups are meant to represent a "continuous editing" session (typing / IME
1079        // composition updates). UI frameworks may issue non-edit commands during editing (e.g.
1080        // viewport width updates every frame for soft-wrapping), and those should *not* break
1081        // the current coalescing group.
1082        //
1083        // Cursor/selection/navigation and history traversal commands indicate the next insertion
1084        // should start a fresh group; other non-insert edit commands close the group when pushed.
1085        if matches!(
1086            command,
1087            Command::Cursor(_) | Command::Edit(EditCommand::Undo | EditCommand::Redo)
1088        ) {
1089            self.undo_redo.end_group();
1090        }
1091
1092        // Execute command
1093        let result = match command {
1094            Command::Edit(edit_cmd) => self.execute_edit(edit_cmd),
1095            Command::Cursor(cursor_cmd) => self.execute_cursor(cursor_cmd),
1096            Command::View(view_cmd) => self.execute_view(view_cmd),
1097            Command::Style(style_cmd) => self.execute_style(style_cmd),
1098        }?;
1099
1100        // Keep snippet placeholder ranges stable under subsequent edits.
1101        //
1102        // Note: snippet insertion itself (`ApplySnippet`) creates anchors in **post-edit**
1103        // coordinates, so we must not apply the delta again for that command.
1104        if !skip_snippet_delta
1105            && let (Some(delta), Some(session)) =
1106                (self.last_text_delta.as_ref(), self.snippet_session.as_mut())
1107        {
1108            session.apply_delta(delta);
1109        }
1110
1111        Ok(result)
1112    }
1113
1114    /// Get the structured text delta produced by the last successful `execute()` call, if any.
1115    pub fn last_text_delta(&self) -> Option<&TextDelta> {
1116        self.last_text_delta.as_ref()
1117    }
1118
1119    /// Take the structured text delta produced by the last successful `execute()` call, if any.
1120    pub fn take_last_text_delta(&mut self) -> Option<TextDelta> {
1121        self.last_text_delta.take()
1122    }
1123
1124    /// Batch execute commands (transactional)
1125    pub fn execute_batch(
1126        &mut self,
1127        commands: Vec<Command>,
1128    ) -> Result<Vec<CommandResult>, CommandError> {
1129        let mut results = Vec::new();
1130
1131        for command in commands {
1132            let result = self.execute(command)?;
1133            results.push(result);
1134        }
1135
1136        Ok(results)
1137    }
1138
1139    /// Get the bounded command history.
1140    ///
1141    /// Large text payloads are stored as summaries so this debug-oriented history does not keep
1142    /// another full copy of pasted or inserted text.
1143    pub fn get_command_history(&self) -> &[Command] {
1144        &self.command_history
1145    }
1146
1147    /// Get the maximum number of commands retained in history.
1148    pub fn command_history_limit(&self) -> usize {
1149        self.command_history_limit
1150    }
1151
1152    /// Set the maximum number of commands retained in history; `0` disables history recording.
1153    pub fn set_command_history_limit(&mut self, limit: usize) {
1154        self.command_history_limit = limit;
1155        self.trim_command_history_to_limit();
1156    }
1157
1158    /// Can undo
1159    pub fn can_undo(&self) -> bool {
1160        self.undo_redo.can_undo()
1161    }
1162
1163    /// Can redo
1164    pub fn can_redo(&self) -> bool {
1165        self.undo_redo.can_redo()
1166    }
1167
1168    /// Undo stack depth (counted by undo steps; grouped undo may pop multiple steps at once)
1169    pub fn undo_depth(&self) -> usize {
1170        self.undo_redo.undo_depth()
1171    }
1172
1173    /// Redo stack depth (counted by undo steps)
1174    pub fn redo_depth(&self) -> usize {
1175        self.undo_redo.redo_depth()
1176    }
1177
1178    /// Number of redo branches available at the current history node.
1179    ///
1180    /// - In a purely linear history, this is `0` or `1`.
1181    /// - When you undo and then make a new edit, the previous redo path becomes an **alternate
1182    ///   branch** (undo tree).
1183    pub fn redo_branch_count(&self) -> usize {
1184        self.undo_redo.redo_branch_count()
1185    }
1186
1187    /// Index of the currently selected redo branch at the current node, if any.
1188    pub fn selected_redo_branch_index(&self) -> Option<usize> {
1189        self.undo_redo.selected_redo_branch_index()
1190    }
1191
1192    /// Select which redo branch `EditCommand::Redo` will follow from the current node.
1193    pub fn select_redo_branch(&mut self, index: usize) -> Result<(), CommandError> {
1194        self.undo_redo.end_group();
1195        self.undo_redo.select_redo_branch(index)
1196    }
1197
1198    /// Currently open undo group ID (for insert coalescing only)
1199    pub fn current_change_group(&self) -> Option<usize> {
1200        self.undo_redo.current_group_id()
1201    }
1202
1203    /// Timeout used when deciding whether adjacent insertion commands remain in one undo group.
1204    pub fn undo_coalescing_timeout(&self) -> Duration {
1205        self.undo_redo.coalescing_timeout()
1206    }
1207
1208    /// Configure the insertion coalescing timeout; `Duration::ZERO` disables time-based merging.
1209    pub fn set_undo_coalescing_timeout(&mut self, timeout: Duration) {
1210        self.undo_redo.set_coalescing_timeout(timeout);
1211    }
1212
1213    /// Whether current state is at clean point (for dirty tracking)
1214    pub fn is_clean(&self) -> bool {
1215        self.undo_redo.is_clean()
1216    }
1217
1218    /// Mark current state as clean point (call after saving file)
1219    pub fn mark_clean(&mut self) {
1220        self.undo_redo.mark_clean();
1221    }
1222
1223    /// Capture a persistable snapshot of the undo/redo history for this document.
1224    ///
1225    /// Callers are expected to persist the current document text separately.
1226    pub fn undo_history_snapshot(&self) -> UndoHistorySnapshot {
1227        self.undo_redo.snapshot()
1228    }
1229
1230    /// Restore a previously captured [`UndoHistorySnapshot`].
1231    ///
1232    /// Notes:
1233    /// - This does **not** modify the current document text.
1234    /// - Callers should only restore a snapshot into the **same text** it was captured from.
1235    pub fn restore_undo_history(
1236        &mut self,
1237        snapshot: UndoHistorySnapshot,
1238    ) -> Result<(), UndoHistoryRestoreError> {
1239        self.last_text_delta = None;
1240        self.undo_redo.restore_from_snapshot(snapshot)
1241    }
1242
1243    /// Get a reference to the Editor Core
1244    pub fn editor(&self) -> &EditorCore {
1245        &self.editor
1246    }
1247
1248    /// Get a mutable reference to the field-private Editor Core.
1249    ///
1250    /// Prefer [`execute`](Self::execute) for command-driven mutations that must keep text, layout,
1251    /// cursor, selection, folding, style, and undo state synchronized. This accessor is intended for
1252    /// advanced callers that need to invoke public [`EditorCore`] methods directly; it does not
1253    /// expose private fields.
1254    pub fn editor_mut(&mut self) -> &mut EditorCore {
1255        &mut self.editor
1256    }
1257
1258    /// Get current tab key behavior used by [`EditCommand::InsertTab`].
1259    pub fn tab_key_behavior(&self) -> TabKeyBehavior {
1260        self.tab_key_behavior
1261    }
1262
1263    /// Set tab key behavior used by [`EditCommand::InsertTab`].
1264    pub fn set_tab_key_behavior(&mut self, behavior: TabKeyBehavior) {
1265        self.tab_key_behavior = behavior;
1266    }
1267
1268    /// Get the current indentation configuration used by [`EditCommand::InsertNewline`] when
1269    /// `auto_indent=true`.
1270    pub fn indentation_config(&self) -> &IndentationConfig {
1271        &self.indentation_config
1272    }
1273
1274    /// Replace the indentation configuration used by [`EditCommand::InsertNewline`] when
1275    /// `auto_indent=true`.
1276    pub fn set_indentation_config(&mut self, config: IndentationConfig) {
1277        self.indentation_config = config;
1278    }
1279
1280    /// Get the current auto-pairs configuration.
1281    pub fn auto_pairs_config(&self) -> &AutoPairsConfig {
1282        &self.auto_pairs
1283    }
1284
1285    /// Replace the auto-pairs configuration.
1286    pub fn set_auto_pairs_config(&mut self, config: AutoPairsConfig) {
1287        self.auto_pairs = config;
1288    }
1289
1290    /// Enable/disable auto-pairs behavior (convenience wrapper).
1291    pub fn set_auto_pairs_enabled(&mut self, enabled: bool) {
1292        self.auto_pairs.enabled = enabled;
1293    }
1294
1295    /// Return `true` if a snippet session is currently active for this view.
1296    pub fn has_active_snippet_session(&self) -> bool {
1297        self.snippet_session
1298            .as_ref()
1299            .map(|s| s.is_active())
1300            .unwrap_or(false)
1301    }
1302
1303    /// Get the current snippet session (placeholders + navigation), if any.
1304    pub fn snippet_session(&self) -> Option<&SnippetSession> {
1305        self.snippet_session.as_ref()
1306    }
1307
1308    /// Replace the current snippet session.
1309    pub fn set_snippet_session(&mut self, session: Option<SnippetSession>) {
1310        self.snippet_session = session;
1311    }
1312
1313    /// Get the sticky x position (in cells) used by visual-row cursor movement.
1314    pub fn preferred_x_cells(&self) -> Option<usize> {
1315        self.preferred_x_cells
1316    }
1317
1318    /// Set the sticky x position (in cells) used by visual-row cursor movement.
1319    pub fn set_preferred_x_cells(&mut self, preferred_x_cells: Option<usize>) {
1320        self.preferred_x_cells = preferred_x_cells;
1321    }
1322
1323    /// Get the preferred line ending for saving this document.
1324    pub fn line_ending(&self) -> LineEnding {
1325        self.line_ending
1326    }
1327
1328    /// Override the preferred line ending for saving this document.
1329    pub fn set_line_ending(&mut self, line_ending: LineEnding) {
1330        self.line_ending = line_ending;
1331    }
1332
1333    // Private method: execute edit command
1334    fn execute_view(&mut self, command: ViewCommand) -> Result<CommandResult, CommandError> {
1335        match command {
1336            ViewCommand::SetViewportWidth { width } => {
1337                if width == 0 {
1338                    return Err(CommandError::Other(
1339                        "Viewport width must be greater than 0".to_string(),
1340                    ));
1341                }
1342
1343                self.editor.set_view_options(
1344                    width,
1345                    self.editor.layout_engine.wrap_mode(),
1346                    self.editor.layout_engine.wrap_indent(),
1347                    self.editor.layout_engine.tab_width(),
1348                );
1349                Ok(CommandResult::Success)
1350            }
1351            ViewCommand::SetWrapMode { mode } => {
1352                self.editor.set_view_options(
1353                    self.editor.viewport_width,
1354                    mode,
1355                    self.editor.layout_engine.wrap_indent(),
1356                    self.editor.layout_engine.tab_width(),
1357                );
1358                Ok(CommandResult::Success)
1359            }
1360            ViewCommand::SetWrapIndent { indent } => {
1361                self.editor.set_view_options(
1362                    self.editor.viewport_width,
1363                    self.editor.layout_engine.wrap_mode(),
1364                    indent,
1365                    self.editor.layout_engine.tab_width(),
1366                );
1367                Ok(CommandResult::Success)
1368            }
1369            ViewCommand::SetTabWidth { width } => {
1370                if width == 0 {
1371                    return Err(CommandError::Other(
1372                        "Tab width must be greater than 0".to_string(),
1373                    ));
1374                }
1375
1376                self.editor.set_view_options(
1377                    self.editor.viewport_width,
1378                    self.editor.layout_engine.wrap_mode(),
1379                    self.editor.layout_engine.wrap_indent(),
1380                    width,
1381                );
1382                Ok(CommandResult::Success)
1383            }
1384            ViewCommand::SetTabKeyBehavior { behavior } => {
1385                self.tab_key_behavior = behavior;
1386                Ok(CommandResult::Success)
1387            }
1388            ViewCommand::SetIndentationConfig { config } => {
1389                self.indentation_config = config;
1390                Ok(CommandResult::Success)
1391            }
1392            ViewCommand::SetAutoPairsConfig { config } => {
1393                self.set_auto_pairs_config(config);
1394                Ok(CommandResult::Success)
1395            }
1396            ViewCommand::SetAutoPairsEnabled { enabled } => {
1397                self.set_auto_pairs_enabled(enabled);
1398                Ok(CommandResult::Success)
1399            }
1400            ViewCommand::SetWordBoundaryAsciiBoundaryChars { boundary_chars } => {
1401                self.editor
1402                    .set_word_boundary_ascii_boundary_chars(&boundary_chars);
1403                Ok(CommandResult::Success)
1404            }
1405            ViewCommand::ResetWordBoundaryDefaults => {
1406                self.editor.reset_word_boundary_defaults();
1407                Ok(CommandResult::Success)
1408            }
1409            ViewCommand::ScrollTo { line } => {
1410                if line >= self.editor.line_index.line_count() {
1411                    return Err(CommandError::InvalidPosition { line, column: 0 });
1412                }
1413
1414                // Scroll operation only validates line number validity
1415                // Actual scrolling handled by frontend
1416                Ok(CommandResult::Success)
1417            }
1418            ViewCommand::GetViewport { start_row, count } => {
1419                let grid = self.editor.get_headless_grid_styled(start_row, count);
1420                Ok(CommandResult::Viewport(grid))
1421            }
1422        }
1423    }
1424
1425    // Private method: execute style command
1426    fn execute_style(&mut self, command: StyleCommand) -> Result<CommandResult, CommandError> {
1427        match command {
1428            StyleCommand::AddStyle {
1429                start,
1430                end,
1431                style_id,
1432            } => {
1433                if start >= end {
1434                    return Err(CommandError::InvalidRange { start, end });
1435                }
1436
1437                let interval = crate::intervals::Interval::new(start, end, style_id);
1438                self.editor.insert_style_interval(interval);
1439                Ok(CommandResult::Success)
1440            }
1441            StyleCommand::RemoveStyle {
1442                start,
1443                end,
1444                style_id,
1445            } => {
1446                self.editor.remove_style_interval(start, end, style_id);
1447                Ok(CommandResult::Success)
1448            }
1449            StyleCommand::Fold {
1450                start_line,
1451                end_line,
1452            } => {
1453                if start_line >= end_line {
1454                    return Err(CommandError::InvalidRange {
1455                        start: start_line,
1456                        end: end_line,
1457                    });
1458                }
1459
1460                let mut region = crate::intervals::FoldRegion::new(start_line, end_line);
1461                region.collapse();
1462                self.editor.folding_manager.add_region(region);
1463                self.editor
1464                    .sync_visual_row_index_for_logical_range(start_line, end_line);
1465                Ok(CommandResult::Success)
1466            }
1467            StyleCommand::Unfold { start_line } => {
1468                let affected = self
1469                    .editor
1470                    .folding_manager
1471                    .innermost_region_bounds_for_line(start_line);
1472                self.editor.folding_manager.expand_line(start_line);
1473                if let Some((start, end)) = affected {
1474                    self.editor
1475                        .sync_visual_row_index_for_logical_range(start, end);
1476                }
1477                Ok(CommandResult::Success)
1478            }
1479            StyleCommand::UnfoldAll => {
1480                let affected = self
1481                    .editor
1482                    .folding_manager
1483                    .regions()
1484                    .iter()
1485                    .filter(|region| region.is_collapsed)
1486                    .fold(None::<(usize, usize)>, |acc, region| match acc {
1487                        Some((start, end)) => {
1488                            Some((start.min(region.start_line), end.max(region.end_line)))
1489                        }
1490                        None => Some((region.start_line, region.end_line)),
1491                    });
1492                self.editor.folding_manager.expand_all();
1493                if let Some((start, end)) = affected {
1494                    self.editor
1495                        .sync_visual_row_index_for_logical_range(start, end);
1496                }
1497                Ok(CommandResult::Success)
1498            }
1499            StyleCommand::UpdateBracketMatchHighlights => {
1500                self.execute_update_bracket_match_highlights_command()
1501            }
1502            StyleCommand::ClearBracketMatchHighlights => {
1503                self.execute_clear_bracket_match_highlights_command()
1504            }
1505        }
1506    }
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511    use super::*;
1512
1513    #[test]
1514    fn test_edit_insert() {
1515        let mut executor = CommandExecutor::new("Hello", 80);
1516
1517        let result = executor.execute(Command::Edit(EditCommand::Insert {
1518            offset: 5,
1519            text: " World".to_string(),
1520        }));
1521
1522        assert!(result.is_ok());
1523        assert_eq!(executor.editor().get_text(), "Hello World");
1524    }
1525
1526    #[test]
1527    fn test_edit_delete() {
1528        let mut executor = CommandExecutor::new("Hello World", 80);
1529
1530        let result = executor.execute(Command::Edit(EditCommand::Delete {
1531            start: 5,
1532            length: 6,
1533        }));
1534
1535        assert!(result.is_ok());
1536        assert_eq!(executor.editor().get_text(), "Hello");
1537    }
1538
1539    #[test]
1540    fn test_edit_replace() {
1541        let mut executor = CommandExecutor::new("Hello World", 80);
1542
1543        let result = executor.execute(Command::Edit(EditCommand::Replace {
1544            start: 6,
1545            length: 5,
1546            text: "Rust".to_string(),
1547        }));
1548
1549        assert!(result.is_ok());
1550        assert_eq!(executor.editor().get_text(), "Hello Rust");
1551    }
1552
1553    #[test]
1554    fn test_cursor_move_to() {
1555        let mut executor = CommandExecutor::new("Line 1\nLine 2\nLine 3", 80);
1556
1557        let result = executor.execute(Command::Cursor(CursorCommand::MoveTo {
1558            line: 1,
1559            column: 3,
1560        }));
1561
1562        assert!(result.is_ok());
1563        assert_eq!(executor.editor().cursor_position(), Position::new(1, 3));
1564    }
1565
1566    #[test]
1567    fn test_cursor_selection() {
1568        let mut executor = CommandExecutor::new("Hello World", 80);
1569
1570        let result = executor.execute(Command::Cursor(CursorCommand::SetSelection {
1571            start: Position::new(0, 0),
1572            end: Position::new(0, 5),
1573        }));
1574
1575        assert!(result.is_ok());
1576        assert!(executor.editor().selection().is_some());
1577    }
1578
1579    #[test]
1580    fn test_view_set_width() {
1581        let mut executor = CommandExecutor::new("Test", 80);
1582
1583        let result = executor.execute(Command::View(ViewCommand::SetViewportWidth { width: 40 }));
1584
1585        assert!(result.is_ok());
1586        assert_eq!(executor.editor().viewport_width(), 40);
1587    }
1588
1589    #[test]
1590    fn test_style_add_remove() {
1591        let mut executor = CommandExecutor::new("Hello World", 80);
1592
1593        // Add style
1594        let result = executor.execute(Command::Style(StyleCommand::AddStyle {
1595            start: 0,
1596            end: 5,
1597            style_id: 1,
1598        }));
1599        assert!(result.is_ok());
1600
1601        // Remove style
1602        let result = executor.execute(Command::Style(StyleCommand::RemoveStyle {
1603            start: 0,
1604            end: 5,
1605            style_id: 1,
1606        }));
1607        assert!(result.is_ok());
1608    }
1609
1610    #[test]
1611    fn test_batch_execution() {
1612        let mut executor = CommandExecutor::new("", 80);
1613
1614        let commands = vec![
1615            Command::Edit(EditCommand::Insert {
1616                offset: 0,
1617                text: "Hello".to_string(),
1618            }),
1619            Command::Edit(EditCommand::Insert {
1620                offset: 5,
1621                text: " World".to_string(),
1622            }),
1623        ];
1624
1625        let results = executor.execute_batch(commands);
1626        assert!(results.is_ok());
1627        assert_eq!(executor.editor().get_text(), "Hello World");
1628    }
1629
1630    #[test]
1631    fn test_error_invalid_offset() {
1632        let mut executor = CommandExecutor::new("Hello", 80);
1633
1634        let result = executor.execute(Command::Edit(EditCommand::Insert {
1635            offset: 100,
1636            text: "X".to_string(),
1637        }));
1638
1639        assert!(result.is_err());
1640        assert!(matches!(
1641            result.unwrap_err(),
1642            CommandError::InvalidOffset(_)
1643        ));
1644    }
1645}