use std::collections::HashSet;
use std::ops::Range;
use crate::callout::CalloutInfo;
use crate::marker::{HeadingInfo, ListItemInfo};
pub fn heading_extent(headings: &[HeadingInfo], idx: usize, line_count: usize) -> Range<usize> {
let h = &headings[idx];
let start = h.line + 1;
let mut end = line_count;
for next in &headings[idx + 1..] {
if next.level <= h.level {
end = next.line;
break;
}
}
start..end.max(start)
}
pub fn heading_is_foldable(headings: &[HeadingInfo], idx: usize, line_count: usize) -> bool {
let h = &headings[idx];
match headings.get(idx + 1) {
None => h.line + 1 < line_count,
Some(next) if next.level > h.level => true,
Some(next) => next.line > h.line + 1,
}
}
pub fn list_item_extent(items: &[ListItemInfo], idx: usize) -> Range<usize> {
let item = &items[idx];
(item.line + 1)..item.fold_end_line.max(item.line + 1)
}
pub fn list_item_is_foldable(items: &[ListItemInfo], idx: usize) -> bool {
items[idx].fold_end_line > items[idx].line + 1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FoldKind {
Heading,
List,
Callout,
}
#[derive(Debug, Clone)]
pub struct FoldAnchor {
pub byte_offset: usize,
pub line: usize,
pub extent: Range<usize>,
pub kind: FoldKind,
pub level: u32,
}
pub fn fold_anchors(
headings: &[HeadingInfo],
list_items: &[ListItemInfo],
callouts: &[CalloutInfo],
line_count: usize,
) -> Vec<FoldAnchor> {
let mut anchors = Vec::new();
for (i, h) in headings.iter().enumerate() {
if heading_is_foldable(headings, i, line_count) {
anchors.push(FoldAnchor {
byte_offset: h.byte_offset,
line: h.line,
extent: heading_extent(headings, i, line_count),
kind: FoldKind::Heading,
level: h.level as u32,
});
}
}
for (i, li) in list_items.iter().enumerate() {
if list_item_is_foldable(list_items, i) {
anchors.push(FoldAnchor {
byte_offset: li.byte_offset,
line: li.line,
extent: list_item_extent(list_items, i),
kind: FoldKind::List,
level: li.depth as u32,
});
}
}
for c in callouts {
if c.is_foldable() {
anchors.push(FoldAnchor {
byte_offset: c.byte_offset,
line: c.header_line,
extent: c.body_extent(),
kind: FoldKind::Callout,
level: c.depth as u32,
});
}
}
anchors
}
pub fn extent_for_offset(anchors: &[FoldAnchor], offset: usize) -> Option<Range<usize>> {
anchors
.iter()
.find(|a| a.byte_offset == offset)
.map(|a| a.extent.clone())
}
pub fn hidden_line_ranges(anchors: &[FoldAnchor], folded: &HashSet<usize>) -> Vec<Range<usize>> {
let mut ranges: Vec<Range<usize>> = anchors
.iter()
.filter(|a| folded.contains(&a.byte_offset))
.map(|a| a.extent.clone())
.filter(|r| r.end > r.start)
.collect();
ranges.sort_by_key(|r| r.start);
let mut merged: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
for r in ranges {
match merged.last_mut() {
Some(last) if r.start <= last.end => last.end = last.end.max(r.end),
_ => merged.push(r),
}
}
merged
}
pub fn section_heading(headings: &[HeadingInfo], line: usize) -> Option<usize> {
headings.iter().rposition(|h| h.line <= line)
}
#[cfg(test)]
mod tests {
use super::*;
fn h(level: u8, line: usize) -> HeadingInfo {
HeadingInfo {
level,
text: String::new(),
line,
byte_offset: line * 100,
}
}
fn li(line: usize, fold_end_line: usize) -> ListItemInfo {
ListItemInfo {
line,
byte_offset: line * 100,
fold_end_line,
depth: 1,
}
}
fn callout(header_line: usize, end_line: usize, foldable: bool) -> CalloutInfo {
CalloutInfo {
kind: crate::callout::CalloutKind::Note,
title: "Note".into(),
foldable,
default_collapsed: false,
header_line,
byte_offset: header_line * 100,
end_line,
depth: 1,
}
}
#[test]
fn callout_extent_and_union() {
let hs = [h(1, 0)];
let cs = [callout(5, 8, true)];
let anchors = fold_anchors(&hs, &[], &cs, 3);
let folded: HashSet<usize> = [hs[0].byte_offset, cs[0].byte_offset].into();
assert_eq!(hidden_line_ranges(&anchors, &folded), vec![1..3, 6..8]);
assert_eq!(extent_for_offset(&anchors, 500), Some(6..8));
}
#[test]
fn non_opted_in_callout_never_folds() {
let cs = [callout(2, 5, false)];
let anchors = fold_anchors(&[], &[], &cs, 10);
assert!(anchors.is_empty());
let folded: HashSet<usize> = [cs[0].byte_offset].into();
assert!(hidden_line_ranges(&anchors, &folded).is_empty());
}
#[test]
fn extent_stops_at_same_level_sibling() {
let hs = [h(1, 0), h(1, 3)];
assert_eq!(heading_extent(&hs, 0, 10), 1..3);
assert_eq!(heading_extent(&hs, 1, 10), 4..10); }
#[test]
fn extent_swallows_deeper_subheadings() {
let hs = [h(1, 0), h(2, 2), h(3, 4), h(1, 6)];
assert_eq!(heading_extent(&hs, 0, 10), 1..6); assert_eq!(heading_extent(&hs, 1, 10), 3..6); assert_eq!(heading_extent(&hs, 3, 10), 7..10);
}
#[test]
fn extent_empty_when_no_body() {
let hs = [h(1, 0), h(1, 1)];
assert_eq!(heading_extent(&hs, 0, 5), 1..1);
assert!(!heading_is_foldable(&hs, 0, 5));
assert!(heading_is_foldable(&hs, 1, 5));
}
#[test]
fn nested_folds_coalesce() {
let hs = [h(1, 0), h(2, 2), h(3, 4), h(1, 6)];
let folded: HashSet<usize> = [hs[0].byte_offset, hs[1].byte_offset].into();
assert_eq!(
hidden_line_ranges(&fold_anchors(&hs, &[], &[], 10), &folded),
vec![1..6]
);
}
#[test]
fn disjoint_folds_stay_separate() {
let hs = [h(1, 0), h(1, 3), h(1, 6)];
let folded: HashSet<usize> = [hs[0].byte_offset, hs[2].byte_offset].into();
assert_eq!(
hidden_line_ranges(&fold_anchors(&hs, &[], &[], 10), &folded),
vec![1..3, 7..10]
);
}
#[test]
fn list_item_extent_and_foldability() {
let items = [li(0, 3), li(1, 3), li(2, 3)];
assert_eq!(list_item_extent(&items, 0), 1..3);
assert!(list_item_is_foldable(&items, 0));
assert_eq!(list_item_extent(&items, 1), 2..3);
assert!(list_item_is_foldable(&items, 1));
assert!(!list_item_is_foldable(&items, 2)); }
#[test]
fn nested_list_folds_coalesce() {
let items = [li(0, 3), li(1, 3)];
let folded: HashSet<usize> = [items[0].byte_offset, items[1].byte_offset].into();
assert_eq!(
hidden_line_ranges(&fold_anchors(&[], &items, &[], 10), &folded),
vec![1..3]
);
}
#[test]
fn mixed_heading_and_list_union() {
let hs = [h(1, 0)]; let items = [li(5, 8)]; let folded: HashSet<usize> = [hs[0].byte_offset, items[0].byte_offset].into();
assert_eq!(
hidden_line_ranges(&fold_anchors(&hs, &items, &[], 3), &folded),
vec![1..3, 6..8]
);
}
#[test]
fn extent_for_offset_resolves_all_kinds() {
let hs = [h(1, 0)];
let items = [li(5, 8)];
let cs = [callout(9, 12, true)];
let anchors = fold_anchors(&hs, &items, &cs, 3);
assert_eq!(extent_for_offset(&anchors, 0), Some(1..3)); assert_eq!(extent_for_offset(&anchors, 500), Some(6..8)); assert_eq!(extent_for_offset(&anchors, 900), Some(10..12)); assert_eq!(extent_for_offset(&anchors, 999), None); }
#[test]
fn section_heading_finds_enclosing() {
let hs = [h(1, 0), h(2, 4), h(1, 8)];
assert_eq!(section_heading(&hs, 0), Some(0));
assert_eq!(section_heading(&hs, 5), Some(1));
assert_eq!(section_heading(&hs, 9), Some(2));
}
}