1use std::ops::Range;
21
22use serde::{Deserialize, Serialize};
23
24use crate::buffer::LineEdit;
25use crate::changeset::{Bias, ChangeSet};
26use crate::position::{ByteOffset, byte_to_visual_col};
27use crate::text::Document;
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
32pub enum FoldStrategy {
33 #[default]
34 Indent,
35 Markdown,
37}
38
39const MAX_BLANK_RUN: usize = 1000;
42const FENCE_LOOKBACK: usize = 200;
45
46fn indent(doc: &Document, line: usize, tab_width: usize) -> Option<usize> {
48 let text = doc.line_text(line);
49 let rest = text.trim_start_matches([' ', '\t']);
50 if rest.is_empty() {
51 return None;
52 }
53 let lead = text.len() - rest.len();
54 Some(byte_to_visual_col(doc, ByteOffset(doc.line_start(line).0 + lead), tab_width).0)
55}
56
57fn next_non_blank(
60 doc: &Document,
61 line: usize,
62 tab_width: usize,
63 limit: usize,
64) -> Option<(usize, usize)> {
65 (line + 1..doc.line_count().min((line + 1).saturating_add(limit)))
66 .find_map(|l| indent(doc, l, tab_width).map(|w| (l, w)))
67}
68
69fn heading_level(text: &str) -> Option<usize> {
71 let t = text.strip_prefix(" ").or_else(|| text.strip_prefix(" "));
72 let t = t.or_else(|| text.strip_prefix(' ')).unwrap_or(text);
73 let hashes = t.len() - t.trim_start_matches('#').len();
74 let after = &t[hashes..];
75 ((1..=6).contains(&hashes) && (after.is_empty() || after.starts_with([' ', '\t'])))
76 .then_some(hashes)
77}
78
79fn is_fence(text: &str) -> bool {
80 let t = text.trim_start_matches(' ');
81 text.len() - t.len() <= 3 && (t.starts_with("```") || t.starts_with("~~~"))
82}
83
84fn in_fence(doc: &Document, line: usize) -> bool {
87 let fences = (line.saturating_sub(FENCE_LOOKBACK)..line)
88 .filter(|&l| is_fence(&doc.line_text(l)))
89 .count();
90 fences % 2 == 1
91}
92
93fn markdown_heading(doc: &Document, line: usize) -> Option<usize> {
95 heading_level(&doc.line_text(line)).filter(|_| !in_fence(doc, line))
96}
97
98pub fn region(
101 doc: &Document,
102 header: usize,
103 strategy: FoldStrategy,
104 tab_width: usize,
105) -> Option<usize> {
106 if header >= doc.line_count() {
107 return None;
108 }
109 let mut last = header;
110 match strategy {
111 FoldStrategy::Indent => {
112 let w = indent(doc, header, tab_width)?;
113 for l in header + 1..doc.line_count() {
114 match indent(doc, l, tab_width) {
115 None => {}
116 Some(i) if i <= w => break,
117 Some(_) => last = l,
118 }
119 }
120 }
121 FoldStrategy::Markdown => {
122 let level = markdown_heading(doc, header)?;
123 let mut fenced = false;
124 for l in header + 1..doc.line_count() {
125 let text = doc.line_text(l);
126 if is_fence(&text) {
127 fenced = !fenced;
128 } else if !fenced && heading_level(&text).is_some_and(|h| h <= level) {
129 break;
130 }
131 if fenced || !text.trim().is_empty() {
132 last = l;
133 }
134 }
135 }
136 }
137 (last > header).then_some(last)
138}
139
140pub fn is_header(doc: &Document, line: usize, strategy: FoldStrategy, tab_width: usize) -> bool {
143 if line >= doc.line_count() {
144 return false;
145 }
146 match strategy {
147 FoldStrategy::Indent => indent(doc, line, tab_width).is_some_and(|w| {
148 next_non_blank(doc, line, tab_width, MAX_BLANK_RUN).is_some_and(|(_, n)| n > w)
149 }),
150 FoldStrategy::Markdown => {
151 let Some(level) = heading_level(&doc.line_text(line)) else {
152 return false;
153 };
154 let next = next_non_blank(doc, line, tab_width, MAX_BLANK_RUN);
156 next.is_some_and(|(l, _)| {
157 let text = doc.line_text(l);
158 is_fence(&text) || heading_level(&text).is_none_or(|h| h > level)
159 }) && !in_fence(doc, line)
160 }
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub struct Region {
169 pub header: usize,
170 pub level: usize,
171 pub last: usize,
172}
173
174pub fn regions(doc: &Document, strategy: FoldStrategy, tab_width: usize) -> Vec<Region> {
176 let mut out = Vec::new();
177 match strategy {
178 FoldStrategy::Indent => {
179 let mut open: Vec<(usize, usize)> = Vec::new();
181 let mut last_non_blank = 0;
182 let mut prev: Option<(usize, usize)> = None;
183 let close = |open: &mut Vec<(usize, usize)>, out: &mut Vec<Region>, w, last| {
184 while let Some(&(o, i)) = open.last() {
185 if o < w {
186 break;
187 }
188 out[i].last = last;
189 open.pop();
190 }
191 };
192 for l in 0..doc.line_count() {
193 let Some(w) = indent(doc, l, tab_width) else {
194 continue;
195 };
196 if let Some((pl, pw)) = prev
199 && w > pw
200 {
201 out.push(Region {
202 header: pl,
203 level: open.len() + 1,
204 last: pl,
205 });
206 open.push((pw, out.len() - 1));
207 }
208 close(&mut open, &mut out, w, last_non_blank);
209 last_non_blank = l;
210 prev = Some((l, w));
211 }
212 close(&mut open, &mut out, 0, last_non_blank);
213 }
214 FoldStrategy::Markdown => {
215 let mut fenced = false;
216 for l in 0..doc.line_count() {
217 let text = doc.line_text(l);
218 if is_fence(&text) {
219 fenced = !fenced;
220 } else if !fenced
221 && let Some(level) = heading_level(&text)
222 && let Some(last) = region(doc, l, strategy, tab_width)
223 {
224 out.push(Region {
225 header: l,
226 level,
227 last,
228 });
229 }
230 }
231 }
232 }
233 out
234}
235
236pub fn enclosing(
239 doc: &Document,
240 line: usize,
241 strategy: FoldStrategy,
242 tab_width: usize,
243) -> Option<(usize, usize)> {
244 if let Some(last) = region(doc, line, strategy, tab_width) {
245 return Some((line, last));
246 }
247 match strategy {
248 FoldStrategy::Indent => {
249 let w = indent(doc, line, tab_width)
251 .or_else(|| next_non_blank(doc, line, tab_width, usize::MAX).map(|(_, w)| w))?;
252 let header = (0..line)
253 .rev()
254 .find(|&l| indent(doc, l, tab_width).is_some_and(|i| i < w))?;
255 let last = region(doc, header, strategy, tab_width)?;
256 (last >= line).then_some((header, last))
257 }
258 FoldStrategy::Markdown => (0..line).rev().find_map(|l| {
259 let last = region(doc, l, strategy, tab_width)?;
260 (last >= line).then_some((l, last))
261 }),
262 }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq)]
266struct Fold {
267 range: Range<ByteOffset>,
270 header: usize,
271 last: usize,
272}
273
274#[derive(Clone, Debug, Default, PartialEq, Eq)]
276pub struct FoldSet {
277 folds: Vec<Fold>,
279 filter: Option<Vec<(usize, usize)>>,
282 hidden: Vec<(usize, usize, usize)>,
286 fold_hidden: Vec<(usize, usize)>,
288 stale: bool,
290}
291
292impl FoldSet {
293 pub fn is_empty(&self) -> bool {
295 self.folds.is_empty()
296 }
297
298 pub fn has_filter(&self) -> bool {
300 self.filter.is_some()
301 }
302
303 pub fn filter_hidden_count(&self) -> usize {
305 self.filter
306 .as_ref()
307 .map_or(0, |f| f.iter().map(|r| r.1 + 1 - r.0).sum())
308 }
309
310 pub fn filter_hides(&self, line: usize) -> bool {
312 self.filter.as_ref().is_some_and(|f| {
313 let i = f.partition_point(|r| r.1 < line);
314 f.get(i).is_some_and(|r| r.0 <= line)
315 })
316 }
317
318 pub fn fold_hidden_range(&self, line: usize) -> Option<(usize, usize)> {
320 let i = self.fold_hidden.partition_point(|h| h.1 < line);
321 self.fold_hidden.get(i).copied().filter(|h| h.0 <= line)
322 }
323
324 pub(crate) fn set_filter(&mut self, shown: Option<&[usize]>, line_count: usize) {
327 self.filter = shown.map(|shown| {
328 let mut ranges = Vec::new();
329 let mut next = 0;
330 for &l in shown.iter().filter(|&&l| l < line_count) {
331 if l > next {
332 ranges.push((next, l - 1));
333 }
334 next = next.max(l + 1);
335 }
336 if next < line_count {
337 ranges.push((next, line_count - 1));
338 }
339 ranges
340 });
341 self.rebuild();
342 }
343
344 pub(crate) fn set_filter_from(&mut self, from: usize, shown: &[usize], line_count: usize) {
347 let Some(f) = &mut self.filter else {
348 return;
349 };
350 f.retain_mut(|r| {
351 r.1 = r.1.min(from.saturating_sub(1));
352 r.0 < from
353 });
354 let mut next = from;
355 let mut push = |a: usize, b: usize| match f.last_mut() {
356 Some(last) if last.1 + 1 == a => last.1 = b,
357 _ => f.push((a, b)),
358 };
359 for &l in shown.iter().filter(|&&l| l >= from && l < line_count) {
360 if l > next {
361 push(next, l - 1);
362 }
363 next = next.max(l + 1);
364 }
365 if next < line_count {
366 push(next, line_count - 1);
367 }
368 self.rebuild();
369 }
370
371 pub(crate) fn unfilter(&mut self, lines: &[usize]) {
373 let Some(f) = &mut self.filter else {
374 return;
375 };
376 let mut changed = false;
377 for &l in lines {
378 let i = f.partition_point(|r| r.1 < l);
379 let Some(&(a, b)) = f.get(i).filter(|r| r.0 <= l) else {
380 continue;
381 };
382 changed = true;
383 let mut parts = Vec::with_capacity(2);
384 if a < l {
385 parts.push((a, l - 1));
386 }
387 if l < b {
388 parts.push((l + 1, b));
389 }
390 f.splice(i..=i, parts);
391 }
392 if changed {
393 self.rebuild();
394 }
395 }
396
397 pub(crate) fn filter_line_edits(&mut self, edits: &[LineEdit]) {
401 let Some(f) = &mut self.filter else {
402 return;
403 };
404 if edits.is_empty() {
405 return;
406 }
407 for e in edits {
408 *f = map_filter_ranges(f, *e);
409 }
410 if !self.stale {
412 self.rebuild();
413 }
414 }
415
416 pub fn headers(&self) -> impl Iterator<Item = usize> + '_ {
418 self.folds.iter().map(|f| f.header)
419 }
420
421 pub fn is_folded(&self, header: usize) -> bool {
422 self.folds.iter().any(|f| f.header == header)
423 }
424
425 pub fn hidden_range(&self, line: usize) -> Option<(usize, usize)> {
427 let i = self.hidden.partition_point(|h| h.1 < line);
428 self.hidden
429 .get(i)
430 .filter(|h| h.0 <= line)
431 .map(|h| (h.0, h.1))
432 }
433
434 pub fn is_hidden(&self, line: usize) -> bool {
435 self.hidden_range(line).is_some()
436 }
437
438 pub fn hidden_count(&self) -> usize {
440 self.hidden.last().map_or(0, |h| h.2 + h.1 + 1 - h.0)
441 }
442
443 pub fn hidden_before(&self, line: usize) -> usize {
445 let i = self.hidden.partition_point(|h| h.0 < line);
446 match i.checked_sub(1).map(|i| self.hidden[i]) {
447 None => 0,
448 Some((start, end, before)) => before + end.min(line - 1) + 1 - start,
449 }
450 }
451
452 pub fn visible_line(&self, index: usize, line_count: usize) -> usize {
455 let i = self.hidden.partition_point(|h| h.0 - h.2 <= index);
458 let line = match i.checked_sub(1).map(|i| self.hidden[i]) {
459 None => index,
460 Some((start, end, before)) => index + before + end + 1 - start,
461 };
462 line.min(line_count.saturating_sub(1))
463 }
464
465 pub(crate) fn insert(
467 &mut self,
468 doc: &Document,
469 regions: impl IntoIterator<Item = (usize, usize)>,
470 ) {
471 for (header, last) in regions {
472 if last > header && last < doc.line_count() {
473 self.folds.push(Fold {
474 range: doc.line_end(header)..doc.line_end(last),
475 header,
476 last,
477 });
478 }
479 }
480 self.folds.sort_by_key(|f| f.range.start);
481 self.folds.dedup_by_key(|f| f.header);
482 self.rebuild();
483 }
484
485 pub(crate) fn remove(&mut self, header: usize) -> bool {
487 let before = self.folds.len();
488 self.folds.retain(|f| f.header != header);
489 self.rebuild();
490 self.folds.len() != before
491 }
492
493 pub(crate) fn retain(&mut self, mut keep: impl FnMut(usize, usize) -> bool) {
495 self.folds.retain(|f| keep(f.header, f.last));
496 self.rebuild();
497 }
498
499 pub(crate) fn map(&mut self, changes: &ChangeSet) {
503 self.stale = true;
504 self.folds.retain_mut(|f| {
505 let touched = changes.edits().iter().any(|e| {
506 if e.start == e.end {
507 f.range.start < e.start && e.start <= f.range.end
508 } else {
509 e.start <= f.range.end && e.end > f.range.start
510 }
511 });
512 f.range = changes.map_position(f.range.start, Bias::After)
513 ..changes.map_position(f.range.end, Bias::Before);
514 !touched
515 });
516 }
517
518 pub(crate) fn relines(&mut self, doc: &Document) {
520 if !std::mem::take(&mut self.stale) {
521 return;
522 }
523 for f in &mut self.folds {
524 f.header = doc.line_of(f.range.start);
525 f.last = doc.line_of(f.range.end);
526 }
527 self.folds.retain(|f| f.last > f.header);
528 self.rebuild();
529 }
530
531 fn rebuild(&mut self) {
532 self.fold_hidden.clear();
533 for f in &self.folds {
534 let (a, b) = (f.header + 1, f.last);
535 match self.fold_hidden.last_mut() {
536 Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
537 _ => self.fold_hidden.push((a, b)),
538 }
539 }
540 let filter: &[(usize, usize)] = self.filter.as_deref().unwrap_or(&[]);
542 let (mut i, mut j) = (0, 0);
543 let mut merged: Vec<(usize, usize)> =
544 Vec::with_capacity(self.fold_hidden.len() + filter.len());
545 while i < self.fold_hidden.len() || j < filter.len() {
546 let take_fold = j >= filter.len()
547 || (i < self.fold_hidden.len() && self.fold_hidden[i].0 <= filter[j].0);
548 let (a, b) = if take_fold {
549 i += 1;
550 self.fold_hidden[i - 1]
551 } else {
552 j += 1;
553 filter[j - 1]
554 };
555 match merged.last_mut() {
556 Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
557 _ => merged.push((a, b)),
558 }
559 }
560 self.hidden.clear();
561 let mut hidden_so_far = 0;
562 for (a, b) in merged {
563 self.hidden.push((a, b, hidden_so_far));
564 hidden_so_far += b + 1 - a;
565 }
566 }
567}
568
569fn map_filter_ranges(ranges: &[(usize, usize)], e: LineEdit) -> Vec<(usize, usize)> {
572 let mut out = Vec::with_capacity(ranges.len() + 1);
573 if e.old_end == usize::MAX {
574 for &(a, b) in ranges {
576 if a < e.start {
577 out.push((a, b.min(e.start - 1)));
578 }
579 }
580 return out;
581 }
582 let shift = |l: usize| (l as isize + e.new_end as isize - e.old_end as isize) as usize;
583 for &(a, b) in ranges {
584 if b < e.start {
585 out.push((a, b));
586 } else if a > e.old_end {
587 out.push((shift(a), shift(b)));
588 } else {
589 if a < e.start {
590 out.push((a, e.start - 1));
591 }
592 if b > e.old_end {
593 out.push((shift(e.old_end + 1), shift(b)));
594 }
595 }
596 }
597 out
598}