fix(spell): Don't crash on Unicode punctuation before a word (#120)

known_run stepped one byte past the delimiter before a word run, which lands inside a multi-byte character (Chinese punctuation, emoji) and panics when slicing. It now steps past the whole character. Regression test with Chinese punctuation and emoji.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Co-authored-by: zzxzz12345 <[email protected]>
Цей коміт міститься в:
胡翀таzzxzz12345 authored and GitHub committed 2026-10-28 14:15:47 +08:00
1 джерело 96be40cd84
коміт a10e3c8898
1 file changed
+10 -1
+10 -1
Переглянути файл
@@ -364,7 +364,8 @@ impl Speller {
/// Whether the letter run `w` was split from is a known term as a whole:
/// `IntelliJ` (`Intelli` + `J`), `PostgreSQL` (`Postgre` + `SQL`).
fn known_run(&self, text: &str, w: &WordSpan) -> bool {
let start = text[..w.start].rfind(|c: char| !c.is_alphabetic()).map_or(0, |i| i + 1);
let start =
text[..w.start].char_indices().rev().find(|(_, c)| !c.is_alphabetic()).map_or(0, |(i, c)| i + c.len_utf8());
let end = text[w.end..].find(|c: char| !c.is_alphabetic()).map_or(text.len(), |i| w.end + i);
(start, end) != (w.start, w.end) && self.user_word(&text[start..end].to_lowercase())
}
@@ -504,6 +505,14 @@ mod tests {
assert_eq!(typos[0].range, Range::new(Pos::new(0, 3), Pos::new(0, 10)));
}
#[test]
fn misspellings_after_unicode_punctuation_do_not_panic() {
let text = "// 状态:Recieve and 【mesage】 😀writting";
let typos = Speller::default().check(text, &whole(text, RegionKind::Comment), &|| false).unwrap();
let words: Vec<_> = typos.iter().map(|t| t.word.as_str()).collect();
assert_eq!(words, ["Recieve", "mesage", "writting"]);
}
#[test]
fn identifiers_forgive_case_and_terms() {
let s = Speller::default();