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