Skip to main content

fresh/view/
overlay.rs

1use crate::model::marker::{MarkerId, MarkerList};
2use ratatui::style::{Color, Style};
3use std::collections::HashMap;
4use std::ops::Range;
5
6// Re-export types from fresh-core for shared type usage
7pub use fresh_core::overlay::{OverlayHandle, OverlayNamespace};
8
9/// Overlay face - defines the visual appearance of an overlay
10#[derive(Debug, Clone, PartialEq)]
11pub enum OverlayFace {
12    /// Underline with a specific style
13    Underline { color: Color, style: UnderlineStyle },
14    /// Background color
15    Background { color: Color },
16    /// Foreground (text) color
17    Foreground { color: Color },
18    /// Combined style with multiple attributes (fully resolved colors)
19    Style { style: Style },
20    /// Style with theme key references - resolved at render time
21    ///
22    /// Theme keys like "ui.status_bar_fg" or "editor.selection_bg"
23    /// are resolved when rendering, so overlays automatically update
24    /// when the theme changes.
25    ThemedStyle {
26        /// Fallback style with RGB colors (used if theme keys don't resolve)
27        fallback_style: Style,
28        /// Theme key for foreground color (e.g., "ui.status_bar_fg")
29        fg_theme: Option<String>,
30        /// Theme key for background color (e.g., "editor.selection_bg")
31        bg_theme: Option<String>,
32        /// When `true`, apply `fg` only on cells whose existing fg
33        /// equals the resolved bg (a same-colour collision).
34        fg_on_collision_only: bool,
35    },
36}
37
38impl OverlayFace {
39    /// Create an OverlayFace from OverlayOptions
40    ///
41    /// If the options contain theme key references, creates a ThemedStyle
42    /// for runtime resolution. Otherwise creates a fully resolved Style.
43    pub fn from_options(options: &fresh_core::api::OverlayOptions) -> Self {
44        use crate::view::theme::named_color_from_str;
45        use ratatui::style::Modifier;
46
47        let mut style = Style::default();
48
49        if let Some(ref fg) = options.fg {
50            if let Some((r, g, b)) = fg.as_rgb() {
51                style = style.fg(Color::Rgb(r, g, b));
52            } else if let Some(key) = fg.as_theme_key() {
53                if let Some(color) = named_color_from_str(key) {
54                    style = style.fg(color);
55                }
56            }
57        }
58
59        if let Some(ref bg) = options.bg {
60            if let Some((r, g, b)) = bg.as_rgb() {
61                style = style.bg(Color::Rgb(r, g, b));
62            } else if let Some(key) = bg.as_theme_key() {
63                if let Some(color) = named_color_from_str(key) {
64                    style = style.bg(color);
65                }
66            }
67        }
68
69        let mut modifiers = Modifier::empty();
70        if options.bold {
71            modifiers |= Modifier::BOLD;
72        }
73        if options.italic {
74            modifiers |= Modifier::ITALIC;
75        }
76        if options.underline {
77            modifiers |= Modifier::UNDERLINED;
78        }
79        if options.strikethrough {
80            modifiers |= Modifier::CROSSED_OUT;
81        }
82        if !modifiers.is_empty() {
83            style = style.add_modifier(modifiers);
84        }
85
86        // Only treat as theme keys if they're NOT recognized named colors
87        // (named colors were already resolved to concrete Color values above)
88        let fg_theme = options
89            .fg
90            .as_ref()
91            .and_then(|c| c.as_theme_key())
92            .filter(|key| named_color_from_str(key).is_none())
93            .map(String::from);
94        let bg_theme = options
95            .bg
96            .as_ref()
97            .and_then(|c| c.as_theme_key())
98            .filter(|key| named_color_from_str(key).is_none())
99            .map(String::from);
100
101        if fg_theme.is_some() || bg_theme.is_some() {
102            OverlayFace::ThemedStyle {
103                fallback_style: style,
104                fg_theme,
105                bg_theme,
106                fg_on_collision_only: options.fg_on_collision_only,
107            }
108        } else {
109            OverlayFace::Style { style }
110        }
111    }
112}
113
114/// Style of underline
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum UnderlineStyle {
117    /// Straight line
118    Straight,
119    /// Wavy/squiggly line (for errors)
120    Wavy,
121    /// Dotted line
122    Dotted,
123    /// Dashed line
124    Dashed,
125}
126
127/// Priority for overlay z-ordering
128/// Higher priority overlays are rendered on top of lower priority ones
129pub type Priority = i32;
130
131/// An overlay represents a visual decoration over a range of text
132/// Uses markers for content-anchored positions that automatically adjust with edits
133#[derive(Debug, Clone)]
134pub struct Overlay {
135    /// Unique handle for this overlay (opaque, for removal by handle)
136    pub handle: OverlayHandle,
137
138    /// Namespace this overlay belongs to (for bulk removal)
139    pub namespace: Option<OverlayNamespace>,
140
141    /// Start marker (left affinity - stays before inserted text)
142    pub start_marker: MarkerId,
143
144    /// End marker (right affinity - moves after inserted text)
145    pub end_marker: MarkerId,
146
147    /// Visual appearance of the overlay
148    pub face: OverlayFace,
149
150    /// Priority for z-ordering (higher = on top)
151    pub priority: Priority,
152
153    /// Optional tooltip/message to show when hovering over this overlay
154    pub message: Option<String>,
155
156    /// Whether to extend the overlay's background to the end of the visual line
157    /// Used for full-width line highlighting (e.g., in diff views)
158    pub extend_to_line_end: bool,
159
160    /// Optional URL for OSC 8 terminal hyperlinks.
161    /// When set, the rendered text in this overlay becomes a clickable hyperlink.
162    pub url: Option<String>,
163
164    /// Theme key that produced this overlay's primary color (e.g. "diagnostic.warning_bg").
165    /// Recorded at creation time so the theme inspector can show the exact key
166    /// without reverse-mapping colors.
167    pub theme_key: Option<&'static str>,
168}
169
170impl Overlay {
171    /// Create a new overlay with markers at the given range
172    ///
173    /// # Arguments
174    /// * `marker_list` - MarkerList to create markers in
175    /// * `range` - Byte range for the overlay
176    /// * `face` - Visual appearance
177    ///
178    /// Returns the overlay (which contains its handle for later removal)
179    pub fn new(marker_list: &mut MarkerList, range: Range<usize>, face: OverlayFace) -> Self {
180        let start_marker = marker_list.create(range.start, true); // left affinity
181        let end_marker = marker_list.create(range.end, false); // right affinity
182
183        Self {
184            handle: OverlayHandle::new(),
185            namespace: None,
186            start_marker,
187            end_marker,
188            face,
189            priority: 0,
190            message: None,
191            extend_to_line_end: false,
192            url: None,
193            theme_key: None,
194        }
195    }
196
197    /// Create an overlay with a namespace (for bulk removal)
198    pub fn with_namespace(
199        marker_list: &mut MarkerList,
200        range: Range<usize>,
201        face: OverlayFace,
202        namespace: OverlayNamespace,
203    ) -> Self {
204        let mut overlay = Self::new(marker_list, range, face);
205        overlay.namespace = Some(namespace);
206        overlay
207    }
208
209    /// Like [`with_namespace`], but the end marker uses left gravity so the
210    /// overlay does not grow when text is inserted immediately after it.
211    ///
212    /// Used for search-match highlights, which must stay anchored to the matched
213    /// text and not absorb adjacent typing (issue #2053).
214    ///
215    /// [`with_namespace`]: Overlay::with_namespace
216    pub fn with_namespace_fixed_end(
217        marker_list: &mut MarkerList,
218        range: Range<usize>,
219        face: OverlayFace,
220        namespace: OverlayNamespace,
221    ) -> Self {
222        let start_marker = marker_list.create(range.start, true); // left affinity
223        let end_marker = marker_list.create_left_gravity(range.end);
224
225        Self {
226            handle: OverlayHandle::new(),
227            namespace: Some(namespace),
228            start_marker,
229            end_marker,
230            face,
231            priority: 0,
232            message: None,
233            extend_to_line_end: false,
234            url: None,
235            theme_key: None,
236        }
237    }
238
239    /// Create an overlay with a specific priority
240    pub fn with_priority(
241        marker_list: &mut MarkerList,
242        range: Range<usize>,
243        face: OverlayFace,
244        priority: Priority,
245    ) -> Self {
246        let mut overlay = Self::new(marker_list, range, face);
247        overlay.priority = priority;
248        overlay
249    }
250
251    /// Add a message/tooltip to this overlay
252    pub fn with_message(mut self, message: String) -> Self {
253        self.message = Some(message);
254        self
255    }
256
257    /// Set the priority
258    pub fn with_priority_value(mut self, priority: Priority) -> Self {
259        self.priority = priority;
260        self
261    }
262
263    /// Set the namespace
264    pub fn with_namespace_value(mut self, namespace: OverlayNamespace) -> Self {
265        self.namespace = Some(namespace);
266        self
267    }
268
269    /// Set whether to extend the overlay to the end of the visual line
270    pub fn with_extend_to_line_end(mut self, extend: bool) -> Self {
271        self.extend_to_line_end = extend;
272        self
273    }
274
275    /// Set the theme key that produced this overlay's color
276    pub fn with_theme_key(mut self, key: &'static str) -> Self {
277        self.theme_key = Some(key);
278        self
279    }
280
281    /// Get the current byte range by resolving markers
282    /// This is called once per frame during rendering setup
283    pub fn range(&self, marker_list: &MarkerList) -> Range<usize> {
284        let start = marker_list.get_position(self.start_marker).unwrap_or(0);
285        let end = marker_list.get_position(self.end_marker).unwrap_or(0);
286        start..end
287    }
288
289    /// Check if this overlay contains a position
290    pub fn contains(&self, position: usize, marker_list: &MarkerList) -> bool {
291        self.range(marker_list).contains(&position)
292    }
293
294    /// Check if this overlay overlaps with a range
295    pub fn overlaps(&self, range: &Range<usize>, marker_list: &MarkerList) -> bool {
296        let self_range = self.range(marker_list);
297        self_range.start < range.end && range.start < self_range.end
298    }
299}
300
301/// Manages overlays for a buffer
302/// Overlays are sorted by priority for efficient rendering
303#[derive(Debug, Clone)]
304pub struct OverlayManager {
305    /// All active overlays, indexed for O(1) lookup by handle
306    overlays: Vec<Overlay>,
307    /// `MarkerId -> index into overlays` for O(log N + k) `remove_in_range`.
308    /// Both endpoints of each overlay are registered. Kept in sync with
309    /// every push / swap_remove on `overlays`, and rebuilt after any sort.
310    marker_to_idx: HashMap<MarkerId, usize>,
311}
312
313impl OverlayManager {
314    /// Create a new empty overlay manager
315    pub fn new() -> Self {
316        Self {
317            overlays: Vec::new(),
318            marker_to_idx: HashMap::new(),
319        }
320    }
321
322    /// Add an overlay and return its handle for later removal
323    pub fn add(&mut self, overlay: Overlay) -> OverlayHandle {
324        let handle = overlay.handle.clone();
325        // Binary-search the priority-ordered insertion point and shift in
326        // place. Avoids the O(n²·log n) sort-on-every-add the prior impl
327        // had — the docstring on `extend` warned about this.
328        let priority = overlay.priority;
329        let pos = self.overlays.partition_point(|o| o.priority <= priority);
330        self.overlays.insert(pos, overlay);
331        // Every entry from `pos` onward shifted by one — re-index that tail.
332        // Tail length is small when adds are append-shaped (the common case
333        // for plugins that emit per-line clear+rebuild).
334        for (i, o) in self.overlays.iter().enumerate().skip(pos) {
335            self.marker_to_idx.insert(o.start_marker, i);
336            self.marker_to_idx.insert(o.end_marker, i);
337        }
338        handle
339    }
340
341    /// Append many overlays at once, sorting a single time at the end.
342    ///
343    /// `add` re-sorts the whole vector on every insertion, which is O(n² log n)
344    /// when a caller has N overlays to add. Use this instead when rebuilding an
345    /// overlay set from scratch (e.g. `set_virtual_buffer_content`), where the
346    /// caller already owns the full list up front.
347    pub fn extend<I: IntoIterator<Item = Overlay>>(&mut self, overlays: I) {
348        self.overlays.extend(overlays);
349        self.overlays.sort_by_key(|o| o.priority);
350        self.rebuild_marker_index();
351    }
352
353    /// Remove an overlay by its handle
354    pub fn remove_by_handle(
355        &mut self,
356        handle: &OverlayHandle,
357        marker_list: &mut MarkerList,
358    ) -> bool {
359        if let Some(pos) = self.overlays.iter().position(|o| &o.handle == handle) {
360            let overlay = self.overlays.remove(pos);
361            self.marker_to_idx.remove(&overlay.start_marker);
362            self.marker_to_idx.remove(&overlay.end_marker);
363            // Vec::remove shifts every subsequent entry down by one — repair.
364            for (i, o) in self.overlays.iter().enumerate().skip(pos) {
365                self.marker_to_idx.insert(o.start_marker, i);
366                self.marker_to_idx.insert(o.end_marker, i);
367            }
368            marker_list.delete(overlay.start_marker);
369            marker_list.delete(overlay.end_marker);
370            true
371        } else {
372            false
373        }
374    }
375
376    /// Remove all overlays in a namespace
377    pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
378        let mut indices: Vec<usize> = self
379            .overlays
380            .iter()
381            .enumerate()
382            .filter_map(|(i, o)| (o.namespace.as_ref() == Some(namespace)).then_some(i))
383            .collect();
384        if indices.is_empty() {
385            return;
386        }
387        indices.sort_unstable_by(|a, b| b.cmp(a));
388        for idx in indices {
389            self.swap_remove_at(idx, marker_list);
390        }
391        // Restore priority order after swap_removes.
392        self.overlays.sort_by_key(|o| o.priority);
393        self.rebuild_marker_index();
394    }
395
396    /// Replace overlays in a namespace that overlap a range with new overlays.
397    ///
398    /// This preserves overlays outside the range, which helps avoid flicker and
399    /// unnecessary marker churn during viewport-only updates.
400    pub fn replace_range_in_namespace(
401        &mut self,
402        namespace: &OverlayNamespace,
403        range: &Range<usize>,
404        mut new_overlays: Vec<Overlay>,
405        marker_list: &mut MarkerList,
406    ) {
407        // Find overlays in this namespace that overlap the range. Use the
408        // marker-tree to narrow candidates; verify each candidate's true
409        // range and namespace before removing.
410        if range.start < range.end {
411            let hits = marker_list.query_range(range.start, range.end);
412            let mut candidates: Vec<usize> = hits
413                .iter()
414                .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
415                .collect();
416            candidates.sort_unstable();
417            candidates.dedup();
418            let mut to_remove: Vec<usize> = candidates
419                .into_iter()
420                .filter(|&idx| {
421                    let o = &self.overlays[idx];
422                    if o.namespace.as_ref() != Some(namespace) {
423                        return false;
424                    }
425                    let start = marker_list.get_position(o.start_marker).unwrap_or(0);
426                    let end = marker_list.get_position(o.end_marker).unwrap_or(0);
427                    start < range.end && range.start < end
428                })
429                .collect();
430            to_remove.sort_unstable_by(|a, b| b.cmp(a));
431            for idx in to_remove {
432                self.swap_remove_at(idx, marker_list);
433            }
434        }
435
436        if !new_overlays.is_empty() {
437            self.overlays.append(&mut new_overlays);
438        }
439        self.overlays.sort_by_key(|o| o.priority);
440        self.rebuild_marker_index();
441    }
442
443    /// Remove all overlays in a range and clean up their markers
444    pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
445        // O(log N + k) for the lookup; restoring the priority-sorted
446        // invariant after `swap_remove` is O(N) (adaptive sort on a
447        // near-sorted vec) plus O(N) marker_to_idx rebuild. For typical
448        // markdown_compose workloads where overlays in a buffer share
449        // the same priority, the adaptive sort is a no-op pass.
450        // Spanning overlays (start < range.start && end > range.end) are
451        // not detected — same precondition as ConcealManager.
452        if range.start >= range.end {
453            return;
454        }
455        let hits = marker_list.query_range(range.start, range.end);
456        if hits.is_empty() {
457            return;
458        }
459        let mut candidates: Vec<usize> = hits
460            .iter()
461            .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
462            .collect();
463        candidates.sort_unstable();
464        candidates.dedup();
465
466        let mut to_remove: Vec<usize> = candidates
467            .into_iter()
468            .filter(|&idx| {
469                let o = &self.overlays[idx];
470                let start = marker_list.get_position(o.start_marker).unwrap_or(0);
471                let end = marker_list.get_position(o.end_marker).unwrap_or(0);
472                start < range.end && range.start < end
473            })
474            .collect();
475        if to_remove.is_empty() {
476            return;
477        }
478        to_remove.sort_unstable_by(|a, b| b.cmp(a));
479        for idx in to_remove {
480            self.swap_remove_at(idx, marker_list);
481        }
482        // Restore priority order broken by swap_removes.
483        self.overlays.sort_by_key(|o| o.priority);
484        self.rebuild_marker_index();
485    }
486
487    /// Like [`remove_in_range`], but only removes overlays belonging to
488    /// `namespace`. Overlays in other namespaces (e.g. editor-owned LSP
489    /// diagnostics) that happen to overlap the range are left intact.
490    ///
491    /// [`remove_in_range`]: Self::remove_in_range
492    pub fn remove_in_range_for_namespace(
493        &mut self,
494        range: &Range<usize>,
495        namespace: &OverlayNamespace,
496        marker_list: &mut MarkerList,
497    ) {
498        if range.start >= range.end {
499            return;
500        }
501        let hits = marker_list.query_range(range.start, range.end);
502        if hits.is_empty() {
503            return;
504        }
505        let mut candidates: Vec<usize> = hits
506            .iter()
507            .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
508            .collect();
509        candidates.sort_unstable();
510        candidates.dedup();
511
512        let mut to_remove: Vec<usize> = candidates
513            .into_iter()
514            .filter(|&idx| {
515                let o = &self.overlays[idx];
516                if o.namespace.as_ref() != Some(namespace) {
517                    return false;
518                }
519                let start = marker_list.get_position(o.start_marker).unwrap_or(0);
520                let end = marker_list.get_position(o.end_marker).unwrap_or(0);
521                start < range.end && range.start < end
522            })
523            .collect();
524        if to_remove.is_empty() {
525            return;
526        }
527        to_remove.sort_unstable_by(|a, b| b.cmp(a));
528        for idx in to_remove {
529            self.swap_remove_at(idx, marker_list);
530        }
531        // Restore priority order broken by swap_removes.
532        self.overlays.sort_by_key(|o| o.priority);
533        self.rebuild_marker_index();
534    }
535
536    /// Clear all overlays and their markers
537    pub fn clear(&mut self, marker_list: &mut MarkerList) {
538        for overlay in &self.overlays {
539            marker_list.delete(overlay.start_marker);
540            marker_list.delete(overlay.end_marker);
541        }
542        self.overlays.clear();
543        self.marker_to_idx.clear();
544    }
545
546    /// Swap-remove the entry at `idx`, deleting its markers and patching
547    /// `marker_to_idx` for whatever entry got swapped in. Caller is
548    /// responsible for restoring sort order if needed.
549    fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
550        let removed = self.overlays.swap_remove(idx);
551        self.marker_to_idx.remove(&removed.start_marker);
552        self.marker_to_idx.remove(&removed.end_marker);
553        marker_list.delete(removed.start_marker);
554        marker_list.delete(removed.end_marker);
555        if let Some(moved) = self.overlays.get(idx) {
556            self.marker_to_idx.insert(moved.start_marker, idx);
557            self.marker_to_idx.insert(moved.end_marker, idx);
558        }
559    }
560
561    /// Rebuild `marker_to_idx` from the current `overlays` order.
562    /// Called after sorts that scramble indices.
563    fn rebuild_marker_index(&mut self) {
564        self.marker_to_idx.clear();
565        for (i, o) in self.overlays.iter().enumerate() {
566            self.marker_to_idx.insert(o.start_marker, i);
567            self.marker_to_idx.insert(o.end_marker, i);
568        }
569    }
570
571    /// Get all overlays at a specific position, sorted by priority
572    pub fn at_position(&self, position: usize, marker_list: &MarkerList) -> Vec<&Overlay> {
573        self.overlays
574            .iter()
575            .filter(|o| {
576                let range = o.range(marker_list);
577                range.contains(&position)
578            })
579            .collect()
580    }
581
582    /// Get all overlays that overlap with a range, sorted by priority
583    pub fn in_range(&self, range: &Range<usize>, marker_list: &MarkerList) -> Vec<&Overlay> {
584        self.overlays
585            .iter()
586            .filter(|o| o.overlaps(range, marker_list))
587            .collect()
588    }
589
590    /// Query overlays in a viewport range efficiently using the marker interval tree
591    ///
592    /// This is much faster than calling `at_position()` for every character in the range.
593    /// Returns overlays with their resolved byte ranges.
594    ///
595    /// # Performance
596    /// - Old approach: O(N * M) where N = positions to check, M = overlay count
597    /// - This approach: O(log M + k) where k = overlays in viewport (typically 2-10)
598    pub fn query_viewport(
599        &self,
600        start: usize,
601        end: usize,
602        marker_list: &MarkerList,
603    ) -> Vec<(&Overlay, Range<usize>)> {
604        use std::collections::HashMap;
605
606        // Query the marker interval tree once for all markers in viewport
607        // This is O(log N + k) where k = markers in viewport
608        let visible_markers = marker_list.query_range(start, end);
609
610        // Build a quick lookup map: marker_id -> position
611        let marker_positions: HashMap<_, _> = visible_markers
612            .into_iter()
613            .map(|(id, start, _end)| (id, start))
614            .collect();
615
616        // Find overlays whose markers overlap with the viewport.
617        // At least one marker must be in the viewport, but the other may be
618        // outside (e.g. a multi-line overlay partially scrolled out of view).
619        // For the out-of-viewport marker, fall back to resolving its position
620        // directly from the marker list.
621        self.overlays
622            .iter()
623            .filter_map(|overlay| {
624                let start_in_vp = marker_positions.get(&overlay.start_marker).copied();
625                let end_in_vp = marker_positions.get(&overlay.end_marker).copied();
626
627                // At least one marker must be in the viewport for the overlay
628                // to be visible at all
629                if start_in_vp.is_none() && end_in_vp.is_none() {
630                    return None;
631                }
632
633                // For the marker outside the viewport, resolve its position directly
634                let start_pos =
635                    start_in_vp.or_else(|| marker_list.get_position(overlay.start_marker))?;
636                let end_pos = end_in_vp.or_else(|| marker_list.get_position(overlay.end_marker))?;
637
638                let range = start_pos..end_pos;
639
640                // Only include if actually overlaps viewport.
641                // For zero-width ranges (e.g. diagnostics at a single position),
642                // check that the point is within [start, end] (inclusive).
643                // For non-zero ranges, check standard overlap: start < end && end > start.
644                let included = if range.start == range.end {
645                    range.start >= start && range.start <= end
646                } else {
647                    range.start < end && range.end > start
648                };
649
650                if included {
651                    Some((overlay, range))
652                } else {
653                    None
654                }
655            })
656            .collect()
657    }
658
659    /// Get overlay by handle
660    pub fn get_by_handle(&self, handle: &OverlayHandle) -> Option<&Overlay> {
661        self.overlays.iter().find(|o| &o.handle == handle)
662    }
663
664    /// Get mutable overlay by handle
665    pub fn get_by_handle_mut(&mut self, handle: &OverlayHandle) -> Option<&mut Overlay> {
666        self.overlays.iter_mut().find(|o| &o.handle == handle)
667    }
668
669    /// Get total number of overlays
670    pub fn len(&self) -> usize {
671        self.overlays.len()
672    }
673
674    /// Check if there are any overlays
675    pub fn is_empty(&self) -> bool {
676        self.overlays.is_empty()
677    }
678
679    /// Get all overlays (for rendering)
680    pub fn all(&self) -> &[Overlay] {
681        &self.overlays
682    }
683
684    /// Test-only: assert `marker_to_idx` is consistent with `overlays`,
685    /// and that priorities are non-decreasing along the vector.
686    /// Panics on any divergence. Used by property tests.
687    #[cfg(test)]
688    fn check_invariants(&self) {
689        assert_eq!(
690            self.marker_to_idx.len(),
691            self.overlays.len() * 2,
692            "marker_to_idx size != 2 * overlays.len()"
693        );
694        for (i, o) in self.overlays.iter().enumerate() {
695            assert_eq!(
696                self.marker_to_idx.get(&o.start_marker).copied(),
697                Some(i),
698                "start_marker {:?} of overlay {} mismapped",
699                o.start_marker,
700                i,
701            );
702            assert_eq!(
703                self.marker_to_idx.get(&o.end_marker).copied(),
704                Some(i),
705                "end_marker {:?} of overlay {} mismapped",
706                o.end_marker,
707                i,
708            );
709        }
710        // Priority order — only enforceable when nothing is mid-cycle.
711        // Tests check this via `assert_priority_sorted` after points
712        // where the invariant is supposed to hold (e.g. after `add`).
713    }
714
715    /// Test-only: assert overlays are non-decreasing by priority.
716    #[cfg(test)]
717    fn assert_priority_sorted(&self) {
718        for w in self.overlays.windows(2) {
719            assert!(
720                w[0].priority <= w[1].priority,
721                "priority order broken: {} after {}",
722                w[1].priority,
723                w[0].priority,
724            );
725        }
726    }
727}
728
729impl Default for OverlayManager {
730    fn default() -> Self {
731        Self::new()
732    }
733}
734
735/// Helper functions for creating common overlay types
736impl Overlay {
737    /// Create an error underline overlay (wavy red line)
738    pub fn error(
739        marker_list: &mut MarkerList,
740        range: Range<usize>,
741        message: Option<String>,
742    ) -> Self {
743        let mut overlay = Self::with_priority(
744            marker_list,
745            range,
746            OverlayFace::Underline {
747                color: Color::Red,
748                style: UnderlineStyle::Wavy,
749            },
750            10, // Higher priority for errors
751        );
752        overlay.message = message;
753        overlay
754    }
755
756    /// Create a warning underline overlay (wavy yellow line)
757    pub fn warning(
758        marker_list: &mut MarkerList,
759        range: Range<usize>,
760        message: Option<String>,
761    ) -> Self {
762        let mut overlay = Self::with_priority(
763            marker_list,
764            range,
765            OverlayFace::Underline {
766                color: Color::Yellow,
767                style: UnderlineStyle::Wavy,
768            },
769            5, // Medium priority for warnings
770        );
771        overlay.message = message;
772        overlay
773    }
774
775    /// Create an info underline overlay (wavy blue line)
776    pub fn info(
777        marker_list: &mut MarkerList,
778        range: Range<usize>,
779        message: Option<String>,
780    ) -> Self {
781        let mut overlay = Self::with_priority(
782            marker_list,
783            range,
784            OverlayFace::Underline {
785                color: Color::Blue,
786                style: UnderlineStyle::Wavy,
787            },
788            3, // Lower priority for info
789        );
790        overlay.message = message;
791        overlay
792    }
793
794    /// Create a hint underline overlay (dotted gray line)
795    pub fn hint(
796        marker_list: &mut MarkerList,
797        range: Range<usize>,
798        message: Option<String>,
799    ) -> Self {
800        let mut overlay = Self::with_priority(
801            marker_list,
802            range,
803            OverlayFace::Underline {
804                color: Color::Gray,
805                style: UnderlineStyle::Dotted,
806            },
807            1, // Lowest priority for hints
808        );
809        overlay.message = message;
810        overlay
811    }
812
813    /// Create a selection highlight overlay
814    pub fn selection(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
815        let mut overlay = Self::with_priority(
816            marker_list,
817            range,
818            OverlayFace::Background {
819                color: Color::Rgb(38, 79, 120), // VSCode-like selection color
820            },
821            -10, // Very low priority so it's under other overlays
822        );
823        overlay.theme_key = Some("editor.selection_bg");
824        overlay
825    }
826
827    /// Create a search result highlight overlay
828    pub fn search_match(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
829        let mut overlay = Self::with_priority(
830            marker_list,
831            range,
832            OverlayFace::Background {
833                color: Color::Rgb(72, 72, 0), // Yellow-ish highlight
834            },
835            -5, // Low priority
836        );
837        overlay.theme_key = Some("search.match_bg");
838        overlay
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    #[test]
847    fn test_overlay_creation_with_markers() {
848        let mut marker_list = MarkerList::new();
849        marker_list.set_buffer_size(100);
850
851        let overlay = Overlay::new(
852            &mut marker_list,
853            5..10,
854            OverlayFace::Background { color: Color::Red },
855        );
856
857        assert_eq!(marker_list.get_position(overlay.start_marker), Some(5));
858        assert_eq!(marker_list.get_position(overlay.end_marker), Some(10));
859        assert_eq!(overlay.range(&marker_list), 5..10);
860    }
861
862    #[test]
863    fn test_overlay_adjusts_with_insert() {
864        let mut marker_list = MarkerList::new();
865        marker_list.set_buffer_size(100);
866
867        let overlay = Overlay::new(
868            &mut marker_list,
869            10..20,
870            OverlayFace::Background { color: Color::Red },
871        );
872
873        // Insert before overlay
874        marker_list.adjust_for_insert(5, 10);
875
876        // Overlay should have moved forward
877        assert_eq!(overlay.range(&marker_list), 20..30);
878    }
879
880    #[test]
881    fn test_overlay_adjusts_with_delete() {
882        let mut marker_list = MarkerList::new();
883        marker_list.set_buffer_size(100);
884
885        let overlay = Overlay::new(
886            &mut marker_list,
887            20..30,
888            OverlayFace::Background { color: Color::Red },
889        );
890
891        // Delete before overlay
892        marker_list.adjust_for_delete(5, 10);
893
894        // Overlay should have moved backward
895        assert_eq!(overlay.range(&marker_list), 10..20);
896    }
897
898    #[test]
899    fn test_overlay_manager_add_remove() {
900        let mut marker_list = MarkerList::new();
901        marker_list.set_buffer_size(100);
902        let mut manager = OverlayManager::new();
903
904        let overlay = Overlay::new(
905            &mut marker_list,
906            5..10,
907            OverlayFace::Background { color: Color::Red },
908        );
909
910        let handle = manager.add(overlay);
911        assert_eq!(manager.len(), 1);
912
913        manager.remove_by_handle(&handle, &mut marker_list);
914        assert_eq!(manager.len(), 0);
915    }
916
917    #[test]
918    fn test_overlay_namespace_clear() {
919        let mut marker_list = MarkerList::new();
920        marker_list.set_buffer_size(100);
921        let mut manager = OverlayManager::new();
922
923        let ns = OverlayNamespace::from_string("todo".to_string());
924
925        // Add overlays in namespace
926        let overlay1 = Overlay::with_namespace(
927            &mut marker_list,
928            5..10,
929            OverlayFace::Background { color: Color::Red },
930            ns.clone(),
931        );
932        let overlay2 = Overlay::with_namespace(
933            &mut marker_list,
934            15..20,
935            OverlayFace::Background { color: Color::Blue },
936            ns.clone(),
937        );
938        // Add overlay without namespace
939        let overlay3 = Overlay::new(
940            &mut marker_list,
941            25..30,
942            OverlayFace::Background {
943                color: Color::Green,
944            },
945        );
946
947        manager.add(overlay1);
948        manager.add(overlay2);
949        manager.add(overlay3);
950        assert_eq!(manager.len(), 3);
951
952        // Clear only the namespace
953        manager.clear_namespace(&ns, &mut marker_list);
954        assert_eq!(manager.len(), 1); // Only overlay3 remains
955    }
956
957    #[test]
958    fn test_overlay_priority_sorting() {
959        let mut marker_list = MarkerList::new();
960        marker_list.set_buffer_size(100);
961        let mut manager = OverlayManager::new();
962
963        manager.add(Overlay::with_priority(
964            &mut marker_list,
965            5..10,
966            OverlayFace::Background { color: Color::Red },
967            10,
968        ));
969        manager.add(Overlay::with_priority(
970            &mut marker_list,
971            5..10,
972            OverlayFace::Background { color: Color::Blue },
973            5,
974        ));
975        manager.add(Overlay::with_priority(
976            &mut marker_list,
977            5..10,
978            OverlayFace::Background {
979                color: Color::Green,
980            },
981            15,
982        ));
983
984        let overlays = manager.at_position(7, &marker_list);
985        assert_eq!(overlays.len(), 3);
986        // Should be sorted by priority (low to high)
987        assert_eq!(overlays[0].priority, 5);
988        assert_eq!(overlays[1].priority, 10);
989        assert_eq!(overlays[2].priority, 15);
990    }
991
992    #[test]
993    fn test_overlay_contains_and_overlaps() {
994        let mut marker_list = MarkerList::new();
995        marker_list.set_buffer_size(100);
996
997        let overlay = Overlay::new(
998            &mut marker_list,
999            10..20,
1000            OverlayFace::Background { color: Color::Red },
1001        );
1002
1003        assert!(!overlay.contains(9, &marker_list));
1004        assert!(overlay.contains(10, &marker_list));
1005        assert!(overlay.contains(15, &marker_list));
1006        assert!(overlay.contains(19, &marker_list));
1007        assert!(!overlay.contains(20, &marker_list));
1008
1009        assert!(!overlay.overlaps(&(0..10), &marker_list));
1010        assert!(overlay.overlaps(&(5..15), &marker_list));
1011        assert!(overlay.overlaps(&(15..25), &marker_list));
1012        assert!(!overlay.overlaps(&(20..30), &marker_list));
1013    }
1014
1015    #[test]
1016    fn test_overlay_remove_in_range_keeps_only_disjoint() {
1017        let mut marker_list = MarkerList::new();
1018        marker_list.set_buffer_size(200);
1019        let mut manager = OverlayManager::new();
1020
1021        manager.add(Overlay::new(
1022            &mut marker_list,
1023            0..5,
1024            OverlayFace::Background { color: Color::Red },
1025        ));
1026        manager.add(Overlay::new(
1027            &mut marker_list,
1028            10..20,
1029            OverlayFace::Background { color: Color::Blue },
1030        ));
1031        manager.add(Overlay::new(
1032            &mut marker_list,
1033            30..40,
1034            OverlayFace::Background {
1035                color: Color::Green,
1036            },
1037        ));
1038        manager.add(Overlay::new(
1039            &mut marker_list,
1040            50..60,
1041            OverlayFace::Background {
1042                color: Color::Yellow,
1043            },
1044        ));
1045
1046        // Range 15..35 overlaps overlays #2 (10..20) and #3 (30..40), leaves #1 and #4.
1047        manager.remove_in_range(&(15..35), &mut marker_list);
1048
1049        let kept: Vec<_> = manager
1050            .all()
1051            .iter()
1052            .map(|o| o.range(&marker_list))
1053            .collect();
1054        assert_eq!(kept, vec![0..5, 50..60]);
1055    }
1056
1057    #[test]
1058    fn test_overlay_remove_in_range_deletes_markers() {
1059        let mut marker_list = MarkerList::new();
1060        marker_list.set_buffer_size(100);
1061        let mut manager = OverlayManager::new();
1062
1063        let overlay = Overlay::new(
1064            &mut marker_list,
1065            10..20,
1066            OverlayFace::Background { color: Color::Red },
1067        );
1068        let start_id = overlay.start_marker;
1069        let end_id = overlay.end_marker;
1070        manager.add(overlay);
1071
1072        manager.remove_in_range(&(0..50), &mut marker_list);
1073
1074        assert_eq!(manager.len(), 0);
1075        assert_eq!(marker_list.get_position(start_id), None);
1076        assert_eq!(marker_list.get_position(end_id), None);
1077    }
1078
1079    #[test]
1080    fn test_overlay_remove_in_range_endpoint_semantics() {
1081        // Touching at a single endpoint must NOT remove (start == range.end or end == range.start).
1082        let mut marker_list = MarkerList::new();
1083        marker_list.set_buffer_size(100);
1084        let mut manager = OverlayManager::new();
1085
1086        manager.add(Overlay::new(
1087            &mut marker_list,
1088            10..20,
1089            OverlayFace::Background { color: Color::Red },
1090        ));
1091
1092        manager.remove_in_range(&(20..30), &mut marker_list);
1093        assert_eq!(manager.len(), 1);
1094        manager.remove_in_range(&(0..10), &mut marker_list);
1095        assert_eq!(manager.len(), 1);
1096        manager.remove_in_range(&(19..21), &mut marker_list);
1097        assert_eq!(manager.len(), 0);
1098    }
1099
1100    /// Mirrors the production cycle: per line in `lines_changed`, clear
1101    /// overlays in the line's byte range, then re-add the line's overlays.
1102    /// Steady-state count holds throughout. Same shape as the matching
1103    /// conceal perf test for direct comparison.
1104    ///
1105    /// Run with:
1106    ///   cargo nextest run -p fresh-editor --no-capture \
1107    ///     view::overlay::tests::perf_full_buffer_rebuild_pass
1108    #[test]
1109    fn perf_full_buffer_rebuild_pass() {
1110        const LINES: usize = 500;
1111        const LINE_BYTES: usize = 50;
1112        const OVERLAYS_PER_LINE: usize = 5;
1113
1114        let mut marker_list = MarkerList::new();
1115        marker_list.set_buffer_size(LINES * LINE_BYTES);
1116        let mut manager = OverlayManager::new();
1117
1118        let overlay_byte = |line: usize, k: usize| -> usize {
1119            line * LINE_BYTES + k * (LINE_BYTES / OVERLAYS_PER_LINE)
1120        };
1121        let make_overlay = |ml: &mut MarkerList, line: usize, k: usize| {
1122            let s = overlay_byte(line, k);
1123            Overlay::new(
1124                ml,
1125                s..(s + 2),
1126                OverlayFace::Background { color: Color::Red },
1127            )
1128        };
1129
1130        // Populate steady state.
1131        for line in 0..LINES {
1132            for k in 0..OVERLAYS_PER_LINE {
1133                let o = make_overlay(&mut marker_list, line, k);
1134                manager.add(o);
1135            }
1136        }
1137        let initial = LINES * OVERLAYS_PER_LINE;
1138
1139        // One full-buffer `lines_changed` pass: per line, clear + re-add.
1140        let start = std::time::Instant::now();
1141        for line in 0..LINES {
1142            let line_range = (line * LINE_BYTES)..((line + 1) * LINE_BYTES);
1143            manager.remove_in_range(&line_range, &mut marker_list);
1144            for k in 0..OVERLAYS_PER_LINE {
1145                let o = make_overlay(&mut marker_list, line, k);
1146                manager.add(o);
1147            }
1148        }
1149        let elapsed = start.elapsed();
1150
1151        eprintln!(
1152            "[perf] overlay full-buffer rebuild ({LINES} lines, {} entries steady): \
1153             {:?} total, {:?}/line",
1154            initial,
1155            elapsed,
1156            elapsed / LINES as u32,
1157        );
1158        assert_eq!(manager.len(), initial);
1159    }
1160
1161    mod proptests {
1162        use super::*;
1163        use proptest::prelude::*;
1164
1165        #[derive(Debug, Clone)]
1166        enum Op {
1167            Add {
1168                start: usize,
1169                len: usize,
1170                priority: i32,
1171                ns_idx: u8,
1172            },
1173            RemoveInRange {
1174                start: usize,
1175                end: usize,
1176            },
1177            ClearNamespace {
1178                ns_idx: u8,
1179            },
1180            ReplaceRange {
1181                start: usize,
1182                end: usize,
1183                ns_idx: u8,
1184                /// New overlays to insert in the same range; same shape
1185                /// as `Add` but len capped to satisfy precondition.
1186                new_overlays: Vec<(usize, usize, i32)>,
1187            },
1188        }
1189
1190        const BUFFER_SIZE: usize = 200;
1191        const MAX_OVERLAY_LEN: usize = 4;
1192        const MIN_QUERY_LEN: usize = MAX_OVERLAY_LEN + 1;
1193
1194        fn arb_overlay_spec() -> impl Strategy<Value = (usize, usize, i32)> {
1195            (
1196                0..(BUFFER_SIZE - MAX_OVERLAY_LEN),
1197                1..=MAX_OVERLAY_LEN,
1198                -5i32..=5i32,
1199            )
1200        }
1201
1202        fn arb_op() -> impl Strategy<Value = Op> {
1203            prop_oneof![
1204                3 => arb_overlay_spec().prop_flat_map(|(start, len, priority)| {
1205                    (Just(start), Just(len), Just(priority), 0u8..3u8)
1206                }).prop_map(|(start, len, priority, ns_idx)| Op::Add {
1207                    start, len, priority, ns_idx,
1208                }),
1209                2 => (0..BUFFER_SIZE, MIN_QUERY_LEN..=BUFFER_SIZE)
1210                    .prop_map(|(start, qlen)| {
1211                        let s = start.min(BUFFER_SIZE - 1);
1212                        let e = (s + qlen).min(BUFFER_SIZE);
1213                        Op::RemoveInRange { start: s, end: e }
1214                    }),
1215                1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
1216                1 => (
1217                    0..BUFFER_SIZE,
1218                    MIN_QUERY_LEN..=BUFFER_SIZE,
1219                    0u8..3u8,
1220                    prop::collection::vec(arb_overlay_spec(), 0..4),
1221                )
1222                    .prop_map(|(start, qlen, ns_idx, new_overlays)| {
1223                        let s = start.min(BUFFER_SIZE - 1);
1224                        let e = (s + qlen).min(BUFFER_SIZE);
1225                        Op::ReplaceRange { start: s, end: e, ns_idx, new_overlays }
1226                    }),
1227            ]
1228        }
1229
1230        fn nsf(idx: u8) -> OverlayNamespace {
1231            OverlayNamespace::from_string(format!("ns{idx}"))
1232        }
1233
1234        proptest! {
1235            /// Invariants must hold after every sequence of operations.
1236            /// Plus: after `remove_in_range(r)`, no surviving overlay's
1237            /// range overlaps `r`. Plus: after `add` / `extend` /
1238            /// `clear_namespace` / `replace_range_in_namespace`, the
1239            /// vector is sorted by priority. Note: priority order may be
1240            /// transiently broken right after `remove_in_range` until the
1241            /// next `add` — production callers always pair these.
1242            #[test]
1243            fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..30)) {
1244                let mut marker_list = MarkerList::new();
1245                marker_list.set_buffer_size(BUFFER_SIZE);
1246                let mut manager = OverlayManager::new();
1247
1248                for op in ops {
1249                    match op {
1250                        Op::Add { start, len, priority, ns_idx } => {
1251                            let o = Overlay::with_namespace(
1252                                &mut marker_list,
1253                                start..(start + len),
1254                                OverlayFace::Background { color: Color::Red },
1255                                nsf(ns_idx),
1256                            );
1257                            let mut o = o;
1258                            o.priority = priority;
1259                            manager.add(o);
1260                            manager.check_invariants();
1261                            manager.assert_priority_sorted();
1262                        }
1263                        Op::RemoveInRange { start, end } => {
1264                            manager.remove_in_range(&(start..end), &mut marker_list);
1265                            for (o, rng) in manager.query_viewport(start, end, &marker_list) {
1266                                let overlaps = rng.start < end && start < rng.end;
1267                                prop_assert!(
1268                                    !overlaps,
1269                                    "overlay {:?} (handle {:?}) survived remove_in_range({start}..{end})",
1270                                    rng, o.handle,
1271                                );
1272                            }
1273                            manager.check_invariants();
1274                        }
1275                        Op::ClearNamespace { ns_idx } => {
1276                            manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
1277                            manager.check_invariants();
1278                            manager.assert_priority_sorted();
1279                        }
1280                        Op::ReplaceRange { start, end, ns_idx, new_overlays } => {
1281                            let new: Vec<Overlay> = new_overlays.into_iter().map(|(s, l, p)| {
1282                                let mut o = Overlay::with_namespace(
1283                                    &mut marker_list,
1284                                    s..(s + l),
1285                                    OverlayFace::Background { color: Color::Blue },
1286                                    nsf(ns_idx),
1287                                );
1288                                o.priority = p;
1289                                o
1290                            }).collect();
1291                            manager.replace_range_in_namespace(
1292                                &nsf(ns_idx),
1293                                &(start..end),
1294                                new,
1295                                &mut marker_list,
1296                            );
1297                            manager.check_invariants();
1298                            manager.assert_priority_sorted();
1299                        }
1300                    }
1301                }
1302            }
1303        }
1304    }
1305}