Skip to main content

fresh/
state.rs

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