use text_document::MoveMode;
use text_document::TextDocument;
use text_document::{BlockSnapshot, FlowElementSnapshot};
const MD: &str = "# Title\n\nA normal paragraph.\n\n> A single-level blockquote.\n>\n> > Nested blockquote at depth 2. Inline formatting works inside.\n> >\n> > > A third-level nested blockquote, for good measure.\n\n## After\n\nTrailing paragraph at the end.\n";
const MD_WITH_TABLE: &str = "# Title\n\nA normal paragraph.\n\n> A single-level blockquote.\n>\n> > Nested blockquote at depth 2. Inline formatting works inside.\n> >\n> > > A third-level nested blockquote, for good measure.\n\n## A table\n\n| A | B |\n|---|---|\n| c | d |\n\n## After\n\nTrailing paragraph at the end.\n";
fn collect_blocks(els: &[FlowElementSnapshot], out: &mut Vec<BlockSnapshot>) {
for el in els {
match el {
FlowElementSnapshot::Block(b) => out.push(b.clone()),
FlowElementSnapshot::Frame(f) => collect_blocks(&f.elements, out),
FlowElementSnapshot::Table(t) => {
for cell in &t.cells {
out.extend(cell.blocks.iter().cloned());
}
}
}
}
}
fn flow_position_of(doc: &TextDocument, needle: &str) -> usize {
let snap = doc.snapshot_flow();
let mut blocks = Vec::new();
collect_blocks(&snap.elements, &mut blocks);
blocks
.into_iter()
.find(|b| b.text.contains(needle))
.map(|b| b.position)
.expect("block with needle present in flow")
}
fn assert_backspace_lands_in(md: &str, needle: &str, expected_after_fragment: &str) {
let doc = TextDocument::new();
doc.set_markdown(md).unwrap().wait().unwrap();
let flow_pos = flow_position_of(&doc, needle);
let caret = flow_pos + 5;
let before_len = doc.to_plain_text().unwrap().chars().count();
let cursor = doc.cursor_at(caret);
cursor.set_position(caret, MoveMode::MoveAnchor);
cursor.delete_previous_char().unwrap();
let after = doc.to_plain_text().unwrap();
assert_eq!(
after.chars().count(),
before_len - 1,
"backspace at flow pos {caret} must remove exactly one char"
);
assert!(
after.contains(expected_after_fragment),
"backspace at flow pos {caret} must edit the '{needle}' block, not another \
block (expected fragment {expected_after_fragment:?}; got: {after:?})"
);
}
#[test]
fn backspace_in_depth2_blockquote() {
assert_backspace_lands_in(
MD,
"Nested blockquote at depth 2",
"Nestd blockquote at depth 2",
);
}
#[test]
fn backspace_in_depth2_blockquote_with_table_present() {
assert_backspace_lands_in(
MD_WITH_TABLE,
"Nested blockquote at depth 2",
"Nestd blockquote at depth 2",
);
}
#[test]
fn backspace_after_table_with_blockquotes_present() {
assert_backspace_lands_in(
MD_WITH_TABLE,
"Trailing paragraph at the end",
"Traiing paragraph at the end",
);
}
#[test]
fn backspace_in_trailing_paragraph_no_table() {
assert_backspace_lands_in(
MD,
"Trailing paragraph at the end",
"Traiing paragraph at the end",
);
}
#[test]
fn cross_frame_backspace_then_followup_stays_consistent() {
let doc = TextDocument::new();
doc.set_markdown(MD).unwrap().wait().unwrap();
let quote_pos = flow_position_of(&doc, "A single-level blockquote");
let before = doc.to_plain_text().unwrap();
let cursor = doc.cursor_at(quote_pos);
cursor.set_position(quote_pos, MoveMode::MoveAnchor);
cursor.delete_previous_char().unwrap();
let pos_after_first = cursor.position();
cursor.set_position(pos_after_first, MoveMode::MoveAnchor);
cursor.delete_previous_char().unwrap();
let after = doc.to_plain_text().unwrap();
assert_eq!(
after.chars().count(),
before.chars().count() - 2,
"two cross-frame backspaces must remove exactly two characters total"
);
}
#[test]
fn delete_spanning_depth2_quote_prunes_all_levels() {
let doc = TextDocument::new();
doc.set_markdown(MD).unwrap().wait().unwrap();
let quote_pos = flow_position_of(&doc, "Nested blockquote at depth 2");
let end_pos = quote_pos + "Nested blockquote at depth 2. Inline formatting works inside.".len();
let cursor = doc.cursor_at(quote_pos);
cursor.set_position(quote_pos, MoveMode::MoveAnchor);
cursor.set_position(end_pos, MoveMode::KeepAnchor);
cursor.remove_selected_text().unwrap();
let after_pos = cursor.position();
cursor.set_position(after_pos, MoveMode::MoveAnchor);
cursor.delete_previous_char().unwrap();
let plain = doc.to_plain_text().unwrap();
assert!(
!plain.contains("Nested blockquote at depth 2."),
"the depth-2 quote text must be gone after the delete"
);
}
#[test]
fn insert_frame_inside_document_keeps_fast_path_alive() {
let doc = TextDocument::new();
doc.set_markdown(MD).unwrap().wait().unwrap();
let para_pos = flow_position_of(&doc, "A normal paragraph");
let cursor = doc.cursor_at(para_pos + 5);
cursor.set_position(para_pos + 5, MoveMode::MoveAnchor);
cursor.insert_frame().unwrap();
assert_backspace_lands_in(
MD,
"Trailing paragraph at the end",
"Traiing paragraph at the end",
);
let trail = flow_position_of(&doc, "Trailing paragraph at the end");
let cursor = doc.cursor_at(trail + 5);
cursor.set_position(trail + 5, MoveMode::MoveAnchor);
cursor.delete_previous_char().unwrap();
let plain = doc.to_plain_text().unwrap();
assert!(
plain.contains("Traiing paragraph at the end"),
"after insert_frame in a nested context, backspace must still land in the right block; got: {plain:?}"
);
}