Skip to main content

fresh/
state.rs

1use crate::model::buffer::{Buffer, LineNumber};
2use crate::model::cursor::{Cursor, Cursors};
3use crate::model::document_model::{
4    DocumentCapabilities, DocumentModel, DocumentPosition, ViewportContent, ViewportLine,
5};
6use crate::model::event::{
7    Event, MarginContentData, MarginPositionData, OverlayFace as EventOverlayFace, PopupData,
8    PopupPositionData,
9};
10use crate::model::filesystem::FileSystem;
11use crate::model::marker::{MarkerId, MarkerList};
12use crate::primitives::detected_language::DetectedLanguage;
13use crate::primitives::grammar::GrammarRegistry;
14use crate::primitives::highlight_engine::HighlightEngine;
15use crate::primitives::indent::IndentCalculator;
16use crate::primitives::reference_highlighter::ReferenceHighlighter;
17use crate::primitives::text_property::TextPropertyManager;
18use crate::view::bracket_highlight_overlay::BracketHighlightOverlay;
19use crate::view::conceal::ConcealManager;
20use crate::view::folding::LspFoldRanges;
21use crate::view::margin::{MarginAnnotation, MarginContent, MarginManager, MarginPosition};
22use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, UnderlineStyle};
23use crate::view::popup::{
24    Popup, PopupContent, PopupKind, PopupListItem, PopupManager, PopupPosition,
25};
26use crate::view::reference_highlight_overlay::ReferenceHighlightOverlay;
27use crate::view::soft_break::SoftBreakManager;
28use crate::view::virtual_text::VirtualTextManager;
29use anyhow::Result;
30use ratatui::style::{Color, Style};
31use std::cell::RefCell;
32use std::ops::Range;
33use std::sync::Arc;
34
35/// A marker whose position was displaced by a deletion.
36/// Stored in LogEntry (for single edits) or Event::BulkEdit (for bulk edits).
37/// On undo, the marker is restored to its exact original position.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub enum DisplacedMarker {
40    /// Marker from the main marker_list (virtual text, overlays)
41    Main { id: u64, position: usize },
42    /// Marker from margins.indicator_markers (breakpoints, line indicators)
43    Margin { id: u64, position: usize },
44}
45
46impl DisplacedMarker {
47    /// Encode as (u64, usize) for compact storage. Uses high bit to tag source.
48    pub fn encode(&self) -> (u64, usize) {
49        match self {
50            Self::Main { id, position } => (*id, *position),
51            Self::Margin { id, position } => (*id | (1u64 << 63), *position),
52        }
53    }
54
55    /// Decode from (u64, usize) compact representation.
56    pub fn decode(tagged_id: u64, position: usize) -> Self {
57        if (tagged_id >> 63) == 1 {
58            Self::Margin {
59                id: tagged_id & !(1u64 << 63),
60                position,
61            }
62        } else {
63            Self::Main {
64                id: tagged_id,
65                position,
66            }
67        }
68    }
69}
70
71/// Display mode for a buffer
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum ViewMode {
74    /// Plain source rendering
75    Source,
76    /// Document-style page view with centered content, concealed markers,
77    /// and plugin-driven word wrapping (previously called "compose mode")
78    PageView,
79}
80
81/// Per-buffer user settings that should be preserved across file reloads (auto-revert).
82///
83/// These are user overrides that apply to a specific buffer, separate from:
84/// - File-derived state (syntax highlighting, language detection)
85/// - View-specific state (scroll position, line wrap - those live in SplitViewState)
86///
87/// TODO: Consider moving view-related settings (line numbers, debug mode) to SplitViewState
88/// to allow per-split preferences. Currently line numbers is in margins (coupled with plugin
89/// gutters), and debug_highlight_mode is in EditorState, but both could arguably be per-view
90/// rather than per-buffer.
91#[derive(Debug, Clone)]
92pub struct BufferSettings {
93    /// Resolved whitespace indicator visibility for this buffer.
94    /// Set based on global + language config; can be toggled per-buffer by user
95    pub whitespace: crate::config::WhitespaceVisibility,
96
97    /// Whether pressing Tab should insert a tab character instead of spaces.
98    /// Set based on language config; can be toggled per-buffer by user
99    pub use_tabs: bool,
100
101    /// Tab size (number of spaces per tab character) for rendering.
102    /// Used for visual display of tab characters and indent calculations.
103    /// Set based on language config; can be changed per-buffer by user
104    pub tab_size: usize,
105
106    /// Whether to auto-close brackets, parentheses, and quotes.
107    /// Set based on global + language config.
108    pub auto_close: bool,
109
110    /// Whether to surround selected text with matching pairs when typing a delimiter.
111    /// Set based on global + language config.
112    pub auto_surround: bool,
113
114    /// Extra characters (beyond alphanumeric + `_`) considered part of
115    /// identifiers for this language. Used by completion providers.
116    pub word_characters: String,
117}
118
119impl Default for BufferSettings {
120    fn default() -> Self {
121        Self {
122            whitespace: crate::config::WhitespaceVisibility::default(),
123            use_tabs: false,
124            tab_size: 4,
125            auto_close: true,
126            auto_surround: true,
127            word_characters: String::new(),
128        }
129    }
130}
131
132/// The complete editor state - everything needed to represent the current editing session
133///
134/// NOTE: Viewport is NOT stored here - it lives in SplitViewState.
135/// This is because viewport is view-specific (each split can view the same buffer
136/// at different scroll positions), while EditorState represents the buffer content.
137pub struct EditorState {
138    /// The text buffer
139    pub buffer: Buffer,
140
141    /// Syntax highlighter (tree-sitter or TextMate based on language)
142    pub highlighter: HighlightEngine,
143
144    /// Auto-indent calculator for smart indentation (RefCell for interior mutability)
145    pub indent_calculator: RefCell<IndentCalculator>,
146
147    /// Overlays for visual decorations (underlines, highlights, etc.)
148    pub overlays: OverlayManager,
149
150    /// Marker list for content-anchored overlay positions
151    pub marker_list: MarkerList,
152
153    /// Virtual text manager for inline hints (type hints, parameter hints, etc.)
154    pub virtual_texts: VirtualTextManager,
155
156    /// Conceal ranges for hiding/replacing byte ranges during rendering
157    pub conceals: ConcealManager,
158
159    /// Soft break points for marker-based line wrapping during rendering
160    pub soft_breaks: SoftBreakManager,
161
162    /// Popups for floating windows (completion, documentation, etc.)
163    pub popups: PopupManager,
164
165    /// Margins for line numbers, annotations, gutter symbols, etc.)
166    pub margins: MarginManager,
167
168    /// Cached line number for primary cursor (0-indexed)
169    /// Maintained incrementally to avoid O(n) scanning on every render
170    pub primary_cursor_line_number: LineNumber,
171
172    /// Current mode (for modal editing, if implemented)
173    pub mode: String,
174
175    /// Text properties for virtual buffers (embedded metadata in text ranges)
176    /// Used by virtual buffers to store location info, severity, etc.
177    pub text_properties: TextPropertyManager,
178
179    /// Whether to show cursors in this buffer (default true)
180    /// Can be set to false for virtual buffers like diagnostics panels
181    pub show_cursors: bool,
182
183    /// Set once a plugin explicitly controls this buffer's cursor
184    /// visibility via `setBufferShowCursors`. When true, the widget
185    /// runtime stops overriding the cursor (`apply_widget_focus_cursor`
186    /// becomes a no-op for this buffer) — letting a widget-panel plugin
187    /// own its pane's cursor, e.g. git log's cursor-driven commit list.
188    pub cursor_visibility_locked: bool,
189
190    /// Whether editing is disabled for this buffer (default false)
191    /// When true, typing, deletion, cut/paste, undo/redo are blocked
192    /// but navigation, selection, and copy are still allowed
193    pub editing_disabled: bool,
194
195    /// Whether this buffer can be scrolled (default true). Fixed buffer-group
196    /// panels (toolbars, headers, footers) set this to false so the mouse
197    /// wheel is ignored and no scrollbar is drawn.
198    pub scrollable: bool,
199
200    /// Per-buffer user settings (tab size, indentation style, etc.)
201    /// These settings are preserved across file reloads (auto-revert)
202    pub buffer_settings: BufferSettings,
203
204    /// Semantic highlighter for word occurrence highlighting
205    pub reference_highlighter: ReferenceHighlighter,
206
207    /// Whether this buffer is a composite view (e.g., side-by-side diff)
208    pub is_composite_buffer: bool,
209
210    /// Debug mode: reveal highlight/overlay spans (WordPerfect-style)
211    pub debug_highlight_mode: bool,
212
213    /// Debounced semantic highlight cache
214    pub reference_highlight_overlay: ReferenceHighlightOverlay,
215
216    /// Bracket matching highlight overlay
217    pub bracket_highlight_overlay: BracketHighlightOverlay,
218
219    /// Cached LSP semantic tokens (converted to buffer byte ranges)
220    pub semantic_tokens: Option<SemanticTokenStore>,
221
222    /// Last-known LSP folding ranges for this buffer, tracked by byte markers
223    /// so they auto-adjust when content is inserted or deleted around them
224    /// (issue #1571).
225    pub folding_ranges: LspFoldRanges,
226
227    /// The detected language ID for this buffer (e.g., "rust", "csharp", "text").
228    /// Used for LSP config lookup and internal identification.
229    pub language: String,
230
231    /// Human-readable language display name (e.g., "Rust", "C#", "Plain Text").
232    /// Shown in the status bar and Set Language prompt.
233    // TODO: Consider embedding `DetectedLanguage` directly in `EditorState`
234    // instead of copying its fields, to avoid duplication between the two structs.
235    pub display_name: String,
236
237    /// Per-logical-line visual-row-count cache (pipeline-output).
238    /// Populated by both the renderer (as a side effect of rendering a
239    /// visible frame) and the scroll-math miss handler.  Entries are
240    /// keyed on every pipeline input; mutations to any input produce a
241    /// different key so stale entries are never returned — see
242    /// `docs/internal/line-wrap-cache-plan.md`.
243    pub line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache,
244
245    /// Whole-buffer prefix-sum index over per-line visual row counts.
246    /// Sits one tier above `line_wrap_cache`: answers
247    /// "what visual row contains byte B?" / "what byte sits at row R?"
248    /// in O(log N_lines) for scrollbar drag, scrollbar render, and
249    /// `ensure_visible` wrapped scrolling.  Built lazily from
250    /// `line_wrap_cache`; same invalidation source (pipeline-input
251    /// version + geometry).  See
252    /// `crate::view::visual_row_index` for invariants.
253    pub visual_row_index: crate::view::visual_row_index::VisualRowIndex,
254}
255
256impl EditorState {
257    /// Create a new editor state with an empty buffer
258    ///
259    /// Note: width/height parameters are kept for backward compatibility but
260    /// are no longer used - viewport is now owned by SplitViewState.
261    /// Apply a detected language to this state. This is the single mutation point
262    /// for changing the language of a buffer after creation.
263    pub fn apply_language(&mut self, detected: DetectedLanguage) {
264        self.language = detected.name;
265        self.display_name = detected.display_name;
266        self.highlighter = detected.highlighter;
267        if let Some(lang) = &detected.ts_language {
268            self.reference_highlighter.set_language(lang);
269        }
270    }
271
272    /// Create a new state with a buffer and default (plain text) language.
273    /// All other fields are initialized to their defaults.
274    fn new_from_buffer(buffer: Buffer) -> Self {
275        let mut marker_list = MarkerList::new();
276        if !buffer.is_empty() {
277            marker_list.adjust_for_insert(0, buffer.len());
278        }
279
280        Self {
281            buffer,
282            highlighter: HighlightEngine::None,
283            indent_calculator: RefCell::new(IndentCalculator::new()),
284            overlays: OverlayManager::new(),
285            marker_list,
286            virtual_texts: VirtualTextManager::new(),
287            conceals: ConcealManager::new(),
288            soft_breaks: SoftBreakManager::new(),
289            popups: PopupManager::new(),
290            margins: MarginManager::new(),
291            primary_cursor_line_number: LineNumber::Absolute(0),
292            mode: "insert".to_string(),
293            text_properties: TextPropertyManager::new(),
294            show_cursors: true,
295            cursor_visibility_locked: false,
296            editing_disabled: false,
297            scrollable: true,
298            buffer_settings: BufferSettings::default(),
299            reference_highlighter: ReferenceHighlighter::new(),
300            is_composite_buffer: false,
301            debug_highlight_mode: false,
302            reference_highlight_overlay: ReferenceHighlightOverlay::new(),
303            bracket_highlight_overlay: BracketHighlightOverlay::new(),
304            semantic_tokens: None,
305            folding_ranges: LspFoldRanges::new(),
306            language: "text".to_string(),
307            display_name: "Text".to_string(),
308            line_wrap_cache: crate::view::line_wrap_cache::LineWrapCache::default(),
309            visual_row_index: crate::view::visual_row_index::VisualRowIndex::default(),
310        }
311    }
312
313    pub fn new(
314        _width: u16,
315        _height: u16,
316        large_file_threshold: usize,
317        fs: Arc<dyn FileSystem + Send + Sync>,
318    ) -> Self {
319        Self::new_from_buffer(Buffer::new(large_file_threshold, fs))
320    }
321
322    /// Create a new editor state with an empty buffer associated with a file path.
323    /// Used for files that don't exist yet — the path is set so saving will create the file.
324    pub fn new_with_path(
325        large_file_threshold: usize,
326        fs: Arc<dyn FileSystem + Send + Sync>,
327        path: std::path::PathBuf,
328    ) -> Self {
329        Self::new_from_buffer(Buffer::new_with_path(large_file_threshold, fs, path))
330    }
331
332    /// Set the syntax highlighting language based on a virtual buffer name.
333    /// Handles names like `*OLD:test.ts*` or `*OURS*.c` by stripping markers
334    /// and detecting language from the cleaned filename.
335    pub fn set_language_from_name(&mut self, name: &str, registry: &GrammarRegistry) {
336        let detected = DetectedLanguage::from_virtual_name(name, registry);
337        tracing::debug!(
338            "Set highlighter for virtual buffer based on name: {} (backend: {}, language: {})",
339            name,
340            detected.highlighter.backend_name(),
341            detected.name
342        );
343        self.apply_language(detected);
344    }
345
346    /// Create an editor state from a file
347    ///
348    /// Note: width/height parameters are kept for backward compatibility but
349    /// are no longer used - viewport is now owned by SplitViewState.
350    pub fn from_file(
351        path: &std::path::Path,
352        _width: u16,
353        _height: u16,
354        large_file_threshold: usize,
355        registry: &GrammarRegistry,
356        fs: Arc<dyn FileSystem + Send + Sync>,
357    ) -> anyhow::Result<Self> {
358        let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
359        let first_line = buffer.first_line_lossy();
360        let detected = registry
361            .find_by_path(path, first_line.as_deref())
362            .map(|entry| DetectedLanguage::from_entry(entry, registry))
363            .unwrap_or_else(DetectedLanguage::plain_text);
364        let mut state = Self::new_from_buffer(buffer);
365        state.apply_language(detected);
366        Ok(state)
367    }
368
369    /// Create an editor state from a file with language configuration.
370    ///
371    /// This version uses the provided languages configuration for syntax detection,
372    /// allowing user-configured filename patterns to be respected for highlighting.
373    ///
374    /// Note: width/height parameters are kept for backward compatibility but
375    /// are no longer used - viewport is now owned by SplitViewState.
376    pub fn from_file_with_languages(
377        path: &std::path::Path,
378        _width: u16,
379        _height: u16,
380        large_file_threshold: usize,
381        registry: &GrammarRegistry,
382        languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
383        fs: Arc<dyn FileSystem + Send + Sync>,
384    ) -> anyhow::Result<Self> {
385        let buffer = Buffer::load_from_file(path, large_file_threshold, fs)?;
386        let first_line = buffer.first_line_lossy();
387        let detected =
388            DetectedLanguage::from_path(path, first_line.as_deref(), registry, languages);
389        let mut state = Self::new_from_buffer(buffer);
390        state.apply_language(detected);
391        Ok(state)
392    }
393
394    /// Create an editor state from a buffer and a pre-built `DetectedLanguage`.
395    ///
396    /// This is useful when you have already loaded a buffer with a specific encoding
397    /// and want to create an EditorState from it.
398    pub fn from_buffer_with_language(buffer: Buffer, detected: DetectedLanguage) -> Self {
399        let mut state = Self::new_from_buffer(buffer);
400        state.apply_language(detected);
401        state
402    }
403
404    /// Handle an Insert event - adjusts markers, buffer, highlighter, cursors, and line numbers
405    fn apply_insert(
406        &mut self,
407        cursors: &mut Cursors,
408        position: usize,
409        text: &str,
410        cursor_id: crate::model::event::CursorId,
411    ) {
412        let newlines_inserted = text.matches('\n').count();
413
414        // CRITICAL: Adjust markers BEFORE modifying buffer
415        self.marker_list.adjust_for_insert(position, text.len());
416        self.margins.adjust_for_insert(position, text.len());
417
418        // Insert text into buffer
419        self.buffer.insert(position, text);
420
421        // Notify highlighter of the insert (adjusts checkpoint marker positions)
422        // and invalidate span cache for the edited range.
423        self.highlighter.notify_insert(position, text.len());
424        self.highlighter
425            .invalidate_range(position..position + text.len());
426
427        // Note: reference_highlight_overlay uses markers that auto-adjust,
428        // so no manual invalidation needed
429
430        // Adjust all cursors after the edit
431        cursors.adjust_for_edit(position, 0, text.len());
432
433        // Move the cursor that made the edit to the end of the insertion
434        if let Some(cursor) = cursors.get_mut(cursor_id) {
435            cursor.position = position + text.len();
436            cursor.clear_selection();
437        }
438
439        // Update primary cursor line number if this was the primary cursor
440        if cursor_id == cursors.primary_id() {
441            self.primary_cursor_line_number = match self.primary_cursor_line_number {
442                LineNumber::Absolute(line) => LineNumber::Absolute(line + newlines_inserted),
443                LineNumber::Relative {
444                    line,
445                    from_cached_line,
446                } => LineNumber::Relative {
447                    line: line + newlines_inserted,
448                    from_cached_line,
449                },
450            };
451        }
452    }
453
454    /// Handle a Delete event - adjusts markers, buffer, highlighter, cursors, and line numbers
455    fn apply_delete(
456        &mut self,
457        cursors: &mut Cursors,
458        range: &std::ops::Range<usize>,
459        cursor_id: crate::model::event::CursorId,
460        deleted_text: &str,
461    ) {
462        let len = range.len();
463
464        // Count newlines deleted BEFORE the primary cursor's original position.
465        // For backspace: cursor was at range.end, so all deleted newlines are before it.
466        // For forward delete: cursor was at range.start, so no deleted newlines are before it.
467        let primary_newlines_removed = if cursor_id == cursors.primary_id() {
468            let cursor_pos = cursors.get(cursor_id).map_or(range.start, |c| c.position);
469            let bytes_before_cursor = cursor_pos
470                .saturating_sub(range.start)
471                .min(len)
472                .min(deleted_text.len());
473            deleted_text[..bytes_before_cursor].matches('\n').count()
474        } else {
475            0
476        };
477
478        // Drop virtual texts whose anchors are being erased. This is what
479        // makes inlay hints disappear immediately when the range containing
480        // them is deleted; without this the marker would just clamp to
481        // range.start and the hint would linger at the wrong position until
482        // the next LSP refresh.
483        self.virtual_texts
484            .remove_in_range(&mut self.marker_list, range.start, range.end);
485
486        // CRITICAL: Adjust markers BEFORE modifying buffer
487        self.marker_list.adjust_for_delete(range.start, len);
488        self.margins.adjust_for_delete(range.start, len);
489
490        // Delete from buffer
491        self.buffer.delete(range.clone());
492
493        // Notify highlighter of the delete (adjusts checkpoint marker positions)
494        // and invalidate span cache for the edited range.
495        self.highlighter.notify_delete(range.start, len);
496        self.highlighter.invalidate_range(range.clone());
497
498        // Note: reference_highlight_overlay uses markers that auto-adjust,
499        // so no manual invalidation needed
500
501        // Adjust all cursors after the edit
502        cursors.adjust_for_edit(range.start, len, 0);
503
504        // Move the cursor that made the edit to the start of deletion
505        if let Some(cursor) = cursors.get_mut(cursor_id) {
506            cursor.position = range.start;
507            cursor.clear_selection();
508        }
509
510        // Update primary cursor line number if this was the primary cursor
511        if cursor_id == cursors.primary_id() && primary_newlines_removed > 0 {
512            self.primary_cursor_line_number = match self.primary_cursor_line_number {
513                LineNumber::Absolute(line) => {
514                    LineNumber::Absolute(line.saturating_sub(primary_newlines_removed))
515                }
516                LineNumber::Relative {
517                    line,
518                    from_cached_line,
519                } => LineNumber::Relative {
520                    line: line.saturating_sub(primary_newlines_removed),
521                    from_cached_line,
522                },
523            };
524        }
525    }
526
527    /// Apply an event to the state - THE ONLY WAY TO MODIFY STATE
528    /// This is the heart of the event-driven architecture
529    pub fn apply(&mut self, cursors: &mut Cursors, event: &Event) {
530        match event {
531            Event::Insert {
532                position,
533                text,
534                cursor_id,
535            } => self.apply_insert(cursors, *position, text, *cursor_id),
536
537            Event::Delete {
538                range,
539                cursor_id,
540                deleted_text,
541            } => self.apply_delete(cursors, range, *cursor_id, deleted_text),
542
543            Event::MoveCursor {
544                cursor_id,
545                new_position,
546                new_anchor,
547                new_sticky_column,
548                ..
549            } => {
550                if let Some(cursor) = cursors.get_mut(*cursor_id) {
551                    cursor.position = *new_position;
552                    cursor.anchor = *new_anchor;
553                    cursor.sticky_column = *new_sticky_column;
554                }
555
556                // Update primary cursor line number if this is the primary cursor
557                // Try to get exact line number from buffer, or estimate for large files
558                if *cursor_id == cursors.primary_id() {
559                    self.primary_cursor_line_number =
560                        match self.buffer.offset_to_position(*new_position) {
561                            Some(pos) => LineNumber::Absolute(pos.line),
562                            None => {
563                                // Large file without line metadata - estimate line number
564                                // Use default estimated_line_length of 80 bytes
565                                let estimated_line = *new_position / 80;
566                                LineNumber::Absolute(estimated_line)
567                            }
568                        };
569                }
570            }
571
572            Event::AddCursor {
573                cursor_id,
574                position,
575                anchor,
576            } => {
577                let cursor = if let Some(anchor) = anchor {
578                    Cursor::with_selection(*anchor, *position)
579                } else {
580                    Cursor::new(*position)
581                };
582
583                // Insert cursor with the specific ID from the event
584                // This is important for undo/redo to work correctly
585                cursors.insert_with_id(*cursor_id, cursor);
586
587                cursors.normalize();
588            }
589
590            Event::RemoveCursor { cursor_id, .. } => {
591                cursors.remove(*cursor_id);
592            }
593
594            // View events (Scroll, SetViewport, Recenter) are now handled at Editor level
595            // via SplitViewState. They should not reach EditorState.apply().
596            Event::Scroll { .. } | Event::SetViewport { .. } | Event::Recenter => {
597                // These events are intercepted in Editor::apply_event_to_active_buffer
598                // and routed to SplitViewState. If we get here, something is wrong.
599                tracing::warn!("View event {:?} reached EditorState.apply() - should be handled by SplitViewState", event);
600            }
601
602            Event::SetAnchor {
603                cursor_id,
604                position,
605            } => {
606                // Set the anchor (selection start) for a specific cursor
607                // Also disable deselect_on_move so movement preserves the selection (Emacs mark mode)
608                if let Some(cursor) = cursors.get_mut(*cursor_id) {
609                    cursor.anchor = Some(*position);
610                    cursor.deselect_on_move = false;
611                }
612            }
613
614            Event::CancelAnchor { cursor_id } => {
615                // Re-enable deselect_on_move so that moving the cursor
616                // causes it to drop its selection (exiting "mark mode")
617                if let Some(cursor) = cursors.get_mut(*cursor_id) {
618                    cursor.deselect_on_move = true;
619                }
620            }
621
622            Event::ClearAnchor { cursor_id } => {
623                // Clear the anchor and reset deselect_on_move to cancel mark mode
624                // Also clear block selection if active
625                if let Some(cursor) = cursors.get_mut(*cursor_id) {
626                    cursor.anchor = None;
627                    cursor.deselect_on_move = true;
628                    cursor.clear_block_selection();
629                }
630            }
631
632            Event::ChangeMode { mode } => {
633                self.mode = mode.clone();
634            }
635
636            Event::AddOverlay {
637                namespace,
638                range,
639                face,
640                priority,
641                message,
642                extend_to_line_end,
643                url,
644            } => {
645                tracing::trace!(
646                    "AddOverlay: namespace={:?}, range={:?}, face={:?}, priority={}",
647                    namespace,
648                    range,
649                    face,
650                    priority
651                );
652                // Convert event overlay face to overlay face
653                let overlay_face = convert_event_face_to_overlay_face(face);
654                tracing::trace!("Converted face: {:?}", overlay_face);
655
656                let mut overlay = Overlay::with_priority(
657                    &mut self.marker_list,
658                    range.clone(),
659                    overlay_face,
660                    *priority,
661                );
662                overlay.namespace = namespace.clone();
663                overlay.message = message.clone();
664                overlay.extend_to_line_end = *extend_to_line_end;
665                overlay.url = url.clone();
666
667                let actual_range = overlay.range(&self.marker_list);
668                tracing::trace!(
669                    "Created overlay with markers - actual range: {:?}, handle={:?}",
670                    actual_range,
671                    overlay.handle
672                );
673
674                self.overlays.add(overlay);
675            }
676
677            Event::RemoveOverlay { handle } => {
678                tracing::trace!("RemoveOverlay: handle={:?}", handle);
679                self.overlays
680                    .remove_by_handle(handle, &mut self.marker_list);
681            }
682
683            Event::RemoveOverlaysInRange { range } => {
684                self.overlays.remove_in_range(range, &mut self.marker_list);
685            }
686
687            Event::ClearNamespace { namespace } => {
688                tracing::trace!("ClearNamespace: namespace={:?}", namespace);
689                self.overlays
690                    .clear_namespace(namespace, &mut self.marker_list);
691            }
692
693            Event::ClearOverlays => {
694                self.overlays.clear(&mut self.marker_list);
695            }
696
697            Event::ShowPopup { popup } => {
698                // The replay path has no theme handle, so we synthesize the
699                // popup with theme *defaults*. Editor-level callers
700                // (`Editor::apply_event_to_active_buffer`) intercept
701                // `Event::ShowPopup` *before* it reaches `state.apply` and
702                // build the popup with the live theme — see `event_apply.rs`.
703                // This arm is reached only when tests drive `state.apply`
704                // directly (no surrounding `Editor`).
705                use crate::view::theme::{default_popup_bg, default_popup_border_fg};
706                let popup_obj = convert_popup_data_to_popup(
707                    popup,
708                    default_popup_bg().into(),
709                    default_popup_border_fg().into(),
710                );
711                self.popups.show_or_replace(popup_obj);
712            }
713
714            Event::HidePopup => {
715                self.popups.hide();
716            }
717
718            Event::ClearPopups => {
719                self.popups.clear();
720            }
721
722            Event::PopupSelectNext => {
723                if let Some(popup) = self.popups.top_mut() {
724                    popup.select_next();
725                }
726            }
727
728            Event::PopupSelectPrev => {
729                if let Some(popup) = self.popups.top_mut() {
730                    popup.select_prev();
731                }
732            }
733
734            Event::PopupPageDown => {
735                if let Some(popup) = self.popups.top_mut() {
736                    popup.page_down();
737                }
738            }
739
740            Event::PopupPageUp => {
741                if let Some(popup) = self.popups.top_mut() {
742                    popup.page_up();
743                }
744            }
745
746            Event::AddMarginAnnotation {
747                line,
748                position,
749                content,
750                annotation_id,
751            } => {
752                let margin_position = convert_margin_position(position);
753                let margin_content = convert_margin_content(content);
754                let annotation = if let Some(id) = annotation_id {
755                    MarginAnnotation::with_id(*line, margin_position, margin_content, id.clone())
756                } else {
757                    MarginAnnotation::new(*line, margin_position, margin_content)
758                };
759                self.margins.add_annotation(annotation);
760            }
761
762            Event::RemoveMarginAnnotation { annotation_id } => {
763                self.margins.remove_by_id(annotation_id);
764            }
765
766            Event::RemoveMarginAnnotationsAtLine { line, position } => {
767                let margin_position = convert_margin_position(position);
768                self.margins.remove_at_line(*line, margin_position);
769            }
770
771            Event::ClearMarginPosition { position } => {
772                let margin_position = convert_margin_position(position);
773                self.margins.clear_position(margin_position);
774            }
775
776            Event::ClearMargins => {
777                self.margins.clear_all();
778            }
779
780            Event::SetLineNumbers { enabled } => {
781                self.margins.configure_for_line_numbers(*enabled);
782            }
783
784            // Split events are handled at the Editor level, not at EditorState level
785            // These are no-ops here as they affect the split layout, not buffer state
786            Event::SplitPane { .. }
787            | Event::CloseSplit { .. }
788            | Event::SetActiveSplit { .. }
789            | Event::AdjustSplitRatio { .. }
790            | Event::NextSplit
791            | Event::PrevSplit => {
792                // No-op: split events are handled by Editor, not EditorState
793            }
794
795            Event::Batch { events, .. } => {
796                // Apply all events in the batch sequentially
797                // This ensures multi-cursor operations are applied atomically
798                for event in events {
799                    self.apply(cursors, event);
800                }
801            }
802
803            Event::BulkEdit {
804                new_snapshot,
805                new_cursors,
806                edits,
807                displaced_markers,
808                ..
809            } => {
810                // Restore the target buffer state (piece tree + buffers) for this event.
811                // - For undo: snapshots are swapped, so new_snapshot is the original state
812                // - For redo: new_snapshot is the state after edits
813                // Restoring buffers alongside the tree is critical because
814                // consolidate_after_save() can replace buffers between snapshot and restore.
815                if let Some(snapshot) = new_snapshot {
816                    self.buffer.restore_buffer_state(snapshot);
817                }
818
819                // Replay marker adjustments from the edit list.
820                // For redo: same adjustments as the forward path.
821                // For undo: inverse() has swapped del/ins, so adjustments are reversed.
822                // Edits are in descending position order — process as-is so later
823                // positions are adjusted first (no cascading shift errors).
824                //
825                // For replacements (del > 0 AND ins > 0 at same position), we only
826                // adjust for the net delta to avoid the marker-at-boundary problem
827                // where sequential delete+insert pushes markers incorrectly.
828                for &(pos, del_len, ins_len) in edits {
829                    if del_len > 0 && ins_len > 0 {
830                        // Replacement: adjust by net delta only
831                        if ins_len > del_len {
832                            let net = ins_len - del_len;
833                            self.marker_list.adjust_for_insert(pos, net);
834                            self.margins.adjust_for_insert(pos, net);
835                        } else if del_len > ins_len {
836                            let net = del_len - ins_len;
837                            self.marker_list.adjust_for_delete(pos, net);
838                            self.margins.adjust_for_delete(pos, net);
839                        }
840                        // If equal: net delta 0, no adjustment needed
841                    } else if del_len > 0 {
842                        self.marker_list.adjust_for_delete(pos, del_len);
843                        self.margins.adjust_for_delete(pos, del_len);
844                    } else if ins_len > 0 {
845                        self.marker_list.adjust_for_insert(pos, ins_len);
846                        self.margins.adjust_for_insert(pos, ins_len);
847                    }
848                }
849
850                // Restore displaced markers to their original positions.
851                // This fixes markers that were inside a deleted range and collapsed
852                // to the deletion boundary — they're now moved back to their exact
853                // original positions after the text has been restored by undo.
854                if !displaced_markers.is_empty() {
855                    self.restore_displaced_markers(displaced_markers);
856                }
857
858                // Clear ephemeral decorations — their source systems will re-push
859                // correct positions after the edit notification.
860                self.virtual_texts.clear(&mut self.marker_list);
861
862                use crate::view::overlay::OverlayNamespace;
863                let namespaces = ["lsp-diagnostic", "reference-highlight", "bracket-highlight"];
864                for ns in &namespaces {
865                    self.overlays.clear_namespace(
866                        &OverlayNamespace::from_string(ns.to_string()),
867                        &mut self.marker_list,
868                    );
869                }
870
871                // Update cursor positions
872                for (cursor_id, position, anchor) in new_cursors {
873                    if let Some(cursor) = cursors.get_mut(*cursor_id) {
874                        cursor.position = *position;
875                        cursor.anchor = *anchor;
876                    }
877                }
878
879                // Invalidate highlight cache for entire buffer
880                self.highlighter.invalidate_all();
881
882                // Update primary cursor line number
883                let primary_pos = cursors.primary().position;
884                self.primary_cursor_line_number = match self.buffer.offset_to_position(primary_pos)
885                {
886                    Some(pos) => crate::model::buffer::LineNumber::Absolute(pos.line),
887                    None => crate::model::buffer::LineNumber::Absolute(0),
888                };
889            }
890        }
891    }
892
893    /// Capture positions of markers strictly inside a deleted range.
894    /// Call this BEFORE applying the delete. Returns encoded displaced markers.
895    pub fn capture_displaced_markers(&self, range: &Range<usize>) -> Vec<(u64, usize)> {
896        let mut displaced = Vec::new();
897        if range.is_empty() {
898            return displaced;
899        }
900        for (marker_id, start, _end) in self.marker_list.query_range(range.start, range.end) {
901            if start > range.start && start < range.end {
902                displaced.push(
903                    DisplacedMarker::Main {
904                        id: marker_id.0,
905                        position: start,
906                    }
907                    .encode(),
908                );
909            }
910        }
911        for (marker_id, start, _end) in self.margins.query_indicator_range(range.start, range.end) {
912            if start > range.start && start < range.end {
913                displaced.push(
914                    DisplacedMarker::Margin {
915                        id: marker_id.0,
916                        position: start,
917                    }
918                    .encode(),
919                );
920            }
921        }
922        displaced
923    }
924
925    /// Capture displaced markers for multiple delete ranges (BulkEdit).
926    pub fn capture_displaced_markers_bulk(
927        &self,
928        edits: &[(usize, usize, String)],
929    ) -> Vec<(u64, usize)> {
930        let mut displaced = Vec::new();
931        for (pos, del_len, _text) in edits {
932            if *del_len > 0 {
933                displaced.extend(self.capture_displaced_markers(&(*pos..*pos + *del_len)));
934            }
935        }
936        displaced
937    }
938
939    /// Restore displaced markers to their exact original positions.
940    pub fn restore_displaced_markers(&mut self, displaced: &[(u64, usize)]) {
941        for &(tagged_id, original_pos) in displaced {
942            let dm = DisplacedMarker::decode(tagged_id, original_pos);
943            match dm {
944                DisplacedMarker::Main { id, position } => {
945                    self.marker_list.set_position(MarkerId(id), position);
946                }
947                DisplacedMarker::Margin { id, position } => {
948                    self.margins.set_indicator_position(MarkerId(id), position);
949                }
950            }
951        }
952    }
953
954    /// Apply multiple events in sequence
955    pub fn apply_many(&mut self, cursors: &mut Cursors, events: &[Event]) {
956        for event in events {
957            self.apply(cursors, event);
958        }
959    }
960
961    /// Called when this buffer loses focus (e.g., switching to another buffer,
962    /// opening a prompt, focusing file explorer, etc.)
963    /// Dismisses transient popups like Hover and Signature Help.
964    pub fn on_focus_lost(&mut self) {
965        if self.popups.dismiss_transient() {
966            tracing::debug!("Dismissed transient popup on buffer focus loss");
967        }
968    }
969}
970
971/// Convert event overlay face to the actual overlay face
972fn convert_event_face_to_overlay_face(event_face: &EventOverlayFace) -> OverlayFace {
973    match event_face {
974        EventOverlayFace::Underline { color, style } => {
975            let underline_style = match style {
976                crate::model::event::UnderlineStyle::Straight => UnderlineStyle::Straight,
977                crate::model::event::UnderlineStyle::Wavy => UnderlineStyle::Wavy,
978                crate::model::event::UnderlineStyle::Dotted => UnderlineStyle::Dotted,
979                crate::model::event::UnderlineStyle::Dashed => UnderlineStyle::Dashed,
980            };
981            OverlayFace::Underline {
982                color: Color::Rgb(color.0, color.1, color.2),
983                style: underline_style,
984            }
985        }
986        EventOverlayFace::Background { color } => OverlayFace::Background {
987            color: Color::Rgb(color.0, color.1, color.2),
988        },
989        EventOverlayFace::Foreground { color } => OverlayFace::Foreground {
990            color: Color::Rgb(color.0, color.1, color.2),
991        },
992        EventOverlayFace::Style { options } => {
993            use crate::view::theme::named_color_from_str;
994            use ratatui::style::Modifier;
995
996            // Build fallback style from RGB values or named colors
997            let mut style = Style::default();
998
999            // Extract foreground color (RGB, named color, or default white)
1000            if let Some(ref fg) = options.fg {
1001                if let Some((r, g, b)) = fg.as_rgb() {
1002                    style = style.fg(Color::Rgb(r, g, b));
1003                } else if let Some(key) = fg.as_theme_key() {
1004                    if let Some(color) = named_color_from_str(key) {
1005                        style = style.fg(color);
1006                    }
1007                }
1008            }
1009
1010            // Extract background color (RGB, named color, or fallback)
1011            if let Some(ref bg) = options.bg {
1012                if let Some((r, g, b)) = bg.as_rgb() {
1013                    style = style.bg(Color::Rgb(r, g, b));
1014                } else if let Some(key) = bg.as_theme_key() {
1015                    if let Some(color) = named_color_from_str(key) {
1016                        style = style.bg(color);
1017                    }
1018                }
1019            }
1020
1021            // Apply modifiers
1022            let mut modifiers = Modifier::empty();
1023            if options.bold {
1024                modifiers |= Modifier::BOLD;
1025            }
1026            if options.italic {
1027                modifiers |= Modifier::ITALIC;
1028            }
1029            if options.underline {
1030                modifiers |= Modifier::UNDERLINED;
1031            }
1032            if options.strikethrough {
1033                modifiers |= Modifier::CROSSED_OUT;
1034            }
1035            if !modifiers.is_empty() {
1036                style = style.add_modifier(modifiers);
1037            }
1038
1039            // Extract theme keys (exclude recognized named colors, already resolved above)
1040            let fg_theme = options
1041                .fg
1042                .as_ref()
1043                .and_then(|c| c.as_theme_key())
1044                .filter(|key| named_color_from_str(key).is_none())
1045                .map(String::from);
1046            let bg_theme = options
1047                .bg
1048                .as_ref()
1049                .and_then(|c| c.as_theme_key())
1050                .filter(|key| named_color_from_str(key).is_none())
1051                .map(String::from);
1052
1053            // If theme keys are provided, use ThemedStyle for runtime resolution
1054            if fg_theme.is_some() || bg_theme.is_some() {
1055                OverlayFace::ThemedStyle {
1056                    fallback_style: style,
1057                    fg_theme,
1058                    bg_theme,
1059                    fg_on_collision_only: options.fg_on_collision_only,
1060                }
1061            } else {
1062                OverlayFace::Style { style }
1063            }
1064        }
1065    }
1066}
1067
1068/// Convert popup data to the actual popup object.
1069///
1070/// `popup_bg` and `popup_border_fg` come from the active theme (the
1071/// caller resolves them from `Theme::popup_bg` / `Theme::popup_border_fg`,
1072/// which are already `ratatui::style::Color`). The replay path inside
1073/// `EditorState::apply` has no theme handle and falls back to theme defaults.
1074pub(crate) fn convert_popup_data_to_popup(
1075    data: &PopupData,
1076    popup_bg: Color,
1077    popup_border_fg: Color,
1078) -> Popup {
1079    let content = match &data.content {
1080        crate::model::event::PopupContentData::Text(lines) => PopupContent::Text(lines.clone()),
1081        crate::model::event::PopupContentData::List { items, selected } => PopupContent::List {
1082            items: items
1083                .iter()
1084                .map(|item| PopupListItem {
1085                    text: item.text.clone(),
1086                    detail: item.detail.clone(),
1087                    icon: item.icon.clone(),
1088                    data: item.data.clone(),
1089                    disabled: false,
1090                })
1091                .collect(),
1092            selected: *selected,
1093        },
1094    };
1095
1096    let position = match data.position {
1097        PopupPositionData::AtCursor => PopupPosition::AtCursor,
1098        PopupPositionData::BelowCursor => PopupPosition::BelowCursor,
1099        PopupPositionData::AboveCursor => PopupPosition::AboveCursor,
1100        PopupPositionData::Fixed { x, y } => PopupPosition::Fixed { x, y },
1101        PopupPositionData::Centered => PopupPosition::Centered,
1102        PopupPositionData::BottomRight => PopupPosition::BottomRight,
1103        PopupPositionData::AboveStatusBarAt { x, status_row } => {
1104            PopupPosition::AboveStatusBarAt { x, status_row }
1105        }
1106    };
1107
1108    // Map the explicit kind hint to PopupKind for input handling
1109    let kind = match data.kind {
1110        crate::model::event::PopupKindHint::Completion => PopupKind::Completion,
1111        crate::model::event::PopupKindHint::List => PopupKind::List,
1112        crate::model::event::PopupKindHint::Text => PopupKind::Text,
1113    };
1114
1115    // Kind-implied resolver default: a popup whose kind is
1116    // `Completion` always confirms by inserting the selected
1117    // completion, regardless of who built it. Other kinds need an
1118    // explicit resolver (LSP confirm, plugin action, LSP status, code
1119    // action) because the same `List` kind is used for all four, so we
1120    // can't infer which feature owns the popup from its kind alone.
1121    let resolver = match kind {
1122        PopupKind::Completion => crate::view::popup::PopupResolver::Completion,
1123        _ => crate::view::popup::PopupResolver::None,
1124    };
1125
1126    // Popups that appear under the user's cursor without an explicit
1127    // user gesture default to *unfocused* so the next keystroke drives
1128    // the buffer rather than the popup. The user grabs focus with
1129    // `popup_focus` (default `Alt+T`). Popups that the user
1130    // *explicitly* invokes (completion via Tab, list/action choosers
1131    // via plugin commands, …) keep the historical focused-on-show
1132    // behavior — type-to-filter and arrow-nav need to "just work"
1133    // without an extra keystroke.
1134    // Only `PopupKindHint` variants reach this path — Completion, List,
1135    // Text. Hover/Action popups are constructed directly in editor code
1136    // (`lsp_requests.rs`) and set their own `focused` flag.
1137    let focused = match kind {
1138        // Completion popups are user-invoked (type-to-trigger / explicit
1139        // `lsp_completion`), so type-to-filter and arrow-nav need to
1140        // "just work" without the user pressing the focus-popup key.
1141        PopupKind::Completion => true,
1142        // List popups are typically explicit user invocations (plugin
1143        // action popups, status-bar menus). Keep them focused on show.
1144        PopupKind::List => true,
1145        // Text popups are auto-shown informational overlays —
1146        // unfocused so they don't swallow the user's next keystroke.
1147        PopupKind::Text => false,
1148        // Direct-construction kinds (Hover, Action) are not produced by
1149        // `Event::ShowPopup`; default to unfocused if ever reached so
1150        // an auto-shown overlay doesn't grab the keyboard by accident.
1151        PopupKind::Hover | PopupKind::Action => false,
1152    };
1153
1154    Popup {
1155        kind,
1156        title: data.title.clone(),
1157        description: data.description.clone(),
1158        transient: data.transient,
1159        content,
1160        position,
1161        width: data.width,
1162        max_height: data.max_height,
1163        bordered: data.bordered,
1164        border_style: Style::default().fg(popup_border_fg),
1165        background_style: Style::default().bg(popup_bg),
1166        scroll_offset: 0,
1167        text_selection: None,
1168        accept_key_hint: None,
1169        resolver,
1170        focused,
1171        focus_key_hint: None,
1172    }
1173}
1174
1175/// Convert margin position data to the actual margin position
1176fn convert_margin_position(position: &MarginPositionData) -> MarginPosition {
1177    match position {
1178        MarginPositionData::Left => MarginPosition::Left,
1179        MarginPositionData::Right => MarginPosition::Right,
1180    }
1181}
1182
1183/// Convert margin content data to the actual margin content
1184fn convert_margin_content(content: &MarginContentData) -> MarginContent {
1185    match content {
1186        MarginContentData::Text(text) => MarginContent::Text(text.clone()),
1187        MarginContentData::Symbol { text, color } => {
1188            if let Some((r, g, b)) = color {
1189                MarginContent::colored_symbol(text.clone(), Color::Rgb(*r, *g, *b))
1190            } else {
1191                MarginContent::symbol(text.clone(), Style::default())
1192            }
1193        }
1194        MarginContentData::Empty => MarginContent::Empty,
1195    }
1196}
1197
1198impl EditorState {
1199    /// Prepare viewport for rendering (called before frame render)
1200    ///
1201    /// This pre-loads all data that will be needed for rendering the current viewport,
1202    /// ensuring that subsequent read-only access during rendering will succeed.
1203    ///
1204    /// Takes viewport parameters since viewport is now owned by SplitViewState.
1205    pub fn prepare_for_render(&mut self, top_byte: usize, height: u16) -> Result<()> {
1206        self.buffer.prepare_viewport(top_byte, height as usize)?;
1207        Ok(())
1208    }
1209
1210    /// Resolve all plugin-injected virtual-line anchor byte positions
1211    /// for this buffer.  Sorted ascending.
1212    ///
1213    /// Used by `Viewport::scroll_down` / `scroll_up` /
1214    /// `find_max_visual_scroll_position` so the scroll math counts the
1215    /// rows the renderer actually draws (e.g. markdown_compose's
1216    /// `┌─┬─┐` table borders) when computing `max_scroll_row`.  Without
1217    /// this, mouse wheel and PageDown clamp to a row count that
1218    /// ignores virtual lines and stop short of the buffer's real tail.
1219    ///
1220    /// Empty when no plugin has added virtual lines.
1221    pub fn collect_virtual_line_positions(&self) -> Vec<usize> {
1222        if self.virtual_texts.is_empty() {
1223            return Vec::new();
1224        }
1225        let mut v: Vec<usize> = self
1226            .virtual_texts
1227            .query_lines_in_range(&self.marker_list, 0, self.buffer.len() + 1)
1228            .into_iter()
1229            .map(|(pos, _vt)| pos)
1230            .collect();
1231        v.sort_unstable();
1232        v
1233    }
1234
1235    /// Resolve all plugin-injected soft-break `(byte_position, indent)`
1236    /// pairs for this buffer.
1237    ///
1238    /// Returns a sorted slice suitable for passing to `Viewport::scroll_up` /
1239    /// `scroll_down`, which use it to keep their visual-row counting in
1240    /// lock-step with the renderer (which applies these same breaks via
1241    /// `apply_soft_breaks`).  The `indent` field is the column count of
1242    /// hanging-indent spaces the plugin asked the renderer to inject
1243    /// after the break — the wrap counter needs it to compute the
1244    /// continuation segment's effective width correctly.
1245    ///
1246    /// Empty when no plugin is wrapping the buffer.
1247    pub fn collect_soft_break_positions(&self) -> Vec<(usize, u16)> {
1248        if self.soft_breaks.is_empty() {
1249            return Vec::new();
1250        }
1251        // query_viewport already returns pairs sorted by ascending position.
1252        self.soft_breaks
1253            .query_viewport(0, self.buffer.len() + 1, &self.marker_list)
1254    }
1255
1256    // ========== DocumentModel Helper Methods ==========
1257    // These methods provide convenient access to DocumentModel functionality
1258    // while maintaining backward compatibility with existing code.
1259
1260    /// Get text in a range, driving lazy loading transparently
1261    ///
1262    /// This is a convenience wrapper around DocumentModel::get_range that:
1263    /// - Drives lazy loading automatically (never fails due to unloaded data)
1264    /// - Uses byte offsets directly
1265    /// - Returns String (not Result) - errors are logged internally
1266    /// - Returns empty string for invalid ranges
1267    ///
1268    /// This is the preferred API for getting text ranges. The caller never needs
1269    /// to worry about lazy loading or buffer preparation.
1270    ///
1271    /// # Example
1272    /// ```ignore
1273    /// let text = state.get_text_range(0, 100);
1274    /// ```
1275    pub fn get_text_range(&mut self, start: usize, end: usize) -> String {
1276        // TextBuffer::get_text_range_mut() handles lazy loading automatically
1277        match self
1278            .buffer
1279            .get_text_range_mut(start, end.saturating_sub(start))
1280        {
1281            Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1282            Err(e) => {
1283                tracing::warn!("Failed to get text range {}..{}: {}", start, end, e);
1284                String::new()
1285            }
1286        }
1287    }
1288
1289    /// Get the content of a line by its byte offset
1290    ///
1291    /// Returns the line containing the given offset, along with its start position.
1292    /// This uses DocumentModel's viewport functionality for consistent behavior.
1293    ///
1294    /// # Returns
1295    /// `Some((line_start_offset, line_content))` if successful, `None` if offset is invalid
1296    pub fn get_line_at_offset(&mut self, offset: usize) -> Option<(usize, String)> {
1297        use crate::model::document_model::DocumentModel;
1298
1299        // Find the start of the line containing this offset
1300        // Scan backwards to find the previous newline or start of buffer
1301        let mut line_start = offset;
1302        while line_start > 0 {
1303            if let Ok(text) = self.buffer.get_text_range_mut(line_start - 1, 1) {
1304                if text.first() == Some(&b'\n') {
1305                    break;
1306                }
1307                line_start -= 1;
1308            } else {
1309                break;
1310            }
1311        }
1312
1313        // Get a single line viewport starting at the line start
1314        let viewport = self
1315            .get_viewport_content(
1316                crate::model::document_model::DocumentPosition::byte(line_start),
1317                1,
1318            )
1319            .ok()?;
1320
1321        viewport
1322            .lines
1323            .first()
1324            .map(|line| (line.byte_offset, line.content.clone()))
1325    }
1326
1327    /// Get text from current cursor position to end of line
1328    ///
1329    /// This is a common pattern in editing operations. Uses DocumentModel
1330    /// for consistent behavior across file sizes.
1331    pub fn get_text_to_end_of_line(&mut self, cursor_pos: usize) -> Result<String> {
1332        use crate::model::document_model::DocumentModel;
1333
1334        // Get the line containing cursor
1335        let viewport = self.get_viewport_content(
1336            crate::model::document_model::DocumentPosition::byte(cursor_pos),
1337            1,
1338        )?;
1339
1340        if let Some(line) = viewport.lines.first() {
1341            let line_start = line.byte_offset;
1342            let line_end = line_start + line.content.len();
1343
1344            if cursor_pos >= line_start && cursor_pos <= line_end {
1345                let offset_in_line = cursor_pos - line_start;
1346                // Use get() to safely handle potential non-char-boundary offsets
1347                Ok(line.content.get(offset_in_line..).unwrap_or("").to_string())
1348            } else {
1349                Ok(String::new())
1350            }
1351        } else {
1352            Ok(String::new())
1353        }
1354    }
1355
1356    /// Replace cached semantic tokens with a new store.
1357    pub fn set_semantic_tokens(&mut self, store: SemanticTokenStore) {
1358        self.semantic_tokens = Some(store);
1359    }
1360
1361    /// Clear cached semantic tokens (e.g., when tokens are invalidated).
1362    pub fn clear_semantic_tokens(&mut self) {
1363        self.semantic_tokens = None;
1364    }
1365
1366    /// Get the server-provided semantic token result_id if available.
1367    pub fn semantic_tokens_result_id(&self) -> Option<&str> {
1368        self.semantic_tokens
1369            .as_ref()
1370            .and_then(|store| store.result_id.as_deref())
1371    }
1372}
1373
1374/// Implement DocumentModel trait for EditorState
1375///
1376/// This provides a clean abstraction layer between rendering/editing operations
1377/// and the underlying text buffer implementation.
1378impl DocumentModel for EditorState {
1379    fn capabilities(&self) -> DocumentCapabilities {
1380        let line_count = self.buffer.line_count();
1381        DocumentCapabilities {
1382            has_line_index: line_count.is_some(),
1383            uses_lazy_loading: false, // TODO: add large file detection
1384            byte_length: self.buffer.len(),
1385            approximate_line_count: line_count.unwrap_or_else(|| {
1386                // Estimate assuming ~80 bytes per line
1387                self.buffer.len() / 80
1388            }),
1389        }
1390    }
1391
1392    fn get_viewport_content(
1393        &mut self,
1394        start_pos: DocumentPosition,
1395        max_lines: usize,
1396    ) -> Result<ViewportContent> {
1397        // Convert to byte offset
1398        let start_offset = self.position_to_offset(start_pos)?;
1399
1400        // Use new efficient line iteration that tracks line numbers during iteration
1401        // by accumulating line_feed_cnt from pieces (single source of truth)
1402        let line_iter = self.buffer.iter_lines_from(start_offset, max_lines)?;
1403        let has_more = line_iter.has_more;
1404
1405        let lines = line_iter
1406            .map(|line_data| ViewportLine {
1407                byte_offset: line_data.byte_offset,
1408                content: line_data.content,
1409                has_newline: line_data.has_newline,
1410                approximate_line_number: line_data.line_number,
1411            })
1412            .collect();
1413
1414        Ok(ViewportContent {
1415            start_position: DocumentPosition::ByteOffset(start_offset),
1416            lines,
1417            has_more,
1418        })
1419    }
1420
1421    fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize> {
1422        match pos {
1423            DocumentPosition::ByteOffset(offset) => Ok(offset),
1424            DocumentPosition::LineColumn { line, column } => {
1425                if !self.has_line_index() {
1426                    anyhow::bail!("Line indexing not available for this document");
1427                }
1428                // Use piece tree's position conversion
1429                let position = crate::model::piece_tree::Position { line, column };
1430                Ok(self.buffer.position_to_offset(position))
1431            }
1432        }
1433    }
1434
1435    fn offset_to_position(&self, offset: usize) -> DocumentPosition {
1436        if self.has_line_index() {
1437            if let Some(pos) = self.buffer.offset_to_position(offset) {
1438                DocumentPosition::LineColumn {
1439                    line: pos.line,
1440                    column: pos.column,
1441                }
1442            } else {
1443                // Line index exists but metadata unavailable - fall back to byte offset
1444                DocumentPosition::ByteOffset(offset)
1445            }
1446        } else {
1447            DocumentPosition::ByteOffset(offset)
1448        }
1449    }
1450
1451    fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String> {
1452        let start_offset = self.position_to_offset(start)?;
1453        let end_offset = self.position_to_offset(end)?;
1454
1455        if start_offset > end_offset {
1456            anyhow::bail!(
1457                "Invalid range: start offset {} > end offset {}",
1458                start_offset,
1459                end_offset
1460            );
1461        }
1462
1463        let bytes = self
1464            .buffer
1465            .get_text_range_mut(start_offset, end_offset - start_offset)?;
1466
1467        Ok(String::from_utf8_lossy(&bytes).into_owned())
1468    }
1469
1470    fn get_line_content(&mut self, line_number: usize) -> Option<String> {
1471        if !self.has_line_index() {
1472            return None;
1473        }
1474
1475        // Convert line number to byte offset
1476        let line_start_offset = self.buffer.line_start_offset(line_number)?;
1477
1478        // Get line content using iterator
1479        let mut iter = self.buffer.line_iterator(line_start_offset, 80);
1480        if let Some((_start, content)) = iter.next_line() {
1481            let has_newline = content.ends_with('\n');
1482            let line_content = if has_newline {
1483                content[..content.len() - 1].to_string()
1484            } else {
1485                content
1486            };
1487            Some(line_content)
1488        } else {
1489            None
1490        }
1491    }
1492
1493    fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)> {
1494        let bytes = self.buffer.get_text_range_mut(offset, size)?;
1495
1496        Ok((offset, String::from_utf8_lossy(&bytes).into_owned()))
1497    }
1498
1499    fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize> {
1500        let offset = self.position_to_offset(pos)?;
1501        self.buffer.insert_bytes(offset, text.as_bytes().to_vec());
1502        Ok(text.len())
1503    }
1504
1505    fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()> {
1506        let start_offset = self.position_to_offset(start)?;
1507        let end_offset = self.position_to_offset(end)?;
1508
1509        if start_offset > end_offset {
1510            anyhow::bail!(
1511                "Invalid range: start offset {} > end offset {}",
1512                start_offset,
1513                end_offset
1514            );
1515        }
1516
1517        self.buffer.delete(start_offset..end_offset);
1518        Ok(())
1519    }
1520
1521    fn replace(
1522        &mut self,
1523        start: DocumentPosition,
1524        end: DocumentPosition,
1525        text: &str,
1526    ) -> Result<()> {
1527        // Delete then insert
1528        self.delete(start, end)?;
1529        self.insert(start, text)?;
1530        Ok(())
1531    }
1532
1533    fn find_matches(
1534        &mut self,
1535        pattern: &str,
1536        search_range: Option<(DocumentPosition, DocumentPosition)>,
1537    ) -> Result<Vec<usize>> {
1538        let (start_offset, end_offset) = if let Some((start, end)) = search_range {
1539            (
1540                self.position_to_offset(start)?,
1541                self.position_to_offset(end)?,
1542            )
1543        } else {
1544            (0, self.buffer.len())
1545        };
1546
1547        // Get text in range
1548        let bytes = self
1549            .buffer
1550            .get_text_range_mut(start_offset, end_offset - start_offset)?;
1551        let text = String::from_utf8_lossy(&bytes);
1552
1553        // Find all matches (simple substring search for now)
1554        let mut matches = Vec::new();
1555        let mut search_offset = 0;
1556        while let Some(pos) = text[search_offset..].find(pattern) {
1557            matches.push(start_offset + search_offset + pos);
1558            search_offset += pos + pattern.len();
1559        }
1560
1561        Ok(matches)
1562    }
1563}
1564
1565/// Cached semantic tokens for a buffer.
1566#[derive(Clone, Debug)]
1567pub struct SemanticTokenStore {
1568    /// Buffer version the tokens correspond to.
1569    pub version: u64,
1570    /// Server-provided result identifier (if any).
1571    pub result_id: Option<String>,
1572    /// Raw semantic token data (u32 array, 5 integers per token).
1573    pub data: Vec<u32>,
1574    /// All semantic token spans resolved to byte ranges.
1575    pub tokens: Vec<SemanticTokenSpan>,
1576}
1577
1578/// A semantic token span resolved to buffer byte offsets.
1579#[derive(Clone, Debug)]
1580pub struct SemanticTokenSpan {
1581    pub range: Range<usize>,
1582    pub token_type: String,
1583    pub modifiers: Vec<String>,
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588    use crate::model::filesystem::StdFileSystem;
1589    use std::sync::Arc;
1590
1591    fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
1592        Arc::new(StdFileSystem)
1593    }
1594    use super::*;
1595    use crate::model::event::CursorId;
1596
1597    #[test]
1598    fn test_state_new() {
1599        let state = EditorState::new(
1600            80,
1601            24,
1602            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1603            test_fs(),
1604        );
1605        assert!(state.buffer.is_empty());
1606    }
1607
1608    #[test]
1609    fn test_apply_insert() {
1610        let mut state = EditorState::new(
1611            80,
1612            24,
1613            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1614            test_fs(),
1615        );
1616        let mut cursors = Cursors::new();
1617        let cursor_id = cursors.primary_id();
1618
1619        state.apply(
1620            &mut cursors,
1621            &Event::Insert {
1622                position: 0,
1623                text: "hello".to_string(),
1624                cursor_id,
1625            },
1626        );
1627
1628        assert_eq!(state.buffer.to_string().unwrap(), "hello");
1629        assert_eq!(cursors.primary().position, 5);
1630        assert!(state.buffer.is_modified());
1631    }
1632
1633    #[test]
1634    fn test_apply_delete() {
1635        let mut state = EditorState::new(
1636            80,
1637            24,
1638            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1639            test_fs(),
1640        );
1641        let mut cursors = Cursors::new();
1642        let cursor_id = cursors.primary_id();
1643
1644        // Insert then delete
1645        state.apply(
1646            &mut cursors,
1647            &Event::Insert {
1648                position: 0,
1649                text: "hello world".to_string(),
1650                cursor_id,
1651            },
1652        );
1653
1654        state.apply(
1655            &mut cursors,
1656            &Event::Delete {
1657                range: 5..11,
1658                deleted_text: " world".to_string(),
1659                cursor_id,
1660            },
1661        );
1662
1663        assert_eq!(state.buffer.to_string().unwrap(), "hello");
1664        assert_eq!(cursors.primary().position, 5);
1665    }
1666
1667    #[test]
1668    fn test_apply_move_cursor() {
1669        let mut state = EditorState::new(
1670            80,
1671            24,
1672            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1673            test_fs(),
1674        );
1675        let mut cursors = Cursors::new();
1676        let cursor_id = cursors.primary_id();
1677
1678        state.apply(
1679            &mut cursors,
1680            &Event::Insert {
1681                position: 0,
1682                text: "hello".to_string(),
1683                cursor_id,
1684            },
1685        );
1686
1687        state.apply(
1688            &mut cursors,
1689            &Event::MoveCursor {
1690                cursor_id,
1691                old_position: 5,
1692                new_position: 2,
1693                old_anchor: None,
1694                new_anchor: None,
1695                old_sticky_column: 0,
1696                new_sticky_column: 0,
1697            },
1698        );
1699
1700        assert_eq!(cursors.primary().position, 2);
1701    }
1702
1703    #[test]
1704    fn test_apply_add_cursor() {
1705        let mut state = EditorState::new(
1706            80,
1707            24,
1708            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1709            test_fs(),
1710        );
1711        let mut cursors = Cursors::new();
1712        let cursor_id = CursorId(1);
1713
1714        state.apply(
1715            &mut cursors,
1716            &Event::AddCursor {
1717                cursor_id,
1718                position: 5,
1719                anchor: None,
1720            },
1721        );
1722
1723        assert_eq!(cursors.count(), 2);
1724    }
1725
1726    #[test]
1727    fn test_apply_many() {
1728        let mut state = EditorState::new(
1729            80,
1730            24,
1731            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1732            test_fs(),
1733        );
1734        let mut cursors = Cursors::new();
1735        let cursor_id = cursors.primary_id();
1736
1737        let events = vec![
1738            Event::Insert {
1739                position: 0,
1740                text: "hello ".to_string(),
1741                cursor_id,
1742            },
1743            Event::Insert {
1744                position: 6,
1745                text: "world".to_string(),
1746                cursor_id,
1747            },
1748        ];
1749
1750        state.apply_many(&mut cursors, &events);
1751
1752        assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1753    }
1754
1755    #[test]
1756    fn test_cursor_adjustment_after_insert() {
1757        let mut state = EditorState::new(
1758            80,
1759            24,
1760            crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1761            test_fs(),
1762        );
1763        let mut cursors = Cursors::new();
1764        let cursor_id = cursors.primary_id();
1765
1766        // Add a second cursor at position 5
1767        state.apply(
1768            &mut cursors,
1769            &Event::AddCursor {
1770                cursor_id: CursorId(1),
1771                position: 5,
1772                anchor: None,
1773            },
1774        );
1775
1776        // Insert at position 0 - should push second cursor forward
1777        state.apply(
1778            &mut cursors,
1779            &Event::Insert {
1780                position: 0,
1781                text: "abc".to_string(),
1782                cursor_id,
1783            },
1784        );
1785
1786        // Second cursor should be at position 5 + 3 = 8
1787        if let Some(cursor) = cursors.get(CursorId(1)) {
1788            assert_eq!(cursor.position, 8);
1789        }
1790    }
1791
1792    // DocumentModel trait tests
1793    mod document_model_tests {
1794        use super::*;
1795        use crate::model::document_model::{DocumentModel, DocumentPosition};
1796
1797        #[test]
1798        fn test_capabilities_small_file() {
1799            let mut state = EditorState::new(
1800                80,
1801                24,
1802                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1803                test_fs(),
1804            );
1805            state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1806
1807            let caps = state.capabilities();
1808            assert!(caps.has_line_index, "Small file should have line index");
1809            assert_eq!(caps.byte_length, "line1\nline2\nline3".len());
1810            assert_eq!(caps.approximate_line_count, 3, "Should have 3 lines");
1811        }
1812
1813        #[test]
1814        fn test_position_conversions() {
1815            let mut state = EditorState::new(
1816                80,
1817                24,
1818                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1819                test_fs(),
1820            );
1821            state.buffer = Buffer::from_str_test("hello\nworld\ntest");
1822
1823            // Test ByteOffset -> offset
1824            let pos1 = DocumentPosition::ByteOffset(6);
1825            let offset1 = state.position_to_offset(pos1).unwrap();
1826            assert_eq!(offset1, 6);
1827
1828            // Test LineColumn -> offset
1829            let pos2 = DocumentPosition::LineColumn { line: 1, column: 0 };
1830            let offset2 = state.position_to_offset(pos2).unwrap();
1831            assert_eq!(offset2, 6, "Line 1, column 0 should be at byte 6");
1832
1833            // Test offset -> position (should return LineColumn for small files)
1834            let converted = state.offset_to_position(6);
1835            match converted {
1836                DocumentPosition::LineColumn { line, column } => {
1837                    assert_eq!(line, 1);
1838                    assert_eq!(column, 0);
1839                }
1840                _ => panic!("Expected LineColumn for small file"),
1841            }
1842        }
1843
1844        #[test]
1845        fn test_get_viewport_content() {
1846            let mut state = EditorState::new(
1847                80,
1848                24,
1849                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1850                test_fs(),
1851            );
1852            state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1853
1854            let content = state
1855                .get_viewport_content(DocumentPosition::ByteOffset(0), 3)
1856                .unwrap();
1857
1858            assert_eq!(content.lines.len(), 3);
1859            assert_eq!(content.lines[0].content, "line1");
1860            assert_eq!(content.lines[1].content, "line2");
1861            assert_eq!(content.lines[2].content, "line3");
1862            assert!(content.has_more);
1863        }
1864
1865        #[test]
1866        fn test_get_range() {
1867            let mut state = EditorState::new(
1868                80,
1869                24,
1870                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1871                test_fs(),
1872            );
1873            state.buffer = Buffer::from_str_test("hello world");
1874
1875            let text = state
1876                .get_range(
1877                    DocumentPosition::ByteOffset(0),
1878                    DocumentPosition::ByteOffset(5),
1879                )
1880                .unwrap();
1881            assert_eq!(text, "hello");
1882
1883            let text2 = state
1884                .get_range(
1885                    DocumentPosition::ByteOffset(6),
1886                    DocumentPosition::ByteOffset(11),
1887                )
1888                .unwrap();
1889            assert_eq!(text2, "world");
1890        }
1891
1892        #[test]
1893        fn test_get_line_content() {
1894            let mut state = EditorState::new(
1895                80,
1896                24,
1897                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1898                test_fs(),
1899            );
1900            state.buffer = Buffer::from_str_test("line1\nline2\nline3");
1901
1902            let line0 = state.get_line_content(0).unwrap();
1903            assert_eq!(line0, "line1");
1904
1905            let line1 = state.get_line_content(1).unwrap();
1906            assert_eq!(line1, "line2");
1907
1908            let line2 = state.get_line_content(2).unwrap();
1909            assert_eq!(line2, "line3");
1910        }
1911
1912        #[test]
1913        fn test_insert_delete() {
1914            let mut state = EditorState::new(
1915                80,
1916                24,
1917                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1918                test_fs(),
1919            );
1920            state.buffer = Buffer::from_str_test("hello world");
1921
1922            // Insert text
1923            let bytes_inserted = state
1924                .insert(DocumentPosition::ByteOffset(6), "beautiful ")
1925                .unwrap();
1926            assert_eq!(bytes_inserted, 10);
1927            assert_eq!(state.buffer.to_string().unwrap(), "hello beautiful world");
1928
1929            // Delete text
1930            state
1931                .delete(
1932                    DocumentPosition::ByteOffset(6),
1933                    DocumentPosition::ByteOffset(16),
1934                )
1935                .unwrap();
1936            assert_eq!(state.buffer.to_string().unwrap(), "hello world");
1937        }
1938
1939        #[test]
1940        fn test_replace() {
1941            let mut state = EditorState::new(
1942                80,
1943                24,
1944                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1945                test_fs(),
1946            );
1947            state.buffer = Buffer::from_str_test("hello world");
1948
1949            state
1950                .replace(
1951                    DocumentPosition::ByteOffset(0),
1952                    DocumentPosition::ByteOffset(5),
1953                    "hi",
1954                )
1955                .unwrap();
1956            assert_eq!(state.buffer.to_string().unwrap(), "hi world");
1957        }
1958
1959        #[test]
1960        fn test_find_matches() {
1961            let mut state = EditorState::new(
1962                80,
1963                24,
1964                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1965                test_fs(),
1966            );
1967            state.buffer = Buffer::from_str_test("hello world hello");
1968
1969            let matches = state.find_matches("hello", None).unwrap();
1970            assert_eq!(matches.len(), 2);
1971            assert_eq!(matches[0], 0);
1972            assert_eq!(matches[1], 12);
1973        }
1974
1975        #[test]
1976        fn test_prepare_for_render() {
1977            let mut state = EditorState::new(
1978                80,
1979                24,
1980                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1981                test_fs(),
1982            );
1983            state.buffer = Buffer::from_str_test("line1\nline2\nline3\nline4\nline5");
1984
1985            // Should not panic - pass top_byte=0 and height=24 (typical viewport params)
1986            state.prepare_for_render(0, 24).unwrap();
1987        }
1988
1989        #[test]
1990        fn test_helper_get_text_range() {
1991            let mut state = EditorState::new(
1992                80,
1993                24,
1994                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
1995                test_fs(),
1996            );
1997            state.buffer = Buffer::from_str_test("hello world");
1998
1999            // Test normal range
2000            let text = state.get_text_range(0, 5);
2001            assert_eq!(text, "hello");
2002
2003            // Test middle range
2004            let text2 = state.get_text_range(6, 11);
2005            assert_eq!(text2, "world");
2006        }
2007
2008        #[test]
2009        fn test_helper_get_line_at_offset() {
2010            let mut state = EditorState::new(
2011                80,
2012                24,
2013                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2014                test_fs(),
2015            );
2016            state.buffer = Buffer::from_str_test("line1\nline2\nline3");
2017
2018            // Get first line (offset 0)
2019            let (offset, content) = state.get_line_at_offset(0).unwrap();
2020            assert_eq!(offset, 0);
2021            assert_eq!(content, "line1");
2022
2023            // Get second line (offset in middle of line)
2024            let (offset2, content2) = state.get_line_at_offset(8).unwrap();
2025            assert_eq!(offset2, 6); // Line starts at byte 6
2026            assert_eq!(content2, "line2");
2027
2028            // Get last line
2029            let (offset3, content3) = state.get_line_at_offset(12).unwrap();
2030            assert_eq!(offset3, 12);
2031            assert_eq!(content3, "line3");
2032        }
2033
2034        #[test]
2035        fn test_helper_get_text_to_end_of_line() {
2036            let mut state = EditorState::new(
2037                80,
2038                24,
2039                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2040                test_fs(),
2041            );
2042            state.buffer = Buffer::from_str_test("hello world\nline2");
2043
2044            // From beginning of line
2045            let text = state.get_text_to_end_of_line(0).unwrap();
2046            assert_eq!(text, "hello world");
2047
2048            // From middle of line
2049            let text2 = state.get_text_to_end_of_line(6).unwrap();
2050            assert_eq!(text2, "world");
2051
2052            // From end of line
2053            let text3 = state.get_text_to_end_of_line(11).unwrap();
2054            assert_eq!(text3, "");
2055
2056            // From second line
2057            let text4 = state.get_text_to_end_of_line(12).unwrap();
2058            assert_eq!(text4, "line2");
2059        }
2060    }
2061
2062    // Virtual text integration tests
2063    mod virtual_text_integration_tests {
2064        use super::*;
2065        use crate::view::virtual_text::VirtualTextPosition;
2066        use ratatui::style::Style;
2067
2068        #[test]
2069        fn test_virtual_text_add_and_query() {
2070            let mut state = EditorState::new(
2071                80,
2072                24,
2073                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2074                test_fs(),
2075            );
2076            state.buffer = Buffer::from_str_test("hello world");
2077
2078            // Initialize marker list for buffer
2079            if !state.buffer.is_empty() {
2080                state.marker_list.adjust_for_insert(0, state.buffer.len());
2081            }
2082
2083            // Add virtual text at position 5 (after 'hello')
2084            let vtext_id = state.virtual_texts.add(
2085                &mut state.marker_list,
2086                5,
2087                ": string".to_string(),
2088                Style::default(),
2089                VirtualTextPosition::AfterChar,
2090                0,
2091            );
2092
2093            // Query should return the virtual text
2094            let results = state.virtual_texts.query_range(&state.marker_list, 0, 11);
2095            assert_eq!(results.len(), 1);
2096            assert_eq!(results[0].0, 5); // Position
2097            assert_eq!(results[0].1.text, ": string");
2098
2099            // Build lookup should work
2100            let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 11);
2101            assert!(lookup.contains_key(&5));
2102            assert_eq!(lookup[&5].len(), 1);
2103            assert_eq!(lookup[&5][0].text, ": string");
2104
2105            // Clean up
2106            state.virtual_texts.remove(&mut state.marker_list, vtext_id);
2107            assert!(state.virtual_texts.is_empty());
2108        }
2109
2110        #[test]
2111        fn test_virtual_text_position_tracking_on_insert() {
2112            let mut state = EditorState::new(
2113                80,
2114                24,
2115                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2116                test_fs(),
2117            );
2118            state.buffer = Buffer::from_str_test("hello world");
2119
2120            // Initialize marker list for buffer
2121            if !state.buffer.is_empty() {
2122                state.marker_list.adjust_for_insert(0, state.buffer.len());
2123            }
2124
2125            // Add virtual text at position 6 (the 'w' in 'world')
2126            let _vtext_id = state.virtual_texts.add(
2127                &mut state.marker_list,
2128                6,
2129                "/*param*/".to_string(),
2130                Style::default(),
2131                VirtualTextPosition::BeforeChar,
2132                0,
2133            );
2134
2135            // Insert "beautiful " at position 6 using Event
2136            let mut cursors = Cursors::new();
2137            let cursor_id = cursors.primary_id();
2138            state.apply(
2139                &mut cursors,
2140                &Event::Insert {
2141                    position: 6,
2142                    text: "beautiful ".to_string(),
2143                    cursor_id,
2144                },
2145            );
2146
2147            // Virtual text should now be at position 16 (6 + 10)
2148            let results = state.virtual_texts.query_range(&state.marker_list, 0, 30);
2149            assert_eq!(results.len(), 1);
2150            assert_eq!(results[0].0, 16); // Position should have moved
2151            assert_eq!(results[0].1.text, "/*param*/");
2152        }
2153
2154        #[test]
2155        fn test_virtual_text_position_tracking_on_delete() {
2156            let mut state = EditorState::new(
2157                80,
2158                24,
2159                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2160                test_fs(),
2161            );
2162            state.buffer = Buffer::from_str_test("hello beautiful world");
2163
2164            // Initialize marker list for buffer
2165            if !state.buffer.is_empty() {
2166                state.marker_list.adjust_for_insert(0, state.buffer.len());
2167            }
2168
2169            // Add virtual text at position 16 (the 'w' in 'world')
2170            let _vtext_id = state.virtual_texts.add(
2171                &mut state.marker_list,
2172                16,
2173                ": string".to_string(),
2174                Style::default(),
2175                VirtualTextPosition::AfterChar,
2176                0,
2177            );
2178
2179            // Delete "beautiful " (positions 6-16) using Event
2180            let mut cursors = Cursors::new();
2181            let cursor_id = cursors.primary_id();
2182            state.apply(
2183                &mut cursors,
2184                &Event::Delete {
2185                    range: 6..16,
2186                    deleted_text: "beautiful ".to_string(),
2187                    cursor_id,
2188                },
2189            );
2190
2191            // Virtual text should now be at position 6
2192            let results = state.virtual_texts.query_range(&state.marker_list, 0, 20);
2193            assert_eq!(results.len(), 1);
2194            assert_eq!(results[0].0, 6); // Position should have moved back
2195            assert_eq!(results[0].1.text, ": string");
2196        }
2197
2198        #[test]
2199        fn test_multiple_virtual_texts_with_priorities() {
2200            let mut state = EditorState::new(
2201                80,
2202                24,
2203                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2204                test_fs(),
2205            );
2206            state.buffer = Buffer::from_str_test("let x = 5");
2207
2208            // Initialize marker list for buffer
2209            if !state.buffer.is_empty() {
2210                state.marker_list.adjust_for_insert(0, state.buffer.len());
2211            }
2212
2213            // Add type hint after 'x' (position 5)
2214            state.virtual_texts.add(
2215                &mut state.marker_list,
2216                5,
2217                ": i32".to_string(),
2218                Style::default(),
2219                VirtualTextPosition::AfterChar,
2220                0, // Lower priority - renders first
2221            );
2222
2223            // Add another hint at same position with higher priority
2224            state.virtual_texts.add(
2225                &mut state.marker_list,
2226                5,
2227                " /* inferred */".to_string(),
2228                Style::default(),
2229                VirtualTextPosition::AfterChar,
2230                10, // Higher priority - renders second
2231            );
2232
2233            // Build lookup - should have both, sorted by priority (lower first)
2234            let lookup = state.virtual_texts.build_lookup(&state.marker_list, 0, 10);
2235            assert!(lookup.contains_key(&5));
2236            let vtexts = &lookup[&5];
2237            assert_eq!(vtexts.len(), 2);
2238            // Lower priority first (like layer ordering)
2239            assert_eq!(vtexts[0].text, ": i32");
2240            assert_eq!(vtexts[1].text, " /* inferred */");
2241        }
2242
2243        #[test]
2244        fn test_virtual_text_clear() {
2245            let mut state = EditorState::new(
2246                80,
2247                24,
2248                crate::config::LARGE_FILE_THRESHOLD_BYTES as usize,
2249                test_fs(),
2250            );
2251            state.buffer = Buffer::from_str_test("test");
2252
2253            // Initialize marker list for buffer
2254            if !state.buffer.is_empty() {
2255                state.marker_list.adjust_for_insert(0, state.buffer.len());
2256            }
2257
2258            // Add multiple virtual texts
2259            state.virtual_texts.add(
2260                &mut state.marker_list,
2261                0,
2262                "hint1".to_string(),
2263                Style::default(),
2264                VirtualTextPosition::BeforeChar,
2265                0,
2266            );
2267            state.virtual_texts.add(
2268                &mut state.marker_list,
2269                2,
2270                "hint2".to_string(),
2271                Style::default(),
2272                VirtualTextPosition::AfterChar,
2273                0,
2274            );
2275
2276            assert_eq!(state.virtual_texts.len(), 2);
2277
2278            // Clear all
2279            state.virtual_texts.clear(&mut state.marker_list);
2280            assert!(state.virtual_texts.is_empty());
2281
2282            // Query should return nothing
2283            let results = state.virtual_texts.query_range(&state.marker_list, 0, 10);
2284            assert!(results.is_empty());
2285        }
2286    }
2287}