mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 02:59:17 -06:00
LRC-LIB-KEYWORDS: the keyword filter follows a rename without panicking
When a keyword was renamed or merged, the keyword filter followed it by cutting the old name off by bytes: names match whatever their case, and where a letter's case changes its length (ẞ / ß) the cut could land inside a letter and panic (`ẞẞA` → `Road` with the filter on `ßßa|é`). It now uses the catalog's level-based `reparent`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7029c8fbbd
commit
cdca2b9cf8
@@ -32,7 +32,7 @@ pub fn is_under(k: &str, parent: &str) -> bool {
|
||||
|
||||
/// Replace the `from` prefix of `k` (which [`is_under`] `from`) with `to`. By levels, not bytes:
|
||||
/// `from` may be written in another case, which can change a letter's length (ẞ / ß).
|
||||
fn reparent(k: &str, from: &str, to: &str) -> String {
|
||||
pub fn reparent(k: &str, from: &str, to: &str) -> String {
|
||||
let skip = from.split(SEP).filter(|s| !s.trim().is_empty()).count();
|
||||
let rest: Vec<&str> = k.split(SEP).filter(|s| !s.trim().is_empty()).skip(skip).collect();
|
||||
std::iter::once(to).filter(|t| !t.is_empty()).chain(rest).collect::<Vec<_>>().join("|")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Library-wide keyword commands: list (tree with counts), suggestions, rename, delete, merge;
|
||||
//! keyword sets (nine keywords a keystroke away: ⌥1–⌥9) and Recent Keywords.
|
||||
|
||||
use lightcraft_catalog::keywords::{clean, is_under};
|
||||
use lightcraft_catalog::keywords::{clean, is_under, reparent};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, cmd, str_param};
|
||||
@@ -197,9 +197,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let to = str_param(p, "to").ok_or_else(|| bad("keyword.rename", "missing `to`"))?.to_string();
|
||||
let op = s.catalog.rename_keyword_ops(&from, &to).map_err(|e| bad("keyword.rename", e.to_string()))?;
|
||||
let (f, t) = (clean(&from), clean(&to));
|
||||
commit_keywords(s, "Rename Keyword", op, |k| {
|
||||
Some(if is_under(k, &f) { format!("{t}{}", &k[f.len().min(k.len())..]) } else { k.to_string() })
|
||||
})
|
||||
commit_keywords(s, "Rename Keyword", op, |k| Some(if is_under(k, &f) { reparent(k, &f, &t) } else { k.to_string() }))
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
@@ -230,7 +228,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let (from, into) = (from.iter().map(|f| clean(f)).collect::<Vec<_>>(), clean(&into));
|
||||
commit_keywords(s, "Merge Keywords", op, |k| {
|
||||
Some(match from.iter().find(|f| is_under(k, f)) {
|
||||
Some(f) => format!("{into}{}", &k[f.len().min(k.len())..]),
|
||||
Some(f) => reparent(k, f, &into),
|
||||
None => k.to_string(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -859,6 +859,8 @@ mod tests_import;
|
||||
#[cfg(test)]
|
||||
mod tests_import_move;
|
||||
#[cfg(test)]
|
||||
mod tests_keyword_list;
|
||||
#[cfg(test)]
|
||||
mod tests_libops;
|
||||
#[cfg(test)]
|
||||
mod tests_library;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//! The keyword list's commands (Lightroom Classic's Keyword List): create, edit, move and purge
|
||||
//! keywords, their attributes, and the default parent for new keywords.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// The keyword filter follows a renamed keyword whatever the case it was typed in, also where a
|
||||
/// letter's case changes its length (ẞ / ß): it used to be cut by bytes, which could land inside
|
||||
/// a letter and panic.
|
||||
#[test]
|
||||
fn the_filter_follows_a_rename_whatever_the_case() {
|
||||
let mut s = Session::with_demo();
|
||||
let id = s.visible_cloned()[0].0;
|
||||
s.execute("photo.setMeta", &json!({"ids": [id], "keywords": ["ßßa|é"]})).unwrap();
|
||||
s.execute("library.filter", &json!({"keyword": "ßßa|é"})).unwrap();
|
||||
s.execute("keyword.rename", &json!({"from": "ẞẞA", "to": "Road"})).unwrap();
|
||||
assert_eq!(s.filter.keyword.as_deref(), Some("Road|é"));
|
||||
s.execute("keyword.merge", &json!({"from": ["ROAD"], "into": "Weg"})).unwrap();
|
||||
assert_eq!(s.filter.keyword.as_deref(), Some("Weg|é"));
|
||||
}
|
||||
Reference in New Issue
Block a user