@@ -654,31 +654,28 @@ pub fn yank(vault: &Vault, r: NRef) -> String {
654654 render(&vault.tree, r, 1, true)
655655}
656656
657−/// Paste rendered text after/before a node as siblings (§10.3 `p`/`P`).
657+/// Paste rendered text after/before a node as siblings (§10.3 `p`/`P`),
658+/// clamped by the ordering rule (§3.1).
658659pub fn paste(vault: &mut Vault, at: NRef, text: &str, after: bool) -> std::io::Result<()> {
659− let n = vault.tree.node(at);
660− if n.kind == Kind::Root {
660+ if vault.tree.node(at).kind == Kind::Root {
661661 return Err(io_err("cannot paste beside the root"));
662662 }
663− let file = at.0;
664− let indent = vault.tree.indent(at);
665− let level_base = vault.tree.level(at);
666− // re-indent/level the pasted text: parse it standalone and re-emit at
667− // the target's position.
668− let shifted = shift_document(text, level_base, indent);
669− if after {
670− let pos = trimmed_end(&vault.tree.files[file].text, n.span);
671− insert_at(vault, file, pos, "\n\n", &shifted)
672− } else {
673− let pos = n.span.start;
674− vault.write_span(file, Span { start: pos, end: pos }, &format!("{}\n", shifted))
675− }
663+ // a block root stands in its parent file as its embed
664+ let at = stand_in(&vault.tree, at);
665+ let Some(parent) = vault.tree.node(at).parent.map(|p| (at.0, p)) else {
666+ return Err(io_err("cannot paste beside the root"));
667+ };
668+ let pos = vault.tree.raw_children(parent).iter().position(|&k| k == at).unwrap_or(0);
669+ let shifted = shift_document(text, vault.tree.level(parent), child_indent(&vault.tree, parent));
670+ place(vault, None, parent, pos + after as usize, &shifted).map(|_| ())
676671}
677672
678−/// Parse a standalone document and re-emit it at the given level/indent.
679−/// `level` is the display level the document's *root* should take; `indent`
680−/// its indent. Everything inside keeps its position relative to the root.
681−pub fn shift_document(text: &str, level: usize, indent: usize) -> String {
673+/// Parse a standalone document and re-emit it as children of a node whose
674+/// level is `parent_level` (the root's is 0), at `indent`: a top-level
675+/// section becomes a level `parent_level + 1` heading, a top-level item sits
676+/// in a section of level `parent_level`. Everything inside keeps its position
677+/// relative to its top-level node.
678+pub fn shift_document(text: &str, parent_level: usize, indent: usize) -> String {
682679 let block = crate::parse::Block {
683680 id: None,
684681 path: "clip.md".into(),
@@ -702,13 +699,15 @@ pub fn shift_document(text: &str, level: usize, indent: usize) -> String {
702699 let r = render(&tree, *k, 1, true);
703700 let first = r.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
704701 let t = first.trim_start();
705− let cur_level = if t.starts_with('#') {
706− t.chars().take_while(|&c| c == '#').count()
702+ // render(_, 1) writes a section root at level 1 and an item root in
703+ // a section of level 1 (its nested sections at 2)
704+ let ld = if t.starts_with('#') {
705+ let cur_level = t.chars().take_while(|&c| c == '#').count();
706+ parent_level as isize + 1 - cur_level as isize
707707 } else {
708− 1
708+ parent_level as isize - 1
709709 };
710710 let cur_indent = first.len() - t.len();
711− let ld = level as isize - cur_level as isize;
712711 let id = indent as isize - cur_indent as isize;
713712 out.push_str(&shift_lines(&r, ld, id));
714713 }
@@ -784,6 +783,7 @@ fn fence_transition(raw: &str, open: &mut Option<(char, usize)>) -> bool {
784783
785784/// Refile: move a subtree under a new parent as its last child (§6.5).
786785pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<()> {
786+ let dest = vault.tree.resolved_child(dest);
787787 let n = vault.tree.node(r);
788788 // guard: cannot refile into own subtree — ancestry followed through
789789 // embeds, so a destination inside a nested block counts too
@@ -801,140 +801,164 @@ pub fn refile(vault: &mut Vault, r: NRef, dest: NRef) -> std::io::Result<()> {
801801 }
802802 anc = resolved_parent(&vault.tree, a);
803803 }
804− if n.is_block() || n.is_embed() {
805− // move only the embed line (§6.5)
806− let embed_ref = if n.is_embed() {
807− r
808− } else {
809− // find the embed node for this block
810− let id = n.block.as_ref().unwrap().id.clone().unwrap();
811− let mut embed_ref = None;
812− for (fi, f) in vault.tree.files.iter().enumerate() {
813− for (ni, nd) in f.nodes.iter().enumerate() {
814− if nd.embed.as_ref() == Some(&id) {
815− embed_ref = Some((fi, ni));
816− }
817− }
818− }
819− match embed_ref {
820− Some(e) => e,
821− None => return Err(io_err("block has no embed")),
822− }
823− };
824− // just the `![[id]]` line, without its old indent: insert_child_text
825− // indents it for the destination
826− let embed_text = {
827− let en = vault.tree.node(embed_ref);
828− en.title_span.text(&vault.tree.files[embed_ref.0].text).trim().to_string()
829− };
830− let dest_key = vault.key_of(dest);
831− let dest_path = vault.tree.path(dest);
832− // remove embed from source (no reload)
833− let src_file = embed_ref.0;
834− let span = vault.tree.node(embed_ref).span;
835− let src_text = vault.tree.files[src_file].text.clone();
836− remove_span_no_reload(vault, src_file, span)?;
837− let Some(dest) = vault
838− .find_by_key(&dest_key)
839− .or_else(|| vault.find_by_path(&dest_path))
840− else {
841− // put the source back rather than lose the embed
842− vault.write_file_text(src_file, &src_text)?;
843− return Err(io_err("destination lost"));
844− };
845− insert_child_text(vault, dest, &embed_text, true)
846− } else {
847− // plain node: same-file span surgery when possible (no reload, so
848− // sibling structure is preserved), cross-file otherwise.
849− let src_file = r.0;
850− // on-disk spelling: nested blocks travel as their embed lines
851− let rendered = render(&vault.tree, r, 1, false);
852− let span = vault.tree.node(r).span;
853− if dest.0 == src_file && !vault.tree.node(dest).is_embed() {
854− let text = vault.tree.files[src_file].text.clone();
855− let dest_span = vault.tree.node(dest).span;
856− let level = vault.tree.level(dest) + 1;
857− let indent = if vault.tree.node(dest).kind == Kind::Item {
858− vault.tree.indent(dest) + 2
859− } else {
860− vault.tree.indent(dest)
861− };
862− let shifted = shift_document(&rendered, level, indent);
863− let (start, end) = removal_range(&text, span);
864− let mut new_text = String::with_capacity(text.len());
865− new_text.push_str(&text[..start]);
866− new_text.push_str(&text[end..]);
867− // adjust insertion point for the removal
868− let mut ins = after_removal(dest_span.end.min(text.len()), start, end);
869− while ins > 0 && new_text.as_bytes()[ins - 1] == b'\n' {
870− ins -= 1;
804+ // a block moves by its embed line (§6.5); a plain node by its span, in
805+ // its on-disk spelling, so nested blocks travel as their embed lines
806+ let moving = stand_in(&vault.tree, r);
807+ if vault.tree.node(moving).is_block() {
808+ return Err(io_err("block has no embed"));
809+ }
810+ let rendered = render(&vault.tree, moving, 1, false);
811+ let shifted = shift_document(&rendered, vault.tree.level(dest), child_indent(&vault.tree, dest));
812+ place(vault, Some(moving), dest, usize::MAX, &shifted).map(|_| ())
813+}
814+
815+// ---------------------------------------------------------------- placement
816+
817+/// The node that stands for `r` among its parent's children in a file: a
818+/// block root's embed (§4.7), else `r` itself.
819+fn stand_in(tree: &crate::tree::Tree, r: NRef) -> NRef {
820+ if r.0 != tree.root.0 && tree.node(r).is_block() {
821+ if let Some(e) = resolved_parent(tree, r) {
822+ if tree.node(e).is_embed() {
823+ return e;
871824 }
872− let kids = vault.tree.resolved_children(dest);
873− let all_items = !kids.is_empty()
874− && kids.iter().all(|&k| vault.tree.node(k).kind == Kind::Item);
875− let first_is_item = shifted.trim_start().starts_with("- ");
876− let sep = if all_items && first_is_item { "\n" } else { "\n\n" };
877− new_text.insert_str(ins, &format!("{}{}", sep, shifted));
878− vault.write_file_text(src_file, &new_text)?;
879− vault.reload()
880− } else {
881− let dest_key = vault.key_of(dest);
882− let dest_path = vault.tree.path(dest);
883− let src_text = vault.tree.files[src_file].text.clone();
884− remove_span_with_separator(vault, src_file, span)?;
885− let Some(dest) = vault
886− .find_by_key(&dest_key)
887− .or_else(|| vault.find_by_path(&dest_path))
888− else {
889− // never lose the subtree: put the source back
890− vault.write_file_text(src_file, &src_text)?;
891− return Err(io_err("destination lost"));
892− };
893− insert_child_text(vault, dest, &rendered, false)
894825 }
895826 }
827+ r
828+}
829+
830+/// Indent of a child of `parent` (§3.1): items nest under items, everything
831+/// under a section stays at the section's own indent.
832+fn child_indent(tree: &crate::tree::Tree, parent: NRef) -> usize {
833+ match tree.node(parent).kind {
834+ Kind::Item => tree.indent(parent) + 2,
835+ Kind::Section => tree.indent(parent),
836+ Kind::Root => 0,
837+ }
838+}
839+
840+/// Spellings of a document's top-level nodes, in order. A heading embed is
841+/// a section, a bare embed an item (§4.7).
842+fn top_kinds(doc: &str) -> Vec<Kind> {
843+ let pf = crate::parse::parse_file("clip.md", doc, 0, None);
844+ pf.nodes[pf.root_node]
845+ .children
846+ .iter()
847+ .map(|&c| pf.nodes[c].kind)
848+ .collect()
849+}
850+
851+/// Clamp a wanted child index by the ordering rule `(text | item)*
852+/// section*` (§3.1): items no later than the first section child, sections
853+/// no earlier. `kids` are the parent's child nodes without the one moving.
854+fn clamp_index(tree: &crate::tree::Tree, kids: &[NRef], want: usize, kinds: &[Kind]) -> usize {
855+ let first_section = kids
856+ .iter()
857+ .position(|&k| tree.node(k).kind == Kind::Section)
858+ .unwrap_or(kids.len());
859+ let lo = if kinds.contains(&Kind::Section) { first_section } else { 0 };
860+ let hi = if kinds.contains(&Kind::Item) { first_section } else { kids.len() };
861+ want.min(kids.len()).clamp(lo, hi)
896862}
897863
898−/// Insert a rendered document as the last child of `parent`.
899−fn insert_child_text(
864+/// Write `doc` (already shifted to a child position of `parent`) as the
865+/// `want`-th child node of `parent`, clamped by the ordering rule (§3.1).
866+/// With `moving`, that node's own lines are removed in the same edit: text
867+/// children around it stay where they are. The insertion is written before
868+/// any removal in another file, so a failure never loses text. Returns the
869+/// index the document landed at among `parent`'s child nodes.
870+fn place(
900871 vault: &mut Vault,
872+ moving: Option<NRef>,
901873 parent: NRef,
902− text: &str,
903− is_embed: bool,
904−) -> std::io::Result<()> {
905− let node = vault.tree.node(parent);
874+ want: usize,
875+ doc: &str,
876+) -> std::io::Result<usize> {
877+ let tree = &vault.tree;
878+ let pnode = tree.node(parent);
879+ let kids: Vec<NRef> = tree
880+ .raw_children(parent)
881+ .into_iter()
882+ .filter(|&k| Some(k) != moving)
883+ .collect();
884+ let kinds = top_kinds(doc);
885+ if kinds.is_empty() {
886+ return Err(io_err("nothing to place"));
887+ }
888+ let idx = clamp_index(tree, &kids, want, &kinds);
889+ let first = kinds[0];
890+ let last = *kinds.last().unwrap();
906891 let file = parent.0;
907− // Items nest under items; everything under a section stays at the
908− // section's own indent (§3.1, §4.10).
909− let indent = if node.kind == Kind::Item {
910− vault.tree.indent(parent) + 2
911− } else {
912− vault.tree.indent(parent)
892+ let mut text = tree.files[file].text.clone();
893+ // the parent's content as it will be, without the moving node
894+ let content: Vec<crate::parse::Content> = pnode
895+ .content
896+ .iter()
897+ .copied()
898+ .filter(|c| !matches!(c, crate::parse::Content::Node(n) if Some((file, *n)) == moving))
899+ .collect();
900+ // positions in the original text, then adjusted for a same-file removal
901+ let next = kids.get(idx).copied();
902+ let mut pos = match next {
903+ Some(k) => tree.node(k).span.start,
904+ None => pnode.span.end.min(text.len()),
913905 };
914− let level = vault.tree.level(parent) + 1;
915− let shifted = if is_embed {
916− let mut s = String::new();
917− for line in text.trim_end_matches('\n').split('\n') {
918− s.push_str(&" ".repeat(indent));
919− s.push_str(line);
920− s.push('\n');
906+ // appending after the last child: is the list there tight?
907+ let tight_after = match content.last() {
908+ Some(crate::parse::Content::Node(k)) => {
909+ let kn = &tree.files[file].nodes[*k];
910+ kn.kind == Kind::Item && !kn.span.text(&text).ends_with("\n\n")
921911 }
922− s
923− } else {
924− shift_document(text, level, indent)
912+ Some(crate::parse::Content::Text(_)) => false,
913+ None => pnode.kind == Kind::Item,
914+ };
915+ let next_kind = next.map(|k| tree.node(k).kind);
916+ let removal = match moving {
917+ Some(m) if m.0 == file => Some(removal_range(&text, tree.node(m).span)),
918+ _ => None,
925919 };
926− // Separator: item children of an item-only list stay tight; sections and
927− // first children of a body-less node get a blank line, except in a block
928− // file whose spacing is the file's own (canonical: one blank).
929− let kids = vault.tree.resolved_children(parent);
930− let all_items = !kids.is_empty()
931− && kids.iter().all(|&k| vault.tree.node(k).kind == Kind::Item);
932− let first_line_is_item = shifted.trim_start().starts_with("- ");
933− let sep = if all_items && first_line_is_item { "\n" } else { "\n\n" };
934− let t = vault.tree.files[file].text.clone();
935− let pos = trimmed_end(&t, node.span);
936− insert_at(vault, file, pos, sep, &shifted)?;
937− vault.reload()
920+ if let Some((start, end)) = removal {
921+ text = format!("{}{}", &text[..start], &text[end..]);
922+ pos = after_removal(pos, start, end);
923+ }
924+ let body = doc.trim_end_matches('\n');
925+ let insertion = match next_kind {
926+ Some(nk) => {
927+ // before the next child node: keep a blank line before a
928+ // heading and after anything that is not a tight item run
929+ let lead = if first == Kind::Section && pos > 0 && !text[..pos].ends_with("\n\n") {
930+ "\n"
931+ } else {
932+ ""
933+ };
934+ let tight = last == Kind::Item && nk == Kind::Item && !text[..pos].ends_with("\n\n");
935+ format!("{}{}\n{}", lead, body, if tight { "" } else { "\n" })
936+ }
937+ None => {
938+ while pos > 0 && text.as_bytes()[pos - 1] == b'\n' {
939+ pos -= 1;
940+ }
941+ let sep = if pos == 0 {
942+ ""
943+ } else if first == Kind::Item && tight_after {
944+ "\n"
945+ } else {
946+ "\n\n"
947+ };
948+ let nl = if text[pos..].starts_with('\n') { "" } else { "\n" };
949+ format!("{}{}{}", sep, body, nl)
950+ }
951+ };
952+ text.insert_str(pos, &insertion);
953+ vault.write_file_text(file, &text)?;
954+ if let Some(m) = moving {
955+ if m.0 != file {
956+ // the other file's node refs are untouched by the write above
957+ remove_span_no_reload(vault, m.0, vault.tree.node(m).span)?;
958+ }
959+ }
960+ vault.reload()?;
961+ Ok(idx)
938962}
939963
940964/// Archive: refile under the top-level `Archive` section (§6.5).
@@ -1075,11 +1099,20 @@ pub fn make_block(vault: &mut Vault, r: NRef) -> std::io::Result<Id> {
10751099 let prefix = vault.unique_prefix(&id);
10761100 let fname = crate::ident::filename(&prefix, &slug(&n.title));
10771101 crate::vault::atomic_write(&vault.dir.join(&fname), &file_text)?;
1078− // replace the node's span with an embed (§6.1.3)
1102+ // replace the node's span with an embed in the node's form (§6.1.3,
1103+ // §4.7), keeping the blank lines that separated it from what follows
10791104 let file = r.0;
10801105 let span = n.span;
1081− let indent = vault.tree.indent(r);
1082− let embed = format!("{}![[{}]]\n", " ".repeat(indent), id);
1106+ let indent = " ".repeat(n.indent);
1107+ let mut embed = match n.kind {
1108+ Kind::Section => format!("{}{} ![[{}]]\n", indent, "#".repeat(vault.tree.level(r)), id),
1109+ _ => format!("{}![[{}]]\n", indent, id),
1110+ };
1111+ let old = span.text(&vault.tree.files[file].text);
1112+ let trailing = old.len() - old.trim_end_matches('\n').len();
1113+ for _ in 1..trailing {
1114+ embed.push('\n');
1115+ }
10831116 vault.write_span(file, span, &embed)?;
10841117 vault.reload()?;
10851118 Ok(id)
@@ -1109,13 +1142,10 @@ pub fn set_property(vault: &mut Vault, r: NRef, key: &str, value: &str) -> std::
11091142/// embed line, so blocks and plain nodes swap freely in the parent file.
11101143pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<()> {
11111144 // a block root stands in its parent file as its embed
1112− let r = match vault.tree.node(r).block.as_ref().and_then(|b| b.id.clone()) {
1113− Some(_) if r.0 != vault.tree.root.0 => match resolved_parent(&vault.tree, r) {
1114− Some(e) if vault.tree.node(e).is_embed() => e,
1115− _ => return Ok(()),
1116− },
1117− _ => r,
1118− };
1145+ let r = stand_in(&vault.tree, r);
1146+ if vault.tree.node(r).is_block() {
1147+ return Ok(());
1148+ }
11191149 let parent = match vault.tree.node(r).parent {
11201150 Some(p) => (r.0, p),
11211151 None => return Ok(()),
@@ -1137,6 +1167,15 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
11371167 return Ok(());
11381168 }
11391169 let other = siblings[swap_with];
1170+ // the ordering rule (§3.1): an item never goes below a section sibling
1171+ let (rk, ok) = (vault.tree.node(r).kind, vault.tree.node(other).kind);
1172+ if rk != ok {
1173+ return Err(io_err(if rk == Kind::Item {
1174+ "an item cannot move below a section"
1175+ } else {
1176+ "a section cannot move above an item"
1177+ }));
1178+ }
11401179 let file = r.0;
11411180 let text = vault.tree.files[file].text.clone();
11421181 // swap the nodes' own lines; trailing blank lines stay where they are,
@@ -1171,149 +1210,147 @@ pub fn move_sibling(vault: &mut Vault, r: NRef, down: bool) -> std::io::Result<(
11711210 vault.write_file_text(file, &new_text)
11721211}
11731212
1174−/// Toggle spelling section ↔ item (§10.3 `~`). The checkbox, if any, is kept.
1213+/// Toggle spelling section ↔ item (§10.3 `~`). The checkbox, if any, is kept,
1214+/// and the subtree is re-indented and re-levelled for the new spelling. The
1215+/// node moves to its parent's boundary if it has to (§3.1), so no sibling
1216+/// changes parent; a block's embed changes form and moves with it (§4.7).
11751217pub fn toggle_spelling(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
1218+ let r = vault.tree.resolved_child(r);
11761219 let n = vault.tree.node(r);
1177− if n.is_embed() || n.kind == Kind::Root {
1220+ if n.kind == Kind::Root {
11781221 return Ok(());
11791222 }
1223+ let to_section = n.kind == Kind::Item;
1224+ let stand = stand_in(&vault.tree, r);
11801225 let file = r.0;
1181− let ts = n.title_span;
1182− let line = ts.text(&vault.tree.files[file].text).to_string();
1183− let indent_str = &line[..line.len() - line.trim_start().len()];
1184− let trimmed = line.trim_start();
1185− // split off marker and checkbox
1186− let (after_marker, written_level) = if trimmed.starts_with('#') {
1187− let h = trimmed.chars().take_while(|&c| c == '#').count();
1188− (trimmed[h..].trim_start(), h)
1226+ let text = vault.tree.files[file].text.clone();
1227+ let span = Span { start: n.span.start, end: n.span.end.min(text.len()) };
1228+ if stand != r {
1229+ // a block: respell its file's root at level 1, then its embed
1230+ let respelled = respell(span.text(&text), to_section, 1);
1231+ let id = n.block.as_ref().and_then(|b| b.id.clone());
1232+ vault.write_span(file, span, &respelled)?;
1233+ let Some(e) = id.and_then(|id| vault.tree.embed_of(&id)) else {
1234+ return vault.reload();
1235+ };
1236+ return respell_embed(vault, e, to_section);
1237+ }
1238+ let Some(parent) = n.parent.map(|p| (file, p)) else { return Ok(()) };
1239+ let respelled = respell(span.text(&text), to_section, vault.tree.level(parent) + 1);
1240+ reposition(vault, r, parent, respelled, to_section)
1241+}
1242+
1243+/// Rewrite an embed in the other form (§4.7) — heading for a section-spelled
1244+/// block, bare for an item — moving it to its parent's boundary if needed.
1245+pub fn respell_embed(vault: &mut Vault, e: NRef, to_section: bool) -> std::io::Result<()> {
1246+ let en = vault.tree.node(e);
1247+ let Some(id) = en.embed.clone() else { return Ok(()) };
1248+ let Some(parent) = en.parent.map(|p| (e.0, p)) else { return Ok(()) };
1249+ let text = &vault.tree.files[e.0].text;
1250+ let old = Span { start: en.span.start, end: en.span.end.min(text.len()) }.text(text);
1251+ let rest = old.find('\n').map(|i| &old[i..]).unwrap_or("\n");
1252+ let indent = " ".repeat(en.indent);
1253+ let line = if to_section {
1254+ format!("{}{} ![[{}]]", indent, "#".repeat(vault.tree.level(parent) + 1), id)
11891255 } else {
1190− (trimmed.trim_start_matches("- "), 0)
1256+ format!("{}![[{}]]", indent, id)
11911257 };
1192− let (checkbox, title) = if let Some(rest) = after_marker.strip_prefix("[ ] ") {
1193− ("[ ] ", rest)
1194− } else if let Some(rest) = after_marker
1195− .strip_prefix("[x] ")
1196− .or_else(|| after_marker.strip_prefix("[X] "))
1197− .or_else(|| after_marker.strip_prefix("[-] "))
1198− {
1199− ("[x] ", rest)
1200− } else {
1201− ("", after_marker)
1258+ let new = format!("{}{}", line, rest);
1259+ reposition(vault, e, parent, new, to_section)
1260+}
1261+
1262+/// Replace `r`'s span with `new` (the same node, respelled), in place when
1263+/// the ordering rule allows it there, else at the parent's boundary.
1264+fn reposition(
1265+ vault: &mut Vault,
1266+ r: NRef,
1267+ parent: NRef,
1268+ new: String,
1269+ to_section: bool,
1270+) -> std::io::Result<()> {
1271+ let tree = &vault.tree;
1272+ let kids = tree.raw_children(parent);
1273+ let pos = kids.iter().position(|&k| k == r).unwrap_or(0);
1274+ let others: Vec<NRef> = kids.iter().copied().filter(|&k| k != r).collect();
1275+ let kind = if to_section { Kind::Section } else { Kind::Item };
1276+ if clamp_index(tree, &others, pos, &[kind]) == pos {
1277+ let span = tree.node(r).span;
1278+ vault.write_span(r.0, span, &new)?;
1279+ return vault.reload();
1280+ }
1281+ place(vault, Some(r), parent, pos, &new).map(|_| ())
1282+}
1283+
1284+
1285+/// A node's text respelled: its title line as a section at `level` or as an
1286+/// item, and everything under it shifted to match — an item's content sits
1287+/// two columns in, a section's at its own indent, and nested headings stay
1288+/// one below their new parent.
1289+fn respell(span_text: &str, to_section: bool, level: usize) -> String {
1290+ let (first, rest) = match span_text.find('\n') {
1291+ Some(i) => (&span_text[..i], &span_text[i..]),
1292+ None => (span_text, ""),
12021293 };
1203− let new_line = match n.kind {
1204− Kind::Section => format!("{}- {}{}", indent_str, checkbox, title),
1205− Kind::Item => {
1206− // item → section: deeper than the nearest section ancestor so
1207− // the parser keeps it a child (§3.1)
1208− let lvl = (written_level + 1).max(vault.tree.level(r) + 1).max(1);
1209− format!("{}{} {}{}", indent_str, "#".repeat(lvl), checkbox, title)
1294+ let indent_str = &first[..first.len() - first.trim_start().len()];
1295+ let (checkbox, title) = match checkbox_span(first) {
1296+ Some((s, e)) => {
1297+ let state = if &first[s..s + 3] == "[ ]" { "[ ] " } else { "[x] " };
1298+ (state, &first[e..])
12101299 }
1211− Kind::Root => unreachable!(),
1300+ // an empty title (`-`, `#`) has no marker end
1301+ None => ("", marker_end(first).map(|m| &first[m..]).unwrap_or("")),
1302+ };
1303+ let line = if to_section {
1304+ format!("{}{} {}{}", indent_str, "#".repeat(level.max(1)), checkbox, title)
1305+ } else {
1306+ format!("{}- {}{}", indent_str, checkbox, title)
12121307 };
1213− vault.write_span(file, ts, &new_line)
1308+ let (dl, di) = if to_section { (1, -2) } else { (-1, 2) };
1309+ // the rest starts with the newline that ends the title line
1310+ let shifted = shift_lines(&rest[1.min(rest.len())..], dl, di);
1311+ if rest.is_empty() {
1312+ line
1313+ } else {
1314+ format!("{}\n{}", line.trim_end(), shifted)
1315+ }
12141316}
12151317
1216−/// Demote: become the last child of the previous sibling (§10.3 `>`).
1217−/// Same-file span surgery, no reload.
1318+/// Demote: become the last child of the previous sibling node (§10.3 `>`),
1319+/// clamped by the ordering rule (§3.1).
12181320pub fn demote(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
1219− let parent = match vault.tree.node(r).parent {
1220− Some(p) => (r.0, p),
1221− None => return Ok(()),
1222− };
1223− let siblings = vault.tree.resolved_children(parent);
1224− let pos = match siblings.iter().position(|&s| s == r) {
1225− Some(p) => p,
1226− None => return Ok(()),
1227− };
1321+ let m = stand_in(&vault.tree, r);
1322+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(()) };
1323+ let kids = vault.tree.raw_children(parent);
1324+ let Some(pos) = kids.iter().position(|&k| k == m) else { return Ok(()) };
12281325 if pos == 0 {
12291326 return Ok(());
12301327 }
1231− let prev = siblings[pos - 1];
1232− if prev.0 != r.0 || vault.tree.node(r).is_embed() || vault.tree.node(prev).is_embed() {
1233− // cross-file or embed demotion falls back to refile
1234− return refile(vault, r, prev);
1235− }
1236− // Sections and items nest differently: delegate to the same logic refile
1237− // uses so the shifted text is right for the spelling.
1238− return refile(vault, r, prev);
1328+ let prev = vault.tree.resolved_child(kids[pos - 1]);
1329+ refile(vault, r, prev)
12391330}
12401331
1241−/// Promote: become the next sibling of the parent (§10.3 `<`). Same-file
1242−/// span surgery; out of a block's root, the node moves to the parent file
1243−/// right after the block's embed.
1332+/// Promote: become the next sibling of the parent (§10.3 `<`), clamped by the
1333+/// ordering rule (§3.1): an item leaving a section lands before the first
1334+/// section among the parent's siblings. Out of a block's root, the node moves
1335+/// to the parent file beside the block's embed.
12441336pub fn promote(vault: &mut Vault, r: NRef) -> std::io::Result<()> {
1245− let n = vault.tree.node(r);
1246− let parent = match n.parent {
1247− Some(p) => (r.0, p),
1248− None => return Ok(()),
1249− };
1337+ let m = stand_in(&vault.tree, r);
1338+ let Some(parent) = vault.tree.node(m).parent.map(|p| (m.0, p)) else { return Ok(()) };
12501339 if vault.tree.node(parent).kind == Kind::Root {
12511340 return Ok(());
12521341 }
1253− let file = r.0;
1254− // on-disk spelling: nested blocks travel as their embed lines
1255− let rendered = render(&vault.tree, r, 1, false);
1256− let span = n.span;
1257− let text = vault.tree.files[file].text.clone();
1258− let (start, end) = removal_range(&text, span);
1259− let mut new_text = String::with_capacity(text.len());
1260− new_text.push_str(&text[..start]);
1261− new_text.push_str(&text[end..]);
1262− if vault.tree.node(parent).is_block() && file != vault.tree.root.0 {
1263− // the parent is a block root: its sibling position is beside the embed
1264− let Some(embed) = resolved_parent(&vault.tree, parent) else {
1265− return Err(io_err("block has no embed"));
1266− };
1267− // a section beside the embed sits one below the embed's enclosing
1268− // section (the embed line itself is spelled as an item)
1269− let in_section = vault
1270− .tree
1271− .ancestors(embed)
1272− .iter()
1273− .any(|&a| vault.tree.node(a).kind == Kind::Section);
1274− let level = if vault.tree.node(r).kind == Kind::Section && in_section {
1275− vault.tree.level(embed) + 1
1276− } else {
1277− vault.tree.level(embed).max(1)
1278− };
1279− let indent = vault.tree.indent(embed);
1280− let shifted = shift_document(&rendered, level, indent);
1281− let efile = embed.0;
1282− let espan = vault.tree.node(embed).title_span;
1283− let etext = vault.tree.files[efile].text.clone();
1284− let ins = (espan.end + 1).min(etext.len());
1285− let tight = vault.tree.node(r).kind == Kind::Item
1286− && vault.tree.node(parent).kind == Kind::Item;
1287− let mut out = String::with_capacity(etext.len() + shifted.len() + 2);
1288− out.push_str(&etext[..ins]);
1289− if !out.ends_with('\n') {
1290− out.push('\n');
1291− }
1292− if !tight {
1293− out.push('\n');
1294− }
1295− out.push_str(&shifted);
1296− let after = &etext[ins..];
1297− if !tight && !after.is_empty() && !after.starts_with('\n') {
1298− out.push('\n');
1299− }
1300− out.push_str(after);
1301− vault.write_file_text(file, &new_text)?;
1302− vault.write_file_text(efile, &out)?;
1303− return vault.reload();
1304− }
1305− let parent_span = vault.tree.node(parent).span;
1306− let level = vault.tree.level(parent);
1307− let indent = vault.tree.indent(parent);
1308− let shifted = shift_document(&rendered, level, indent);
1309− // the parent contains r, so its span end moves back by the removed bytes
1310− let mut ins = after_removal(parent_span.end.min(text.len()), start, end);
1311− while ins > 0 && new_text.as_bytes()[ins - 1] == b'\n' {
1312− ins -= 1;
1342+ // the parent's own position: its embed if it is a block root
1343+ let pstand = stand_in(&vault.tree, parent);
1344+ let Some(grand) = vault.tree.node(pstand).parent.map(|p| (pstand.0, p)) else {
1345+ return Ok(());
1346+ };
1347+ if vault.tree.node(grand).kind == Kind::Root && pstand.0 != vault.tree.root.0 {
1348+ return Err(io_err("block has no embed"));
13131349 }
1314− new_text.insert_str(ins, &format!("\n\n{}", shifted));
1315− vault.write_file_text(file, &new_text)?;
1316− vault.reload()
1350+ let pos = vault.tree.raw_children(grand).iter().position(|&k| k == pstand).unwrap_or(0);
1351+ let rendered = render(&vault.tree, m, 1, false);
1352+ let shifted = shift_document(&rendered, vault.tree.level(grand), child_indent(&vault.tree, grand));
1353+ place(vault, Some(m), grand, pos + 1, &shifted).map(|_| ())
13171354}
13181355
13191356/// Rename a node's title (used by the editor when it detects a title change,