Skip to main content

editor_core/
state.rs

1//! Editor State Interface
2//!
3//! Provides a complete state query interface for the editor, used for frontend rendering and state synchronization.
4//!
5//! # Overview
6//!
7//! The state interface layer exposes the editor's internal state to the frontend in a structured, immutable manner.
8//! It supports:
9//!
10//! - **State Queries**: Retrieve document, cursor, viewport, and other state information
11//! - **Version Tracking**: Track state changes through version numbers
12//! - **Change Notifications**: Subscribe to state change events
13//! - **Viewport Management**: Obtain rendering data for visible regions
14//!
15//! # Example
16//!
17//! ```rust
18//! use editor_core::{EditorStateManager, StateChangeType};
19//!
20//! let mut manager = EditorStateManager::new("Hello, World!", 80);
21//!
22//! // Query document state
23//! let doc_state = manager.get_document_state();
24//! println!("Line count: {}", doc_state.line_count);
25//!
26//! // Subscribe to state changes
27//! manager.subscribe(|change| {
28//!     println!("State changed: {:?}", change.change_type);
29//! });
30//!
31//! // Modify document and mark changes
32//! manager.editor_mut().piece_table.insert(0, "New: ");
33//! manager.mark_modified(StateChangeType::DocumentModified);
34//! ```
35
36use crate::intervals::{FoldRegion, Interval, StyleId, StyleLayerId};
37use crate::processing::{DocumentProcessor, ProcessingEdit};
38use crate::snapshot::HeadlessGrid;
39use crate::{
40    Command, CommandError, CommandExecutor, CommandResult, CursorCommand, EditCommand, EditorCore,
41    Position, Selection, SelectionDirection, StyleCommand, ViewCommand,
42};
43use std::collections::HashSet;
44use std::ops::Range;
45
46/// Document state
47#[derive(Debug, Clone)]
48pub struct DocumentState {
49    /// Total documentLine count
50    pub line_count: usize,
51    /// Total document character count
52    pub char_count: usize,
53    /// Total document byte count
54    pub byte_count: usize,
55    /// Whether document has been modified
56    pub is_modified: bool,
57    /// Document version number (incremented after each modification)
58    pub version: u64,
59}
60
61/// Cursor state
62#[derive(Debug, Clone)]
63pub struct CursorState {
64    /// Primary cursor position (logical coordinates)
65    pub position: Position,
66    /// Primary cursor position (char offsets)
67    pub offset: usize,
68    /// Multi-cursor list (active positions of secondary carets, excluding primary)
69    pub multi_cursors: Vec<Position>,
70    /// Primary selection range (only primary; returns None for empty selection)
71    pub selection: Option<Selection>,
72    /// All selection set (including primary; each Selection may be empty)
73    pub selections: Vec<Selection>,
74    /// Index of primary in `selections`
75    pub primary_selection_index: usize,
76}
77
78/// Viewport state
79#[derive(Debug, Clone)]
80pub struct ViewportState {
81    /// Viewport width (in character cells)
82    pub width: usize,
83    /// Viewport height (line count, determined by the frontend)
84    pub height: Option<usize>,
85    /// Current scroll position (visual line number)
86    pub scroll_top: usize,
87    /// Visible visual line range
88    pub visible_lines: Range<usize>,
89}
90
91/// Undo/redo stack state
92#[derive(Debug, Clone)]
93pub struct UndoRedoState {
94    /// Can undo
95    pub can_undo: bool,
96    /// Can redo
97    pub can_redo: bool,
98    /// Undo stack depth
99    pub undo_depth: usize,
100    /// Redo stack depth
101    pub redo_depth: usize,
102    /// Current change group ID
103    pub current_change_group: Option<usize>,
104}
105
106/// Folding state
107#[derive(Debug, Clone)]
108pub struct FoldingState {
109    /// All folding regions
110    pub regions: Vec<FoldRegion>,
111    /// Count of collapsed lines
112    pub collapsed_line_count: usize,
113    /// Count of visible logical lines
114    pub visible_logical_lines: usize,
115    /// Total visual line count (considering folding)
116    pub total_visual_lines: usize,
117}
118
119/// Style state
120#[derive(Debug, Clone)]
121pub struct StyleState {
122    /// Total number of style intervals
123    pub style_count: usize,
124}
125
126/// State change type
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum StateChangeType {
129    /// Document content modified
130    DocumentModified,
131    /// Cursor moved
132    CursorMoved,
133    /// Selection changed
134    SelectionChanged,
135    /// Viewport changed
136    ViewportChanged,
137    /// Folding state changed
138    FoldingChanged,
139    /// Style changed
140    StyleChanged,
141}
142
143/// State change record
144#[derive(Debug, Clone)]
145pub struct StateChange {
146    /// Change type
147    pub change_type: StateChangeType,
148    /// Old version number
149    pub old_version: u64,
150    /// New version number
151    pub new_version: u64,
152    /// Affected region (character offset range)
153    pub affected_region: Option<Range<usize>>,
154}
155
156impl StateChange {
157    /// Create a new state change record without an affected region.
158    pub fn new(change_type: StateChangeType, old_version: u64, new_version: u64) -> Self {
159        Self {
160            change_type,
161            old_version,
162            new_version,
163            affected_region: None,
164        }
165    }
166
167    /// Attach the affected character range to this change record.
168    pub fn with_region(mut self, region: Range<usize>) -> Self {
169        self.affected_region = Some(region);
170        self
171    }
172}
173
174/// Complete editor state snapshot
175#[derive(Debug, Clone)]
176pub struct EditorState {
177    /// Document state
178    pub document: DocumentState,
179    /// Cursor state
180    pub cursor: CursorState,
181    /// Viewport state
182    pub viewport: ViewportState,
183    /// Undo/redo state
184    pub undo_redo: UndoRedoState,
185    /// Folding state
186    pub folding: FoldingState,
187    /// Style state
188    pub style: StyleState,
189}
190
191/// State change callback function type
192pub type StateChangeCallback = Box<dyn FnMut(&StateChange) + Send>;
193
194/// Editor state manager
195///
196/// `EditorStateManager` wraps the command executor ([`CommandExecutor`]) and its internal [`EditorCore`]
197/// and provides the following features:
198///
199/// - **State Queries**: Retrieve various state snapshots (document, cursor, viewport, etc.)
200/// - **Version Tracking**: Automatically increment version number after each modification, supporting incremental updates
201/// - **Change Notifications**: Notify subscribers of state changes via callback mechanism
202/// - **Viewport Management**: Manage scroll position and visible regions
203/// - **Modification Tracking**: Track whether the document has been modified (for save prompts)
204///
205/// # Architecture Notes
206///
207/// The state manager adopts a "unidirectional data flow" pattern:
208///
209/// 1. Frontend executes commands via [`execute()`](EditorStateManager::execute) (recommended)
210/// 2. Or directly modifies internal state via [`editor_mut()`](EditorStateManager::editor_mut) (advanced usage)
211/// 3. If using `editor_mut()`, call [`mark_modified()`](EditorStateManager::mark_modified) after modification
212///    to mark the change type
213/// 3. Manager increments version number and triggers all subscribed callbacks
214/// 4. Frontend retrieves the latest state via various `get_*_state()` methods
215///
216/// # Example
217///
218/// ```rust
219/// use editor_core::{Command, EditCommand, EditorStateManager};
220///
221/// let mut manager = EditorStateManager::new("Initial text", 80);
222///
223/// // Subscribe to state changes
224/// manager.subscribe(|change| {
225///     println!("Version {} -> {}: {:?}",
226///         change.old_version, change.new_version, change.change_type);
227/// });
228///
229/// // Modify document (automatically maintains consistency + automatically triggers state notifications)
230/// manager.execute(Command::Edit(EditCommand::Insert {
231///     offset: 0,
232///     text: "New: ".to_string(),
233/// })).unwrap();
234///
235/// // Query state
236/// let doc_state = manager.get_document_state();
237/// assert!(doc_state.is_modified);
238/// assert_eq!(doc_state.version, 1);
239/// ```
240pub struct EditorStateManager {
241    /// Command executor (wraps EditorCore and maintains consistency)
242    executor: CommandExecutor,
243    /// State version number
244    state_version: u64,
245    /// Whether document has been modified
246    is_modified: bool,
247    /// State change callback list
248    callbacks: Vec<StateChangeCallback>,
249    /// Current scroll position
250    scroll_top: usize,
251    /// Viewport height (optional)
252    viewport_height: Option<usize>,
253}
254
255impl EditorStateManager {
256    /// Create a new state manager
257    pub fn new(text: &str, viewport_width: usize) -> Self {
258        Self {
259            executor: CommandExecutor::new(text, viewport_width),
260            state_version: 0,
261            is_modified: false,
262            callbacks: Vec::new(),
263            scroll_top: 0,
264            viewport_height: None,
265        }
266    }
267
268    /// Create an empty state manager
269    pub fn empty(viewport_width: usize) -> Self {
270        Self::new("", viewport_width)
271    }
272
273    /// Get a reference to the Editor Core
274    pub fn editor(&self) -> &EditorCore {
275        self.executor.editor()
276    }
277
278    /// Get a mutable reference to the Editor Core
279    pub fn editor_mut(&mut self) -> &mut EditorCore {
280        self.executor.editor_mut()
281    }
282
283    /// Execute a command and automatically trigger state change notifications.
284    ///
285    /// - This method calls the underlying [`CommandExecutor`] to ensure consistency of components
286    ///   such as `piece_table` / `line_index` / `layout_engine`.
287    /// - For commands that cause state changes, [`mark_modified`](Self::mark_modified) is automatically called.
288    /// - For pure query commands (such as `GetViewport`), the version number is not incremented.
289    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
290        let change_type = Self::change_type_for_command(&command);
291        let is_delete_like = matches!(
292            &command,
293            Command::Edit(EditCommand::Backspace | EditCommand::DeleteForward)
294        );
295
296        // Detect changes for potential no-ops: when command execution succeeds but state doesn't change, version should not increment.
297        let cursor_before = self.executor.editor().cursor_position();
298        let selection_before = self.executor.editor().selection().cloned();
299        let secondary_before = self.executor.editor().secondary_selections().to_vec();
300        let viewport_width_before = self.executor.editor().viewport_width;
301        let char_count_before = self.executor.editor().char_count();
302
303        let result = self.executor.execute(command)?;
304        let char_count_after = self.executor.editor().char_count();
305
306        if let Some(change_type) = change_type {
307            let changed = match change_type {
308                StateChangeType::CursorMoved => {
309                    self.executor.editor().cursor_position() != cursor_before
310                        || self.executor.editor().secondary_selections()
311                            != secondary_before.as_slice()
312                }
313                StateChangeType::SelectionChanged => {
314                    self.executor.editor().cursor_position() != cursor_before
315                        || self.executor.editor().selection().cloned() != selection_before
316                        || self.executor.editor().secondary_selections()
317                            != secondary_before.as_slice()
318                }
319                StateChangeType::ViewportChanged => {
320                    self.executor.editor().viewport_width != viewport_width_before
321                }
322                StateChangeType::DocumentModified => {
323                    // EditCommand::Backspace / DeleteForward can be valid no-ops at boundaries.
324                    // Detect via char count change (they only delete text).
325                    if is_delete_like {
326                        char_count_after != char_count_before
327                    } else {
328                        true
329                    }
330                }
331                // Style/folding commands are currently treated as "success means change".
332                StateChangeType::FoldingChanged | StateChangeType::StyleChanged => true,
333            };
334
335            if changed {
336                if matches!(change_type, StateChangeType::DocumentModified) {
337                    let is_modified = !self.executor.is_clean();
338                    self.mark_modified_internal(change_type, Some(is_modified));
339                } else {
340                    self.mark_modified_internal(change_type, None);
341                }
342            }
343        }
344
345        Ok(result)
346    }
347
348    fn change_type_for_command(command: &Command) -> Option<StateChangeType> {
349        match command {
350            Command::Edit(EditCommand::InsertText { text }) if text.is_empty() => None,
351            Command::Edit(EditCommand::Delete { length: 0, .. }) => None,
352            Command::Edit(EditCommand::Replace {
353                length: 0, text, ..
354            }) if text.is_empty() => None,
355            Command::Edit(EditCommand::EndUndoGroup) => None,
356            Command::Edit(_) => Some(StateChangeType::DocumentModified),
357            Command::Cursor(CursorCommand::MoveTo { .. } | CursorCommand::MoveBy { .. }) => {
358                Some(StateChangeType::CursorMoved)
359            }
360            Command::Cursor(
361                CursorCommand::SetSelection { .. }
362                | CursorCommand::ExtendSelection { .. }
363                | CursorCommand::ClearSelection
364                | CursorCommand::SetSelections { .. }
365                | CursorCommand::ClearSecondarySelections
366                | CursorCommand::SetRectSelection { .. }
367                | CursorCommand::FindNext { .. }
368                | CursorCommand::FindPrev { .. },
369            ) => Some(StateChangeType::SelectionChanged),
370            Command::View(
371                ViewCommand::SetViewportWidth { .. } | ViewCommand::SetTabWidth { .. },
372            ) => Some(StateChangeType::ViewportChanged),
373            Command::View(
374                ViewCommand::SetTabKeyBehavior { .. }
375                | ViewCommand::ScrollTo { .. }
376                | ViewCommand::GetViewport { .. },
377            ) => None,
378            Command::Style(StyleCommand::AddStyle { .. } | StyleCommand::RemoveStyle { .. }) => {
379                Some(StateChangeType::StyleChanged)
380            }
381            Command::Style(
382                StyleCommand::Fold { .. } | StyleCommand::Unfold { .. } | StyleCommand::UnfoldAll,
383            ) => Some(StateChangeType::FoldingChanged),
384        }
385    }
386
387    /// Get current version number
388    pub fn version(&self) -> u64 {
389        self.state_version
390    }
391
392    /// Set viewport height
393    pub fn set_viewport_height(&mut self, height: usize) {
394        self.viewport_height = Some(height);
395    }
396
397    /// Set scroll position
398    pub fn set_scroll_top(&mut self, scroll_top: usize) {
399        let old_scroll = self.scroll_top;
400        self.scroll_top = scroll_top;
401
402        if old_scroll != scroll_top {
403            self.notify_change(StateChangeType::ViewportChanged);
404        }
405    }
406
407    /// Get complete editor state snapshot
408    pub fn get_full_state(&self) -> EditorState {
409        EditorState {
410            document: self.get_document_state(),
411            cursor: self.get_cursor_state(),
412            viewport: self.get_viewport_state(),
413            undo_redo: self.get_undo_redo_state(),
414            folding: self.get_folding_state(),
415            style: self.get_style_state(),
416        }
417    }
418
419    /// Get document state
420    pub fn get_document_state(&self) -> DocumentState {
421        let editor = self.executor.editor();
422        DocumentState {
423            line_count: editor.line_count(),
424            char_count: editor.char_count(),
425            byte_count: editor.get_text().len(),
426            is_modified: self.is_modified,
427            version: self.state_version,
428        }
429    }
430
431    /// Get cursor state
432    pub fn get_cursor_state(&self) -> CursorState {
433        let editor = self.executor.editor();
434        let mut selections: Vec<Selection> =
435            Vec::with_capacity(1 + editor.secondary_selections().len());
436
437        let primary = editor.selection().cloned().unwrap_or(Selection {
438            start: editor.cursor_position(),
439            end: editor.cursor_position(),
440            direction: SelectionDirection::Forward,
441        });
442        selections.push(primary);
443        selections.extend(editor.secondary_selections().iter().cloned());
444
445        let (selections, primary_selection_index) =
446            crate::selection_set::normalize_selections(selections, 0);
447        let primary = selections
448            .get(primary_selection_index)
449            .cloned()
450            .unwrap_or(Selection {
451                start: editor.cursor_position(),
452                end: editor.cursor_position(),
453                direction: SelectionDirection::Forward,
454            });
455
456        let position = primary.end;
457        let offset = editor
458            .line_index
459            .position_to_char_offset(position.line, position.column);
460
461        let selection = if primary.start == primary.end {
462            None
463        } else {
464            Some(primary)
465        };
466
467        let multi_cursors: Vec<Position> = selections
468            .iter()
469            .enumerate()
470            .filter_map(|(idx, sel)| {
471                if idx == primary_selection_index {
472                    None
473                } else {
474                    Some(sel.end)
475                }
476            })
477            .collect();
478
479        CursorState {
480            position,
481            offset,
482            multi_cursors,
483            selection,
484            selections,
485            primary_selection_index,
486        }
487    }
488
489    /// Get viewport state
490    pub fn get_viewport_state(&self) -> ViewportState {
491        let editor = self.executor.editor();
492        let total_visual_lines = editor.visual_line_count();
493        let visible_end = if let Some(height) = self.viewport_height {
494            self.scroll_top + height
495        } else {
496            total_visual_lines
497        };
498
499        ViewportState {
500            width: editor.viewport_width,
501            height: self.viewport_height,
502            scroll_top: self.scroll_top,
503            visible_lines: self.scroll_top..visible_end.min(total_visual_lines),
504        }
505    }
506
507    /// Get undo/redo state
508    pub fn get_undo_redo_state(&self) -> UndoRedoState {
509        UndoRedoState {
510            can_undo: self.executor.can_undo(),
511            can_redo: self.executor.can_redo(),
512            undo_depth: self.executor.undo_depth(),
513            redo_depth: self.executor.redo_depth(),
514            current_change_group: self.executor.current_change_group(),
515        }
516    }
517
518    /// Get folding state
519    pub fn get_folding_state(&self) -> FoldingState {
520        let editor = self.executor.editor();
521        let regions = editor.folding_manager.regions().to_vec();
522        let collapsed_line_count: usize = regions
523            .iter()
524            .filter(|r| r.is_collapsed)
525            .map(|r| r.end_line - r.start_line)
526            .sum();
527
528        let visible_logical_lines = editor.line_count() - collapsed_line_count;
529
530        FoldingState {
531            regions,
532            collapsed_line_count,
533            visible_logical_lines,
534            total_visual_lines: editor.visual_line_count(),
535        }
536    }
537
538    /// Get style state
539    pub fn get_style_state(&self) -> StyleState {
540        let editor = self.executor.editor();
541        let layered_count: usize = editor.style_layers.values().map(|t| t.len()).sum();
542        StyleState {
543            style_count: editor.interval_tree.len() + layered_count,
544        }
545    }
546
547    /// Get all styles within the specified range
548    pub fn get_styles_in_range(&self, start: usize, end: usize) -> Vec<(usize, usize, StyleId)> {
549        let editor = self.executor.editor();
550        let mut result: Vec<(usize, usize, StyleId)> = editor
551            .interval_tree
552            .query_range(start, end)
553            .iter()
554            .map(|interval| (interval.start, interval.end, interval.style_id))
555            .collect();
556
557        for tree in editor.style_layers.values() {
558            result.extend(
559                tree.query_range(start, end)
560                    .iter()
561                    .map(|interval| (interval.start, interval.end, interval.style_id)),
562            );
563        }
564
565        result.sort_unstable_by_key(|(s, e, id)| (*s, *e, *id));
566        result
567    }
568
569    /// Get all styles at the specified position
570    pub fn get_styles_at(&self, offset: usize) -> Vec<StyleId> {
571        let editor = self.executor.editor();
572        let mut styles: Vec<StyleId> = editor
573            .interval_tree
574            .query_point(offset)
575            .iter()
576            .map(|interval| interval.style_id)
577            .collect();
578
579        for tree in editor.style_layers.values() {
580            styles.extend(
581                tree.query_point(offset)
582                    .iter()
583                    .map(|interval| interval.style_id),
584            );
585        }
586
587        styles.sort_unstable();
588        styles.dedup();
589        styles
590    }
591
592    /// Replace all intervals in the specified style layer.
593    ///
594    /// Suitable for scenarios such as LSP semantic highlighting and simple syntax highlighting that require "full layer refresh".
595    /// This method only triggers `StyleChanged` once, avoiding version number explosion due to individual insertions.
596    pub fn replace_style_layer(&mut self, layer: StyleLayerId, intervals: Vec<Interval>) {
597        let editor = self.executor.editor_mut();
598
599        if intervals.is_empty() {
600            editor.style_layers.remove(&layer);
601            self.mark_modified(StateChangeType::StyleChanged);
602            return;
603        }
604
605        let tree = editor.style_layers.entry(layer).or_default();
606        tree.clear();
607
608        for interval in intervals {
609            if interval.start < interval.end {
610                tree.insert(interval);
611            }
612        }
613
614        self.mark_modified(StateChangeType::StyleChanged);
615    }
616
617    /// Clear the specified style layer.
618    pub fn clear_style_layer(&mut self, layer: StyleLayerId) {
619        let editor = self.executor.editor_mut();
620        editor.style_layers.remove(&layer);
621        self.mark_modified(StateChangeType::StyleChanged);
622    }
623
624    /// Replace folding regions wholesale.
625    ///
626    /// If `preserve_collapsed` is true, any region that matches an existing collapsed region
627    /// (`start_line`, `end_line`) will remain collapsed after replacement.
628    pub fn replace_folding_regions(
629        &mut self,
630        mut regions: Vec<FoldRegion>,
631        preserve_collapsed: bool,
632    ) {
633        if preserve_collapsed {
634            let collapsed: HashSet<(usize, usize)> = self
635                .editor()
636                .folding_manager
637                .regions()
638                .iter()
639                .filter(|r| r.is_collapsed)
640                .map(|r| (r.start_line, r.end_line))
641                .collect();
642
643            for region in &mut regions {
644                if collapsed.contains(&(region.start_line, region.end_line)) {
645                    region.is_collapsed = true;
646                }
647            }
648        }
649
650        self.editor_mut().folding_manager.replace_regions(regions);
651        self.mark_modified(StateChangeType::FoldingChanged);
652    }
653
654    /// Clear all folding regions.
655    pub fn clear_folding_regions(&mut self) {
656        self.editor_mut().folding_manager.clear();
657        self.mark_modified(StateChangeType::FoldingChanged);
658    }
659
660    /// Apply derived-state edits produced by a document processor (highlighting, folding, etc.).
661    pub fn apply_processing_edits<I>(&mut self, edits: I)
662    where
663        I: IntoIterator<Item = ProcessingEdit>,
664    {
665        for edit in edits {
666            match edit {
667                ProcessingEdit::ReplaceStyleLayer { layer, intervals } => {
668                    self.replace_style_layer(layer, intervals);
669                }
670                ProcessingEdit::ClearStyleLayer { layer } => {
671                    self.clear_style_layer(layer);
672                }
673                ProcessingEdit::ReplaceFoldingRegions {
674                    regions,
675                    preserve_collapsed,
676                } => {
677                    self.replace_folding_regions(regions, preserve_collapsed);
678                }
679                ProcessingEdit::ClearFoldingRegions => {
680                    self.clear_folding_regions();
681                }
682            }
683        }
684    }
685
686    /// Run a [`DocumentProcessor`] against the current document and apply its edits.
687    pub fn apply_processor<P>(&mut self, processor: &mut P) -> Result<(), P::Error>
688    where
689        P: DocumentProcessor,
690    {
691        let edits = processor.process(self)?;
692        self.apply_processing_edits(edits);
693        Ok(())
694    }
695
696    /// Get viewport content
697    pub fn get_viewport_content(&self, start_row: usize, count: usize) -> HeadlessGrid {
698        let editor = self.executor.editor();
699        let text = editor.get_text();
700        let generator = crate::SnapshotGenerator::from_text_with_tab_width(
701            &text,
702            editor.viewport_width,
703            editor.layout_engine.tab_width(),
704        );
705        generator.get_headless_grid(start_row, count)
706    }
707
708    /// Get styled viewport content (by visual line).
709    ///
710    /// - Supports soft wrapping (based on `LayoutEngine`)
711    /// - `Cell.styles` will contain the merged result of `interval_tree` and all `style_layers`
712    pub fn get_viewport_content_styled(
713        &self,
714        start_visual_row: usize,
715        count: usize,
716    ) -> HeadlessGrid {
717        self.executor
718            .editor()
719            .get_headless_grid_styled(start_visual_row, count)
720    }
721
722    /// Subscribe to state change notifications
723    pub fn subscribe<F>(&mut self, callback: F)
724    where
725        F: FnMut(&StateChange) + Send + 'static,
726    {
727        self.callbacks.push(Box::new(callback));
728    }
729
730    /// Check if state has changed since a version
731    pub fn has_changed_since(&self, version: u64) -> bool {
732        self.state_version > version
733    }
734
735    /// Mark document as modified and increment version number
736    pub fn mark_modified(&mut self, change_type: StateChangeType) {
737        self.mark_modified_internal(change_type, None);
738    }
739
740    fn mark_modified_internal(
741        &mut self,
742        change_type: StateChangeType,
743        is_modified_override: Option<bool>,
744    ) {
745        let old_version = self.state_version;
746        self.state_version += 1;
747
748        // Only mark as modified for document content changes
749        if matches!(change_type, StateChangeType::DocumentModified) {
750            self.is_modified = is_modified_override.unwrap_or(true);
751        }
752
753        let change = StateChange::new(change_type, old_version, self.state_version);
754        self.notify_callbacks(&change);
755    }
756
757    /// Mark document as unmodified (e.g., after saving)
758    pub fn mark_saved(&mut self) {
759        self.executor.mark_clean();
760        self.is_modified = false;
761    }
762
763    /// Notify state change (without modifying version number)
764    fn notify_change(&mut self, change_type: StateChangeType) {
765        let change = StateChange::new(change_type, self.state_version, self.state_version);
766        self.notify_callbacks(&change);
767    }
768
769    /// Notify all callbacks
770    fn notify_callbacks(&mut self, change: &StateChange) {
771        for callback in &mut self.callbacks {
772            callback(change);
773        }
774    }
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780
781    #[test]
782    fn test_document_state() {
783        let manager = EditorStateManager::new("Hello World\nLine 2", 80);
784        let doc_state = manager.get_document_state();
785
786        assert_eq!(doc_state.line_count, 2);
787        assert_eq!(doc_state.char_count, 18); // Including newline
788        assert!(!doc_state.is_modified);
789        assert_eq!(doc_state.version, 0);
790    }
791
792    #[test]
793    fn test_cursor_state() {
794        let manager = EditorStateManager::new("Hello World", 80);
795        let cursor_state = manager.get_cursor_state();
796
797        assert_eq!(cursor_state.position, Position::new(0, 0));
798        assert_eq!(cursor_state.offset, 0);
799        assert!(cursor_state.selection.is_none());
800    }
801
802    #[test]
803    fn test_viewport_state() {
804        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
805        manager.set_viewport_height(10);
806        manager.set_scroll_top(1);
807
808        let viewport_state = manager.get_viewport_state();
809
810        assert_eq!(viewport_state.width, 80);
811        assert_eq!(viewport_state.height, Some(10));
812        assert_eq!(viewport_state.scroll_top, 1);
813        assert_eq!(viewport_state.visible_lines, 1..3);
814    }
815
816    #[test]
817    fn test_folding_state() {
818        let manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
819        let folding_state = manager.get_folding_state();
820
821        assert_eq!(folding_state.regions.len(), 0);
822        assert_eq!(folding_state.collapsed_line_count, 0);
823        assert_eq!(folding_state.visible_logical_lines, 3);
824    }
825
826    #[test]
827    fn test_style_state() {
828        let manager = EditorStateManager::new("Hello World", 80);
829        let style_state = manager.get_style_state();
830
831        assert_eq!(style_state.style_count, 0);
832    }
833
834    #[test]
835    fn test_full_state() {
836        let manager = EditorStateManager::new("Test", 80);
837        let full_state = manager.get_full_state();
838
839        assert_eq!(full_state.document.line_count, 1);
840        assert_eq!(full_state.cursor.position, Position::new(0, 0));
841        assert_eq!(full_state.viewport.width, 80);
842    }
843
844    #[test]
845    fn test_version_tracking() {
846        let mut manager = EditorStateManager::new("Test", 80);
847
848        assert_eq!(manager.version(), 0);
849        assert!(!manager.has_changed_since(0));
850
851        manager.mark_modified(StateChangeType::DocumentModified);
852
853        assert_eq!(manager.version(), 1);
854        assert!(manager.has_changed_since(0));
855        assert!(!manager.has_changed_since(1));
856    }
857
858    #[test]
859    fn test_modification_tracking() {
860        let mut manager = EditorStateManager::new("Test", 80);
861
862        assert!(!manager.get_document_state().is_modified);
863
864        manager.mark_modified(StateChangeType::DocumentModified);
865        assert!(manager.get_document_state().is_modified);
866
867        manager.mark_saved();
868        assert!(!manager.get_document_state().is_modified);
869    }
870
871    #[test]
872    fn test_undo_redo_state_and_dirty_tracking() {
873        let mut manager = EditorStateManager::empty(80);
874
875        let state = manager.get_undo_redo_state();
876        assert!(!state.can_undo);
877        assert!(!state.can_redo);
878
879        manager
880            .execute(Command::Edit(EditCommand::InsertText {
881                text: "abc".to_string(),
882            }))
883            .unwrap();
884
885        assert!(manager.get_document_state().is_modified);
886        let state = manager.get_undo_redo_state();
887        assert!(state.can_undo);
888        assert!(!state.can_redo);
889        assert_eq!(state.undo_depth, 1);
890
891        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
892        assert!(!manager.get_document_state().is_modified);
893        let state = manager.get_undo_redo_state();
894        assert!(!state.can_undo);
895        assert!(state.can_redo);
896
897        manager.execute(Command::Edit(EditCommand::Redo)).unwrap();
898        assert!(manager.get_document_state().is_modified);
899        let state = manager.get_undo_redo_state();
900        assert!(state.can_undo);
901        assert!(!state.can_redo);
902    }
903
904    #[test]
905    fn test_insert_tab_undo_restores_clean_state() {
906        let mut manager = EditorStateManager::empty(80);
907        assert!(!manager.get_document_state().is_modified);
908
909        manager
910            .execute(Command::Edit(EditCommand::InsertTab))
911            .unwrap();
912        assert!(manager.get_document_state().is_modified);
913
914        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
915        assert!(!manager.get_document_state().is_modified);
916    }
917
918    #[test]
919    fn test_insert_tab_spaces_undo_restores_clean_state() {
920        let mut manager = EditorStateManager::empty(80);
921        manager
922            .execute(Command::View(ViewCommand::SetTabKeyBehavior {
923                behavior: crate::TabKeyBehavior::Spaces,
924            }))
925            .unwrap();
926
927        manager
928            .execute(Command::Edit(EditCommand::InsertTab))
929            .unwrap();
930        assert!(manager.get_document_state().is_modified);
931
932        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
933        assert!(!manager.get_document_state().is_modified);
934    }
935
936    #[test]
937    fn test_state_change_callback() {
938        use std::sync::{Arc, Mutex};
939
940        let mut manager = EditorStateManager::new("Test", 80);
941
942        let callback_called = Arc::new(Mutex::new(false));
943        let callback_called_clone = callback_called.clone();
944
945        manager.subscribe(move |_change| {
946            *callback_called_clone.lock().unwrap() = true;
947        });
948
949        manager.mark_modified(StateChangeType::CursorMoved);
950
951        // Verify callback was called
952        assert!(*callback_called.lock().unwrap());
953    }
954
955    #[test]
956    fn test_execute_cursor_noop_does_not_bump_version() {
957        let mut manager = EditorStateManager::new("A", 80);
958        assert_eq!(manager.version(), 0);
959
960        // Continue moving left at the beginning of file (unchanged after clamp), version should not change.
961        manager
962            .execute(Command::Cursor(CursorCommand::MoveBy {
963                delta_line: 0,
964                delta_column: -1,
965            }))
966            .unwrap();
967        assert_eq!(manager.editor().cursor_position(), Position::new(0, 0));
968        assert_eq!(manager.version(), 0);
969
970        // Move to end of line (changed), version increments.
971        manager
972            .execute(Command::Cursor(CursorCommand::MoveTo {
973                line: 0,
974                column: usize::MAX,
975            }))
976            .unwrap();
977        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
978        assert_eq!(manager.version(), 1);
979
980        // Continue moving right at end of line (unchanged after clamp), version should not change.
981        let version_before = manager.version();
982        manager
983            .execute(Command::Cursor(CursorCommand::MoveBy {
984                delta_line: 0,
985                delta_column: 1,
986            }))
987            .unwrap();
988        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
989        assert_eq!(manager.version(), version_before);
990    }
991
992    #[test]
993    fn test_viewport_height() {
994        let mut manager = EditorStateManager::new("Test", 80);
995
996        assert_eq!(manager.get_viewport_state().height, None);
997
998        manager.set_viewport_height(20);
999        assert_eq!(manager.get_viewport_state().height, Some(20));
1000    }
1001
1002    #[test]
1003    fn test_scroll_position() {
1004        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3\nLine 4", 80);
1005        manager.set_viewport_height(2);
1006
1007        assert_eq!(manager.get_viewport_state().scroll_top, 0);
1008        assert_eq!(manager.get_viewport_state().visible_lines, 0..2);
1009
1010        manager.set_scroll_top(2);
1011        assert_eq!(manager.get_viewport_state().scroll_top, 2);
1012        assert_eq!(manager.get_viewport_state().visible_lines, 2..4);
1013    }
1014
1015    #[test]
1016    fn test_get_styles() {
1017        let mut manager = EditorStateManager::new("Hello World", 80);
1018
1019        // Add style via editor
1020        manager
1021            .editor_mut()
1022            .interval_tree
1023            .insert(crate::intervals::Interval::new(0, 5, 1));
1024
1025        let styles = manager.get_styles_in_range(0, 10);
1026        assert_eq!(styles.len(), 1);
1027        assert_eq!(styles[0], (0, 5, 1));
1028
1029        let styles_at = manager.get_styles_at(3);
1030        assert_eq!(styles_at.len(), 1);
1031        assert_eq!(styles_at[0], 1);
1032    }
1033
1034    #[test]
1035    fn test_replace_style_layer_affects_queries() {
1036        let mut manager = EditorStateManager::new("Hello", 80);
1037
1038        manager.replace_style_layer(
1039            StyleLayerId::SEMANTIC_TOKENS,
1040            vec![Interval::new(0, 1, 100)],
1041        );
1042
1043        assert_eq!(manager.get_styles_at(0), vec![100]);
1044
1045        // Base layer + layered styles are merged.
1046        manager
1047            .editor_mut()
1048            .interval_tree
1049            .insert(Interval::new(0, 5, 1));
1050
1051        assert_eq!(manager.get_styles_at(0), vec![1, 100]);
1052    }
1053
1054    #[test]
1055    fn test_viewport_content_styled_wraps_and_includes_styles() {
1056        let mut manager = EditorStateManager::new("abcdef", 3);
1057
1058        // Highlight "bcd" across a wrap boundary: "abc" | "def"
1059        manager.replace_style_layer(StyleLayerId::SIMPLE_SYNTAX, vec![Interval::new(1, 4, 7)]);
1060
1061        let grid = manager.get_viewport_content_styled(0, 10);
1062        assert_eq!(grid.actual_line_count(), 2);
1063
1064        let line0 = &grid.lines[0];
1065        assert_eq!(line0.logical_line_index, 0);
1066        assert!(!line0.is_wrapped_part);
1067        assert_eq!(line0.cells.len(), 3);
1068        assert_eq!(line0.cells[0].ch, 'a');
1069        assert_eq!(line0.cells[1].ch, 'b');
1070        assert_eq!(line0.cells[2].ch, 'c');
1071        assert_eq!(line0.cells[0].styles, Vec::<StyleId>::new());
1072        assert_eq!(line0.cells[1].styles, vec![7]);
1073        assert_eq!(line0.cells[2].styles, vec![7]);
1074
1075        let line1 = &grid.lines[1];
1076        assert_eq!(line1.logical_line_index, 0);
1077        assert!(line1.is_wrapped_part);
1078        assert_eq!(line1.cells.len(), 3);
1079        assert_eq!(line1.cells[0].ch, 'd');
1080        assert_eq!(line1.cells[0].styles, vec![7]);
1081        assert_eq!(line1.cells[1].ch, 'e');
1082        assert_eq!(line1.cells[1].styles, Vec::<StyleId>::new());
1083    }
1084}