use std::collections::HashSet;
use std::ops::Range;
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
}
pub fn extent_for_offset(
headings: &[HeadingInfo],
list_items: &[ListItemInfo],
offset: usize,
line_count: usize,
) -> Option<Range<usize>> {
if let Some(idx) = headings.iter().position(|h| h.byte_offset == offset) {
return Some(heading_extent(headings, idx, line_count));
}
let idx = list_items.iter().position(|i| i.byte_offset == offset)?;
Some(list_item_extent(list_items, idx))
}
pub fn hidden_line_ranges(
headings: &[HeadingInfo],
list_items: &[ListItemInfo],
folded: &HashSet<usize>,
line_count: usize,
) -> Vec<Range<usize>> {
if folded.is_empty() {
return Vec::new();
}
let mut ranges: Vec<Range<usize>> = headings
.iter()
.enumerate()
.filter(|(_, h)| folded.contains(&h.byte_offset))
.map(|(i, _)| heading_extent(headings, i, line_count))
.chain(
list_items
.iter()
.enumerate()
.filter(|(_, li)| folded.contains(&li.byte_offset))
.map(|(i, _)| list_item_extent(list_items, i)),
)
.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,
}
}
#[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(&hs, &[], &folded, 10), 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(&hs, &[], &folded, 10), 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(&[], &items, &folded, 10), 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(&hs, &items, &folded, 3),
vec![1..3, 6..8]
);
}
#[test]
fn extent_for_offset_resolves_both_kinds() {
let hs = [h(1, 0)];
let items = [li(5, 8)];
assert_eq!(extent_for_offset(&hs, &items, 0, 3), Some(1..3)); assert_eq!(extent_for_offset(&hs, &items, 500, 3), Some(6..8)); assert_eq!(extent_for_offset(&hs, &items, 999, 3), 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));
}
}