From a10e3c8898eb4a1562f408e7423935fefbf393d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BF=80?= <1020327414@qq.com> Date: Wed, 28 Oct 2026 14:15:47 +0800 Subject: [PATCH] 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 <8338183+zzxzz12345@users.noreply.github.com> --- crates/ide-spell/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/ide-spell/src/lib.rs b/crates/ide-spell/src/lib.rs index 4d5a51fbc..3e4f8023f 100644 --- a/crates/ide-spell/src/lib.rs +++ b/crates/ide-spell/src/lib.rs @@ -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();