Skip to main content

editor_core/
intervals.rs

1//! Phase 4: Styles and Folding (Intervals & Visibility)
2//!
3//! Uses Interval Tree to manage style metadata and code folding.
4
5/// Style ID type
6pub type StyleId = u32;
7
8/// Built-in style id used for folding placeholder text (e.g. `/*...*/`, `use ...`).
9///
10/// Consumers should map this to a muted style.
11pub const FOLD_PLACEHOLDER_STYLE_ID: StyleId = 0x0300_0001;
12
13/// Built-in style id for LSP `textDocument/documentHighlight` (kind: Text/unspecified).
14pub const DOCUMENT_HIGHLIGHT_TEXT_STYLE_ID: StyleId = 0x0400_0001;
15/// Built-in style id for LSP `textDocument/documentHighlight` (kind: Read).
16pub const DOCUMENT_HIGHLIGHT_READ_STYLE_ID: StyleId = 0x0400_0002;
17/// Built-in style id for LSP `textDocument/documentHighlight` (kind: Write).
18pub const DOCUMENT_HIGHLIGHT_WRITE_STYLE_ID: StyleId = 0x0400_0003;
19
20/// Style layer ID
21///
22/// Used to distinguish style sources (e.g., LSP semantic highlighting, simple syntax highlighting, diagnostics, etc.),
23/// allowing replacement/clearing of one layer without affecting other style layers.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct StyleLayerId(pub u32);
26
27impl StyleLayerId {
28    /// Create a style layer id from a raw numeric identifier.
29    pub const fn new(id: u32) -> Self {
30        Self(id)
31    }
32
33    /// LSP `semanticTokens` style layer (recommended for semantic highlighting).
34    pub const SEMANTIC_TOKENS: Self = Self(1);
35
36    /// Simple syntax highlighting style layer (e.g., regex-based JSON/INI highlighting).
37    pub const SIMPLE_SYNTAX: Self = Self(2);
38
39    /// Sublime Text `.sublime-syntax` style layer (lightweight syntax highlighting/folding).
40    pub const SUBLIME_SYNTAX: Self = Self(3);
41
42    /// LSP diagnostics overlay layer.
43    ///
44    /// This is intended for underlines / gutter markers sourced from LSP diagnostics.
45    pub const DIAGNOSTICS: Self = Self(4);
46
47    /// LSP `textDocument/documentHighlight` overlay layer.
48    pub const DOCUMENT_HIGHLIGHTS: Self = Self(5);
49
50    /// Tree-sitter syntax highlighting style layer.
51    pub const TREE_SITTER: Self = Self(6);
52}
53
54/// Interval structure
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Interval {
57    /// Start offset (bytes or characters, depending on usage scenario)
58    pub start: usize,
59    /// End offset (exclusive)
60    pub end: usize,
61    /// Style ID
62    pub style_id: StyleId,
63}
64
65impl Interval {
66    /// Create a new interval with `[start, end)` offsets and a style id.
67    pub fn new(start: usize, end: usize, style_id: StyleId) -> Self {
68        Self {
69            start,
70            end,
71            style_id,
72        }
73    }
74
75    /// Check if interval contains a specific position
76    pub fn contains(&self, pos: usize) -> bool {
77        self.start <= pos && pos < self.end
78    }
79
80    /// Check if two intervals overlap
81    pub fn overlaps(&self, other: &Interval) -> bool {
82        self.start < other.end && other.start < self.end
83    }
84}
85
86/// Interval tree - manages style intervals
87///
88/// Uses a sorted vector with binary search for efficient interval queries.
89/// Query complexity: O(log n + k), where k is the number of overlapping intervals.
90/// Insertion complexity: O(n) (requires maintaining sort order).
91pub struct IntervalTree {
92    /// List of intervals (kept sorted by start position)
93    intervals: Vec<Interval>,
94    /// Prefix maximum end position: `prefix_max_end[i] = max(intervals[0..=i].end)`
95    ///
96    /// Used for early pruning in `query_point` / `query_range`, avoiding degradation to O(n) scan when there are many style intervals.
97    prefix_max_end: Vec<usize>,
98}
99
100impl IntervalTree {
101    /// Create an empty interval tree.
102    pub fn new() -> Self {
103        Self {
104            intervals: Vec::new(),
105            prefix_max_end: Vec::new(),
106        }
107    }
108
109    fn rebuild_prefix_max_end_from(&mut self, start_idx: usize) {
110        if self.intervals.is_empty() {
111            self.prefix_max_end.clear();
112            return;
113        }
114
115        if self.prefix_max_end.len() != self.intervals.len() {
116            self.prefix_max_end.resize(self.intervals.len(), 0);
117        }
118
119        let mut max_end = if start_idx == 0 {
120            0
121        } else {
122            self.prefix_max_end[start_idx - 1]
123        };
124
125        for (idx, interval) in self.intervals.iter().enumerate().skip(start_idx) {
126            max_end = max_end.max(interval.end);
127            self.prefix_max_end[idx] = max_end;
128        }
129    }
130
131    fn rebuild_prefix_max_end(&mut self) {
132        self.rebuild_prefix_max_end_from(0);
133    }
134
135    /// Insert an interval
136    pub fn insert(&mut self, interval: Interval) {
137        // Find insertion position (maintaining sort order)
138        let pos = self
139            .intervals
140            .binary_search_by_key(&interval.start, |i| i.start)
141            .unwrap_or_else(|pos| pos);
142
143        self.intervals.insert(pos, interval);
144        self.prefix_max_end.insert(pos, 0);
145        self.rebuild_prefix_max_end_from(pos);
146    }
147
148    /// Remove interval that exactly matches the specified interval
149    pub fn remove(&mut self, start: usize, end: usize, style_id: StyleId) -> bool {
150        if let Some(pos) = self
151            .intervals
152            .iter()
153            .position(|i| i.start == start && i.end == end && i.style_id == style_id)
154        {
155            self.intervals.remove(pos);
156            self.prefix_max_end.remove(pos);
157            if pos < self.intervals.len() {
158                self.rebuild_prefix_max_end_from(pos);
159            }
160            true
161        } else {
162            false
163        }
164    }
165
166    /// Query all intervals containing a specific position
167    /// Optimized version: uses binary search to locate interval range that may contain pos
168    pub fn query_point(&self, pos: usize) -> Vec<&Interval> {
169        self.query_point_impl(pos).0
170    }
171
172    fn query_point_impl(&self, pos: usize) -> (Vec<&Interval>, usize) {
173        if self.intervals.is_empty() {
174            return (Vec::new(), 0);
175        }
176
177        let mut result = Vec::new();
178        let mut scanned = 0usize;
179
180        // Use binary search to find first position where start > pos
181        let search_key = pos.saturating_add(1);
182        let idx = match self
183            .intervals
184            .binary_search_by_key(&search_key, |i| i.start)
185        {
186            Ok(idx) => idx,
187            Err(idx) => idx,
188        };
189
190        // Starting from idx-1, check backward for all intervals that may contain pos
191        // Because intervals are sorted by start, all intervals with start <= pos are before idx
192        for i in (0..idx).rev() {
193            scanned = scanned.saturating_add(1);
194
195            // If maximum end of `intervals[0..=i]` is <= pos, earlier intervals cannot contain pos.
196            if self.prefix_max_end[i] <= pos {
197                break;
198            }
199
200            let interval = &self.intervals[i];
201            if interval.contains(pos) {
202                result.push(interval);
203            }
204        }
205
206        (result, scanned)
207    }
208
209    #[cfg(test)]
210    fn query_point_scan_count(&self, pos: usize) -> usize {
211        self.query_point_impl(pos).1
212    }
213
214    /// Query all intervals overlapping with specified range
215    /// Optimized version: uses binary search to locate interval range that may overlap
216    pub fn query_range(&self, start: usize, end: usize) -> Vec<&Interval> {
217        if self.intervals.is_empty() || start >= end {
218            return Vec::new();
219        }
220
221        let mut result = Vec::new();
222
223        // Use binary search to find first position where interval.start >= end
224        // All intervals that may overlap are before this position
225        let search_end = match self.intervals.binary_search_by_key(&end, |i| i.start) {
226            Ok(idx) => idx,
227            Err(idx) => idx,
228        };
229
230        if search_end == 0 {
231            return result;
232        }
233
234        // First find position where start >= start, then expand backward,
235        // until `prefix_max_end` indicates earlier intervals cannot cross start.
236        let mut scan_start = match self.intervals.binary_search_by_key(&start, |i| i.start) {
237            Ok(idx) | Err(idx) => idx.min(search_end),
238        };
239
240        while scan_start > 0 && self.prefix_max_end[scan_start - 1] > start {
241            scan_start -= 1;
242        }
243
244        for interval in self.intervals[scan_start..search_end].iter() {
245            if interval.start < end && interval.end > start {
246                result.push(interval);
247            }
248        }
249
250        result
251    }
252
253    /// Clear all intervals
254    pub fn clear(&mut self) {
255        self.intervals.clear();
256        self.prefix_max_end.clear();
257    }
258
259    /// Get number of intervals
260    pub fn len(&self) -> usize {
261        self.intervals.len()
262    }
263
264    /// Check if empty
265    pub fn is_empty(&self) -> bool {
266        self.intervals.is_empty()
267    }
268
269    /// Update offsets (when text changes)
270    ///
271    /// Call this method to update all intervals when inserting text of `delta` length at position `pos`
272    pub fn update_for_insertion(&mut self, pos: usize, delta: usize) {
273        for interval in &mut self.intervals {
274            if interval.start >= pos {
275                interval.start += delta;
276                interval.end += delta;
277            } else if interval.end > pos {
278                // Interval spans insertion point, extend end position
279                interval.end += delta;
280            }
281        }
282        self.rebuild_prefix_max_end();
283    }
284
285    /// Update offsets (when text is deleted)
286    ///
287    /// Call this method to update all intervals when deleting text in range `[start, end)`
288    pub fn update_for_deletion(&mut self, start: usize, end: usize) {
289        let delta = end - start;
290        let mut to_remove = Vec::new();
291
292        for (idx, interval) in self.intervals.iter_mut().enumerate() {
293            if interval.end <= start {
294                // Interval is before deletion range, unaffected
295                continue;
296            } else if interval.start >= end {
297                // Interval is after deletion range, move forward
298                interval.start -= delta;
299                interval.end -= delta;
300            } else if interval.start >= start && interval.end <= end {
301                // Interval is completely within deletion range, mark for removal
302                to_remove.push(idx);
303            } else if interval.start < start && interval.end > end {
304                // Interval spans deletion range, shrink
305                interval.end -= delta;
306            } else if interval.start < start {
307                // Interval partially in deletion range (end part)
308                interval.end = start;
309            } else {
310                // Interval partially in deletion range (start part)
311                interval.start = start;
312                interval.end -= delta;
313            }
314        }
315
316        // Remove completely deleted intervals
317        for idx in to_remove.into_iter().rev() {
318            self.intervals.remove(idx);
319        }
320
321        self.rebuild_prefix_max_end();
322    }
323}
324
325impl Default for IntervalTree {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331/// Fold region
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct FoldRegion {
334    /// Start line number
335    pub start_line: usize,
336    /// End line number (inclusive)
337    pub end_line: usize,
338    /// Whether folded
339    pub is_collapsed: bool,
340    /// Placeholder text shown when folded (e.g., "[...]")
341    pub placeholder: String,
342}
343
344impl FoldRegion {
345    /// Create a folding region for an inclusive line range.
346    pub fn new(start_line: usize, end_line: usize) -> Self {
347        Self {
348            start_line,
349            end_line,
350            is_collapsed: false,
351            placeholder: String::from("[...]"),
352        }
353    }
354
355    /// Create a folding region with a custom placeholder string.
356    pub fn with_placeholder(start_line: usize, end_line: usize, placeholder: String) -> Self {
357        Self {
358            start_line,
359            end_line,
360            is_collapsed: false,
361            placeholder,
362        }
363    }
364
365    /// Expand
366    pub fn expand(&mut self) {
367        self.is_collapsed = false;
368    }
369
370    /// Collapse
371    pub fn collapse(&mut self) {
372        self.is_collapsed = true;
373    }
374
375    /// Toggle fold state
376    pub fn toggle(&mut self) {
377        self.is_collapsed = !self.is_collapsed;
378    }
379
380    /// Check if line number is within fold region
381    pub fn contains_line(&self, line: usize) -> bool {
382        line >= self.start_line && line <= self.end_line
383    }
384}
385
386/// Folding manager
387pub struct FoldingManager {
388    /// Fold regions sourced from external/derived providers (LSP, sublime syntax, etc.).
389    derived_regions: Vec<FoldRegion>,
390    /// Fold regions created explicitly by the user (via commands).
391    user_regions: Vec<FoldRegion>,
392    /// Cached merged view (sorted/deduplicated) used for rendering and coordinate mapping.
393    merged_regions: Vec<FoldRegion>,
394}
395
396impl FoldingManager {
397    /// Create an empty folding manager.
398    pub fn new() -> Self {
399        Self {
400            derived_regions: Vec::new(),
401            user_regions: Vec::new(),
402            merged_regions: Vec::new(),
403        }
404    }
405
406    fn rebuild_merged_regions(&mut self) {
407        self.merged_regions.clear();
408        self.merged_regions
409            .extend(self.derived_regions.iter().cloned());
410        self.merged_regions
411            .extend(self.user_regions.iter().cloned());
412
413        self.merged_regions
414            .sort_by_key(|r| (r.start_line, r.end_line));
415        self.merged_regions
416            .dedup_by(|a, b| a.start_line == b.start_line && a.end_line == b.end_line);
417    }
418
419    fn normalize_regions(regions: &mut Vec<FoldRegion>) {
420        regions.sort_by_key(|r| (r.start_line, r.end_line));
421        regions.dedup_by(|a, b| a.start_line == b.start_line && a.end_line == b.end_line);
422        regions.retain(|r| r.end_line > r.start_line);
423    }
424
425    fn clamp_regions(regions: &mut Vec<FoldRegion>, max_line: usize) {
426        for r in regions.iter_mut() {
427            r.start_line = r.start_line.min(max_line);
428            r.end_line = r.end_line.min(max_line);
429        }
430        Self::normalize_regions(regions);
431    }
432
433    /// Add a user-created fold region.
434    pub fn add_region(&mut self, region: FoldRegion) {
435        // Keep sorted by start line.
436        let pos = self
437            .user_regions
438            .binary_search_by_key(&region.start_line, |r| r.start_line)
439            .unwrap_or_else(|pos| pos);
440
441        self.user_regions.insert(pos, region);
442        Self::normalize_regions(&mut self.user_regions);
443        self.rebuild_merged_regions();
444    }
445
446    /// Remove a user-created fold region.
447    pub fn remove_region(&mut self, start_line: usize, end_line: usize) -> bool {
448        if let Some(pos) = self
449            .user_regions
450            .iter()
451            .position(|r| r.start_line == start_line && r.end_line == end_line)
452        {
453            self.user_regions.remove(pos);
454            self.rebuild_merged_regions();
455            true
456        } else {
457            false
458        }
459    }
460
461    /// Get fold region containing specified line (merged view).
462    pub fn get_region_for_line(&self, line: usize) -> Option<&FoldRegion> {
463        self.merged_regions.iter().find(|r| r.contains_line(line))
464    }
465
466    /// Get mutable reference to a fold region containing specified line (prefers user folds).
467    pub fn get_region_for_line_mut(&mut self, line: usize) -> Option<&mut FoldRegion> {
468        if let Some(region) = self.user_regions.iter_mut().find(|r| r.contains_line(line)) {
469            return Some(region);
470        }
471        self.derived_regions
472            .iter_mut()
473            .find(|r| r.contains_line(line))
474    }
475
476    /// Collapse specified line
477    pub fn collapse_line(&mut self, line: usize) -> bool {
478        if let Some(region) = self.get_region_for_line_mut(line) {
479            region.collapse();
480            self.rebuild_merged_regions();
481            true
482        } else {
483            false
484        }
485    }
486
487    /// Expand specified line
488    pub fn expand_line(&mut self, line: usize) -> bool {
489        if let Some(region) = self.get_region_for_line_mut(line) {
490            region.expand();
491            self.rebuild_merged_regions();
492            true
493        } else {
494            false
495        }
496    }
497
498    /// Toggle fold state of specified line
499    pub fn toggle_line(&mut self, line: usize) -> bool {
500        if let Some(region) = self.get_region_for_line_mut(line) {
501            region.toggle();
502            self.rebuild_merged_regions();
503            true
504        } else {
505            false
506        }
507    }
508
509    /// Toggle fold region starting at specified line (preferring "innermost" region).
510    ///
511    /// `rust-analyzer` / LSP folding ranges often contain nested regions. To make TUI and other frontends
512    /// behave more intuitively when "cursor is on a start line", we choose:
513    /// - Among all regions with `start_line == line`, the one with smallest `end_line` (innermost)
514    pub fn toggle_region_starting_at_line(&mut self, start_line: usize) -> bool {
515        if self.merged_regions.is_empty() {
516            return false;
517        }
518
519        // Find the innermost region among both sources, preferring user folds on ties.
520        let mut best_source = None::<(bool, usize)>; // (is_user, index)
521        let mut best_end = usize::MAX;
522
523        for (is_user, regions) in [
524            (true, &mut self.user_regions),
525            (false, &mut self.derived_regions),
526        ] {
527            let Ok(mut idx) = regions.binary_search_by_key(&start_line, |r| r.start_line) else {
528                continue;
529            };
530
531            while idx > 0 && regions[idx - 1].start_line == start_line {
532                idx -= 1;
533            }
534
535            for (i, region) in regions[idx..].iter().enumerate() {
536                if region.start_line != start_line {
537                    break;
538                }
539                if region.end_line <= region.start_line {
540                    continue;
541                }
542                if region.end_line < best_end
543                    || (region.end_line == best_end
544                        && best_source.is_some_and(|(prev_is_user, _)| !prev_is_user && is_user))
545                {
546                    best_end = region.end_line;
547                    best_source = Some((is_user, i));
548                }
549            }
550        }
551
552        let Some((is_user, idx)) = best_source else {
553            return false;
554        };
555
556        if is_user {
557            if let Some(region) = self.user_regions.get_mut(idx) {
558                region.toggle();
559            }
560        } else if let Some(region) = self.derived_regions.get_mut(idx) {
561            region.toggle();
562        }
563
564        self.rebuild_merged_regions();
565        true
566    }
567
568    /// Calculate mapping from logical line to visual line
569    ///
570    /// Returns the visual line number for the specified logical line number, or None if folded
571    pub fn logical_to_visual(&self, logical_line: usize, base_visual: usize) -> Option<usize> {
572        let mut hidden_lines = 0;
573
574        for region in &self.merged_regions {
575            if region.is_collapsed {
576                if logical_line > region.start_line && logical_line <= region.end_line {
577                    // This line is folded
578                    return None;
579                } else if logical_line > region.end_line {
580                    // This fold region is before the target line, count hidden lines
581                    hidden_lines += region.end_line - region.start_line;
582                }
583            }
584        }
585
586        Some(base_visual + logical_line - hidden_lines)
587    }
588
589    /// Calculate mapping from visual line to logical line
590    pub fn visual_to_logical(&self, visual_line: usize, base_visual: usize) -> usize {
591        let mut logical = visual_line - base_visual;
592
593        for region in &self.merged_regions {
594            if region.is_collapsed {
595                let hidden_lines = region.end_line - region.start_line;
596
597                if logical == region.start_line {
598                    // Visual line is exactly the fold start line
599                    return region.start_line;
600                } else if logical > region.start_line {
601                    // Visual line is after fold region, need to add hidden lines
602                    logical += hidden_lines;
603                }
604            }
605        }
606
607        logical
608    }
609
610    /// Get all fold regions
611    pub fn regions(&self) -> &[FoldRegion] {
612        &self.merged_regions
613    }
614
615    /// Get all derived fold regions.
616    pub fn derived_regions(&self) -> &[FoldRegion] {
617        &self.derived_regions
618    }
619
620    /// Get all user-created fold regions.
621    pub fn user_regions(&self) -> &[FoldRegion] {
622        &self.user_regions
623    }
624
625    /// Clear all fold regions (derived + user).
626    pub fn clear(&mut self) {
627        self.derived_regions.clear();
628        self.user_regions.clear();
629        self.merged_regions.clear();
630    }
631
632    /// Clear all derived fold regions, leaving user folds intact.
633    pub fn clear_derived_regions(&mut self) {
634        self.derived_regions.clear();
635        self.rebuild_merged_regions();
636    }
637
638    /// Replace derived fold regions (will be sorted by start line and deduplicated).
639    pub fn replace_derived_regions(&mut self, mut regions: Vec<FoldRegion>) {
640        Self::normalize_regions(&mut regions);
641        self.derived_regions = regions;
642        self.rebuild_merged_regions();
643    }
644
645    /// Replace fold regions with new list (legacy API).
646    ///
647    /// This replaces *derived* fold regions, leaving user folds intact.
648    pub fn replace_regions(&mut self, regions: Vec<FoldRegion>) {
649        self.replace_derived_regions(regions);
650    }
651
652    /// Expand all folds
653    pub fn expand_all(&mut self) {
654        for region in &mut self.derived_regions {
655            region.expand();
656        }
657        for region in &mut self.user_regions {
658            region.expand();
659        }
660        self.rebuild_merged_regions();
661    }
662
663    /// Collapse all regions
664    pub fn collapse_all(&mut self) {
665        for region in &mut self.derived_regions {
666            region.collapse();
667        }
668        for region in &mut self.user_regions {
669            region.collapse();
670        }
671        self.rebuild_merged_regions();
672    }
673
674    /// Update fold regions to account for an edit that changes the number of logical lines.
675    ///
676    /// This is intended to keep **user folds** stable across newline insertions/deletions.
677    ///
678    /// - `edit_line` is the logical line where the edit occurred (pre-edit).
679    /// - `line_delta` is the net change in line count (`+n` for inserted newlines, `-n` for deleted).
680    pub fn apply_line_delta(&mut self, edit_line: usize, line_delta: isize) {
681        if line_delta == 0 {
682            return;
683        }
684
685        let apply = |regions: &mut Vec<FoldRegion>| {
686            for region in regions.iter_mut() {
687                if edit_line <= region.start_line {
688                    let start = region.start_line as isize + line_delta;
689                    let end = region.end_line as isize + line_delta;
690                    region.start_line = start.max(0) as usize;
691                    region.end_line = end.max(0) as usize;
692                } else if edit_line <= region.end_line {
693                    let end = region.end_line as isize + line_delta;
694                    region.end_line = end.max(region.start_line as isize) as usize;
695                }
696            }
697        };
698
699        apply(&mut self.derived_regions);
700        apply(&mut self.user_regions);
701    }
702
703    /// Clamp fold regions to the given `line_count` after a text edit, dropping invalid regions.
704    pub fn clamp_to_line_count(&mut self, line_count: usize) {
705        let max_line = line_count.saturating_sub(1);
706        Self::clamp_regions(&mut self.derived_regions, max_line);
707        Self::clamp_regions(&mut self.user_regions, max_line);
708        self.rebuild_merged_regions();
709    }
710}
711
712impl Default for FoldingManager {
713    fn default() -> Self {
714        Self::new()
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[test]
723    fn test_interval_contains() {
724        let interval = Interval::new(10, 20, 1);
725        assert!(interval.contains(10));
726        assert!(interval.contains(15));
727        assert!(interval.contains(19));
728        assert!(!interval.contains(20));
729        assert!(!interval.contains(9));
730    }
731
732    #[test]
733    fn test_interval_overlaps() {
734        let i1 = Interval::new(10, 20, 1);
735        let i2 = Interval::new(15, 25, 2);
736        let i3 = Interval::new(25, 30, 3);
737
738        assert!(i1.overlaps(&i2));
739        assert!(i2.overlaps(&i1));
740        assert!(!i1.overlaps(&i3));
741        assert!(!i3.overlaps(&i1));
742    }
743
744    #[test]
745    fn test_interval_tree_insert() {
746        let mut tree = IntervalTree::new();
747        tree.insert(Interval::new(10, 20, 1));
748        tree.insert(Interval::new(5, 15, 2));
749        tree.insert(Interval::new(15, 25, 3));
750
751        assert_eq!(tree.len(), 3);
752    }
753
754    #[test]
755    fn test_interval_tree_query_point() {
756        let mut tree = IntervalTree::new();
757        tree.insert(Interval::new(10, 20, 1));
758        tree.insert(Interval::new(5, 15, 2));
759        tree.insert(Interval::new(15, 25, 3));
760
761        let results = tree.query_point(12);
762        assert_eq!(results.len(), 2); // intervals 1 and 2
763
764        let results = tree.query_point(18);
765        assert_eq!(results.len(), 2); // intervals 1 and 3
766    }
767
768    #[test]
769    fn test_interval_tree_query_point_prunes_scan() {
770        let mut tree = IntervalTree::new();
771
772        // Construct many non-overlapping intervals: ideally point query should only need to check few candidate intervals.
773        for i in 0..10_000usize {
774            let start = i * 2;
775            tree.insert(Interval::new(start, start + 1, 1));
776        }
777
778        let pos = 2 * 10_000 - 2; // Falls within last interval
779        let results = tree.query_point(pos);
780        assert_eq!(results.len(), 1);
781
782        // After pruning with prefix_max_end, should avoid degrading to linear scan of all intervals.
783        assert!(
784            tree.query_point_scan_count(pos) <= 4,
785            "scan should be pruned for disjoint intervals"
786        );
787    }
788
789    #[test]
790    fn test_interval_tree_query_range() {
791        let mut tree = IntervalTree::new();
792        tree.insert(Interval::new(10, 20, 1));
793        tree.insert(Interval::new(25, 35, 2));
794        tree.insert(Interval::new(40, 50, 3));
795
796        let results = tree.query_range(15, 30);
797        assert_eq!(results.len(), 2); // intervals 1 and 2
798
799        let results = tree.query_range(0, 60);
800        assert_eq!(results.len(), 3); // all intervals
801    }
802
803    #[test]
804    fn test_interval_tree_update_insertion() {
805        let mut tree = IntervalTree::new();
806        tree.insert(Interval::new(10, 20, 1));
807        tree.insert(Interval::new(30, 40, 2));
808
809        tree.update_for_insertion(15, 5);
810
811        assert_eq!(tree.intervals[0].start, 10);
812        assert_eq!(tree.intervals[0].end, 25); // 20 + 5
813
814        assert_eq!(tree.intervals[1].start, 35); // 30 + 5
815        assert_eq!(tree.intervals[1].end, 45); // 40 + 5
816    }
817
818    #[test]
819    fn test_interval_tree_update_deletion() {
820        let mut tree = IntervalTree::new();
821        tree.insert(Interval::new(10, 20, 1));
822        tree.insert(Interval::new(30, 40, 2));
823        tree.insert(Interval::new(50, 60, 3));
824
825        tree.update_for_deletion(25, 35);
826
827        assert_eq!(tree.intervals[0].start, 10);
828        assert_eq!(tree.intervals[0].end, 20); // Unaffected
829
830        assert_eq!(tree.intervals[1].start, 25); // 30 - (35-25)
831        assert_eq!(tree.intervals[1].end, 30); // 40 - (35-25)
832
833        assert_eq!(tree.intervals[2].start, 40); // 50 - 10
834        assert_eq!(tree.intervals[2].end, 50); // 60 - 10
835    }
836
837    #[test]
838    fn test_fold_region() {
839        let mut region = FoldRegion::new(5, 10);
840        assert!(!region.is_collapsed);
841
842        region.collapse();
843        assert!(region.is_collapsed);
844
845        region.expand();
846        assert!(!region.is_collapsed);
847
848        region.toggle();
849        assert!(region.is_collapsed);
850    }
851
852    #[test]
853    fn test_folding_manager() {
854        let mut manager = FoldingManager::new();
855
856        manager.add_region(FoldRegion::new(5, 10));
857        manager.add_region(FoldRegion::new(15, 20));
858
859        assert!(manager.collapse_line(7));
860        assert!(manager.get_region_for_line(7).unwrap().is_collapsed);
861
862        assert!(manager.expand_line(7));
863        assert!(!manager.get_region_for_line(7).unwrap().is_collapsed);
864    }
865
866    #[test]
867    fn test_logical_to_visual_with_folding() {
868        let mut manager = FoldingManager::new();
869
870        let mut region = FoldRegion::new(5, 10);
871        region.collapse();
872        manager.add_region(region);
873
874        // Line before fold
875        assert_eq!(manager.logical_to_visual(3, 0), Some(3));
876
877        // Fold start line
878        assert_eq!(manager.logical_to_visual(5, 0), Some(5));
879
880        // Middle line of fold should return None
881        assert_eq!(manager.logical_to_visual(7, 0), None);
882
883        // Line after fold should have adjusted position
884        assert_eq!(manager.logical_to_visual(15, 0), Some(10)); // 15 - 5 hidden lines
885    }
886
887    #[test]
888    fn test_multiple_overlapping_styles() {
889        let mut tree = IntervalTree::new();
890
891        // Add overlapping style intervals
892        tree.insert(Interval::new(0, 100, 1)); // Syntax highlighting
893        tree.insert(Interval::new(20, 30, 2)); // Search highlighting
894        tree.insert(Interval::new(25, 35, 3)); // Selection region
895
896        // Query position 27, should have 3 styles
897        let styles = tree.query_point(27);
898        assert_eq!(styles.len(), 3);
899
900        // Verify all styles were found
901        let style_ids: Vec<StyleId> = styles.iter().map(|i| i.style_id).collect();
902        assert!(style_ids.contains(&1));
903        assert!(style_ids.contains(&2));
904        assert!(style_ids.contains(&3));
905    }
906}