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