use std::ops::Range;
use serde::{Deserialize, Serialize};
use crate::buffer::LineEdit;
use crate::changeset::{Bias, ChangeSet};
use crate::position::{ByteOffset, byte_to_visual_col};
use crate::text::Document;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum FoldStrategy {
#[default]
Indent,
Markdown,
}
const MAX_BLANK_RUN: usize = 1000;
const FENCE_LOOKBACK: usize = 200;
fn indent(doc: &Document, line: usize, tab_width: usize) -> Option<usize> {
let text = doc.line_text(line);
let rest = text.trim_start_matches([' ', '\t']);
if rest.is_empty() {
return None;
}
let lead = text.len() - rest.len();
Some(byte_to_visual_col(doc, ByteOffset(doc.line_start(line).0 + lead), tab_width).0)
}
fn next_non_blank(
doc: &Document,
line: usize,
tab_width: usize,
limit: usize,
) -> Option<(usize, usize)> {
(line + 1..doc.line_count().min((line + 1).saturating_add(limit)))
.find_map(|l| indent(doc, l, tab_width).map(|w| (l, w)))
}
fn heading_level(text: &str) -> Option<usize> {
let t = text.strip_prefix(" ").or_else(|| text.strip_prefix(" "));
let t = t.or_else(|| text.strip_prefix(' ')).unwrap_or(text);
let hashes = t.len() - t.trim_start_matches('#').len();
let after = &t[hashes..];
((1..=6).contains(&hashes) && (after.is_empty() || after.starts_with([' ', '\t'])))
.then_some(hashes)
}
fn is_fence(text: &str) -> bool {
let t = text.trim_start_matches(' ');
text.len() - t.len() <= 3 && (t.starts_with("```") || t.starts_with("~~~"))
}
fn in_fence(doc: &Document, line: usize) -> bool {
let fences = (line.saturating_sub(FENCE_LOOKBACK)..line)
.filter(|&l| is_fence(&doc.line_text(l)))
.count();
fences % 2 == 1
}
fn markdown_heading(doc: &Document, line: usize) -> Option<usize> {
heading_level(&doc.line_text(line)).filter(|_| !in_fence(doc, line))
}
pub fn region(
doc: &Document,
header: usize,
strategy: FoldStrategy,
tab_width: usize,
) -> Option<usize> {
if header >= doc.line_count() {
return None;
}
let mut last = header;
match strategy {
FoldStrategy::Indent => {
let w = indent(doc, header, tab_width)?;
for l in header + 1..doc.line_count() {
match indent(doc, l, tab_width) {
None => {}
Some(i) if i <= w => break,
Some(_) => last = l,
}
}
}
FoldStrategy::Markdown => {
let level = markdown_heading(doc, header)?;
let mut fenced = false;
for l in header + 1..doc.line_count() {
let text = doc.line_text(l);
if is_fence(&text) {
fenced = !fenced;
} else if !fenced && heading_level(&text).is_some_and(|h| h <= level) {
break;
}
if fenced || !text.trim().is_empty() {
last = l;
}
}
}
}
(last > header).then_some(last)
}
pub fn is_header(doc: &Document, line: usize, strategy: FoldStrategy, tab_width: usize) -> bool {
if line >= doc.line_count() {
return false;
}
match strategy {
FoldStrategy::Indent => indent(doc, line, tab_width).is_some_and(|w| {
next_non_blank(doc, line, tab_width, MAX_BLANK_RUN).is_some_and(|(_, n)| n > w)
}),
FoldStrategy::Markdown => {
let Some(level) = heading_level(&doc.line_text(line)) else {
return false;
};
let next = next_non_blank(doc, line, tab_width, MAX_BLANK_RUN);
next.is_some_and(|(l, _)| {
let text = doc.line_text(l);
is_fence(&text) || heading_level(&text).is_none_or(|h| h > level)
}) && !in_fence(doc, line)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Region {
pub header: usize,
pub level: usize,
pub last: usize,
}
pub fn regions(doc: &Document, strategy: FoldStrategy, tab_width: usize) -> Vec<Region> {
let mut out = Vec::new();
match strategy {
FoldStrategy::Indent => {
let mut open: Vec<(usize, usize)> = Vec::new();
let mut last_non_blank = 0;
let mut prev: Option<(usize, usize)> = None;
let close = |open: &mut Vec<(usize, usize)>, out: &mut Vec<Region>, w, last| {
while let Some(&(o, i)) = open.last() {
if o < w {
break;
}
out[i].last = last;
open.pop();
}
};
for l in 0..doc.line_count() {
let Some(w) = indent(doc, l, tab_width) else {
continue;
};
if let Some((pl, pw)) = prev
&& w > pw
{
out.push(Region {
header: pl,
level: open.len() + 1,
last: pl,
});
open.push((pw, out.len() - 1));
}
close(&mut open, &mut out, w, last_non_blank);
last_non_blank = l;
prev = Some((l, w));
}
close(&mut open, &mut out, 0, last_non_blank);
}
FoldStrategy::Markdown => {
let mut fenced = false;
for l in 0..doc.line_count() {
let text = doc.line_text(l);
if is_fence(&text) {
fenced = !fenced;
} else if !fenced
&& let Some(level) = heading_level(&text)
&& let Some(last) = region(doc, l, strategy, tab_width)
{
out.push(Region {
header: l,
level,
last,
});
}
}
}
}
out
}
pub fn enclosing(
doc: &Document,
line: usize,
strategy: FoldStrategy,
tab_width: usize,
) -> Option<(usize, usize)> {
if let Some(last) = region(doc, line, strategy, tab_width) {
return Some((line, last));
}
match strategy {
FoldStrategy::Indent => {
let w = indent(doc, line, tab_width)
.or_else(|| next_non_blank(doc, line, tab_width, usize::MAX).map(|(_, w)| w))?;
let header = (0..line)
.rev()
.find(|&l| indent(doc, l, tab_width).is_some_and(|i| i < w))?;
let last = region(doc, header, strategy, tab_width)?;
(last >= line).then_some((header, last))
}
FoldStrategy::Markdown => (0..line).rev().find_map(|l| {
let last = region(doc, l, strategy, tab_width)?;
(last >= line).then_some((l, last))
}),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Fold {
range: Range<ByteOffset>,
header: usize,
last: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FoldSet {
folds: Vec<Fold>,
filter: Option<Vec<(usize, usize)>>,
hidden: Vec<(usize, usize, usize)>,
fold_hidden: Vec<(usize, usize)>,
stale: bool,
}
impl FoldSet {
pub fn is_empty(&self) -> bool {
self.folds.is_empty()
}
pub fn has_filter(&self) -> bool {
self.filter.is_some()
}
pub fn filter_hidden_count(&self) -> usize {
self.filter
.as_ref()
.map_or(0, |f| f.iter().map(|r| r.1 + 1 - r.0).sum())
}
pub fn filter_hides(&self, line: usize) -> bool {
self.filter.as_ref().is_some_and(|f| {
let i = f.partition_point(|r| r.1 < line);
f.get(i).is_some_and(|r| r.0 <= line)
})
}
pub fn fold_hidden_range(&self, line: usize) -> Option<(usize, usize)> {
let i = self.fold_hidden.partition_point(|h| h.1 < line);
self.fold_hidden.get(i).copied().filter(|h| h.0 <= line)
}
pub(crate) fn set_filter(&mut self, shown: Option<&[usize]>, line_count: usize) {
self.filter = shown.map(|shown| {
let mut ranges = Vec::new();
let mut next = 0;
for &l in shown.iter().filter(|&&l| l < line_count) {
if l > next {
ranges.push((next, l - 1));
}
next = next.max(l + 1);
}
if next < line_count {
ranges.push((next, line_count - 1));
}
ranges
});
self.rebuild();
}
pub(crate) fn set_filter_from(&mut self, from: usize, shown: &[usize], line_count: usize) {
let Some(f) = &mut self.filter else {
return;
};
f.retain_mut(|r| {
r.1 = r.1.min(from.saturating_sub(1));
r.0 < from
});
let mut next = from;
let mut push = |a: usize, b: usize| match f.last_mut() {
Some(last) if last.1 + 1 == a => last.1 = b,
_ => f.push((a, b)),
};
for &l in shown.iter().filter(|&&l| l >= from && l < line_count) {
if l > next {
push(next, l - 1);
}
next = next.max(l + 1);
}
if next < line_count {
push(next, line_count - 1);
}
self.rebuild();
}
pub(crate) fn unfilter(&mut self, lines: &[usize]) {
let Some(f) = &mut self.filter else {
return;
};
let mut changed = false;
for &l in lines {
let i = f.partition_point(|r| r.1 < l);
let Some(&(a, b)) = f.get(i).filter(|r| r.0 <= l) else {
continue;
};
changed = true;
let mut parts = Vec::with_capacity(2);
if a < l {
parts.push((a, l - 1));
}
if l < b {
parts.push((l + 1, b));
}
f.splice(i..=i, parts);
}
if changed {
self.rebuild();
}
}
pub(crate) fn filter_line_edits(&mut self, edits: &[LineEdit]) {
let Some(f) = &mut self.filter else {
return;
};
if edits.is_empty() {
return;
}
for e in edits {
*f = map_filter_ranges(f, *e);
}
if !self.stale {
self.rebuild();
}
}
pub fn headers(&self) -> impl Iterator<Item = usize> + '_ {
self.folds.iter().map(|f| f.header)
}
pub fn is_folded(&self, header: usize) -> bool {
self.folds.iter().any(|f| f.header == header)
}
pub fn hidden_range(&self, line: usize) -> Option<(usize, usize)> {
let i = self.hidden.partition_point(|h| h.1 < line);
self.hidden
.get(i)
.filter(|h| h.0 <= line)
.map(|h| (h.0, h.1))
}
pub fn is_hidden(&self, line: usize) -> bool {
self.hidden_range(line).is_some()
}
pub fn hidden_count(&self) -> usize {
self.hidden.last().map_or(0, |h| h.2 + h.1 + 1 - h.0)
}
pub fn hidden_before(&self, line: usize) -> usize {
let i = self.hidden.partition_point(|h| h.0 < line);
match i.checked_sub(1).map(|i| self.hidden[i]) {
None => 0,
Some((start, end, before)) => before + end.min(line - 1) + 1 - start,
}
}
pub fn visible_line(&self, index: usize, line_count: usize) -> usize {
let i = self.hidden.partition_point(|h| h.0 - h.2 <= index);
let line = match i.checked_sub(1).map(|i| self.hidden[i]) {
None => index,
Some((start, end, before)) => index + before + end + 1 - start,
};
line.min(line_count.saturating_sub(1))
}
pub(crate) fn insert(
&mut self,
doc: &Document,
regions: impl IntoIterator<Item = (usize, usize)>,
) {
for (header, last) in regions {
if last > header && last < doc.line_count() {
self.folds.push(Fold {
range: doc.line_end(header)..doc.line_end(last),
header,
last,
});
}
}
self.folds.sort_by_key(|f| f.range.start);
self.folds.dedup_by_key(|f| f.header);
self.rebuild();
}
pub(crate) fn remove(&mut self, header: usize) -> bool {
let before = self.folds.len();
self.folds.retain(|f| f.header != header);
self.rebuild();
self.folds.len() != before
}
pub(crate) fn retain(&mut self, mut keep: impl FnMut(usize, usize) -> bool) {
self.folds.retain(|f| keep(f.header, f.last));
self.rebuild();
}
pub(crate) fn map(&mut self, changes: &ChangeSet) {
self.stale = true;
self.folds.retain_mut(|f| {
let touched = changes.edits().iter().any(|e| {
if e.start == e.end {
f.range.start < e.start && e.start <= f.range.end
} else {
e.start <= f.range.end && e.end > f.range.start
}
});
f.range = changes.map_position(f.range.start, Bias::After)
..changes.map_position(f.range.end, Bias::Before);
!touched
});
}
pub(crate) fn relines(&mut self, doc: &Document) {
if !std::mem::take(&mut self.stale) {
return;
}
for f in &mut self.folds {
f.header = doc.line_of(f.range.start);
f.last = doc.line_of(f.range.end);
}
self.folds.retain(|f| f.last > f.header);
self.rebuild();
}
fn rebuild(&mut self) {
self.fold_hidden.clear();
for f in &self.folds {
let (a, b) = (f.header + 1, f.last);
match self.fold_hidden.last_mut() {
Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
_ => self.fold_hidden.push((a, b)),
}
}
let filter: &[(usize, usize)] = self.filter.as_deref().unwrap_or(&[]);
let (mut i, mut j) = (0, 0);
let mut merged: Vec<(usize, usize)> =
Vec::with_capacity(self.fold_hidden.len() + filter.len());
while i < self.fold_hidden.len() || j < filter.len() {
let take_fold = j >= filter.len()
|| (i < self.fold_hidden.len() && self.fold_hidden[i].0 <= filter[j].0);
let (a, b) = if take_fold {
i += 1;
self.fold_hidden[i - 1]
} else {
j += 1;
filter[j - 1]
};
match merged.last_mut() {
Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
_ => merged.push((a, b)),
}
}
self.hidden.clear();
let mut hidden_so_far = 0;
for (a, b) in merged {
self.hidden.push((a, b, hidden_so_far));
hidden_so_far += b + 1 - a;
}
}
}
fn map_filter_ranges(ranges: &[(usize, usize)], e: LineEdit) -> Vec<(usize, usize)> {
let mut out = Vec::with_capacity(ranges.len() + 1);
if e.old_end == usize::MAX {
for &(a, b) in ranges {
if a < e.start {
out.push((a, b.min(e.start - 1)));
}
}
return out;
}
let shift = |l: usize| (l as isize + e.new_end as isize - e.old_end as isize) as usize;
for &(a, b) in ranges {
if b < e.start {
out.push((a, b));
} else if a > e.old_end {
out.push((shift(a), shift(b)));
} else {
if a < e.start {
out.push((a, e.start - 1));
}
if b > e.old_end {
out.push((shift(e.old_end + 1), shift(b)));
}
}
}
out
}