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