use super::clamp_to_cursor_position;
pub(super) fn next_paragraph_start(text: &str, index: usize) -> usize {
let current_line_start = line_start_at_or_before(text, index);
let Some(current_paragraph_start) =
paragraph_start_containing_or_after(text, current_line_start)
else {
return clamp_to_cursor_position(text, index);
};
for line in lines(text) {
if line.start <= current_paragraph_start {
continue;
}
if !line.is_blank && starts_paragraph(text, line.start) {
return line.start;
}
}
clamp_to_cursor_position(text, text.len())
}
pub(super) fn previous_paragraph_start(text: &str, index: usize) -> usize {
let current_line_start = line_start_at_or_before(text, index);
let current_paragraph_start = paragraph_start_containing_or_before(text, current_line_start)
.unwrap_or(current_line_start);
lines(text)
.filter(|line| line.start < current_paragraph_start)
.filter(|line| !line.is_blank && starts_paragraph(text, line.start))
.map(|line| line.start)
.last()
.unwrap_or_else(|| clamp_to_cursor_position(text, index))
}
fn line_start_at_or_before(text: &str, index: usize) -> usize {
let index = super::clamp_to_boundary(text, index);
text[..index]
.rfind('\n')
.map_or(0, |newline_index| newline_index + '\n'.len_utf8())
}
fn paragraph_start_containing_or_after(text: &str, line_start: usize) -> Option<usize> {
let mut previous_start = None;
for line in lines(text) {
if !line.is_blank && starts_paragraph(text, line.start) {
previous_start = Some(line.start);
}
if line.start >= line_start {
return if line.is_blank {
lines(text)
.filter(|candidate| candidate.start > line.start)
.find(|candidate| {
!candidate.is_blank && starts_paragraph(text, candidate.start)
})
.map(|candidate| candidate.start)
} else {
previous_start
};
}
}
previous_start
}
fn paragraph_start_containing_or_before(text: &str, line_start: usize) -> Option<usize> {
let mut previous_start = None;
for line in lines(text) {
if line.start > line_start {
break;
}
if !line.is_blank && starts_paragraph(text, line.start) {
previous_start = Some(line.start);
}
}
previous_start
}
fn starts_paragraph(text: &str, line_start: usize) -> bool {
if line_start == 0 {
return true;
}
let previous_line_end = line_start - '\n'.len_utf8();
let previous_line_start = text[..previous_line_end]
.rfind('\n')
.map_or(0, |newline_index| newline_index + '\n'.len_utf8());
text[previous_line_start..previous_line_end]
.trim()
.is_empty()
}
fn lines(text: &str) -> impl Iterator<Item = ParagraphLine> + '_ {
let mut start = 0;
std::iter::from_fn(move || {
if start > text.len() {
return None;
}
let line_start = start;
let line_end = text[start..]
.find('\n')
.map_or(text.len(), |newline_offset| start + newline_offset);
start = line_end
.checked_add('\n'.len_utf8())
.filter(|next_start| *next_start <= text.len())
.unwrap_or(text.len() + 1);
Some(ParagraphLine {
start: line_start,
is_blank: text[line_start..line_end].trim().is_empty(),
})
})
}
#[derive(Clone, Copy, Debug)]
struct ParagraphLine {
start: usize,
is_blank: bool,
}