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    /// Create an overlay with a specific priority
210    pub fn with_priority(
211        marker_list: &mut MarkerList,
212        range: Range<usize>,
213        face: OverlayFace,
214        priority: Priority,
215    ) -> Self {
216        let mut overlay = Self::new(marker_list, range, face);
217        overlay.priority = priority;
218        overlay
219    }
220
221    /// Add a message/tooltip to this overlay
222    pub fn with_message(mut self, message: String) -> Self {
223        self.message = Some(message);
224        self
225    }
226
227    /// Set the priority
228    pub fn with_priority_value(mut self, priority: Priority) -> Self {
229        self.priority = priority;
230        self
231    }
232
233    /// Set the namespace
234    pub fn with_namespace_value(mut self, namespace: OverlayNamespace) -> Self {
235        self.namespace = Some(namespace);
236        self
237    }
238
239    /// Set whether to extend the overlay to the end of the visual line
240    pub fn with_extend_to_line_end(mut self, extend: bool) -> Self {
241        self.extend_to_line_end = extend;
242        self
243    }
244
245    /// Set the theme key that produced this overlay's color
246    pub fn with_theme_key(mut self, key: &'static str) -> Self {
247        self.theme_key = Some(key);
248        self
249    }
250
251    /// Get the current byte range by resolving markers
252    /// This is called once per frame during rendering setup
253    pub fn range(&self, marker_list: &MarkerList) -> Range<usize> {
254        let start = marker_list.get_position(self.start_marker).unwrap_or(0);
255        let end = marker_list.get_position(self.end_marker).unwrap_or(0);
256        start..end
257    }
258
259    /// Check if this overlay contains a position
260    pub fn contains(&self, position: usize, marker_list: &MarkerList) -> bool {
261        self.range(marker_list).contains(&position)
262    }
263
264    /// Check if this overlay overlaps with a range
265    pub fn overlaps(&self, range: &Range<usize>, marker_list: &MarkerList) -> bool {
266        let self_range = self.range(marker_list);
267        self_range.start < range.end && range.start < self_range.end
268    }
269}
270
271/// Manages overlays for a buffer
272/// Overlays are sorted by priority for efficient rendering
273#[derive(Debug, Clone)]
274pub struct OverlayManager {
275    /// All active overlays, indexed for O(1) lookup by handle
276    overlays: Vec<Overlay>,
277    /// `MarkerId -> index into overlays` for O(log N + k) `remove_in_range`.
278    /// Both endpoints of each overlay are registered. Kept in sync with
279    /// every push / swap_remove on `overlays`, and rebuilt after any sort.
280    marker_to_idx: HashMap<MarkerId, usize>,
281}
282
283impl OverlayManager {
284    /// Create a new empty overlay manager
285    pub fn new() -> Self {
286        Self {
287            overlays: Vec::new(),
288            marker_to_idx: HashMap::new(),
289        }
290    }
291
292    /// Add an overlay and return its handle for later removal
293    pub fn add(&mut self, overlay: Overlay) -> OverlayHandle {
294        let handle = overlay.handle.clone();
295        // Binary-search the priority-ordered insertion point and shift in
296        // place. Avoids the O(n²·log n) sort-on-every-add the prior impl
297        // had — the docstring on `extend` warned about this.
298        let priority = overlay.priority;
299        let pos = self.overlays.partition_point(|o| o.priority <= priority);
300        self.overlays.insert(pos, overlay);
301        // Every entry from `pos` onward shifted by one — re-index that tail.
302        // Tail length is small when adds are append-shaped (the common case
303        // for plugins that emit per-line clear+rebuild).
304        for (i, o) in self.overlays.iter().enumerate().skip(pos) {
305            self.marker_to_idx.insert(o.start_marker, i);
306            self.marker_to_idx.insert(o.end_marker, i);
307        }
308        handle
309    }
310
311    /// Append many overlays at once, sorting a single time at the end.
312    ///
313    /// `add` re-sorts the whole vector on every insertion, which is O(n² log n)
314    /// when a caller has N overlays to add. Use this instead when rebuilding an
315    /// overlay set from scratch (e.g. `set_virtual_buffer_content`), where the
316    /// caller already owns the full list up front.
317    pub fn extend<I: IntoIterator<Item = Overlay>>(&mut self, overlays: I) {
318        self.overlays.extend(overlays);
319        self.overlays.sort_by_key(|o| o.priority);
320        self.rebuild_marker_index();
321    }
322
323    /// Remove an overlay by its handle
324    pub fn remove_by_handle(
325        &mut self,
326        handle: &OverlayHandle,
327        marker_list: &mut MarkerList,
328    ) -> bool {
329        if let Some(pos) = self.overlays.iter().position(|o| &o.handle == handle) {
330            let overlay = self.overlays.remove(pos);
331            self.marker_to_idx.remove(&overlay.start_marker);
332            self.marker_to_idx.remove(&overlay.end_marker);
333            // Vec::remove shifts every subsequent entry down by one — repair.
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            marker_list.delete(overlay.start_marker);
339            marker_list.delete(overlay.end_marker);
340            true
341        } else {
342            false
343        }
344    }
345
346    /// Remove all overlays in a namespace
347    pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
348        let mut indices: Vec<usize> = self
349            .overlays
350            .iter()
351            .enumerate()
352            .filter_map(|(i, o)| (o.namespace.as_ref() == Some(namespace)).then_some(i))
353            .collect();
354        if indices.is_empty() {
355            return;
356        }
357        indices.sort_unstable_by(|a, b| b.cmp(a));
358        for idx in indices {
359            self.swap_remove_at(idx, marker_list);
360        }
361        // Restore priority order after swap_removes.
362        self.overlays.sort_by_key(|o| o.priority);
363        self.rebuild_marker_index();
364    }
365
366    /// Replace overlays in a namespace that overlap a range with new overlays.
367    ///
368    /// This preserves overlays outside the range, which helps avoid flicker and
369    /// unnecessary marker churn during viewport-only updates.
370    pub fn replace_range_in_namespace(
371        &mut self,
372        namespace: &OverlayNamespace,
373        range: &Range<usize>,
374        mut new_overlays: Vec<Overlay>,
375        marker_list: &mut MarkerList,
376    ) {
377        // Find overlays in this namespace that overlap the range. Use the
378        // marker-tree to narrow candidates; verify each candidate's true
379        // range and namespace before removing.
380        if range.start < range.end {
381            let hits = marker_list.query_range(range.start, range.end);
382            let mut candidates: Vec<usize> = hits
383                .iter()
384                .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
385                .collect();
386            candidates.sort_unstable();
387            candidates.dedup();
388            let mut to_remove: Vec<usize> = candidates
389                .into_iter()
390                .filter(|&idx| {
391                    let o = &self.overlays[idx];
392                    if o.namespace.as_ref() != Some(namespace) {
393                        return false;
394                    }
395                    let start = marker_list.get_position(o.start_marker).unwrap_or(0);
396                    let end = marker_list.get_position(o.end_marker).unwrap_or(0);
397                    start < range.end && range.start < end
398                })
399                .collect();
400            to_remove.sort_unstable_by(|a, b| b.cmp(a));
401            for idx in to_remove {
402                self.swap_remove_at(idx, marker_list);
403            }
404        }
405
406        if !new_overlays.is_empty() {
407            self.overlays.append(&mut new_overlays);
408        }
409        self.overlays.sort_by_key(|o| o.priority);
410        self.rebuild_marker_index();
411    }
412
413    /// Remove all overlays in a range and clean up their markers
414    pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
415        // O(log N + k) for the lookup; restoring the priority-sorted
416        // invariant after `swap_remove` is O(N) (adaptive sort on a
417        // near-sorted vec) plus O(N) marker_to_idx rebuild. For typical
418        // markdown_compose workloads where overlays in a buffer share
419        // the same priority, the adaptive sort is a no-op pass.
420        // Spanning overlays (start < range.start && end > range.end) are
421        // not detected — same precondition as ConcealManager.
422        if range.start >= range.end {
423            return;
424        }
425        let hits = marker_list.query_range(range.start, range.end);
426        if hits.is_empty() {
427            return;
428        }
429        let mut candidates: Vec<usize> = hits
430            .iter()
431            .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
432            .collect();
433        candidates.sort_unstable();
434        candidates.dedup();
435
436        let mut to_remove: Vec<usize> = candidates
437            .into_iter()
438            .filter(|&idx| {
439                let o = &self.overlays[idx];
440                let start = marker_list.get_position(o.start_marker).unwrap_or(0);
441                let end = marker_list.get_position(o.end_marker).unwrap_or(0);
442                start < range.end && range.start < end
443            })
444            .collect();
445        if to_remove.is_empty() {
446            return;
447        }
448        to_remove.sort_unstable_by(|a, b| b.cmp(a));
449        for idx in to_remove {
450            self.swap_remove_at(idx, marker_list);
451        }
452        // Restore priority order broken by swap_removes.
453        self.overlays.sort_by_key(|o| o.priority);
454        self.rebuild_marker_index();
455    }
456
457    /// Clear all overlays and their markers
458    pub fn clear(&mut self, marker_list: &mut MarkerList) {
459        for overlay in &self.overlays {
460            marker_list.delete(overlay.start_marker);
461            marker_list.delete(overlay.end_marker);
462        }
463        self.overlays.clear();
464        self.marker_to_idx.clear();
465    }
466
467    /// Swap-remove the entry at `idx`, deleting its markers and patching
468    /// `marker_to_idx` for whatever entry got swapped in. Caller is
469    /// responsible for restoring sort order if needed.
470    fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
471        let removed = self.overlays.swap_remove(idx);
472        self.marker_to_idx.remove(&removed.start_marker);
473        self.marker_to_idx.remove(&removed.end_marker);
474        marker_list.delete(removed.start_marker);
475        marker_list.delete(removed.end_marker);
476        if let Some(moved) = self.overlays.get(idx) {
477            self.marker_to_idx.insert(moved.start_marker, idx);
478            self.marker_to_idx.insert(moved.end_marker, idx);
479        }
480    }
481
482    /// Rebuild `marker_to_idx` from the current `overlays` order.
483    /// Called after sorts that scramble indices.
484    fn rebuild_marker_index(&mut self) {
485        self.marker_to_idx.clear();
486        for (i, o) in self.overlays.iter().enumerate() {
487            self.marker_to_idx.insert(o.start_marker, i);
488            self.marker_to_idx.insert(o.end_marker, i);
489        }
490    }
491
492    /// Get all overlays at a specific position, sorted by priority
493    pub fn at_position(&self, position: usize, marker_list: &MarkerList) -> Vec<&Overlay> {
494        self.overlays
495            .iter()
496            .filter(|o| {
497                let range = o.range(marker_list);
498                range.contains(&position)
499            })
500            .collect()
501    }
502
503    /// Get all overlays that overlap with a range, sorted by priority
504    pub fn in_range(&self, range: &Range<usize>, marker_list: &MarkerList) -> Vec<&Overlay> {
505        self.overlays
506            .iter()
507            .filter(|o| o.overlaps(range, marker_list))
508            .collect()
509    }
510
511    /// Query overlays in a viewport range efficiently using the marker interval tree
512    ///
513    /// This is much faster than calling `at_position()` for every character in the range.
514    /// Returns overlays with their resolved byte ranges.
515    ///
516    /// # Performance
517    /// - Old approach: O(N * M) where N = positions to check, M = overlay count
518    /// - This approach: O(log M + k) where k = overlays in viewport (typically 2-10)
519    pub fn query_viewport(
520        &self,
521        start: usize,
522        end: usize,
523        marker_list: &MarkerList,
524    ) -> Vec<(&Overlay, Range<usize>)> {
525        use std::collections::HashMap;
526
527        // Query the marker interval tree once for all markers in viewport
528        // This is O(log N + k) where k = markers in viewport
529        let visible_markers = marker_list.query_range(start, end);
530
531        // Build a quick lookup map: marker_id -> position
532        let marker_positions: HashMap<_, _> = visible_markers
533            .into_iter()
534            .map(|(id, start, _end)| (id, start))
535            .collect();
536
537        // Find overlays whose markers overlap with the viewport.
538        // At least one marker must be in the viewport, but the other may be
539        // outside (e.g. a multi-line overlay partially scrolled out of view).
540        // For the out-of-viewport marker, fall back to resolving its position
541        // directly from the marker list.
542        self.overlays
543            .iter()
544            .filter_map(|overlay| {
545                let start_in_vp = marker_positions.get(&overlay.start_marker).copied();
546                let end_in_vp = marker_positions.get(&overlay.end_marker).copied();
547
548                // At least one marker must be in the viewport for the overlay
549                // to be visible at all
550                if start_in_vp.is_none() && end_in_vp.is_none() {
551                    return None;
552                }
553
554                // For the marker outside the viewport, resolve its position directly
555                let start_pos =
556                    start_in_vp.or_else(|| marker_list.get_position(overlay.start_marker))?;
557                let end_pos = end_in_vp.or_else(|| marker_list.get_position(overlay.end_marker))?;
558
559                let range = start_pos..end_pos;
560
561                // Only include if actually overlaps viewport.
562                // For zero-width ranges (e.g. diagnostics at a single position),
563                // check that the point is within [start, end] (inclusive).
564                // For non-zero ranges, check standard overlap: start < end && end > start.
565                let included = if range.start == range.end {
566                    range.start >= start && range.start <= end
567                } else {
568                    range.start < end && range.end > start
569                };
570
571                if included {
572                    Some((overlay, range))
573                } else {
574                    None
575                }
576            })
577            .collect()
578    }
579
580    /// Get overlay by handle
581    pub fn get_by_handle(&self, handle: &OverlayHandle) -> Option<&Overlay> {
582        self.overlays.iter().find(|o| &o.handle == handle)
583    }
584
585    /// Get mutable overlay by handle
586    pub fn get_by_handle_mut(&mut self, handle: &OverlayHandle) -> Option<&mut Overlay> {
587        self.overlays.iter_mut().find(|o| &o.handle == handle)
588    }
589
590    /// Get total number of overlays
591    pub fn len(&self) -> usize {
592        self.overlays.len()
593    }
594
595    /// Check if there are any overlays
596    pub fn is_empty(&self) -> bool {
597        self.overlays.is_empty()
598    }
599
600    /// Get all overlays (for rendering)
601    pub fn all(&self) -> &[Overlay] {
602        &self.overlays
603    }
604
605    /// Test-only: assert `marker_to_idx` is consistent with `overlays`,
606    /// and that priorities are non-decreasing along the vector.
607    /// Panics on any divergence. Used by property tests.
608    #[cfg(test)]
609    fn check_invariants(&self) {
610        assert_eq!(
611            self.marker_to_idx.len(),
612            self.overlays.len() * 2,
613            "marker_to_idx size != 2 * overlays.len()"
614        );
615        for (i, o) in self.overlays.iter().enumerate() {
616            assert_eq!(
617                self.marker_to_idx.get(&o.start_marker).copied(),
618                Some(i),
619                "start_marker {:?} of overlay {} mismapped",
620                o.start_marker,
621                i,
622            );
623            assert_eq!(
624                self.marker_to_idx.get(&o.end_marker).copied(),
625                Some(i),
626                "end_marker {:?} of overlay {} mismapped",
627                o.end_marker,
628                i,
629            );
630        }
631        // Priority order — only enforceable when nothing is mid-cycle.
632        // Tests check this via `assert_priority_sorted` after points
633        // where the invariant is supposed to hold (e.g. after `add`).
634    }
635
636    /// Test-only: assert overlays are non-decreasing by priority.
637    #[cfg(test)]
638    fn assert_priority_sorted(&self) {
639        for w in self.overlays.windows(2) {
640            assert!(
641                w[0].priority <= w[1].priority,
642                "priority order broken: {} after {}",
643                w[1].priority,
644                w[0].priority,
645            );
646        }
647    }
648}
649
650impl Default for OverlayManager {
651    fn default() -> Self {
652        Self::new()
653    }
654}
655
656/// Helper functions for creating common overlay types
657impl Overlay {
658    /// Create an error underline overlay (wavy red line)
659    pub fn error(
660        marker_list: &mut MarkerList,
661        range: Range<usize>,
662        message: Option<String>,
663    ) -> Self {
664        let mut overlay = Self::with_priority(
665            marker_list,
666            range,
667            OverlayFace::Underline {
668                color: Color::Red,
669                style: UnderlineStyle::Wavy,
670            },
671            10, // Higher priority for errors
672        );
673        overlay.message = message;
674        overlay
675    }
676
677    /// Create a warning underline overlay (wavy yellow line)
678    pub fn warning(
679        marker_list: &mut MarkerList,
680        range: Range<usize>,
681        message: Option<String>,
682    ) -> Self {
683        let mut overlay = Self::with_priority(
684            marker_list,
685            range,
686            OverlayFace::Underline {
687                color: Color::Yellow,
688                style: UnderlineStyle::Wavy,
689            },
690            5, // Medium priority for warnings
691        );
692        overlay.message = message;
693        overlay
694    }
695
696    /// Create an info underline overlay (wavy blue line)
697    pub fn info(
698        marker_list: &mut MarkerList,
699        range: Range<usize>,
700        message: Option<String>,
701    ) -> Self {
702        let mut overlay = Self::with_priority(
703            marker_list,
704            range,
705            OverlayFace::Underline {
706                color: Color::Blue,
707                style: UnderlineStyle::Wavy,
708            },
709            3, // Lower priority for info
710        );
711        overlay.message = message;
712        overlay
713    }
714
715    /// Create a hint underline overlay (dotted gray line)
716    pub fn hint(
717        marker_list: &mut MarkerList,
718        range: Range<usize>,
719        message: Option<String>,
720    ) -> Self {
721        let mut overlay = Self::with_priority(
722            marker_list,
723            range,
724            OverlayFace::Underline {
725                color: Color::Gray,
726                style: UnderlineStyle::Dotted,
727            },
728            1, // Lowest priority for hints
729        );
730        overlay.message = message;
731        overlay
732    }
733
734    /// Create a selection highlight overlay
735    pub fn selection(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
736        let mut overlay = Self::with_priority(
737            marker_list,
738            range,
739            OverlayFace::Background {
740                color: Color::Rgb(38, 79, 120), // VSCode-like selection color
741            },
742            -10, // Very low priority so it's under other overlays
743        );
744        overlay.theme_key = Some("editor.selection_bg");
745        overlay
746    }
747
748    /// Create a search result highlight overlay
749    pub fn search_match(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
750        let mut overlay = Self::with_priority(
751            marker_list,
752            range,
753            OverlayFace::Background {
754                color: Color::Rgb(72, 72, 0), // Yellow-ish highlight
755            },
756            -5, // Low priority
757        );
758        overlay.theme_key = Some("search.match_bg");
759        overlay
760    }
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766
767    #[test]
768    fn test_overlay_creation_with_markers() {
769        let mut marker_list = MarkerList::new();
770        marker_list.set_buffer_size(100);
771
772        let overlay = Overlay::new(
773            &mut marker_list,
774            5..10,
775            OverlayFace::Background { color: Color::Red },
776        );
777
778        assert_eq!(marker_list.get_position(overlay.start_marker), Some(5));
779        assert_eq!(marker_list.get_position(overlay.end_marker), Some(10));
780        assert_eq!(overlay.range(&marker_list), 5..10);
781    }
782
783    #[test]
784    fn test_overlay_adjusts_with_insert() {
785        let mut marker_list = MarkerList::new();
786        marker_list.set_buffer_size(100);
787
788        let overlay = Overlay::new(
789            &mut marker_list,
790            10..20,
791            OverlayFace::Background { color: Color::Red },
792        );
793
794        // Insert before overlay
795        marker_list.adjust_for_insert(5, 10);
796
797        // Overlay should have moved forward
798        assert_eq!(overlay.range(&marker_list), 20..30);
799    }
800
801    #[test]
802    fn test_overlay_adjusts_with_delete() {
803        let mut marker_list = MarkerList::new();
804        marker_list.set_buffer_size(100);
805
806        let overlay = Overlay::new(
807            &mut marker_list,
808            20..30,
809            OverlayFace::Background { color: Color::Red },
810        );
811
812        // Delete before overlay
813        marker_list.adjust_for_delete(5, 10);
814
815        // Overlay should have moved backward
816        assert_eq!(overlay.range(&marker_list), 10..20);
817    }
818
819    #[test]
820    fn test_overlay_manager_add_remove() {
821        let mut marker_list = MarkerList::new();
822        marker_list.set_buffer_size(100);
823        let mut manager = OverlayManager::new();
824
825        let overlay = Overlay::new(
826            &mut marker_list,
827            5..10,
828            OverlayFace::Background { color: Color::Red },
829        );
830
831        let handle = manager.add(overlay);
832        assert_eq!(manager.len(), 1);
833
834        manager.remove_by_handle(&handle, &mut marker_list);
835        assert_eq!(manager.len(), 0);
836    }
837
838    #[test]
839    fn test_overlay_namespace_clear() {
840        let mut marker_list = MarkerList::new();
841        marker_list.set_buffer_size(100);
842        let mut manager = OverlayManager::new();
843
844        let ns = OverlayNamespace::from_string("todo".to_string());
845
846        // Add overlays in namespace
847        let overlay1 = Overlay::with_namespace(
848            &mut marker_list,
849            5..10,
850            OverlayFace::Background { color: Color::Red },
851            ns.clone(),
852        );
853        let overlay2 = Overlay::with_namespace(
854            &mut marker_list,
855            15..20,
856            OverlayFace::Background { color: Color::Blue },
857            ns.clone(),
858        );
859        // Add overlay without namespace
860        let overlay3 = Overlay::new(
861            &mut marker_list,
862            25..30,
863            OverlayFace::Background {
864                color: Color::Green,
865            },
866        );
867
868        manager.add(overlay1);
869        manager.add(overlay2);
870        manager.add(overlay3);
871        assert_eq!(manager.len(), 3);
872
873        // Clear only the namespace
874        manager.clear_namespace(&ns, &mut marker_list);
875        assert_eq!(manager.len(), 1); // Only overlay3 remains
876    }
877
878    #[test]
879    fn test_overlay_priority_sorting() {
880        let mut marker_list = MarkerList::new();
881        marker_list.set_buffer_size(100);
882        let mut manager = OverlayManager::new();
883
884        manager.add(Overlay::with_priority(
885            &mut marker_list,
886            5..10,
887            OverlayFace::Background { color: Color::Red },
888            10,
889        ));
890        manager.add(Overlay::with_priority(
891            &mut marker_list,
892            5..10,
893            OverlayFace::Background { color: Color::Blue },
894            5,
895        ));
896        manager.add(Overlay::with_priority(
897            &mut marker_list,
898            5..10,
899            OverlayFace::Background {
900                color: Color::Green,
901            },
902            15,
903        ));
904
905        let overlays = manager.at_position(7, &marker_list);
906        assert_eq!(overlays.len(), 3);
907        // Should be sorted by priority (low to high)
908        assert_eq!(overlays[0].priority, 5);
909        assert_eq!(overlays[1].priority, 10);
910        assert_eq!(overlays[2].priority, 15);
911    }
912
913    #[test]
914    fn test_overlay_contains_and_overlaps() {
915        let mut marker_list = MarkerList::new();
916        marker_list.set_buffer_size(100);
917
918        let overlay = Overlay::new(
919            &mut marker_list,
920            10..20,
921            OverlayFace::Background { color: Color::Red },
922        );
923
924        assert!(!overlay.contains(9, &marker_list));
925        assert!(overlay.contains(10, &marker_list));
926        assert!(overlay.contains(15, &marker_list));
927        assert!(overlay.contains(19, &marker_list));
928        assert!(!overlay.contains(20, &marker_list));
929
930        assert!(!overlay.overlaps(&(0..10), &marker_list));
931        assert!(overlay.overlaps(&(5..15), &marker_list));
932        assert!(overlay.overlaps(&(15..25), &marker_list));
933        assert!(!overlay.overlaps(&(20..30), &marker_list));
934    }
935
936    #[test]
937    fn test_overlay_remove_in_range_keeps_only_disjoint() {
938        let mut marker_list = MarkerList::new();
939        marker_list.set_buffer_size(200);
940        let mut manager = OverlayManager::new();
941
942        manager.add(Overlay::new(
943            &mut marker_list,
944            0..5,
945            OverlayFace::Background { color: Color::Red },
946        ));
947        manager.add(Overlay::new(
948            &mut marker_list,
949            10..20,
950            OverlayFace::Background { color: Color::Blue },
951        ));
952        manager.add(Overlay::new(
953            &mut marker_list,
954            30..40,
955            OverlayFace::Background {
956                color: Color::Green,
957            },
958        ));
959        manager.add(Overlay::new(
960            &mut marker_list,
961            50..60,
962            OverlayFace::Background {
963                color: Color::Yellow,
964            },
965        ));
966
967        // Range 15..35 overlaps overlays #2 (10..20) and #3 (30..40), leaves #1 and #4.
968        manager.remove_in_range(&(15..35), &mut marker_list);
969
970        let kept: Vec<_> = manager
971            .all()
972            .iter()
973            .map(|o| o.range(&marker_list))
974            .collect();
975        assert_eq!(kept, vec![0..5, 50..60]);
976    }
977
978    #[test]
979    fn test_overlay_remove_in_range_deletes_markers() {
980        let mut marker_list = MarkerList::new();
981        marker_list.set_buffer_size(100);
982        let mut manager = OverlayManager::new();
983
984        let overlay = Overlay::new(
985            &mut marker_list,
986            10..20,
987            OverlayFace::Background { color: Color::Red },
988        );
989        let start_id = overlay.start_marker;
990        let end_id = overlay.end_marker;
991        manager.add(overlay);
992
993        manager.remove_in_range(&(0..50), &mut marker_list);
994
995        assert_eq!(manager.len(), 0);
996        assert_eq!(marker_list.get_position(start_id), None);
997        assert_eq!(marker_list.get_position(end_id), None);
998    }
999
1000    #[test]
1001    fn test_overlay_remove_in_range_endpoint_semantics() {
1002        // Touching at a single endpoint must NOT remove (start == range.end or end == range.start).
1003        let mut marker_list = MarkerList::new();
1004        marker_list.set_buffer_size(100);
1005        let mut manager = OverlayManager::new();
1006
1007        manager.add(Overlay::new(
1008            &mut marker_list,
1009            10..20,
1010            OverlayFace::Background { color: Color::Red },
1011        ));
1012
1013        manager.remove_in_range(&(20..30), &mut marker_list);
1014        assert_eq!(manager.len(), 1);
1015        manager.remove_in_range(&(0..10), &mut marker_list);
1016        assert_eq!(manager.len(), 1);
1017        manager.remove_in_range(&(19..21), &mut marker_list);
1018        assert_eq!(manager.len(), 0);
1019    }
1020
1021    /// Mirrors the production cycle: per line in `lines_changed`, clear
1022    /// overlays in the line's byte range, then re-add the line's overlays.
1023    /// Steady-state count holds throughout. Same shape as the matching
1024    /// conceal perf test for direct comparison.
1025    ///
1026    /// Run with:
1027    ///   cargo nextest run -p fresh-editor --no-capture \
1028    ///     view::overlay::tests::perf_full_buffer_rebuild_pass
1029    #[test]
1030    fn perf_full_buffer_rebuild_pass() {
1031        const LINES: usize = 500;
1032        const LINE_BYTES: usize = 50;
1033        const OVERLAYS_PER_LINE: usize = 5;
1034
1035        let mut marker_list = MarkerList::new();
1036        marker_list.set_buffer_size(LINES * LINE_BYTES);
1037        let mut manager = OverlayManager::new();
1038
1039        let overlay_byte = |line: usize, k: usize| -> usize {
1040            line * LINE_BYTES + k * (LINE_BYTES / OVERLAYS_PER_LINE)
1041        };
1042        let make_overlay = |ml: &mut MarkerList, line: usize, k: usize| {
1043            let s = overlay_byte(line, k);
1044            Overlay::new(
1045                ml,
1046                s..(s + 2),
1047                OverlayFace::Background { color: Color::Red },
1048            )
1049        };
1050
1051        // Populate steady state.
1052        for line in 0..LINES {
1053            for k in 0..OVERLAYS_PER_LINE {
1054                let o = make_overlay(&mut marker_list, line, k);
1055                manager.add(o);
1056            }
1057        }
1058        let initial = LINES * OVERLAYS_PER_LINE;
1059
1060        // One full-buffer `lines_changed` pass: per line, clear + re-add.
1061        let start = std::time::Instant::now();
1062        for line in 0..LINES {
1063            let line_range = (line * LINE_BYTES)..((line + 1) * LINE_BYTES);
1064            manager.remove_in_range(&line_range, &mut marker_list);
1065            for k in 0..OVERLAYS_PER_LINE {
1066                let o = make_overlay(&mut marker_list, line, k);
1067                manager.add(o);
1068            }
1069        }
1070        let elapsed = start.elapsed();
1071
1072        eprintln!(
1073            "[perf] overlay full-buffer rebuild ({LINES} lines, {} entries steady): \
1074             {:?} total, {:?}/line",
1075            initial,
1076            elapsed,
1077            elapsed / LINES as u32,
1078        );
1079        assert_eq!(manager.len(), initial);
1080    }
1081
1082    mod proptests {
1083        use super::*;
1084        use proptest::prelude::*;
1085
1086        #[derive(Debug, Clone)]
1087        enum Op {
1088            Add {
1089                start: usize,
1090                len: usize,
1091                priority: i32,
1092                ns_idx: u8,
1093            },
1094            RemoveInRange {
1095                start: usize,
1096                end: usize,
1097            },
1098            ClearNamespace {
1099                ns_idx: u8,
1100            },
1101            ReplaceRange {
1102                start: usize,
1103                end: usize,
1104                ns_idx: u8,
1105                /// New overlays to insert in the same range; same shape
1106                /// as `Add` but len capped to satisfy precondition.
1107                new_overlays: Vec<(usize, usize, i32)>,
1108            },
1109        }
1110
1111        const BUFFER_SIZE: usize = 200;
1112        const MAX_OVERLAY_LEN: usize = 4;
1113        const MIN_QUERY_LEN: usize = MAX_OVERLAY_LEN + 1;
1114
1115        fn arb_overlay_spec() -> impl Strategy<Value = (usize, usize, i32)> {
1116            (
1117                0..(BUFFER_SIZE - MAX_OVERLAY_LEN),
1118                1..=MAX_OVERLAY_LEN,
1119                -5i32..=5i32,
1120            )
1121        }
1122
1123        fn arb_op() -> impl Strategy<Value = Op> {
1124            prop_oneof![
1125                3 => arb_overlay_spec().prop_flat_map(|(start, len, priority)| {
1126                    (Just(start), Just(len), Just(priority), 0u8..3u8)
1127                }).prop_map(|(start, len, priority, ns_idx)| Op::Add {
1128                    start, len, priority, ns_idx,
1129                }),
1130                2 => (0..BUFFER_SIZE, MIN_QUERY_LEN..=BUFFER_SIZE)
1131                    .prop_map(|(start, qlen)| {
1132                        let s = start.min(BUFFER_SIZE - 1);
1133                        let e = (s + qlen).min(BUFFER_SIZE);
1134                        Op::RemoveInRange { start: s, end: e }
1135                    }),
1136                1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
1137                1 => (
1138                    0..BUFFER_SIZE,
1139                    MIN_QUERY_LEN..=BUFFER_SIZE,
1140                    0u8..3u8,
1141                    prop::collection::vec(arb_overlay_spec(), 0..4),
1142                )
1143                    .prop_map(|(start, qlen, ns_idx, new_overlays)| {
1144                        let s = start.min(BUFFER_SIZE - 1);
1145                        let e = (s + qlen).min(BUFFER_SIZE);
1146                        Op::ReplaceRange { start: s, end: e, ns_idx, new_overlays }
1147                    }),
1148            ]
1149        }
1150
1151        fn nsf(idx: u8) -> OverlayNamespace {
1152            OverlayNamespace::from_string(format!("ns{idx}"))
1153        }
1154
1155        proptest! {
1156            /// Invariants must hold after every sequence of operations.
1157            /// Plus: after `remove_in_range(r)`, no surviving overlay's
1158            /// range overlaps `r`. Plus: after `add` / `extend` /
1159            /// `clear_namespace` / `replace_range_in_namespace`, the
1160            /// vector is sorted by priority. Note: priority order may be
1161            /// transiently broken right after `remove_in_range` until the
1162            /// next `add` — production callers always pair these.
1163            #[test]
1164            fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..30)) {
1165                let mut marker_list = MarkerList::new();
1166                marker_list.set_buffer_size(BUFFER_SIZE);
1167                let mut manager = OverlayManager::new();
1168
1169                for op in ops {
1170                    match op {
1171                        Op::Add { start, len, priority, ns_idx } => {
1172                            let o = Overlay::with_namespace(
1173                                &mut marker_list,
1174                                start..(start + len),
1175                                OverlayFace::Background { color: Color::Red },
1176                                nsf(ns_idx),
1177                            );
1178                            let mut o = o;
1179                            o.priority = priority;
1180                            manager.add(o);
1181                            manager.check_invariants();
1182                            manager.assert_priority_sorted();
1183                        }
1184                        Op::RemoveInRange { start, end } => {
1185                            manager.remove_in_range(&(start..end), &mut marker_list);
1186                            for (o, rng) in manager.query_viewport(start, end, &marker_list) {
1187                                let overlaps = rng.start < end && start < rng.end;
1188                                prop_assert!(
1189                                    !overlaps,
1190                                    "overlay {:?} (handle {:?}) survived remove_in_range({start}..{end})",
1191                                    rng, o.handle,
1192                                );
1193                            }
1194                            manager.check_invariants();
1195                        }
1196                        Op::ClearNamespace { ns_idx } => {
1197                            manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
1198                            manager.check_invariants();
1199                            manager.assert_priority_sorted();
1200                        }
1201                        Op::ReplaceRange { start, end, ns_idx, new_overlays } => {
1202                            let new: Vec<Overlay> = new_overlays.into_iter().map(|(s, l, p)| {
1203                                let mut o = Overlay::with_namespace(
1204                                    &mut marker_list,
1205                                    s..(s + l),
1206                                    OverlayFace::Background { color: Color::Blue },
1207                                    nsf(ns_idx),
1208                                );
1209                                o.priority = p;
1210                                o
1211                            }).collect();
1212                            manager.replace_range_in_namespace(
1213                                &nsf(ns_idx),
1214                                &(start..end),
1215                                new,
1216                                &mut marker_list,
1217                            );
1218                            manager.check_invariants();
1219                            manager.assert_priority_sorted();
1220                        }
1221                    }
1222                }
1223            }
1224        }
1225    }
1226}