@@ -110,6 +110,159 @@ impl Inverse {
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
118+#[derive(Debug, Default)]
122+ transit: Vec<(String, usize)>,
126+ /// The entry undo would take back.
127+ pub fn last_undo(&self) -> Option<&Inverse> {
131+ /// The entry redo would do again.
132+ pub fn last_redo(&self) -> Option<&Inverse> {
136+ /// How many entries undo can take back.
137+ pub fn depth(&self) -> usize {
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);
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
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)) {
174+ undo[n - 1].changes.push(c.clone());
177+ if !inv.changes.is_empty() {
178+ self.undo.push(inv);
179+ at = Some(self.undo.len());
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)| {
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));
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()),
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 });
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) };
245+ let done = inv.description.clone();
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())
113fn today() -> String {
266fn today() -> String {
114 jiff::Zoned::now().date().to_string()
267 jiff::Zoned::now().date().to_string()
@@ -136,13 +289,6 @@ fn capture_inner(
136 task: bool,
289 task: bool,
137 target: Option<NRef>,
290 target: Option<NRef>,
138) -> std::io::Result<NRef> {
291) -> std::io::Result<NRef> {
139− let dest = match target {
142− let inbox = find_or_create_inbox(vault)?;
143− find_or_create_day(vault, inbox)?
146 // The first line is the title; further lines are the item's body, so a
292 // The first line is the title; further lines are the item's body, so a
147 // captured document (`capture < file.md`) nests under the item instead
293 // captured document (`capture < file.md`) nests under the item instead
148 // of injecting structure beside it (§4.1): its headings are pushed below
294 // of injecting structure beside it (§4.1): its headings are pushed below
@@ -154,14 +300,17 @@ fn capture_inner(
155 first.trim().to_string()
301 first.trim().to_string()
303+ // refused before the inbox or its day is created
157− let mut line = format!("- {}", title);
304+ let mut line = item_line(&title)?;
305+ let dest = match target {
306+ Some(t) => write_target(&vault.tree, t)?,
308+ let inbox = find_or_create_inbox(vault)?;
309+ find_or_create_day(vault, inbox)?
158 if !rest.trim().is_empty() {
312 if !rest.trim().is_empty() {
159− let dn = vault.tree.node(dest);
160− let item_indent = if dn.kind == Kind::Item {
161− vault.tree.indent(dest) + 2
163− vault.tree.indent(dest)
313+ let item_indent = child_indent(&vault.tree, dest);
165 let level = vault.tree.level(dest) as isize;
314 let level = vault.tree.level(dest) as isize;
166 let body = shift_lines(rest.trim_end(), level, item_indent as isize + 2);
315 let body = shift_lines(rest.trim_end(), level, item_indent as isize + 2);
167 line.push('\n');
316 line.push('\n');
@@ -188,9 +337,13 @@ fn find_or_create_day(vault: &mut Vault, inbox: NRef) -> std::io::Result<NRef> {
188 return Ok(c);
337 return Ok(c);
191− // `## <today>` as the last child of Inbox (§7)
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();
192 let level = vault.tree.level(inbox) + 1;
344 let level = vault.tree.level(inbox) + 1;
193− let indent = vault.tree.indent(inbox);
345+ let level = written_level(&vault.tree, last).map_or(level, |w| level.min(w));
346+ let indent = child_indent(&vault.tree, inbox);
194 let heading = format!(
347 let heading = format!(
195 "{}{} {}",
348 "{}{} {}",
196 " ".repeat(indent),
349 " ".repeat(indent),
@@ -199,10 +352,9 @@ fn find_or_create_day(vault: &mut Vault, inbox: NRef) -> std::io::Result<NRef> {
200 let inbox_key = vault.key_of(inbox);
353 let inbox_key = vault.key_of(inbox);
201 append_structural_line(vault, inbox, &heading)?;
354 append_structural_line(vault, inbox, &heading)?;
203 let inbox = vault
355 let inbox = vault
204 .find_by_key(&inbox_key)
356 .find_by_key(&inbox_key)
205− .ok_or_else(|| io_err("inbox lost after reload"))?;
357+ .ok_or_else(|| io_err("inbox lost after the write"))?;
206 for c in vault.tree.resolved_children(inbox) {
358 for c in vault.tree.resolved_children(inbox) {
207 if vault.tree.node(c).title == day {
359 if vault.tree.node(c).title == day {
208 return Ok(c);
360 return Ok(c);
@@ -282,20 +434,22 @@ fn append_child_line(
282 line: &str,
434 line: &str,
283 section_ok: bool,
435 section_ok: bool,
284) -> std::io::Result<NRef> {
436) -> 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)?;
285 let parent_key = vault.key_of(parent);
440 let parent_key = vault.key_of(parent);
286 let node = vault.tree.node(parent);
441 let node = vault.tree.node(parent);
287 let file = parent.0;
442 let file = parent.0;
288 // Items under a section sit at the section's own indent; items under an
443 // Items under a section sit at the section's own indent; items under an
289 // item nest one level deeper (§3.1, §4.10).
444 // item nest one level deeper (§3.1, §4.10).
290− let indent = if node.kind == Kind::Item {
291− vault.tree.indent(parent) + 2
293− vault.tree.indent(parent)
445+ let indent = child_indent(&vault.tree, parent);
295 let text = vault.tree.files[file].text.clone();
446 let text = vault.tree.files[file].text.clone();
296 let kids = vault.tree.raw_children(parent);
447 let kids = vault.tree.raw_children(parent);
297 let is_section = |r: NRef| vault.tree.node(r).kind == Kind::Section;
448 let is_section = |r: NRef| vault.tree.node(r).kind == Kind::Section;
298 let first_section = kids.iter().position(|&c| is_section(c));
449 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();
299 let index;
453 let index;
300 match (first_section, section_ok) {
454 match (first_section, section_ok) {
301 (Some(_), true) => {
455 (Some(_), true) => {
@@ -307,10 +461,12 @@ fn append_child_line(
307 *kids.iter().rev().find(|&&c| is_section(c)).unwrap()
461 *kids.iter().rev().find(|&&c| is_section(c)).unwrap()
309 let title = line.trim_start().strip_prefix("- ").unwrap_or(line.trim_start());
463 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
310 let heading = format!(
466 let heading = format!(
311 "{}{} {}",
467 "{}{} {}",
312− " ".repeat(vault.tree.indent(last)),
468+ " ".repeat(vault.tree.node(last).indent),
313− "#".repeat(vault.tree.level(last)),
469+ "#".repeat(written_level(&vault.tree, Some(last)).unwrap_or(1)),
316 let pos = trimmed_end(&text, node.span);
472 let pos = trimmed_end(&text, node.span);
@@ -325,7 +481,7 @@ fn append_child_line(
325 pos -= 1;
481 pos -= 1;
327 let prev_is_item = fs > 0 && !is_section(kids[fs - 1]);
483 let prev_is_item = fs > 0 && !is_section(kids[fs - 1]);
328− let sep = if prev_is_item { "" } else { "\n" };
484+ let sep = if prev_is_item || (fs == 0 && bare_item) { "" } else { "\n" };
329 vault.write_span(
485 vault.write_span(
331 Span { start: pos, end: pos },
487 Span { start: pos, end: pos },
@@ -337,17 +493,16 @@ fn append_child_line(
337 let line_indented = format!("{}{}", " ".repeat(indent), line);
493 let line_indented = format!("{}{}", " ".repeat(indent), line);
338 let pos = trimmed_end(&text, node.span);
494 let pos = trimmed_end(&text, node.span);
339 // blank line before unless the previous sibling is also an item
495 // blank line before unless the previous sibling is also an item
340− // (tight list)
496+ // (tight list), or there is none under a bare item
341− let prev_is_item = kids.last().map(|&c| !is_section(c)).unwrap_or(false);
497+ let prev_is_item = kids.last().map(|&c| !is_section(c)).unwrap_or(bare_item);
342 let sep = if prev_is_item { "\n" } else { "\n\n" };
498 let sep = if prev_is_item { "\n" } else { "\n\n" };
343 insert_at(vault, file, pos, sep, &line_indented)?;
499 insert_at(vault, file, pos, sep, &line_indented)?;
344 index = kids.len();
500 index = kids.len();
348 let parent = vault
503 let parent = vault
349 .find_by_key(&parent_key)
504 .find_by_key(&parent_key)
350− .ok_or_else(|| io_err("parent lost after reload"))?;
505+ .ok_or_else(|| io_err("parent lost after the write"))?;
353 .resolved_children(parent)
508 .resolved_children(parent)
@@ -372,7 +527,7 @@ pub fn toggle_task(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
373/// Toggle task-ness itself (§10.3 `t`).
528/// Toggle task-ness itself (§10.3 `t`).
374pub fn toggle_taskness(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
529pub fn toggle_taskness(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
375− let r = vault.tree.resolved_child(r);
530+ let r = write_target(&vault.tree, r)?;
376 let state = match vault.tree.node(r).task {
531 let state = match vault.tree.node(r).task {
377 Some(_) => None,
532 Some(_) => None,
378 None => Some(TaskState::Open),
533 None => Some(TaskState::Open),
@@ -387,8 +542,11 @@ pub fn set_task(vault: &mut Vault, r: NRef, state: Option<TaskState>) -> std::io
387 let n = vault.tree.node(r);
542 let n = vault.tree.node(r);
388 let is_block = n.is_block();
543 let is_block = n.is_block();
389 let file = r.0;
544 let file = r.0;
390− let ts = n.title_span;
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(|| {
391− let line = ts.text(&vault.tree.files[file].text).to_string();
548+ (n.title_span, n.title_span.text(&vault.tree.files[file].text).to_string())
392 let mark = |st: TaskState| match st {
550 let mark = |st: TaskState| match st {
393 TaskState::Open => "[ ]",
551 TaskState::Open => "[ ]",
394 TaskState::Done => "[x]", // also rewrites `[X]` / `[-]`
552 TaskState::Done => "[x]", // also rewrites `[X]` / `[-]`
@@ -407,7 +565,9 @@ pub fn set_task(vault: &mut Vault, r: NRef, state: Option<TaskState>) -> std::io
408 (None, None) => line.clone(),
566 (None, None) => line.clone(),
410− if new_line != line {
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) {
411 vault.write_span(file, ts, &new_line)?;
571 vault.write_span(file, ts, &new_line)?;
413 if is_block {
573 if is_block {
@@ -440,6 +600,26 @@ fn marker_end(line: &str) -> Option<usize> {
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() {
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))
443/// Byte range of the checkbox right after the marker (`[ ]`, `[x]`, `[X]`,
623/// Byte range of the checkbox right after the marker (`[ ]`, `[x]`, `[X]`,
444/// `[-]`) plus the space after it (§4.2). Checkbox-like text later in the
624/// `[-]`) plus the space after it (§4.2). Checkbox-like text later in the
445/// title is title text.
625/// title is title text.
@@ -468,17 +648,13 @@ pub fn set_frontmatter_key(
468 value: Option<&str>,
648 value: Option<&str>,
469) -> std::io::Result<()> {
649) -> std::io::Result<()> {
470 let f = vault.tree.files[file].clone();
650 let f = vault.tree.files[file].clone();
471− let root_node = f.nodes[f.root_node].children.get(0).copied();
651+ let block = file_block(&f).cloned();
472− let block = root_node.and_then(|rn| f.nodes[rn].block.clone());
473 let (mut raw, fm_span) = match &block {
652 let (mut raw, fm_span) = match &block {
474 Some(b) => (b.frontmatter_raw.clone(), b.frontmatter_span),
653 Some(b) => (b.frontmatter_raw.clone(), b.frontmatter_span),
475 None => (String::new(), None),
654 None => (String::new(), None),
477 // rewrite the line for `key` inside raw
656 // rewrite the line for `key` inside raw
478 let mut lines: Vec<String> = raw.split_inclusive('\n').map(|s| s.to_string()).collect();
657 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
482 let key_prefix = format!("{}:", key);
658 let key_prefix = format!("{}:", key);
483 let mut found = false;
659 let mut found = false;
484 for l in lines.iter_mut() {
660 for l in lines.iter_mut() {
@@ -525,14 +701,18 @@ pub fn set_frontmatter_key(
525 vault.write_file_text(file, &new_text)
701 vault.write_file_text(file, &new_text)
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())
528/// All top-level keys of a block's frontmatter, for the property editor.
712/// All top-level keys of a block's frontmatter, for the property editor.
529pub fn frontmatter_lines(vault: &Vault, file: usize) -> Vec<(String, String, bool)> {
713pub fn frontmatter_lines(vault: &Vault, file: usize) -> Vec<(String, String, bool)> {
530 // (key, value, editable) — unknown-structure lines are read-only (§10.6)
714 // (key, value, editable) — unknown-structure lines are read-only (§10.6)
531− let f = &vault.tree.files[file];
715+ let Some(b) = file_block(&vault.tree.files[file]) else {
532− let Some(&rn) = f.nodes[f.root_node].children.first() else {
535− let Some(b) = &f.nodes[rn].block else {
536 return Vec::new();
716 return Vec::new();
538 let mut out = Vec::new();
718 let mut out = Vec::new();
@@ -555,31 +735,54 @@ pub fn frontmatter_lines(vault: &Vault, file: usize) -> Vec<(String, String, boo
556/// Delete a subtree: its span leaves its file; block files go to trash (§11.5),
736/// Delete a subtree: its span leaves its file; block files go to trash (§11.5),
557/// including blocks nested anywhere under it, so none is left orphaned.
737/// 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
558−pub fn delete_subtree(vault: &mut Vault, r: NRef) -> std::io::Result<String> {
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
744+ let copies: Vec<Id> = conflict_copies(&vault.tree, r)
746+ .filter_map(|c| vault.tree.node(c).block.as_ref()?.id.clone())
748+ let mut count = delete_one(vault, r)?;
750+ if let Some(c) = vault.tree.block_by_id(&id) {
751+ count += delete_one(vault, c)?;
757+/// `delete_subtree` for `r` alone.
758+fn delete_one(vault: &mut Vault, r: NRef) -> std::io::Result<usize> {
559 let n = vault.tree.node(r);
759 let n = vault.tree.node(r);
560 if n.kind == Kind::Root {
760 if n.kind == Kind::Root {
561 return Err(io_err("cannot delete the root"));
761 return Err(io_err("cannot delete the root"));
764+ vault.tree.walk(vault.tree.resolved_child(r), &mut |_, _| count += 1);
563 if n.is_block() || n.is_embed() {
765 if n.is_block() || n.is_embed() {
564 let target = if n.is_embed() { vault.tree.resolved_child(r) } else { r };
766 let target = if n.is_embed() { vault.tree.resolved_child(r) } else { r };
565 if target == r && n.is_embed() {
767 if target == r && n.is_embed() {
566− // broken embed: just the line
768+ // broken embed: just the line. So for a second embed of a
567− let span = n.span;
769+ // block, which renders as broken (§6.2): the block, and the
568− remove_span_with_separator(vault, r.0, span)?;
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)?;
569− return Ok("broken embed removed".into());
773+ return Ok(1);
775+ // the block itself first: it leaves by its own embed
571 let ids = nested_block_ids(vault, target);
776 let ids = nested_block_ids(vault, target);
572− let path = vault.tree.node(target).block.as_ref().unwrap().path.clone();
777+ if let Some((own, nested)) = ids.split_first() {
574− trash_block(vault, &id)?;
778+ trash_block(vault, own)?;
779+ trash_nested(vault, nested)?;
576− return Ok(format!("block {:?} trashed", path));
781+ return Ok(count);
578 let ids = plain_remove(vault, r)?;
783 let ids = plain_remove(vault, r)?;
579− for id in ids {
784+ trash_nested(vault, &ids)?;
580− trash_block(vault, &id)?;
785+ Ok(count)
582− Ok("subtree trashed".into())
585/// Ids of every block in the resolved subtree of `r` (pre-order, `r` itself
788/// Ids of every block in the resolved subtree of `r` (pre-order, `r` itself
@@ -602,22 +805,16 @@ fn plain_remove(vault: &mut Vault, r: NRef) -> std::io::Result<Vec<Id>> {
602 let name = format!("{}.md", slug(&vault.tree.node(r).title));
805 let name = format!("{}.md", slug(&vault.tree.node(r).title));
603 vault.trash_text(&name, &text)?;
806 vault.trash_text(&name, &text)?;
604 let span = vault.tree.node(r).span;
807 let span = vault.tree.node(r).span;
605− remove_span_with_separator(vault, r.0, span)?;
808+ remove_span(vault, r.0, span)?;
812+/// Remove the embed line a block is stitched in at (if it is still
609−/// Remove a block's embed line (if it is still anywhere) and move its file to
813+/// anywhere) and move its file to the trash.
611fn trash_block(vault: &mut Vault, id: &Id) -> std::io::Result<()> {
814fn trash_block(vault: &mut Vault, id: &Id) -> std::io::Result<()> {
612− let embed = vault.tree.files.iter().enumerate().find_map(|(fi, f)| {
815+ if let Some(e) = vault.tree.embed_of(id) {
615− .position(|nd| nd.embed.as_ref() == Some(id))
618− if let Some(e) = embed {
619− let span = vault.tree.node(e).span;
816+ let span = embed_line_span(&vault.tree, e);
620− remove_span_with_separator(vault, e.0, span)?;
817+ remove_span(vault, e.0, span)?;
622 if let Some(b) = vault.tree.block_by_id(id) {
819 if let Some(b) = vault.tree.block_by_id(id) {
623 vault.trash_file(b.0)?;
820 vault.trash_file(b.0)?;
@@ -625,6 +822,36 @@ fn trash_block(vault: &mut Vault, id: &Id) -> std::io::Result<()> {
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<()> {
830+ if let Some(b) = vault.tree.block_by_id(id) {
831+ vault.trash_file(b.0)?;
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() {
852+ Span { start: n.span.start, end: at }
628/// The byte range to cut when removing `span`: the node plus whatever blank
855/// The byte range to cut when removing `span`: the node plus whatever blank
629/// separator would otherwise be doubled or left dangling. A blank line that
856/// separator would otherwise be doubled or left dangling. A blank line that
630/// separated the node from what follows stays when nothing blank precedes
857/// separated the node from what follows stays when nothing blank precedes
@@ -658,17 +885,12 @@ fn after_removal(pos: usize, start: usize, end: usize) -> usize {
660/// Remove a span plus one adjacent blank separator line, then re-parse just
887/// Remove a span plus one adjacent blank separator line, then re-parse just
661−/// that file (no full reload, so other files' node refs stay valid).
888+/// that file, so other files' node refs stay valid.
662−fn remove_span_no_reload(vault: &mut Vault, file: usize, span: Span) -> std::io::Result<()> {
889+fn remove_span(vault: &mut Vault, file: usize, span: Span) -> std::io::Result<()> {
663 let (start, end) = removal_range(&vault.tree.files[file].text, span);
890 let (start, end) = removal_range(&vault.tree.files[file].text, span);
664 vault.write_span(file, Span { start, end }, "")
891 vault.write_span(file, Span { start, end }, "")
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)
672/// Yank: render the subtree resolved (the register's text) (§10.3).
894/// Yank: render the subtree resolved (the register's text) (§10.3).
673pub fn yank(vault: &Vault, r: NRef) -> String {
895pub fn yank(vault: &Vault, r: NRef) -> String {
674 render(&vault.tree, r, 1, true)
896 render(&vault.tree, r, 1, true)
@@ -676,7 +898,7 @@ pub fn yank(vault: &Vault, r: NRef) -> String {
677/// Paste rendered text after/before a node as siblings (§10.3 `p`/`P`),
899/// Paste rendered text after/before a node as siblings (§10.3 `p`/`P`),
678/// clamped by the ordering rule (§3.1).
900/// clamped by the ordering rule (§3.1).
679−pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::Result<bool> {
901+pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::Result<Placed> {
680 if vault.tree.node(at).kind == Kind::Root {
902 if vault.tree.node(at).kind == Kind::Root {
681 return Err(io_err("cannot paste beside the root"));
903 return Err(io_err("cannot paste beside the root"));
@@ -687,7 +909,8 @@ pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::R
688 let pos = vault.tree.raw_children(parent).iter().position(|&k| k == at).unwrap_or(0);
910 let pos = vault.tree.raw_children(parent).iter().position(|&k| k == at).unwrap_or(0);
689 let shifted = shift_document(text, vault.tree.level(parent), child_indent(&vault.tree, parent));
911 let shifted = shift_document(text, vault.tree.level(parent), child_indent(&vault.tree, parent));
690− place(vault, None, parent, pos + after as usize, &shifted)
912+ let (nodes, clamped) = place(vault, &[], parent, pos + after as usize, &shifted)?;
913+ Placed::of(&nodes, 0, clamped)
693/// Parse a standalone document and re-emit it as children of a node whose
916/// Parse a standalone document and re-emit it as children of a node whose
@@ -696,19 +919,9 @@ pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::R
696/// in a section of level `parent_level`. Everything inside keeps its position
919/// in a section of level `parent_level`. Everything inside keeps its position
697/// relative to its top-level node.
920/// relative to its top-level node.
698pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String {
921pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String {
699− let block = crate::parse::Block {
922+ let block = crate::parse::Block::new("clip.md", None, None);
701− path: "clip.md".into(),
702− props: Default::default(),
703− frontmatter_raw: String::new(),
704− frontmatter_span: None,
706− let pf = crate::parse::parse_file("clip.md", text, 0, Some(block));
923+ let pf = crate::parse::parse_file("clip.md", text, Some(block));
707− let tree = crate::tree::Tree {
924+ let tree = crate::tree::Tree::new(vec![pf]);
712 let mut out = String::new();
925 let mut out = String::new();
713 let kids = tree.resolved_children(tree.root);
926 let kids = tree.resolved_children(tree.root);
714 for (i, k) in kids.iter().enumerate() {
927 for (i, k) in kids.iter().enumerate() {
@@ -720,11 +933,9 @@ pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String
720 let t = first.trim_start();
933 let t = first.trim_start();
721 // render(_, 1) writes a section root at level 1 and an item root in
934 // render(_, 1) writes a section root at level 1 and an item root in
722 // a section of level 1 (its nested sections at 2)
935 // a section of level 1 (its nested sections at 2)
723− let ld = if t.starts_with('#') {
936+ let ld = match atx_hashes(t) {
724− let cur_level = t.chars().take_while(|&c| c == '#').count();
725− parent_level as isize + 1 - cur_level as isize
937+ Some(cur_level) => parent_level as isize + 1 - cur_level as isize,
727− parent_level as isize - 1
938+ None => parent_level as isize - 1,
729 let cur_indent = first.len() - t.len();
940 let cur_indent = first.len() - t.len();
730 let id = indent as isize - cur_indent as isize;
941 let id = indent as isize - cur_indent as isize;
@@ -740,26 +951,33 @@ pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String
742/// Shift every line of a rendered document by (level_delta, indent_delta).
953/// 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.
743fn shift_lines(raw: &str, level_delta: isize, indent_delta: isize) -> String {
956fn shift_lines(raw: &str, level_delta: isize, indent_delta: isize) -> String {
744 let mut out = String::new();
957 let mut out = String::new();
745 let mut fence: Option<(char, usize)> = None;
958 let mut fence: Option<(char, usize)> = None;
746 for line in raw.split_inclusive('\n') {
959 for line in raw.split_inclusive('\n') {
747 let l = line.strip_suffix('\n').unwrap_or(line);
960 let l = line.strip_suffix('\n').unwrap_or(line);
748 let nl = if line.ends_with('\n') { "\n" } else { "" };
961 let nl = if line.ends_with('\n') { "\n" } else { "" };
749− if fence_transition(l, &mut fence) || fence.is_some() {
962+ let in_code = crate::parse::fence_transition(l, &mut fence) || fence.is_some();
750− out.push_str(l);
963+ let trimmed = l.trim_start();
964+ if trimmed.is_empty() {
751 out.push_str(nl);
965 out.push_str(nl);
752 continue;
966 continue;
754− let trimmed = l.trim_start();
968+ if in_code {
755− if trimmed.is_empty() {
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));
756 out.push_str(nl);
975 out.push_str(nl);
757 continue;
976 continue;
759 let cur_indent = l.len() - trimmed.len();
978 let cur_indent = l.len() - trimmed.len();
760 let new_indent = (cur_indent as isize + indent_delta).max(0) as usize;
979 let new_indent = (cur_indent as isize + indent_delta).max(0) as usize;
761− if trimmed.starts_with('#') {
980+ if let Some(hashes) = atx_hashes(trimmed) {
762− let hashes = trimmed.chars().take_while(|&c| c == '#').count();
763 let new_level = (hashes as isize + level_delta).max(1) as usize;
981 let new_level = (hashes as isize + level_delta).max(1) as usize;
764 out.push_str(&" ".repeat(new_indent));
982 out.push_str(&" ".repeat(new_indent));
765 out.push_str(&"#".repeat(new_level));
983 out.push_str(&"#".repeat(new_level));
@@ -774,35 +992,17 @@ fn shift_lines(raw: &str, level_delta: isize, indent_delta: isize) -> String {
777−fn fence_transition(raw: &str, open: &mut Option<(char, usize)>) -> bool {
995+/// The level of an ATX heading line, given without its indent: `#`+ then a
778− let t = raw.trim_start();
996+/// space or the end of the line (§4.2). `#tag` is text, as the parser reads it.
779− let first = match t.chars().next() {
997+fn atx_hashes(trimmed: &str) -> Option<usize> {
780− Some(c) if c == '`' || c == '~' => c,
783− let count = t.chars().take_while(|&c| c == first).count();
998+ let hashes = trimmed.bytes().take_while(|&b| b == b'#').count();
784− if count < 3 {
999+ let after = &trimmed[hashes..];
785− return false;
1000+ (hashes > 0 && (after.is_empty() || after.starts_with(' '))).then_some(hashes)
789− *open = Some((first, count));
793− if *c == first && count >= *n {
803/// Refile: move a subtree under a new parent as its last child (§6.5).
1003/// Refile: move a subtree under a new parent as its last child (§6.5).
804−pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<bool> {
1004+pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<Placed> {
805− let dest = vault.tree.resolved_child(dest);
1005+ let dest = write_target(&vault.tree, dest)?;
806 let n = vault.tree.node(r);
1006 let n = vault.tree.node(r);
807 // guard: cannot refile into own subtree — ancestry followed through
1007 // guard: cannot refile into own subtree — ancestry followed through
808 // embeds, so a destination inside a nested block counts too
1008 // embeds, so a destination inside a nested block counts too
@@ -816,7 +1016,7 @@ pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<bool> {
817 guard += 1;
1017 guard += 1;
818 if guard > 10_000 {
1018 if guard > 10_000 {
819− break; // embed cycle: a diagnostic elsewhere
1019+ break; // embed cycle: `check` reports it (§6.2)
821 anc = resolved_parent(&vault.tree, a);
1021 anc = resolved_parent(&vault.tree, a);
@@ -826,9 +1026,61 @@ pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<bool> {
826 if vault.tree.node(moving).is_block() {
1026 if vault.tree.node(moving).is_block() {
827 return Err(io_err("block has no embed"));
1027 return Err(io_err("block has no embed"));
829− let rendered = render(&vault.tree, moving, 1, false);
1029+ // with its conflict copies, or with the node a copy is of (§12.5)
830− let shifted = shift_document(&rendered, vault.tree.level(dest), child_indent(&vault.tree, dest));
1030+ let run = pair_run(&vault.tree, moving);
831− place(vault, Some(moving), dest, usize::MAX, &shifted)
1031+ if run.iter().any(|&m| m != moving && within(&vault.tree, dest, m)) {
1032+ return Err(io_err(PAIR_INTO_ITSELF));
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)
1039+/// A node and its conflict copy move as one (§12.5): neither into the
1041+const PAIR_INTO_ITSELF: &str = "cannot move a node and its conflict copy into either of them";
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() {
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") {
1057+ out.push_str(&shift_document(&render(tree, m, 1, false), level, indent));
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)]
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 }
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 })
834/// Where a dragged node is dropped (§10.3): before a node, as its sibling,
1086/// Where a dragged node is dropped (§10.3): before a node, as its sibling,
@@ -840,11 +1092,11 @@ pub enum Drop {
842/// Move `r` relative to `target` (§10.3 drag and drop). Refuses a move into
1094/// Move `r` relative to `target` (§10.3 drag and drop). Refuses a move into
843−/// `r`'s own subtree. Returns whether the ordering rule changed the place.
1095+/// `r`'s own subtree.
844−pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::io::Result<bool> {
1096+pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::io::Result<Placed> {
845 let target = vault.tree.resolved_child(target);
1097 let target = vault.tree.resolved_child(target);
846 if target == vault.tree.resolved_child(r) {
1098 if target == vault.tree.resolved_child(r) {
847− return Ok(false);
1099+ return Ok(Placed::stayed(&vault.tree, r));
849 match drop {
1101 match drop {
850 Drop::Into => refile(vault, r, target),
1102 Drop::Into => refile(vault, r, target),
@@ -858,11 +1110,27 @@ pub fn move_node(vault: &mut Vault, r: NRef, target: NRef, drop: Drop) -> std::i
858 return Err(io_err("cannot move a node into itself"));
1110 return Err(io_err("cannot move a node into itself"));
860 let moving = stand_in(&vault.tree, r);
1112 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));
1119+ if run.iter().any(|&m| m != moving && within(&vault.tree, parent, m)) {
1120+ return Err(io_err(PAIR_INTO_ITSELF));
1122+ // the target's index among the children `place` sees: without
1123+ // the moving nodes, which may come before it
861− let idx = vault.tree.raw_children(parent).iter().position(|&k| k == t).unwrap_or(0);
1126+ .raw_children(parent)
862− let rendered = render(&vault.tree, moving, 1, false);
1127+ .into_iter()
863− let shifted =
1128+ .filter(|k| !run.contains(k))
864− shift_document(&rendered, vault.tree.level(parent), child_indent(&vault.tree, parent));
1129+ .position(|k| k == t)
865− place(vault, Some(moving), parent, idx, &shifted)
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)
@@ -900,12 +1168,29 @@ fn stand_in(tree: &crate::tree::Tree, r: NRef) -> NRef {
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"));
903/// Indent of a child of `parent` (§3.1): items nest under items, everything
1184/// Indent of a child of `parent` (§3.1): items nest under items, everything
904−/// under a section stays at the section's own indent.
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.
905fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
1189fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
1190+ let n = tree.node(parent);
906− match tree.node(parent).kind {
1191+ match n.kind {
907− Kind::Item => tree.indent(parent) + 2,
1192+ Kind::Item => n.indent + 2,
908− Kind::Section => tree.indent(parent),
1193+ Kind::Section => n.indent,
909 Kind::Root => 0,
1194 Kind::Root => 0,
@@ -913,7 +1198,7 @@ fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
913/// Spellings of a document's top-level nodes, in order. A heading embed is
1198/// Spellings of a document's top-level nodes, in order. A heading embed is
914/// a section, a bare embed an item (§4.7).
1199/// a section, a bare embed an item (§4.7).
915fn top_kinds(doc: &str) -> Vec<Kind> {
1200fn top_kinds(doc: &str) -> Vec<Kind> {
916− let pf = crate::parse::parse_file("clip.md", doc, 0, None);
1201+ let pf = crate::parse::parse_file("clip.md", doc, None);
917 pf.nodes[pf.root_node]
1202 pf.nodes[pf.root_node]
918 .children
1203 .children
@@ -921,6 +1206,12 @@ fn top_kinds(doc: &str) -> Vec<Kind> {
921 .collect()
1206 .collect()
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)
924/// Clamp a wanted child index by the ordering rule `(text | item)*
1215/// Clamp a wanted child index by the ordering rule `(text | item)*
925/// section*` (§3.1): items no later than the first section child, sections
1216/// section*` (§3.1): items no later than the first section child, sections
926/// no earlier. `kids` are the parent's child nodes without the one moving.
1217/// no earlier. `kids` are the parent's child nodes without the one moving.
@@ -934,41 +1225,98 @@ fn clamp_index(tree: &crate::tree::Tree, kids: &[NRef], want: usize, kinds: &[Ki
934 want.min(kids.len()).clamp(lo, hi)
1225 want.min(kids.len()).clamp(lo, hi)
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))
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();
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();
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));
1261+ return doc.to_string();
1263+ let at = first.span.start;
1264+ format!("{}{}", &doc[..at], shift_lines(&doc[at..], want as isize - have as isize, 0))
937/// Write `doc` (already shifted to a child position of `parent`) as the
1267/// Write `doc` (already shifted to a child position of `parent`) as the
938/// `want`-th child node of `parent`, clamped by the ordering rule (§3.1).
1268/// `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,
939−/// With `moving`, that node's own lines are removed in the same edit: text
1270+/// §12.5), have their own lines removed in the same edit: text children
940−/// children around it stay where they are. The insertion is written before
1271+/// around them stay where they are. The insertion is written before any
941−/// any removal in another file, so a failure never loses text. Returns
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
942−/// whether the ordering rule moved it from the wanted index.
1274+/// they were written at, and whether the ordering rule moved them from
1275+/// the wanted index.
943fn place(
1276fn place(
944 vault: &mut Vault,
1277 vault: &mut Vault,
945− moving: Option<NRef>,
1278+ moving: &[NRef],
946 parent: NRef,
1279 parent: NRef,
947 want: usize,
1280 want: usize,
948 doc: &str,
1281 doc: &str,
949−) -> std::io::Result<bool> {
1282+) -> std::io::Result<(Vec<NRef>, bool)> {
950 let tree = &vault.tree;
1283 let tree = &vault.tree;
951 let pnode = tree.node(parent);
1284 let pnode = tree.node(parent);
952 let kids: Vec<NRef> = tree
1285 let kids: Vec<NRef> = tree
953 .raw_children(parent)
1286 .raw_children(parent)
954 .into_iter()
1287 .into_iter()
955− .filter(|&k| Some(k) != moving)
1288+ .filter(|k| !moving.contains(k))
956 .collect();
1289 .collect();
957 let kinds = top_kinds(doc);
1290 let kinds = top_kinds(doc);
958 if kinds.is_empty() {
1291 if kinds.is_empty() {
959 return Err(io_err("nothing to place"));
1292 return Err(io_err("nothing to place"));
961− let idx = clamp_index(tree, &kids, want, &kinds);
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))
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 {
962 let first = kinds[0];
1310 let first = kinds[0];
963 let last = *kinds.last().unwrap();
1311 let last = *kinds.last().unwrap();
964 let file = parent.0;
1312 let file = parent.0;
965 let mut text = tree.files[file].text.clone();
1313 let mut text = tree.files[file].text.clone();
966− // the parent's content as it will be, without the moving node
1314+ // the parent's content as it will be, without the moving nodes
967 let content: Vec<crate::parse::Content> = pnode
1315 let content: Vec<crate::parse::Content> = pnode
968 .content
1316 .content
970 .copied()
1318 .copied()
971− .filter(|c| !matches!(c, crate::parse::Content::Node(n) if Some((file, *n)) == moving))
1319+ .filter(|c| !matches!(c, crate::parse::Content::Node(n) if moving.contains(&(file, *n))))
972 .collect();
1320 .collect();
973 // positions in the original text, then adjusted for a same-file removal
1321 // positions in the original text, then adjusted for a same-file removal
974 let next = kids.get(idx).copied();
1322 let next = kids.get(idx).copied();
@@ -996,17 +1344,52 @@ fn place(
996 Some(crate::parse::Content::Text(_)) => false,
1344 Some(crate::parse::Content::Text(_)) => false,
997 None => pnode.kind == Kind::Item,
1345 None => pnode.kind == Kind::Item,
999− let next_kind = next.map(|k| tree.node(k).kind);
1347+ // inserting before an item: after another item, the list is as tight as
1000− let removal = match moving {
1348+ // the text there says (read below); starting the list, it is read in
1001− Some(m) if m.0 == file => Some(removal_range(&text, tree.node(m).span)),
1349+ // the list as found, the moving nodes still in it: between `next` and
1002− _ => None,
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)
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")
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) => {
1004− if let Some((start, end)) = removal {
1373+ 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);
1005 text = format!("{}{}", &text[..start], &text[end..]);
1377 text = format!("{}{}", &text[..start], &text[end..]);
1006 pos = after_removal(pos, start, end);
1378 pos = after_removal(pos, start, end);
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(),
1387+ let prev = idx.checked_sub(1).map(|i| kids[i]);
1388+ let doc = level_among(tree, prev, next, &doc);
1008 let body = doc.trim_end_matches('\n');
1389 let body = doc.trim_end_matches('\n');
1390+ let tops = top_starts(body);
1391+ // the insertion, and where `body` starts in it
1009− let insertion = match next_kind {
1392+ let (insertion, at) = match next_kind {
1010 Some(nk) => {
1393 Some(nk) => {
1011 // before the next child node: keep a blank line before a
1394 // before the next child node: keep a blank line before a
1012 // heading and after anything that is not a tight item run
1395 // heading and after anything that is not a tight item run
@@ -1015,8 +1398,10 @@ fn place(
1015 } else {
1398 } else {
1018− let tight = last == Kind::Item && nk == Kind::Item && !text[..pos].ends_with("\n\n");
1401+ let tight = last == Kind::Item
1402+ && nk == Kind::Item
1403+ && if starts_run { run_tight } else { !text[..pos].ends_with("\n\n") };
1019− format!("{}{}\n{}", lead, body, if tight { "" } else { "\n" })
1404+ (format!("{}{}\n{}", lead, body, if tight { "" } else { "\n" }), lead.len())
1021 None => {
1406 None => {
1022 while pos > 0 && text.as_bytes()[pos - 1] == b'\n' {
1407 while pos > 0 && text.as_bytes()[pos - 1] == b'\n' {
@@ -1030,23 +1415,37 @@ fn place(
1032 let nl = if text[pos..].starts_with('\n') { "" } else { "\n" };
1417 let nl = if text[pos..].starts_with('\n') { "" } else { "\n" };
1033− format!("{}{}{}", sep, body, nl)
1418+ (format!("{}{}{}", sep, body, nl), sep.len())
1421+ let start = pos + at;
1036 text.insert_str(pos, &insertion);
1422 text.insert_str(pos, &insertion);
1037 vault.write_file_text(file, &text)?;
1423 vault.write_file_text(file, &text)?;
1038− if let Some(m) = moving {
1040− // the other file's node refs are untouched by the write above
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)> =
1041− remove_span_no_reload(vault, m.0, vault.tree.node(m).span)?;
1427+ moving.iter().filter(|m| m.0 != file).map(|&m| (m.0, vault.tree.node(m).span)).collect();
1042− }
1428+ for (f, span) in away.into_iter().rev() {
1429+ remove_span(vault, f, span)?;
1044− vault.reload()?;
1431+ // `file` is as written: each node starts where its line went in
1432+ let f = &vault.tree.files[file];
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)))
1045− Ok(idx != want.min(kids.len()))
1438+ Ok((placed, clamped != want.min(kids.len())))
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()
1048/// Archive: refile under the top-level `Archive` section (§6.5).
1447/// Archive: refile under the top-level `Archive` section (§6.5).
1049−pub fn archive(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
1448+pub fn archive(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1050 let root = vault.tree.root;
1449 let root = vault.tree.root;
1051 let mut dest = None;
1450 let mut dest = None;
1052 for c in vault.tree.resolved_children(root) {
1451 for c in vault.tree.resolved_children(root) {
@@ -1072,12 +1471,22 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
1072 vault.tree.walk(target, &mut |t, r| {
1471 vault.tree.walk(target, &mut |t, r| {
1073 let n = t.node(r);
1472 let n = t.node(r);
1074 if n.task == Some(TaskState::Done) {
1473 if n.task == Some(TaskState::Done) {
1075− let (open, _total) = t.task_counts(r);
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));
1076− if open == 0 {
1478+ if !open {
1077 to_clear.push(r);
1479 to_clear.push(r);
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
1488+ .filter(|&r| conflict_copies(&vault.tree, r).iter().all(|c| to_clear.contains(c)))
1081 // skip nodes under another node being cleared
1490 // skip nodes under another node being cleared
1082 let tops: Vec<NRef> = to_clear
1491 let tops: Vec<NRef> = to_clear
@@ -1097,10 +1506,17 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
1097 .collect();
1506 .collect();
1098 let mut blocks: Vec<Id> = Vec::new();
1507 let mut blocks: Vec<Id> = Vec::new();
1099 let mut plain: Vec<NRef> = Vec::new();
1508 let mut plain: Vec<NRef> = Vec::new();
1509+ let mut nested: Vec<Id> = Vec::new();
1100 for &r in &tops {
1510 for &r in &tops {
1101 let n = vault.tree.node(r);
1511 let n = vault.tree.node(r);
1102 match n.block.as_ref().and_then(|b| b.id.clone()) {
1512 match n.block.as_ref().and_then(|b| b.id.clone()) {
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));
1103− Some(id) => blocks.push(id),
1518+ blocks.push(id);
1104 None => plain.push(r),
1520 None => plain.push(r),
@@ -1109,13 +1525,14 @@ pub fn clear_done(vault: &mut Vault, target: NRef) -> std::io::Result<usize> {
1109 let mut done = 0;
1525 let mut done = 0;
1110 for r in plain {
1526 for r in plain {
1111 let ids = plain_remove(vault, r)?;
1527 let ids = plain_remove(vault, r)?;
1112− blocks.extend(ids);
1528+ nested.extend(ids);
1113 done += 1;
1529 done += 1;
1115 let n_block_tops = tops.len() - done;
1531 let n_block_tops = tops.len() - done;
1116 for id in blocks {
1532 for id in blocks {
1117 trash_block(vault, &id)?;
1533 trash_block(vault, &id)?;
1535+ trash_nested(vault, &nested)?;
1119 Ok(done + n_block_tops)
1536 Ok(done + n_block_tops)
@@ -1125,13 +1542,7 @@ fn resolved_parent(tree: &crate::tree::Tree, r: NRef) -> Option<NRef> {
1125 let p = tree.node(r).parent?;
1542 let p = tree.node(r).parent?;
1126 let pr = (r.0, p);
1543 let pr = (r.0, p);
1127 if tree.node(pr).kind == Kind::Root && r.0 != tree.root.0 {
1544 if tree.node(pr).kind == Kind::Root && r.0 != tree.root.0 {
1128− let id = tree.node(r).block.as_ref()?.id.clone()?;
1545+ tree.embed_of(tree.node(r).block.as_ref()?.id.as_ref()?)
1129− tree.files.iter().enumerate().find_map(|(fi, f)| {
1132− .position(|nd| nd.embed.as_ref() == Some(&id))
1133− .map(|ni| (fi, ni))
1135 } else if tree.node(pr).kind == Kind::Root {
1546 } else if tree.node(pr).kind == Kind::Root {
1137 } else {
1548 } else {
@@ -1155,14 +1566,28 @@ pub fn make_block(vault: &mut Vault, r: NRef) -> std::io::Result<Id> {
1155 file_text.push_str(&body);
1566 file_text.push_str(&body);
1156 let prefix = vault.unique_prefix(&id);
1567 let prefix = vault.unique_prefix(&id);
1157 let fname = crate::ident::filename(&prefix, &slug(&n.title));
1568 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
1572+ vault.check_unchanged(file)?;
1158− crate::vault::atomic_write(&vault.dir.join(&fname), &file_text)?;
1573+ let full = vault.dir.join(&fname);
1574+ crate::vault::atomic_write(&full, &file_text)?;
1159 // replace the node's span with an embed in the node's form (§6.1.3,
1575 // replace the node's span with an embed in the node's form (§6.1.3,
1160 // §4.7), keeping the blank lines that separated it from what follows
1576 // §4.7), keeping the blank lines that separated it from what follows
1162 let span = n.span;
1577 let span = n.span;
1163 let indent = " ".repeat(n.indent);
1578 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());
1164 let mut embed = match n.kind {
1586 let mut embed = match n.kind {
1165− Kind::Section => format!("{}{} ![[{}]]\n", indent, "#".repeat(vault.tree.level(r)), id),
1588+ let line = format!("{}{} ![[{}]]\n", indent, "#".repeat(vault.tree.level(r)), id);
1589+ level_among(&vault.tree, prev, next, &line)
1166 _ => format!("{}![[{}]]\n", indent, id),
1591 _ => format!("{}![[{}]]\n", indent, id),
1168 let old = span.text(&vault.tree.files[file].text);
1593 let old = span.text(&vault.tree.files[file].text);
@@ -1170,8 +1595,13 @@ pub fn make_block(vault: &mut Vault, r: NRef) -> std::io::Result<Id> {
1170 for _ in 1..trailing {
1595 for _ in 1..trailing {
1171 embed.push('\n');
1596 embed.push('\n');
1173− vault.write_span(file, span, &embed)?;
1598+ if let Err(e) = vault.write_span(file, span, &embed) {
1174− vault.reload()?;
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);
1604+ vault.add_file(&fname, &file_text);
@@ -1181,7 +1611,7 @@ pub fn set_property(vault: &mut Vault, r: NRef, key: &str, value: &str) -> std::
1181 let target = if vault.tree.node(r).is_block() {
1611 let target = if vault.tree.node(r).is_block() {
1183 } else if vault.tree.node(r).is_embed() {
1613 } else if vault.tree.node(r).is_embed() {
1184− vault.tree.resolved_child(r)
1614+ write_target(&vault.tree, r)?
1185 } else {
1615 } else {
1186 // make_block reloads, so `r` is stale: find the new block by its id
1616 // make_block reloads, so `r` is stale: find the new block by its id
1187 let id = make_block(vault, r)?;
1617 let id = make_block(vault, r)?;
@@ -1199,6 +1629,9 @@ pub fn set_property(vault: &mut Vault, r: NRef, key: &str, value: &str) -> std::
1200/// Move a node among its siblings (§10.3 `J`/`K`). A block moves by its
1630/// Move a node among its siblings (§10.3 `J`/`K`). A block moves by its
1201/// embed line, so blocks and plain nodes swap freely in the parent file.
1631/// 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.
1202pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<()> {
1635pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<()> {
1203 // a block root stands in its parent file as its embed
1636 // a block root stands in its parent file as its embed
1204 let r = stand_in(&vault.tree, r);
1637 let r = stand_in(&vault.tree, r);
@@ -1214,20 +1647,21 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
1214 Some(p) => p,
1647 Some(p) => p,
1215 None => return Ok(()),
1648 None => return Ok(()),
1217− let swap_with = if down {
1650+ let run = |i: usize| paired_run(&vault.tree, &siblings, i);
1218− pos + 1
1651+ let (a, b) = run(pos);
1652+ let other = if down {
1653+ if b + 1 >= siblings.len() {
1219 } else {
1657 } else {
1220− match pos.checked_sub(1) {
1658+ match a.checked_sub(1) {
1221− Some(p) => p,
1659+ Some(p) => run(p),
1222 None => return Ok(()),
1660 None => return Ok(()),
1225− if swap_with >= siblings.len() {
1228− let other = siblings[swap_with];
1229 // the ordering rule (§3.1): an item never goes below a section sibling
1663 // the ordering rule (§3.1): an item never goes below a section sibling
1230− let (rk, ok) = (vault.tree.node(r).kind, vault.tree.node(other).kind);
1664+ let (rk, ok) = (vault.tree.node(siblings[a]).kind, vault.tree.node(siblings[other.0]).kind);
1231 if rk != ok {
1665 if rk != ok {
1232 return Err(io_err(if rk == Kind::Item {
1666 return Err(io_err(if rk == Kind::Item {
1233 "an item cannot move below a section"
1667 "an item cannot move below a section"
@@ -1237,7 +1671,7 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
1238 let file = r.0;
1672 let file = r.0;
1239 let text = vault.tree.files[file].text.clone();
1673 let text = vault.tree.files[file].text.clone();
1240− // swap the nodes' own lines; trailing blank lines stay where they are,
1674+ // swap the runs' own lines; trailing blank lines stay where they are,
1241 // so the list's spacing is unchanged
1675 // so the list's spacing is unchanged
1242 let core = |sp: Span| {
1676 let core = |sp: Span| {
1243 let end = sp.end.min(text.len());
1677 let end = sp.end.min(text.len());
@@ -1247,9 +1681,12 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
1248 Span { start: sp.start, end: e }
1682 Span { start: sp.start, end: e }
1684+ let (fr, sr) = if down { ((a, b), other) } else { (other, (a, b)) };
1685+ let lines = |(s, e): (usize, usize)| Span {
1250− let a = core(vault.tree.node(r).span);
1686+ start: vault.tree.node(siblings[s]).span.start,
1251− let b = core(vault.tree.node(other).span);
1687+ end: core(vault.tree.node(siblings[e]).span).end,
1252− let (first, second) = if a.start < b.start { (a, b) } else { (b, a) };
1689+ let (first, second) = (lines(fr), lines(sr));
1253 let with_nl = |t: &str| {
1690 let with_nl = |t: &str| {
1254 if t.ends_with('\n') {
1691 if t.ends_with('\n') {
1255 t.to_string()
1692 t.to_string()
@@ -1257,18 +1694,95 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
1257 format!("{}\n", t)
1694 format!("{}\n", t)
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))
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];
1713+ render(&vault.tree, k, base, false)
1715+ .map(|l| if l.is_empty() { "\n".to_string() } else { format!("{}{}\n", pad, l) }),
1717+ // the blank lines between a node and its copy stay
1719+ let next = vault.tree.node(siblings[i + 1]).span.start;
1720+ out.push_str(&text[core(vault.tree.node(k).span).end..next]);
1260 let mut new_text = format!(
1725 let mut new_text = format!(
1261 "{}{}{}{}",
1726 "{}{}{}{}",
1262 &text[..first.start],
1727 &text[..first.start],
1263 with_nl(second.text(&text)),
1728 with_nl(second.text(&text)),
1264 &text[first.end..second.start],
1729 &text[first.end..second.start],
1265− with_nl(first.text(&text)),
1730+ lower,
1267 let rest = &text[second.end..];
1732 let rest = &text[second.end..];
1268 new_text.push_str(rest);
1733 new_text.push_str(rest);
1269 vault.write_file_text(file, &new_text)
1734 vault.write_file_text(file, &new_text)
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()
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) {
1747+ while a > 0 && is_copy(tree, kids[a]) {
1751+ while b + 1 < kids.len() && is_copy(tree, kids[b + 1]) {
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()
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()
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(),
1272/// Toggle spelling section ↔ item (§10.3 `~`). The checkbox, if any, is kept,
1786/// Toggle spelling section ↔ item (§10.3 `~`). The checkbox, if any, is kept,
1273/// and the subtree is re-indented and re-levelled for the new spelling. The
1787/// and the subtree is re-indented and re-levelled for the new spelling. The
1274/// node moves to its parent's boundary if it has to (§3.1), so no sibling
1788/// node moves to its parent's boundary if it has to (§3.1), so no sibling
@@ -1280,26 +1794,57 @@ pub fn toggle_spelling(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
1280 return Ok(false);
1794 return Ok(false);
1282 let to_section = n.kind == Kind::Item;
1796 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));
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)
1806+ return respell_embed(vault, r, to_section);
1808+ // `## ---` spelled as an item is `- ---`, a thematic break (§4.4)
1809+ if !to_section && n.task.is_none() {
1810+ item_line(&n.title)?;
1283 let stand = stand_in(&vault.tree, r);
1812 let stand = stand_in(&vault.tree, r);
1284 let file = r.0;
1813 let file = r.0;
1285 let text = vault.tree.files[file].text.clone();
1814 let text = vault.tree.files[file].text.clone();
1286 let span = Span { start: n.span.start, end: n.span.end.min(text.len()) };
1815 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)
1823+ .map(|l| l.text + "\n")
1825+ let src = shift_lines(&rendered, 0, n.indent as isize) + &"\n".repeat(blanks);
1287 if stand != r {
1826 if stand != r {
1288− // a block: respell its file's root at level 1, then its embed
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):
1831+ vault.check_unchanged(stand.0)?;
1289− let respelled = respell(span.text(&text), to_section, 1);
1832+ let respelled = respell(&src, to_section, 1);
1290 let id = n.block.as_ref().and_then(|b| b.id.clone());
1833 let id = n.block.as_ref().and_then(|b| b.id.clone());
1291 vault.write_span(file, span, &respelled)?;
1834 vault.write_span(file, span, &respelled)?;
1292 let Some(e) = id.and_then(|id| vault.tree.embed_of(&id)) else {
1835 let Some(e) = id.and_then(|id| vault.tree.embed_of(&id)) else {
1294 return Ok(false);
1836 return Ok(false);
1296 return respell_embed(vault, e, to_section);
1838 return respell_embed(vault, e, to_section);
1298 let Some(parent) = n.parent.map(|p| (file, p)) else { return Ok(false) };
1840 let Some(parent) = n.parent.map(|p| (file, p)) else { return Ok(false) };
1299− let respelled = respell(span.text(&text), to_section, vault.tree.level(parent) + 1);
1841+ let respelled = respell(&src, to_section, vault.tree.level(parent) + 1);
1300 reposition(vault, r, parent, respelled, to_section)
1842 reposition(vault, r, parent, respelled, to_section)
1845+/// Why `~` is refused on either side of a conflict pair (§12.5).
1846+pub const PAIR_SPELLING: &str = "resolve its conflict";
1303/// Rewrite an embed in the other form (§4.7) — heading for a section-spelled
1848/// Rewrite an embed in the other form (§4.7) — heading for a section-spelled
1304/// block, bare for an item — moving it to its parent's boundary if needed.
1849/// block, bare for an item — moving it to its parent's boundary if needed.
1305pub fn respell_embed(vault: &mut Vault, e: NRef, to_section: bool) -> std::io::Result<bool> {
1850pub fn respell_embed(vault: &mut Vault, e: NRef, to_section: bool) -> std::io::Result<bool> {
@@ -1335,11 +1880,12 @@ fn reposition(
1335 let kind = if to_section { Kind::Section } else { Kind::Item };
1880 let kind = if to_section { Kind::Section } else { Kind::Item };
1336 if clamp_index(tree, &others, pos, &[kind]) == pos {
1881 if clamp_index(tree, &others, pos, &[kind]) == pos {
1337 let span = tree.node(r).span;
1882 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);
1338 vault.write_span(r.0, span, &new)?;
1885 vault.write_span(r.0, span, &new)?;
1340 return Ok(false);
1886 return Ok(false);
1342− place(vault, Some(r), parent, pos, &new).map(|_| true)
1888+ place(vault, &[r], parent, pos, &new).map(|_| true)
1345/// A node's text respelled: its title line as a section at `level` or as an
1891/// A node's text respelled: its title line as a section at `level` or as an
@@ -1377,15 +1923,18 @@ fn respell(span_text: &str, to_section: bool, level: usize) -> String {
1378/// Demote: become the last child of the previous sibling node (§10.3 `>`),
1924/// Demote: become the last child of the previous sibling node (§10.3 `>`),
1379/// clamped by the ordering rule (§3.1).
1925/// clamped by the ordering rule (§3.1).
1380−pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
1926+pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1927+ let stayed = Placed::stayed(&vault.tree, r);
1381 let m = stand_in(&vault.tree, r);
1928 let m = stand_in(&vault.tree, r);
1382− let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(false) };
1929+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(stayed) };
1383 let kids = vault.tree.raw_children(parent);
1930 let kids = vault.tree.raw_children(parent);
1384− let Some(pos) = kids.iter().position(|&k| k == m) else { return Ok(false) };
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);
1385− if pos == 0 {
1934+ if a == 0 {
1386− return Ok(false);
1935+ return Ok(stayed);
1388− let prev = vault.tree.resolved_child(kids[pos - 1]);
1937+ let prev = vault.tree.resolved_child(kids[a - 1]);
1389 refile(vault, r, prev)
1938 refile(vault, r, prev)
@@ -1393,24 +1942,27 @@ pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
1393/// ordering rule (§3.1): an item leaving a section lands before the first
1942/// ordering rule (§3.1): an item leaving a section lands before the first
1394/// section among the parent's siblings. Out of a block's root, the node moves
1943/// section among the parent's siblings. Out of a block's root, the node moves
1395/// to the parent file beside the block's embed.
1944/// to the parent file beside the block's embed.
1396−pub fn promote(vault: &mut Vault, r: NRef) -> std::io::Result<bool> {
1945+pub fn promote(vault: &mut Vault, r: NRef) -> std::io::Result<Placed> {
1946+ let stayed = Placed::stayed(&vault.tree, r);
1397 let m = stand_in(&vault.tree, r);
1947 let m = stand_in(&vault.tree, r);
1398− let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(false) };
1948+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(stayed) };
1399 if vault.tree.node(parent).kind == Kind::Root {
1949 if vault.tree.node(parent).kind == Kind::Root {
1400− return Ok(false);
1950+ return Ok(stayed);
1402 // the parent's own position: its embed if it is a block root
1952 // the parent's own position: its embed if it is a block root
1403 let pstand = stand_in(&vault.tree, parent);
1953 let pstand = stand_in(&vault.tree, parent);
1404 let Some(grand) = vault.tree.node(pstand).parent.map(|p| (pstand.0, p)) else {
1954 let Some(grand) = vault.tree.node(pstand).parent.map(|p| (pstand.0, p)) else {
1405− return Ok(false);
1955+ return Ok(stayed);
1407 if vault.tree.node(grand).kind == Kind::Root && pstand.0 != vault.tree.root.0 {
1957 if vault.tree.node(grand).kind == Kind::Root && pstand.0 != vault.tree.root.0 {
1408 return Err(io_err("block has no embed"));
1958 return Err(io_err("block has no embed"));
1410 let pos = vault.tree.raw_children(grand).iter().position(|&k| k == pstand).unwrap_or(0);
1960 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)
1411− let rendered = render(&vault.tree, m, 1, false);
1962+ let run = pair_run(&vault.tree, m);
1412− let shifted = shift_document(&rendered, vault.tree.level(grand), child_indent(&vault.tree, grand));
1963+ let doc = run_doc(&vault.tree, &run, grand);
1413− place(vault, Some(m), grand, pos + 1, &shifted)
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)
1416/// Append a plain item child titled `title` under `parent`; returns the new
1968/// Append a plain item child titled `title` under `parent`; returns the new
@@ -1420,7 +1972,25 @@ pub fn append_child_public(
1420 parent: NRef,
1972 parent: NRef,
1421 title: &str,
1973 title: &str,
1422) -> std::io::Result<NRef> {
1974) -> std::io::Result<NRef> {
1423− append_child_line(vault, parent, &format!("- {}", title), true)
1975+ append_child_line(vault, parent, &item_line(title)?, true)
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)?;
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"));
1426fn io_err(msg: &str) -> std::io::Error {
1996fn io_err(msg: &str) -> std::io::Error {