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