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::{Command, EditCommand, EditorStateManager};
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 through the command path so text, layout, and derived state stay in sync.
32//! manager.execute(Command::Edit(EditCommand::Insert {
33//!     offset: 0,
34//!     text: "New: ".to_string(),
35//! })).unwrap();
36//! ```
37
38use crate::delta::TextDelta;
39use crate::intervals::{FoldRegion, Interval, StyleId, StyleLayerId};
40use crate::processing::{DocumentProcessor, ProcessingEdit};
41use crate::snapshot::{ComposedGrid, HeadlessGrid};
42use crate::{
43    AnchorBias, Command, CommandError, CommandExecutor, CommandResult, CursorCommand, Decoration,
44    DecorationLayerId, Diagnostic, EditCommand, EditorCore, LineEnding, Position, Selection,
45    SelectionDirection, StyleCommand, TextAnchor, UndoHistoryRestoreError, UndoHistorySnapshot,
46    ViewCommand,
47};
48use std::collections::BTreeMap;
49use std::ops::Range;
50use std::sync::Arc;
51
52/// Document state
53#[derive(Debug, Clone)]
54pub struct DocumentState {
55    /// Total documentLine count
56    pub line_count: usize,
57    /// Total document character count
58    pub char_count: usize,
59    /// Total document byte count
60    pub byte_count: usize,
61    /// Whether document has been modified
62    pub is_modified: bool,
63    /// Document version number (incremented after each modification)
64    pub version: u64,
65}
66
67/// Cursor state
68#[derive(Debug, Clone)]
69pub struct CursorState {
70    /// Primary cursor position (logical coordinates)
71    pub position: Position,
72    /// Primary cursor position (char offsets)
73    pub offset: usize,
74    /// Multi-cursor list (active positions of secondary carets, excluding primary)
75    pub multi_cursors: Vec<Position>,
76    /// Primary selection range (only primary; returns None for empty selection)
77    pub selection: Option<Selection>,
78    /// All selection set (including primary; each Selection may be empty)
79    pub selections: Vec<Selection>,
80    /// Index of primary in `selections`
81    pub primary_selection_index: usize,
82}
83
84/// Viewport state
85#[derive(Debug, Clone)]
86pub struct ViewportState {
87    /// Viewport width (in character cells)
88    pub width: usize,
89    /// Viewport height (line count, determined by the frontend)
90    pub height: Option<usize>,
91    /// Current scroll position (visual line number)
92    pub scroll_top: usize,
93    /// Sub-row smooth-scroll offset (0..=65535, normalized).
94    pub sub_row_offset: u16,
95    /// Overscan rows used to compute `prefetch_lines`.
96    pub overscan_rows: usize,
97    /// Visible visual line range
98    pub visible_lines: Range<usize>,
99    /// Recommended prefetch range (visible range expanded by `overscan_rows`).
100    pub prefetch_lines: Range<usize>,
101    /// Total visual lines under current wrap/folding state.
102    pub total_visual_lines: usize,
103}
104
105/// Smooth-scrolling state for single-view manager.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct SmoothScrollState {
108    /// Top visual row anchor.
109    pub top_visual_row: usize,
110    /// Sub-row offset within top row (0..=65535, normalized).
111    pub sub_row_offset: u16,
112    /// Overscan rows for prefetching.
113    pub overscan_rows: usize,
114}
115
116/// Undo/redo stack state
117#[derive(Debug, Clone)]
118pub struct UndoRedoState {
119    /// Can undo
120    pub can_undo: bool,
121    /// Can redo
122    pub can_redo: bool,
123    /// Undo stack depth
124    pub undo_depth: usize,
125    /// Redo stack depth
126    pub redo_depth: usize,
127    /// Number of redo branches available at the current history node.
128    pub redo_branch_count: usize,
129    /// Currently selected redo branch index, if any.
130    pub selected_redo_branch_index: Option<usize>,
131    /// Current change group ID
132    pub current_change_group: Option<usize>,
133}
134
135/// Folding state
136#[derive(Debug, Clone)]
137pub struct FoldingState {
138    /// All folding regions
139    pub regions: Vec<FoldRegion>,
140    /// Count of collapsed lines
141    pub collapsed_line_count: usize,
142    /// Count of visible logical lines
143    pub visible_logical_lines: usize,
144    /// Total visual line count (considering folding)
145    pub total_visual_lines: usize,
146}
147
148/// Diagnostics state
149#[derive(Debug, Clone)]
150pub struct DiagnosticsState {
151    /// Total number of diagnostics.
152    pub diagnostics_count: usize,
153}
154
155/// Decorations state
156#[derive(Debug, Clone)]
157pub struct DecorationsState {
158    /// Total number of decoration layers.
159    pub layer_count: usize,
160    /// Total number of decorations (across all layers).
161    pub decoration_count: usize,
162}
163
164/// Style state
165#[derive(Debug, Clone)]
166pub struct StyleState {
167    /// Total number of style intervals
168    pub style_count: usize,
169}
170
171/// State change type
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum StateChangeType {
174    /// Document content modified
175    DocumentModified,
176    /// Cursor moved
177    CursorMoved,
178    /// Selection changed
179    SelectionChanged,
180    /// Navigation / anchor state changed (bookmarks, marks, jump list, ...).
181    NavigationChanged,
182    /// Viewport changed
183    ViewportChanged,
184    /// Folding state changed
185    FoldingChanged,
186    /// Style changed
187    StyleChanged,
188    /// Decorations changed
189    DecorationsChanged,
190    /// Diagnostics changed
191    DiagnosticsChanged,
192    /// Document symbols / outline changed
193    SymbolsChanged,
194}
195
196/// State change record
197#[derive(Debug, Clone)]
198pub struct StateChange {
199    /// Change type
200    pub change_type: StateChangeType,
201    /// Old version number
202    pub old_version: u64,
203    /// New version number
204    pub new_version: u64,
205    /// Affected region (character offset range)
206    pub affected_region: Option<Range<usize>>,
207    /// Structured text delta for document changes (if available).
208    pub text_delta: Option<Arc<TextDelta>>,
209}
210
211impl StateChange {
212    /// Create a new state change record without an affected region.
213    pub fn new(change_type: StateChangeType, old_version: u64, new_version: u64) -> Self {
214        Self {
215            change_type,
216            old_version,
217            new_version,
218            affected_region: None,
219            text_delta: None,
220        }
221    }
222
223    /// Attach the affected character range to this change record.
224    pub fn with_region(mut self, region: Range<usize>) -> Self {
225        self.affected_region = Some(region);
226        self
227    }
228
229    /// Attach a structured text delta to this change record.
230    pub fn with_text_delta(mut self, delta: Arc<TextDelta>) -> Self {
231        self.text_delta = Some(delta);
232        self
233    }
234}
235
236/// Complete editor state snapshot
237#[derive(Debug, Clone)]
238pub struct EditorState {
239    /// Document state
240    pub document: DocumentState,
241    /// Cursor state
242    pub cursor: CursorState,
243    /// Viewport state
244    pub viewport: ViewportState,
245    /// Undo/redo state
246    pub undo_redo: UndoRedoState,
247    /// Folding state
248    pub folding: FoldingState,
249    /// Diagnostics state
250    pub diagnostics: DiagnosticsState,
251    /// Decorations state
252    pub decorations: DecorationsState,
253    /// Style state
254    pub style: StyleState,
255}
256
257/// State change callback function type
258pub type StateChangeCallback = Box<dyn FnMut(&StateChange) + Send>;
259
260#[derive(Debug, Default, Clone, PartialEq, Eq)]
261struct BookmarkSet {
262    anchors: Vec<TextAnchor>,
263}
264
265impl BookmarkSet {
266    fn toggle_line_start(&mut self, line_start_offset: usize) -> bool {
267        let anchor = TextAnchor::new(line_start_offset, AnchorBias::Left);
268        match self
269            .anchors
270            .binary_search_by_key(&anchor.offset, |a| a.offset)
271        {
272            Ok(idx) => {
273                self.anchors.remove(idx);
274                false
275            }
276            Err(idx) => {
277                self.anchors.insert(idx, anchor);
278                true
279            }
280        }
281    }
282
283    fn clear(&mut self) {
284        self.anchors.clear();
285    }
286
287    fn apply_delta(&mut self, delta: &TextDelta) {
288        for a in &mut self.anchors {
289            a.apply_delta(delta);
290        }
291        self.anchors.sort_by_key(|a| a.offset);
292        self.anchors.dedup_by_key(|a| a.offset);
293    }
294
295    fn line_numbers(&self, line_index: &crate::LineIndex) -> Vec<usize> {
296        let mut lines: Vec<usize> = self
297            .anchors
298            .iter()
299            .map(|a| line_index.char_offset_to_position(a.offset).0)
300            .collect();
301        lines.sort_unstable();
302        lines.dedup();
303        lines
304    }
305
306    fn next_after_line_start(&self, current_line_start: usize) -> Option<TextAnchor> {
307        self.anchors
308            .iter()
309            .copied()
310            .find(|a| a.offset > current_line_start)
311            .or_else(|| self.anchors.first().copied())
312    }
313
314    fn prev_before_line_start(&self, current_line_start: usize) -> Option<TextAnchor> {
315        self.anchors
316            .iter()
317            .copied()
318            .rfind(|a| a.offset < current_line_start)
319            .or_else(|| self.anchors.last().copied())
320    }
321}
322
323#[derive(Debug, Default, Clone, PartialEq, Eq)]
324struct MarkSet {
325    marks: BTreeMap<String, TextAnchor>,
326}
327
328impl MarkSet {
329    fn set(&mut self, name: String, offset: usize) {
330        self.marks
331            .insert(name, TextAnchor::new(offset, AnchorBias::Right));
332    }
333
334    fn get(&self, name: &str) -> Option<TextAnchor> {
335        self.marks.get(name).copied()
336    }
337
338    fn remove(&mut self, name: &str) -> bool {
339        self.marks.remove(name).is_some()
340    }
341
342    fn clear(&mut self) {
343        self.marks.clear();
344    }
345
346    fn names(&self) -> Vec<String> {
347        self.marks.keys().cloned().collect()
348    }
349
350    fn apply_delta(&mut self, delta: &TextDelta) {
351        for anchor in self.marks.values_mut() {
352            anchor.apply_delta(delta);
353        }
354    }
355}
356
357#[derive(Debug, Default, Clone, PartialEq, Eq)]
358struct JumpList {
359    back: Vec<TextAnchor>,
360    forward: Vec<TextAnchor>,
361    max_len: usize,
362}
363
364impl JumpList {
365    fn new(max_len: usize) -> Self {
366        Self {
367            back: Vec::new(),
368            forward: Vec::new(),
369            max_len: max_len.max(1),
370        }
371    }
372
373    fn record(&mut self, offset: usize) {
374        let anchor = TextAnchor::new(offset, AnchorBias::Right);
375        if self.back.last().is_some_and(|last| *last == anchor) {
376            return;
377        }
378
379        self.back.push(anchor);
380        self.forward.clear();
381
382        if self.back.len() > self.max_len {
383            let overflow = self.back.len() - self.max_len;
384            self.back.drain(0..overflow);
385        }
386    }
387
388    fn back(&mut self, current_offset: usize) -> Option<TextAnchor> {
389        let current = TextAnchor::new(current_offset, AnchorBias::Right);
390        let target = self.back.pop()?;
391        if !self.forward.last().is_some_and(|last| *last == current) {
392            self.forward.push(current);
393        }
394        Some(target)
395    }
396
397    fn forward(&mut self, current_offset: usize) -> Option<TextAnchor> {
398        let current = TextAnchor::new(current_offset, AnchorBias::Right);
399        let target = self.forward.pop()?;
400        if !self.back.last().is_some_and(|last| *last == current) {
401            self.back.push(current);
402        }
403        Some(target)
404    }
405
406    fn clear(&mut self) {
407        self.back.clear();
408        self.forward.clear();
409    }
410
411    fn apply_delta(&mut self, delta: &TextDelta) {
412        for a in self.back.iter_mut().chain(self.forward.iter_mut()) {
413            a.apply_delta(delta);
414        }
415    }
416}
417
418/// Editor state manager
419///
420/// `EditorStateManager` wraps the command executor ([`CommandExecutor`]) and its internal [`EditorCore`]
421/// and provides the following features:
422///
423/// - **State Queries**: Retrieve various state snapshots (document, cursor, viewport, etc.)
424/// - **Version Tracking**: Automatically increment version number after each modification, supporting incremental updates
425/// - **Change Notifications**: Notify subscribers of state changes via callback mechanism
426/// - **Viewport Management**: Manage scroll position and visible regions
427/// - **Modification Tracking**: Track whether the document has been modified (for save prompts)
428///
429/// # Architecture Notes
430///
431/// The state manager adopts a "unidirectional data flow" pattern:
432///
433/// 1. Frontends execute edits, cursor movement, viewport changes, and other user actions via
434///    [`execute()`](EditorStateManager::execute) or [`CommandExecutor`].
435/// 2. Derived state such as diagnostics, styles, decorations, symbols, and folds is replaced via
436///    explicit manager or [`Workspace`](crate::Workspace) methods so notifications stay in sync.
437/// 3. [`editor_mut()`](EditorStateManager::editor_mut) is reserved for advanced access to
438///    `EditorCore` public methods; `EditorCore` fields remain private and cannot be mutated
439///    directly.
440/// 4. Manager increments version number and triggers all subscribed callbacks.
441/// 5. Frontends retrieve the latest state via various `get_*_state()` methods.
442///
443/// # Example
444///
445/// ```rust
446/// use editor_core::{Command, EditCommand, EditorStateManager};
447///
448/// let mut manager = EditorStateManager::new("Initial text", 80);
449///
450/// // Subscribe to state changes
451/// manager.subscribe(|change| {
452///     println!("Version {} -> {}: {:?}",
453///         change.old_version, change.new_version, change.change_type);
454/// });
455///
456/// // Modify document (automatically maintains consistency + automatically triggers state notifications)
457/// manager.execute(Command::Edit(EditCommand::Insert {
458///     offset: 0,
459///     text: "New: ".to_string(),
460/// })).unwrap();
461///
462/// // Query state
463/// let doc_state = manager.get_document_state();
464/// assert!(doc_state.is_modified);
465/// assert_eq!(doc_state.version, 1);
466/// ```
467pub struct EditorStateManager {
468    /// Command executor (wraps EditorCore and maintains consistency)
469    executor: CommandExecutor,
470    /// State version number
471    state_version: u64,
472    /// Whether document has been modified
473    is_modified: bool,
474    /// State change callback list
475    callbacks: Vec<StateChangeCallback>,
476    /// Current scroll position
477    scroll_top: usize,
478    /// Sub-row smooth-scroll offset.
479    scroll_sub_row_offset: u16,
480    /// Overscan rows for prefetching.
481    overscan_rows: usize,
482    /// Viewport height (optional)
483    viewport_height: Option<usize>,
484    /// Structured text delta produced by the last document edit.
485    last_text_delta: Option<Arc<TextDelta>>,
486    bookmarks: BookmarkSet,
487    marks: MarkSet,
488    jump_list: JumpList,
489}
490
491impl EditorStateManager {
492    /// Create a new state manager
493    pub fn new(text: &str, viewport_width: usize) -> Self {
494        Self {
495            executor: CommandExecutor::new(text, viewport_width),
496            state_version: 0,
497            is_modified: false,
498            callbacks: Vec::new(),
499            scroll_top: 0,
500            scroll_sub_row_offset: 0,
501            overscan_rows: 0,
502            viewport_height: None,
503            last_text_delta: None,
504            bookmarks: BookmarkSet::default(),
505            marks: MarkSet::default(),
506            jump_list: JumpList::new(200),
507        }
508    }
509
510    /// Create an empty state manager
511    pub fn empty(viewport_width: usize) -> Self {
512        Self::new("", viewport_width)
513    }
514
515    /// Get a reference to the Editor Core
516    pub fn editor(&self) -> &EditorCore {
517        self.executor.editor()
518    }
519
520    /// Get a mutable reference to the field-private Editor Core.
521    ///
522    /// Prefer [`execute`](Self::execute) and the explicit derived-state methods on this manager for
523    /// observable mutations. This accessor is intended for advanced callers that need to invoke
524    /// public [`EditorCore`] methods directly; it does not expose private storage, layout, folding,
525    /// style, or cursor fields.
526    pub fn editor_mut(&mut self) -> &mut EditorCore {
527        self.executor.editor_mut()
528    }
529
530    /// Get the preferred line ending for saving this document.
531    pub fn line_ending(&self) -> LineEnding {
532        self.executor.line_ending()
533    }
534
535    /// Override the preferred line ending for saving this document.
536    pub fn set_line_ending(&mut self, line_ending: LineEnding) {
537        self.executor.set_line_ending(line_ending);
538    }
539
540    /// Return `true` if this editor currently has an active snippet session.
541    pub fn has_active_snippet_session(&self) -> bool {
542        self.executor.has_active_snippet_session()
543    }
544
545    /// Get the current document text converted to the preferred line ending for saving.
546    pub fn get_text_for_saving(&self) -> String {
547        let text = self.editor().get_text();
548        self.line_ending().apply_to_text(&text)
549    }
550
551    /// Execute a command and automatically trigger state change notifications.
552    ///
553    /// - This method calls the underlying [`CommandExecutor`] to ensure consistency of components
554    ///   such as the text buffer, line index, and layout engine.
555    /// - For commands that cause state changes, [`mark_modified`](Self::mark_modified) is automatically called.
556    /// - For pure query commands (such as `GetViewport`), the version number is not incremented.
557    pub fn execute(&mut self, command: Command) -> Result<CommandResult, CommandError> {
558        let change_type = Self::change_type_for_command(&command);
559        let is_delete_like = matches!(
560            &command,
561            Command::Edit(EditCommand::Backspace | EditCommand::DeleteForward)
562        );
563
564        // Detect changes for potential no-ops: when command execution succeeds but state doesn't change, version should not increment.
565        let cursor_before = self.executor.editor().cursor_position();
566        let selection_before = self.executor.editor().selection().cloned();
567        let secondary_before = self.executor.editor().secondary_selections().to_vec();
568        let viewport_width_before = self.executor.editor().viewport_width();
569        let char_count_before = self.executor.editor().char_count();
570
571        let result = self.executor.execute(command)?;
572        let char_count_after = self.executor.editor().char_count();
573        let delta_present = self.executor.last_text_delta().is_some();
574
575        if let Some(change_type) = change_type {
576            let changed = match change_type {
577                StateChangeType::CursorMoved => {
578                    self.executor.editor().cursor_position() != cursor_before
579                        || self.executor.editor().secondary_selections()
580                            != secondary_before.as_slice()
581                }
582                StateChangeType::SelectionChanged => {
583                    self.executor.editor().cursor_position() != cursor_before
584                        || self.executor.editor().selection().cloned() != selection_before
585                        || self.executor.editor().secondary_selections()
586                            != secondary_before.as_slice()
587                }
588                StateChangeType::ViewportChanged => {
589                    self.executor.editor().viewport_width() != viewport_width_before
590                }
591                StateChangeType::DocumentModified => {
592                    // EditCommand::Backspace / DeleteForward can be valid no-ops at boundaries.
593                    // Detect via char count change (they only delete text).
594                    if is_delete_like {
595                        char_count_after != char_count_before
596                    } else {
597                        delta_present
598                    }
599                }
600                StateChangeType::NavigationChanged => true,
601                // Style/folding/diagnostics commands are currently treated as "success means change".
602                StateChangeType::FoldingChanged
603                | StateChangeType::StyleChanged
604                | StateChangeType::DecorationsChanged
605                | StateChangeType::DiagnosticsChanged
606                | StateChangeType::SymbolsChanged => true,
607            };
608
609            if changed {
610                if matches!(change_type, StateChangeType::DocumentModified) {
611                    let is_modified = !self.executor.is_clean();
612                    let delta = self.executor.take_last_text_delta().map(Arc::new);
613                    if let Some(ref delta) = delta {
614                        self.bookmarks.apply_delta(delta);
615                        self.marks.apply_delta(delta);
616                        self.jump_list.apply_delta(delta);
617                    }
618                    self.last_text_delta = delta.clone();
619                    self.mark_modified_internal(change_type, Some(is_modified), delta);
620                } else {
621                    self.mark_modified_internal(change_type, None, None);
622                }
623            }
624        }
625
626        Ok(result)
627    }
628
629    fn change_type_for_command(command: &Command) -> Option<StateChangeType> {
630        match command {
631            Command::Edit(EditCommand::Delete { length: 0, .. }) => None,
632            Command::Edit(EditCommand::Replace {
633                length: 0, text, ..
634            }) if text.is_empty() => None,
635            Command::Edit(EditCommand::EndUndoGroup) => None,
636            Command::Edit(_) => Some(StateChangeType::DocumentModified),
637            Command::Cursor(
638                CursorCommand::MoveTo { .. }
639                | CursorCommand::MoveBy { .. }
640                | CursorCommand::MoveVisualBy { .. }
641                | CursorCommand::MoveToVisual { .. }
642                | CursorCommand::MoveToLineStart
643                | CursorCommand::MoveToLineEnd
644                | CursorCommand::MoveToVisualLineStart
645                | CursorCommand::MoveToVisualLineEnd
646                | CursorCommand::MoveGraphemeLeft
647                | CursorCommand::MoveGraphemeRight
648                | CursorCommand::MoveWordLeft
649                | CursorCommand::MoveWordRight
650                | CursorCommand::MoveToMatchingBracket,
651            ) => Some(StateChangeType::CursorMoved),
652            Command::Cursor(
653                CursorCommand::SetSelection { .. }
654                | CursorCommand::ExtendSelection { .. }
655                | CursorCommand::ClearSelection
656                | CursorCommand::SetSelections { .. }
657                | CursorCommand::ClearSecondarySelections
658                | CursorCommand::SetRectSelection { .. }
659                | CursorCommand::SelectLine
660                | CursorCommand::SelectWord
661                | CursorCommand::ExpandSelection
662                | CursorCommand::ExpandSelectionBy { .. }
663                | CursorCommand::SnippetNextPlaceholder
664                | CursorCommand::SnippetPrevPlaceholder
665                | CursorCommand::AddCursorAbove
666                | CursorCommand::AddCursorBelow
667                | CursorCommand::AddNextOccurrence { .. }
668                | CursorCommand::AddAllOccurrences { .. }
669                | CursorCommand::FindNext { .. }
670                | CursorCommand::FindPrev { .. },
671            ) => Some(StateChangeType::SelectionChanged),
672            Command::View(
673                ViewCommand::SetViewportWidth { .. }
674                | ViewCommand::SetWrapMode { .. }
675                | ViewCommand::SetWrapIndent { .. }
676                | ViewCommand::SetTabWidth { .. },
677            ) => Some(StateChangeType::ViewportChanged),
678            Command::View(
679                ViewCommand::SetTabKeyBehavior { .. }
680                | ViewCommand::SetIndentationConfig { .. }
681                | ViewCommand::SetAutoPairsConfig { .. }
682                | ViewCommand::SetAutoPairsEnabled { .. }
683                | ViewCommand::SetWordBoundaryAsciiBoundaryChars { .. }
684                | ViewCommand::ResetWordBoundaryDefaults
685                | ViewCommand::ScrollTo { .. }
686                | ViewCommand::GetViewport { .. },
687            ) => None,
688            Command::Style(
689                StyleCommand::AddStyle { .. }
690                | StyleCommand::RemoveStyle { .. }
691                | StyleCommand::UpdateBracketMatchHighlights
692                | StyleCommand::ClearBracketMatchHighlights,
693            ) => Some(StateChangeType::StyleChanged),
694            Command::Style(
695                StyleCommand::Fold { .. } | StyleCommand::Unfold { .. } | StyleCommand::UnfoldAll,
696            ) => Some(StateChangeType::FoldingChanged),
697        }
698    }
699
700    /// Get current version number
701    pub fn version(&self) -> u64 {
702        self.state_version
703    }
704
705    /// Set viewport height
706    pub fn set_viewport_height(&mut self, height: usize) {
707        self.viewport_height = Some(height);
708    }
709
710    /// Set scroll position
711    pub fn set_scroll_top(&mut self, scroll_top: usize) {
712        let old_scroll = self.scroll_top;
713        self.scroll_top = scroll_top;
714
715        if old_scroll != scroll_top {
716            self.notify_change(StateChangeType::ViewportChanged);
717        }
718    }
719
720    /// Set sub-row smooth-scroll offset (normalized 0..=65535).
721    pub fn set_scroll_sub_row_offset(&mut self, sub_row_offset: u16) {
722        let old = self.scroll_sub_row_offset;
723        self.scroll_sub_row_offset = sub_row_offset;
724        if old != sub_row_offset {
725            self.notify_change(StateChangeType::ViewportChanged);
726        }
727    }
728
729    /// Set overscan rows for viewport prefetch range.
730    pub fn set_overscan_rows(&mut self, overscan_rows: usize) {
731        let old = self.overscan_rows;
732        self.overscan_rows = overscan_rows;
733        if old != overscan_rows {
734            self.notify_change(StateChangeType::ViewportChanged);
735        }
736    }
737
738    /// Set full smooth-scroll state.
739    pub fn set_smooth_scroll_state(&mut self, state: SmoothScrollState) {
740        let mut changed = false;
741        if self.scroll_top != state.top_visual_row {
742            self.scroll_top = state.top_visual_row;
743            changed = true;
744        }
745        if self.scroll_sub_row_offset != state.sub_row_offset {
746            self.scroll_sub_row_offset = state.sub_row_offset;
747            changed = true;
748        }
749        if self.overscan_rows != state.overscan_rows {
750            self.overscan_rows = state.overscan_rows;
751            changed = true;
752        }
753        if changed {
754            self.notify_change(StateChangeType::ViewportChanged);
755        }
756    }
757
758    /// Get smooth-scroll state.
759    pub fn get_smooth_scroll_state(&self) -> SmoothScrollState {
760        SmoothScrollState {
761            top_visual_row: self.scroll_top,
762            sub_row_offset: self.scroll_sub_row_offset,
763            overscan_rows: self.overscan_rows,
764        }
765    }
766
767    /// Get complete editor state snapshot
768    pub fn get_full_state(&self) -> EditorState {
769        EditorState {
770            document: self.get_document_state(),
771            cursor: self.get_cursor_state(),
772            viewport: self.get_viewport_state(),
773            undo_redo: self.get_undo_redo_state(),
774            folding: self.get_folding_state(),
775            diagnostics: self.get_diagnostics_state(),
776            decorations: self.get_decorations_state(),
777            style: self.get_style_state(),
778        }
779    }
780
781    /// Get document state
782    pub fn get_document_state(&self) -> DocumentState {
783        let editor = self.executor.editor();
784        DocumentState {
785            line_count: editor.line_count(),
786            char_count: editor.char_count(),
787            byte_count: editor.get_text().len(),
788            is_modified: self.is_modified,
789            version: self.state_version,
790        }
791    }
792
793    /// Get cursor state
794    pub fn get_cursor_state(&self) -> CursorState {
795        let editor = self.executor.editor();
796        let mut selections: Vec<Selection> =
797            Vec::with_capacity(1 + editor.secondary_selections().len());
798
799        let primary = editor.selection().cloned().unwrap_or(Selection {
800            start: editor.cursor_position(),
801            end: editor.cursor_position(),
802            direction: SelectionDirection::Forward,
803        });
804        selections.push(primary);
805        selections.extend(editor.secondary_selections().iter().cloned());
806
807        let (selections, primary_selection_index) =
808            crate::selection_set::normalize_selections(selections, 0);
809        let primary = selections
810            .get(primary_selection_index)
811            .cloned()
812            .unwrap_or(Selection {
813                start: editor.cursor_position(),
814                end: editor.cursor_position(),
815                direction: SelectionDirection::Forward,
816            });
817
818        let position = primary.end;
819        let offset = editor
820            .line_index()
821            .position_to_char_offset(position.line, position.column);
822
823        let selection = if primary.start == primary.end {
824            None
825        } else {
826            Some(primary)
827        };
828
829        let multi_cursors: Vec<Position> = selections
830            .iter()
831            .enumerate()
832            .filter_map(|(idx, sel)| {
833                if idx == primary_selection_index {
834                    None
835                } else {
836                    Some(sel.end)
837                }
838            })
839            .collect();
840
841        CursorState {
842            position,
843            offset,
844            multi_cursors,
845            selection,
846            selections,
847            primary_selection_index,
848        }
849    }
850
851    /// Get viewport state
852    pub fn get_viewport_state(&self) -> ViewportState {
853        let editor = self.executor.editor();
854        let total_visual_lines = editor.visual_line_count();
855        let clamped_top = self.scroll_top.min(total_visual_lines);
856        let visible_end = if let Some(height) = self.viewport_height {
857            clamped_top.saturating_add(height)
858        } else {
859            total_visual_lines
860        };
861        let visible_lines = clamped_top..visible_end.min(total_visual_lines);
862        let prefetch_start = visible_lines.start.saturating_sub(self.overscan_rows);
863        let prefetch_end = visible_lines
864            .end
865            .saturating_add(self.overscan_rows)
866            .min(total_visual_lines);
867
868        ViewportState {
869            width: editor.viewport_width(),
870            height: self.viewport_height,
871            scroll_top: clamped_top,
872            sub_row_offset: self.scroll_sub_row_offset,
873            overscan_rows: self.overscan_rows,
874            visible_lines,
875            prefetch_lines: prefetch_start..prefetch_end,
876            total_visual_lines,
877        }
878    }
879
880    /// Get undo/redo state
881    pub fn get_undo_redo_state(&self) -> UndoRedoState {
882        UndoRedoState {
883            can_undo: self.executor.can_undo(),
884            can_redo: self.executor.can_redo(),
885            undo_depth: self.executor.undo_depth(),
886            redo_depth: self.executor.redo_depth(),
887            redo_branch_count: self.executor.redo_branch_count(),
888            selected_redo_branch_index: self.executor.selected_redo_branch_index(),
889            current_change_group: self.executor.current_change_group(),
890        }
891    }
892
893    /// Get folding state
894    pub fn get_folding_state(&self) -> FoldingState {
895        let editor = self.executor.editor();
896        let regions = editor.folding_manager().regions().to_vec();
897        let collapsed_line_count: usize = regions
898            .iter()
899            .filter(|r| r.is_collapsed)
900            .map(|r| r.end_line - r.start_line)
901            .sum();
902
903        let visible_logical_lines = editor.line_count() - collapsed_line_count;
904
905        FoldingState {
906            regions,
907            collapsed_line_count,
908            visible_logical_lines,
909            total_visual_lines: editor.visual_line_count(),
910        }
911    }
912
913    /// Get style state
914    pub fn get_style_state(&self) -> StyleState {
915        let editor = self.executor.editor();
916        let layered_count: usize = editor.style_layers().values().map(|t| t.len()).sum();
917        StyleState {
918            style_count: editor.interval_tree().len() + layered_count,
919        }
920    }
921
922    /// Get diagnostics state.
923    pub fn get_diagnostics_state(&self) -> DiagnosticsState {
924        let editor = self.executor.editor();
925        DiagnosticsState {
926            diagnostics_count: editor.diagnostics().len(),
927        }
928    }
929
930    /// Get decorations state.
931    pub fn get_decorations_state(&self) -> DecorationsState {
932        let editor = self.executor.editor();
933        let decoration_count: usize = editor.decorations().values().map(|d| d.len()).sum();
934        DecorationsState {
935            layer_count: editor.decorations().len(),
936            decoration_count,
937        }
938    }
939
940    /// Get all styles within the specified range
941    pub fn get_styles_in_range(&self, start: usize, end: usize) -> Vec<(usize, usize, StyleId)> {
942        let editor = self.executor.editor();
943        let mut result: Vec<(usize, usize, StyleId)> = editor
944            .interval_tree()
945            .query_range(start, end)
946            .iter()
947            .map(|interval| (interval.start, interval.end, interval.style_id))
948            .collect();
949
950        for tree in editor.style_layers().values() {
951            result.extend(
952                tree.query_range(start, end)
953                    .iter()
954                    .map(|interval| (interval.start, interval.end, interval.style_id)),
955            );
956        }
957
958        result.sort_unstable_by_key(|(s, e, id)| (*s, *e, *id));
959        result
960    }
961
962    /// Get all styles at the specified position
963    pub fn get_styles_at(&self, offset: usize) -> Vec<StyleId> {
964        let editor = self.executor.editor();
965        let mut styles: Vec<StyleId> = editor
966            .interval_tree()
967            .query_point(offset)
968            .iter()
969            .map(|interval| interval.style_id)
970            .collect();
971
972        for tree in editor.style_layers().values() {
973            styles.extend(
974                tree.query_point(offset)
975                    .iter()
976                    .map(|interval| interval.style_id),
977            );
978        }
979
980        styles.sort_unstable();
981        styles.dedup();
982        styles
983    }
984
985    /// Replace all intervals in the specified style layer.
986    ///
987    /// Suitable for scenarios such as LSP semantic highlighting and simple syntax highlighting that require "full layer refresh".
988    /// This method only triggers `StyleChanged` once, avoiding version number explosion due to individual insertions.
989    pub fn replace_style_layer(&mut self, layer: StyleLayerId, intervals: Vec<Interval>) {
990        self.executor
991            .editor_mut()
992            .replace_style_layer(layer, intervals);
993        self.mark_modified(StateChangeType::StyleChanged);
994    }
995
996    /// Clear the specified style layer.
997    pub fn clear_style_layer(&mut self, layer: StyleLayerId) {
998        self.executor.editor_mut().clear_style_layer(layer);
999        self.mark_modified(StateChangeType::StyleChanged);
1000    }
1001
1002    /// Replace diagnostics wholesale.
1003    pub fn replace_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
1004        self.executor.editor_mut().replace_diagnostics(diagnostics);
1005        self.mark_modified(StateChangeType::DiagnosticsChanged);
1006    }
1007
1008    /// Clear all diagnostics.
1009    pub fn clear_diagnostics(&mut self) {
1010        self.executor.editor_mut().clear_diagnostics();
1011        self.mark_modified(StateChangeType::DiagnosticsChanged);
1012    }
1013
1014    /// Replace document symbols / outline wholesale.
1015    pub fn replace_document_symbols(&mut self, symbols: crate::DocumentOutline) {
1016        self.executor.editor_mut().replace_document_symbols(symbols);
1017        self.mark_modified(StateChangeType::SymbolsChanged);
1018    }
1019
1020    /// Clear document symbols / outline.
1021    pub fn clear_document_symbols(&mut self) {
1022        self.executor.editor_mut().clear_document_symbols();
1023        self.mark_modified(StateChangeType::SymbolsChanged);
1024    }
1025
1026    /// Replace a decoration layer wholesale.
1027    pub fn replace_decorations(&mut self, layer: DecorationLayerId, decorations: Vec<Decoration>) {
1028        self.executor
1029            .editor_mut()
1030            .replace_decorations(layer, decorations);
1031        self.mark_modified(StateChangeType::DecorationsChanged);
1032    }
1033
1034    /// Clear a decoration layer.
1035    pub fn clear_decorations(&mut self, layer: DecorationLayerId) {
1036        self.executor.editor_mut().clear_decorations(layer);
1037        self.mark_modified(StateChangeType::DecorationsChanged);
1038    }
1039
1040    /// Replace folding regions wholesale.
1041    ///
1042    /// If `preserve_collapsed` is true, matching existing collapsed derived regions stay collapsed
1043    /// after replacement, including conservative matches across small line-number drift.
1044    pub fn replace_folding_regions(&mut self, regions: Vec<FoldRegion>, preserve_collapsed: bool) {
1045        self.executor
1046            .editor_mut()
1047            .replace_folding_regions(regions, preserve_collapsed);
1048        self.mark_modified(StateChangeType::FoldingChanged);
1049    }
1050
1051    /// Clear all *derived* folding regions (leaves user folds intact).
1052    pub fn clear_folding_regions(&mut self) {
1053        self.executor.editor_mut().clear_derived_folding_regions();
1054        self.mark_modified(StateChangeType::FoldingChanged);
1055    }
1056
1057    /// Toggle the fold region that starts on the current cursor line.
1058    pub fn toggle_fold_at_current_line(&mut self) -> bool {
1059        let line = self.executor.editor().cursor_position().line;
1060        let toggled = self.executor.editor_mut().toggle_fold_at_line(line);
1061        if toggled {
1062            self.mark_modified(StateChangeType::FoldingChanged);
1063        }
1064        toggled
1065    }
1066
1067    /// Expand all folding regions.
1068    pub fn expand_all_folds(&mut self) {
1069        let had_collapsed = self
1070            .executor
1071            .editor()
1072            .folding_manager()
1073            .regions()
1074            .iter()
1075            .any(|region| region.is_collapsed);
1076        self.executor.editor_mut().expand_all_folds();
1077        if had_collapsed {
1078            self.mark_modified(StateChangeType::FoldingChanged);
1079        }
1080    }
1081
1082    /// Apply derived-state edits produced by a document processor (highlighting, folding, etc.).
1083    pub fn apply_processing_edits<I>(&mut self, edits: I)
1084    where
1085        I: IntoIterator<Item = ProcessingEdit>,
1086    {
1087        for edit in edits {
1088            match edit {
1089                ProcessingEdit::ReplaceStyleLayer { layer, intervals } => {
1090                    self.replace_style_layer(layer, intervals);
1091                }
1092                ProcessingEdit::ClearStyleLayer { layer } => {
1093                    self.clear_style_layer(layer);
1094                }
1095                ProcessingEdit::ReplaceFoldingRegions {
1096                    regions,
1097                    preserve_collapsed,
1098                } => {
1099                    self.replace_folding_regions(regions, preserve_collapsed);
1100                }
1101                ProcessingEdit::ClearFoldingRegions => {
1102                    self.clear_folding_regions();
1103                }
1104                ProcessingEdit::ReplaceDiagnostics { diagnostics } => {
1105                    self.replace_diagnostics(diagnostics);
1106                }
1107                ProcessingEdit::ClearDiagnostics => {
1108                    self.clear_diagnostics();
1109                }
1110                ProcessingEdit::ReplaceDecorations { layer, decorations } => {
1111                    self.replace_decorations(layer, decorations);
1112                }
1113                ProcessingEdit::ClearDecorations { layer } => {
1114                    self.clear_decorations(layer);
1115                }
1116                ProcessingEdit::ReplaceDocumentSymbols { symbols } => {
1117                    self.replace_document_symbols(symbols);
1118                }
1119                ProcessingEdit::ClearDocumentSymbols => {
1120                    self.clear_document_symbols();
1121                }
1122            }
1123        }
1124    }
1125
1126    /// Run a [`DocumentProcessor`] against the current document and apply its edits.
1127    pub fn apply_processor<P>(&mut self, processor: &mut P) -> Result<(), P::Error>
1128    where
1129        P: DocumentProcessor,
1130    {
1131        let edits = processor.process(self)?;
1132        self.apply_processing_edits(edits);
1133        Ok(())
1134    }
1135
1136    /// Get viewport content
1137    pub fn get_viewport_content(&self, start_row: usize, count: usize) -> HeadlessGrid {
1138        let editor = self.executor.editor();
1139        let text = editor.get_text();
1140        let generator = crate::SnapshotGenerator::from_text_with_layout_options(
1141            &text,
1142            editor.viewport_width(),
1143            editor.layout_engine().tab_width(),
1144            editor.layout_engine().wrap_mode(),
1145            editor.layout_engine().wrap_indent(),
1146        );
1147        generator.get_headless_grid(start_row, count)
1148    }
1149
1150    /// Get styled viewport content (by visual line).
1151    ///
1152    /// - Supports soft wrapping (based on `LayoutEngine`)
1153    /// - `Cell.styles` will contain the merged result of `interval_tree` and all `style_layers`
1154    pub fn get_viewport_content_styled(
1155        &self,
1156        start_visual_row: usize,
1157        count: usize,
1158    ) -> HeadlessGrid {
1159        self.executor
1160            .editor()
1161            .get_headless_grid_styled(start_visual_row, count)
1162    }
1163
1164    /// Get lightweight minimap content (by visual line).
1165    pub fn get_minimap_content(&self, start_visual_row: usize, count: usize) -> crate::MinimapGrid {
1166        self.executor
1167            .editor()
1168            .get_minimap_grid(start_visual_row, count)
1169    }
1170
1171    /// Get a decoration-aware composed viewport snapshot (by composed visual line).
1172    ///
1173    /// See [`EditorCore::get_headless_grid_composed`](crate::EditorCore::get_headless_grid_composed)
1174    /// for detailed semantics and caveats.
1175    pub fn get_viewport_content_composed(
1176        &self,
1177        start_visual_row: usize,
1178        count: usize,
1179    ) -> ComposedGrid {
1180        self.executor
1181            .editor()
1182            .get_headless_grid_composed(start_visual_row, count)
1183    }
1184
1185    /// Get total visual line count under current wrap/folding state.
1186    pub fn total_visual_lines(&self) -> usize {
1187        self.executor.editor().visual_line_count()
1188    }
1189
1190    /// Map global visual row to `(logical_line, visual_in_logical)`.
1191    pub fn visual_to_logical_line(&self, visual_row: usize) -> (usize, usize) {
1192        self.executor.editor().visual_to_logical_line(visual_row)
1193    }
1194
1195    /// Map logical position to visual `(row, x_cells)`.
1196    pub fn logical_position_to_visual(&self, line: usize, column: usize) -> Option<(usize, usize)> {
1197        self.executor
1198            .editor()
1199            .logical_position_to_visual(line, column)
1200    }
1201
1202    /// Map visual `(row, x_cells)` back to logical position.
1203    pub fn visual_position_to_logical(
1204        &self,
1205        visual_row: usize,
1206        x_cells: usize,
1207    ) -> Option<Position> {
1208        self.executor
1209            .editor()
1210            .visual_position_to_logical(visual_row, x_cells)
1211    }
1212
1213    /// Subscribe to state change notifications
1214    pub fn subscribe<F>(&mut self, callback: F)
1215    where
1216        F: FnMut(&StateChange) + Send + 'static,
1217    {
1218        self.callbacks.push(Box::new(callback));
1219    }
1220
1221    /// Check if state has changed since a version
1222    pub fn has_changed_since(&self, version: u64) -> bool {
1223        self.state_version > version
1224    }
1225
1226    /// Mark document as modified and increment version number
1227    pub fn mark_modified(&mut self, change_type: StateChangeType) {
1228        self.mark_modified_internal(change_type, None, None);
1229    }
1230
1231    fn mark_modified_internal(
1232        &mut self,
1233        change_type: StateChangeType,
1234        is_modified_override: Option<bool>,
1235        delta: Option<Arc<TextDelta>>,
1236    ) {
1237        let old_version = self.state_version;
1238        self.state_version += 1;
1239
1240        // Only mark as modified for document content changes
1241        if matches!(change_type, StateChangeType::DocumentModified) {
1242            self.is_modified = is_modified_override.unwrap_or(true);
1243        }
1244
1245        let mut change = StateChange::new(change_type, old_version, self.state_version);
1246        if let Some(delta) = delta {
1247            change = change.with_text_delta(delta);
1248        }
1249        self.notify_callbacks(&change);
1250    }
1251
1252    /// Mark document as unmodified (e.g., after saving)
1253    pub fn mark_saved(&mut self) {
1254        self.executor.mark_clean();
1255        self.is_modified = false;
1256    }
1257
1258    /// Capture a persistable snapshot of the undo/redo history for this document.
1259    pub fn undo_history_snapshot(&self) -> UndoHistorySnapshot {
1260        self.executor.undo_history_snapshot()
1261    }
1262
1263    /// Restore a previously captured [`UndoHistorySnapshot`].
1264    ///
1265    /// Notes:
1266    /// - This does **not** modify the current document text.
1267    /// - Callers should only restore a snapshot into the **same text** it was captured from.
1268    pub fn restore_undo_history(
1269        &mut self,
1270        snapshot: UndoHistorySnapshot,
1271    ) -> Result<(), UndoHistoryRestoreError> {
1272        self.last_text_delta = None;
1273        self.executor.restore_undo_history(snapshot)?;
1274        self.is_modified = !self.executor.is_clean();
1275        Ok(())
1276    }
1277
1278    /// Notify state change (without modifying version number)
1279    fn notify_change(&mut self, change_type: StateChangeType) {
1280        let change = StateChange::new(change_type, self.state_version, self.state_version);
1281        self.notify_callbacks(&change);
1282    }
1283
1284    /// Get the structured text delta produced by the last document edit, if any.
1285    pub fn last_text_delta(&self) -> Option<&TextDelta> {
1286        self.last_text_delta.as_deref()
1287    }
1288
1289    /// Take the structured text delta produced by the last document edit, if any.
1290    pub fn take_last_text_delta(&mut self) -> Option<Arc<TextDelta>> {
1291        self.last_text_delta.take()
1292    }
1293
1294    /// Toggle a bookmark at the current cursor line.
1295    ///
1296    /// Returns `true` if a bookmark was added, or `false` if an existing bookmark on that line was
1297    /// removed.
1298    pub fn toggle_bookmark_at_cursor_line(&mut self) -> bool {
1299        let line = self.executor.editor().cursor_position().line;
1300        let line_start = self
1301            .executor
1302            .editor()
1303            .line_index()
1304            .position_to_char_offset(line, 0);
1305        let added = self.bookmarks.toggle_line_start(line_start);
1306        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1307        added
1308    }
1309
1310    /// Return all bookmark line numbers (0-based).
1311    pub fn bookmark_lines(&self) -> Vec<usize> {
1312        self.bookmarks
1313            .line_numbers(self.executor.editor().line_index())
1314    }
1315
1316    /// Clear all bookmarks.
1317    pub fn clear_bookmarks(&mut self) {
1318        self.bookmarks.clear();
1319        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1320    }
1321
1322    /// Move the cursor to the next bookmark (wrapping to the first bookmark).
1323    ///
1324    /// Returns the new cursor position, or `None` if there are no bookmarks.
1325    pub fn goto_next_bookmark(&mut self) -> Result<Option<Position>, CommandError> {
1326        let line = self.executor.editor().cursor_position().line;
1327        let current_line_start = self
1328            .executor
1329            .editor()
1330            .line_index()
1331            .position_to_char_offset(line, 0);
1332
1333        let Some(target) = self.bookmarks.next_after_line_start(current_line_start) else {
1334            return Ok(None);
1335        };
1336
1337        let (line, column) = self
1338            .executor
1339            .editor()
1340            .line_index()
1341            .char_offset_to_position(target.offset);
1342        self.execute(Command::Cursor(CursorCommand::MoveTo { line, column }))?;
1343        let _ = self.execute(Command::Cursor(CursorCommand::ClearSelection))?;
1344        Ok(Some(Position::new(line, column)))
1345    }
1346
1347    /// Move the cursor to the previous bookmark (wrapping to the last bookmark).
1348    ///
1349    /// Returns the new cursor position, or `None` if there are no bookmarks.
1350    pub fn goto_prev_bookmark(&mut self) -> Result<Option<Position>, CommandError> {
1351        let line = self.executor.editor().cursor_position().line;
1352        let current_line_start = self
1353            .executor
1354            .editor()
1355            .line_index()
1356            .position_to_char_offset(line, 0);
1357
1358        let Some(target) = self.bookmarks.prev_before_line_start(current_line_start) else {
1359            return Ok(None);
1360        };
1361
1362        let (line, column) = self
1363            .executor
1364            .editor()
1365            .line_index()
1366            .char_offset_to_position(target.offset);
1367        self.execute(Command::Cursor(CursorCommand::MoveTo { line, column }))?;
1368        let _ = self.execute(Command::Cursor(CursorCommand::ClearSelection))?;
1369        Ok(Some(Position::new(line, column)))
1370    }
1371
1372    /// Set (or replace) a named mark at the current cursor position.
1373    pub fn set_mark_at_cursor(&mut self, name: String) -> Result<(), CommandError> {
1374        if name.trim().is_empty() {
1375            return Err(CommandError::Other("Mark name cannot be empty".to_string()));
1376        }
1377
1378        let pos = self.executor.editor().cursor_position();
1379        let offset = self
1380            .executor
1381            .editor()
1382            .line_index()
1383            .position_to_char_offset(pos.line, pos.column);
1384        self.marks.set(name, offset);
1385        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1386        Ok(())
1387    }
1388
1389    /// Move the cursor to a named mark (if present).
1390    ///
1391    /// Returns the new cursor position, or `None` if the mark does not exist.
1392    pub fn goto_mark(&mut self, name: &str) -> Result<Option<Position>, CommandError> {
1393        let Some(anchor) = self.marks.get(name) else {
1394            return Ok(None);
1395        };
1396        let (line, column) = self
1397            .executor
1398            .editor()
1399            .line_index()
1400            .char_offset_to_position(anchor.offset);
1401        self.execute(Command::Cursor(CursorCommand::MoveTo { line, column }))?;
1402        let _ = self.execute(Command::Cursor(CursorCommand::ClearSelection))?;
1403        Ok(Some(Position::new(line, column)))
1404    }
1405
1406    /// Remove a named mark.
1407    ///
1408    /// Returns `true` if the mark existed.
1409    pub fn clear_mark(&mut self, name: &str) -> bool {
1410        let existed = self.marks.remove(name);
1411        if existed {
1412            self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1413        }
1414        existed
1415    }
1416
1417    /// Return all mark names (deterministic order).
1418    pub fn mark_names(&self) -> Vec<String> {
1419        self.marks.names()
1420    }
1421
1422    /// Clear all marks.
1423    pub fn clear_all_marks(&mut self) {
1424        self.marks.clear();
1425        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1426    }
1427
1428    /// Record the current cursor position as a jump-list location.
1429    ///
1430    /// Typical usage: call this *before* performing a “jump” (go-to-definition, search result,
1431    /// symbol navigation, ...).
1432    pub fn push_jump_location(&mut self) {
1433        let pos = self.executor.editor().cursor_position();
1434        let offset = self
1435            .executor
1436            .editor()
1437            .line_index()
1438            .position_to_char_offset(pos.line, pos.column);
1439        self.jump_list.record(offset);
1440        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1441    }
1442
1443    /// Jump back in the jump list.
1444    ///
1445    /// Returns the new cursor position, or `None` if there is no back entry.
1446    pub fn jump_back(&mut self) -> Result<Option<Position>, CommandError> {
1447        let pos = self.executor.editor().cursor_position();
1448        let current_offset = self
1449            .executor
1450            .editor()
1451            .line_index()
1452            .position_to_char_offset(pos.line, pos.column);
1453
1454        let Some(target) = self.jump_list.back(current_offset) else {
1455            return Ok(None);
1456        };
1457
1458        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1459
1460        let (line, column) = self
1461            .executor
1462            .editor()
1463            .line_index()
1464            .char_offset_to_position(target.offset);
1465        self.execute(Command::Cursor(CursorCommand::MoveTo { line, column }))?;
1466        let _ = self.execute(Command::Cursor(CursorCommand::ClearSelection))?;
1467        Ok(Some(Position::new(line, column)))
1468    }
1469
1470    /// Jump forward in the jump list.
1471    ///
1472    /// Returns the new cursor position, or `None` if there is no forward entry.
1473    pub fn jump_forward(&mut self) -> Result<Option<Position>, CommandError> {
1474        let pos = self.executor.editor().cursor_position();
1475        let current_offset = self
1476            .executor
1477            .editor()
1478            .line_index()
1479            .position_to_char_offset(pos.line, pos.column);
1480
1481        let Some(target) = self.jump_list.forward(current_offset) else {
1482            return Ok(None);
1483        };
1484
1485        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1486
1487        let (line, column) = self
1488            .executor
1489            .editor()
1490            .line_index()
1491            .char_offset_to_position(target.offset);
1492        self.execute(Command::Cursor(CursorCommand::MoveTo { line, column }))?;
1493        let _ = self.execute(Command::Cursor(CursorCommand::ClearSelection))?;
1494        Ok(Some(Position::new(line, column)))
1495    }
1496
1497    /// Clear the jump list (both back/forward stacks).
1498    pub fn clear_jump_list(&mut self) {
1499        self.jump_list.clear();
1500        self.mark_modified_internal(StateChangeType::NavigationChanged, None, None);
1501    }
1502
1503    /// Notify all callbacks
1504    fn notify_callbacks(&mut self, change: &StateChange) {
1505        for callback in &mut self.callbacks {
1506            callback(change);
1507        }
1508    }
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513    use super::*;
1514
1515    #[test]
1516    fn test_document_state() {
1517        let manager = EditorStateManager::new("Hello World\nLine 2", 80);
1518        let doc_state = manager.get_document_state();
1519
1520        assert_eq!(doc_state.line_count, 2);
1521        assert_eq!(doc_state.char_count, 18); // Including newline
1522        assert!(!doc_state.is_modified);
1523        assert_eq!(doc_state.version, 0);
1524    }
1525
1526    #[test]
1527    fn test_cursor_state() {
1528        let manager = EditorStateManager::new("Hello World", 80);
1529        let cursor_state = manager.get_cursor_state();
1530
1531        assert_eq!(cursor_state.position, Position::new(0, 0));
1532        assert_eq!(cursor_state.offset, 0);
1533        assert!(cursor_state.selection.is_none());
1534    }
1535
1536    #[test]
1537    fn test_viewport_state() {
1538        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
1539        manager.set_viewport_height(10);
1540        manager.set_scroll_top(1);
1541
1542        let viewport_state = manager.get_viewport_state();
1543
1544        assert_eq!(viewport_state.width, 80);
1545        assert_eq!(viewport_state.height, Some(10));
1546        assert_eq!(viewport_state.scroll_top, 1);
1547        assert_eq!(viewport_state.visible_lines, 1..3);
1548    }
1549
1550    #[test]
1551    fn test_folding_state() {
1552        let manager = EditorStateManager::new("Line 1\nLine 2\nLine 3", 80);
1553        let folding_state = manager.get_folding_state();
1554
1555        assert_eq!(folding_state.regions.len(), 0);
1556        assert_eq!(folding_state.collapsed_line_count, 0);
1557        assert_eq!(folding_state.visible_logical_lines, 3);
1558    }
1559
1560    #[test]
1561    fn test_style_state() {
1562        let manager = EditorStateManager::new("Hello World", 80);
1563        let style_state = manager.get_style_state();
1564
1565        assert_eq!(style_state.style_count, 0);
1566    }
1567
1568    #[test]
1569    fn test_full_state() {
1570        let manager = EditorStateManager::new("Test", 80);
1571        let full_state = manager.get_full_state();
1572
1573        assert_eq!(full_state.document.line_count, 1);
1574        assert_eq!(full_state.cursor.position, Position::new(0, 0));
1575        assert_eq!(full_state.viewport.width, 80);
1576    }
1577
1578    #[test]
1579    fn test_version_tracking() {
1580        let mut manager = EditorStateManager::new("Test", 80);
1581
1582        assert_eq!(manager.version(), 0);
1583        assert!(!manager.has_changed_since(0));
1584
1585        manager.mark_modified(StateChangeType::DocumentModified);
1586
1587        assert_eq!(manager.version(), 1);
1588        assert!(manager.has_changed_since(0));
1589        assert!(!manager.has_changed_since(1));
1590    }
1591
1592    #[test]
1593    fn test_modification_tracking() {
1594        let mut manager = EditorStateManager::new("Test", 80);
1595
1596        assert!(!manager.get_document_state().is_modified);
1597
1598        manager.mark_modified(StateChangeType::DocumentModified);
1599        assert!(manager.get_document_state().is_modified);
1600
1601        manager.mark_saved();
1602        assert!(!manager.get_document_state().is_modified);
1603    }
1604
1605    #[test]
1606    fn test_undo_redo_state_and_dirty_tracking() {
1607        let mut manager = EditorStateManager::empty(80);
1608
1609        let state = manager.get_undo_redo_state();
1610        assert!(!state.can_undo);
1611        assert!(!state.can_redo);
1612
1613        manager
1614            .execute(Command::Edit(EditCommand::InsertText {
1615                text: "abc".to_string(),
1616            }))
1617            .unwrap();
1618
1619        assert!(manager.get_document_state().is_modified);
1620        let state = manager.get_undo_redo_state();
1621        assert!(state.can_undo);
1622        assert!(!state.can_redo);
1623        assert_eq!(state.undo_depth, 1);
1624
1625        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
1626        assert!(!manager.get_document_state().is_modified);
1627        let state = manager.get_undo_redo_state();
1628        assert!(!state.can_undo);
1629        assert!(state.can_redo);
1630
1631        manager.execute(Command::Edit(EditCommand::Redo)).unwrap();
1632        assert!(manager.get_document_state().is_modified);
1633        let state = manager.get_undo_redo_state();
1634        assert!(state.can_undo);
1635        assert!(!state.can_redo);
1636    }
1637
1638    #[test]
1639    fn test_insert_tab_undo_restores_clean_state() {
1640        let mut manager = EditorStateManager::empty(80);
1641        assert!(!manager.get_document_state().is_modified);
1642
1643        manager
1644            .execute(Command::Edit(EditCommand::InsertTab))
1645            .unwrap();
1646        assert!(manager.get_document_state().is_modified);
1647
1648        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
1649        assert!(!manager.get_document_state().is_modified);
1650    }
1651
1652    #[test]
1653    fn test_insert_tab_spaces_undo_restores_clean_state() {
1654        let mut manager = EditorStateManager::empty(80);
1655        manager
1656            .execute(Command::View(ViewCommand::SetTabKeyBehavior {
1657                behavior: crate::TabKeyBehavior::Spaces,
1658            }))
1659            .unwrap();
1660
1661        manager
1662            .execute(Command::Edit(EditCommand::InsertTab))
1663            .unwrap();
1664        assert!(manager.get_document_state().is_modified);
1665
1666        manager.execute(Command::Edit(EditCommand::Undo)).unwrap();
1667        assert!(!manager.get_document_state().is_modified);
1668    }
1669
1670    #[test]
1671    fn test_state_change_callback() {
1672        use std::sync::{Arc, Mutex};
1673
1674        let mut manager = EditorStateManager::new("Test", 80);
1675
1676        let callback_called = Arc::new(Mutex::new(false));
1677        let callback_called_clone = callback_called.clone();
1678
1679        manager.subscribe(move |_change| {
1680            *callback_called_clone.lock().unwrap() = true;
1681        });
1682
1683        manager.mark_modified(StateChangeType::CursorMoved);
1684
1685        // Verify callback was called
1686        assert!(*callback_called.lock().unwrap());
1687    }
1688
1689    #[test]
1690    fn test_execute_cursor_noop_does_not_bump_version() {
1691        let mut manager = EditorStateManager::new("A", 80);
1692        assert_eq!(manager.version(), 0);
1693
1694        // Continue moving left at the beginning of file (unchanged after clamp), version should not change.
1695        manager
1696            .execute(Command::Cursor(CursorCommand::MoveBy {
1697                delta_line: 0,
1698                delta_column: -1,
1699            }))
1700            .unwrap();
1701        assert_eq!(manager.editor().cursor_position(), Position::new(0, 0));
1702        assert_eq!(manager.version(), 0);
1703
1704        // Move to end of line (changed), version increments.
1705        manager
1706            .execute(Command::Cursor(CursorCommand::MoveTo {
1707                line: 0,
1708                column: usize::MAX,
1709            }))
1710            .unwrap();
1711        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
1712        assert_eq!(manager.version(), 1);
1713
1714        // Continue moving right at end of line (unchanged after clamp), version should not change.
1715        let version_before = manager.version();
1716        manager
1717            .execute(Command::Cursor(CursorCommand::MoveBy {
1718                delta_line: 0,
1719                delta_column: 1,
1720            }))
1721            .unwrap();
1722        assert_eq!(manager.editor().cursor_position(), Position::new(0, 1));
1723        assert_eq!(manager.version(), version_before);
1724    }
1725
1726    #[test]
1727    fn test_viewport_height() {
1728        let mut manager = EditorStateManager::new("Test", 80);
1729
1730        assert_eq!(manager.get_viewport_state().height, None);
1731
1732        manager.set_viewport_height(20);
1733        assert_eq!(manager.get_viewport_state().height, Some(20));
1734    }
1735
1736    #[test]
1737    fn test_scroll_position() {
1738        let mut manager = EditorStateManager::new("Line 1\nLine 2\nLine 3\nLine 4", 80);
1739        manager.set_viewport_height(2);
1740
1741        assert_eq!(manager.get_viewport_state().scroll_top, 0);
1742        assert_eq!(manager.get_viewport_state().visible_lines, 0..2);
1743
1744        manager.set_scroll_top(2);
1745        assert_eq!(manager.get_viewport_state().scroll_top, 2);
1746        assert_eq!(manager.get_viewport_state().visible_lines, 2..4);
1747    }
1748
1749    #[test]
1750    fn test_get_styles() {
1751        let mut manager = EditorStateManager::new("Hello World", 80);
1752
1753        // Add style through the controlled editor-core API.
1754        manager
1755            .editor_mut()
1756            .insert_style_interval(crate::intervals::Interval::new(0, 5, 1));
1757
1758        let styles = manager.get_styles_in_range(0, 10);
1759        assert_eq!(styles.len(), 1);
1760        assert_eq!(styles[0], (0, 5, 1));
1761
1762        let styles_at = manager.get_styles_at(3);
1763        assert_eq!(styles_at.len(), 1);
1764        assert_eq!(styles_at[0], 1);
1765    }
1766
1767    #[test]
1768    fn test_replace_style_layer_affects_queries() {
1769        let mut manager = EditorStateManager::new("Hello", 80);
1770
1771        manager.replace_style_layer(
1772            StyleLayerId::SEMANTIC_TOKENS,
1773            vec![Interval::new(0, 1, 100)],
1774        );
1775
1776        assert_eq!(manager.get_styles_at(0), vec![100]);
1777
1778        // Base layer + layered styles are merged.
1779        manager
1780            .editor_mut()
1781            .insert_style_interval(Interval::new(0, 5, 1));
1782
1783        assert_eq!(manager.get_styles_at(0), vec![1, 100]);
1784    }
1785
1786    #[test]
1787    fn test_viewport_content_styled_wraps_and_includes_styles() {
1788        let mut manager = EditorStateManager::new("abcdef", 3);
1789
1790        // Highlight "bcd" across a wrap boundary: "abc" | "def"
1791        manager.replace_style_layer(StyleLayerId::SIMPLE_SYNTAX, vec![Interval::new(1, 4, 7)]);
1792
1793        let grid = manager.get_viewport_content_styled(0, 10);
1794        assert_eq!(grid.actual_line_count(), 2);
1795
1796        let line0 = &grid.lines[0];
1797        assert_eq!(line0.logical_line_index, 0);
1798        assert!(!line0.is_wrapped_part);
1799        assert_eq!(line0.cells.len(), 3);
1800        assert_eq!(line0.cells[0].ch, 'a');
1801        assert_eq!(line0.cells[1].ch, 'b');
1802        assert_eq!(line0.cells[2].ch, 'c');
1803        assert_eq!(line0.cells[0].styles, Vec::<StyleId>::new());
1804        assert_eq!(line0.cells[1].styles, vec![7]);
1805        assert_eq!(line0.cells[2].styles, vec![7]);
1806
1807        let line1 = &grid.lines[1];
1808        assert_eq!(line1.logical_line_index, 0);
1809        assert!(line1.is_wrapped_part);
1810        assert_eq!(line1.cells.len(), 3);
1811        assert_eq!(line1.cells[0].ch, 'd');
1812        assert_eq!(line1.cells[0].styles, vec![7]);
1813        assert_eq!(line1.cells[1].ch, 'e');
1814        assert_eq!(line1.cells[1].styles, Vec::<StyleId>::new());
1815    }
1816
1817    #[test]
1818    fn test_smooth_scroll_state_and_prefetch_lines() {
1819        let mut manager = EditorStateManager::new("a\nb\nc\nd\n", 80);
1820        manager.set_viewport_height(2);
1821        manager.set_scroll_top(1);
1822        manager.set_scroll_sub_row_offset(123);
1823        manager.set_overscan_rows(2);
1824
1825        let smooth = manager.get_smooth_scroll_state();
1826        assert_eq!(
1827            smooth,
1828            SmoothScrollState {
1829                top_visual_row: 1,
1830                sub_row_offset: 123,
1831                overscan_rows: 2
1832            }
1833        );
1834
1835        let viewport = manager.get_viewport_state();
1836        assert_eq!(viewport.visible_lines, 1..3);
1837        assert_eq!(viewport.sub_row_offset, 123);
1838        assert_eq!(viewport.overscan_rows, 2);
1839        assert_eq!(viewport.prefetch_lines, 0..5);
1840        assert_eq!(viewport.total_visual_lines, 5);
1841    }
1842
1843    #[test]
1844    fn test_minimap_content_returns_lightweight_summary() {
1845        let mut manager = EditorStateManager::new("abc def\n", 80);
1846        manager.replace_style_layer(StyleLayerId::SIMPLE_SYNTAX, vec![Interval::new(0, 3, 9)]);
1847
1848        let minimap = manager.get_minimap_content(0, 1);
1849        assert_eq!(minimap.actual_line_count(), 1);
1850        let line = &minimap.lines[0];
1851        assert_eq!(line.logical_line_index, 0);
1852        assert_eq!(line.visual_in_logical, 0);
1853        assert_eq!(line.char_offset_start, 0);
1854        assert_eq!(line.char_offset_end, 7);
1855        assert!(line.total_cells >= line.non_whitespace_cells);
1856        assert_eq!(line.dominant_style, Some(9));
1857        assert!(!line.is_fold_placeholder_appended);
1858    }
1859}