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