From 234339a78681c634075336e442ffe1f3685d706b Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:47:07 +0000 Subject: [PATCH] Refuse pages in edit.copy and edit.cut Found while testing crates/engine/src/cmd/edit.rs:499 (copy_to_clipboard, called from copy at :536 and cut at :542) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/edit.rs | 11 +++++++++-- crates/engine/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 21f09569..eef4beaa 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -505,7 +505,12 @@ fn bump_name(n: &str) -> String { /// The copied form of the selection. pub fn copy_to_clipboard(s: &mut Session, ids: &[NodeId]) -> Result<()> { let ds = s.ds()?; - let (nodes, images, bounds) = copied(&ds.doc, ids); + // Pages are not layers: they can't be pasted anywhere. + let layers: Vec = ids.iter().copied().filter(|i| !ds.doc.node(*i).is_some_and(Node::is_page)).collect(); + if layers.is_empty() && !ids.is_empty() { + return Err(EngineError::Disabled("edit.copy".into(), "pages can't be copied; use page.duplicate".into())); + } + let (nodes, images, bounds) = copied(&ds.doc, &layers); let (source, source_path, uid) = (ds.doc.clone(), ds.path.clone(), ds.uid); s.clipboard.source = Some(source); s.clipboard.source_path = source_path; @@ -605,7 +610,9 @@ fn copy(s: &mut Session, p: &Value) -> Result { fn cut(s: &mut Session, p: &Value) -> Result { let ids = req_targets("edit.cut", s, p)?; copy_to_clipboard(s, &ids)?; - delete(s, &json!({"ids": ids})) + let doc = s.doc()?; + let layers: Vec = ids.into_iter().filter(|i| !doc.node(*i).is_some_and(Node::is_page)).collect(); + delete(s, &json!({"ids": layers})) } /// Paste the clipboard under `parent` with the copied top-left moved to `at` (page coords). diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index dbf432d4..6010559e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -621,3 +621,20 @@ fn find_replace_changing_only_the_case_replaces_every_occurrence() { assert_eq!(s.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, "FigCraft and FigCraft, FigCraft"); assert_eq!(s.doc().unwrap().node(f).unwrap().name, "FigCraft / FigCraft"); } + +#[test] +fn copy_and_cut_ignore_pages_so_paste_keeps_the_document_valid() { + for cmd in ["edit.copy", "edit.cut"] { + let mut s = sess(); + let page = s.page().unwrap(); + s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + let r = s.execute(cmd, &json!({"ids": [page.0]})); + if let Ok(v) = &r { + assert!(v.get("deleted").is_none_or(|d| d == &json!([])), "{cmd} reported the page as deleted: {v}"); + } + let _ = s.execute("edit.paste", &json!({})); + let doc = s.doc().unwrap(); + assert!(doc.validate().is_empty(), "{cmd}: {:?}", doc.validate()); + assert!(doc.node(page).is_some(), "{cmd} removed the page"); + } +}