use std::cell::OnceCell;
use std::ops::Range;
use crate::document::wrap::Indent;
use crate::document::ParsedDoc;
use crate::markdown::ast::{to_u32, ListItem};
use crate::markdown::segment_break;
use crate::markdown::table_layout::char_cells;
use crate::markdown::{
strip_atx_closing, Block, ColOrigin, ContentKind, InlineColMap, RowOrigin, SrcLines,
};
fn own_origins(parsed: &ParsedDoc, block: usize) -> (&[RowOrigin], usize) {
let own = parsed.block_own_line_count(block);
let first = parsed.source_map.rendered_lines_for_block(block).start;
let origins = parsed
.row_origins()
.get(first..first.saturating_add(own))
.unwrap_or(&[]);
(origins, parsed.latex_source_offset(block))
}
pub fn row_for_line(parsed: &ParsedDoc, block: usize, line: usize) -> usize {
let (origins, band) = own_origins(parsed, block);
if origins.is_empty() {
return 0;
}
let line = to_u32(line);
let last = origins.len() - 1;
let row = origins
.iter()
.position(|o| o.lines.as_ref().is_some_and(|l| l.end > line))
.or_else(|| origins.iter().rposition(|o| o.lines.is_some()))
.unwrap_or(last);
(row + band).min(last)
}
pub fn row_for_pos(parsed: &ParsedDoc, block: usize, pos: RawPos) -> usize {
let row = row_for_line(parsed, block, pos.line);
let (origins, band) = own_origins(parsed, block);
let chrome = row
.checked_sub(band)
.and_then(|r| origins.get(r))
.is_some_and(|o| o.cols == ColOrigin::Chrome);
if !chrome {
return row;
}
let line = to_u32(pos.line);
(row + 1..)
.take_while(|&r| lines_of_row(parsed, block, r).is_some_and(|l| l.contains(&line)))
.find(|&r| raw_to_rendered_col(parsed, block, r, pos).is_some())
.unwrap_or(row)
}
pub fn lines_of_row(parsed: &ParsedDoc, block: usize, row: usize) -> Option<Range<u32>> {
let (origins, band) = own_origins(parsed, block);
origins.get(row.checked_sub(band)?)?.lines.clone()
}
pub fn line_for_row(parsed: &ParsedDoc, block: usize, row: usize) -> usize {
let (origins, band) = own_origins(parsed, block);
let Some(row) = row.checked_sub(band) else {
return 0;
};
let row = row.min(origins.len().saturating_sub(1));
origins
.get(..=row)
.unwrap_or(&[])
.iter()
.rev()
.find_map(RowOrigin::first_line)
.map_or(0, |l| l as usize)
}
pub fn revealed_diagram_line(parsed: &ParsedDoc, block: usize, row: usize) -> Option<usize> {
let (origins, band) = own_origins(parsed, block);
let k = row.checked_sub(band)?;
let line = origins.get(k)?.lines.as_ref()?.start as usize;
(line == k).then_some(line)
}
pub fn stacked_lines(parsed: &ParsedDoc, block: usize, row: usize) -> Option<Range<u32>> {
let (origins, band) = own_origins(parsed, block);
let origin = origins.get(row.checked_sub(band)?)?;
let ColOrigin::Content {
kind: kind @ (ContentKind::Inline | ContentKind::Flow),
..
} = origin.cols
else {
return None;
};
let lines = origin.lines.clone()?;
if lines.len() > 1 {
return Some(lines);
}
if !parsed.reflow_paragraphs || kind != ContentKind::Flow {
return None;
}
let range_start = parsed.source_map.original_range_for_block(block)?.start;
let ast = parsed.real_block_for_byte(range_start)?;
match leaf_at(ast, lines.start)? {
Block::Paragraph { inlines, .. }
if crate::markdown::renderer::paragraph_reflows(parsed.reflow_paragraphs, inlines) =>
{
Some(lines)
}
_ => None,
}
}
pub fn cursor_stack(parsed: &ParsedDoc, block: usize, pos: RawPos) -> Option<(usize, Range<u32>)> {
if parsed.is_diagram_reveal_block(block) {
return None;
}
let line = to_u32(pos.line);
let row = row_for_pos(parsed, block, pos);
let own = lines_of_row(parsed, block, row)?;
if !own.contains(&line) {
if table_row(parsed, block, row).is_some() {
return None;
}
if lines_of_row(parsed, block, row + 1).is_some_and(|next| next.contains(&own.start)) {
return Some((row, line..line + 1));
}
let lines = stacked_lines(parsed, block, row).unwrap_or(own);
return Some((row, lines.start.min(line)..lines.end.max(line + 1)));
}
if let Some(lines) = stacked_lines(parsed, block, row) {
return Some((row, lines));
}
let range_start = parsed.source_map.original_range_for_block(block)?.start;
let leaf = leaf_at(parsed.real_block_for_byte(range_start)?, line)?;
if !leaf.is_setext_heading() || line + 1 != leaf.span().end {
return None;
}
let text = row_for_line(parsed, block, leaf.span().start as usize);
stacked_lines(parsed, block, text).map(|lines| (text, lines))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RawPos {
pub line: usize,
pub col: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct RowCache {
chars: Box<[char]>,
range_start: Option<usize>,
first_line: usize,
lead: usize,
positions: OnceCell<Option<Box<[RawPos]>>>,
}
impl RowCache {
fn new(parsed: &ParsedDoc, block: usize, abs: usize) -> Option<Self> {
let chars = parsed
.lines
.get(abs)?
.spans
.iter()
.flat_map(|s| s.content.chars())
.collect();
let range_start = parsed
.source_map
.original_range_for_block(block)
.map(|r| r.start);
let first_line = range_start.map_or(0, |b| parsed.byte_to_line(b));
let lead = range_start
.and_then(|b| parsed.source().get(parsed.line_start_byte(first_line)..b))
.map_or(0, |t| t.chars().count());
Some(Self {
chars,
range_start,
first_line,
lead,
positions: OnceCell::new(),
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct JoinedMap {
positions: Box<[RawPos]>,
breaks: Box<[Cut]>,
}
#[derive(Debug, Clone, Copy)]
struct Cut {
at: usize,
line: usize,
len: usize,
}
impl JoinedMap {
fn new(slices: &[(usize, usize, &str)], labels: &crate::markdown::RefLabels) -> Self {
let joined: Vec<&str> = slices.iter().map(|&(_, _, s)| s).collect();
let map = InlineColMap::build_inline(&joined.join("\n"), labels);
let mut starts = Vec::with_capacity(slices.len());
let mut at = 0usize;
for &(_, _, s) in slices {
starts.push(at);
at += s.chars().count() + 1;
}
let pos = |c: usize| {
let i = starts.partition_point(|&s| s <= c).saturating_sub(1);
let (line, col, _) = slices[i];
RawPos {
line,
col: col + (c - starts[i]),
}
};
let forward = &map.rendered_to_raw_vec()[..map.rendered_len()];
let mut vanished = map.vanished_breaks().iter().peekable();
let mut breaks = Vec::new();
for (k, &raw) in forward.iter().enumerate() {
while let Some(&(at, raw)) = vanished.next_if(|&&(at, _)| at == k) {
breaks.push(Cut {
at,
line: pos(raw).line,
len: 0,
});
}
if map.is_break(k) {
breaks.push(Cut {
at: k,
line: pos(raw).line,
len: 1,
});
}
}
breaks.extend(vanished.map(|&(at, raw)| Cut {
at,
line: pos(raw).line,
len: 0,
}));
Self {
positions: forward.iter().map(|&c| pos(c)).collect(),
breaks: breaks.into_boxed_slice(),
}
}
fn part(&self, lines: Range<u32>) -> Option<Vec<RawPos>> {
let (first, last) = (lines.start as usize, lines.end.saturating_sub(1) as usize);
let before = |line: usize| self.breaks.partition_point(|cut| cut.line < line);
let from = before(first)
.checked_sub(1)
.map_or(0, |i| self.breaks[i].at + self.breaks[i].len);
let to = self
.breaks
.get(before(last))
.map_or(self.positions.len(), |cut| cut.at);
self.positions.get(from..to).map(<[RawPos]>::to_vec)
}
}
struct Row<'a> {
parsed: &'a ParsedDoc,
block: usize,
origin: &'a RowOrigin,
cache: &'a RowCache,
chars: &'a [char],
start: usize,
range_start: Option<usize>,
first_line: usize,
lead: usize,
}
impl<'a> Row<'a> {
fn new(parsed: &'a ParsedDoc, block: usize, row: usize) -> Option<Self> {
let (origins, band) = own_origins(parsed, block);
let origin = origins.get(row.checked_sub(band)?)?;
debug_assert!(
band == 0 || origin.cols == ColOrigin::Chrome,
"a math-preview band over content rows"
);
let abs = parsed.source_map.rendered_lines_for_block(block).start + row;
let cache = parsed.row_cache_or_init(abs, || RowCache::new(parsed, block, abs))?;
let chars = &cache.chars[..];
let start = match origin.cols {
ColOrigin::Content { rendered_col, .. } => char_at_cell(chars, rendered_col as usize),
ColOrigin::Chrome => 0,
};
Some(Self {
parsed,
block,
origin,
cache,
chars,
start,
range_start: cache.range_start,
first_line: cache.first_line,
lead: cache.lead,
})
}
fn content_len(&self) -> usize {
self.chars.len() - self.start
}
fn line_space(&self, pos: RawPos) -> RawPos {
match pos.line {
0 => RawPos {
line: 0,
col: pos.col + self.lead,
},
_ => pos,
}
}
fn caller_space(&self, pos: RawPos) -> RawPos {
match pos.line {
0 => RawPos {
line: 0,
col: pos.col.saturating_sub(self.lead),
},
_ => pos,
}
}
fn ast(&self) -> Option<&'a Block> {
self.parsed.real_block_for_byte(self.range_start?)
}
fn content_col(&self, line: u32) -> Option<u32> {
let src = leaf_at(self.ast()?, line)?.src()?;
src.col((line - src.first) as usize)
}
fn chrome_col(&self, line: u32) -> usize {
let Some(src) = self
.ast()
.and_then(|b| leaf_at(b, line))
.and_then(Block::src)
else {
return 0;
};
nearest_content_col(src, line, |l| self.line_len(l))
}
fn positions(&self) -> Option<&'a [RawPos]> {
let cache = self.cache;
cache
.positions
.get_or_init(|| self.build_positions().map(Vec::into_boxed_slice))
.as_deref()
}
fn build_positions(&self) -> Option<Vec<RawPos>> {
let ColOrigin::Content {
raw_col,
kind: ContentKind::Inline | ContentKind::Flow,
..
} = self.origin.cols
else {
return None;
};
let lines = self.origin.lines.clone()?;
let leaf = self.ast().and_then(|b| leaf_at(b, lines.start));
let labels = self.parsed.ref_labels();
let slices = self.slices(lines.clone(), raw_col, leaf, false);
let shown = self.content_len();
let back_link = self.chars.ends_with(&[' ', '↩']);
let fits = |len: usize| len == shown || (back_link && len + 2 == shown);
let mut by_line = Vec::with_capacity(shown + 1);
let content = &self.chars[self.start..];
for (i, &(line, col, slice)) in slices.iter().enumerate() {
let at = by_line.len();
let vanished = at
.checked_sub(1)
.and_then(|before| content.get(before))
.zip(content.get(at))
.is_some_and(|(&before, &after)| segment_break::vanishes(before, after));
if i > 0 && !vanished {
let (prev, prev_col, prev_slice) = slices[i - 1];
by_line.push(RawPos {
line: prev,
col: prev_col + prev_slice.chars().count(),
});
}
let map = InlineColMap::build_inline(slice, labels);
let forward = map.rendered_to_raw_vec();
by_line.extend(
forward[..map.rendered_len()]
.iter()
.map(|&c| RawPos { line, col: col + c }),
);
}
let end = slices
.last()
.map_or(RawPos::default(), |&(line, col, slice)| RawPos {
line,
col: col + slice.chars().count(),
});
if fits(by_line.len()) {
by_line.push(end);
return Some(by_line);
}
let mut joined = match leaf {
Some(Block::Paragraph { src, .. }) => {
let first_col = src.col(0)?;
self.parsed.with_joined_map(
(self.block, src.first),
|| JoinedMap::new(&self.slices(src.span(), first_col, leaf, true), labels),
|map| map.part(lines),
)?
}
_ if slices.len() >= 2 => {
JoinedMap::new(&self.slices(lines.clone(), raw_col, leaf, true), labels)
.part(lines)?
}
_ => return None,
};
if !fits(joined.len()) {
return None;
}
joined.push(end);
Some(joined)
}
fn slices(
&self,
lines: Range<u32>,
first_col: u32,
leaf: Option<&Block>,
hard_breaks: bool,
) -> Vec<(usize, usize, &'a str)> {
let leaf_end = leaf.map_or(0, |l| l.span().end);
let atx =
leaf.is_some_and(|l| matches!(l, Block::Heading { .. }) && !l.is_setext_heading());
let start = lines.start;
lines
.map(|l| {
let line = self.line_text(l as usize);
let col = if l == start {
Some(first_col)
} else {
self.content_col(l)
};
let col = col.map_or_else(
|| {
line.chars()
.take_while(|c| matches!(c, '>' | ' ' | '\t'))
.count()
},
|c| c as usize,
);
let byte = line.char_indices().nth(col).map_or(line.len(), |(b, _)| b);
let slice = &line[byte..];
let trailing = slice.chars().rev().take_while(|&c| c == '\\').count();
let slice = match slice.strip_suffix('\\') {
Some(s) if !hard_breaks && l + 1 < leaf_end && trailing % 2 == 1 => s,
_ => slice,
};
let slice = if atx { strip_atx_closing(slice) } else { slice };
(l as usize, col, slice)
})
.collect()
}
fn home_line(&self, row: usize) -> usize {
line_for_row(self.parsed, self.block, row)
}
fn line_text(&self, line: usize) -> &'a str {
if self.range_start.is_none() {
return "";
}
let text = self.parsed.source_line(self.first_line + line);
text.strip_suffix('\r').unwrap_or(text)
}
fn line_len(&self, line: usize) -> usize {
self.line_text(line).chars().count()
}
fn prefix_anchor(&self, line: usize, raw_col: usize) -> usize {
let raw: Vec<char> = self.line_text(line).chars().take(raw_col).collect();
let raw_gap = raw
.iter()
.rev()
.take_while(|c| matches!(c, ' ' | '\t'))
.count();
let rendered_gap = self.chars[..self.start]
.iter()
.rev()
.take_while(|&&c| c == ' ')
.count();
raw_col - raw_gap.saturating_sub(rendered_gap)
}
}
fn char_at_cell(chars: &[char], cell: usize) -> usize {
let mut at = 0usize;
for (i, &ch) in chars.iter().enumerate() {
if at >= cell {
return i;
}
at += char_cells(ch);
}
chars.len()
}
fn leaf_at(block: &Block, line: u32) -> Option<&Block> {
match block {
Block::BlockQuote { blocks, .. } | Block::FootnoteDefinition { blocks, .. } => {
blocks.iter().find_map(|b| leaf_at(b, line))
}
Block::List { items, .. } => items
.iter()
.flat_map(|i| &i.blocks)
.find_map(|b| leaf_at(b, line)),
leaf => leaf.span().contains(&line).then_some(leaf),
}
}
fn nearest_content_col(src: &SrcLines, line: u32, line_len: impl Fn(usize) -> usize) -> usize {
let k = (line - src.first) as usize;
let at = |i: usize| {
let col = src.col(i)? as usize;
(col < line_len(src.first as usize + i)).then_some(col)
};
(k..src.len())
.find_map(at)
.or_else(|| (0..k).rev().find_map(at))
.unwrap_or(0)
}
pub fn revealed_indent(parsed: &ParsedDoc, block: usize, line: usize) -> Indent {
indent_and_skip(parsed, block, line).map_or(Indent::NONE, |(indent, _)| indent)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LineLayout {
pub skip: usize,
pub indent: Indent,
}
pub fn source_line_layout(parsed: &ParsedDoc, line: usize) -> LineLayout {
let byte = parsed.line_start_byte(line);
let Some(block) = parsed.source_map.block_for_byte(byte) else {
return LineLayout::default();
};
let first = parsed
.source_map
.original_range_for_block(block)
.map_or(line, |r| parsed.byte_to_line(r.start));
let rel = line.saturating_sub(first);
let Some((indent, range_skip)) = indent_and_skip(parsed, block, rel) else {
return LineLayout::default();
};
let row = row_for_line(parsed, block, rel);
if !reveals(parsed, block, row) {
let (origins, band) = own_origins(parsed, block);
let origin = row.checked_sub(band).and_then(|r| origins.get(r));
if let Some(RowOrigin {
lines: Some(lines),
cols:
ColOrigin::Content {
raw_col,
rendered_col,
..
},
hang,
}) = origin
{
if lines.contains(&to_u32(rel)) {
return LineLayout {
skip: *raw_col as usize,
indent: Indent {
lead: *rendered_col as usize,
hang: *hang as usize,
},
};
}
}
}
LineLayout {
skip: range_skip,
indent,
}
}
fn indent_and_skip(parsed: &ParsedDoc, block: usize, line: usize) -> Option<(Indent, usize)> {
let range_start = parsed
.source_map
.original_range_for_block(block)
.map(|r| r.start)?;
let ast = parsed.real_block_for_byte(range_start)?;
let first_line = parsed.byte_to_line(range_start);
let rel = to_u32(line);
let src = leaf_at(ast, rel).and_then(Block::src)?;
let text_of = |l: usize| parsed.source_line(first_line + l);
let chars: Vec<char> = text_of(line).chars().collect();
let skip = if line == 0 {
parsed
.source()
.get(parsed.line_start_byte(first_line)..range_start)
.map_or(0, |t| t.chars().count())
} else {
0
};
let own_col = src.col((rel - src.first) as usize);
let col = own_col.map_or_else(
|| nearest_content_col(src, rel, |l| text_of(l).chars().count()),
|c| c as usize,
);
let cells = |from: usize, to: usize| -> usize {
chars
.get(from.min(chars.len())..to.min(chars.len()))
.map_or(0, |cs| cs.iter().map(|&c| char_cells(c)).sum())
};
let prefix = cells(skip.min(col), col);
let own_indent = if leaf_at(ast, rel).is_some_and(is_verbatim_leaf) {
chars
.get(col..)
.map_or(0, |rest| rest.iter().take_while(|&&c| c == ' ').count())
} else {
0
};
let lead = own_col
.filter(|_| prefix > 0)
.and_then(|_| {
let (origins, band) = own_origins(parsed, block);
let row = row_for_line(parsed, block, line);
let origin = origins.get(row.checked_sub(band)?)?;
if !origin.lines.as_ref()?.contains(&rel) {
return None;
}
match origin.cols {
ColOrigin::Content { rendered_col, .. } => {
Some((rendered_col as usize).saturating_sub(prefix))
}
ColOrigin::Chrome => None,
}
})
.unwrap_or(0);
let indent = Indent {
lead,
hang: lead + prefix + own_indent,
};
Some((indent, skip))
}
fn is_verbatim_leaf(leaf: &Block) -> bool {
matches!(
leaf,
Block::CodeBlock { .. }
| Block::Html(..)
| Block::MetadataBlock { .. }
| Block::ImageBlock { .. }
)
}
pub fn reveals(parsed: &ParsedDoc, block: usize, row: usize) -> bool {
let (origins, band) = own_origins(parsed, block);
let origin = row.checked_sub(band).and_then(|r| origins.get(r));
!matches!(
origin.map(|o| o.cols),
Some(ColOrigin::Content {
kind: ContentKind::Verbatim,
..
})
)
}
pub fn rendered_to_raw_col(
parsed: &ParsedDoc,
block: usize,
row: usize,
rendered: usize,
) -> RawPos {
let Some(r) = Row::new(parsed, block, row) else {
return RawPos {
line: line_for_row(parsed, block, row),
col: 0,
};
};
let ColOrigin::Content { raw_col, kind, .. } = r.origin.cols else {
let line = r.home_line(row);
let col = r.chrome_col(to_u32(line));
return r.caller_space(RawPos {
line,
col: col.min(r.line_len(line)),
});
};
if rendered < r.start && kind != ContentKind::Verbatim {
let line = r.home_line(row);
let text = r.line_text(line);
let indent = text.chars().take_while(|c| matches!(c, ' ' | '\t')).count();
let anchor = r.prefix_anchor(line, raw_col as usize);
let col = (rendered + anchor).saturating_sub(r.start).max(indent);
if col < anchor {
return r.caller_space(RawPos { line, col });
}
}
let k = rendered.saturating_sub(r.start).min(r.content_len());
if let Some(positions) = r.positions() {
return r.caller_space(positions[k.min(positions.len() - 1)]);
}
let line = r.home_line(row);
r.caller_space(RawPos {
line,
col: (raw_col as usize).saturating_add(k).min(r.line_len(line)),
})
}
pub fn raw_to_rendered_col(
parsed: &ParsedDoc,
block: usize,
row: usize,
pos: RawPos,
) -> Option<usize> {
let r = Row::new(parsed, block, row)?;
let ColOrigin::Content { raw_col, kind, .. } = r.origin.cols else {
return None;
};
let pos = r.line_space(pos);
let lines = r.origin.lines.clone()?;
if !lines.contains(&to_u32(pos.line)) {
return None;
}
if pos.line == lines.start as usize
&& pos.col < raw_col as usize
&& kind != ContentKind::Verbatim
{
let anchor = r.prefix_anchor(pos.line, raw_col as usize);
return Some((pos.col + r.start).saturating_sub(anchor).min(r.start));
}
if kind == ContentKind::Verbatim {
let k = pos
.col
.saturating_sub(raw_col as usize)
.min(r.content_len());
return Some(r.start + k);
}
let positions = r.positions()?;
Some(r.start + positions.partition_point(|p| *p < pos).min(r.content_len()))
}
pub fn raw_to_rendered_col_near(
parsed: &ParsedDoc,
block: usize,
row: usize,
pos: RawPos,
) -> usize {
if let Some(col) = raw_to_rendered_col(parsed, block, row, pos) {
return col;
}
let Some(r) = Row::new(parsed, block, row) else {
return pos.col;
};
let pos = r.line_space(pos);
match r.origin.cols {
ColOrigin::Content { raw_col, .. } if r.origin.first_line() == Some(to_u32(pos.line)) => {
r.start
+ pos
.col
.saturating_sub(raw_col as usize)
.min(r.content_len())
}
ColOrigin::Content { .. } => r.start,
ColOrigin::Chrome => pos.col.min(r.chars.len()),
}
}
pub fn item_content_cols(parsed: &ParsedDoc, line: usize) -> Vec<usize> {
let Some((block, first)) = parsed.real_block_at_line(line) else {
return Vec::new();
};
block
.items_holding(to_u32(line - first))
.map(|items| items.into_iter().filter_map(item_content_col).collect())
.unwrap_or_default()
}
fn item_content_col(item: &ListItem) -> Option<usize> {
item.blocks
.iter()
.filter(|b| !matches!(b, Block::CodeBlock { .. }))
.find_map(|b| b.src()?.cols().flatten().next())
.map(|c| c as usize)
}
pub fn table_lines(parsed: &ParsedDoc, line: usize) -> Option<(usize, Vec<Option<usize>>)> {
let (block, first) = parsed.real_block_at_line(line)?;
let Some(Block::Table { src, .. }) = leaf_at(block, to_u32(line - first)) else {
return None;
};
let cols = src.cols().map(|c| c.map(|c| c as usize)).collect();
Some((first + src.first as usize, cols))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRowHit {
pub line: usize,
pub index: u32,
pub sub: usize,
pub cells: bool,
pub rows: Range<usize>,
pub raw_col: usize,
}
pub fn table_row(parsed: &ParsedDoc, block: usize, row: usize) -> Option<TableRowHit> {
let (origins, band) = own_origins(parsed, block);
let r = row.checked_sub(band)?;
let origin = origins.get(r)?;
let cells_of = |o: &RowOrigin| match o.cols {
ColOrigin::Content {
raw_col,
kind: ContentKind::TableRow { row, sub },
..
} => Some((
o.first_line()? as usize,
row,
sub as usize,
raw_col as usize,
)),
_ => None,
};
let (at, (line, index, sub, raw_col)) = if let Some(hit) = cells_of(origin) {
(r, hit)
} else {
if origin.cols != ColOrigin::Chrome {
return None;
}
let line = origin
.first_line()
.or_else(|| origins.get(r + 1)?.first_line())?;
let range_start = parsed.source_map.original_range_for_block(block)?.start;
let ast = parsed.real_block_for_byte(range_start)?;
if !matches!(leaf_at(ast, line), Some(Block::Table { .. })) {
return None;
}
let shows = |o: &RowOrigin| cells_of(o).is_some_and(|(l, ..)| l == line as usize);
let at = origins[..r]
.iter()
.rposition(shows)
.or_else(|| origins.get(r + 1).and_then(cells_of).map(|_| r + 1))
.or_else(|| origins[..r].iter().rposition(|o| cells_of(o).is_some()))?;
let (line, index, _, raw_col) = cells_of(&origins[at])?;
(at, (line, index, 0, raw_col))
};
let same = |o: &RowOrigin| cells_of(o).is_some_and(|(l, i, ..)| l == line && i == index);
let first = origins[..at]
.iter()
.rposition(|o| !same(o))
.map_or(0, |i| i + 1);
let end = origins[at..]
.iter()
.position(|o| !same(o))
.map_or(origins.len(), |i| at + i);
Some(TableRowHit {
line,
index,
sub,
cells: at == r,
rows: first + band..end + band,
raw_col,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TablePart {
TopBorder,
Header { sub: usize },
HeavyRule,
DataRow { row: usize, sub: usize },
Separator,
BottomBorder,
}
pub type TableParts = Vec<Option<(usize, TablePart)>>;
pub fn table_parts(parsed: &ParsedDoc, block: usize) -> TableParts {
let (origins, band) = own_origins(parsed, block);
let cells = |i: usize| match origins.get(i)?.cols {
ColOrigin::Content {
kind: ContentKind::TableRow { row, sub },
..
} => Some((row as usize, sub as usize)),
_ => None,
};
if !(0..origins.len()).any(|i| cells(i).is_some()) {
return Vec::new();
}
let mut out = vec![None; band + origins.len()];
let data = |i: usize| cells(i).filter(|&(row, _)| row > 0);
let chrome = |i: usize| origins.get(i).is_some_and(|o| o.cols == ColOrigin::Chrome);
let mut i = 0;
while i < origins.len() {
if !(chrome(i) && origins[i].lines.is_none() && cells(i + 1) == Some((0, 0))) {
i += 1;
continue;
}
let Some(header) = origins[i + 1].first_line().map(|l| l as usize) else {
i += 1;
continue;
};
let mut mark = |i: usize, part| out[band + i] = Some((header, part));
mark(i, TablePart::TopBorder);
i += 1;
while let Some((0, sub)) = cells(i) {
mark(i, TablePart::Header { sub });
i += 1;
}
if chrome(i) {
mark(i, TablePart::HeavyRule);
i += 1;
}
while chrome(i) || data(i).is_some() {
if let Some((row, sub)) = data(i) {
mark(i, TablePart::DataRow { row: row - 1, sub });
} else if data(i + 1).is_some() {
mark(i, TablePart::Separator);
} else {
mark(i, TablePart::BottomBorder);
i += 1;
break;
}
i += 1;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Theme;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn doc(src: &str) -> ParsedDoc {
ParsedDoc::build(src, theme(), true, 4)
}
fn block_at(doc: &ParsedDoc, byte: usize) -> usize {
doc.source_map.block_for_byte(byte).unwrap()
}
#[test]
fn a_source_lines_layout_is_what_its_row_paints() {
let d = doc(" para text\n\n- a\n\n ```\n x\n ```\n");
let at = |line| source_line_layout(&d, line);
assert_eq!(
at(0),
LineLayout {
skip: 2,
indent: Indent::NONE
}
);
assert_eq!(
at(2),
LineLayout {
skip: 0,
indent: Indent::hanging(2)
}
);
assert_eq!(
at(5),
LineLayout {
skip: 2,
indent: Indent { lead: 3, hang: 5 }
}
);
}
#[test]
fn a_line_with_no_row_shares_the_next_lines_row() {
let d = doc("> a\n>\n> [r]: /u\n> b\n");
let b = block_at(&d, 0);
assert_eq!(
(0..4).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[0, 1, 2, 2]
);
assert_eq!(line_for_row(&d, b, 2), 3);
}
#[test]
fn a_blank_between_an_items_blocks_has_a_row_of_its_own() {
let d = doc("- a\n\n b\n");
let b = block_at(&d, 0);
assert_eq!(
(0..3).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[0, 1, 2]
);
let d = doc("- x\n - b\n\n - d\n");
let b = block_at(&d, 0);
assert_eq!(
(0..4).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[0, 1, 2, 3]
);
}
#[test]
fn a_marker_on_a_row_of_its_own_keeps_the_rows_below_on_their_lines() {
let d = doc("- ```bash\n code\n ```\n- next\n");
let b = block_at(&d, 0);
assert_eq!(
(0..4).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[0, 2, 3, 4]
);
assert_eq!(
(0..5).map(|r| line_for_row(&d, b, r)).collect::<Vec<_>>(),
[0, 0, 1, 2, 3]
);
}
#[test]
fn a_row_no_line_owns_belongs_to_the_line_above() {
let d = doc("- a\n ```\n x\n- b\n");
let b = block_at(&d, 0);
assert_eq!(lines_of_row(&d, b, 3), None);
assert_eq!(line_for_row(&d, b, 3), 2);
assert_eq!(row_for_line(&d, b, 3), 4);
}
#[test]
fn a_line_past_every_row_skips_trailing_chrome() {
let d = doc("- a\n ```\n x\n");
let b = block_at(&d, 0);
let rows = d.block_own_line_count(b);
assert_eq!(
lines_of_row(&d, b, rows - 1),
None,
"the placeholder closes the block"
);
assert_eq!(row_for_line(&d, b, 9), rows - 2);
}
#[test]
fn a_table_line_lands_on_its_first_content_row() {
let d = doc("| a | b |\n|---|---|\n| 1 | 2 |\n");
let b = block_at(&d, 0);
assert_eq!(
(0..3).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[1, 2, 3]
);
assert_eq!(line_for_row(&d, b, 0), 0);
assert_eq!(line_for_row(&d, b, 4), 2);
}
#[test]
fn a_tables_borders_snap_onto_its_rows() {
let d = doc("| a | b |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n");
let b = block_at(&d, 0);
let hit = |r| table_row(&d, b, r).map(|h| (h.line, h.index, h.cells));
assert_eq!(
(0..7).map(hit).collect::<Vec<_>>(),
[
Some((0, 0, false)),
Some((0, 0, true)),
Some((2, 1, false)),
Some((2, 1, true)),
Some((2, 1, false)),
Some((3, 2, true)),
Some((3, 2, false)),
]
);
assert_eq!(table_row(&d, b, 3).unwrap().rows, 3..4);
}
#[test]
fn a_nested_tables_rows_are_found_inside_their_block() {
let d = doc("- item\n\n | a | b |\n |---|---|\n | 1 | 2 |\n");
let b = block_at(&d, 0);
assert_eq!(table_row(&d, b, 0), None, "the item's text");
let header = row_for_line(&d, b, 2);
assert_eq!(
table_row(&d, b, header - 1).map(|h| (h.line, h.cells)),
Some((2, false)),
"the top border"
);
let data = row_for_line(&d, b, 4);
let hit = table_row(&d, b, data).unwrap();
assert_eq!((hit.line, hit.index, hit.sub, hit.cells), (4, 1, 0, true));
assert_eq!(hit.rows, data..data + 1);
assert_eq!(
table_row(&d, b, data - 1).map(|h| h.line),
Some(4),
"heavy rule"
);
}
#[test]
fn a_wrapped_table_rows_chunks_share_it() {
let src = "| a | b |\n|---|---|\n| x | aa bb cc dd ee ff |\n";
let d = ParsedDoc::build_with_overrides(
src,
theme(),
true,
4,
None,
None,
false,
16,
false,
false,
true,
false,
None,
);
let b = block_at(&d, 0);
let first = row_for_line(&d, b, 2);
let hit = table_row(&d, b, first + 1).unwrap();
assert!(hit.rows.len() > 1, "fixture: the row wraps");
assert_eq!((hit.line, hit.sub, hit.rows.start), (2, 1, first));
}
#[test]
fn every_line_of_a_multi_line_row_lands_on_it() {
let d = doc("Title\nmore\n=====\n");
let b = block_at(&d, 0);
assert_eq!(lines_of_row(&d, b, 0), Some(0..2));
assert_eq!(
(0..3).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[0, 0, 1]
);
assert_eq!(line_for_row(&d, b, 1), 2);
}
#[test]
fn a_math_preview_band_shifts_the_source_rows_down() {
let mut d = doc("$$\nx\n$$\n");
let b = block_at(&d, 0);
d.math_source_offset = Some((b, 1));
assert_eq!(lines_of_row(&d, b, 0), None);
assert_eq!(
line_for_row(&d, b, 0),
0,
"a band row resolves to the first line"
);
assert_eq!(
(0..3).map(|l| row_for_line(&d, b, l)).collect::<Vec<_>>(),
[1, 2, 3]
);
assert_eq!(
(1..4).map(|r| line_for_row(&d, b, r)).collect::<Vec<_>>(),
[0, 1, 2]
);
}
#[test]
fn an_image_pins_every_reserved_row_to_its_line() {
let d = doc("\n");
let b = block_at(&d, 0);
assert_eq!(d.block_own_line_count(b), 4);
assert!((0..4).all(|r| line_for_row(&d, b, r) == 0));
assert_eq!(row_for_line(&d, b, 0), 0);
}
#[test]
fn a_position_lands_on_the_row_showing_its_char() {
let d = doc("- - a\n - b\n- c\n");
let b = block_at(&d, 0);
assert_eq!(row_for_line(&d, b, 0), 0);
for col in [0, 2, 4, 5] {
assert_eq!(row_for_pos(&d, b, RawPos { line: 0, col }), 1, "col {col}");
}
assert_eq!(row_for_pos(&d, b, RawPos { line: 2, col: 2 }), 3);
let d = doc("- ```bash\n x\n ```\n");
let b = block_at(&d, 0);
assert_eq!(row_for_pos(&d, b, RawPos { line: 0, col: 4 }), 0);
}
fn row_with(d: &ParsedDoc, block: usize, needle: &str) -> usize {
let rows = d.source_map.rendered_lines_for_block(block);
rows.clone()
.position(|r| {
d.lines[r]
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains(needle)
})
.expect("row present")
}
fn pos(line: usize, col: usize) -> RawPos {
RawPos { line, col }
}
#[test]
fn only_verbatim_rows_never_reveal() {
let d = doc("```rust\nlet x = 1;\n```\n\n indented\n\npara\n\n```\nopen\n");
let fenced = block_at(&d, 0);
assert_eq!(
(0..3).map(|r| reveals(&d, fenced, r)).collect::<Vec<_>>(),
[true, false, true]
);
let src = d.source();
let indented = block_at(&d, src.find("indented").unwrap());
assert!(!reveals(&d, indented, 0));
assert!(reveals(&d, block_at(&d, src.find("para").unwrap()), 0));
let open = block_at(&d, src.find("open").unwrap());
assert!(!reveals(&d, open, row_with(&d, open, "open")));
}
#[test]
fn a_figures_off_formula_reveals_only_its_delimiters() {
let d = ParsedDoc::build_with_overrides(
"$$\nE = mc^2\n$$\n",
theme(),
true,
4,
None,
None,
false,
80,
false,
false,
false,
false,
None,
);
let b = block_at(&d, 0);
let body = row_with(&d, b, "E = mc^2");
assert!(!reveals(&d, b, body));
assert!(reveals(&d, b, body - 1) && reveals(&d, b, body + 1));
}
#[test]
fn a_code_row_maps_past_its_pad_cell() {
let d = doc("- a\n\n ```\n let x = 1;\n ```\n");
let b = block_at(&d, 0);
let row = row_with(&d, b, "let x");
let eq = 2 + "let x ".len();
assert_eq!(rendered_to_raw_col(&d, b, row, 9), pos(3, eq));
assert_eq!(raw_to_rendered_col(&d, b, row, pos(3, eq)), Some(9));
assert_eq!(
rendered_to_raw_col(&d, b, row, 2),
pos(3, 2),
"the pad cell"
);
assert_eq!(
rendered_to_raw_col(&d, b, row, 0),
pos(3, 2),
"the item's indent"
);
assert_eq!(rendered_to_raw_col(&d, b, row, 60), pos(3, 12), "the fill");
assert_eq!(raw_to_rendered_col(&d, b, row, pos(3, 0)), Some(3));
}
#[test]
fn a_chrome_rows_click_lands_past_the_container_prefix() {
let d = doc("- a\n\n ```rust\n x\n ```\n");
let b = block_at(&d, 0);
let label = row_with(&d, b, "rust");
assert_eq!(rendered_to_raw_col(&d, b, label, 3), pos(2, 2));
assert_eq!(rendered_to_raw_col(&d, b, label + 2, 0), pos(4, 2));
let d = doc("> - ```\n> x\n> ```\n");
let b = block_at(&d, 0);
let body = row_with(&d, b, "x");
assert_eq!(rendered_to_raw_col(&d, b, body + 1, 0), pos(2, 4));
assert_eq!(rendered_to_raw_col(&d, b, body - 1, 0), pos(0, 4));
let d = doc("> Foo\n> ===\n");
let b = block_at(&d, 0);
assert_eq!(lines_of_row(&d, b, 1), Some(1..2), "the rule row");
assert_eq!(rendered_to_raw_col(&d, b, 1, 0), pos(1, 2));
}
#[test]
fn an_indented_blocks_first_line_counts_from_its_range() {
let d = doc("Intro.\n\n let x = 1;\n");
let b = block_at(&d, d.source().find("let").unwrap());
assert_eq!(rendered_to_raw_col(&d, b, 0, 7), pos(0, 6));
assert_eq!(raw_to_rendered_col(&d, b, 0, pos(0, 6)), Some(7));
}
#[test]
fn a_prefix_aligns_with_its_raw_marker_from_the_right() {
let d = doc("- a\n - [ ] b\n");
let b = block_at(&d, 0);
let row = row_with(&d, b, "b");
let text: String = d.lines[row]
.spans
.iter()
.map(|s| s.content.as_ref())
.collect();
let bracket = text.chars().position(|c| c == '[').unwrap();
assert_eq!(rendered_to_raw_col(&d, b, row, bracket), pos(1, 4));
assert_eq!(raw_to_rendered_col(&d, b, row, pos(1, 4)), Some(bracket));
assert_eq!(
rendered_to_raw_col(&d, b, row, 0),
pos(1, 2),
"indent lands on the marker"
);
}
#[test]
fn a_padded_marker_aligns_with_its_glyph() {
for (src, marker) in [("1. foo\n", "1."), ("- foo\n", "-"), ("> foo\n", ">")] {
let d = doc(src);
let b = block_at(&d, 0);
let content = src.find('f').unwrap();
for (i, _) in marker.char_indices() {
assert_eq!(
rendered_to_raw_col(&d, b, 0, i),
pos(0, i),
"{src:?} char {i}"
);
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(0, i)),
Some(i),
"{src:?} col {i}"
);
}
let start = raw_to_rendered_col(&d, b, 0, pos(0, content)).unwrap();
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(0, content - 1)),
Some(start),
"{src:?}: a surplus space"
);
}
}
#[test]
fn an_escaped_trailing_backslash_maps_to_itself() {
let d = doc("a\\\\\nb\n");
let b = block_at(&d, 0);
assert_eq!(rendered_to_raw_col(&d, b, 0, 1), pos(0, 2));
assert_eq!(raw_to_rendered_col(&d, b, 0, pos(0, 2)), Some(1));
}
#[test]
fn a_flow_maps_across_its_lines() {
let d = ParsedDoc::build_with_overrides(
"one\n*two\nthree*\n",
theme(),
true,
4,
None,
None,
false,
80,
false,
false,
true,
true,
None,
);
let b = block_at(&d, 0);
assert_eq!(rendered_to_raw_col(&d, b, 0, 3), pos(0, 3), "the break");
assert_eq!(
rendered_to_raw_col(&d, b, 0, 4),
pos(1, 1),
"`t` past the `*`"
);
assert_eq!(rendered_to_raw_col(&d, b, 0, 8), pos(2, 0));
assert_eq!(raw_to_rendered_col(&d, b, 0, pos(2, 2)), Some(10));
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(1, 0)),
Some(4),
"a marker shows on the next char"
);
}
#[test]
fn a_row_cut_from_an_inline_maps_through_its_paragraph() {
let d = doc("a *b\nc* d\n");
let b = block_at(&d, 0);
assert_eq!(lines_of_row(&d, b, 0), Some(0..1));
assert_eq!(lines_of_row(&d, b, 1), Some(1..2));
assert_eq!(
rendered_to_raw_col(&d, b, 0, 2),
pos(0, 3),
"`b` past the `*`"
);
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(0, 4)),
Some(3),
"the row's end"
);
assert_eq!(rendered_to_raw_col(&d, b, 1, 0), pos(1, 0));
assert_eq!(
rendered_to_raw_col(&d, b, 1, 2),
pos(1, 3),
"`d` past the `*`"
);
assert_eq!(raw_to_rendered_col(&d, b, 1, pos(1, 1)), Some(1), "the `*`");
}
#[test]
fn a_row_no_map_matches_has_no_exact_columns() {
let d = doc("x[^ss] y\n\n[^ß]: n\n");
let b = block_at(&d, 0);
assert_eq!(raw_to_rendered_col(&d, b, 0, pos(0, 7)), None);
assert_eq!(raw_to_rendered_col_near(&d, b, 0, pos(0, 7)), 7);
}
#[test]
fn a_rows_references_resolve_against_the_documents_definitions() {
let d = doc("see [it][r] now [^n] [^x]\n\n[r]: /u\n[^n]: note\n");
let b = block_at(&d, 0);
let text: String = d.lines[0]
.spans
.iter()
.map(|s| s.content.as_ref())
.collect();
assert!(text.starts_with("see it now [n] [^x]"), "{text:?}");
assert_eq!(
rendered_to_raw_col(&d, b, 0, 7),
pos(0, 12),
"the `n` of `now`"
);
assert_eq!(raw_to_rendered_col(&d, b, 0, pos(0, 12)), Some(7));
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(0, 19)),
Some(13),
"the `]` of `[^n]`"
);
assert_eq!(
raw_to_rendered_col(&d, b, 0, pos(0, 23)),
Some(17),
"the `x` of `[^x]`"
);
}
#[test]
fn an_atx_headings_closing_sequence_is_not_content() {
for src in ["## Title ##\n", "> ## Title ##\n"] {
let d = doc(src);
let b = block_at(&d, 0);
let row = row_with(&d, b, "Title");
let t = src.find('T').unwrap();
let shown = raw_to_rendered_col(&d, b, row, pos(0, t)).expect("an exact map");
assert_eq!(
raw_to_rendered_col(&d, b, row, pos(0, t + 4)),
Some(shown + 4),
"{src:?}"
);
assert_eq!(
rendered_to_raw_col(&d, b, row, shown + 4),
pos(0, t + 4),
"{src:?}"
);
}
}
#[test]
fn table_parts_follow_the_rows_the_renderer_drew() {
use TablePart::*;
let build = |src: &str, striping: bool| {
ParsedDoc::build_with_overrides(
src,
theme(),
true,
4,
None,
None,
striping,
24,
false,
false,
true,
false,
None,
)
};
let wrapped = "| a | b |\n|---|---|\n| 1 | alpha bravo charlie delta |\n| 3 | 4 |\n";
for striping in [false, true] {
let parsed = build(wrapped, striping);
let parts: Vec<_> = table_parts(&parsed, 0).into_iter().flatten().collect();
let want = [
TopBorder,
Header { sub: 0 },
HeavyRule,
DataRow { row: 0, sub: 0 },
DataRow { row: 0, sub: 1 },
Separator,
DataRow { row: 1, sub: 0 },
BottomBorder,
];
assert_eq!(parts, want.map(|p| (0, p)), "striping {striping}");
}
assert!(
table_parts(&doc("para\n"), 0).is_empty(),
"a block with no table"
);
let parsed = doc("| a |\n|---|\n");
let parts: Vec<_> = table_parts(&parsed, 0).into_iter().flatten().collect();
let want = [TopBorder, Header { sub: 0 }, HeavyRule, BottomBorder];
assert_eq!(parts, want.map(|p| (0, p)));
let parsed = doc("- | a |\n |---|\n | 1 |\n\n | c |\n |---|\n");
let parts = table_parts(&parsed, 0);
assert_eq!(parts[0], None, "the marker's row");
assert_eq!(parts[1], Some((0, TopBorder)));
assert_eq!(parts[4], Some((0, DataRow { row: 0, sub: 0 })));
assert_eq!(parts[5], Some((0, BottomBorder)));
let second: Vec<_> = parts.iter().flatten().filter(|(h, _)| *h == 4).collect();
assert_eq!(second.first(), Some(&&(4, TopBorder)));
assert_eq!(second.last(), Some(&&(4, BottomBorder)));
}
#[test]
fn item_content_cols_name_each_item_holding_a_line() {
let cases: [(&str, usize, &[usize]); 8] = [
("- foo\n", 0, &[2]),
("10. foo\n", 0, &[4]),
("- foo\n bar\n", 1, &[4]),
("- outer\n - inner\n", 1, &[2, 4]),
("- outer\n - inner\n\n more\n", 3, &[2]),
("> - foo\n", 0, &[]),
("x[^n]\n\n[^n]: - foo\n", 2, &[]),
("para\n", 0, &[]),
];
for (src, line, want) in cases {
assert_eq!(
item_content_cols(&doc(src), line),
want,
"{src:?} line {line}"
);
}
}
}