1use std::ops::Range;
16
17use serde::{Deserialize, Serialize};
18
19use crate::changeset::{Bias, ChangeSet};
20use crate::position::{ByteOffset, byte_to_visual_col};
21use crate::text::Document;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub enum FoldStrategy {
27 #[default]
28 Indent,
29 Markdown,
31}
32
33const MAX_BLANK_RUN: usize = 1000;
36const FENCE_LOOKBACK: usize = 200;
39
40fn indent(doc: &Document, line: usize, tab_width: usize) -> Option<usize> {
42 let text = doc.line_text(line);
43 let rest = text.trim_start_matches([' ', '\t']);
44 if rest.is_empty() {
45 return None;
46 }
47 let lead = text.len() - rest.len();
48 Some(byte_to_visual_col(doc, ByteOffset(doc.line_start(line).0 + lead), tab_width).0)
49}
50
51fn next_non_blank(
54 doc: &Document,
55 line: usize,
56 tab_width: usize,
57 limit: usize,
58) -> Option<(usize, usize)> {
59 (line + 1..doc.line_count().min((line + 1).saturating_add(limit)))
60 .find_map(|l| indent(doc, l, tab_width).map(|w| (l, w)))
61}
62
63fn heading_level(text: &str) -> Option<usize> {
65 let t = text.strip_prefix(" ").or_else(|| text.strip_prefix(" "));
66 let t = t.or_else(|| text.strip_prefix(' ')).unwrap_or(text);
67 let hashes = t.len() - t.trim_start_matches('#').len();
68 let after = &t[hashes..];
69 ((1..=6).contains(&hashes) && (after.is_empty() || after.starts_with([' ', '\t'])))
70 .then_some(hashes)
71}
72
73fn is_fence(text: &str) -> bool {
74 let t = text.trim_start_matches(' ');
75 text.len() - t.len() <= 3 && (t.starts_with("```") || t.starts_with("~~~"))
76}
77
78fn in_fence(doc: &Document, line: usize) -> bool {
81 let fences = (line.saturating_sub(FENCE_LOOKBACK)..line)
82 .filter(|&l| is_fence(&doc.line_text(l)))
83 .count();
84 fences % 2 == 1
85}
86
87fn markdown_heading(doc: &Document, line: usize) -> Option<usize> {
89 heading_level(&doc.line_text(line)).filter(|_| !in_fence(doc, line))
90}
91
92pub fn region(
95 doc: &Document,
96 header: usize,
97 strategy: FoldStrategy,
98 tab_width: usize,
99) -> Option<usize> {
100 if header >= doc.line_count() {
101 return None;
102 }
103 let mut last = header;
104 match strategy {
105 FoldStrategy::Indent => {
106 let w = indent(doc, header, tab_width)?;
107 for l in header + 1..doc.line_count() {
108 match indent(doc, l, tab_width) {
109 None => {}
110 Some(i) if i <= w => break,
111 Some(_) => last = l,
112 }
113 }
114 }
115 FoldStrategy::Markdown => {
116 let level = markdown_heading(doc, header)?;
117 let mut fenced = false;
118 for l in header + 1..doc.line_count() {
119 let text = doc.line_text(l);
120 if is_fence(&text) {
121 fenced = !fenced;
122 } else if !fenced && heading_level(&text).is_some_and(|h| h <= level) {
123 break;
124 }
125 if fenced || !text.trim().is_empty() {
126 last = l;
127 }
128 }
129 }
130 }
131 (last > header).then_some(last)
132}
133
134pub fn is_header(doc: &Document, line: usize, strategy: FoldStrategy, tab_width: usize) -> bool {
137 if line >= doc.line_count() {
138 return false;
139 }
140 match strategy {
141 FoldStrategy::Indent => indent(doc, line, tab_width).is_some_and(|w| {
142 next_non_blank(doc, line, tab_width, MAX_BLANK_RUN).is_some_and(|(_, n)| n > w)
143 }),
144 FoldStrategy::Markdown => {
145 let Some(level) = heading_level(&doc.line_text(line)) else {
146 return false;
147 };
148 let next = next_non_blank(doc, line, tab_width, MAX_BLANK_RUN);
150 next.is_some_and(|(l, _)| {
151 let text = doc.line_text(l);
152 is_fence(&text) || heading_level(&text).is_none_or(|h| h > level)
153 }) && !in_fence(doc, line)
154 }
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct Region {
163 pub header: usize,
164 pub level: usize,
165 pub last: usize,
166}
167
168pub fn regions(doc: &Document, strategy: FoldStrategy, tab_width: usize) -> Vec<Region> {
170 let mut out = Vec::new();
171 match strategy {
172 FoldStrategy::Indent => {
173 let mut open: Vec<(usize, usize)> = Vec::new();
175 let mut last_non_blank = 0;
176 let mut prev: Option<(usize, usize)> = None;
177 let close = |open: &mut Vec<(usize, usize)>, out: &mut Vec<Region>, w, last| {
178 while let Some(&(o, i)) = open.last() {
179 if o < w {
180 break;
181 }
182 out[i].last = last;
183 open.pop();
184 }
185 };
186 for l in 0..doc.line_count() {
187 let Some(w) = indent(doc, l, tab_width) else {
188 continue;
189 };
190 if let Some((pl, pw)) = prev
193 && w > pw
194 {
195 out.push(Region {
196 header: pl,
197 level: open.len() + 1,
198 last: pl,
199 });
200 open.push((pw, out.len() - 1));
201 }
202 close(&mut open, &mut out, w, last_non_blank);
203 last_non_blank = l;
204 prev = Some((l, w));
205 }
206 close(&mut open, &mut out, 0, last_non_blank);
207 }
208 FoldStrategy::Markdown => {
209 let mut fenced = false;
210 for l in 0..doc.line_count() {
211 let text = doc.line_text(l);
212 if is_fence(&text) {
213 fenced = !fenced;
214 } else if !fenced
215 && let Some(level) = heading_level(&text)
216 && let Some(last) = region(doc, l, strategy, tab_width)
217 {
218 out.push(Region {
219 header: l,
220 level,
221 last,
222 });
223 }
224 }
225 }
226 }
227 out
228}
229
230pub fn enclosing(
233 doc: &Document,
234 line: usize,
235 strategy: FoldStrategy,
236 tab_width: usize,
237) -> Option<(usize, usize)> {
238 if let Some(last) = region(doc, line, strategy, tab_width) {
239 return Some((line, last));
240 }
241 match strategy {
242 FoldStrategy::Indent => {
243 let w = indent(doc, line, tab_width)
245 .or_else(|| next_non_blank(doc, line, tab_width, usize::MAX).map(|(_, w)| w))?;
246 let header = (0..line)
247 .rev()
248 .find(|&l| indent(doc, l, tab_width).is_some_and(|i| i < w))?;
249 let last = region(doc, header, strategy, tab_width)?;
250 (last >= line).then_some((header, last))
251 }
252 FoldStrategy::Markdown => (0..line).rev().find_map(|l| {
253 let last = region(doc, l, strategy, tab_width)?;
254 (last >= line).then_some((l, last))
255 }),
256 }
257}
258
259#[derive(Clone, Debug, PartialEq, Eq)]
260struct Fold {
261 range: Range<ByteOffset>,
264 header: usize,
265 last: usize,
266}
267
268#[derive(Clone, Debug, Default, PartialEq, Eq)]
270pub struct FoldSet {
271 folds: Vec<Fold>,
273 hidden: Vec<(usize, usize, usize)>,
276 stale: bool,
278}
279
280impl FoldSet {
281 pub fn is_empty(&self) -> bool {
282 self.folds.is_empty()
283 }
284
285 pub fn headers(&self) -> impl Iterator<Item = usize> + '_ {
287 self.folds.iter().map(|f| f.header)
288 }
289
290 pub fn is_folded(&self, header: usize) -> bool {
291 self.folds.iter().any(|f| f.header == header)
292 }
293
294 pub fn hidden_range(&self, line: usize) -> Option<(usize, usize)> {
296 let i = self.hidden.partition_point(|h| h.1 < line);
297 self.hidden
298 .get(i)
299 .filter(|h| h.0 <= line)
300 .map(|h| (h.0, h.1))
301 }
302
303 pub fn is_hidden(&self, line: usize) -> bool {
304 self.hidden_range(line).is_some()
305 }
306
307 pub fn hidden_count(&self) -> usize {
309 self.hidden.last().map_or(0, |h| h.2 + h.1 + 1 - h.0)
310 }
311
312 pub fn hidden_before(&self, line: usize) -> usize {
314 let i = self.hidden.partition_point(|h| h.0 < line);
315 match i.checked_sub(1).map(|i| self.hidden[i]) {
316 None => 0,
317 Some((start, end, before)) => before + end.min(line - 1) + 1 - start,
318 }
319 }
320
321 pub fn visible_line(&self, index: usize, line_count: usize) -> usize {
324 let i = self.hidden.partition_point(|h| h.0 - h.2 <= index);
327 let line = match i.checked_sub(1).map(|i| self.hidden[i]) {
328 None => index,
329 Some((start, end, before)) => index + before + end + 1 - start,
330 };
331 line.min(line_count.saturating_sub(1))
332 }
333
334 pub(crate) fn insert(
336 &mut self,
337 doc: &Document,
338 regions: impl IntoIterator<Item = (usize, usize)>,
339 ) {
340 for (header, last) in regions {
341 if last > header && last < doc.line_count() {
342 self.folds.push(Fold {
343 range: doc.line_end(header)..doc.line_end(last),
344 header,
345 last,
346 });
347 }
348 }
349 self.folds.sort_by_key(|f| f.range.start);
350 self.folds.dedup_by_key(|f| f.header);
351 self.rebuild();
352 }
353
354 pub(crate) fn remove(&mut self, header: usize) -> bool {
356 let before = self.folds.len();
357 self.folds.retain(|f| f.header != header);
358 self.rebuild();
359 self.folds.len() != before
360 }
361
362 pub(crate) fn retain(&mut self, mut keep: impl FnMut(usize, usize) -> bool) {
364 self.folds.retain(|f| keep(f.header, f.last));
365 self.rebuild();
366 }
367
368 pub(crate) fn map(&mut self, changes: &ChangeSet) {
372 self.stale = true;
373 self.folds.retain_mut(|f| {
374 let touched = changes.edits().iter().any(|e| {
375 if e.start == e.end {
376 f.range.start < e.start && e.start <= f.range.end
377 } else {
378 e.start <= f.range.end && e.end > f.range.start
379 }
380 });
381 f.range = changes.map_position(f.range.start, Bias::After)
382 ..changes.map_position(f.range.end, Bias::Before);
383 !touched
384 });
385 }
386
387 pub(crate) fn relines(&mut self, doc: &Document) {
389 if !std::mem::take(&mut self.stale) {
390 return;
391 }
392 for f in &mut self.folds {
393 f.header = doc.line_of(f.range.start);
394 f.last = doc.line_of(f.range.end);
395 }
396 self.folds.retain(|f| f.last > f.header);
397 self.rebuild();
398 }
399
400 fn rebuild(&mut self) {
401 self.hidden.clear();
402 let mut hidden_so_far = 0;
403 let mut merged: Vec<(usize, usize)> = Vec::new();
404 for f in &self.folds {
405 let (a, b) = (f.header + 1, f.last);
406 match merged.last_mut() {
407 Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
408 _ => merged.push((a, b)),
409 }
410 }
411 for (a, b) in merged {
412 self.hidden.push((a, b, hidden_so_far));
413 hidden_so_far += b + 1 - a;
414 }
415 }
416}