@@ -110,159 +110,6 @@ impl Inverse {
110110 }
111111}
112112
113−/// The session op log (§10.10): what undo would take back and redo would
114−/// do again, and the blocks an editor save left in transit (§5.2), each
115−/// by its file, with the length of `undo` once the entry of the save that
116−/// wrote its embed out was in. What the TUI and the app hold between
117−/// verbs.
118−#[derive(Debug, Default)]
119−pub struct OpLog {
120− undo: Vec<Inverse>,
121− redo: Vec<Inverse>,
122− transit: Vec<(String, usize)>,
123−}
124−
125−impl OpLog {
126− /// The entry undo would take back.
127− pub fn last_undo(&self) -> Option<&Inverse> {
128− self.undo.last()
129− }
130−
131− /// The entry redo would do again.
132− pub fn last_redo(&self) -> Option<&Inverse> {
133− self.redo.last()
134− }
135−
136− /// How many entries undo can take back.
137− pub fn depth(&self) -> usize {
138− self.undo.len()
139− }
140−
141− /// An operation's entry. `None`, an operation that changed nothing,
142− /// leaves the log as it is, the redo stack too. True when recorded.
143− pub fn record(&mut self, inv: Option<Inverse>) -> bool {
144− let Some(inv) = inv else { return false };
145− self.undo.push(inv);
146− self.redo.clear();
147− true
148− }
149−
150− /// `record` for a save of the editor, its *Revert*, or a reload that
151− /// re-renders it (§11.2): a block an earlier save left in transit
152− /// (§5.2) and this one deletes is deleted in the entry of that save,
153− /// which wrote its embed out, so one undo puts back its file and its
154− /// embed together, never the file embedded nowhere. One this save
155− /// pastes back, writing its embed again, makes that save, this one and
156− /// those between one entry, so no undo stops where its file is embedded
157− /// nowhere either. `in_transit` are the files of the blocks the editor
158− /// holds in transit now, remembered with this save's entry until their
159− /// file is deleted or embedded again; `editing`, whether it is still
160− /// open. True when there was an entry to record.
161− pub fn record_edit(&mut self, inv: Option<Inverse>, vault: &Vault, editing: bool, in_transit: Vec<String>) -> bool {
162− // the length of `undo` once this save's entry is in, if it has one
163− let mut at = None;
164− let recorded = inv.is_some();
165− if let Some(mut inv) = inv {
166− let (transit, undo) = (&self.transit, &mut self.undo);
167− inv.changes.retain(|c| {
168− let entry = transit.iter().find(|(p, _)| *p == c.path && c.after.is_none());
169− // that entry, and none since, left the file as it was
170− let Some(&(_, n)) = entry.filter(|&&(_, n)| n > 0 && n <= undo.len()) else { return true };
171− if undo[n - 1..].iter().any(|e| e.changes.iter().any(|x| x.path == c.path)) {
172− return true;
173− }
174− undo[n - 1].changes.push(c.clone());
175− false
176− });
177− if !inv.changes.is_empty() {
178− self.undo.push(inv);
179− at = Some(self.undo.len());
180− }
181− self.redo.clear();
182− }
183− // pasted back: joined from the earliest save that wrote out the
184− // embed of a block embedded again now
185− let mut back: Vec<usize> =
186− self.transit.iter().filter(|(p, _)| embedded(vault, p) == Some(true)).map(|&(_, n)| n).collect();
187− back.sort_unstable();
188− if let Some(n) = back.into_iter().find(|&n| n > 0 && self.join(n - 1)) {
189− // the entries from `n - 1` on are the one at `n - 1` now, if any
190− let joined = self.undo.len() == n;
191− at = at.and(joined.then_some(n));
192− self.transit.retain_mut(|(_, m)| {
193− *m = (*m).min(n);
194− *m < n || joined
195− });
196− }
197− // still in transit, or pasted back with its embed not written yet
198− self.transit.retain(|(p, _)| editing && embedded(vault, p) == Some(false));
199− if let Some(n) = at {
200− for p in in_transit {
201− if !self.transit.iter().any(|(q, _)| *q == p) {
202− self.transit.push((p, n));
203− }
204− }
205− }
206− recorded
207− }
208−
209− /// Make the entries from `from` on one: for each file, its text before
210− /// the first and after the last; one that ends as it began is left
211− /// out, and an entry left with none is dropped. False, with the entries
212− /// as they were, where a file changed from outside between two of
213− /// them: undoing them as one would drop that change.
214− fn join(&mut self, from: usize) -> bool {
215− let Some(first) = self.undo.get(from) else { return false };
216− let description = first.description.clone();
217− let mut changes: Vec<Change> = Vec::new();
218− for c in self.undo[from..].iter().flat_map(|e| &e.changes) {
219− match changes.iter_mut().find(|x| x.path == c.path) {
220− Some(x) if x.after == c.before => x.after = c.after.clone(),
221− Some(_) => return false,
222− None => changes.push(c.clone()),
223− }
224− }
225− changes.retain(|c| c.before != c.after);
226− self.undo.truncate(from);
227− if !changes.is_empty() {
228− self.undo.push(Inverse { changes, description });
229− }
230− true
231− }
232−
233− /// Undo the last entry, or with `undo` false redo the last one undone
234− /// (§10.10), and say which by its description. `None` when there is
235− /// none. Refused — a file it touched changed since — it stays where
236− /// it was. Either way the blocks in transit are let go: the entries
237− /// they were remembered with may go.
238− pub fn step(&mut self, vault: &mut Vault, undo: bool) -> Option<std::io::Result<String>> {
239− self.transit.clear();
240− let (from, to) = if undo { (&mut self.undo, &mut self.redo) } else { (&mut self.redo, &mut self.undo) };
241− let inv = from.pop()?;
242− let res = if undo { inv.undo(vault) } else { inv.redo(vault) };
243− Some(match res {
244− Ok(()) => {
245− let done = inv.description.clone();
246− to.push(inv);
247− Ok(done)
248− }
249− Err(e) => {
250− from.push(inv);
251− Err(e)
252− }
253− })
254− }
255−}
256−
257−/// Whether the block whose file is `path` is embedded now; `None` where
258−/// the file is gone.
259−fn embedded(vault: &Vault, path: &str) -> Option<bool> {
260− let tree = &vault.tree;
261− let f = vault.file_index(path)?;
262− let (_, id) = tree.blocks.iter().find(|(r, _)| r.0 == f)?;
263− Some(tree.embed_of(id).is_some())
264−}
265−
266113fn today() -> String {
267114 jiff::Zoned::now().date().to_string()
268115}
@@ -289,6 +136,13 @@ fn capture_inner(
289136 task: bool,
290137 target: Option<NRef>,
291138) -> std::io::Result<NRef> {
139+ let dest = match target {
140+ Some(t) => t,
141+ None => {
142+ let inbox = find_or_create_inbox(vault)?;
143+ find_or_create_day(vault, inbox)?
144+ }
145+ };
292146 // The first line is the title; further lines are the item's body, so a
293147 // captured document (`capture < file.md`) nests under the item instead
294148 // of injecting structure beside it (§4.1): its headings are pushed below
@@ -300,17 +154,14 @@ fn capture_inner(
300154 } else {
301155 first.trim().to_string()
302156 };
303− // refused before the inbox or its day is created
304− let mut line = item_line(&title)?;
305− let dest = match target {
306− Some(t) => write_target(&vault.tree, t)?,
307− None => {
308− let inbox = find_or_create_inbox(vault)?;
309− find_or_create_day(vault, inbox)?
310− }
311− };
157+ let mut line = format!("- {}", title);
312158 if !rest.trim().is_empty() {
313− let item_indent = child_indent(&vault.tree, dest);
159+ let dn = vault.tree.node(dest);
160+ let item_indent = if dn.kind == Kind::Item {
161+ vault.tree.indent(dest) + 2
162+ } else {
163+ vault.tree.indent(dest)
164+ };
314165 let level = vault.tree.level(dest) as isize;
315166 let body = shift_lines(rest.trim_end(), level, item_indent as isize + 2);
316167 line.push('\n');
@@ -337,13 +188,9 @@ fn find_or_create_day(vault: &mut Vault, inbox: NRef) -> std::io::Result<NRef> {
337188 return Ok(c);
338189 }
339190 }
340− // `## <today>` as the last child of Inbox (§7), at its child indent: an
341− // Inbox respelled as an item is still the inbox (§3.1); no deeper than
342− // a day before it as written, which would take it as its child
343− let last = vault.tree.raw_children(inbox).last().copied();
191+ // `## <today>` as the last child of Inbox (§7)
344192 let level = vault.tree.level(inbox) + 1;
345− let level = written_level(&vault.tree, last).map_or(level, |w| level.min(w));
346− let indent = child_indent(&vault.tree, inbox);
193+ let indent = vault.tree.indent(inbox);
347194 let heading = format!(
348195 "{}{} {}",
349196 " ".repeat(indent),
@@ -352,9 +199,10 @@ fn find_or_create_day(vault: &mut Vault, inbox: NRef) -> std::io::Result<NRef> {
352199 );
353200 let inbox_key = vault.key_of(inbox);
354201 append_structural_line(vault, inbox, &heading)?;
202+ vault.reload()?;
355203 let inbox = vault
356204 .find_by_key(&inbox_key)
357− .ok_or_else(|| io_err("inbox lost after the write"))?;
205+ .ok_or_else(|| io_err("inbox lost after reload"))?;
358206 for c in vault.tree.resolved_children(inbox) {
359207 if vault.tree.node(c).title == day {
360208 return Ok(c);
@@ -434,22 +282,20 @@ fn append_child_line(
434282 line: &str,
435283 section_ok: bool,
436284) -> std::io::Result<NRef> {
437− // under the node an embed stands for: an embed line has no children
438− // in its file (§6.2)
439− let parent = write_target(&vault.tree, parent)?;
440285 let parent_key = vault.key_of(parent);
441286 let node = vault.tree.node(parent);
442287 let file = parent.0;
443288 // Items under a section sit at the section's own indent; items under an
444289 // item nest one level deeper (§3.1, §4.10).
445− let indent = child_indent(&vault.tree, parent);
290+ let indent = if node.kind == Kind::Item {
291+ vault.tree.indent(parent) + 2
292+ } else {
293+ vault.tree.indent(parent)
294+ };
446295 let text = vault.tree.files[file].text.clone();
447296 let kids = vault.tree.raw_children(parent);
448297 let is_section = |r: NRef| vault.tree.node(r).kind == Kind::Section;
449298 let first_section = kids.iter().position(|&c| is_section(c));
450− // §4.2 puts one blank line between a node's body and its first child:
451− // an item with no body has its first child right under its title
452− let bare_item = node.kind == Kind::Item && node.body_lines(&text).is_empty();
453299 let index;
454300 match (first_section, section_ok) {
455301 (Some(_), true) => {
@@ -461,12 +307,10 @@ fn append_child_line(
461307 *kids.iter().rev().find(|&&c| is_section(c)).unwrap()
462308 };
463309 let title = line.trim_start().strip_prefix("- ").unwrap_or(line.trim_start());
464− // at the last section's written indent, which its parent
465− // reaches, and written level, which keeps it beside it
466310 let heading = format!(
467311 "{}{} {}",
468− " ".repeat(vault.tree.node(last).indent),
469− "#".repeat(written_level(&vault.tree, Some(last)).unwrap_or(1)),
312+ " ".repeat(vault.tree.indent(last)),
313+ "#".repeat(vault.tree.level(last)),
470314 title
471315 );
472316 let pos = trimmed_end(&text, node.span);
@@ -481,7 +325,7 @@ fn append_child_line(
481325 pos -= 1;
482326 }
483327 let prev_is_item = fs > 0 && !is_section(kids[fs - 1]);
484− let sep = if prev_is_item || (fs == 0 && bare_item) { "" } else { "\n" };
328+ let sep = if prev_is_item { "" } else { "\n" };
485329 vault.write_span(
486330 file,
487331 Span { start: pos, end: pos },
@@ -493,16 +337,17 @@ fn append_child_line(
493337 let line_indented = format!("{}{}", " ".repeat(indent), line);
494338 let pos = trimmed_end(&text, node.span);
495339 // blank line before unless the previous sibling is also an item
496− // (tight list), or there is none under a bare item
497− let prev_is_item = kids.last().map(|&c| !is_section(c)).unwrap_or(bare_item);
340+ // (tight list)
341+ let prev_is_item = kids.last().map(|&c| !is_section(c)).unwrap_or(false);
498342 let sep = if prev_is_item { "\n" } else { "\n\n" };
499343 insert_at(vault, file, pos, sep, &line_indented)?;
500344 index = kids.len();
501345 }
502346 }
347+ vault.reload()?;
503348 let parent = vault
504349 .find_by_key(&parent_key)
505− .ok_or_else(|| io_err("parent lost after the write"))?;
350+ .ok_or_else(|| io_err("parent lost after reload"))?;
506351 vault
507352 .tree
508353 .resolved_children(parent)
@@ -527,7 +372,7 @@ pub fn toggle_task(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
527372
528373/// Toggle task-ness itself (§10.3 `t`).
529374pub fn toggle_taskness(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
530− let r = write_target(&vault.tree, r)?;
375+ let r = vault.tree.resolved_child(r);
531376 let state = match vault.tree.node(r).task {
532377 Some(_) => None,
533378 None => Some(TaskState::Open),
@@ -542,11 +387,8 @@ pub fn set_task(vault: &mut Vault, r: NRef, state: Option<TaskState>) -> std::io
542387 let n = vault.tree.node(r);
543388 let is_block = n.is_block();
544389 let file = r.0;
545− // a setext title has no marker to put the checkbox after: it is
546− // rewritten as an ATX heading, underline and all (§4.2)
547− let (ts, line) = setext_as_atx(&vault.tree, r).unwrap_or_else(|| {
548− (n.title_span, n.title_span.text(&vault.tree.files[file].text).to_string())
549− });
390+ let ts = n.title_span;
391+ let line = ts.text(&vault.tree.files[file].text).to_string();
550392 let mark = |st: TaskState| match st {
551393 TaskState::Open => "[ ]",
552394 TaskState::Done => "[x]", // also rewrites `[X]` / `[-]`
@@ -565,9 +407,7 @@ pub fn set_task(vault: &mut Vault, r: NRef, state: Option<TaskState>) -> std::io
565407 },
566408 (None, None) => line.clone(),
567409 };
568− // `- [ ] ---` unchecked is `- ---`, a thematic break (§4.4)
569− not_a_break(&new_line)?;
570− if new_line != ts.text(&vault.tree.files[file].text) {
410+ if new_line != line {
571411 vault.write_span(file, ts, &new_line)?;
572412 }
573413 if is_block {
@@ -600,26 +440,6 @@ fn marker_end(line: &str) -> Option<usize> {
600440 }
601441}
602442
603−/// A setext section's title (§4.2: read, converted on write) as an ATX
604−/// heading at its written level and indent, with the span it replaces: the
605−/// title line and its underline, the line after it. `None` for any other
606−/// node. The written level keeps what nests under the heading unchanged.
607−fn setext_as_atx(tree: &crate::tree::Tree, r: NRef) -> Option<(Span, String)> {
608− let n = tree.node(r);
609− let text = tree.text_of(r);
610− let line = n.title_span.text(text);
611− let t = line.trim_start();
612− // every other section's title line is an ATX heading (or a heading embed)
613− if n.kind != Kind::Section || atx_hashes(t).is_some() {
614− return None;
615− }
616− let under = text[n.title_span.end..].find('\n').map_or(text.len(), |i| n.title_span.end + i + 1);
617− let end = text[under..].find('\n').map_or(text.len(), |i| under + i);
618− let indent = &line[..line.len() - t.len()];
619− let atx = format!("{}{} {}", indent, "#".repeat(n.level.unwrap_or(1)), n.title);
620− Some((Span { start: n.title_span.start, end }, atx))
621−}
622−
623443/// Byte range of the checkbox right after the marker (`[ ]`, `[x]`, `[X]`,
624444/// `[-]`) plus the space after it (§4.2). Checkbox-like text later in the
625445/// title is title text.
@@ -648,13 +468,17 @@ pub fn set_frontmatter_key(
648468 value: Option<&str>,
649469) -> std::io::Result<()> {
650470 let f = vault.tree.files[file].clone();
651− let block = file_block(&f).cloned();
471+ let root_node = f.nodes[f.root_node].children.get(0).copied();
472+ let block = root_node.and_then(|rn| f.nodes[rn].block.clone());
652473 let (mut raw, fm_span) = match &block {
653474 Some(b) => (b.frontmatter_raw.clone(), b.frontmatter_span),
654475 None => (String::new(), None),
655476 };
656477 // rewrite the line for `key` inside raw
657478 let mut lines: Vec<String> = raw.split_inclusive('\n').map(|s| s.to_string()).collect();
479+ if !lines.is_empty() && !lines.last().unwrap().ends_with('\n') {
480+ // keep as is; we append a newline when needed
481+ }
658482 let key_prefix = format!("{}:", key);
659483 let mut found = false;
660484 for l in lines.iter_mut() {
@@ -701,18 +525,14 @@ pub fn set_frontmatter_key(
701525 vault.write_file_text(file, &new_text)
702526}
703527
704−/// The block whose frontmatter a file holds: a block file's root, its
705−/// first node (§4.9); root.md's, the implicit Root, whose frontmatter holds
706−/// vault-level properties.
707−fn file_block(f: &crate::parse::ParsedFile) -> Option<&crate::parse::Block> {
708− let root = &f.nodes[f.root_node];
709− root.block.as_ref().or_else(|| f.nodes[*root.children.first()?].block.as_ref())
710−}
711−
712528/// All top-level keys of a block's frontmatter, for the property editor.
713529pub fn frontmatter_lines(vault: &Vault, file: usize) -> Vec<(String, String, bool)> {
714530 // (key, value, editable) — unknown-structure lines are read-only (§10.6)
715− let Some(b) = file_block(&vault.tree.files[file]) else {
531+ let f = &vault.tree.files[file];
532+ let Some(&rn) = f.nodes[f.root_node].children.first() else {
533+ return Vec::new();
534+ };
535+ let Some(b) = &f.nodes[rn].block else {
716536 return Vec::new();
717537 };
718538 let mut out = Vec::new();
@@ -735,54 +555,31 @@ pub fn frontmatter_lines(vault: &Vault, file: usize) -> Vec<(String, String, boo
735555
736556/// Delete a subtree: its span leaves its file; block files go to trash (§11.5),
737557/// including blocks nested anywhere under it, so none is left orphaned.
738−/// Returns how many nodes went, the node itself included, for the TUI to
739−/// say (§10.1).
740−pub fn delete_subtree(vault: &mut Vault, r: NRef) -> std::io::Result<usize> {
741− // a node's conflict copies go with it, or the first would pair with
742− // the node before it (§12.5); a copy goes alone, as keeping ours
743− // sends it
744− let copies: Vec<Id> = conflict_copies(&vault.tree, r)
745− .into_iter()
746− .filter_map(|c| vault.tree.node(c).block.as_ref()?.id.clone())
747− .collect();
748− let mut count = delete_one(vault, r)?;
749− for id in copies {
750− if let Some(c) = vault.tree.block_by_id(&id) {
751− count += delete_one(vault, c)?;
752− }
753− }
754− Ok(count)
755−}
756−
757−/// `delete_subtree` for `r` alone.
758−fn delete_one(vault: &mut Vault, r: NRef) -> std::io::Result<usize> {
558+pub fn delete_subtree(vault: &mut Vault, r: NRef) -> std::io::Result<String> {
759559 let n = vault.tree.node(r);
760560 if n.kind == Kind::Root {
761561 return Err(io_err("cannot delete the root"));
762562 }
763− let mut count = 0;
764− vault.tree.walk(vault.tree.resolved_child(r), &mut |_, _| count += 1);
765563 if n.is_block() || n.is_embed() {
766564 let target = if n.is_embed() { vault.tree.resolved_child(r) } else { r };
767565 if target == r && n.is_embed() {
768− // broken embed: just the line. So for a second embed of a
769− // block, which renders as broken (§6.2): the block, and the
770− // embed it is stitched in at, are not this line's
771− let span = embed_line_span(&vault.tree, r);
772− remove_span(vault, r.0, span)?;
773− return Ok(1);
566+ // broken embed: just the line
567+ let span = n.span;
568+ remove_span_with_separator(vault, r.0, span)?;
569+ return Ok("broken embed removed".into());
774570 }
775− // the block itself first: it leaves by its own embed
776571 let ids = nested_block_ids(vault, target);
777− if let Some((own, nested)) = ids.split_first() {
778− trash_block(vault, own)?;
779− trash_nested(vault, nested)?;
572+ let path = vault.tree.node(target).block.as_ref().unwrap().path.clone();
573+ for id in ids {
574+ trash_block(vault, &id)?;
780575 }
781− return Ok(count);
576+ return Ok(format!("block {:?} trashed", path));
782577 }
783578 let ids = plain_remove(vault, r)?;
784− trash_nested(vault, &ids)?;
785− Ok(count)
579+ for id in ids {
580+ trash_block(vault, &id)?;
581+ }
582+ Ok("subtree trashed".into())
786583}
787584
788585/// Ids of every block in the resolved subtree of `r` (pre-order, `r` itself
@@ -805,16 +602,22 @@ fn plain_remove(vault: &mut Vault, r: NRef) -> std::io::Result<Vec<Id>> {
805602 let name = format!("{}.md", slug(&vault.tree.node(r).title));
806603 vault.trash_text(&name, &text)?;
807604 let span = vault.tree.node(r).span;
808− remove_span(vault, r.0, span)?;
605+ remove_span_with_separator(vault, r.0, span)?;
809606 Ok(ids)
810607}
811608
812−/// Remove the embed line a block is stitched in at (if it is still
813−/// anywhere) and move its file to the trash.
609+/// Remove a block's embed line (if it is still anywhere) and move its file to
610+/// the trash.
814611fn trash_block(vault: &mut Vault, id: &Id) -> std::io::Result<()> {
815− if let Some(e) = vault.tree.embed_of(id) {
816− let span = embed_line_span(&vault.tree, e);
817− remove_span(vault, e.0, span)?;
612+ let embed = vault.tree.files.iter().enumerate().find_map(|(fi, f)| {
613+ f.nodes
614+ .iter()
615+ .position(|nd| nd.embed.as_ref() == Some(id))
616+ .map(|ni| (fi, ni))
617+ });
618+ if let Some(e) = embed {
619+ let span = vault.tree.node(e).span;
620+ remove_span_with_separator(vault, e.0, span)?;
818621 }
819622 if let Some(b) = vault.tree.block_by_id(id) {
820623 vault.trash_file(b.0)?;
@@ -822,36 +625,6 @@ fn trash_block(vault: &mut Vault, id: &Id) -> std::io::Result<()> {
822625 Ok(())
823626}
824627
825−/// Move the files of blocks nested in a removed subtree to the trash. Their
826−/// embeds went with the lines and files that held them: an embed of one
827−/// still elsewhere is a second embed, a line of its own (§6.2), and stays.
828−fn trash_nested(vault: &mut Vault, ids: &[Id]) -> std::io::Result<()> {
829− for id in ids {
830− if let Some(b) = vault.tree.block_by_id(id) {
831− vault.trash_file(b.0)?;
832− }
833− }
834− Ok(())
835−}
836−
837−/// The part of an embed's span that is the embed itself: its line and the
838−/// blank lines after it. Lines nested under an embed are a diagnostic
839−/// (§4.7) that the reading pane shows in the parent, so they stay there
840−/// when the embed goes: nothing else holds a copy of them (§11.5).
841−fn embed_line_span(tree: &crate::tree::Tree, e: NRef) -> Span {
842− let n = tree.node(e);
843− let text = tree.text_of(e);
844− let end = n.span.end.min(text.len());
845− let mut at = text[n.title_span.end..end].find('\n').map_or(end, |i| n.title_span.end + i + 1);
846− for l in text[at..end].split_inclusive('\n') {
847− if !l.trim().is_empty() {
848− break;
849− }
850− at += l.len();
851− }
852− Span { start: n.span.start, end: at }
853−}
854−
855628/// The byte range to cut when removing `span`: the node plus whatever blank
856629/// separator would otherwise be doubled or left dangling. A blank line that
857630/// separated the node from what follows stays when nothing blank precedes
@@ -885,12 +658,17 @@ fn after_removal(pos: usize, start: usize, end: usize) -> usize {
885658}
886659
887660/// Remove a span plus one adjacent blank separator line, then re-parse just
888−/// that file, so other files' node refs stay valid.
889−fn remove_span(vault: &mut Vault, file: usize, span: Span) -> std::io::Result<()> {
661+/// that file (no full reload, so other files' node refs stay valid).
662+fn remove_span_no_reload(vault: &mut Vault, file: usize, span: Span) -> std::io::Result<()> {
890663 let (start, end) = removal_range(&vault.tree.files[file].text, span);
891664 vault.write_span(file, Span { start, end }, "")
892665}
893666
667+/// Remove a span plus one adjacent blank separator line.
668+fn remove_span_with_separator(vault: &mut Vault, file: usize, span: Span) -> std::io::Result<()> {
669+ remove_span_no_reload(vault, file, span)
670+}
671+
894672/// Yank: render the subtree resolved (the register's text) (§10.3).
895673pub fn yank(vault: &Vault, r: NRef) -> String {
896674 render(&vault.tree, r, 1, true)
@@ -898,7 +676,7 @@ pub fn yank(vault: &Vault, r: NRef) -> String {
898676
899677/// Paste rendered text after/before a node as siblings (§10.3 `p`/`P`),
900678/// clamped by the ordering rule (§3.1).
901−pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::Result<Placed> {
679+pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::Result<bool> {
902680 if vault.tree.node(at).kind == Kind::Root {
903681 return Err(io_err("cannot paste beside the root"));
904682 }
@@ -909,8 +687,7 @@ pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::R
909687 };
910688 let pos = vault.tree.raw_children(parent).iter().position(|&k| k == at).unwrap_or(0);
911689 let shifted = shift_document(text, vault.tree.level(parent), child_indent(&vault.tree, parent));
912− let (nodes, clamped) = place(vault, &[], parent, pos + after as usize, &shifted)?;
913− Placed::of(&nodes, 0, clamped)
690+ place(vault, None, parent, pos + after as usize, &shifted)
914691}
915692
916693/// Parse a standalone document and re-emit it as children of a node whose
@@ -919,9 +696,19 @@ pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::R
919696/// in a section of level `parent_level`. Everything inside keeps its position
920697/// relative to its top-level node.
921698pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String {
922− let block = crate::parse::Block::new("clip.md", None, None);
923− let pf = crate::parse::parse_file("clip.md", text, Some(block));
924− let tree = crate::tree::Tree::new(vec![pf]);
699+ let block = crate::parse::Block {
700+ id: None,
701+ path: "clip.md".into(),
702+ props: Default::default(),
703+ frontmatter_raw: String::new(),
704+ frontmatter_span: None,
705+ };
706+ let pf = crate::parse::parse_file("clip.md", text, 0, Some(block));
707+ let tree = crate::tree::Tree {
708+ files: vec![pf],
709+ root: (0, 0),
710+ blocks: vec![],
711+ };
925712 let mut out = String::new();
926713 let kids = tree.resolved_children(tree.root);
927714 for (i, k) in kids.iter().enumerate() {
@@ -933,9 +720,11 @@ pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String
933720 let t = first.trim_start();
934721 // render(_, 1) writes a section root at level 1 and an item root in
935722 // a section of level 1 (its nested sections at 2)
936− let ld = match atx_hashes(t) {
937− Some(cur_level) => parent_level as isize + 1 - cur_level as isize,
938− None => parent_level as isize - 1,
723+ let ld = if t.starts_with('#') {
724+ let cur_level = t.chars().take_while(|&c| c == '#').count();
725+ parent_level as isize + 1 - cur_level as isize
726+ } else {
727+ parent_level as isize - 1
939728 };
940729 let cur_indent = first.len() - t.len();
941730 let id = indent as isize - cur_indent as isize;
@@ -951,33 +740,26 @@ pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String
951740}
952741
953742/// Shift every line of a rendered document by (level_delta, indent_delta).
954−/// Fenced code is shifted too, so it stays in its node's region (§3.3), but
955−/// never re-levelled.
956743fn shift_lines(raw: &str, level_delta: isize, indent_delta: isize) -> String {
957744 let mut out = String::new();
958745 let mut fence: Option<(char, usize)> = None;
959746 for line in raw.split_inclusive('\n') {
960747 let l = line.strip_suffix('\n').unwrap_or(line);
961748 let nl = if line.ends_with('\n') { "\n" } else { "" };
962− let in_code = crate::parse::fence_transition(l, &mut fence) || fence.is_some();
963− let trimmed = l.trim_start();
964− if trimmed.is_empty() {
749+ if fence_transition(l, &mut fence) || fence.is_some() {
750+ out.push_str(l);
965751 out.push_str(nl);
966752 continue;
967753 }
968− if in_code {
969− // only the leading spaces change: a tab, or any indentation
970− // inside the code, stays as written
971− let code = l.trim_start_matches(' ');
972− let cur_indent = l.len() - code.len();
973− out.push_str(&" ".repeat((cur_indent as isize + indent_delta).max(0) as usize));
974− out.push_str(code);
754+ let trimmed = l.trim_start();
755+ if trimmed.is_empty() {
975756 out.push_str(nl);
976757 continue;
977758 }
978759 let cur_indent = l.len() - trimmed.len();
979760 let new_indent = (cur_indent as isize + indent_delta).max(0) as usize;
980− if let Some(hashes) = atx_hashes(trimmed) {
761+ if trimmed.starts_with('#') {
762+ let hashes = trimmed.chars().take_while(|&c| c == '#').count();
981763 let new_level = (hashes as isize + level_delta).max(1) as usize;
982764 out.push_str(&" ".repeat(new_indent));
983765 out.push_str(&"#".repeat(new_level));
@@ -992,17 +774,35 @@ fn shift_lines(raw: &str, level_delta: isize, indent_delta: isize) -> String {
992774 out
993775}
994776
995−/// The level of an ATX heading line, given without its indent: `#`+ then a
996−/// space or the end of the line (§4.2). `#tag` is text, as the parser reads it.
997−fn atx_hashes(trimmed: &str) -> Option<usize> {
998− let hashes = trimmed.bytes().take_while(|&b| b == b'#').count();
999− let after = &trimmed[hashes..];
1000− (hashes > 0 && (after.is_empty() || after.starts_with(' '))).then_some(hashes)
777+fn fence_transition(raw: &str, open: &mut Option<(char, usize)>) -> bool {
778+ let t = raw.trim_start();
779+ let first = match t.chars().next() {
780+ Some(c) if c == '`' || c == '~' => c,
781+ _ => return false,
782+ };
783+ let count = t.chars().take_while(|&c| c == first).count();
784+ if count < 3 {
785+ return false;
786+ }
787+ match open {
788+ None => {
789+ *open = Some((first, count));
790+ true
791+ }
792+ Some((c, n)) => {
793+ if *c == first && count >= *n {
794+ *open = None;
795+ true
796+ } else {
797+ false
798+ }
799+ }
800+ }
1001801}
1002802
1003803/// Refile: move a subtree under a new parent as its last child (§6.5).
1004−pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<Placed> {
1005− let dest = write_target(&vault.tree, dest)?;
804+pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<bool> {
805+ let dest = vault.tree.resolved_child(dest);
1006806 let n = vault.tree.node(r);
1007807 // guard: cannot refile into own subtree — ancestry followed through
1008808 // embeds, so a destination inside a nested block counts too
@@ -1016,7 +816,7 @@ pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<Placed>
1016816 }
1017817 guard += 1;
1018818 if guard > 10_000 {
1019− break; // embed cycle: `check` reports it (§6.2)
819+ break; // embed cycle: a diagnostic elsewhere
1020820 }
1021821 anc = resolved_parent(&vault.tree, a);
1022822 }
@@ -1026,61 +826,9 @@ pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<Placed>
1026826 if vault.tree.node(moving).is_block() {
1027827 return Err(io_err("block has no embed"));
1028828 }
1029− // with its conflict copies, or with the node a copy is of (§12.5)
1030− let run = pair_run(&vault.tree, moving);
1031− if run.iter().any(|&m| m != moving && within(&vault.tree, dest, m)) {
1032− return Err(io_err(PAIR_INTO_ITSELF));
1033− }
1034− let doc = run_doc(&vault.tree, &run, dest);
1035− let (nodes, clamped) = place(vault, &run, dest, usize::MAX, &doc)?;
1036− Placed::of(&nodes, run.iter().position(|&m| m == moving).unwrap_or(0), clamped)
1037−}
1038−
1039−/// A node and its conflict copy move as one (§12.5): neither into the
1040−/// other.
1041−const PAIR_INTO_ITSELF: &str = "cannot move a node and its conflict copy into either of them";
1042−
1043−/// `run` (a node and its conflict copies, §12.5, as they stand in their
1044−/// parent's file) as a document of children of `parent`: each in its
1045−/// on-disk spelling, shifted there, a blank line before a heading or
1046−/// where there was one.
1047−fn run_doc(tree: &crate::tree::Tree, run: &[NRef], parent: NRef) -> String {
1048− let (level, indent) = (tree.level(parent), child_indent(tree, parent));
1049− let mut out = String::new();
1050− for (i, &m) in run.iter().enumerate() {
1051− if i > 0 {
1052− let (sp, text) = (tree.node(run[i - 1]).span, tree.text_of(run[i - 1]));
1053− if tree.node(m).kind == Kind::Section || text[sp.start..sp.end.min(text.len())].ends_with("\n\n") {
1054− out.push('\n');
1055− }
1056− }
1057− out.push_str(&shift_document(&render(tree, m, 1, false), level, indent));
1058− }
1059− out
1060−}
1061−
1062−/// Where a verb put the node it moved or made, by position — never found
1063−/// again by its title (§3.4): the node as the outline shows it (a block,
1064−/// not its embed), and whether the ordering rule (§3.1), not the verb,
1065−/// chose the place.
1066−#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1067−pub struct Placed {
1068− pub node: NRef,
1069− pub clamped: bool,
1070−}
1071−
1072−impl Placed {
1073− /// A verb that left `r` where it was.
1074− fn stayed(tree: &crate::tree::Tree, r: NRef) -> Placed {
1075− Placed { node: tree.resolved_child(r), clamped: false }
1076− }
1077−
1078− /// The `i`-th of the nodes `place` put (a run's, in order: the one a
1079− /// verb moved among its conflict copies).
1080− fn of(nodes: &[NRef], i: usize, clamped: bool) -> std::io::Result<Placed> {
1081− let node = *nodes.get(i).ok_or_else(|| io_err("the placed node is not where it was written"))?;
1082− Ok(Placed { node, clamped })
1083− }
829+ let rendered = render(&vault.tree, moving, 1, false);
830+ let shifted = shift_document(&rendered, vault.tree.level(dest), child_indent(&vault.tree, dest));
831+ place(vault, Some(moving), dest, usize::MAX, &shifted)
1084832}
1085833
1086834/// Where a dragged node is dropped (§10.3): before a node, as its sibling,
@@ -1092,11 +840,11 @@ pub enum Drop {
1092840}
1093841
1094842/// Move `r` relative to `target` (§10.3 drag and drop). Refuses a move into
1095−/// `r`'s own subtree.
1096−pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::io::Result<Placed> {
843+/// `r`'s own subtree. Returns whether the ordering rule changed the place.
844+pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::io::Result<bool> {
1097845 let target = vault.tree.resolved_child(target);
1098846 if target == vault.tree.resolved_child(r) {
1099− return Ok(Placed::stayed(&vault.tree, r));
847+ return Ok(false);
1100848 }
1101849 match drop {
1102850 Drop::Into => refile(vault, r, target),
@@ -1110,27 +858,11 @@ pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::i
1110858 return Err(io_err("cannot move a node into itself"));
1111859 }
1112860 let moving = stand_in(&vault.tree, r);
1113− // with its conflict copies, or with the node a copy is of
1114− // (§12.5); before either of them, it stays where it is
1115− let run = pair_run(&vault.tree, moving);
1116− if run.contains(&t) {
1117− return Ok(Placed::stayed(&vault.tree, r));
1118− }
1119− if run.iter().any(|&m| m != moving && within(&vault.tree, parent, m)) {
1120− return Err(io_err(PAIR_INTO_ITSELF));
1121− }
1122− // the target's index among the children `place` sees: without
1123− // the moving nodes, which may come before it
1124− let idx = vault
1125− .tree
1126− .raw_children(parent)
1127− .into_iter()
1128− .filter(|k| !run.contains(k))
1129− .position(|k| k == t)
1130− .unwrap_or(0);
1131− let doc = run_doc(&vault.tree, &run, parent);
1132− let (nodes, clamped) = place(vault, &run, parent, idx, &doc)?;
1133− Placed::of(&nodes, run.iter().position(|&m| m == moving).unwrap_or(0), clamped)
861+ let idx = vault.tree.raw_children(parent).iter().position(|&k| k == t).unwrap_or(0);
862+ let rendered = render(&vault.tree, moving, 1, false);
863+ let shifted =
864+ shift_document(&rendered, vault.tree.level(parent), child_indent(&vault.tree, parent));
865+ place(vault, Some(moving), parent, idx, &shifted)
1134866 }
1135867 }
1136868}
@@ -1168,29 +900,12 @@ fn stand_in(tree: &crate::tree::Tree, r: NRef) -> NRef {
1168900 r
1169901}
1170902
1171−/// The node a verb writes into at `r`: a block's own embed is the block
1172−/// (§4.7). An embed that resolves to nothing, broken or a second embed of
1173−/// a block (§6.2), is only a line of the file that holds it: it has no
1174−/// checkbox, properties or children of its own, so the verb is refused
1175−/// rather than writing them into that file.
1176−fn write_target(tree: &crate::tree::Tree, r: NRef) -> std::io::Result<NRef> {
1177− let t = tree.resolved_child(r);
1178− if tree.node(t).is_embed() {
1179− return Err(io_err("a broken or duplicate embed has no node to write to"));
1180− }
1181− Ok(t)
1182−}
1183−
1184903/// Indent of a child of `parent` (§3.1): items nest under items, everything
1185−/// under a section stays at the section's own indent. Measured from the
1186−/// parent's written indent, not its derived one: nesting written with tabs
1187−/// or 4 spaces is read (§4.2), and a line at the derived child indent could
1188−/// fall short of such a parent and parse as its sibling.
904+/// under a section stays at the section's own indent.
1189905fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
1190− let n = tree.node(parent);
1191− match n.kind {
1192− Kind::Item => n.indent + 2,
1193− Kind::Section => n.indent,
906+ match tree.node(parent).kind {
907+ Kind::Item => tree.indent(parent) + 2,
908+ Kind::Section => tree.indent(parent),
1194909 Kind::Root => 0,
1195910 }
1196911}
@@ -1198,7 +913,7 @@ fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
1198913/// Spellings of a document's top-level nodes, in order. A heading embed is
1199914/// a section, a bare embed an item (§4.7).
1200915fn top_kinds(doc: &str) -> Vec<Kind> {
1201− let pf = crate::parse::parse_file("clip.md", doc, None);
916+ let pf = crate::parse::parse_file("clip.md", doc, 0, None);
1202917 pf.nodes[pf.root_node]
1203918 .children
1204919 .iter()
@@ -1206,12 +921,6 @@ fn top_kinds(doc: &str) -> Vec<Kind> {
1206921 .collect()
1207922}
1208923
1209−/// The indent a document's first top-level node is written at.
1210−fn top_indent(doc: &str) -> Option<usize> {
1211− let pf = crate::parse::parse_file("clip.md", doc, None);
1212− pf.nodes[pf.root_node].children.first().map(|&c| pf.nodes[c].indent)
1213−}
1214−
1215924/// Clamp a wanted child index by the ordering rule `(text | item)*
1216925/// section*` (§3.1): items no later than the first section child, sections
1217926/// no earlier. `kids` are the parent's child nodes without the one moving.
@@ -1225,98 +934,41 @@ fn clamp_index(tree: &crate::tree::Tree, kids: &[NRef], want: usize, kinds: &[Ki
1225934 want.min(kids.len()).clamp(lo, hi)
1226935}
1227936
1228−/// The level of `k` as written, if it is a section. A written level is
1229−/// honoured (§4.7): a section written after it deeper than that is its
1230−/// child, and one written before it shallower than that takes it (and
1231−/// every section after it) as a child (§3.1). The level `k`'s position
1232−/// gives can be deeper: under an item, sections may be written shallower.
1233−fn written_level(tree: &crate::tree::Tree, k: Option<NRef>) -> Option<usize> {
1234− k.map(|k| tree.node(k)).filter(|n| n.kind == Kind::Section).map(|n| n.level.unwrap_or(1))
1235−}
1236−
1237−/// `doc` (shifted to a child position) with its top-level sections, and
1238−/// everything under them, re-levelled to stay siblings of the sections
1239−/// around them (`written_level`): no deeper than `prev`, the sibling
1240−/// section they are written after, and no shallower than `next`, the one
1241−/// they are written before.
1242−fn level_among(tree: &crate::tree::Tree, prev: Option<NRef>, next: Option<NRef>, doc: &str) -> String {
1243− let (most, least) = (written_level(tree, prev), written_level(tree, next));
1244− if most.is_none() && least.is_none() {
1245− return doc.to_string();
1246− }
1247− let pf = crate::parse::parse_file("clip.md", doc, None);
1248− let tops = &pf.nodes[pf.root_node].children;
1249− // a document's sections come after its items (§3.1)
1250− let Some(first) = tops.iter().map(|&c| &pf.nodes[c]).find(|n| n.kind == Kind::Section) else {
1251− return doc.to_string();
1252− };
1253− // indentation closes a section before its level does (§4.2): a sibling
1254− // written deeper than the sections, before them, or shallower, after
1255− // them, is no parent or child of theirs, whatever its level
1256− let most = most.filter(|_| prev.is_some_and(|p| tree.node(p).indent <= first.indent));
1257− let least = least.filter(|_| next.is_some_and(|k| tree.node(k).indent >= first.indent));
1258− let have = tops.last().and_then(|&c| pf.nodes[c].level).unwrap_or(1);
1259− let want = most.map_or(have, |m| have.min(m)).max(least.unwrap_or(1));
1260− if want == have {
1261− return doc.to_string();
1262− }
1263− let at = first.span.start;
1264− format!("{}{}", &doc[..at], shift_lines(&doc[at..], want as isize - have as isize, 0))
1265−}
1266−
1267937/// Write `doc` (already shifted to a child position of `parent`) as the
1268938/// `want`-th child node of `parent`, clamped by the ordering rule (§3.1).
1269−/// The `moving` nodes (siblings, in order: a node and its conflict copies,
1270−/// §12.5), have their own lines removed in the same edit: text children
1271−/// around them stay where they are. The insertion is written before any
1272−/// removal in another file, so a failure never loses text. Returns the
1273−/// document's top-level nodes where they are now, found by the position
1274−/// they were written at, and whether the ordering rule moved them from
1275−/// the wanted index.
939+/// With `moving`, that node's own lines are removed in the same edit: text
940+/// children around it stay where they are. The insertion is written before
941+/// any removal in another file, so a failure never loses text. Returns
942+/// whether the ordering rule moved it from the wanted index.
1276943fn place(
1277944 vault: &mut Vault,
1278− moving: &[NRef],
945+ moving: Option<NRef>,
1279946 parent: NRef,
1280947 want: usize,
1281948 doc: &str,
1282−) -> std::io::Result<(Vec<NRef>, bool)> {
949+) -> std::io::Result<bool> {
1283950 let tree = &vault.tree;
1284951 let pnode = tree.node(parent);
1285952 let kids: Vec<NRef> = tree
1286953 .raw_children(parent)
1287954 .into_iter()
1288− .filter(|k| !moving.contains(k))
955+ .filter(|&k| Some(k) != moving)
1289956 .collect();
1290957 let kinds = top_kinds(doc);
1291958 if kinds.is_empty() {
1292959 return Err(io_err("nothing to place"));
1293960 }
1294− let clamped = clamp_index(tree, &kids, want, &kinds);
1295− // nothing goes between a conflict copy and the node before it, which
1296− // it would pair with (§12.5): past the copies, unless the node they
1297− // follow is the one moving, back in its place
1298− let mut idx = clamped;
1299− let orig = tree.raw_children(parent);
1300− let back = |k: NRef| {
1301− let before = orig.iter().position(|&o| o == k).and_then(|i| i.checked_sub(1)).map(|i| orig[i]);
1302− before.is_some_and(|b| moving.contains(&b))
1303− };
1304− if !kids.get(idx).is_some_and(|&k| back(k)) {
1305− let copy_at = |i: usize| kids.get(i).is_some_and(|&k| is_copy(tree, k));
1306− while copy_at(idx) && clamp_index(tree, &kids, idx + 1, &kinds) == idx + 1 {
1307− idx += 1;
1308− }
1309− }
961+ let idx = clamp_index(tree, &kids, want, &kinds);
1310962 let first = kinds[0];
1311963 let last = *kinds.last().unwrap();
1312964 let file = parent.0;
1313965 let mut text = tree.files[file].text.clone();
1314− // the parent's content as it will be, without the moving nodes
966+ // the parent's content as it will be, without the moving node
1315967 let content: Vec<crate::parse::Content> = pnode
1316968 .content
1317969 .iter()
1318970 .copied()
1319− .filter(|c| !matches!(c, crate::parse::Content::Node(n) if moving.contains(&(file, *n))))
971+ .filter(|c| !matches!(c, crate::parse::Content::Node(n) if Some((file, *n)) == moving))
1320972 .collect();
1321973 // positions in the original text, then adjusted for a same-file removal
1322974 let next = kids.get(idx).copied();
@@ -1344,52 +996,17 @@ fn place(
1344996 Some(crate::parse::Content::Text(_)) => false,
1345997 None => pnode.kind == Kind::Item,
1346998 };
1347− // inserting before an item: after another item, the list is as tight as
1348− // the text there says (read below); starting the list, it is read in
1349− // the list as found, the moving nodes still in it: between `next` and
1350− // the item after it, or else between the last moving node and `next`
1351− // right after it. A list of one item is tight.
1352− let is_item = |c: Option<&crate::parse::Content>| {
1353− matches!(c, Some(crate::parse::Content::Node(k)) if tree.files[file].nodes[*k].kind == Kind::Item)
1354− };
1355− let next_at = next.and_then(|k| content.iter().position(|c| *c == crate::parse::Content::Node(k.1)));
1356− let starts_run = next_at.is_some_and(|i| i == 0 || !is_item(content.get(i - 1)));
1357− let found = &pnode.content;
1358− let found_at = next.and_then(|k| found.iter().position(|c| *c == crate::parse::Content::Node(k.1)));
1359− let no_blank_after = |k: NRef| {
1360− let sp = tree.node(k).span;
1361− !text[sp.start..sp.end.min(text.len())].ends_with("\n\n")
1362− };
1363− let run_tight = match (next, found_at) {
1364− (Some(k), Some(i)) if is_item(found.get(i + 1)) => no_blank_after(k),
1365− (Some(_), Some(i)) => match moving.last() {
1366− Some(&m) if i > 0 && m.0 == file && found[i - 1] == crate::parse::Content::Node(m.1) => {
1367− no_blank_after(m)
1368− }
1369− _ => true,
1370− },
1371− _ => true,
1372− };
1373999 let next_kind = next.map(|k| tree.node(k).kind);
1374− // the last first, so the spans before it stay where they are
1375− for &m in moving.iter().rev().filter(|m| m.0 == file) {
1376− let (start, end) = removal_range(&text, tree.node(m).span);
1000+ let removal = match moving {
1001+ Some(m) if m.0 == file => Some(removal_range(&text, tree.node(m).span)),
1002+ _ => None,
1003+ };
1004+ if let Some((start, end)) = removal {
13771005 text = format!("{}{}", &text[..start], &text[end..]);
13781006 pos = after_removal(pos, start, end);
13791007 }
1380− // before a sibling written deeper than the node (tab or 4-space
1381− // nesting is read, §4.2), the node goes at that sibling's indent: any
1382− // shallower, the sibling would parse as its child
1383− let doc = match (next.map(|k| tree.node(k).indent), top_indent(doc)) {
1384− (Some(want), Some(have)) if want > have => shift_lines(doc, 0, (want - have) as isize),
1385− _ => doc.to_string(),
1386− };
1387− let prev = idx.checked_sub(1).map(|i| kids[i]);
1388− let doc = level_among(tree, prev, next, &doc);
13891008 let body = doc.trim_end_matches('\n');
1390− let tops = top_starts(body);
1391− // the insertion, and where `body` starts in it
1392− let (insertion, at) = match next_kind {
1009+ let insertion = match next_kind {
13931010 Some(nk) => {
13941011 // before the next child node: keep a blank line before a
13951012 // heading and after anything that is not a tight item run
@@ -1398,10 +1015,8 @@ fn place(
13981015 } else {
13991016 ""
14001017 };
1401− let tight = last == Kind::Item
1402− && nk == Kind::Item
1403− && if starts_run { run_tight } else { !text[..pos].ends_with("\n\n") };
1404− (format!("{}{}\n{}", lead, body, if tight { "" } else { "\n" }), lead.len())
1018+ let tight = last == Kind::Item && nk == Kind::Item && !text[..pos].ends_with("\n\n");
1019+ format!("{}{}\n{}", lead, body, if tight { "" } else { "\n" })
14051020 }
14061021 None => {
14071022 while pos > 0 && text.as_bytes()[pos - 1] == b'\n' {
@@ -1415,37 +1030,23 @@ fn place(
14151030 "\n\n"
14161031 };
14171032 let nl = if text[pos..].starts_with('\n') { "" } else { "\n" };
1418− (format!("{}{}{}", sep, body, nl), sep.len())
1033+ format!("{}{}{}", sep, body, nl)
14191034 }
14201035 };
1421− let start = pos + at;
14221036 text.insert_str(pos, &insertion);
14231037 vault.write_file_text(file, &text)?;
1424− // the other file's node refs are untouched by the write above; each
1425− // removal re-parses that file, so the spans are read first
1426− let away: Vec<(usize, Span)> =
1427− moving.iter().filter(|m| m.0 != file).map(|&m| (m.0, vault.tree.node(m).span)).collect();
1428− for (f, span) in away.into_iter().rev() {
1429− remove_span(vault, f, span)?;
1038+ if let Some(m) = moving {
1039+ if m.0 != file {
1040+ // the other file's node refs are untouched by the write above
1041+ remove_span_no_reload(vault, m.0, vault.tree.node(m).span)?;
1042+ }
14301043 }
1431− // `file` is as written: each node starts where its line went in
1432− let f = &vault.tree.files[file];
1433− let placed = tops
1434− .iter()
1435− .filter_map(|&t| f.nodes.iter().position(|n| n.kind != Kind::Root && n.title_span.start == start + t))
1436− .map(|i| vault.tree.resolved_child((file, i)))
1437− .collect();
1438− Ok((placed, clamped != want.min(kids.len())))
1439−}
1440−
1441−/// Where each of a document's top-level nodes starts in it.
1442−fn top_starts(doc: &str) -> Vec<usize> {
1443− let pf = crate::parse::parse_file("clip.md", doc, None);
1444− pf.nodes[pf.root_node].children.iter().map(|&c| pf.nodes[c].title_span.start).collect()
1044+ vault.reload()?;
1045+ Ok(idx != want.min(kids.len()))
14451046}
14461047
14471048/// Archive: refile under the top-level `Archive` section (§6.5).
1448−pub fn archive(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1049+pub fn archive(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
14491050 let root = vault.tree.root;
14501051 let mut dest = None;
14511052 for c in vault.tree.resolved_children(root) {
@@ -1471,22 +1072,12 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
14711072 vault.tree.walk(target, &mut |t, r| {
14721073 let n = t.node(r);
14731074 if n.task == Some(TaskState::Done) {
1474− // any open task below keeps it, one in a conflict copy too,
1475− // which task counts leave out (§3.5)
1476− let mut open = false;
1477− t.walk(r, &mut |t, c| open |= t.node(c).task == Some(TaskState::Open));
1478− if !open {
1075+ let (open, _total) = t.task_counts(r);
1076+ if open == 0 {
14791077 to_clear.push(r);
14801078 }
14811079 }
14821080 });
1483− // a node's conflict copies go with it (§12.5): one whose copies are
1484− // not all cleared too stays, or they would pair with the node before
1485− let to_clear: Vec<NRef> = to_clear
1486− .iter()
1487− .copied()
1488− .filter(|&r| conflict_copies(&vault.tree, r).iter().all(|c| to_clear.contains(c)))
1489− .collect();
14901081 // skip nodes under another node being cleared
14911082 let tops: Vec<NRef> = to_clear
14921083 .iter()
@@ -1506,17 +1097,10 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
15061097 .collect();
15071098 let mut blocks: Vec<Id> = Vec::new();
15081099 let mut plain: Vec<NRef> = Vec::new();
1509− let mut nested: Vec<Id> = Vec::new();
15101100 for &r in &tops {
15111101 let n = vault.tree.node(r);
15121102 match n.block.as_ref().and_then(|b| b.id.clone()) {
1513− Some(id) => {
1514− // the block leaves by its own embed; blocks embedded in its
1515− // file go to the trash with it, as delete_subtree sends them
1516− let ids = nested_block_ids(vault, r);
1517− nested.extend(ids.into_iter().filter(|i| *i != id));
1518− blocks.push(id);
1519− }
1103+ Some(id) => blocks.push(id),
15201104 None => plain.push(r),
15211105 }
15221106 }
@@ -1525,14 +1109,13 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
15251109 let mut done = 0;
15261110 for r in plain {
15271111 let ids = plain_remove(vault, r)?;
1528− nested.extend(ids);
1112+ blocks.extend(ids);
15291113 done += 1;
15301114 }
15311115 let n_block_tops = tops.len() - done;
15321116 for id in blocks {
15331117 trash_block(vault, &id)?;
15341118 }
1535− trash_nested(vault, &nested)?;
15361119 Ok(done + n_block_tops)
15371120}
15381121
@@ -1542,7 +1125,13 @@ fn resolved_parent(tree: &crate::tree::Tree, r: NRef) -> Option<NRef> {
15421125 let p = tree.node(r).parent?;
15431126 let pr = (r.0, p);
15441127 if tree.node(pr).kind == Kind::Root && r.0 != tree.root.0 {
1545− tree.embed_of(tree.node(r).block.as_ref()?.id.as_ref()?)
1128+ let id = tree.node(r).block.as_ref()?.id.clone()?;
1129+ tree.files.iter().enumerate().find_map(|(fi, f)| {
1130+ f.nodes
1131+ .iter()
1132+ .position(|nd| nd.embed.as_ref() == Some(&id))
1133+ .map(|ni| (fi, ni))
1134+ })
15461135 } else if tree.node(pr).kind == Kind::Root {
15471136 None
15481137 } else {
@@ -1566,28 +1155,14 @@ pub fn make_block(vault: &mut Vault, r: NRef) -> std::io::Result<Id> {
15661155 file_text.push_str(&body);
15671156 let prefix = vault.unique_prefix(&id);
15681157 let fname = crate::ident::filename(&prefix, &slug(&n.title));
1569− // the embed cannot be written into a file changed on disk (§11.2), and
1570− // a block file without it would be embedded nowhere: check first
1571− let file = r.0;
1572− vault.check_unchanged(file)?;
1573− let full = vault.dir.join(&fname);
1574− crate::vault::atomic_write(&full, &file_text)?;
1158+ crate::vault::atomic_write(&vault.dir.join(&fname), &file_text)?;
15751159 // replace the node's span with an embed in the node's form (§6.1.3,
15761160 // §4.7), keeping the blank lines that separated it from what follows
1161+ let file = r.0;
15771162 let span = n.span;
15781163 let indent = " ".repeat(n.indent);
1579− // at its level, but kept a sibling of the sections around it as they
1580− // are written: no deeper than the one before it, which would take the
1581− // embed as its child, nor shallower than the one after it
1582− let kids = n.parent.map(|p| vault.tree.raw_children((file, p))).unwrap_or_default();
1583− let at = kids.iter().position(|&c| c == r);
1584− let prev = at.and_then(|i| i.checked_sub(1)).map(|i| kids[i]);
1585− let next = at.and_then(|i| kids.get(i + 1).copied());
15861164 let mut embed = match n.kind {
1587− Kind::Section => {
1588− let line = format!("{}{} ![[{}]]\n", indent, "#".repeat(vault.tree.level(r)), id);
1589− level_among(&vault.tree, prev, next, &line)
1590− }
1165+ Kind::Section => format!("{}{} ![[{}]]\n", indent, "#".repeat(vault.tree.level(r)), id),
15911166 _ => format!("{}![[{}]]\n", indent, id),
15921167 };
15931168 let old = span.text(&vault.tree.files[file].text);
@@ -1595,13 +1170,8 @@ pub fn make_block(vault: &mut Vault, r: NRef) -> std::io::Result<Id> {
15951170 for _ in 1..trailing {
15961171 embed.push('\n');
15971172 }
1598− if let Err(e) = vault.write_span(file, span, &embed) {
1599− // changed in between after all: nothing refers to the new file yet,
1600− // and the node is still in its parent
1601− let _ = std::fs::remove_file(&full);
1602− return Err(e);
1603− }
1604− vault.add_file(&fname, &file_text);
1173+ vault.write_span(file, span, &embed)?;
1174+ vault.reload()?;
16051175 Ok(id)
16061176}
16071177
@@ -1611,7 +1181,7 @@ pub fn set_property(vault: &mut Vault, r: NRef, key: &str, value: &str) -> std::
16111181 let target = if vault.tree.node(r).is_block() {
16121182 r
16131183 } else if vault.tree.node(r).is_embed() {
1614− write_target(&vault.tree, r)?
1184+ vault.tree.resolved_child(r)
16151185 } else {
16161186 // make_block reloads, so `r` is stale: find the new block by its id
16171187 let id = make_block(vault, r)?;
@@ -1629,9 +1199,6 @@ pub fn set_property(vault: &mut Vault, r: NRef, key: &str, value: &str) -> std::
16291199
16301200/// Move a node among its siblings (§10.3 `J`/`K`). A block moves by its
16311201/// embed line, so blocks and plain nodes swap freely in the parent file.
1632−/// A node and the conflict copies after it are one (§12.5): they move
1633−/// together, and a node moves past them together, so no copy is left
1634−/// after another node, which it would pair with.
16351202pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<()> {
16361203 // a block root stands in its parent file as its embed
16371204 let r = stand_in(&vault.tree, r);
@@ -1647,21 +1214,20 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
16471214 Some(p) => p,
16481215 None => return Ok(()),
16491216 };
1650− let run = |i: usize| paired_run(&vault.tree, &siblings, i);
1651− let (a, b) = run(pos);
1652− let other = if down {
1653− if b + 1 >= siblings.len() {
1654− return Ok(());
1655− }
1656− run(b + 1)
1217+ let swap_with = if down {
1218+ pos + 1
16571219 } else {
1658− match a.checked_sub(1) {
1659− Some(p) => run(p),
1220+ match pos.checked_sub(1) {
1221+ Some(p) => p,
16601222 None => return Ok(()),
16611223 }
16621224 };
1225+ if swap_with >= siblings.len() {
1226+ return Ok(());
1227+ }
1228+ let other = siblings[swap_with];
16631229 // the ordering rule (§3.1): an item never goes below a section sibling
1664− let (rk, ok) = (vault.tree.node(siblings[a]).kind, vault.tree.node(siblings[other.0]).kind);
1230+ let (rk, ok) = (vault.tree.node(r).kind, vault.tree.node(other).kind);
16651231 if rk != ok {
16661232 return Err(io_err(if rk == Kind::Item {
16671233 "an item cannot move below a section"
@@ -1671,7 +1237,7 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
16711237 }
16721238 let file = r.0;
16731239 let text = vault.tree.files[file].text.clone();
1674− // swap the runs' own lines; trailing blank lines stay where they are,
1240+ // swap the nodes' own lines; trailing blank lines stay where they are,
16751241 // so the list's spacing is unchanged
16761242 let core = |sp: Span| {
16771243 let end = sp.end.min(text.len());
@@ -1681,12 +1247,9 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
16811247 }
16821248 Span { start: sp.start, end: e }
16831249 };
1684− let (fr, sr) = if down { ((a, b), other) } else { (other, (a, b)) };
1685− let lines = |(s, e): (usize, usize)| Span {
1686− start: vault.tree.node(siblings[s]).span.start,
1687− end: core(vault.tree.node(siblings[e]).span).end,
1688− };
1689− let (first, second) = (lines(fr), lines(sr));
1250+ let a = core(vault.tree.node(r).span);
1251+ let b = core(vault.tree.node(other).span);
1252+ let (first, second) = if a.start < b.start { (a, b) } else { (b, a) };
16901253 let with_nl = |t: &str| {
16911254 if t.ends_with('\n') {
16921255 t.to_string()
@@ -1694,95 +1257,18 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
16941257 format!("{}\n", t)
16951258 }
16961259 };
1697− // The written level and indent decide the parent: a sibling written
1698− // deeper than the one after it (a skipped heading level, §4.7, or a
1699− // wider indent) would nest under that one once below it, and J/K keep
1700− // every other node's parent (§15.6). So the nodes moving down are
1701− // written at the level and indent of the one moving up, their subtrees
1702− // re-levelled with them, as a moved node is (§4.2).
1703− let (fnode, snode) = (vault.tree.node(siblings[fr.0]), vault.tree.node(siblings[sr.0]));
1704− let lower = if (fnode.level, fnode.indent) == (snode.level, snode.indent) {
1705− with_nl(first.text(&text))
1706− } else {
1707− let base = snode.level.unwrap_or_else(|| vault.tree.level(siblings[fr.0]));
1708− let pad = " ".repeat(snode.indent);
1709− let mut out = String::new();
1710− for i in fr.0..=fr.1 {
1711− let k = siblings[i];
1712− out.extend(
1713− render(&vault.tree, k, base, false)
1714− .lines()
1715− .map(|l| if l.is_empty() { "\n".to_string() } else { format!("{}{}\n", pad, l) }),
1716− );
1717− // the blank lines between a node and its copy stay
1718− if i < fr.1 {
1719− let next = vault.tree.node(siblings[i + 1]).span.start;
1720− out.push_str(&text[core(vault.tree.node(k).span).end..next]);
1721− }
1722− }
1723− out
1724− };
17251260 let mut new_text = format!(
17261261 "{}{}{}{}",
17271262 &text[..first.start],
17281263 with_nl(second.text(&text)),
17291264 &text[first.end..second.start],
1730− lower,
1265+ with_nl(first.text(&text)),
17311266 );
17321267 let rest = &text[second.end..];
17331268 new_text.push_str(rest);
17341269 vault.write_file_text(file, &new_text)
17351270}
17361271
1737−/// Whether the child `k` is a conflict copy, which pairs with the node
1738−/// before it (§12.4).
1739−fn is_copy(tree: &crate::tree::Tree, k: NRef) -> bool {
1740− tree.node(tree.resolved_child(k)).conflict().is_some()
1741−}
1742−
1743−/// The run of `kids` that the `i`-th is in: a node and the conflict copies
1744−/// right after it, which pair with it and each other (§12.4), as indices.
1745−fn paired_run(tree: &crate::tree::Tree, kids: &[NRef], i: usize) -> (usize, usize) {
1746− let mut a = i;
1747− while a > 0 && is_copy(tree, kids[a]) {
1748− a -= 1;
1749− }
1750− let mut b = i;
1751− while b + 1 < kids.len() && is_copy(tree, kids[b + 1]) {
1752− b += 1;
1753− }
1754− (a, b)
1755−}
1756−
1757−/// The run `m` (a node as it stands in its parent's file, §4.7) is in
1758−/// there: a node and its conflict copies, which a verb that moves or
1759−/// deletes the one takes with it, or a copy left behind would pair with
1760−/// the node before it (§12.5). `m` alone when it is in no pair.
1761−fn pair_run(tree: &crate::tree::Tree, m: NRef) -> Vec<NRef> {
1762− let Some(p) = tree.node(m).parent else { return vec![m] };
1763− let kids = tree.raw_children((m.0, p));
1764− let Some(i) = kids.iter().position(|&k| k == m) else { return vec![m] };
1765− let (a, b) = paired_run(tree, &kids, i);
1766− kids[a..=b].to_vec()
1767−}
1768−
1769−/// The conflict pair `r` is in (§12.4): the node, then its copies, as the
1770−/// outline shows them; `r` alone when it is in none.
1771−pub fn conflict_pair(tree: &crate::tree::Tree, r: NRef) -> Vec<NRef> {
1772− let m = stand_in(tree, tree.resolved_child(r));
1773− pair_run(tree, m).into_iter().map(|k| tree.resolved_child(k)).collect()
1774−}
1775−
1776−/// The conflict copies of `r` (§12.4), as the outline shows them: none
1777−/// when it is a copy itself.
1778−pub fn conflict_copies(tree: &crate::tree::Tree, r: NRef) -> Vec<NRef> {
1779− let r = tree.resolved_child(r);
1780− match conflict_pair(tree, r).split_first() {
1781− Some((&o, copies)) if o == r && tree.node(r).conflict().is_none() => copies.to_vec(),
1782− _ => Vec::new(),
1783− }
1784−}
1785−
17861272/// Toggle spelling section ↔ item (§10.3 `~`). The checkbox, if any, is kept,
17871273/// and the subtree is re-indented and re-levelled for the new spelling. The
17881274/// node moves to its parent's boundary if it has to (§3.1), so no sibling
@@ -1794,57 +1280,26 @@ pub fn toggle_spelling(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
17941280 return Ok(false);
17951281 }
17961282 let to_section = n.kind == Kind::Item;
1797− // a node in a conflict pair keeps its spelling: moved to its boundary
1798− // it would leave its copy, or a copy its node, to pair with the node
1799− // before (§12.5), and keeping theirs would write the other one back
1800− if conflict_pair(&vault.tree, r).len() > 1 {
1801− return Err(io_err(PAIR_SPELLING));
1802− }
1803− // an embed still here after resolving is broken: there is no block to
1804− // respell, so only the embed's form changes and its id stays (§4.7)
1805− if n.is_embed() {
1806− return respell_embed(vault, r, to_section);
1807− }
1808− // `## ---` spelled as an item is `- ---`, a thematic break (§4.4)
1809− if !to_section && n.task.is_none() {
1810− item_line(&n.title)?;
1811− }
18121283 let stand = stand_in(&vault.tree, r);
18131284 let file = r.0;
18141285 let text = vault.tree.files[file].text.clone();
18151286 let span = Span { start: n.span.start, end: n.span.end.min(text.len()) };
1816− // respelled from the subtree as a move writes it (§4.2): every heading
1817− // in it ATX, a setext one converted, at the level its position gives,
1818− // so re-levelling keeps each under the node it was under; at the node's
1819− // indent, with the blank lines that end its span
1820− let blanks = span.text(&text).lines().rev().take_while(|l| l.trim().is_empty()).count();
1821− let rendered: String = crate::render::render_lines(&vault.tree, r, vault.tree.level(r), false)
1822− .into_iter()
1823− .map(|l| l.text + "\n")
1824− .collect();
1825− let src = shift_lines(&rendered, 0, n.indent as isize) + &"\n".repeat(blanks);
18261287 if stand != r {
1827− // a block: respell its file's root at level 1, then its embed. The
1828− // embed cannot be written into a file changed on disk (§11.2), and
1829− // the block would be left under an embed of the other form (§4.7):
1830− // check first
1831− vault.check_unchanged(stand.0)?;
1832− let respelled = respell(&src, to_section, 1);
1288+ // a block: respell its file's root at level 1, then its embed
1289+ let respelled = respell(span.text(&text), to_section, 1);
18331290 let id = n.block.as_ref().and_then(|b| b.id.clone());
18341291 vault.write_span(file, span, &respelled)?;
18351292 let Some(e) = id.and_then(|id| vault.tree.embed_of(&id)) else {
1293+ vault.reload()?;
18361294 return Ok(false);
18371295 };
18381296 return respell_embed(vault, e, to_section);
18391297 }
18401298 let Some(parent) = n.parent.map(|p| (file, p)) else { return Ok(false) };
1841− let respelled = respell(&src, to_section, vault.tree.level(parent) + 1);
1299+ let respelled = respell(span.text(&text), to_section, vault.tree.level(parent) + 1);
18421300 reposition(vault, r, parent, respelled, to_section)
18431301}
18441302
1845−/// Why `~` is refused on either side of a conflict pair (§12.5).
1846−pub const PAIR_SPELLING: &str = "resolve its conflict";
1847−
18481303/// Rewrite an embed in the other form (§4.7) — heading for a section-spelled
18491304/// block, bare for an item — moving it to its parent's boundary if needed.
18501305pub fn respell_embed(vault: &mut Vault, e: NRef, to_section: bool) -> std::io::Result<bool> {
@@ -1880,12 +1335,11 @@ fn reposition(
18801335 let kind = if to_section { Kind::Section } else { Kind::Item };
18811336 if clamp_index(tree, &others, pos, &[kind]) == pos {
18821337 let span = tree.node(r).span;
1883− let prev = pos.checked_sub(1).map(|i| others[i]);
1884− let new = level_among(tree, prev, others.get(pos).copied(), &new);
18851338 vault.write_span(r.0, span, &new)?;
1339+ vault.reload()?;
18861340 return Ok(false);
18871341 }
1888− place(vault, &[r], parent, pos, &new).map(|_| true)
1342+ place(vault, Some(r), parent, pos, &new).map(|_| true)
18891343}
18901344
18911345/// A node's text respelled: its title line as a section at `level` or as an
@@ -1923,18 +1377,15 @@ fn respell(span_text: &str, to_section: bool, level: usize) -> String {
19231377
19241378/// Demote: become the last child of the previous sibling node (§10.3 `>`),
19251379/// clamped by the ordering rule (§3.1).
1926−pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1927− let stayed = Placed::stayed(&vault.tree, r);
1380+pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
19281381 let m = stand_in(&vault.tree, r);
1929− let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(stayed) };
1382+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(false) };
19301383 let kids = vault.tree.raw_children(parent);
1931− let Some(pos) = kids.iter().position(|&k| k == m) else { return Ok(stayed) };
1932− // under the node before its conflict pair, which moves as one (§12.5)
1933− let (a, _) = paired_run(&vault.tree, &kids, pos);
1934− if a == 0 {
1935− return Ok(stayed);
1384+ let Some(pos) = kids.iter().position(|&k| k == m) else { return Ok(false) };
1385+ if pos == 0 {
1386+ return Ok(false);
19361387 }
1937− let prev = vault.tree.resolved_child(kids[a - 1]);
1388+ let prev = vault.tree.resolved_child(kids[pos - 1]);
19381389 refile(vault, r, prev)
19391390}
19401391
@@ -1942,27 +1393,24 @@ pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
19421393/// ordering rule (§3.1): an item leaving a section lands before the first
19431394/// section among the parent's siblings. Out of a block's root, the node moves
19441395/// to the parent file beside the block's embed.
1945−pub fn promote(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1946− let stayed = Placed::stayed(&vault.tree, r);
1396+pub fn promote(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
19471397 let m = stand_in(&vault.tree, r);
1948− let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(stayed) };
1398+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(false) };
19491399 if vault.tree.node(parent).kind == Kind::Root {
1950− return Ok(stayed);
1400+ return Ok(false);
19511401 }
19521402 // the parent's own position: its embed if it is a block root
19531403 let pstand = stand_in(&vault.tree, parent);
19541404 let Some(grand) = vault.tree.node(pstand).parent.map(|p| (pstand.0, p)) else {
1955− return Ok(stayed);
1405+ return Ok(false);
19561406 };
19571407 if vault.tree.node(grand).kind == Kind::Root && pstand.0 != vault.tree.root.0 {
19581408 return Err(io_err("block has no embed"));
19591409 }
19601410 let pos = vault.tree.raw_children(grand).iter().position(|&k| k == pstand).unwrap_or(0);
1961− // with its conflict pair, which moves as one (§12.5)
1962− let run = pair_run(&vault.tree, m);
1963− let doc = run_doc(&vault.tree, &run, grand);
1964− let (nodes, clamped) = place(vault, &run, grand, pos + 1, &doc)?;
1965− Placed::of(&nodes, run.iter().position(|&k| k == m).unwrap_or(0), clamped)
1411+ let rendered = render(&vault.tree, m, 1, false);
1412+ let shifted = shift_document(&rendered, vault.tree.level(grand), child_indent(&vault.tree, grand));
1413+ place(vault, Some(m), grand, pos + 1, &shifted)
19661414}
19671415
19681416/// Append a plain item child titled `title` under `parent`; returns the new
@@ -1972,25 +1420,7 @@ pub fn append_child_public(
19721420 parent: NRef,
19731421 title: &str,
19741422) -> std::io::Result<NRef> {
1975− append_child_line(vault, parent, &item_line(title)?, true)
1976−}
1977−
1978−/// The item line titled `title`, refused when it would not read back as
1979−/// one: a title of dashes (`---`, `- -`) makes it a thematic break, which
1980−/// CommonMark reads before a bullet, and body text (§4.4). Checked before
1981−/// anything is written, as the TUI won't create an invalid title (§4.3).
1982−fn item_line(title: &str) -> std::io::Result<String> {
1983− let line = format!("- {}", title);
1984− not_a_break(&line)?;
1985− Ok(line)
1986−}
1987−
1988−/// Refuse an item's title line that reads as a thematic break (§4.4).
1989−fn not_a_break(line: &str) -> std::io::Result<()> {
1990− if crate::parse::is_thematic_break(line.trim_start_matches([' ', '\t'])) {
1991− return Err(io_err("a title of dashes reads as a thematic break, not an item"));
1992− }
1993− Ok(())
1423+ append_child_line(vault, parent, &format!("- {}", title), true)
19941424}
19951425
19961426fn io_err(msg: &str) -> std::io::Error {