documents: block commands keep text, tasks and list items whole

Round-1 fixes for R4.7: a caret in heading marks splits at the text; a
split closes and reopens bold, italic and strike, refusing inside links,
code, math and HTML; a task's first text takes no block; retype keeps
task boxes and code indent and takes siblings past the list; an alert
in an item keeps the item's opener; paragraph-before moves a holder's
first text below its opener; delete keeps neighbors from joining; a
heading's text keeps closing hashes and block openers escaped; a heading
level is idempotent, as Typora's menu calls it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 14:08:40 -04:00
1 parent 58e212d4ec
commit b37031b00b
9 files changed
+438 -153

No files matched your search

+107 -3
View File
@@ -62,10 +62,10 @@ fn kinds(text: &str) -> Vec<&'static str> {
}
#[test]
fn headings_set_and_toggle_as_typora() {
fn headings_set_as_typoras_menu() {
use BlockCommand::*;
assert_eq!(ok("a‸b", Heading(2)), "## a‸b");
assert_eq!(ok("## a‸b", Heading(2)), "a‸b");
assert_eq!(ok("## a‸b", Heading(2)), "## a‸b");
assert_eq!(ok("## a‸b", Heading(3)), "### a‸b");
assert_eq!(ok("a‸", Promote), "###### a‸");
assert_eq!(ok("# a‸", Promote), "# a‸");
@@ -189,7 +189,12 @@ fn empty_paragraphs_come_before_and_after() {
assert_eq!(ok("# a‸\nb", ParagraphAfter), "# a\n\n‸\n\nb");
assert_eq!(ok("a\n\nb‸", ParagraphBefore), "a\n\n‸\n\nb");
assert_eq!(ok("a\n# h‸", ParagraphBefore), "a\n\n‸\n\n# h");
assert_eq!(ok("- a‸", ParagraphBefore), "- ‸\n\n a");
let out = ok("- a‸", ParagraphBefore);
assert_eq!(out, "- ‸\n a");
assert_eq!(kinds(&out), ["list"]);
let out = ok("x[^1]\n\n[^1]: a‸", ParagraphBefore);
assert_eq!(out, "x[^1]\n\n[^1]: ‸\n a");
assert_eq!(kinds(&out), ["p", "note"]);
}
#[test]
@@ -268,3 +273,102 @@ fn a_command_past_the_open_block_writes_it_back_first(cx: &mut TestAppContext) {
);
document.read_with(cx, |document, _| assert!(document.open.is_none()));
}
/// The one list's items, each as its blocks' kinds.
fn items(text: &str) -> Vec<Vec<&'static str>> {
let text: String = text.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect();
let root = file_tree(&text);
let Some(Node::List(list)) = root.children().and_then(|kids| kids.first()) else {
panic!("a list first");
};
(list.children.iter())
.map(|item| {
(item
.children()
.map(Vec::as_slice)
.unwrap_or_default()
.iter())
.map(|kid| match kid {
Node::Paragraph(_) => "p",
Node::Code(_) => "code",
_ => "other",
})
.collect()
})
.collect()
}
#[test]
fn a_caret_in_heading_marks_splits_at_the_text() {
use BlockCommand::Rule;
assert_eq!(ok("‸# alpha", Rule), "---\n\n# ‸alpha");
let out = ok("# alpha #‸##", Rule);
assert_eq!(kinds(&out), ["h", "rule"]);
}
#[test]
fn a_task_list_keeps_checked_boxes() {
assert_eq!(
ok("- a\n- [x] b‸", BlockCommand::TaskList),
"- [ ] a\n- [x] b‸"
);
}
#[test]
fn an_alert_in_an_item_keeps_the_item() {
let out = ok("- > [!NOTE]\n > a‸\n- b", BlockCommand::Quote);
assert_eq!(out, "- a‸\n- b");
assert_eq!(items(&out), [vec!["p"], vec!["p"]]);
}
#[test]
fn a_split_closes_and_reopens_inline_marks() {
use BlockCommand::Rule;
let out = ok("**ab‸cd**", Rule);
assert_eq!(out, "**ab**\n\n---\n\n**‸cd**");
assert_eq!(kinds(&out), ["p", "rule", "p"]);
assert_eq!(ran("[a‸b](u)", Rule), Err(EditRefusal::Across));
assert_eq!(ran("`a‸b`", Rule), Err(EditRefusal::Across));
}
#[test]
fn a_retyped_item_keeps_its_code_indent() {
let out = ok("- code‸", BlockCommand::OrderedList);
assert_eq!(out, "1. code‸");
assert_eq!(items(&out), [vec!["code"]]);
}
#[test]
fn deleting_keeps_neighbors_apart() {
let out = ok("p\n# h‸\nq", BlockCommand::Delete);
assert_eq!(out, "p\n‸\nq");
assert_eq!(kinds(&out), ["p", "p"]);
}
#[test]
fn heading_marks_keep_their_text() {
use BlockCommand::*;
let out = ok("alpha #‸", Heading(1));
assert_eq!(out, "# alpha \\#‸");
assert_eq!(kinds(&out), ["h"]);
let out = ok("# - alpha‸", Paragraph);
assert_eq!(out, "\\- alpha‸");
assert_eq!(kinds(&out), ["p"]);
assert_eq!(kinds(&ok("# 1. a‸", Paragraph)), ["p"]);
}
#[test]
fn a_tasks_first_text_takes_no_block() {
use BlockCommand::*;
assert_eq!(ran("- [ ] a‸", Quote), Err(EditRefusal::Outside));
assert_eq!(ran("- [ ] ‸ab", Rule), Err(EditRefusal::Outside));
assert_eq!(ran("- [ ] ‸ab", CodeBlock), Err(EditRefusal::Outside));
assert_eq!(ran("- [ ] a‸", ParagraphBefore), Err(EditRefusal::Outside));
}
#[test]
fn a_selection_past_a_list_takes_its_siblings() {
let out = ok("- ⟨a\n\nb⟩", BlockCommand::OrderedList);
assert_eq!(out, "1. ⟨a\n\n2. b⟩");
assert_eq!(kinds(&out), ["list"]);
}