LRC-LIB-KEYWORDS: renaming keeps the levels below whatever the case

Renaming a keyword cut the keywords below it at the byte length of the
name as typed. Names match whatever their case, but some letters change
length with it (ẞ is three bytes, ß two): `STRAẞE` → `Road` turned
`straße|nord` into `Roadnord`, and a cut inside a letter would panic.
The levels below are now kept whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 14:05:25 -04:00
co-authored by Claude Opus 5.5
parent 411daefa68
commit cd8dfa72c2
+18 -3
View File
@@ -30,10 +30,12 @@ pub fn is_under(k: &str, parent: &str) -> bool {
k == p || k.strip_prefix(&p).is_some_and(|rest| rest.starts_with(SEP))
}
/// Replace the `from` prefix of `k` (which [`is_under`] `from`) with `to`.
/// 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 {
let rest = &k[from.len().min(k.len())..];
if to.is_empty() { rest.trim_start_matches(SEP).to_string() } else { format!("{to}{rest}") }
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("|")
}
/// Keep the first of case-insensitively equal keywords.
@@ -478,6 +480,19 @@ mod tests {
assert_eq!(c.to_snapshot(), before);
}
/// Renaming matches names whatever their case, also where a letter's case changes its length
/// (ẞ is three bytes, ß two): the levels below are kept whole, never cut mid-letter.
#[test]
fn renaming_keeps_the_levels_below_whatever_the_case() {
let (mut c, ids) = lib(&[&["straße|nord"], &["ßa|ü"]]);
let op = c.rename_keyword_ops("STRAẞE", "Road").unwrap();
c.apply(op).unwrap();
assert_eq!(kws(&c, ids[0]), ["Road|nord"]);
let op = c.rename_keyword_ops("ẞA", "Weg").unwrap();
c.apply(op).unwrap();
assert_eq!(kws(&c, ids[1]), ["Weg|ü"]);
}
#[test]
fn suggestions_rank_co_occurrence_and_prefixes() {
let (c, _) = lib(&[&["dog", "park"], &["dog", "park", "ball"], &["dog", "beach"], &["cat"], &["cat"], &["cat"], &["parade"], &["eagle"]]);