diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index f3cd7b6b6..4573ea2c2 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -858,8 +858,9 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p // of other keys linking to the same file share them, rather than each reading, decoding and // keeping a copy of its own. let mut read_images: BTreeMap = BTreeMap::new(); - // (the link as it was, the link now) of each file found away from its path or read again. - let mut moved: Vec<(LinkInfo, LinkInfo)> = vec![]; + // (the path as it was, the content shown, a placed document?, the link now) of each file found + // away from its path or read again. + let mut moved: Vec<(String, String, bool, LinkInfo)> = vec![]; for g in groups(d, None) { let row = rows(std::iter::once(&g), doc_path); let unread = g.document.is_none() && g.link.hash.is_none() && g.link.size.is_none() && previewing(d, &g.key); @@ -892,7 +893,7 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p }; if link != g.link { set_links(d, &g.ids, &link); - moved.push((g.link.clone(), link)); + moved.push((g.link.path.clone(), g.key.clone(), true, link)); } } } @@ -904,7 +905,7 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p let link = f.link_hashed(hash, &g.link); if link != g.link { set_links(d, &g.ids, &link); - moved.push((g.link.clone(), link)); + moved.push((g.link.path.clone(), g.key.clone(), false, link)); } continue; } @@ -930,27 +931,33 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p }; if link != g.link { set_links(d, &g.ids, &link); - moved.push((g.link.clone(), link)); + moved.push((g.link.path.clone(), g.key.clone(), false, link)); } } (Status::Ok, None) => {} } } - // The same links in symbols, graph designs, patterns and opacity masks, which have no ids here. + // The objects in symbols, graph designs, patterns and opacity masks that show the same file and + // content, which have no ids here, whatever else their links hold. if !moved.is_empty() { - let mut by_path: BTreeMap> = BTreeMap::new(); - for (old, new) in moved { - by_path.entry(old.path.clone()).or_default().push((old, new)); + let mut by_path: BTreeMap> = BTreeMap::new(); + for (path, key, document, new) in moved { + by_path.entry(path).or_default().push((key, document, new)); } - let new = |l: &LinkInfo| by_path.get(&l.path).and_then(|m| m.iter().find(|(old, _)| old == l)).map(|(_, new)| new); - d.update_links_where( - |l| new(l).is_some(), - |l| { - if let Some(n) = new(l) { - *l = n.clone(); - } - }, - ); + let new = |n: &Node| { + let (l, key, document) = linked(n)?; + let m = by_path.get(&l.path)?; + m.iter().find(|(k, doc, _)| k == key && *doc == document.is_some()).map(|(_, _, new)| new) + }; + let pick = |n: &Node| new(n).is_some(); + let mut make = |n: &Node| { + let mut out = n.clone(); + if let (Some(link), Some(l)) = (new(n), link_mut(&mut out)) { + *l = link.clone(); + } + (out, None) + }; + Nested { pick: &pick, make: &mut make, next: 0 }.run(d); } out } diff --git a/crates/engine/src/tests/links.rs b/crates/engine/src/tests/links.rs index 07db9e009..fd1217c10 100644 --- a/crates/engine/src/tests/links.rs +++ b/crates/engine/src/tests/links.rs @@ -342,6 +342,48 @@ fn a_symbols_link_to_a_moved_and_modified_file_takes_its_new_path() { assert_eq!(images, [new.clone(), new], "the layers' image, then the symbol's"); } +/// A link inside a symbol that the document records without a relative path, beside a link in the +/// layers to the same file that has one, takes the file's new path after the folder moves. +#[test] +fn a_symbols_link_without_a_relative_path_follows_the_moved_file() { + let a = Folder::new("nested-unrelative-a"); + let b = Folder::new("nested-unrelative-b"); + let pic = a.file("art/photo.png"); + write(&pic, &png(20, 10, RED)); + let mut s = session(); + let id = place(&mut s, &pic); + s.execute("symbol.new", &json!({"name": "Mark", "ids": [id.0]})).unwrap(); + place(&mut s, &pic); + let doc_path = a.file("poster.vectorcraft"); + save(&mut s, &doc_path); + // Removes the relative path of every link under `v` → how many it removed. + fn unrelative(v: &mut Value) -> usize { + match v { + Value::Object(o) => { + let own = match o.get_mut("link") { + Some(Value::Object(l)) => usize::from(l.remove("relative").is_some()), + _ => 0, + }; + own + o.values_mut().map(unrelative).sum::() + } + Value::Array(a) => a.iter_mut().map(unrelative).sum(), + _ => 0, + } + } + let mut saved: Value = serde_json::from_slice(&std::fs::read(&doc_path).unwrap()).unwrap(); + assert_eq!(unrelative(&mut saved["document"]["symbols"]), 1, "the symbol's link"); + assert_eq!(unrelative(&mut saved["document"]["layers"].clone()), 1, "the layers' link keeps its relative path"); + std::fs::write(&doc_path, serde_json::to_vec(&saved).unwrap()).unwrap(); + std::fs::remove_dir_all(&b.0).unwrap(); + std::fs::rename(&a.0, &b.0).unwrap(); + let r = open(&mut s, &b.file("poster.vectorcraft")); + assert_eq!((r["missingLinks"].clone(), r["modifiedLinks"].clone()), (json!([]), json!([])), "{r}"); + let mut paths = vec![]; + s.doc().unwrap().doc.visit_images(|_, im| paths.push(PathBuf::from(&im.link.as_ref().unwrap().path))); + let moved = PathBuf::from(b.file("art/photo.png")); + assert_eq!(paths, [moved.clone(), moved], "the layers' link, then the symbol's"); +} + /// Update Links reads a changed file again for the images inside symbols, patterns and opacity /// masks too: those showing the same content as an image in the layers, and those no object in the /// layers shows. Each takes the file's place as its own Placement Options say, in one undo step.