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