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