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:
1 parent
b37031b00b
commit
892e095025
12 files changed
+435
-90
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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] 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"]);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user