From c01eaadbf36a993b0b8c1a64af0608eacd598b89 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 14:27:22 -0400 Subject: [PATCH] LRC-LIB-KEYWORDS: the parent of a listed keyword is a keyword too "Events" of a listed "Events|Weddings" is in the tree, but `has_keyword` and `keyword_path` only looked at photos and the listing itself: it couldn't be renamed, moved or made the default parent. They now find a keyword through the listed keywords below it as well. Co-Authored-By: Claude Opus 5.5 --- crates/catalog/src/keywords.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/catalog/src/keywords.rs b/crates/catalog/src/keywords.rs index d297d34b3..bf2aee088 100644 --- a/crates/catalog/src/keywords.rs +++ b/crates/catalog/src/keywords.rs @@ -311,10 +311,9 @@ impl Catalog { out } - /// The keyword is in the tree: listed, or on a photo (itself or one below it). + /// The keyword is in the tree: listed or on a photo, itself or one below it. pub fn has_keyword(&self, path: &str) -> bool { - let k = clean(path); - !k.is_empty() && (self.keyword_info(&k).is_some() || self.photos().any(|p| p.meta.keywords.iter().any(|x| is_under(x, &k)))) + self.keyword_path(path).is_some() } /// The keyword as written in the library (whatever the case of `path`): its listing, or the @@ -327,9 +326,11 @@ impl Catalog { if let Some(l) = self.keyword_list.get(&k.to_lowercase()) { return Some(l.path.clone()); } + // the levels of a listed keyword below it, or of a photo's let levels = k.split(SEP).count(); - self.photos() - .flat_map(|p| p.meta.keywords.iter()) + let listed = self.keyword_list.values().map(|l| &l.path); + listed + .chain(self.photos().flat_map(|p| p.meta.keywords.iter())) .find(|x| is_under(x, &k)) .map(|x| x.split(SEP).map(str::trim).filter(|s| !s.is_empty()).take(levels).collect::>().join("|")) } @@ -799,6 +800,19 @@ mod tests { assert!(c.create_keyword_ops("ärzte", KeywordInfo::default(), &[]).is_err(), "exists"); } + /// A keyword that is only the parent of a listed one ("Events" of "Events|Weddings") is a + /// keyword like any other: it is in the tree, so it can be found, moved and renamed. + #[test] + fn the_parent_of_a_listed_keyword_is_a_keyword() { + let mut c = Catalog::new(); + c.apply(Op::SetKeyword { path: "Events|Weddings".into(), info: Some(KeywordInfo::default()) }).unwrap(); + assert!(c.has_keyword("events")); + assert_eq!(c.keyword_path("EVENTS").as_deref(), Some("Events")); + let op = c.edit_keyword_ops("events", "Occasions", KeywordInfo::default()).unwrap(); + c.apply(op).unwrap(); + assert_eq!(listed(&c).iter().map(|(p, _)| p.as_str()).collect::>(), ["Occasions", "Occasions|Weddings"]); + } + #[test] fn suggestions_rank_co_occurrence_and_prefixes() { let (c, _) = lib(&[&["dog", "park"], &["dog", "park", "ball"], &["dog", "beach"], &["cat"], &["cat"], &["cat"], &["parade"], &["eagle"]]);