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/// Built-in style id for IME marked text (composition / preedit).
21///
22/// This is intended for underline/background styling of inline IME preedit strings.
23pub const IME_MARKED_TEXT_STYLE_ID: StyleId = 0x0700_0001;
24
25/// Built-in style id for LSP inlay hints virtual text (`textDocument/inlayHint`).
26///
27/// Renderers should map this to a muted / secondary foreground, and optionally a subtle background.
28pub const INLAY_HINT_STYLE_ID: StyleId = 0x0800_0001;
29
30/// Built-in style id for LSP code lens virtual text (`textDocument/codeLens`).
31///
32/// Renderers should map this to a muted / link-like style.
33pub const CODE_LENS_STYLE_ID: StyleId = 0x0800_0002;
34
35/// Built-in style id for LSP document links (`textDocument/documentLink`).
36///
37/// Renderers should typically draw this as an underline and/or link-like foreground color.
38pub const DOCUMENT_LINK_STYLE_ID: StyleId = 0x0800_0003;
39
40/// Built-in style id for match highlights (e.g. search matches, bracket matches).
41pub const MATCH_HIGHLIGHT_STYLE_ID: StyleId = 0x0800_0004;
42
43/// Style layer ID
44///
45/// Used to distinguish style sources (e.g., LSP semantic highlighting, simple syntax highlighting, diagnostics, etc.),
46/// allowing replacement/clearing of one layer without affecting other style layers.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub struct StyleLayerId(pub u32);
49
50impl StyleLayerId {
51    /// Create a style layer id from a raw numeric identifier.
52    pub const fn new(id: u32) -> Self {
53        Self(id)
54    }
55
56    /// LSP `semanticTokens` style layer (recommended for semantic highlighting).
57    pub const SEMANTIC_TOKENS: Self = Self(1);
58
59    /// Simple syntax highlighting style layer (e.g., regex-based JSON/INI highlighting).
60    pub const SIMPLE_SYNTAX: Self = Self(2);
61
62    /// Sublime Text `.sublime-syntax` style layer (lightweight syntax highlighting/folding).
63    pub const SUBLIME_SYNTAX: Self = Self(3);
64
65    /// LSP diagnostics overlay layer.
66    ///
67    /// This is intended for underlines / gutter markers sourced from LSP diagnostics.
68    pub const DIAGNOSTICS: Self = Self(4);
69
70    /// LSP `textDocument/documentHighlight` overlay layer.
71    pub const DOCUMENT_HIGHLIGHTS: Self = Self(5);
72
73    /// Tree-sitter syntax highlighting style layer.
74    pub const TREE_SITTER: Self = Self(6);
75
76    /// IME marked text (composition / preedit) overlay layer.
77    pub const IME_MARKED_TEXT: Self = Self(7);
78
79    /// LSP document links overlay layer.
80    ///
81    /// This is intended for underlines / click targets sourced from `textDocument/documentLink`.
82    pub const DOCUMENT_LINKS: Self = Self(8);
83
84    /// Match highlights overlay layer (search matches, bracket highlights, etc).
85    pub const MATCH_HIGHLIGHTS: Self = Self(9);
86
87    /// Bracket match overlay layer (matching bracket highlights).
88    pub const BRACKET_MATCHES: Self = Self(10);
89}
90
91/// Interval structure
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Interval {
94    /// Start offset (bytes or characters, depending on usage scenario)
95    pub start: usize,
96    /// End offset (exclusive)
97    pub end: usize,
98    /// Style ID
99    pub style_id: StyleId,
100}
101
102/// Text edit delta used to shift style intervals after text changes.
103///
104/// Coordinates are character offsets in the pre-edit document. A non-zero deletion is applied
105/// first as `[start, start + delete_len)`, then `insert_len` characters are inserted at `start`.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct IntervalTextEdit {
108    /// Character offset where the edit starts.
109    pub start: usize,
110    /// Number of characters deleted from `start`.
111    pub delete_len: usize,
112    /// Number of characters inserted at `start` after deletion.
113    pub insert_len: usize,
114}
115
116impl IntervalTextEdit {
117    /// Create a text edit delta for interval shifting.
118    pub const fn new(start: usize, delete_len: usize, insert_len: usize) -> Self {
119        Self {
120            start,
121            delete_len,
122            insert_len,
123        }
124    }
125}
126
127impl Interval {
128    /// Create a new interval with `[start, end)` offsets and a style id.
129    pub fn new(start: usize, end: usize, style_id: StyleId) -> Self {
130        Self {
131            start,
132            end,
133            style_id,
134        }
135    }
136
137    /// Check if interval contains a specific position
138    pub fn contains(&self, pos: usize) -> bool {
139        self.start <= pos && pos < self.end
140    }
141
142    /// Check if two intervals overlap
143    pub fn overlaps(&self, other: &Interval) -> bool {
144        self.start < other.end && other.start < self.end
145    }
146}
147
148/// Interval tree - manages style intervals
149///
150/// Uses a sorted vector with binary search for efficient interval queries.
151/// Query complexity: O(log n + k), where k is the number of overlapping intervals.
152/// Insertion complexity: O(n) (requires maintaining sort order).
153pub struct IntervalTree {
154    /// List of intervals (kept sorted by start position)
155    intervals: Vec<Interval>,
156    /// Prefix maximum end position: `prefix_max_end[i] = max(intervals[0..=i].end)`
157    ///
158    /// Used for early pruning in `query_point` / `query_range`, avoiding degradation to O(n) scan when there are many style intervals.
159    prefix_max_end: Vec<usize>,
160}
161
162impl IntervalTree {
163    /// Create an empty interval tree.
164    pub fn new() -> Self {
165        Self {
166            intervals: Vec::new(),
167            prefix_max_end: Vec::new(),
168        }
169    }
170
171    fn rebuild_prefix_max_end_from(&mut self, start_idx: usize) {
172        if self.intervals.is_empty() {
173            self.prefix_max_end.clear();
174            return;
175        }
176
177        if self.prefix_max_end.len() != self.intervals.len() {
178            self.prefix_max_end.resize(self.intervals.len(), 0);
179        }
180
181        let mut max_end = if start_idx == 0 {
182            0
183        } else {
184            self.prefix_max_end[start_idx - 1]
185        };
186
187        for (idx, interval) in self.intervals.iter().enumerate().skip(start_idx) {
188            max_end = max_end.max(interval.end);
189            self.prefix_max_end[idx] = max_end;
190        }
191    }
192
193    fn rebuild_prefix_max_end(&mut self) {
194        self.rebuild_prefix_max_end_from(0);
195    }
196
197    fn apply_insertion_to_interval(interval: &mut Interval, pos: usize, delta: usize) {
198        if interval.start >= pos {
199            interval.start += delta;
200            interval.end += delta;
201        } else if interval.end > pos {
202            // Interval spans insertion point, extend end position.
203            interval.end += delta;
204        }
205    }
206
207    fn apply_deletion_to_interval(interval: &mut Interval, start: usize, end: usize) -> bool {
208        if start >= end {
209            return true;
210        }
211
212        let delta = end - start;
213        if interval.end <= start {
214            // Interval is before deletion range, unaffected.
215            true
216        } else if interval.start >= end {
217            // Interval is after deletion range, move forward.
218            interval.start -= delta;
219            interval.end -= delta;
220            true
221        } else if interval.start >= start && interval.end <= end {
222            // Interval is completely within deletion range.
223            false
224        } else if interval.start < start && interval.end > end {
225            // Interval spans deletion range, shrink.
226            interval.end -= delta;
227            true
228        } else if interval.start < start {
229            // Interval partially overlaps the deletion range at its end.
230            interval.end = start;
231            true
232        } else {
233            // Interval partially overlaps the deletion range at its start.
234            interval.start = start;
235            interval.end -= delta;
236            true
237        }
238    }
239
240    #[cfg(debug_assertions)]
241    fn debug_assert_sorted(&self) {
242        debug_assert!(
243            self.intervals
244                .windows(2)
245                .all(|pair| pair[0].start <= pair[1].start),
246            "IntervalTree intervals must remain sorted by start offset"
247        );
248    }
249
250    /// Insert an interval
251    pub fn insert(&mut self, interval: Interval) {
252        // Find insertion position (maintaining sort order)
253        let pos = self
254            .intervals
255            .binary_search_by_key(&interval.start, |i| i.start)
256            .unwrap_or_else(|pos| pos);
257
258        self.intervals.insert(pos, interval);
259        self.prefix_max_end.insert(pos, 0);
260        self.rebuild_prefix_max_end_from(pos);
261    }
262
263    /// Remove interval that exactly matches the specified interval
264    pub fn remove(&mut self, start: usize, end: usize, style_id: StyleId) -> bool {
265        if let Some(pos) = self
266            .intervals
267            .iter()
268            .position(|i| i.start == start && i.end == end && i.style_id == style_id)
269        {
270            self.intervals.remove(pos);
271            self.prefix_max_end.remove(pos);
272            if pos < self.intervals.len() {
273                self.rebuild_prefix_max_end_from(pos);
274            }
275            true
276        } else {
277            false
278        }
279    }
280
281    /// Query all intervals containing a specific position
282    /// Optimized version: uses binary search to locate interval range that may contain pos
283    pub fn query_point(&self, pos: usize) -> Vec<&Interval> {
284        self.query_point_impl(pos).0
285    }
286
287    fn query_point_impl(&self, pos: usize) -> (Vec<&Interval>, usize) {
288        if self.intervals.is_empty() {
289            return (Vec::new(), 0);
290        }
291
292        let mut result = Vec::new();
293        let mut scanned = 0usize;
294
295        // Use binary search to find first position where start > pos
296        let search_key = pos.saturating_add(1);
297        let idx = match self
298            .intervals
299            .binary_search_by_key(&search_key, |i| i.start)
300        {
301            Ok(idx) => idx,
302            Err(idx) => idx,
303        };
304
305        // Starting from idx-1, check backward for all intervals that may contain pos
306        // Because intervals are sorted by start, all intervals with start <= pos are before idx
307        for i in (0..idx).rev() {
308            scanned = scanned.saturating_add(1);
309
310            // If maximum end of `intervals[0..=i]` is <= pos, earlier intervals cannot contain pos.
311            if self.prefix_max_end[i] <= pos {
312                break;
313            }
314
315            let interval = &self.intervals[i];
316            if interval.contains(pos) {
317                result.push(interval);
318            }
319        }
320
321        (result, scanned)
322    }
323
324    #[cfg(test)]
325    fn query_point_scan_count(&self, pos: usize) -> usize {
326        self.query_point_impl(pos).1
327    }
328
329    /// Query all intervals overlapping with specified range
330    /// Optimized version: uses binary search to locate interval range that may overlap
331    pub fn query_range(&self, start: usize, end: usize) -> Vec<&Interval> {
332        if self.intervals.is_empty() || start >= end {
333            return Vec::new();
334        }
335
336        let mut result = Vec::new();
337
338        // Use binary search to find first position where interval.start >= end
339        // All intervals that may overlap are before this position
340        let search_end = match self.intervals.binary_search_by_key(&end, |i| i.start) {
341            Ok(idx) => idx,
342            Err(idx) => idx,
343        };
344
345        if search_end == 0 {
346            return result;
347        }
348
349        // First find position where start >= start, then expand backward,
350        // until `prefix_max_end` indicates earlier intervals cannot cross start.
351        let mut scan_start = match self.intervals.binary_search_by_key(&start, |i| i.start) {
352            Ok(idx) | Err(idx) => idx.min(search_end),
353        };
354
355        while scan_start > 0 && self.prefix_max_end[scan_start - 1] > start {
356            scan_start -= 1;
357        }
358
359        for interval in self.intervals[scan_start..search_end].iter() {
360            if interval.start < end && interval.end > start {
361                result.push(interval);
362            }
363        }
364
365        result
366    }
367
368    /// Clear all intervals
369    pub fn clear(&mut self) {
370        self.intervals.clear();
371        self.prefix_max_end.clear();
372    }
373
374    /// Get number of intervals
375    pub fn len(&self) -> usize {
376        self.intervals.len()
377    }
378
379    /// Check if empty
380    pub fn is_empty(&self) -> bool {
381        self.intervals.is_empty()
382    }
383
384    /// Update offsets (when text changes)
385    ///
386    /// Call this method to update all intervals when inserting text of `delta` length at position `pos`
387    pub fn update_for_insertion(&mut self, pos: usize, delta: usize) {
388        if delta == 0 || self.intervals.is_empty() {
389            return;
390        }
391
392        for interval in &mut self.intervals {
393            Self::apply_insertion_to_interval(interval, pos, delta);
394        }
395        self.rebuild_prefix_max_end();
396        #[cfg(debug_assertions)]
397        self.debug_assert_sorted();
398    }
399
400    /// Update offsets (when text is deleted)
401    ///
402    /// Call this method to update all intervals when deleting text in range `[start, end)`
403    pub fn update_for_deletion(&mut self, start: usize, end: usize) {
404        if start >= end || self.intervals.is_empty() {
405            return;
406        }
407
408        let mut updated = Vec::with_capacity(self.intervals.len());
409        for mut interval in self.intervals.drain(..) {
410            if Self::apply_deletion_to_interval(&mut interval, start, end) {
411                updated.push(interval);
412            }
413        }
414        if !updated
415            .windows(2)
416            .all(|pair| pair[0].start <= pair[1].start)
417        {
418            updated.sort_by_key(|interval| interval.start);
419        }
420        self.intervals = updated;
421
422        self.rebuild_prefix_max_end();
423        #[cfg(debug_assertions)]
424        self.debug_assert_sorted();
425    }
426
427    /// Update offsets for multiple text edits in one tree pass.
428    ///
429    /// Edits are interpreted in pre-edit coordinates and applied from larger `start` offsets to
430    /// smaller offsets, matching the command layer's descending mutation order. Each edit applies
431    /// deletion first and insertion second, preserving the single-edit replacement semantics.
432    pub fn update_for_text_edits(&mut self, edits: &[IntervalTextEdit]) {
433        if self.intervals.is_empty()
434            || !edits
435                .iter()
436                .any(|edit| edit.delete_len > 0 || edit.insert_len > 0)
437        {
438            return;
439        }
440
441        let mut ordered_edits: Vec<IntervalTextEdit> = edits
442            .iter()
443            .copied()
444            .filter(|edit| edit.delete_len > 0 || edit.insert_len > 0)
445            .collect();
446        ordered_edits.sort_by_key(|edit| std::cmp::Reverse(edit.start));
447
448        let mut updated = Vec::with_capacity(self.intervals.len());
449        for mut interval in self.intervals.drain(..) {
450            let mut keep = true;
451            for edit in &ordered_edits {
452                if edit.delete_len > 0 {
453                    let end = edit.start.saturating_add(edit.delete_len);
454                    keep = Self::apply_deletion_to_interval(&mut interval, edit.start, end);
455                    if !keep {
456                        break;
457                    }
458                }
459
460                if edit.insert_len > 0 {
461                    Self::apply_insertion_to_interval(&mut interval, edit.start, edit.insert_len);
462                }
463            }
464
465            if keep {
466                updated.push(interval);
467            }
468        }
469        if !updated
470            .windows(2)
471            .all(|pair| pair[0].start <= pair[1].start)
472        {
473            updated.sort_by_key(|interval| interval.start);
474        }
475        self.intervals = updated;
476
477        self.rebuild_prefix_max_end();
478        #[cfg(debug_assertions)]
479        self.debug_assert_sorted();
480    }
481}
482
483impl Default for IntervalTree {
484    fn default() -> Self {
485        Self::new()
486    }
487}
488
489/// Fold region
490#[derive(Debug, Clone, PartialEq, Eq)]
491pub struct FoldRegion {
492    /// Start line number
493    pub start_line: usize,
494    /// End line number (inclusive)
495    pub end_line: usize,
496    /// Whether folded
497    pub is_collapsed: bool,
498    /// Placeholder text shown when folded (e.g., "[...]")
499    pub placeholder: String,
500}
501
502impl FoldRegion {
503    /// Create a folding region for an inclusive line range.
504    pub fn new(start_line: usize, end_line: usize) -> Self {
505        Self {
506            start_line,
507            end_line,
508            is_collapsed: false,
509            placeholder: String::from("[...]"),
510        }
511    }
512
513    /// Create a folding region with a custom placeholder string.
514    pub fn with_placeholder(start_line: usize, end_line: usize, placeholder: String) -> Self {
515        Self {
516            start_line,
517            end_line,
518            is_collapsed: false,
519            placeholder,
520        }
521    }
522
523    /// Expand
524    pub fn expand(&mut self) {
525        self.is_collapsed = false;
526    }
527
528    /// Collapse
529    pub fn collapse(&mut self) {
530        self.is_collapsed = true;
531    }
532
533    /// Toggle fold state
534    pub fn toggle(&mut self) {
535        self.is_collapsed = !self.is_collapsed;
536    }
537
538    /// Check if line number is within fold region
539    pub fn contains_line(&self, line: usize) -> bool {
540        line >= self.start_line && line <= self.end_line
541    }
542}
543
544/// Folding manager
545pub struct FoldingManager {
546    /// Fold regions sourced from external/derived providers (LSP, sublime syntax, etc.).
547    derived_regions: Vec<FoldRegion>,
548    /// Fold regions created explicitly by the user (via commands).
549    user_regions: Vec<FoldRegion>,
550    /// Cached merged view (sorted/deduplicated) used for rendering and coordinate mapping.
551    merged_regions: Vec<FoldRegion>,
552}
553
554impl FoldingManager {
555    /// Create an empty folding manager.
556    pub fn new() -> Self {
557        Self {
558            derived_regions: Vec::new(),
559            user_regions: Vec::new(),
560            merged_regions: Vec::new(),
561        }
562    }
563
564    fn rebuild_merged_regions(&mut self) {
565        self.merged_regions.clear();
566        // User folds should override derived folds when they share the same (start, end) range.
567        // This makes toggling/collapsing derived regions from the UI deterministic: we can record
568        // the user's collapsed/expanded choice as a user region and have it win in the merged view.
569        self.merged_regions
570            .extend(self.user_regions.iter().cloned());
571        self.merged_regions
572            .extend(self.derived_regions.iter().cloned());
573
574        self.merged_regions
575            .sort_by_key(|r| (r.start_line, r.end_line));
576        self.merged_regions
577            .dedup_by(|a, b| a.start_line == b.start_line && a.end_line == b.end_line);
578    }
579
580    fn normalize_regions(regions: &mut Vec<FoldRegion>) {
581        regions.sort_by_key(|r| (r.start_line, r.end_line));
582        regions.dedup_by(|a, b| a.start_line == b.start_line && a.end_line == b.end_line);
583        regions.retain(|r| r.end_line > r.start_line);
584    }
585
586    fn clamp_regions(regions: &mut Vec<FoldRegion>, max_line: usize) {
587        for r in regions.iter_mut() {
588            r.start_line = r.start_line.min(max_line);
589            r.end_line = r.end_line.min(max_line);
590        }
591        Self::normalize_regions(regions);
592    }
593
594    fn overlapping_line_count(left: &FoldRegion, right: &FoldRegion) -> usize {
595        let start = left.start_line.max(right.start_line);
596        let end = left.end_line.min(right.end_line);
597        if start > end { 0 } else { end - start + 1 }
598    }
599
600    fn collapsed_fuzzy_match_score(
601        existing: &FoldRegion,
602        replacement: &FoldRegion,
603    ) -> Option<(usize, usize, usize)> {
604        if existing.placeholder != replacement.placeholder {
605            return None;
606        }
607
608        let start_delta = existing.start_line.abs_diff(replacement.start_line);
609        let overlapping_line_count = Self::overlapping_line_count(existing, replacement);
610        if start_delta > 1 || overlapping_line_count < 2 {
611            return None;
612        }
613
614        let existing_len = existing.end_line.saturating_sub(existing.start_line);
615        let replacement_len = replacement.end_line.saturating_sub(replacement.start_line);
616        let len_delta = existing_len.abs_diff(replacement_len);
617        let max_len_delta = 2.max(existing_len.max(replacement_len) / 3);
618        if len_delta > max_len_delta {
619            return None;
620        }
621
622        Some((
623            start_delta,
624            len_delta,
625            existing.end_line.abs_diff(replacement.end_line),
626        ))
627    }
628
629    fn preserve_collapsed_states(existing: &[FoldRegion], replacements: &mut [FoldRegion]) {
630        let collapsed = existing
631            .iter()
632            .filter(|region| region.is_collapsed)
633            .collect::<Vec<_>>();
634        if collapsed.is_empty() {
635            return;
636        }
637
638        let mut used = vec![false; collapsed.len()];
639
640        for replacement in replacements.iter_mut() {
641            if let Some(source_idx) = collapsed.iter().enumerate().position(|(idx, existing)| {
642                !used[idx]
643                    && existing.start_line == replacement.start_line
644                    && existing.end_line == replacement.end_line
645            }) {
646                replacement.is_collapsed = true;
647                used[source_idx] = true;
648            }
649        }
650
651        for replacement in replacements
652            .iter_mut()
653            .filter(|region| !region.is_collapsed)
654        {
655            let best = collapsed
656                .iter()
657                .enumerate()
658                .filter_map(|(idx, existing)| {
659                    if used[idx] {
660                        return None;
661                    }
662                    Self::collapsed_fuzzy_match_score(existing, replacement)
663                        .map(|score| (score, idx))
664                })
665                .min_by_key(|(score, idx)| (*score, *idx));
666
667            if let Some((_score, source_idx)) = best {
668                replacement.is_collapsed = true;
669                used[source_idx] = true;
670            }
671        }
672    }
673
674    /// Add a user-created fold region.
675    pub fn add_region(&mut self, region: FoldRegion) {
676        // Keep sorted by start line.
677        let pos = self
678            .user_regions
679            .binary_search_by_key(&region.start_line, |r| r.start_line)
680            .unwrap_or_else(|pos| pos);
681
682        self.user_regions.insert(pos, region);
683        Self::normalize_regions(&mut self.user_regions);
684        self.rebuild_merged_regions();
685    }
686
687    /// Remove a user-created fold region.
688    pub fn remove_region(&mut self, start_line: usize, end_line: usize) -> bool {
689        if let Some(pos) = self
690            .user_regions
691            .iter()
692            .position(|r| r.start_line == start_line && r.end_line == end_line)
693        {
694            self.user_regions.remove(pos);
695            self.rebuild_merged_regions();
696            true
697        } else {
698            false
699        }
700    }
701
702    /// Get fold region containing specified line (merged view).
703    pub fn get_region_for_line(&self, line: usize) -> Option<&FoldRegion> {
704        self.merged_regions.iter().find(|r| r.contains_line(line))
705    }
706
707    pub(crate) fn innermost_region_bounds_for_line(&self, line: usize) -> Option<(usize, usize)> {
708        self.merged_regions
709            .iter()
710            .filter(|region| region.contains_line(line))
711            .min_by_key(|region| {
712                (
713                    region.end_line.saturating_sub(region.start_line),
714                    region.end_line,
715                    region.start_line,
716                )
717            })
718            .map(|region| (region.start_line, region.end_line))
719    }
720
721    /// Get mutable reference to a fold region containing specified line (prefers user folds).
722    pub fn get_region_for_line_mut(&mut self, line: usize) -> Option<&mut FoldRegion> {
723        // Important: a line can be covered by multiple (nested) fold regions.
724        //
725        // When a user clicks a fold marker on an inner block (nested inside an outer fold),
726        // we must act on the *innermost* region for that line. Otherwise, unfolding the
727        // inner region can accidentally target the outer region again (because it also
728        // "contains" the line), making the inner fold appear "stuck".
729        //
730        // Choose the smallest region (end-start) among all regions that contain `line`,
731        // preferring user regions on ties.
732        let mut best: Option<(
733            bool,  /*is_user*/
734            usize, /*idx*/
735            usize, /*len*/
736            usize, /*end*/
737            usize, /*start*/
738        )> = None;
739
740        for (is_user, regions) in [
741            (true, self.user_regions.as_slice()),
742            (false, self.derived_regions.as_slice()),
743        ] {
744            for (idx, region) in regions.iter().enumerate() {
745                if !region.contains_line(line) {
746                    continue;
747                }
748                let len = region.end_line.saturating_sub(region.start_line);
749                let candidate = (is_user, idx, len, region.end_line, region.start_line);
750                match best {
751                    None => best = Some(candidate),
752                    Some((best_is_user, _best_idx, best_len, best_end, best_start)) => {
753                        let better_range = (len, region.end_line, region.start_line)
754                            < (best_len, best_end, best_start);
755                        let prefer_user_tie = (len, region.end_line, region.start_line)
756                            == (best_len, best_end, best_start)
757                            && is_user
758                            && !best_is_user;
759                        if better_range || prefer_user_tie {
760                            best = Some(candidate);
761                        }
762                    }
763                }
764            }
765        }
766
767        let (is_user, idx, _len, _end, _start) = best?;
768
769        if is_user {
770            self.user_regions.get_mut(idx)
771        } else {
772            self.derived_regions.get_mut(idx)
773        }
774    }
775
776    /// Collapse specified line
777    pub fn collapse_line(&mut self, line: usize) -> bool {
778        if let Some(region) = self.get_region_for_line_mut(line) {
779            region.collapse();
780            self.rebuild_merged_regions();
781            true
782        } else {
783            false
784        }
785    }
786
787    /// Expand specified line
788    pub fn expand_line(&mut self, line: usize) -> bool {
789        if let Some(region) = self.get_region_for_line_mut(line) {
790            region.expand();
791            self.rebuild_merged_regions();
792            true
793        } else {
794            false
795        }
796    }
797
798    /// Toggle fold state of specified line
799    pub fn toggle_line(&mut self, line: usize) -> bool {
800        if let Some(region) = self.get_region_for_line_mut(line) {
801            region.toggle();
802            self.rebuild_merged_regions();
803            true
804        } else {
805            false
806        }
807    }
808
809    /// Toggle fold region starting at specified line (preferring "innermost" region).
810    ///
811    /// `rust-analyzer` / LSP folding ranges often contain nested regions. To make TUI and other frontends
812    /// behave more intuitively when "cursor is on a start line", we choose:
813    /// - Among all regions with `start_line == line`, the one with smallest `end_line` (innermost)
814    pub fn toggle_region_starting_at_line(&mut self, start_line: usize) -> bool {
815        if self.merged_regions.is_empty() {
816            return false;
817        }
818
819        // Find the innermost region among both sources, preferring user folds on ties.
820        let mut best_source = None::<(bool, usize)>; // (is_user, index)
821        let mut best_end = usize::MAX;
822
823        for (is_user, regions) in [
824            (true, &mut self.user_regions),
825            (false, &mut self.derived_regions),
826        ] {
827            let Ok(mut idx) = regions.binary_search_by_key(&start_line, |r| r.start_line) else {
828                continue;
829            };
830
831            while idx > 0 && regions[idx - 1].start_line == start_line {
832                idx -= 1;
833            }
834
835            for (i, region) in regions[idx..].iter().enumerate() {
836                if region.start_line != start_line {
837                    break;
838                }
839                if region.end_line <= region.start_line {
840                    continue;
841                }
842                if region.end_line < best_end
843                    || (region.end_line == best_end
844                        && best_source.is_some_and(|(prev_is_user, _)| !prev_is_user && is_user))
845                {
846                    best_end = region.end_line;
847                    best_source = Some((is_user, i));
848                }
849            }
850        }
851
852        let Some((is_user, idx)) = best_source else {
853            return false;
854        };
855
856        if is_user {
857            if let Some(region) = self.user_regions.get_mut(idx) {
858                region.toggle();
859            }
860        } else if let Some(region) = self.derived_regions.get_mut(idx) {
861            region.toggle();
862        }
863
864        self.rebuild_merged_regions();
865        true
866    }
867
868    /// Calculate mapping from logical line to visual line
869    ///
870    /// Returns the visual line number for the specified logical line number, or None if folded
871    pub fn logical_to_visual(&self, logical_line: usize, base_visual: usize) -> Option<usize> {
872        let mut hidden_lines = 0usize;
873
874        for (hidden_start, hidden_end) in self.collapsed_hidden_ranges() {
875            if logical_line >= hidden_start && logical_line < hidden_end {
876                return None;
877            }
878            if logical_line <= hidden_start {
879                break;
880            }
881
882            hidden_lines = hidden_lines.saturating_add(hidden_end.min(logical_line) - hidden_start);
883        }
884
885        Some(base_visual.saturating_add(logical_line.saturating_sub(hidden_lines)))
886    }
887
888    /// Calculate mapping from visual line to logical line
889    pub fn visual_to_logical(&self, visual_line: usize, base_visual: usize) -> usize {
890        let mut logical = visual_line.saturating_sub(base_visual);
891
892        for (hidden_start, hidden_end) in self.collapsed_hidden_ranges() {
893            if logical < hidden_start {
894                break;
895            }
896            logical = logical.saturating_add(hidden_end - hidden_start);
897        }
898
899        logical
900    }
901
902    fn collapsed_hidden_ranges(&self) -> Vec<(usize, usize)> {
903        let mut ranges: Vec<(usize, usize)> = Vec::new();
904
905        for region in self
906            .merged_regions
907            .iter()
908            .filter(|region| region.is_collapsed)
909        {
910            if region.end_line <= region.start_line {
911                continue;
912            }
913
914            let hidden_start = region.start_line.saturating_add(1);
915            let hidden_end = region.end_line.saturating_add(1);
916            if hidden_start >= hidden_end {
917                continue;
918            }
919
920            if let Some((_, last_end)) = ranges.last_mut()
921                && hidden_start <= *last_end
922            {
923                *last_end = (*last_end).max(hidden_end);
924                continue;
925            }
926
927            ranges.push((hidden_start, hidden_end));
928        }
929
930        ranges
931    }
932
933    /// Get all fold regions
934    pub fn regions(&self) -> &[FoldRegion] {
935        &self.merged_regions
936    }
937
938    /// Get all derived fold regions.
939    pub fn derived_regions(&self) -> &[FoldRegion] {
940        &self.derived_regions
941    }
942
943    /// Get all user-created fold regions.
944    pub fn user_regions(&self) -> &[FoldRegion] {
945        &self.user_regions
946    }
947
948    /// Clear all fold regions (derived + user).
949    pub fn clear(&mut self) {
950        self.derived_regions.clear();
951        self.user_regions.clear();
952        self.merged_regions.clear();
953    }
954
955    /// Clear all derived fold regions, leaving user folds intact.
956    pub fn clear_derived_regions(&mut self) {
957        self.derived_regions.clear();
958        self.rebuild_merged_regions();
959    }
960
961    /// Replace derived fold regions (will be sorted by start line and deduplicated).
962    pub fn replace_derived_regions(&mut self, mut regions: Vec<FoldRegion>) {
963        Self::normalize_regions(&mut regions);
964        self.derived_regions = regions;
965        self.rebuild_merged_regions();
966    }
967
968    /// Replace derived fold regions while preserving collapsed state across small range drift.
969    pub fn replace_derived_regions_preserving_collapsed(&mut self, mut regions: Vec<FoldRegion>) {
970        Self::normalize_regions(&mut regions);
971        Self::preserve_collapsed_states(&self.derived_regions, &mut regions);
972        self.derived_regions = regions;
973        self.rebuild_merged_regions();
974    }
975
976    /// Replace fold regions with new list (legacy API).
977    ///
978    /// This replaces *derived* fold regions, leaving user folds intact.
979    pub fn replace_regions(&mut self, regions: Vec<FoldRegion>) {
980        self.replace_derived_regions(regions);
981    }
982
983    /// Expand all folds
984    pub fn expand_all(&mut self) {
985        for region in &mut self.derived_regions {
986            region.expand();
987        }
988        for region in &mut self.user_regions {
989            region.expand();
990        }
991        self.rebuild_merged_regions();
992    }
993
994    /// Collapse all regions
995    pub fn collapse_all(&mut self) {
996        for region in &mut self.derived_regions {
997            region.collapse();
998        }
999        for region in &mut self.user_regions {
1000            region.collapse();
1001        }
1002        self.rebuild_merged_regions();
1003    }
1004
1005    /// Update fold regions to account for an edit that changes the number of logical lines.
1006    ///
1007    /// This is intended to keep **user folds** stable across newline insertions/deletions.
1008    ///
1009    /// - `edit_line` is the logical line where the edit occurred (pre-edit).
1010    /// - `line_delta` is the net change in line count (`+n` for inserted newlines, `-n` for deleted).
1011    pub fn apply_line_delta(&mut self, edit_line: usize, line_delta: isize) {
1012        if line_delta == 0 {
1013            return;
1014        }
1015
1016        let apply = |regions: &mut Vec<FoldRegion>| {
1017            for region in regions.iter_mut() {
1018                if edit_line <= region.start_line {
1019                    let start = region.start_line as isize + line_delta;
1020                    let end = region.end_line as isize + line_delta;
1021                    region.start_line = start.max(0) as usize;
1022                    region.end_line = end.max(0) as usize;
1023                } else if edit_line <= region.end_line {
1024                    let end = region.end_line as isize + line_delta;
1025                    region.end_line = end.max(region.start_line as isize) as usize;
1026                }
1027            }
1028        };
1029
1030        apply(&mut self.derived_regions);
1031        apply(&mut self.user_regions);
1032        self.rebuild_merged_regions();
1033    }
1034
1035    /// Clamp fold regions to the given `line_count` after a text edit, dropping invalid regions.
1036    pub fn clamp_to_line_count(&mut self, line_count: usize) {
1037        let max_line = line_count.saturating_sub(1);
1038        Self::clamp_regions(&mut self.derived_regions, max_line);
1039        Self::clamp_regions(&mut self.user_regions, max_line);
1040        self.rebuild_merged_regions();
1041    }
1042}
1043
1044impl Default for FoldingManager {
1045    fn default() -> Self {
1046        Self::new()
1047    }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052    use super::*;
1053
1054    #[test]
1055    fn test_interval_contains() {
1056        let interval = Interval::new(10, 20, 1);
1057        assert!(interval.contains(10));
1058        assert!(interval.contains(15));
1059        assert!(interval.contains(19));
1060        assert!(!interval.contains(20));
1061        assert!(!interval.contains(9));
1062    }
1063
1064    #[test]
1065    fn test_interval_overlaps() {
1066        let i1 = Interval::new(10, 20, 1);
1067        let i2 = Interval::new(15, 25, 2);
1068        let i3 = Interval::new(25, 30, 3);
1069
1070        assert!(i1.overlaps(&i2));
1071        assert!(i2.overlaps(&i1));
1072        assert!(!i1.overlaps(&i3));
1073        assert!(!i3.overlaps(&i1));
1074    }
1075
1076    #[test]
1077    fn test_interval_tree_insert() {
1078        let mut tree = IntervalTree::new();
1079        tree.insert(Interval::new(10, 20, 1));
1080        tree.insert(Interval::new(5, 15, 2));
1081        tree.insert(Interval::new(15, 25, 3));
1082
1083        assert_eq!(tree.len(), 3);
1084    }
1085
1086    #[test]
1087    fn test_interval_tree_query_point() {
1088        let mut tree = IntervalTree::new();
1089        tree.insert(Interval::new(10, 20, 1));
1090        tree.insert(Interval::new(5, 15, 2));
1091        tree.insert(Interval::new(15, 25, 3));
1092
1093        let results = tree.query_point(12);
1094        assert_eq!(results.len(), 2); // intervals 1 and 2
1095
1096        let results = tree.query_point(18);
1097        assert_eq!(results.len(), 2); // intervals 1 and 3
1098    }
1099
1100    #[test]
1101    fn test_interval_tree_query_point_prunes_scan() {
1102        let mut tree = IntervalTree::new();
1103
1104        // Construct many non-overlapping intervals: ideally point query should only need to check few candidate intervals.
1105        for i in 0..10_000usize {
1106            let start = i * 2;
1107            tree.insert(Interval::new(start, start + 1, 1));
1108        }
1109
1110        let pos = 2 * 10_000 - 2; // Falls within last interval
1111        let results = tree.query_point(pos);
1112        assert_eq!(results.len(), 1);
1113
1114        // After pruning with prefix_max_end, should avoid degrading to linear scan of all intervals.
1115        assert!(
1116            tree.query_point_scan_count(pos) <= 4,
1117            "scan should be pruned for disjoint intervals"
1118        );
1119    }
1120
1121    #[test]
1122    fn test_interval_tree_query_range() {
1123        let mut tree = IntervalTree::new();
1124        tree.insert(Interval::new(10, 20, 1));
1125        tree.insert(Interval::new(25, 35, 2));
1126        tree.insert(Interval::new(40, 50, 3));
1127
1128        let results = tree.query_range(15, 30);
1129        assert_eq!(results.len(), 2); // intervals 1 and 2
1130
1131        let results = tree.query_range(0, 60);
1132        assert_eq!(results.len(), 3); // all intervals
1133    }
1134
1135    #[test]
1136    fn test_interval_tree_update_insertion() {
1137        let mut tree = IntervalTree::new();
1138        tree.insert(Interval::new(10, 20, 1));
1139        tree.insert(Interval::new(30, 40, 2));
1140
1141        tree.update_for_insertion(15, 5);
1142
1143        assert_eq!(tree.intervals[0].start, 10);
1144        assert_eq!(tree.intervals[0].end, 25); // 20 + 5
1145
1146        assert_eq!(tree.intervals[1].start, 35); // 30 + 5
1147        assert_eq!(tree.intervals[1].end, 45); // 40 + 5
1148    }
1149
1150    #[test]
1151    fn test_interval_tree_update_deletion() {
1152        let mut tree = IntervalTree::new();
1153        tree.insert(Interval::new(10, 20, 1));
1154        tree.insert(Interval::new(30, 40, 2));
1155        tree.insert(Interval::new(50, 60, 3));
1156
1157        tree.update_for_deletion(25, 35);
1158
1159        assert_eq!(tree.intervals[0].start, 10);
1160        assert_eq!(tree.intervals[0].end, 20); // Unaffected
1161
1162        assert_eq!(tree.intervals[1].start, 25); // 30 - (35-25)
1163        assert_eq!(tree.intervals[1].end, 30); // 40 - (35-25)
1164
1165        assert_eq!(tree.intervals[2].start, 40); // 50 - 10
1166        assert_eq!(tree.intervals[2].end, 50); // 60 - 10
1167    }
1168
1169    #[test]
1170    fn test_interval_tree_batch_update_matches_sequential_updates() {
1171        fn build_tree() -> IntervalTree {
1172            let mut tree = IntervalTree::new();
1173            tree.insert(Interval::new(0, 4, 1));
1174            tree.insert(Interval::new(6, 12, 2));
1175            tree.insert(Interval::new(15, 25, 3));
1176            tree.insert(Interval::new(20, 24, 4));
1177            tree.insert(Interval::new(30, 38, 5));
1178            tree.insert(Interval::new(38, 45, 6));
1179            tree
1180        }
1181
1182        let edits = vec![
1183            IntervalTextEdit::new(40, 5, 2),
1184            IntervalTextEdit::new(18, 10, 0),
1185            IntervalTextEdit::new(5, 0, 3),
1186        ];
1187
1188        let mut sequential = build_tree();
1189        for edit in &edits {
1190            if edit.delete_len > 0 {
1191                sequential.update_for_deletion(edit.start, edit.start + edit.delete_len);
1192            }
1193            if edit.insert_len > 0 {
1194                sequential.update_for_insertion(edit.start, edit.insert_len);
1195            }
1196        }
1197
1198        let mut batched = build_tree();
1199        batched.update_for_text_edits(&edits);
1200
1201        assert_eq!(batched.intervals, sequential.intervals);
1202        assert_eq!(batched.prefix_max_end, sequential.prefix_max_end);
1203    }
1204
1205    #[test]
1206    fn test_interval_tree_batch_update_keeps_queries_correct() {
1207        let mut tree = IntervalTree::new();
1208        tree.insert(Interval::new(2, 8, 1));
1209        tree.insert(Interval::new(10, 18, 2));
1210        tree.insert(Interval::new(20, 30, 3));
1211
1212        tree.update_for_text_edits(&[
1213            IntervalTextEdit::new(12, 4, 1),
1214            IntervalTextEdit::new(4, 2, 0),
1215        ]);
1216
1217        let point_styles: Vec<StyleId> = tree
1218            .query_point(10)
1219            .into_iter()
1220            .map(|interval| interval.style_id)
1221            .collect();
1222        assert_eq!(point_styles, vec![2]);
1223
1224        let range_styles: Vec<StyleId> = tree
1225            .query_range(0, 32)
1226            .into_iter()
1227            .map(|interval| interval.style_id)
1228            .collect();
1229        assert_eq!(range_styles, vec![1, 2, 3]);
1230    }
1231
1232    #[test]
1233    fn test_fold_region() {
1234        let mut region = FoldRegion::new(5, 10);
1235        assert!(!region.is_collapsed);
1236
1237        region.collapse();
1238        assert!(region.is_collapsed);
1239
1240        region.expand();
1241        assert!(!region.is_collapsed);
1242
1243        region.toggle();
1244        assert!(region.is_collapsed);
1245    }
1246
1247    #[test]
1248    fn test_folding_manager() {
1249        let mut manager = FoldingManager::new();
1250
1251        manager.add_region(FoldRegion::new(5, 10));
1252        manager.add_region(FoldRegion::new(15, 20));
1253
1254        assert!(manager.collapse_line(7));
1255        assert!(manager.get_region_for_line(7).unwrap().is_collapsed);
1256
1257        assert!(manager.expand_line(7));
1258        assert!(!manager.get_region_for_line(7).unwrap().is_collapsed);
1259    }
1260
1261    #[test]
1262    fn test_nested_folds_can_unfold_inner_after_outer_unfold() {
1263        let mut manager = FoldingManager::new();
1264
1265        // Outer region: 1..=10
1266        let mut outer = FoldRegion::new(1, 10);
1267        outer.collapse();
1268        manager.add_region(outer);
1269
1270        // Inner region: 3..=5 (nested inside outer)
1271        let mut inner = FoldRegion::new(3, 5);
1272        inner.collapse();
1273        manager.add_region(inner);
1274
1275        // Step 1: inner is folded.
1276        let inner_before = manager
1277            .regions()
1278            .iter()
1279            .find(|r| r.start_line == 3 && r.end_line == 5)
1280            .unwrap();
1281        assert!(inner_before.is_collapsed);
1282
1283        // Step 2/3: unfold outer.
1284        assert!(manager.expand_line(1));
1285        let outer_after = manager
1286            .regions()
1287            .iter()
1288            .find(|r| r.start_line == 1 && r.end_line == 10)
1289            .unwrap();
1290        assert!(!outer_after.is_collapsed);
1291
1292        // Step 4: now we should still be able to unfold the inner region by line 3.
1293        assert!(manager.expand_line(3));
1294        let inner_after = manager
1295            .regions()
1296            .iter()
1297            .find(|r| r.start_line == 3 && r.end_line == 5)
1298            .unwrap();
1299        assert!(!inner_after.is_collapsed);
1300    }
1301
1302    #[test]
1303    fn test_logical_to_visual_with_folding() {
1304        let mut manager = FoldingManager::new();
1305
1306        let mut region = FoldRegion::new(5, 10);
1307        region.collapse();
1308        manager.add_region(region);
1309
1310        // Line before fold
1311        assert_eq!(manager.logical_to_visual(3, 0), Some(3));
1312
1313        // Fold start line
1314        assert_eq!(manager.logical_to_visual(5, 0), Some(5));
1315
1316        // Middle line of fold should return None
1317        assert_eq!(manager.logical_to_visual(7, 0), None);
1318
1319        // Line after fold should have adjusted position
1320        assert_eq!(manager.logical_to_visual(15, 0), Some(10)); // 15 - 5 hidden lines
1321    }
1322
1323    #[test]
1324    fn test_multiple_overlapping_styles() {
1325        let mut tree = IntervalTree::new();
1326
1327        // Add overlapping style intervals
1328        tree.insert(Interval::new(0, 100, 1)); // Syntax highlighting
1329        tree.insert(Interval::new(20, 30, 2)); // Search highlighting
1330        tree.insert(Interval::new(25, 35, 3)); // Selection region
1331
1332        // Query position 27, should have 3 styles
1333        let styles = tree.query_point(27);
1334        assert_eq!(styles.len(), 3);
1335
1336        // Verify all styles were found
1337        let style_ids: Vec<StyleId> = styles.iter().map(|i| i.style_id).collect();
1338        assert!(style_ids.contains(&1));
1339        assert!(style_ids.contains(&2));
1340        assert!(style_ids.contains(&3));
1341    }
1342}