documents: block commands reread their result

Fixes R4.7 round 2: a heading change or split is parsed again and
refused as Misread when its text, formats or kind would change;
closing hashes and block openers are escaped at split ends; task
boxes below a marker stay; indented code is written from its value;
alerts unwrap past blank and lazy lines; delete counts every gone
opener.

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:36:49 -04:00
1 parent b37031b00b
commit 892e095025
12 files changed
+435 -90

No files matched your search

+3 -3
View File
@@ -9,7 +9,7 @@ use crate::documents::{
};
/// `marked` after `command`: `‸` is the caret, `⟨…⟩` the selection.
fn ran(marked: &str, command: BlockCommand) -> Result<String, EditRefusal> {
pub(super) fn ran(marked: &str, command: BlockCommand) -> Result<String, EditRefusal> {
let caret = marked.find('‸');
let text: String = marked.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect();
let selection = match caret {
@@ -35,12 +35,12 @@ fn ran(marked: &str, command: BlockCommand) -> Result<String, EditRefusal> {
})
}
fn ok(marked: &str, command: BlockCommand) -> String {
pub(super) fn ok(marked: &str, command: BlockCommand) -> String {
ran(marked, command).expect("ran")
}
/// The top-level blocks `text` reads as.
fn kinds(text: &str) -> Vec<&'static str> {
pub(super) fn kinds(text: &str) -> Vec<&'static str> {
let text: String = text.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect();
let root = file_tree(&text);
let kids = root.children().map(Vec::as_slice).unwrap_or_default();
+143
View File
@@ -0,0 +1,143 @@
//! Block commands whose results reread as written.
use markdown::mdast::Node;
use super::block::{kinds, ok, ran};
use crate::documents::{BlockCommand, EditRefusal, markdown::file_tree};
fn plain(marked: &str) -> String {
marked.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect()
}
/// Every node of `text`, in document order.
fn nodes(text: &str, keep: &mut dyn FnMut(&Node)) {
fn walk(node: &Node, keep: &mut dyn FnMut(&Node)) {
keep(node);
for kid in node.children().map(Vec::as_slice).unwrap_or_default() {
walk(kid, keep);
}
}
walk(&file_tree(text), keep);
}
fn checks(text: &str) -> Vec<Option<bool>> {
let mut out = Vec::new();
nodes(text, &mut |node| {
if let Node::ListItem(item) = node {
out.push(item.checked);
}
});
out
}
fn codes(text: &str) -> Vec<String> {
let mut out = Vec::new();
nodes(text, &mut |node| {
if let Node::Code(code) = node {
out.push(code.value.clone());
}
});
out
}
fn texts(text: &str) -> Vec<String> {
let mut out = Vec::new();
nodes(text, &mut |node| {
if let Node::Text(text) = node {
out.push(text.value.clone());
}
});
out
}
#[test]
fn a_split_that_would_reread_otherwise_refuses() {
assert_eq!(
ran("~~ab‸~cd~~", BlockCommand::Rule),
Err(EditRefusal::Misread)
);
assert_eq!(
ran("__ab‸_cd__", BlockCommand::Rule),
Err(EditRefusal::Misread)
);
}
#[test]
fn a_split_rest_that_would_open_a_block_is_escaped() {
let out = plain(&ok("a ‸- b", BlockCommand::Rule));
assert_eq!(out, "a\n\n---\n\n\\- b");
assert_eq!(kinds(&out), ["p", "rule", "p"]);
}
#[test]
fn closing_hashes_stay_text_wherever_a_heading_line_ends() {
let out = plain(&ok("alpha #‸\n===", BlockCommand::Heading(3)));
assert_eq!(out, "### alpha \\#");
assert_eq!(texts(&out), ["alpha #"]);
let out = plain(&ok("# a #‸ b", BlockCommand::Rule));
assert_eq!(kinds(&out), ["h", "rule", "h"]);
assert_eq!(texts(&out), ["a #", "b"]);
}
#[test]
fn a_paragraph_reads_the_heading_body_without_closing_hashes() {
let out = plain(&ok("# ---‸ ###", BlockCommand::Paragraph));
assert_eq!(out, "\\---");
assert_eq!(kinds(&out), ["p"]);
let out = plain(&ok("# [x]: /u‸ ###", BlockCommand::Paragraph));
assert_eq!(kinds(&out), ["p"]);
assert_eq!(texts(&out).concat(), "[x]: /u");
}
#[test]
fn a_task_box_below_its_marker_stays_checked() {
let out = plain(&ok("1.‸\n [x] a", BlockCommand::TaskList));
assert_eq!(checks(&out), [Some(true)]);
assert_eq!(texts(&out), ["a"]);
}
#[test]
fn retyping_keeps_code_columns_past_tabs() {
let source = "-\t\t‸code";
let out = plain(&ok(source, BlockCommand::OrderedList));
assert_eq!(codes(&out), codes(&plain(source)));
assert_eq!(codes(&out).len(), 1);
}
#[test]
fn a_task_cannot_box_code() {
assert_eq!(
ran("- ‸a", BlockCommand::TaskList),
Err(EditRefusal::Code)
);
}
#[test]
fn an_alert_unwraps_into_its_holder() {
assert_eq!(
plain(&ok("- > [!NOTE]‸\n a", BlockCommand::Quote)),
"- a"
);
assert_eq!(
plain(&ok("> > [!NOTE]‸\n> a", BlockCommand::Quote)),
"> a"
);
let out = plain(&ok("- > [!NOTE]‸\n >\n >\n > a", BlockCommand::Quote));
assert_eq!(out, "- a");
assert_eq!(kinds(&out), ["list"]);
}
#[test]
fn an_alert_marker_sharing_its_line_refuses() {
assert_eq!(
ran("> [!NOTE]&#10;a‸", BlockCommand::Quote),
Err(EditRefusal::Misread)
);
}
#[test]
fn delete_keeps_neighbors_apart_past_gone_openers() {
let out = plain(&ok("p\n> # ‸h\nq", BlockCommand::Delete));
assert_eq!(out, "p\n\nq");
assert_eq!(kinds(&out), ["p", "p"]);
}
+1
View File
@@ -367,6 +367,7 @@ fn a_press_after_a_write_back_waits_for_the_long_read(cx: &mut TestAppContext) {
mod attach;
mod block;
mod block_edges;
mod format;
mod history;
mod links;