From bed415459768814a7b789695b77438fa081b4e83 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 09:20:15 -0800 Subject: [PATCH] Links: Find Linked Files resolves parent folders in a saved path A saved link path such as `Disk:Projects:Art::photo.png` (classic Mac OS) or `/old/Projects/Art/../photo.png` names a file in `Projects`. Ranking the files found compared `Art` first, so a file under a folder named `Art` came before one under `Projects`. The `::` runs and `..` components are now resolved before the folder names are compared. --- crates/engine/src/cmd/linkfiles.rs | 23 +++++++++++++++++++-- crates/engine/src/tests/linkfiles.rs | 31 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/linkfiles.rs b/crates/engine/src/cmd/linkfiles.rs index 8341ea904..4601fdd33 100644 --- a/crates/engine/src/cmd/linkfiles.rs +++ b/crates/engine/src/cmd/linkfiles.rs @@ -177,9 +177,28 @@ fn find(s: &mut Session, p: &Value) -> Result { /// How many folder names the file at `found` has in common with the saved path `saved` (whichever /// system's separators it uses, [`vectorcraft_doc::links::path_separators`]), counted from the -/// innermost out up to the first that differs, compared in lowercase. +/// innermost out up to the first that differs, compared in lowercase. The saved path's parent +/// components are resolved first: `..` in a `/` or `\` path, and each colon after the first in a +/// run of colons in a classic Mac OS path (`Disk:Projects:Art::photo.png` is in `Projects`). fn shared_folders(saved: &str, found: &Path) -> usize { - let saved = saved.rsplit(vectorcraft_doc::links::path_separators(saved)).skip(1).filter(|c| !c.is_empty()); + let separators = vectorcraft_doc::links::path_separators(saved); + let classic = separators == [':']; + let mut up = 0usize; + let saved = saved.rsplit(separators).skip(1).filter(move |c| { + let parent = if classic { c.is_empty() } else { *c == ".." }; + if parent { + up += 1; + return false; + } + if c.is_empty() || *c == "." && !classic { + return false; + } + if up > 0 { + up -= 1; + return false; + } + true + }); let found = found.parent().into_iter().flat_map(|p| p.components().rev()).map_while(|c| match c { Component::Normal(n) => n.to_str(), _ => None, diff --git a/crates/engine/src/tests/linkfiles.rs b/crates/engine/src/tests/linkfiles.rs index 0d38cdacb..695ea4141 100644 --- a/crates/engine/src/tests/linkfiles.rs +++ b/crates/engine/src/tests/linkfiles.rs @@ -153,6 +153,37 @@ fn a_classic_mac_path_is_found_by_its_name_and_folders() { assert_eq!(found(&r, "photo.png", &root), files.map(|(p, k, c)| (p.to_string(), k.to_string(), c))); } +/// The folders a saved path shares with a file found are counted after its parent components are +/// resolved: `Art::` in a classic Mac OS path and `Art/..` in any other path go back to `Projects`. +#[test] +fn parent_components_in_a_saved_path_are_resolved_before_ranking() { + for (tag, saved) in [("classic-up", "Old Disk:Projects:Art::photo.png"), ("slash-up", "/old/Projects/Art/../photo.png")] { + let root = folder(tag); + let mut s = session_for(&root); + let photo = png(50, 30, BLUE); + write(&root.join("orig/photo.png"), &photo); + let id = placed(&mut s, &root.join("orig/photo.png")); + s.edit("saved path", |d, _| { + if let NodeKind::Image(im) = &mut d.node_mut(NodeId(id)).unwrap().kind { + im.link.as_mut().unwrap().path = saved.into(); + } + Ok(()) + }) + .unwrap(); + std::fs::remove_dir_all(root.join("orig")).unwrap(); + let search = root.join("search"); + let mut changed = photo.clone(); + *changed.last_mut().unwrap() ^= 1; + write(&search.join("Art/photo.png"), &changed); + write(&search.join("Projects/photo.png"), &changed); + s.execute("links.findFiles", &json!({"folder": search})).unwrap(); + let r = finished(&mut s); + assert_eq!(r["state"].as_str(), Some("done"), "{saved}: {r}"); + let files = [("search/Projects/photo.png", "name", false), ("search/Art/photo.png", "name", false)]; + assert_eq!(found(&r, "photo.png", &root), files.map(|(p, k, c)| (p.to_string(), k.to_string(), c)), "{saved}"); + } +} + /// Without `ids`, a search looks for the files that can't be found; with them, for those objects' /// files, whatever their status. #[test]