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