use std::ops::Range;
use super::{
display_width, wrap_line_at_whitespace_ranges,
wrap_line_at_whitespace_ranges_with_protected_prefix,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ContinuationIndent {
Flush,
Hang(usize),
Continued(usize),
}
impl ContinuationIndent {
pub(super) fn list_item(source_line: &str, width: usize) -> Self {
list_hang(source_line)
.and_then(|hang| fit_hang(hang, width))
.map_or(Self::Flush, Self::Hang)
}
fn continued(hang: usize, width: usize) -> Self {
fit_hang(hang, width).map_or(Self::Flush, Self::Continued)
}
pub(super) fn row_indent(self, row_start: usize) -> usize {
match self {
Self::Flush => 0,
Self::Hang(_) if row_start == 0 => 0,
Self::Hang(hang) | Self::Continued(hang) => hang,
}
}
}
fn fit_hang(hang: usize, width: usize) -> Option<usize> {
(hang < width).then_some(hang)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(in crate::tui) enum StreamLineStart {
#[default]
Fresh,
ContinuesListItem { hang: usize },
}
impl StreamLineStart {
pub(in crate::tui) fn after(emitted: &str, in_code_block: bool) -> Self {
if in_code_block {
return Self::Fresh;
}
let line_start = emitted
.rfind('\n')
.map_or(0, |index| index + '\n'.len_utf8());
list_hang(&emitted[line_start..])
.map_or(Self::Fresh, |hang| Self::ContinuesListItem { hang })
}
pub(super) fn continued_indent(self, width: usize) -> Option<ContinuationIndent> {
match self {
Self::Fresh => None,
Self::ContinuesListItem { hang } => Some(ContinuationIndent::continued(hang, width)),
}
}
}
pub(super) fn wrap_markdown_line_ranges(
line: &str,
width: usize,
indent: ContinuationIndent,
) -> Vec<Range<usize>> {
let protected = |line: &str| {
let protected_prefix_end = markdown_list_body_start(line).unwrap_or_default();
wrap_line_at_whitespace_ranges_with_protected_prefix(line, width, protected_prefix_end)
};
match indent {
ContinuationIndent::Flush => protected(line),
ContinuationIndent::Hang(hang) => {
let mut ranges = protected(line);
ranges.truncate(1);
let rest_start = ranges.first().map_or(0, |first| first.end);
ranges.extend(hung_ranges(line, rest_start, width - hang));
ranges
}
ContinuationIndent::Continued(hang) => hung_ranges(line, 0, width - hang),
}
}
fn hung_ranges(line: &str, start: usize, width: usize) -> Vec<Range<usize>> {
let rest = &line[start..];
let start = start + (rest.len() - rest.trim_start().len());
if start == line.len() {
return Vec::new();
}
wrap_line_at_whitespace_ranges(&line[start..], width)
.into_iter()
.map(|range| range.start + start..range.end + start)
.collect()
}
fn list_hang(line: &str) -> Option<usize> {
markdown_list_body_start(line).map(|start| display_width(&line[..start]))
}
fn markdown_list_body_start(line: &str) -> Option<usize> {
let trimmed = line.trim_start_matches(char::is_whitespace);
let leading_whitespace_len = line.len() - trimmed.len();
let marker_len = trimmed.find(char::is_whitespace)?;
let marker = &trimmed[..marker_len];
let is_list_marker = matches!(marker, "-" | "+" | "*")
|| marker.strip_suffix(['.', ')']).is_some_and(|digits| {
(1..=9).contains(&digits.len()) && digits.bytes().all(|byte| byte.is_ascii_digit())
});
if !is_list_marker {
return None;
}
let separator_len = trimmed[marker_len..]
.chars()
.take_while(|ch| ch.is_whitespace())
.map(char::len_utf8)
.sum::<usize>();
let body_start = leading_whitespace_len + marker_len + separator_len;
(body_start < line.len()).then_some(body_start)
}