use std::collections::HashSet;
use std::path::PathBuf;
use spellbook::Dictionary;
use super::dict;
use super::segment::{self, Word};
const MAX_SUGGESTIONS: usize = 7;
pub struct SpellChecker {
dicts: Vec<Dictionary>,
personal: HashSet<String>,
personal_path: Option<PathBuf>,
}
impl SpellChecker {
pub fn new(
dicts: Vec<Dictionary>,
personal: HashSet<String>,
personal_path: Option<PathBuf>,
) -> Self {
Self {
dicts,
personal,
personal_path,
}
}
pub fn is_enabled(&self) -> bool {
!self.dicts.is_empty()
}
pub fn check_word(&self, word: &str) -> bool {
self.known(word) || segment::strip_marks(word).is_some_and(|plain| self.known(&plain))
}
fn known(&self, word: &str) -> bool {
self.personal.contains(word) || self.dicts.iter().any(|d| d.check(word))
}
pub fn misspellings(&self, line: &str) -> Vec<(usize, usize)> {
if !self.is_enabled() {
return Vec::new();
}
segment::words(line)
.into_iter()
.filter(|w| !self.check_word(&w.text))
.map(|w| (w.start, w.end))
.collect()
}
pub fn misspelled_word_at(&self, line: &str, col: usize) -> Option<Word> {
if !self.is_enabled() {
return None;
}
segment::words(line)
.into_iter()
.find(|w| col >= w.start && col <= w.end && !self.check_word(&w.text))
}
pub fn suggest(&self, word: &str) -> Vec<String> {
let plain = segment::strip_marks(word);
let word = plain.as_deref().unwrap_or(word);
let mut out = Vec::new();
for dict in &self.dicts {
let mut local = Vec::new();
dict.suggest(word, &mut local);
for s in local {
if !out.contains(&s) {
out.push(s);
}
}
if out.len() >= MAX_SUGGESTIONS {
break;
}
}
out.truncate(MAX_SUGGESTIONS);
out
}
pub fn add_to_personal(&mut self, word: &str) -> std::io::Result<()> {
let plain = segment::strip_marks(word);
let word = plain.as_deref().unwrap_or(word);
if !self.personal.insert(word.to_string()) {
return Ok(()); }
if let Some(path) = &self.personal_path {
dict::append_personal(path, word)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const AFF: &str = "SET UTF-8\n";
const DIC: &str = "4\nhello\nworld\ncolour\nистинно\n";
fn checker() -> SpellChecker {
let dict = Dictionary::new(AFF, DIC).unwrap();
SpellChecker::new(vec![dict], HashSet::new(), None)
}
#[test]
fn known_and_unknown_words() {
let c = checker();
assert!(c.check_word("hello"));
assert!(!c.check_word("helo"));
}
#[test]
fn personal_word_accepted() {
let mut c = checker();
assert!(!c.check_word("мойтермин"));
c.add_to_personal("мойтермин").unwrap();
assert!(c.check_word("мойтермин"));
}
#[test]
fn misspellings_returns_ranges() {
let c = checker();
let bad = c.misspellings("hello helo world");
assert_eq!(bad, vec![(6, 10)]);
}
#[test]
fn a_url_is_not_underlined() {
let c = checker();
let line = "hello https://github.com/vshylov/mindfork-rs helo";
assert_eq!(c.misspellings(line), vec![(45, 49)]);
assert!(c.misspelled_word_at(line, 20).is_none());
}
#[test]
fn disabled_checker_flags_nothing() {
let c = SpellChecker::new(Vec::new(), HashSet::new(), None);
assert!(!c.is_enabled());
assert!(c.misspellings("definitely wrng words zzz").is_empty());
assert!(c.misspelled_word_at("wrng", 1).is_none());
}
#[test]
fn misspelled_word_at_cursor() {
let c = checker();
let line = "hello helo world";
let w = c.misspelled_word_at(line, 8).unwrap();
assert_eq!(w.text, "helo");
assert!(c.misspelled_word_at(line, 2).is_none());
}
#[test]
fn suggest_offers_alternatives() {
let c = checker();
let s = c.suggest("helo");
assert!(s.iter().any(|w| w == "hello"));
}
#[test]
fn a_stress_mark_neither_fails_a_word_nor_rescues_a_typo() {
let c = checker();
assert!(c.check_word("и\u{301}стинно"));
assert!(c.check_word("исти\u{301}нно"));
assert!(c.check_word("hello\u{301}"));
assert!(!c.check_word("исти\u{301}но"));
assert!(!c.check_word("helo\u{301}"));
}
#[test]
fn a_stressed_word_is_underlined_whole_or_not_at_all() {
let c = checker();
assert!(c.misspellings("и\u{301}стинно").is_empty());
assert_eq!(c.misspellings("исти\u{301}но"), vec![(0, 7)]);
let w = c.misspelled_word_at("исти\u{301}но", 2).unwrap();
assert_eq!(w.text, "исти\u{301}но");
}
#[test]
fn suggestions_come_from_the_unmarked_word() {
let c = checker();
assert_eq!(c.suggest("hel\u{301}o"), c.suggest("helo"));
assert!(c.suggest("hel\u{301}o").iter().any(|w| w == "hello"));
}
#[test]
fn a_personal_word_is_stored_without_its_marks() {
let mut c = checker();
c.add_to_personal("мойте\u{301}рмин").unwrap();
assert!(c.personal.contains("мойтермин"));
assert!(c.check_word("мо\u{301}йтермин"));
assert!(c.check_word("мойтермин"));
}
}