diff --git a/crates/ide-spell/src/lib.rs b/crates/ide-spell/src/lib.rs index 3e4f8023f..330a55a68 100644 --- a/crates/ide-spell/src/lib.rs +++ b/crates/ide-spell/src/lib.rs @@ -18,7 +18,8 @@ use std::time::SystemTime; pub use words::{MIN_WORD_CHARS, WordSpan, words}; -/// Where a region's words come from; strings and comments understand escapes. +/// Where a region's words come from; strings understand escapes, comments and +/// prose skip backslash commands (`\brief`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RegionKind { Comment, diff --git a/crates/ide-spell/src/words.rs b/crates/ide-spell/src/words.rs index 430fb877f..8c345fca1 100644 --- a/crates/ide-spell/src/words.rs +++ b/crates/ide-spell/src/words.rs @@ -17,7 +17,11 @@ pub struct WordSpan { /// Words of `text[start..end]` in order, as absolute byte ranges. pub fn words(text: &str, start: usize, end: usize, kind: RegionKind, out: &mut Vec) { let Some(slice) = text.get(start..end) else { return }; - let escapes = !matches!(kind, RegionKind::Name); + let escapes = match kind { + RegionKind::Name => Escapes::None, + RegionKind::String => Escapes::Char, + RegionKind::Comment | RegionKind::Prose => Escapes::Word, + }; let mut chunk_start = None; for (i, c) in slice.char_indices().chain(std::iter::once((slice.len(), ' '))) { if c.is_whitespace() { @@ -30,6 +34,18 @@ pub fn words(text: &str, start: usize, end: usize, kind: RegionKind, out: &mut V } } +/// What a `\` hides from the spell checker. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Escapes { + /// Identifiers have no escapes. + None, + /// String literals: the escape letter (`\n`, `\t`), the rest of the word is text. + Char, + /// Comments and prose: the whole word, a Doxygen / TeX command (`\brief`, + /// `\param`) or an escape quoted in a comment (`\n`). + Word, +} + /// URLs, e-mail addresses and the like are not prose. fn skip_chunk(chunk: &str) -> bool { if chunk.contains("://") || chunk.starts_with("www.") { @@ -41,7 +57,7 @@ fn skip_chunk(chunk: &str) -> bool { } } -fn chunk(slice: &str, from: usize, to: usize, escapes: bool, base: usize, out: &mut Vec) { +fn chunk(slice: &str, from: usize, to: usize, escapes: Escapes, base: usize, out: &mut Vec) { let text = &slice[from..to]; if skip_chunk(text) { return; @@ -57,9 +73,15 @@ fn chunk(slice: &str, from: usize, to: usize, escapes: bool, base: usize, out: & if let Some(s) = run.take() { token(text, s, i, base + from, out); } - // `\n`, `\t`, `é`: the escape letter is not part of the next word. - if escapes && c == '\\' { - chars.next(); + if c == '\\' { + match escapes { + Escapes::None => {} + // `\n`, `\t`, `é`: the escape letter is not part of the next word. + Escapes::Char => { + chars.next(); + } + Escapes::Word => while chars.next_if(|&(_, c)| c.is_alphanumeric() || c == '_').is_some() {}, + } } } if let Some(s) = run { @@ -164,6 +186,14 @@ mod tests { assert_eq!(split("// 中文注释 naïve", RegionKind::Comment), ["naïve"]); } + #[test] + fn skips_backslash_commands_in_comments() { + assert_eq!(split("/// \\brief Returns the sum.", RegionKind::Comment), ["Returns", "the", "sum"]); + assert_eq!(split("@see \\ref Foo, \\param[in] count", RegionKind::Comment), ["see", "Foo", "count"]); + assert_eq!(split("prints \\nthe line", RegionKind::Comment), ["prints", "line"]); + assert_eq!(split("\\emph{word} text", RegionKind::Prose), ["word", "text"]); + } + #[test] fn keeps_apostrophes_inside_words() { assert_eq!(split("'don't' shouldn't", RegionKind::Comment), ["don't", "shouldn't"]);