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