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(ViewCommand::SetViewportWidth { .. }) => {
371                Some(StateChangeType::ViewportChanged)
372            }
373            Command::View(ViewCommand::ScrollTo { .. } | ViewCommand::GetViewport { .. }) => None,
374            Command::Style(StyleCommand::AddStyle { .. } | StyleCommand::RemoveStyle { .. }) => {
375                Some(StateChangeType::StyleChanged)
376            }
377            Command::Style(
378                StyleCommand::Fold { .. } | StyleCommand::Unfold { .. } | StyleCommand::UnfoldAll,
379            ) => Some(StateChangeType::FoldingChanged),
380        }
381    }
382
383    /// Get current version number
384    pub fn version(&self) -> u64 {
385        self.state_version
386    }
387
388    /// Set viewport height
389    pub fn set_viewport_height(&mut self, height: usize) {
390        self.viewport_height = Some(height);
391    }
392
393    /// Set scroll position
394    pub fn set_scroll_top(&mut self, scroll_top: usize) {
395        let old_scroll = self.scroll_top;
396        self.scroll_top = scroll_top;
397
398        if old_scroll != scroll_top {
399            self.notify_change(StateChangeType::ViewportChanged);
400        }
401    }
402
403    /// Get complete editor state snapshot
404    pub fn get_full_state(&self) -> EditorState {
405        EditorState {
406            document: self.get_document_state(),
407            cursor: self.get_cursor_state(),
408            viewport: self.get_viewport_state(),
409            undo_redo: self.get_undo_redo_state(),
410            folding: self.get_folding_state(),
411            style: self.get_style_state(),
412        }
413    }
414
415    /// Get document state
416    pub fn get_document_state(&self) -> DocumentState {
417        let editor = self.executor.editor();
418        DocumentState {
419            line_count: editor.line_count(),
420            char_count: editor.char_count(),
421            byte_count: editor.get_text().len(),
422            is_modified: self.is_modified,
423            version: self.state_version,
424        }
425    }
426
427    /// Get cursor state
428    pub fn get_cursor_state(&self) -> CursorState {
429        let editor = self.executor.editor();
430        let mut selections: Vec<Selection> =
431            Vec::with_capacity(1 + editor.secondary_selections().len());
432
433        let primary = editor.selection().cloned().unwrap_or(Selection {
434            start: editor.cursor_position(),
435            end: editor.cursor_position(),
436            direction: SelectionDirection::Forward,
437        });
438        selections.push(primary);
439        selections.extend(editor.secondary_selections().iter().cloned());
440
441        let (selections, primary_selection_index) =
442            crate::selection_set::normalize_selections(selections, 0);
443        let primary = selections
444            .get(primary_selection_index)
445            .cloned()
446            .unwrap_or(Selection {
447                start: editor.cursor_position(),
448                end: editor.cursor_position(),
449                direction: SelectionDirection::Forward,
450            });
451
452        let position = primary.end;
453        let offset = editor
454            .line_index
455            .position_to_char_offset(position.line, position.column);
456
457        let selection = if primary.start == primary.end {
458            None
459        } else {
460            Some(primary)
461        };
462
463        let multi_cursors: Vec<Position> = selections
464            .iter()
465            .enumerate()
466            .filter_map(|(idx, sel)| {
467                if idx == primary_selection_index {
468                    None
469                } else {
470                    Some(sel.end)
471                }
472            })
473            .collect();
474
475        CursorState {
476            position,
477            offset,
478            multi_cursors,
479            selection,
480            selections,
481            primary_selection_index,
482        }
483    }
484
485    /// Get viewport state
486    pub fn get_viewport_state(&self) -> ViewportState {
487        let editor = self.executor.editor();
488        let total_visual_lines = editor.visual_line_count();
489        let visible_end = if let Some(height) = self.viewport_height {
490            self.scroll_top + height
491        } else {
492            total_visual_lines
493        };
494
495        ViewportState {
496            width: editor.viewport_width,
497            height: self.viewport_height,
498            scroll_top: self.scroll_top,
499            visible_lines: self.scroll_top..visible_end.min(total_visual_lines),
500        }
501    }
502
503    /// Get undo/redo state
504    pub fn get_undo_redo_state(&self) -> UndoRedoState {
505        UndoRedoState {
506            can_undo: self.executor.can_undo(),
507            can_redo: self.executor.can_redo(),
508            undo_depth: self.executor.undo_depth(),
509            redo_depth: self.executor.redo_depth(),
510            current_change_group: self.executor.current_change_group(),
511        }
512    }
513
514    /// Get folding state
515    pub fn get_folding_state(&self) -> FoldingState {
516        let editor = self.executor.editor();
517        let regions = editor.folding_manager.regions().to_vec();
518        let collapsed_line_count: usize = regions
519            .iter()
520            .filter(|r| r.is_collapsed)
521            .map(|r| r.end_line - r.start_line)
522            .sum();
523
524        let visible_logical_lines = editor.line_count() - collapsed_line_count;
525
526        FoldingState {
527            regions,
528            collapsed_line_count,
529            visible_logical_lines,
530            total_visual_lines: editor.visual_line_count(),
531        }
532    }
533
534    /// Get style state
535    pub fn get_style_state(&self) -> StyleState {
536        let editor = self.executor.editor();
537        let layered_count: usize = editor.style_layers.values().map(|t| t.len()).sum();
538        StyleState {
539            style_count: editor.interval_tree.len() + layered_count,
540        }
541    }
542
543    /// Get all styles within the specified range
544    pub fn get_styles_in_range(&self, start: usize, end: usize) -> Vec<(usize, usize, StyleId)> {
545        let editor = self.executor.editor();
546        let mut result: Vec<(usize, usize, StyleId)> = editor
547            .interval_tree
548            .query_range(start, end)
549            .iter()
550            .map(|interval| (interval.start, interval.end, interval.style_id))
551            .collect();
552
553        for tree in editor.style_layers.values() {
554            result.extend(
555                tree.query_range(start, end)
556                    .iter()
557                    .map(|interval| (interval.start, interval.end, interval.style_id)),
558            );
559        }
560
561        result.sort_unstable_by_key(|(s, e, id)| (*s, *e, *id));
562        result
563    }
564
565    /// Get all styles at the specified position
566    pub fn get_styles_at(&self, offset: usize) -> Vec<StyleId> {
567        let editor = self.executor.editor();
568        let mut styles: Vec<StyleId> = editor
569            .interval_tree
570            .query_point(offset)
571            .iter()
572            .map(|interval| interval.style_id)
573            .collect();
574
575        for tree in editor.style_layers.values() {
576            styles.extend(
577                tree.query_point(offset)
578                    .iter()
579                    .map(|interval| interval.style_id),
580            );
581        }
582
583        styles.sort_unstable();
584        styles.dedup();
585        styles
586    }
587
588    /// Replace all intervals in the specified style layer.
589    ///
590    /// Suitable for scenarios such as LSP semantic highlighting and simple syntax highlighting that require "full layer refresh".
591    /// This method only triggers `StyleChanged` once, avoiding version number explosion due to individual insertions.
592    pub fn replace_style_layer(&mut self, layer: StyleLayerId, intervals: Vec<Interval>) {
593        let editor = self.executor.editor_mut();
594
595        if intervals.is_empty() {
596            editor.style_layers.remove(&layer);
597            self.mark_modified(StateChangeType::StyleChanged);
598            return;
599        }
600
601        let tree = editor.style_layers.entry(layer).or_default();
602        tree.clear();
603
604        for interval in intervals {
605            if interval.start < interval.end {
606                tree.insert(interval);
607            }
608        }
609
610        self.mark_modified(StateChangeType::StyleChanged);
611    }
612
613    /// Clear the specified style layer.
614    pub fn clear_style_layer(&mut self, layer: StyleLayerId) {
615        let editor = self.executor.editor_mut();
616        editor.style_layers.remove(&layer);
617        self.mark_modified(StateChangeType::StyleChanged);
618    }
619
620    /// Replace folding regions wholesale.
621    ///
622    /// If `preserve_collapsed` is true, any region that matches an existing collapsed region
623    /// (`start_line`, `end_line`) will remain collapsed after replacement.
624    pub fn replace_folding_regions(
625        &mut self,
626        mut regions: Vec<FoldRegion>,
627        preserve_collapsed: bool,
628    ) {
629        if preserve_collapsed {
630            let collapsed: HashSet<(usize, usize)> = self
631                .editor()
632                .folding_manager
633                .regions()
634                .iter()
635                .filter(|r| r.is_collapsed)
636                .map(|r| (r.start_line, r.end_line))
637                .collect();
638
639            for region in &mut regions {
640                if collapsed.contains(&(region.start_line, region.end_line)) {
641                    region.is_collapsed = true;
642                }
643            }
644        }
645
646        self.editor_mut().folding_manager.replace_regions(regions);
647        self.mark_modified(StateChangeType::FoldingChanged);
648    }
649
650    /// Clear all folding regions.
651    pub fn clear_folding_regions(&mut self) {
652        self.editor_mut().folding_manager.clear();
653        self.mark_modified(StateChangeType::FoldingChanged);
654    }
655
656    /// Apply derived-state edits produced by a document processor (highlighting, folding, etc.).
657    pub fn apply_processing_edits<I>(&mut self, edits: I)
658    where
659        I: IntoIterator<Item = ProcessingEdit>,
660    {
661        for edit in edits {
662            match edit {
663                ProcessingEdit::ReplaceStyleLayer { layer, intervals } => {
664                    self.replace_style_layer(layer, intervals);
665                }
666                ProcessingEdit::ClearStyleLayer { layer } => {
667                    self.clear_style_layer(layer);
668                }
669                ProcessingEdit::ReplaceFoldingRegions {
670                    regions,
671                    preserve_collapsed,
672                } => {
673                    self.replace_folding_regions(regions, preserve_collapsed);
674                }
675                ProcessingEdit::ClearFoldingRegions => {
676                    self.clear_folding_regions();
677                }
678            }
679        }
680    }
681
682    /// Run a [`DocumentProcessor`] against the current document and apply its edits.
683    pub fn apply_processor<P>(&mut self, processor: &mut P) -> Result<(), P::Error>
684    where
685        P: DocumentProcessor,
686    {
687        let edits = processor.process(self)?;
688        self.apply_processing_edits(edits);
689        Ok(())
690    }
691
692    /// Get viewport content
693    pub fn get_viewport_content(&self, start_row: usize, count: usize) -> HeadlessGrid {
694        let editor = self.executor.editor();
695        let text = editor.get_text();
696        let generator = crate::SnapshotGenerator::from_text(&text, editor.viewport_width);
697        generator.get_headless_grid(start_row, count)
698    }
699
700    /// Get styled viewport content (by visual line).
701    ///
702    /// - Supports soft wrapping (based on `LayoutEngine`)
703    /// - `Cell.styles` will contain the merged result of `interval_tree` and all `style_layers`
704    pub fn get_viewport_content_styled(
705        &self,
706        start_visual_row: usize,
707        count: usize,
708    ) -> HeadlessGrid {
709        self.executor
710            .editor()
711            .get_headless_grid_styled(start_visual_row, count)
712    }
713
714    /// Subscribe to state change notifications
715    pub fn subscribe<F>(&mut self, callback: F)
716    where
717        F: FnMut(&StateChange) + Send + 'static,
718    {
719        self.callbacks.push(Box::new(callback));
720    }
721
722    /// Check if state has changed since a version
723    pub fn has_changed_since(&self, version: u64) -> bool {
724        self.state_version > version
725    }
726
727    /// Mark document as modified and increment version number
728    pub fn mark_modified(&mut self, change_type: StateChangeType) {
729        self.mark_modified_internal(change_type, None);
730    }
731
732    fn mark_modified_internal(
733        &mut self,
734        change_type: StateChangeType,
735        is_modified_override: Option<bool>,
736    ) {
737        let old_version = self.state_version;
738        self.state_version += 1;
739
740        // Only mark as modified for document content changes
741        if matches!(change_type, StateChangeType::DocumentModified) {
742            self.is_modified = is_modified_override.unwrap_or(true);
743        }
744
745        let change = StateChange::new(change_type, old_version, self.state_version);
746        self.notify_callbacks(&change);
747    }
748
749    /// Mark document as unmodified (e.g., after saving)
750    pub fn mark_saved(&mut self) {
751        self.executor.mark_clean();
752        self.is_modified = false;
753    }
754
755    /// Notify state change (without modifying version number)
756    fn notify_change(&mut self, change_type: StateChangeType) {
757        let change = StateChange::new(change_type, self.state_version, self.state_version);
758        self.notify_callbacks(&change);
759    }
760
761    /// Notify all callbacks
762    fn notify_callbacks(&mut self, change: &StateChange) {
763        for callback in &mut self.callbacks {
764            callback(change);
765        }
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    #[test]
774    fn test_document_state() {
775        let manager = EditorStateManager::new("Hello World\nLine 2", 80);
776        let doc_state = manager.get_document_state();
777
778        assert_eq!(doc_state.line_count, 2);
779        assert_eq!(doc_state.char_count, 18); // Including newline
780        assert!(!doc_state.is_modified);
781        assert_eq!(doc_state.version, 0);
782    }
783
784    #[test]
785    fn test_cursor_state() {
786        let manager = EditorStateManager::new("Hello World", 80);
787        let cursor_state = manager.get_cursor_state();
788
789        assert_eq!(cursor_state.position, Position::new(0, 0));
790        assert_eq!(cursor_state.offset, 0);
791        assert!(cursor_state.selection.is_none());
792    }
793
794    #[test]
795    fn test_viewport_state() {
796        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
797        manager.set_viewport_height(10);
798        manager.set_scroll_top(1);
799
800        let viewport_state = manager.get_viewport_state();
801
802        assert_eq!(viewport_state.width, 80);
803        assert_eq!(viewport_state.height, Some(10));
804        assert_eq!(viewport_state.scroll_top, 1);
805        assert_eq!(viewport_state.visible_lines, 1..3);
806    }
807
808    #[test]
809    fn test_folding_state() {
810        let manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
811        let folding_state = manager.get_folding_state();
812
813        assert_eq!(folding_state.regions.len(), 0);
814        assert_eq!(folding_state.collapsed_line_count, 0);
815        assert_eq!(folding_state.visible_logical_lines, 3);
816    }
817
818    #[test]
819    fn test_style_state() {
820        let manager = EditorStateManager::new("Hello World", 80);
821        let style_state = manager.get_style_state();
822
823        assert_eq!(style_state.style_count, 0);
824    }
825
826    #[test]
827    fn test_full_state() {
828        let manager = EditorStateManager::new("Test", 80);
829        let full_state = manager.get_full_state();
830
831        assert_eq!(full_state.document.line_count, 1);
832        assert_eq!(full_state.cursor.position, Position::new(0, 0));
833        assert_eq!(full_state.viewport.width, 80);
834    }
835
836    #[test]
837    fn test_version_tracking() {
838        let mut manager = EditorStateManager::new("Test", 80);
839
840        assert_eq!(manager.version(), 0);
841        assert!(!manager.has_changed_since(0));
842
843        manager.mark_modified(StateChangeType::DocumentModified);
844
845        assert_eq!(manager.version(), 1);
846        assert!(manager.has_changed_since(0));
847        assert!(!manager.has_changed_since(1));
848    }
849
850    #[test]
851    fn test_modification_tracking() {
852        let mut manager = EditorStateManager::new("Test", 80);
853
854        assert!(!manager.get_document_state().is_modified);
855
856        manager.mark_modified(StateChangeType::DocumentModified);
857        assert!(manager.get_document_state().is_modified);
858
859        manager.mark_saved();
860        assert!(!manager.get_document_state().is_modified);
861    }
862
863    #[test]
864    fn test_undo_redo_state_and_dirty_tracking() {
865        let mut manager = EditorStateManager::empty(80);
866
867        let state = manager.get_undo_redo_state();
868        assert!(!state.can_undo);
869        assert!(!state.can_redo);
870
871        manager
872            .execute(Command::Edit(EditCommand::InsertText {
873                text: "abc".to_string(),
874            }))
875            .unwrap();
876
877        assert!(manager.get_document_state().is_modified);
878        let state = manager.get_undo_redo_state();
879        assert!(state.can_undo);
880        assert!(!state.can_redo);
881        assert_eq!(state.undo_depth, 1);
882
883        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
884        assert!(!manager.get_document_state().is_modified);
885        let state = manager.get_undo_redo_state();
886        assert!(!state.can_undo);
887        assert!(state.can_redo);
888
889        manager.execute(Command::Edit(EditCommand::Redo)).unwrap();
890        assert!(manager.get_document_state().is_modified);
891        let state = manager.get_undo_redo_state();
892        assert!(state.can_undo);
893        assert!(!state.can_redo);
894    }
895
896    #[test]
897    fn test_state_change_callback() {
898        use std::sync::{Arc, Mutex};
899
900        let mut manager = EditorStateManager::new("Test", 80);
901
902        let callback_called = Arc::new(Mutex::new(false));
903        let callback_called_clone = callback_called.clone();
904
905        manager.subscribe(move |_change| {
906            *callback_called_clone.lock().unwrap() = true;
907        });
908
909        manager.mark_modified(StateChangeType::CursorMoved);
910
911        // Verify callback was called
912        assert!(*callback_called.lock().unwrap());
913    }
914
915    #[test]
916    fn test_execute_cursor_noop_does_not_bump_version() {
917        let mut manager = EditorStateManager::new("A", 80);
918        assert_eq!(manager.version(), 0);
919
920        // Continue moving left at the beginning of file (unchanged after clamp), version should not change.
921        manager
922            .execute(Command::Cursor(CursorCommand::MoveBy {
923                delta_line: 0,
924                delta_column: -1,
925            }))
926            .unwrap();
927        assert_eq!(manager.editor().cursor_position(), Position::new(0, 0));
928        assert_eq!(manager.version(), 0);
929
930        // Move to end of line (changed), version increments.
931        manager
932            .execute(Command::Cursor(CursorCommand::MoveTo {
933                line: 0,
934                column: usize::MAX,
935            }))
936            .unwrap();
937        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
938        assert_eq!(manager.version(), 1);
939
940        // Continue moving right at end of line (unchanged after clamp), version should not change.
941        let version_before = manager.version();
942        manager
943            .execute(Command::Cursor(CursorCommand::MoveBy {
944                delta_line: 0,
945                delta_column: 1,
946            }))
947            .unwrap();
948        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
949        assert_eq!(manager.version(), version_before);
950    }
951
952    #[test]
953    fn test_viewport_height() {
954        let mut manager = EditorStateManager::new("Test", 80);
955
956        assert_eq!(manager.get_viewport_state().height, None);
957
958        manager.set_viewport_height(20);
959        assert_eq!(manager.get_viewport_state().height, Some(20));
960    }
961
962    #[test]
963    fn test_scroll_position() {
964        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3\nLine 4", 80);
965        manager.set_viewport_height(2);
966
967        assert_eq!(manager.get_viewport_state().scroll_top, 0);
968        assert_eq!(manager.get_viewport_state().visible_lines, 0..2);
969
970        manager.set_scroll_top(2);
971        assert_eq!(manager.get_viewport_state().scroll_top, 2);
972        assert_eq!(manager.get_viewport_state().visible_lines, 2..4);
973    }
974
975    #[test]
976    fn test_get_styles() {
977        let mut manager = EditorStateManager::new("Hello World", 80);
978
979        // Add style via editor
980        manager
981            .editor_mut()
982            .interval_tree
983            .insert(crate::intervals::Interval::new(0, 5, 1));
984
985        let styles = manager.get_styles_in_range(0, 10);
986        assert_eq!(styles.len(), 1);
987        assert_eq!(styles[0], (0, 5, 1));
988
989        let styles_at = manager.get_styles_at(3);
990        assert_eq!(styles_at.len(), 1);
991        assert_eq!(styles_at[0], 1);
992    }
993
994    #[test]
995    fn test_replace_style_layer_affects_queries() {
996        let mut manager = EditorStateManager::new("Hello", 80);
997
998        manager.replace_style_layer(
999            StyleLayerId::SEMANTIC_TOKENS,
1000            vec![Interval::new(0, 1, 100)],
1001        );
1002
1003        assert_eq!(manager.get_styles_at(0), vec![100]);
1004
1005        // Base layer + layered styles are merged.
1006        manager
1007            .editor_mut()
1008            .interval_tree
1009            .insert(Interval::new(0, 5, 1));
1010
1011        assert_eq!(manager.get_styles_at(0), vec![1, 100]);
1012    }
1013
1014    #[test]
1015    fn test_viewport_content_styled_wraps_and_includes_styles() {
1016        let mut manager = EditorStateManager::new("abcdef", 3);
1017
1018        // Highlight "bcd" across a wrap boundary: "abc" | "def"
1019        manager.replace_style_layer(StyleLayerId::SIMPLE_SYNTAX, vec![Interval::new(1, 4, 7)]);
1020
1021        let grid = manager.get_viewport_content_styled(0, 10);
1022        assert_eq!(grid.actual_line_count(), 2);
1023
1024        let line0 = &grid.lines[0];
1025        assert_eq!(line0.logical_line_index, 0);
1026        assert!(!line0.is_wrapped_part);
1027        assert_eq!(line0.cells.len(), 3);
1028        assert_eq!(line0.cells[0].ch, 'a');
1029        assert_eq!(line0.cells[1].ch, 'b');
1030        assert_eq!(line0.cells[2].ch, 'c');
1031        assert_eq!(line0.cells[0].styles, Vec::<StyleId>::new());
1032        assert_eq!(line0.cells[1].styles, vec![7]);
1033        assert_eq!(line0.cells[2].styles, vec![7]);
1034
1035        let line1 = &grid.lines[1];
1036        assert_eq!(line1.logical_line_index, 0);
1037        assert!(line1.is_wrapped_part);
1038        assert_eq!(line1.cells.len(), 3);
1039        assert_eq!(line1.cells[0].ch, 'd');
1040        assert_eq!(line1.cells[0].styles, vec![7]);
1041        assert_eq!(line1.cells[1].ch, 'e');
1042        assert_eq!(line1.cells[1].styles, Vec::<StyleId>::new());
1043    }
1044}