fix(proofing): use macOS NSSpellChecker for spelling on macOS

The bundled Hunspell dictionary (~49k SCOWL roots) lacked common words like
"cybersecurity", "scalable", and "assistantship", and checked words inside
URLs. On macOS, spelling now uses the system NSSpellChecker (same engine as
TextEdit/Safari): far better vocabulary, skips URLs, and respects words the
user has taught macOS. The custom/tech dictionaries and Harper grammar are
unchanged; non-macOS builds keep the Hunspell fallback.
This commit is contained in:
pj committed 2026-07-13 20:27:58 +05:30
1 parent 4975e76358
commit f11ddd290f
5 files changed
+138 -5

No files matched your search

+28 -3
View File
@@ -27,6 +27,7 @@ struct Harper {
}
pub struct Engine {
#[cfg(not(target_os = "macos"))]
speller: Option<spellbook::Dictionary>,
harper: Option<Harper>,
custom: HashSet<String>,
@@ -87,6 +88,7 @@ fn load_custom(app: &tauri::AppHandle) -> HashSet<String> {
set
}
#[cfg(not(target_os = "macos"))]
fn build_speller(app: &tauri::AppHandle) -> Result<spellbook::Dictionary, String> {
let aff = dict_file(app, "index.aff")?;
let dic = dict_file(app, "index.dic")?;
@@ -100,18 +102,22 @@ fn build_harper() -> Harper {
Harper { linter, dict }
}
#[cfg(not(target_os = "macos"))]
fn is_word_char(c: char) -> bool {
c.is_alphabetic()
}
#[cfg(not(target_os = "macos"))]
fn has_irregular_case(word: &str) -> bool {
word.chars().skip(1).any(|c| c.is_uppercase())
}
#[cfg(not(target_os = "macos"))]
fn is_apostrophe(c: char) -> bool {
c == '\'' || c == '\u{2019}'
}
#[cfg(not(target_os = "macos"))]
fn collect_spelling(
speller: &spellbook::Dictionary,
custom: &HashSet<String>,
@@ -143,6 +149,10 @@ fn collect_spelling(
}
let mut suggestions = Vec::new();
speller.suggest(&word, &mut suggestions);
let lower = word.to_lowercase();
if suggestions.iter().any(|s| s.to_lowercase() == lower) {
continue;
}
suggestions.truncate(5);
issues.push(Issue {
start,
@@ -197,6 +207,7 @@ pub async fn proof_text(
let mut custom = load_custom(&app);
load_tech(&app, &mut custom);
*guard = Some(Engine {
#[cfg(not(target_os = "macos"))]
speller: None,
harper: None,
custom,
@@ -207,10 +218,24 @@ pub async fn proof_text(
let chars: Vec<char> = text.chars().collect();
let mut issues = Vec::new();
if spelling {
if engine.speller.is_none() {
engine.speller = Some(build_speller(&app)?);
#[cfg(target_os = "macos")]
for mi in crate::macspell::check(&text, &engine.custom) {
issues.push(Issue {
start: mi.start,
end: mi.end,
kind: "spelling".into(),
category: "Spelling".into(),
message: format!("“{}” may be misspelled", mi.word),
suggestions: mi.suggestions,
});
}
#[cfg(not(target_os = "macos"))]
{
if engine.speller.is_none() {
engine.speller = Some(build_speller(&app)?);
}
collect_spelling(engine.speller.as_ref().unwrap(), &engine.custom, &chars, &mut issues);
}
collect_spelling(engine.speller.as_ref().unwrap(), &engine.custom, &chars, &mut issues);
}
if grammar {
if engine.harper.is_none() {