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