use crate::ast::*;
use crate::parser;
use crate::utf16::Utf16Map;
#[derive(Debug, Clone)]
pub struct ParseSnapshot {
pub blocks: Vec<BlockNode>,
pub line_count: u32,
pub source_byte_len: usize,
pub source_utf16_len: u32,
}
#[derive(Debug)]
pub struct IncrementalResult {
pub blocks: Vec<BlockNode>,
pub line_count: u32,
pub dirty_start: u32,
pub dirty_end: u32,
pub total_utf16_len: u32,
}
impl ParseSnapshot {
pub fn new(
blocks: Vec<BlockNode>,
line_count: u32,
source_byte_len: usize,
source_utf16_len: u32,
) -> Self {
Self {
blocks,
line_count,
source_byte_len,
source_utf16_len,
}
}
}
pub fn incremental_update(
prev: &ParseSnapshot,
new_source: &str,
edit_utf16_start: u32,
edit_old_utf16_len: u32,
edit_new_utf16_len: u32,
options: &parser::ParseOptions,
) -> IncrementalResult {
if prev.blocks.is_empty() {
return full_parse_result(new_source, options);
}
let edit_utf16_end_old = edit_utf16_start.saturating_add(edit_old_utf16_len);
let byte_delta = new_source.len() as i64 - prev.source_byte_len as i64;
let (first_overlap, last_overlap) =
find_overlap_range(&prev.blocks, edit_utf16_start, edit_utf16_end_old);
let first_dirty = first_overlap.saturating_sub(1);
let last_dirty = (last_overlap + 1).min(prev.blocks.len());
let new_utf16_map = Utf16Map::build(new_source.as_bytes());
if needs_full_reparse(
prev,
first_dirty,
last_dirty,
new_source,
&new_utf16_map,
edit_utf16_start,
edit_new_utf16_len,
) {
return full_parse_result(new_source, options);
}
let reparse_byte_start = prev.blocks[first_dirty].byte_start as usize;
let old_dirty_byte_end = prev.blocks[last_dirty - 1].byte_end as usize;
let reparse_byte_end_raw = (old_dirty_byte_end as i64 + byte_delta).max(0) as usize;
let reparse_byte_end =
extend_to_line_end(new_source, reparse_byte_end_raw.min(new_source.len()));
if reparse_byte_start >= reparse_byte_end || reparse_byte_start > new_source.len() {
return full_parse_result(new_source, options);
}
let dirty_text = &new_source[reparse_byte_start..reparse_byte_end];
let dirty_doc = parser::parse_with_options(dirty_text, ParseMode::Editable, options);
let base_utf16 = new_utf16_map.byte_to_utf16(reparse_byte_start as u32, new_source.as_bytes());
let base_line = count_newlines_before(new_source.as_bytes(), reparse_byte_start);
let prefix_count = first_dirty;
let suffix_start = last_dirty;
let dirty_block_count = dirty_doc.blocks.len();
let total_blocks = prefix_count + dirty_block_count + (prev.blocks.len() - suffix_start);
let mut result_blocks = Vec::with_capacity(total_blocks);
result_blocks.extend_from_slice(&prev.blocks[..first_dirty]);
for mut block in dirty_doc.blocks {
shift_block(
&mut block,
reparse_byte_start as u32,
base_utf16,
base_line as u32,
);
result_blocks.push(block);
}
let dirty_end_idx = result_blocks.len();
let suffix_byte_delta = reparse_byte_end as i64 - old_dirty_byte_end as i64;
let old_dirty_utf16_end = prev.blocks[last_dirty - 1].utf16_end;
let new_dirty_utf16_end =
new_utf16_map.byte_to_utf16(reparse_byte_end as u32, new_source.as_bytes());
let suffix_utf16_delta = new_dirty_utf16_end as i64 - old_dirty_utf16_end as i64;
let old_dirty_line_end = prev.blocks[last_dirty - 1].line_end;
let new_dirty_line_end = (base_line as u32).saturating_add(dirty_doc.line_count);
let suffix_line_delta = new_dirty_line_end as i64 - old_dirty_line_end as i64;
for block in &prev.blocks[suffix_start..] {
let mut b = block.clone();
if !shift_suffix_block(
&mut b,
suffix_byte_delta,
suffix_utf16_delta,
suffix_line_delta,
) {
return full_parse_result(new_source, options);
}
result_blocks.push(b);
}
let new_line_count = match safe_add_delta(prev.line_count, suffix_line_delta) {
Some(lc) => lc,
None => return full_parse_result(new_source, options),
};
IncrementalResult {
blocks: result_blocks,
line_count: new_line_count,
dirty_start: first_dirty as u32,
dirty_end: dirty_end_idx as u32,
total_utf16_len: new_utf16_map.total_utf16_len,
}
}
fn find_overlap_range(
blocks: &[BlockNode],
edit_utf16_start: u32,
edit_utf16_end: u32,
) -> (usize, usize) {
let first = blocks.partition_point(|b| b.utf16_end <= edit_utf16_start);
let last = blocks.partition_point(|b| b.utf16_start < edit_utf16_end);
let first = first.min(blocks.len().saturating_sub(1));
let last = last.max(first + 1).min(blocks.len());
(first, last)
}
fn needs_full_reparse(
prev: &ParseSnapshot,
first_dirty: usize,
last_dirty: usize,
new_source: &str,
new_utf16_map: &Utf16Map,
edit_utf16_start: u32,
edit_new_utf16_len: u32,
) -> bool {
for block in &prev.blocks[first_dirty..last_dirty] {
if matches!(
block.kind,
BlockKind::CodeBlock { .. }
| BlockKind::MermaidDiagram { .. }
| BlockKind::Table { .. }
) {
return true;
}
}
let scan_utf16_start = edit_utf16_start.saturating_sub(3); let scan_utf16_end = edit_utf16_start
.saturating_add(edit_new_utf16_len)
.saturating_add(3);
let scan_byte_start =
new_utf16_map.utf16_to_byte(scan_utf16_start, new_source.as_bytes()) as usize;
let scan_byte_end = (new_utf16_map.utf16_to_byte(scan_utf16_end, new_source.as_bytes())
as usize)
.min(new_source.len());
let scan_start = find_line_start(new_source, scan_byte_start);
let scan_end = extend_to_line_end(new_source, scan_byte_end);
let scan_region = &new_source[scan_start..scan_end];
for line in scan_region.lines() {
if line.trim().starts_with("```") {
return true;
}
}
false
}
fn full_parse_result(source: &str, options: &parser::ParseOptions) -> IncrementalResult {
let doc = parser::parse_with_options(source, ParseMode::Editable, options);
let block_count = doc.blocks.len() as u32;
let utf16_map = Utf16Map::build(source.as_bytes());
IncrementalResult {
blocks: doc.blocks,
line_count: doc.line_count,
dirty_start: 0,
dirty_end: block_count,
total_utf16_len: utf16_map.total_utf16_len,
}
}
fn shift_block(block: &mut BlockNode, byte_base: u32, utf16_base: u32, line_base: u32) {
block.byte_start += byte_base;
block.byte_end += byte_base;
block.utf16_start += utf16_base;
block.utf16_end += utf16_base;
block.line_start += line_base;
block.line_end += line_base;
if let Some(marker) = &mut block.list_marker {
marker.marker_utf16_start += utf16_base;
marker.marker_utf16_end += utf16_base;
marker.marker_byte_start += byte_base;
marker.marker_byte_end += byte_base;
marker.content_byte_start += byte_base;
}
for span in &mut block.inline_spans {
span.utf16_start += utf16_base;
span.utf16_end += utf16_base;
span.content_utf16_start += utf16_base;
span.content_utf16_end += utf16_base;
}
if let BlockKind::BulletList { items } | BlockKind::OrderedList { items, .. } = &mut block.kind
{
for item in items {
for span in &mut item.inline_spans {
span.utf16_start += utf16_base;
span.utf16_end += utf16_base;
span.content_utf16_start += utf16_base;
span.content_utf16_end += utf16_base;
}
}
}
}
fn safe_add_delta(base: u32, delta: i64) -> Option<u32> {
let result = base as i64 + delta;
if result < 0 || result > u32::MAX as i64 {
None
} else {
Some(result as u32)
}
}
fn shift_suffix_block(
block: &mut BlockNode,
byte_delta: i64,
utf16_delta: i64,
line_delta: i64,
) -> bool {
let Some(bs) = safe_add_delta(block.byte_start, byte_delta) else {
return false;
};
let Some(be) = safe_add_delta(block.byte_end, byte_delta) else {
return false;
};
let Some(us) = safe_add_delta(block.utf16_start, utf16_delta) else {
return false;
};
let Some(ue) = safe_add_delta(block.utf16_end, utf16_delta) else {
return false;
};
let Some(ls) = safe_add_delta(block.line_start, line_delta) else {
return false;
};
let Some(le) = safe_add_delta(block.line_end, line_delta) else {
return false;
};
block.byte_start = bs;
block.byte_end = be;
block.utf16_start = us;
block.utf16_end = ue;
block.line_start = ls;
block.line_end = le;
if let Some(marker) = &mut block.list_marker {
let Some(mus) = safe_add_delta(marker.marker_utf16_start, utf16_delta) else {
return false;
};
let Some(mue) = safe_add_delta(marker.marker_utf16_end, utf16_delta) else {
return false;
};
let Some(mbs) = safe_add_delta(marker.marker_byte_start, byte_delta) else {
return false;
};
let Some(mbe) = safe_add_delta(marker.marker_byte_end, byte_delta) else {
return false;
};
let Some(mcs) = safe_add_delta(marker.content_byte_start, byte_delta) else {
return false;
};
marker.marker_utf16_start = mus;
marker.marker_utf16_end = mue;
marker.marker_byte_start = mbs;
marker.marker_byte_end = mbe;
marker.content_byte_start = mcs;
}
for span in &mut block.inline_spans {
let Some(ss) = safe_add_delta(span.utf16_start, utf16_delta) else {
return false;
};
let Some(se) = safe_add_delta(span.utf16_end, utf16_delta) else {
return false;
};
let Some(cs) = safe_add_delta(span.content_utf16_start, utf16_delta) else {
return false;
};
let Some(ce) = safe_add_delta(span.content_utf16_end, utf16_delta) else {
return false;
};
span.utf16_start = ss;
span.utf16_end = se;
span.content_utf16_start = cs;
span.content_utf16_end = ce;
}
if let BlockKind::BulletList { items } | BlockKind::OrderedList { items, .. } = &mut block.kind
{
for item in items {
for span in &mut item.inline_spans {
let Some(ss) = safe_add_delta(span.utf16_start, utf16_delta) else {
return false;
};
let Some(se) = safe_add_delta(span.utf16_end, utf16_delta) else {
return false;
};
let Some(cs) = safe_add_delta(span.content_utf16_start, utf16_delta) else {
return false;
};
let Some(ce) = safe_add_delta(span.content_utf16_end, utf16_delta) else {
return false;
};
span.utf16_start = ss;
span.utf16_end = se;
span.content_utf16_start = cs;
span.content_utf16_end = ce;
}
}
}
true
}
fn count_newlines_before(source: &[u8], byte_offset: usize) -> usize {
source[..byte_offset.min(source.len())]
.iter()
.filter(|&&b| b == b'\n')
.count()
}
fn find_line_start(source: &str, byte_offset: usize) -> usize {
let offset = byte_offset.min(source.len());
if offset == 0 {
return 0;
}
let bytes = source.as_bytes();
for i in (0..offset).rev() {
if bytes[i] == b'\n' {
return i + 1;
}
}
0
}
fn extend_to_line_end(source: &str, byte_offset: usize) -> usize {
let offset = byte_offset.min(source.len());
if offset == source.len() {
return offset;
}
if offset == 0 || source.as_bytes()[offset - 1] == b'\n' {
return offset;
}
let bytes = source.as_bytes();
for (i, &b) in bytes.iter().enumerate().skip(offset) {
if b == b'\n' {
return i + 1;
}
}
source.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser;
fn snapshot(source: &str) -> ParseSnapshot {
let doc = parser::parse(source, ParseMode::Editable);
let utf16_map = Utf16Map::build(source.as_bytes());
ParseSnapshot::new(
doc.blocks,
doc.line_count,
source.len(),
utf16_map.total_utf16_len,
)
}
fn assert_matches_full_parse(result: &IncrementalResult, new_source: &str) {
let full = parser::parse(new_source, ParseMode::Editable);
assert_eq!(
result.blocks.len(),
full.blocks.len(),
"Block count mismatch: incremental={}, full={}",
result.blocks.len(),
full.blocks.len()
);
for (i, (inc, ful)) in result.blocks.iter().zip(full.blocks.iter()).enumerate() {
assert_eq!(
inc.kind, ful.kind,
"Block {} kind mismatch:\n incremental: {:?}\n full: {:?}",
i, inc.kind, ful.kind
);
assert_eq!(
inc.byte_start, ful.byte_start,
"Block {} byte_start: inc={}, full={}",
i, inc.byte_start, ful.byte_start
);
assert_eq!(
inc.byte_end, ful.byte_end,
"Block {} byte_end: inc={}, full={}",
i, inc.byte_end, ful.byte_end
);
assert_eq!(
inc.utf16_start, ful.utf16_start,
"Block {} utf16_start: inc={}, full={}",
i, inc.utf16_start, ful.utf16_start
);
assert_eq!(
inc.utf16_end, ful.utf16_end,
"Block {} utf16_end: inc={}, full={}",
i, inc.utf16_end, ful.utf16_end
);
assert_eq!(
inc.line_start, ful.line_start,
"Block {} line_start: inc={}, full={}",
i, inc.line_start, ful.line_start
);
assert_eq!(
inc.line_end, ful.line_end,
"Block {} line_end: inc={}, full={}",
i, inc.line_end, ful.line_end
);
assert_eq!(
inc.list_marker, ful.list_marker,
"Block {} list_marker mismatch:\n incremental: {:?}\n full: {:?}",
i, inc.list_marker, ful.list_marker
);
assert_eq!(
inc.inline_spans.len(),
ful.inline_spans.len(),
"Block {} inline_span count mismatch: inc={}, full={}",
i,
inc.inline_spans.len(),
ful.inline_spans.len()
);
for (j, (is_span, f_span)) in inc
.inline_spans
.iter()
.zip(ful.inline_spans.iter())
.enumerate()
{
assert_eq!(
is_span, f_span,
"Block {} span {} mismatch:\n incremental: {:?}\n full: {:?}",
i, j, is_span, f_span
);
}
}
assert_eq!(result.line_count, full.line_count, "Line count mismatch");
}
#[test]
fn single_char_insert_in_paragraph() {
let old = "# Title\n\nHello world\n\n---";
let snap = snapshot(old);
let new = "# Title\n\nXHello world\n\n---";
let result = incremental_update(&snap, new, 9, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
assert!(
result.dirty_end - result.dirty_start <= 3,
"Dirty range too large: {}..{}",
result.dirty_start,
result.dirty_end
);
}
#[test]
fn single_char_insert_in_heading() {
let old = "# Title\n\nSome text";
let snap = snapshot(old);
let new = "# Titles\n\nSome text";
let result = incremental_update(&snap, new, 7, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn delete_characters() {
let old = "# Title\n\nHello world\n\n---";
let snap = snapshot(old);
let new = "# Title\n\n world\n\n---";
let result = incremental_update(&snap, new, 9, 5, 0, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn replace_text() {
let old = "# Title\n\nHello\n\nWorld";
let snap = snapshot(old);
let new = "# Title\n\nGoodbye\n\nWorld";
let result = incremental_update(&snap, new, 9, 5, 7, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn insert_newline_splits_paragraph() {
let old = "# Title\n\nHello world";
let snap = snapshot(old);
let new = "# Title\n\nHello\nworld";
let result = incremental_update(&snap, new, 14, 1, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_at_document_end() {
let old = "# Title\n\nText";
let snap = snapshot(old);
let new = "# Title\n\nText more";
let result = incremental_update(&snap, new, 13, 0, 5, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_at_document_start() {
let old = "Hello\n\nWorld";
let snap = snapshot(old);
let new = "# Hello\n\nWorld";
let result = incremental_update(&snap, new, 0, 0, 2, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_in_checkbox() {
let old = "- [ ] Task one\n- [x] Task two";
let snap = snapshot(old);
let new = "- [ ] Task one done\n- [x] Task two";
let result = incremental_update(&snap, new, 14, 0, 5, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_in_bullet_list() {
let old = "- item 1\n- item 2\n- item 3";
let snap = snapshot(old);
let new = "- item 1\n- item TWO\n- item 3";
let result = incremental_update(&snap, new, 11, 6, 8, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn code_fence_triggers_full_reparse() {
let old = "# Title\n\nSome text\n\nMore text";
let snap = snapshot(old);
let new = "# Title\n\n```\nSome text\n\nMore text";
let result = incremental_update(&snap, new, 9, 0, 4, &parser::ParseOptions::default());
assert_eq!(result.dirty_start, 0);
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_inside_code_block_triggers_full_reparse() {
let old = "# Title\n\n```\ncode\n```\n\nText";
let snap = snapshot(old);
let new = "# Title\n\n```\ncode here\n```\n\nText";
let result = incremental_update(&snap, new, 13, 4, 9, &parser::ParseOptions::default());
assert_eq!(result.dirty_start, 0);
assert_matches_full_parse(&result, new);
}
#[test]
fn empty_document_insert() {
let old = "";
let snap = snapshot(old);
let new = "Hello";
let result = incremental_update(&snap, new, 0, 0, 5, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn single_block_document() {
let old = "Hello";
let snap = snapshot(old);
let new = "Hello world";
let result = incremental_update(&snap, new, 5, 0, 6, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn delete_entire_line() {
let old = "# Title\n\nMiddle\n\nEnd";
let snap = snapshot(old);
let new = "# Title\n\n\nEnd";
let result = incremental_update(&snap, new, 9, 7, 0, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn insert_new_line_between_blocks() {
let old = "# Title\n\nEnd";
let snap = snapshot(old);
let new = "# Title\n\nMiddle\n\nEnd";
let result = incremental_update(&snap, new, 9, 0, 8, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_with_inline_formatting() {
let old = "Hello **world**";
let snap = snapshot(old);
let new = "Hello **earth**";
let result = incremental_update(&snap, new, 8, 5, 5, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn multiple_blocks_edit() {
let old = "# One\n\n## Two\n\n### Three\n\nParagraph";
let snap = snapshot(old);
let new = "# One\n\n## TWO\n\n### Three\n\nParagraph";
let result = incremental_update(&snap, new, 10, 3, 3, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
assert!(result.dirty_end - result.dirty_start <= 4);
}
#[test]
fn edit_horizontal_rule() {
let old = "Before\n\n---\n\nAfter";
let snap = snapshot(old);
let new = "Before\n\n--\n\nAfter";
let result = incremental_update(&snap, new, 10, 1, 0, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn unicode_edit() {
let old = "Hello 🌍\n\nWorld";
let snap = snapshot(old);
let new = "Hello 🌍!\n\nWorld";
let result = incremental_update(&snap, new, 8, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn blockquote_edit() {
let old = "# Title\n\n> Quote text\n\nEnd";
let snap = snapshot(old);
let new = "# Title\n\n> Quote text more\n\nEnd";
let result = incremental_update(&snap, new, 20, 0, 5, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn dirty_range_is_minimal() {
let old = "# H1\n\n## H2\n\n### H3\n\nPara\n\n- B1\n\n- B2\n\n> Quote\n\n---\n\nEnd";
let snap = snapshot(old);
let new = "# H1\n\n## H2\n\n### H3\n\nParaX\n\n- B1\n\n- B2\n\n> Quote\n\n---\n\nEnd";
let result = incremental_update(&snap, new, 25, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
let dirty_count = result.dirty_end - result.dirty_start;
assert!(
dirty_count <= 4,
"Expected ≤4 dirty blocks, got {} ({}..{})",
dirty_count,
result.dirty_start,
result.dirty_end
);
}
#[test]
fn test_find_line_start() {
assert_eq!(find_line_start("hello\nworld", 0), 0);
assert_eq!(find_line_start("hello\nworld", 6), 6);
assert_eq!(find_line_start("hello\nworld", 8), 6);
assert_eq!(find_line_start("hello\nworld\nfoo", 12), 12);
}
#[test]
fn test_extend_to_line_end() {
assert_eq!(extend_to_line_end("hello\nworld", 3), 6);
assert_eq!(extend_to_line_end("hello\nworld", 6), 6);
assert_eq!(extend_to_line_end("hello\nworld", 8), 11);
assert_eq!(extend_to_line_end("hello\nworld", 11), 11);
assert_eq!(extend_to_line_end("hello\nworld", 0), 0);
assert_eq!(extend_to_line_end("hello\nworld\n", 12), 12);
}
#[test]
fn test_count_newlines_before() {
assert_eq!(count_newlines_before(b"hello\nworld\nfoo", 0), 0);
assert_eq!(count_newlines_before(b"hello\nworld\nfoo", 6), 1);
assert_eq!(count_newlines_before(b"hello\nworld\nfoo", 12), 2);
}
#[test]
fn test_find_overlap_range() {
let blocks = vec![
make_test_block(0, 5),
make_test_block(5, 10),
make_test_block(10, 15),
];
assert_eq!(find_overlap_range(&blocks, 6, 8), (1, 2));
assert_eq!(find_overlap_range(&blocks, 0, 3), (0, 1));
assert_eq!(find_overlap_range(&blocks, 4, 11), (0, 3));
let (f, l) = find_overlap_range(&blocks, 5, 5);
assert!(
f <= 1 && l >= 2,
"Boundary insertion should include adjacent block: {f}..{l}"
);
}
#[test]
fn edit_inside_nested_list_item() {
let old = "- a\n - b\n - c\n- d";
let snap = snapshot(old);
let new = "- a\n - bXY\n - c\n- d";
let result = incremental_update(&snap, new, 11, 0, 2, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
assert!(
result.dirty_end - result.dirty_start <= 3,
"Dirty range too large: {}..{}",
result.dirty_start,
result.dirty_end
);
}
#[test]
fn insert_nested_item_between_items() {
let old = "- a\n- b";
let snap = snapshot(old);
let new = "- a\n - a1\n- b";
let result = incremental_update(&snap, new, 4, 0, 9, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn indent_change_reclassifies_line() {
let old = "- a\n- b\n- c";
let snap = snapshot(old);
let new = "- a\n - b\n- c";
let result = incremental_update(&snap, new, 4, 0, 4, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn dedent_nested_item() {
let old = "- a\n - b\n- c";
let snap = snapshot(old);
let new = "- a\n- b\n- c";
let result = incremental_update(&snap, new, 4, 8, 0, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn edit_inside_nested_checkbox() {
let old = "- a\n - [ ] task\n - [x] done";
let snap = snapshot(old);
let new = "- a\n - [ ] task now\n - [x] done";
let result = incremental_update(&snap, new, 18, 0, 4, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn nested_list_edit_with_tab_indent() {
let old = "- a\n\t- b\n\t\t- c";
let snap = snapshot(old);
let new = "- a\n\t- bZ\n\t\t- c";
let result = incremental_update(&snap, new, 8, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn nested_item_becomes_deep_code_when_past_cap() {
let old = format!("- a\n{}- deep", " ".repeat(32));
let snap = snapshot(&old);
let new = format!("- a\n{}- deep", " ".repeat(33));
let result = incremental_update(&snap, &new, 4, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&result, &new);
}
#[test]
fn table_edit_triggers_full_reparse() {
let old = "# Title\n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\nEnd";
let snap = snapshot(old);
let new = "# Title\n\n| A | B |\n| --- | --- |\n| X | 2 |\n\nEnd";
let result = incremental_update(&snap, new, 34, 1, 1, &parser::ParseOptions::default());
assert_eq!(result.dirty_start, 0);
assert_matches_full_parse(&result, new);
}
#[test]
fn ordered_list_edit() {
let old = "1. first\n2. second\n3. third";
let snap = snapshot(old);
let new = "1. first\n2. SECOND\n3. third";
let result = incremental_update(&snap, new, 12, 6, 6, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn large_deletion_removes_multiple_blocks() {
let old = "# H1\n\n## H2\n\n### H3\n\nParagraph\n\nEnd";
let snap = snapshot(old);
let new = "# H1\n\nEnd";
let result = incremental_update(&snap, new, 6, 26, 0, &parser::ParseOptions::default());
assert_matches_full_parse(&result, new);
}
#[test]
fn chained_incremental_edits() {
let old = "# Title\n\nHello";
let snap = snapshot(old);
let new1 = "# Title\n\nHelloa";
let r1 = incremental_update(&snap, new1, 14, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&r1, new1);
let utf16_map = Utf16Map::build(new1.as_bytes());
let snap1 = ParseSnapshot::new(
r1.blocks,
r1.line_count,
new1.len(),
utf16_map.total_utf16_len,
);
let new2 = "# Title\n\nHelloab";
let r2 = incremental_update(&snap1, new2, 15, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&r2, new2);
let utf16_map2 = Utf16Map::build(new2.as_bytes());
let snap2 = ParseSnapshot::new(
r2.blocks,
r2.line_count,
new2.len(),
utf16_map2.total_utf16_len,
);
let new3 = "# Title\n\nHelloabc";
let r3 = incremental_update(&snap2, new3, 16, 0, 1, &parser::ParseOptions::default());
assert_matches_full_parse(&r3, new3);
}
fn make_test_block(utf16_start: u32, utf16_end: u32) -> BlockNode {
BlockNode {
kind: BlockKind::Paragraph {
text: String::new(),
},
line_start: 0,
line_end: 1,
utf16_start,
utf16_end,
byte_start: utf16_start,
byte_end: utf16_end,
list_marker: None,
inline_spans: vec![],
}
}
}