use std::ops::Range;
use super::buffer::Buffer;
use crate::render::markdown::block::{BlockKind, Marker};
pub fn indent_cols(line: &str, tab_width: usize) -> usize {
line.chars()
.map_while(|c| match c {
' ' => Some(1),
'\t' => Some(tab_width.max(1)),
_ => None,
})
.sum()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Renumber {
pub line: usize,
pub cols: Range<usize>,
pub number: String,
}
pub struct Lists<'a> {
buffer: &'a Buffer,
kinds: &'a [BlockKind],
tab_width: usize,
}
impl<'a> Lists<'a> {
pub fn new(buffer: &'a Buffer, kinds: &'a [BlockKind], tab_width: usize) -> Self {
Lists {
buffer,
kinds,
tab_width,
}
}
pub fn item_bounds(&self, line: usize) -> Option<(usize, usize)> {
self.item_at(line).map(|s| self.bounds(s))
}
pub fn own_bounds(&self, line: usize) -> Option<(usize, usize)> {
let start = self.item_at(line)?;
let mut e = match self.first_child(start) {
Some(child) => child - 1,
None => self.bounds(start).1,
};
while e > start && self.blank(e) {
e -= 1;
}
Some((start, e))
}
pub fn first_child(&self, line: usize) -> Option<usize> {
let start = self.item_at(line)?;
let (_, end) = self.bounds(start);
let d = self.indent(start);
(start + 1..=end).find(|&l| self.is_item(l) && self.indent(l) > d)
}
pub fn parent(&self, line: usize) -> Option<usize> {
self.parent_of(self.item_at(line)?)
}
pub fn prev_sibling_bounds(&self, line: usize) -> Option<(usize, usize)> {
self.prev_sibling(self.item_at(line)?).map(|s| self.bounds(s))
}
pub fn next_sibling_bounds(&self, line: usize) -> Option<(usize, usize)> {
self.next_sibling(self.item_at(line)?).map(|s| self.bounds(s))
}
pub fn level_bounds(&self, line: usize) -> Option<(usize, usize)> {
let items = self.level_items(line);
Some((*items.first()?, self.bounds(*items.last()?).1))
}
pub fn level_items(&self, line: usize) -> Vec<usize> {
let Some(item) = self.item_at(line) else {
return Vec::new();
};
let mut items = vec![item];
while let Some(p) = self.prev_sibling(items[0]) {
items.insert(0, p);
}
while let Some(n) = self.next_sibling(*items.last().unwrap_or(&item)) {
items.push(n);
}
items
}
pub fn step(&self, line: usize, skip: &[usize]) -> Option<usize> {
let last = self.buffer.last_line();
let near = (0..=last).flat_map(|d| {
let below = (d > 0).then(|| line.checked_add(d)).flatten();
[line.checked_sub(d), below]
});
near.flatten()
.filter(|&l| l <= last && self.is_item(l) && !skip.contains(&l))
.find_map(|l| {
let parent = self.parent_of(l)?;
Some(self.indent(l) - self.indent(parent))
})
}
pub fn item_indent(&self, line: usize) -> Option<usize> {
self.item_at(line).map(|s| self.indent(s))
}
pub fn ordinal(&self, line: usize) -> Option<(u64, Range<usize>)> {
if !self.ordered(line) {
return None;
}
let text = self.buffer.line_text(line);
let ws = text.chars().take_while(|c| *c == ' ' || *c == '\t').count();
let digits: String = text.chars().skip(ws).take_while(char::is_ascii_digit).collect();
Some((digits.parse().ok()?, ws..ws + digits.len()))
}
pub fn renumber(&self, line: usize, start: Option<u64>) -> Vec<Renumber> {
let ordinals: Option<Vec<(usize, u64, Range<usize>)>> = self
.level_items(line)
.into_iter()
.map(|l| self.ordinal(l).map(|(n, cols)| (l, n, cols)))
.collect();
let Some(ordinals) = ordinals.filter(|o| !o.is_empty()) else {
return Vec::new();
};
let first = start.unwrap_or(ordinals[0].1);
ordinals
.into_iter()
.zip(first..)
.filter(|((_, n, _), want)| n != want)
.map(|((line, _, cols), want)| Renumber {
line,
cols,
number: want.to_string(),
})
.collect()
}
fn kind(&self, line: usize) -> Option<&BlockKind> {
self.kinds.get(line)
}
fn is_item(&self, line: usize) -> bool {
matches!(self.kind(line), Some(BlockKind::ListItem { .. }))
}
fn ordered(&self, line: usize) -> bool {
matches!(
self.kind(line),
Some(BlockKind::ListItem {
marker: Marker::Ordered,
..
})
)
}
fn blank(&self, line: usize) -> bool {
matches!(self.kind(line), Some(BlockKind::Blank))
}
fn indent(&self, line: usize) -> usize {
indent_cols(&self.buffer.line_text(line), self.tab_width)
}
fn bounds(&self, start: usize) -> (usize, usize) {
let last = self.buffer.last_line();
let d = self.indent(start);
let mut end = start;
let mut in_fence = false;
let mut i = start + 1;
while i <= last {
let Some(kind) = self.kind(i) else { break };
match kind {
BlockKind::FenceBody { .. } | BlockKind::FenceClose if in_fence => {
in_fence = !matches!(kind, BlockKind::FenceClose);
}
BlockKind::Blank => {
let mut j = i + 1;
while j <= last && self.blank(j) {
j += 1;
}
if j > last || self.indent(j) <= d {
break;
}
i = j;
continue;
}
_ if self.indent(i) > d => {
in_fence = matches!(kind, BlockKind::FenceOpen(_));
}
BlockKind::Paragraph
if matches!(
self.kind(i - 1),
Some(BlockKind::Paragraph | BlockKind::ListItem { .. })
) => {}
_ => break,
}
end = i;
i += 1;
}
(start, end)
}
fn item_at(&self, line: usize) -> Option<usize> {
let mut k = (0..=line).rev().find(|&l| self.is_item(l))?;
while self.bounds(k).1 < line {
k = self.shallower_above(k)?;
}
Some(k)
}
fn shallower_above(&self, line: usize) -> Option<usize> {
let d = self.indent(line);
if d == 0 {
return None;
}
(0..line)
.rev()
.find(|&l| self.is_item(l) && self.indent(l) < d)
}
fn parent_of(&self, item: usize) -> Option<usize> {
let mut k = self.shallower_above(item)?;
while self.bounds(k).1 < item {
k = self.shallower_above(k)?;
}
Some(k)
}
fn same_list(&self, a: usize, b: usize) -> bool {
self.ordered(a) == self.ordered(b) && self.parent_of(a) == self.parent_of(b)
}
fn next_sibling(&self, item: usize) -> Option<usize> {
let last = self.buffer.last_line();
let mut j = self.bounds(item).1 + 1;
while j <= last && self.blank(j) {
j += 1;
}
(j <= last && self.is_item(j) && self.same_list(item, j)).then_some(j)
}
fn prev_sibling(&self, item: usize) -> Option<usize> {
let mut p = item.checked_sub(1)?;
while self.blank(p) {
p = p.checked_sub(1)?;
}
let parent = self.parent_of(item);
let mut x = self.item_at(p)?;
while self.parent_of(x) != parent {
if Some(x) == parent {
return None;
}
x = self.parent_of(x)?;
}
(self.next_sibling(x) == Some(item)).then_some(x)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::markdown::block::BlockCache;
use ropey::Rope;
fn buf(text: &str) -> Buffer {
let mut b = Buffer::empty();
b.rope = Rope::from_str(text);
b
}
fn with<T>(text: &str, f: impl FnOnce(&Lists) -> T) -> T {
let b = buf(text);
let cache = BlockCache::build(&b);
f(&Lists::new(&b, &cache.kinds, 4))
}
#[test]
fn indent_counts_a_tab_as_tab_width() {
assert_eq!(indent_cols(" - a", 4), 2);
assert_eq!(indent_cols("\t- a", 4), 4);
assert_eq!(indent_cols(" \t- a", 2), 3);
assert_eq!(indent_cols("- a", 4), 0);
}
#[test]
fn tight_list_items_carry_their_children() {
let t = "- a\n - a1\n - a2\n - a2x\n- b\n";
with(t, |l| {
assert_eq!(l.item_bounds(0), Some((0, 3)));
assert_eq!(l.item_bounds(1), Some((1, 1)));
assert_eq!(l.item_bounds(2), Some((2, 3)));
assert_eq!(l.item_bounds(4), Some((4, 4)));
});
}
#[test]
fn loose_items_keep_inner_blanks_but_never_trailing_ones() {
let t = "- a\n\n - a1\n\n- b\n\nafter\n";
with(t, |l| {
assert_eq!(l.item_bounds(0), Some((0, 2)));
assert_eq!(l.item_bounds(1), Some((0, 2)));
assert_eq!(l.item_bounds(3), None);
assert_eq!(l.item_bounds(4), Some((4, 4)));
assert_eq!(l.item_bounds(6), None);
});
}
#[test]
fn lazy_continuation_under_a_nested_item() {
let t = "- a\n - b\nlazy\n- c\n";
with(t, |l| {
assert_eq!(l.item_bounds(1), Some((1, 2)));
assert_eq!(l.item_bounds(0), Some((0, 2)));
assert_eq!(l.item_bounds(2), Some((1, 2)));
});
}
#[test]
fn fence_body_less_indented_than_the_item_stays_in_it() {
let t = "- a\n ```\nbody\n ```\n- b\n";
with(t, |l| {
assert_eq!(l.item_bounds(0), Some((0, 3)));
assert_eq!(l.item_bounds(2), Some((0, 3)));
assert_eq!(l.next_sibling_bounds(0), Some((4, 4)));
});
}
#[test]
fn child_paragraph_and_continuation_lines() {
let t = "- a\n wrapped\n\n para\n- b\n";
with(t, |l| {
assert_eq!(l.item_bounds(0), Some((0, 3)));
assert_eq!(l.item_bounds(1), Some((0, 3)));
assert_eq!(l.item_bounds(3), Some((0, 3)));
});
}
#[test]
fn a_list_ends_at_a_heading_rule_or_table_without_a_blank() {
for t in [
"- a\n# h\n",
"- a\n---\n",
"- a\n| x | y |\n|---|---|\n",
] {
with(t, |l| assert_eq!(l.item_bounds(0), Some((0, 0)), "{t:?}"));
}
}
#[test]
fn outside_any_list_there_is_no_item() {
with("para\n\n# h\n", |l| {
assert_eq!(l.item_bounds(0), None);
assert_eq!(l.parent(0), None);
assert_eq!(l.level_bounds(2), None);
});
}
#[test]
fn last_item_without_a_trailing_newline() {
with("- a\n- b\n - c", |l| {
assert_eq!(l.item_bounds(1), Some((1, 2)));
assert_eq!(l.prev_sibling_bounds(2), None);
assert_eq!(l.prev_sibling_bounds(1), Some((0, 0)));
});
}
#[test]
fn inconsistent_child_indents_are_still_siblings() {
let t = "- a\n - b\n - c\n";
with(t, |l| {
assert_eq!(l.parent(1), Some(0));
assert_eq!(l.parent(2), Some(0));
assert_eq!(l.next_sibling_bounds(1), Some((2, 2)));
assert_eq!(l.prev_sibling_bounds(2), Some((1, 1)));
assert_eq!(l.item_bounds(0), Some((0, 2)));
});
}
#[test]
fn tabs_and_spaces_nest_by_columns() {
let t = "- a\n - x\n\t- b\n - c\n";
with(t, |l| {
assert_eq!(l.parent(2), Some(1));
assert_eq!(l.next_sibling_bounds(2), Some((3, 3)));
assert_eq!(l.item_bounds(1), Some((1, 3)));
});
}
#[test]
fn siblings_skip_children_and_stop_at_the_ends() {
let t = "- a\n - a1\n- b\n- c\n - c1\n";
with(t, |l| {
assert_eq!(l.next_sibling_bounds(0), Some((2, 2)));
assert_eq!(l.prev_sibling_bounds(3), Some((2, 2)));
assert_eq!(l.prev_sibling_bounds(2), Some((0, 1)));
assert_eq!(l.prev_sibling_bounds(0), None);
assert_eq!(l.next_sibling_bounds(3), None);
assert_eq!(l.prev_sibling_bounds(4), None);
assert_eq!(l.next_sibling_bounds(1), None);
});
}
#[test]
fn loose_siblings_across_blank_lines() {
with("- a\n\n- b\n\n\n- c\n", |l| {
assert_eq!(l.next_sibling_bounds(0), Some((2, 2)));
assert_eq!(l.next_sibling_bounds(2), Some((5, 5)));
assert_eq!(l.prev_sibling_bounds(5), Some((2, 2)));
});
}
#[test]
fn bullet_and_ordered_at_one_depth_are_different_lists() {
let t = "- a\n- b\n1. c\n2. d\n";
with(t, |l| {
assert_eq!(l.next_sibling_bounds(1), None);
assert_eq!(l.prev_sibling_bounds(2), None);
assert_eq!(l.level_bounds(0), Some((0, 1)));
assert_eq!(l.level_bounds(3), Some((2, 3)));
});
}
#[test]
fn mixed_bullet_markers_are_one_list() {
with("- a\n* b\n+ c\n", |l| assert_eq!(l.level_bounds(1), Some((0, 2))));
}
#[test]
fn ordered_inside_bullet_and_the_reverse() {
let t = "- a\n 1. x\n 2) y\n- b\n - z\n";
with(t, |l| {
assert_eq!(l.parent(2), Some(0));
assert_eq!(l.level_bounds(1), Some((1, 2)));
assert_eq!(l.level_bounds(4), Some((4, 4)));
});
let t = "1. a\n - x\n - y\n2. b\n";
with(t, |l| {
assert_eq!(l.parent(2), Some(0));
assert_eq!(l.next_sibling_bounds(0), Some((3, 3)));
});
}
#[test]
fn level_takes_every_sibling_with_children() {
let t = "- p\n - a\n - a1\n - b\n - c\n - c1\n- q\n";
with(t, |l| {
assert_eq!(l.level_bounds(3), Some((1, 5)));
assert_eq!(l.level_bounds(0), Some((0, 6)));
});
}
#[test]
fn task_items_are_items() {
let t = "- [ ] a\n - [x] b\n- [ ] c\n";
with(t, |l| {
assert_eq!(l.item_bounds(0), Some((0, 1)));
assert_eq!(l.parent(1), Some(0));
assert_eq!(l.next_sibling_bounds(0), Some((2, 2)));
});
}
#[test]
fn step_is_the_nearest_nesting() {
assert_eq!(with("- a\n - b\n- c\n", |l| l.step(2, &[])), Some(2));
let t = "- x\n - y\n\np\n\n- a\n- b\n\np\n\n- m\n - n\n";
assert_eq!(with(t, |l| l.step(6, &[])), Some(3));
assert_eq!(with(t, |l| l.step(10, &[])), Some(2));
assert_eq!(with("- a\n\t- b\n", |l| l.step(0, &[])), Some(4));
assert_eq!(with("- a\n- b\n", |l| l.step(0, &[])), None);
}
#[test]
fn own_bounds_stop_before_the_first_child() {
let t = "- a\n wrapped\n\n para\n\n - b\n - c\n- d\n";
with(t, |l| {
assert_eq!(l.own_bounds(0), Some((0, 3)));
assert_eq!(l.own_bounds(3), Some((0, 3)));
assert_eq!(l.own_bounds(5), Some((5, 5)));
assert_eq!(l.own_bounds(7), Some((7, 7)));
});
}
fn renumbered(text: &str, line: usize) -> String {
with(text, |l| {
let mut lines: Vec<String> = text.lines().map(String::from).collect();
for r in l.renumber(line, None) {
let s = &mut lines[r.line];
let chars: Vec<char> = s.chars().collect();
*s = chars[..r.cols.start].iter().collect::<String>()
+ &r.number
+ &chars[r.cols.end..].iter().collect::<String>();
}
lines.join("\n") + "\n"
})
}
#[test]
fn renumber_counts_up_from_the_first_number() {
assert_eq!(renumbered("1. a\n1. b\n5. c\n", 0), "1. a\n2. b\n3. c\n");
assert_eq!(renumbered("3. a\n1. b\n", 1), "3. a\n4. b\n");
assert_eq!(renumbered("1) a\n1. b\n", 0), "1) a\n2. b\n");
with("1. a\n2. b\n", |l| assert!(l.renumber(0, None).is_empty()));
}
#[test]
fn renumber_touches_one_level() {
let t = "1. a\n 1. x\n 1. y\n1. b\n\n# h\n\n1. p\n1. q\n";
assert_eq!(
renumbered(t, 0),
"1. a\n 1. x\n 1. y\n2. b\n\n# h\n\n1. p\n1. q\n"
);
assert_eq!(
renumbered(t, 2),
"1. a\n 1. x\n 2. y\n1. b\n\n# h\n\n1. p\n1. q\n"
);
}
#[test]
fn renumber_handles_a_change_of_width() {
let t: String = (0..10).map(|i| format!("1. i{i}\n")).collect();
let want: String = (1..=10).map(|i| format!("{i}. i{}\n", i - 1)).collect();
assert_eq!(renumbered(&t, 0), want);
}
#[test]
fn renumber_from_a_given_start() {
with("4. a\n4. b\n", |l| {
let r = l.renumber(0, Some(1));
assert_eq!(r.iter().map(|r| r.number.as_str()).collect::<Vec<_>>(), ["1", "2"]);
});
}
#[test]
fn bullet_lists_are_never_renumbered() {
with("- a\n- b\n", |l| assert!(l.renumber(0, None).is_empty()));
with("prose\n", |l| assert!(l.renumber(0, None).is_empty()));
}
#[test]
fn lists_do_not_cross_a_heading() {
let t = "- a\n\n# h\n\n- b\n";
with(t, |l| {
assert_eq!(l.next_sibling_bounds(0), None);
assert_eq!(l.prev_sibling_bounds(4), None);
});
}
}