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                    if start < end {
428                        // Healthy overlay: remove on genuine half-open overlap.
429                        start < range.end && range.start < end
430                    } else {
431                        // Collapsed (start == end) or inverted (start > end)
432                        // overlay. These arise when an edit erases the overlay's
433                        // anchored text — the markers clamp to the edit point and
434                        // a later insert can even push them past each other
435                        // (issue #2414). A strict overlap test never matches a
436                        // zero-length span, so the dead overlay would linger and
437                        // surface as a phantom search match. Treat it as a point
438                        // and remove it whenever it lands inside the replaced
439                        // range, so it is dropped rather than recreated.
440                        let lo = start.min(end);
441                        let hi = start.max(end);
442                        lo <= range.end && range.start <= hi
443                    }
444                })
445                .collect();
446            to_remove.sort_unstable_by(|a, b| b.cmp(a));
447            for idx in to_remove {
448                self.swap_remove_at(idx, marker_list);
449            }
450        }
451
452        if !new_overlays.is_empty() {
453            self.overlays.append(&mut new_overlays);
454        }
455        self.overlays.sort_by_key(|o| o.priority);
456        self.rebuild_marker_index();
457    }
458
459    /// Remove all overlays in a range and clean up their markers
460    pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
461        // O(log N + k) for the lookup; restoring the priority-sorted
462        // invariant after `swap_remove` is O(N) (adaptive sort on a
463        // near-sorted vec) plus O(N) marker_to_idx rebuild. For typical
464        // markdown_compose workloads where overlays in a buffer share
465        // the same priority, the adaptive sort is a no-op pass.
466        // Spanning overlays (start < range.start && end > range.end) are
467        // not detected — same precondition as ConcealManager.
468        if range.start >= range.end {
469            return;
470        }
471        let hits = marker_list.query_range(range.start, range.end);
472        if hits.is_empty() {
473            return;
474        }
475        let mut candidates: Vec<usize> = hits
476            .iter()
477            .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
478            .collect();
479        candidates.sort_unstable();
480        candidates.dedup();
481
482        let mut to_remove: Vec<usize> = candidates
483            .into_iter()
484            .filter(|&idx| {
485                let o = &self.overlays[idx];
486                let start = marker_list.get_position(o.start_marker).unwrap_or(0);
487                let end = marker_list.get_position(o.end_marker).unwrap_or(0);
488                start < range.end && range.start < end
489            })
490            .collect();
491        if to_remove.is_empty() {
492            return;
493        }
494        to_remove.sort_unstable_by(|a, b| b.cmp(a));
495        for idx in to_remove {
496            self.swap_remove_at(idx, marker_list);
497        }
498        // Restore priority order broken by swap_removes.
499        self.overlays.sort_by_key(|o| o.priority);
500        self.rebuild_marker_index();
501    }
502
503    /// Like [`remove_in_range`], but only removes overlays belonging to
504    /// `namespace`. Overlays in other namespaces (e.g. editor-owned LSP
505    /// diagnostics) that happen to overlap the range are left intact.
506    ///
507    /// [`remove_in_range`]: Self::remove_in_range
508    pub fn remove_in_range_for_namespace(
509        &mut self,
510        range: &Range<usize>,
511        namespace: &OverlayNamespace,
512        marker_list: &mut MarkerList,
513    ) {
514        if range.start >= range.end {
515            return;
516        }
517        let hits = marker_list.query_range(range.start, range.end);
518        if hits.is_empty() {
519            return;
520        }
521        let mut candidates: Vec<usize> = hits
522            .iter()
523            .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
524            .collect();
525        candidates.sort_unstable();
526        candidates.dedup();
527
528        let mut to_remove: Vec<usize> = candidates
529            .into_iter()
530            .filter(|&idx| {
531                let o = &self.overlays[idx];
532                if o.namespace.as_ref() != Some(namespace) {
533                    return false;
534                }
535                let start = marker_list.get_position(o.start_marker).unwrap_or(0);
536                let end = marker_list.get_position(o.end_marker).unwrap_or(0);
537                start < range.end && range.start < end
538            })
539            .collect();
540        if to_remove.is_empty() {
541            return;
542        }
543        to_remove.sort_unstable_by(|a, b| b.cmp(a));
544        for idx in to_remove {
545            self.swap_remove_at(idx, marker_list);
546        }
547        // Restore priority order broken by swap_removes.
548        self.overlays.sort_by_key(|o| o.priority);
549        self.rebuild_marker_index();
550    }
551
552    /// Clear all overlays and their markers
553    pub fn clear(&mut self, marker_list: &mut MarkerList) {
554        for overlay in &self.overlays {
555            marker_list.delete(overlay.start_marker);
556            marker_list.delete(overlay.end_marker);
557        }
558        self.overlays.clear();
559        self.marker_to_idx.clear();
560    }
561
562    /// Swap-remove the entry at `idx`, deleting its markers and patching
563    /// `marker_to_idx` for whatever entry got swapped in. Caller is
564    /// responsible for restoring sort order if needed.
565    fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
566        let removed = self.overlays.swap_remove(idx);
567        self.marker_to_idx.remove(&removed.start_marker);
568        self.marker_to_idx.remove(&removed.end_marker);
569        marker_list.delete(removed.start_marker);
570        marker_list.delete(removed.end_marker);
571        if let Some(moved) = self.overlays.get(idx) {
572            self.marker_to_idx.insert(moved.start_marker, idx);
573            self.marker_to_idx.insert(moved.end_marker, idx);
574        }
575    }
576
577    /// Rebuild `marker_to_idx` from the current `overlays` order.
578    /// Called after sorts that scramble indices.
579    fn rebuild_marker_index(&mut self) {
580        self.marker_to_idx.clear();
581        for (i, o) in self.overlays.iter().enumerate() {
582            self.marker_to_idx.insert(o.start_marker, i);
583            self.marker_to_idx.insert(o.end_marker, i);
584        }
585    }
586
587    /// Get all overlays at a specific position, sorted by priority
588    pub fn at_position(&self, position: usize, marker_list: &MarkerList) -> Vec<&Overlay> {
589        self.overlays
590            .iter()
591            .filter(|o| {
592                let range = o.range(marker_list);
593                range.contains(&position)
594            })
595            .collect()
596    }
597
598    /// Get all overlays that overlap with a range, sorted by priority
599    pub fn in_range(&self, range: &Range<usize>, marker_list: &MarkerList) -> Vec<&Overlay> {
600        self.overlays
601            .iter()
602            .filter(|o| o.overlaps(range, marker_list))
603            .collect()
604    }
605
606    /// Query overlays in a viewport range efficiently using the marker interval tree
607    ///
608    /// This is much faster than calling `at_position()` for every character in the range.
609    /// Returns overlays with their resolved byte ranges.
610    ///
611    /// # Performance
612    /// - Old approach: O(N * M) where N = positions to check, M = overlay count
613    /// - This approach: O(log M + k) where k = overlays in viewport (typically 2-10)
614    pub fn query_viewport(
615        &self,
616        start: usize,
617        end: usize,
618        marker_list: &MarkerList,
619    ) -> Vec<(&Overlay, Range<usize>)> {
620        use std::collections::HashMap;
621
622        // Query the marker interval tree once for all markers in viewport
623        // This is O(log N + k) where k = markers in viewport
624        let visible_markers = marker_list.query_range(start, end);
625
626        // Build a quick lookup map: marker_id -> position
627        let marker_positions: HashMap<_, _> = visible_markers
628            .into_iter()
629            .map(|(id, start, _end)| (id, start))
630            .collect();
631
632        // Find overlays whose markers overlap with the viewport.
633        // At least one marker must be in the viewport, but the other may be
634        // outside (e.g. a multi-line overlay partially scrolled out of view).
635        // For the out-of-viewport marker, fall back to resolving its position
636        // directly from the marker list.
637        self.overlays
638            .iter()
639            .filter_map(|overlay| {
640                let start_in_vp = marker_positions.get(&overlay.start_marker).copied();
641                let end_in_vp = marker_positions.get(&overlay.end_marker).copied();
642
643                // At least one marker must be in the viewport for the overlay
644                // to be visible at all
645                if start_in_vp.is_none() && end_in_vp.is_none() {
646                    return None;
647                }
648
649                // For the marker outside the viewport, resolve its position directly
650                let start_pos =
651                    start_in_vp.or_else(|| marker_list.get_position(overlay.start_marker))?;
652                let end_pos = end_in_vp.or_else(|| marker_list.get_position(overlay.end_marker))?;
653
654                let range = start_pos..end_pos;
655
656                // Only include if actually overlaps viewport.
657                // For zero-width ranges (e.g. diagnostics at a single position),
658                // check that the point is within [start, end] (inclusive).
659                // For non-zero ranges, check standard overlap: start < end && end > start.
660                let included = if range.start == range.end {
661                    range.start >= start && range.start <= end
662                } else {
663                    range.start < end && range.end > start
664                };
665
666                if included {
667                    Some((overlay, range))
668                } else {
669                    None
670                }
671            })
672            .collect()
673    }
674
675    /// Get overlay by handle
676    pub fn get_by_handle(&self, handle: &OverlayHandle) -> Option<&Overlay> {
677        self.overlays.iter().find(|o| &o.handle == handle)
678    }
679
680    /// Get mutable overlay by handle
681    pub fn get_by_handle_mut(&mut self, handle: &OverlayHandle) -> Option<&mut Overlay> {
682        self.overlays.iter_mut().find(|o| &o.handle == handle)
683    }
684
685    /// Get total number of overlays
686    pub fn len(&self) -> usize {
687        self.overlays.len()
688    }
689
690    /// Check if there are any overlays
691    pub fn is_empty(&self) -> bool {
692        self.overlays.is_empty()
693    }
694
695    /// Get all overlays (for rendering)
696    pub fn all(&self) -> &[Overlay] {
697        &self.overlays
698    }
699
700    /// Test-only: assert `marker_to_idx` is consistent with `overlays`,
701    /// and that priorities are non-decreasing along the vector.
702    /// Panics on any divergence. Used by property tests.
703    #[cfg(test)]
704    fn check_invariants(&self) {
705        assert_eq!(
706            self.marker_to_idx.len(),
707            self.overlays.len() * 2,
708            "marker_to_idx size != 2 * overlays.len()"
709        );
710        for (i, o) in self.overlays.iter().enumerate() {
711            assert_eq!(
712                self.marker_to_idx.get(&o.start_marker).copied(),
713                Some(i),
714                "start_marker {:?} of overlay {} mismapped",
715                o.start_marker,
716                i,
717            );
718            assert_eq!(
719                self.marker_to_idx.get(&o.end_marker).copied(),
720                Some(i),
721                "end_marker {:?} of overlay {} mismapped",
722                o.end_marker,
723                i,
724            );
725        }
726        // Priority order — only enforceable when nothing is mid-cycle.
727        // Tests check this via `assert_priority_sorted` after points
728        // where the invariant is supposed to hold (e.g. after `add`).
729    }
730
731    /// Test-only: assert overlays are non-decreasing by priority.
732    #[cfg(test)]
733    fn assert_priority_sorted(&self) {
734        for w in self.overlays.windows(2) {
735            assert!(
736                w[0].priority <= w[1].priority,
737                "priority order broken: {} after {}",
738                w[1].priority,
739                w[0].priority,
740            );
741        }
742    }
743}
744
745impl Default for OverlayManager {
746    fn default() -> Self {
747        Self::new()
748    }
749}
750
751/// Helper functions for creating common overlay types
752impl Overlay {
753    /// Create an error underline overlay (wavy red line)
754    pub fn error(
755        marker_list: &mut MarkerList,
756        range: Range<usize>,
757        message: Option<String>,
758    ) -> Self {
759        let mut overlay = Self::with_priority(
760            marker_list,
761            range,
762            OverlayFace::Underline {
763                color: Color::Red,
764                style: UnderlineStyle::Wavy,
765            },
766            10, // Higher priority for errors
767        );
768        overlay.message = message;
769        overlay
770    }
771
772    /// Create a warning underline overlay (wavy yellow line)
773    pub fn warning(
774        marker_list: &mut MarkerList,
775        range: Range<usize>,
776        message: Option<String>,
777    ) -> Self {
778        let mut overlay = Self::with_priority(
779            marker_list,
780            range,
781            OverlayFace::Underline {
782                color: Color::Yellow,
783                style: UnderlineStyle::Wavy,
784            },
785            5, // Medium priority for warnings
786        );
787        overlay.message = message;
788        overlay
789    }
790
791    /// Create an info underline overlay (wavy blue line)
792    pub fn info(
793        marker_list: &mut MarkerList,
794        range: Range<usize>,
795        message: Option<String>,
796    ) -> Self {
797        let mut overlay = Self::with_priority(
798            marker_list,
799            range,
800            OverlayFace::Underline {
801                color: Color::Blue,
802                style: UnderlineStyle::Wavy,
803            },
804            3, // Lower priority for info
805        );
806        overlay.message = message;
807        overlay
808    }
809
810    /// Create a hint underline overlay (dotted gray line)
811    pub fn hint(
812        marker_list: &mut MarkerList,
813        range: Range<usize>,
814        message: Option<String>,
815    ) -> Self {
816        let mut overlay = Self::with_priority(
817            marker_list,
818            range,
819            OverlayFace::Underline {
820                color: Color::Gray,
821                style: UnderlineStyle::Dotted,
822            },
823            1, // Lowest priority for hints
824        );
825        overlay.message = message;
826        overlay
827    }
828
829    /// Create a selection highlight overlay
830    pub fn selection(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
831        let mut overlay = Self::with_priority(
832            marker_list,
833            range,
834            OverlayFace::Background {
835                color: Color::Rgb(38, 79, 120), // VSCode-like selection color
836            },
837            -10, // Very low priority so it's under other overlays
838        );
839        overlay.theme_key = Some("editor.selection_bg");
840        overlay
841    }
842
843    /// Create a search result highlight overlay
844    pub fn search_match(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
845        let mut overlay = Self::with_priority(
846            marker_list,
847            range,
848            OverlayFace::Background {
849                color: Color::Rgb(72, 72, 0), // Yellow-ish highlight
850            },
851            -5, // Low priority
852        );
853        overlay.theme_key = Some("search.match_bg");
854        overlay
855    }
856}
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    #[test]
863    fn test_overlay_creation_with_markers() {
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            5..10,
870            OverlayFace::Background { color: Color::Red },
871        );
872
873        assert_eq!(marker_list.get_position(overlay.start_marker), Some(5));
874        assert_eq!(marker_list.get_position(overlay.end_marker), Some(10));
875        assert_eq!(overlay.range(&marker_list), 5..10);
876    }
877
878    #[test]
879    fn test_overlay_adjusts_with_insert() {
880        let mut marker_list = MarkerList::new();
881        marker_list.set_buffer_size(100);
882
883        let overlay = Overlay::new(
884            &mut marker_list,
885            10..20,
886            OverlayFace::Background { color: Color::Red },
887        );
888
889        // Insert before overlay
890        marker_list.adjust_for_insert(5, 10);
891
892        // Overlay should have moved forward
893        assert_eq!(overlay.range(&marker_list), 20..30);
894    }
895
896    #[test]
897    fn test_overlay_adjusts_with_delete() {
898        let mut marker_list = MarkerList::new();
899        marker_list.set_buffer_size(100);
900
901        let overlay = Overlay::new(
902            &mut marker_list,
903            20..30,
904            OverlayFace::Background { color: Color::Red },
905        );
906
907        // Delete before overlay
908        marker_list.adjust_for_delete(5, 10);
909
910        // Overlay should have moved backward
911        assert_eq!(overlay.range(&marker_list), 10..20);
912    }
913
914    #[test]
915    fn test_overlay_manager_add_remove() {
916        let mut marker_list = MarkerList::new();
917        marker_list.set_buffer_size(100);
918        let mut manager = OverlayManager::new();
919
920        let overlay = Overlay::new(
921            &mut marker_list,
922            5..10,
923            OverlayFace::Background { color: Color::Red },
924        );
925
926        let handle = manager.add(overlay);
927        assert_eq!(manager.len(), 1);
928
929        manager.remove_by_handle(&handle, &mut marker_list);
930        assert_eq!(manager.len(), 0);
931    }
932
933    #[test]
934    fn test_overlay_namespace_clear() {
935        let mut marker_list = MarkerList::new();
936        marker_list.set_buffer_size(100);
937        let mut manager = OverlayManager::new();
938
939        let ns = OverlayNamespace::from_string("todo".to_string());
940
941        // Add overlays in namespace
942        let overlay1 = Overlay::with_namespace(
943            &mut marker_list,
944            5..10,
945            OverlayFace::Background { color: Color::Red },
946            ns.clone(),
947        );
948        let overlay2 = Overlay::with_namespace(
949            &mut marker_list,
950            15..20,
951            OverlayFace::Background { color: Color::Blue },
952            ns.clone(),
953        );
954        // Add overlay without namespace
955        let overlay3 = Overlay::new(
956            &mut marker_list,
957            25..30,
958            OverlayFace::Background {
959                color: Color::Green,
960            },
961        );
962
963        manager.add(overlay1);
964        manager.add(overlay2);
965        manager.add(overlay3);
966        assert_eq!(manager.len(), 3);
967
968        // Clear only the namespace
969        manager.clear_namespace(&ns, &mut marker_list);
970        assert_eq!(manager.len(), 1); // Only overlay3 remains
971    }
972
973    #[test]
974    fn test_overlay_priority_sorting() {
975        let mut marker_list = MarkerList::new();
976        marker_list.set_buffer_size(100);
977        let mut manager = OverlayManager::new();
978
979        manager.add(Overlay::with_priority(
980            &mut marker_list,
981            5..10,
982            OverlayFace::Background { color: Color::Red },
983            10,
984        ));
985        manager.add(Overlay::with_priority(
986            &mut marker_list,
987            5..10,
988            OverlayFace::Background { color: Color::Blue },
989            5,
990        ));
991        manager.add(Overlay::with_priority(
992            &mut marker_list,
993            5..10,
994            OverlayFace::Background {
995                color: Color::Green,
996            },
997            15,
998        ));
999
1000        let overlays = manager.at_position(7, &marker_list);
1001        assert_eq!(overlays.len(), 3);
1002        // Should be sorted by priority (low to high)
1003        assert_eq!(overlays[0].priority, 5);
1004        assert_eq!(overlays[1].priority, 10);
1005        assert_eq!(overlays[2].priority, 15);
1006    }
1007
1008    #[test]
1009    fn test_overlay_contains_and_overlaps() {
1010        let mut marker_list = MarkerList::new();
1011        marker_list.set_buffer_size(100);
1012
1013        let overlay = Overlay::new(
1014            &mut marker_list,
1015            10..20,
1016            OverlayFace::Background { color: Color::Red },
1017        );
1018
1019        assert!(!overlay.contains(9, &marker_list));
1020        assert!(overlay.contains(10, &marker_list));
1021        assert!(overlay.contains(15, &marker_list));
1022        assert!(overlay.contains(19, &marker_list));
1023        assert!(!overlay.contains(20, &marker_list));
1024
1025        assert!(!overlay.overlaps(&(0..10), &marker_list));
1026        assert!(overlay.overlaps(&(5..15), &marker_list));
1027        assert!(overlay.overlaps(&(15..25), &marker_list));
1028        assert!(!overlay.overlaps(&(20..30), &marker_list));
1029    }
1030
1031    #[test]
1032    fn test_overlay_remove_in_range_keeps_only_disjoint() {
1033        let mut marker_list = MarkerList::new();
1034        marker_list.set_buffer_size(200);
1035        let mut manager = OverlayManager::new();
1036
1037        manager.add(Overlay::new(
1038            &mut marker_list,
1039            0..5,
1040            OverlayFace::Background { color: Color::Red },
1041        ));
1042        manager.add(Overlay::new(
1043            &mut marker_list,
1044            10..20,
1045            OverlayFace::Background { color: Color::Blue },
1046        ));
1047        manager.add(Overlay::new(
1048            &mut marker_list,
1049            30..40,
1050            OverlayFace::Background {
1051                color: Color::Green,
1052            },
1053        ));
1054        manager.add(Overlay::new(
1055            &mut marker_list,
1056            50..60,
1057            OverlayFace::Background {
1058                color: Color::Yellow,
1059            },
1060        ));
1061
1062        // Range 15..35 overlaps overlays #2 (10..20) and #3 (30..40), leaves #1 and #4.
1063        manager.remove_in_range(&(15..35), &mut marker_list);
1064
1065        let kept: Vec<_> = manager
1066            .all()
1067            .iter()
1068            .map(|o| o.range(&marker_list))
1069            .collect();
1070        assert_eq!(kept, vec![0..5, 50..60]);
1071    }
1072
1073    #[test]
1074    fn test_overlay_remove_in_range_deletes_markers() {
1075        let mut marker_list = MarkerList::new();
1076        marker_list.set_buffer_size(100);
1077        let mut manager = OverlayManager::new();
1078
1079        let overlay = Overlay::new(
1080            &mut marker_list,
1081            10..20,
1082            OverlayFace::Background { color: Color::Red },
1083        );
1084        let start_id = overlay.start_marker;
1085        let end_id = overlay.end_marker;
1086        manager.add(overlay);
1087
1088        manager.remove_in_range(&(0..50), &mut marker_list);
1089
1090        assert_eq!(manager.len(), 0);
1091        assert_eq!(marker_list.get_position(start_id), None);
1092        assert_eq!(marker_list.get_position(end_id), None);
1093    }
1094
1095    #[test]
1096    fn test_overlay_remove_in_range_endpoint_semantics() {
1097        // Touching at a single endpoint must NOT remove (start == range.end or end == range.start).
1098        let mut marker_list = MarkerList::new();
1099        marker_list.set_buffer_size(100);
1100        let mut manager = OverlayManager::new();
1101
1102        manager.add(Overlay::new(
1103            &mut marker_list,
1104            10..20,
1105            OverlayFace::Background { color: Color::Red },
1106        ));
1107
1108        manager.remove_in_range(&(20..30), &mut marker_list);
1109        assert_eq!(manager.len(), 1);
1110        manager.remove_in_range(&(0..10), &mut marker_list);
1111        assert_eq!(manager.len(), 1);
1112        manager.remove_in_range(&(19..21), &mut marker_list);
1113        assert_eq!(manager.len(), 0);
1114    }
1115
1116    /// Mirrors the production cycle: per line in `lines_changed`, clear
1117    /// overlays in the line's byte range, then re-add the line's overlays.
1118    /// Steady-state count holds throughout. Same shape as the matching
1119    /// conceal perf test for direct comparison.
1120    ///
1121    /// Run with:
1122    ///   cargo nextest run -p fresh-editor --no-capture \
1123    ///     view::overlay::tests::perf_full_buffer_rebuild_pass
1124    #[test]
1125    fn perf_full_buffer_rebuild_pass() {
1126        const LINES: usize = 500;
1127        const LINE_BYTES: usize = 50;
1128        const OVERLAYS_PER_LINE: usize = 5;
1129
1130        let mut marker_list = MarkerList::new();
1131        marker_list.set_buffer_size(LINES * LINE_BYTES);
1132        let mut manager = OverlayManager::new();
1133
1134        let overlay_byte = |line: usize, k: usize| -> usize {
1135            line * LINE_BYTES + k * (LINE_BYTES / OVERLAYS_PER_LINE)
1136        };
1137        let make_overlay = |ml: &mut MarkerList, line: usize, k: usize| {
1138            let s = overlay_byte(line, k);
1139            Overlay::new(
1140                ml,
1141                s..(s + 2),
1142                OverlayFace::Background { color: Color::Red },
1143            )
1144        };
1145
1146        // Populate steady state.
1147        for line in 0..LINES {
1148            for k in 0..OVERLAYS_PER_LINE {
1149                let o = make_overlay(&mut marker_list, line, k);
1150                manager.add(o);
1151            }
1152        }
1153        let initial = LINES * OVERLAYS_PER_LINE;
1154
1155        // One full-buffer `lines_changed` pass: per line, clear + re-add.
1156        let start = std::time::Instant::now();
1157        for line in 0..LINES {
1158            let line_range = (line * LINE_BYTES)..((line + 1) * LINE_BYTES);
1159            manager.remove_in_range(&line_range, &mut marker_list);
1160            for k in 0..OVERLAYS_PER_LINE {
1161                let o = make_overlay(&mut marker_list, line, k);
1162                manager.add(o);
1163            }
1164        }
1165        let elapsed = start.elapsed();
1166
1167        eprintln!(
1168            "[perf] overlay full-buffer rebuild ({LINES} lines, {} entries steady): \
1169             {:?} total, {:?}/line",
1170            initial,
1171            elapsed,
1172            elapsed / LINES as u32,
1173        );
1174        assert_eq!(manager.len(), initial);
1175    }
1176
1177    mod proptests {
1178        use super::*;
1179        use proptest::prelude::*;
1180
1181        #[derive(Debug, Clone)]
1182        enum Op {
1183            Add {
1184                start: usize,
1185                len: usize,
1186                priority: i32,
1187                ns_idx: u8,
1188            },
1189            RemoveInRange {
1190                start: usize,
1191                end: usize,
1192            },
1193            ClearNamespace {
1194                ns_idx: u8,
1195            },
1196            ReplaceRange {
1197                start: usize,
1198                end: usize,
1199                ns_idx: u8,
1200                /// New overlays to insert in the same range; same shape
1201                /// as `Add` but len capped to satisfy precondition.
1202                new_overlays: Vec<(usize, usize, i32)>,
1203            },
1204        }
1205
1206        const BUFFER_SIZE: usize = 200;
1207        const MAX_OVERLAY_LEN: usize = 4;
1208        const MIN_QUERY_LEN: usize = MAX_OVERLAY_LEN + 1;
1209
1210        fn arb_overlay_spec() -> impl Strategy<Value = (usize, usize, i32)> {
1211            (
1212                0..(BUFFER_SIZE - MAX_OVERLAY_LEN),
1213                1..=MAX_OVERLAY_LEN,
1214                -5i32..=5i32,
1215            )
1216        }
1217
1218        fn arb_op() -> impl Strategy<Value = Op> {
1219            prop_oneof![
1220                3 => arb_overlay_spec().prop_flat_map(|(start, len, priority)| {
1221                    (Just(start), Just(len), Just(priority), 0u8..3u8)
1222                }).prop_map(|(start, len, priority, ns_idx)| Op::Add {
1223                    start, len, priority, ns_idx,
1224                }),
1225                2 => (0..BUFFER_SIZE, MIN_QUERY_LEN..=BUFFER_SIZE)
1226                    .prop_map(|(start, qlen)| {
1227                        let s = start.min(BUFFER_SIZE - 1);
1228                        let e = (s + qlen).min(BUFFER_SIZE);
1229                        Op::RemoveInRange { start: s, end: e }
1230                    }),
1231                1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
1232                1 => (
1233                    0..BUFFER_SIZE,
1234                    MIN_QUERY_LEN..=BUFFER_SIZE,
1235                    0u8..3u8,
1236                    prop::collection::vec(arb_overlay_spec(), 0..4),
1237                )
1238                    .prop_map(|(start, qlen, ns_idx, new_overlays)| {
1239                        let s = start.min(BUFFER_SIZE - 1);
1240                        let e = (s + qlen).min(BUFFER_SIZE);
1241                        Op::ReplaceRange { start: s, end: e, ns_idx, new_overlays }
1242                    }),
1243            ]
1244        }
1245
1246        fn nsf(idx: u8) -> OverlayNamespace {
1247            OverlayNamespace::from_string(format!("ns{idx}"))
1248        }
1249
1250        proptest! {
1251            /// Invariants must hold after every sequence of operations.
1252            /// Plus: after `remove_in_range(r)`, no surviving overlay's
1253            /// range overlaps `r`. Plus: after `add` / `extend` /
1254            /// `clear_namespace` / `replace_range_in_namespace`, the
1255            /// vector is sorted by priority. Note: priority order may be
1256            /// transiently broken right after `remove_in_range` until the
1257            /// next `add` — production callers always pair these.
1258            #[test]
1259            fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..30)) {
1260                let mut marker_list = MarkerList::new();
1261                marker_list.set_buffer_size(BUFFER_SIZE);
1262                let mut manager = OverlayManager::new();
1263
1264                for op in ops {
1265                    match op {
1266                        Op::Add { start, len, priority, ns_idx } => {
1267                            let o = Overlay::with_namespace(
1268                                &mut marker_list,
1269                                start..(start + len),
1270                                OverlayFace::Background { color: Color::Red },
1271                                nsf(ns_idx),
1272                            );
1273                            let mut o = o;
1274                            o.priority = priority;
1275                            manager.add(o);
1276                            manager.check_invariants();
1277                            manager.assert_priority_sorted();
1278                        }
1279                        Op::RemoveInRange { start, end } => {
1280                            manager.remove_in_range(&(start..end), &mut marker_list);
1281                            for (o, rng) in manager.query_viewport(start, end, &marker_list) {
1282                                let overlaps = rng.start < end && start < rng.end;
1283                                prop_assert!(
1284                                    !overlaps,
1285                                    "overlay {:?} (handle {:?}) survived remove_in_range({start}..{end})",
1286                                    rng, o.handle,
1287                                );
1288                            }
1289                            manager.check_invariants();
1290                        }
1291                        Op::ClearNamespace { ns_idx } => {
1292                            manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
1293                            manager.check_invariants();
1294                            manager.assert_priority_sorted();
1295                        }
1296                        Op::ReplaceRange { start, end, ns_idx, new_overlays } => {
1297                            let new: Vec<Overlay> = new_overlays.into_iter().map(|(s, l, p)| {
1298                                let mut o = Overlay::with_namespace(
1299                                    &mut marker_list,
1300                                    s..(s + l),
1301                                    OverlayFace::Background { color: Color::Blue },
1302                                    nsf(ns_idx),
1303                                );
1304                                o.priority = p;
1305                                o
1306                            }).collect();
1307                            manager.replace_range_in_namespace(
1308                                &nsf(ns_idx),
1309                                &(start..end),
1310                                new,
1311                                &mut marker_list,
1312                            );
1313                            manager.check_invariants();
1314                            manager.assert_priority_sorted();
1315                        }
1316                    }
1317                }
1318            }
1319        }
1320    }
1321}