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
parent 411daefa68
commit cd8dfa72c2
No known key found for this signature in database
1 changed files with 18 additions and 3 deletions

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"]]);