diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 7c6ad50e7..616d95557 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -7056,14 +7056,22 @@ fn dictionaries_folder_option_is_saved_searched_and_reported() { assert_eq!(r["dictionariesFolder"], json!(dir.to_string_lossy())); let dicts = r["dictionaries"].as_array().unwrap(); let file = |lang: &str| dicts.iter().find(|d| d["language"] == lang).map(|d| d["file"].clone()); - assert_eq!(file("pl"), Some(json!(dir.join("pl_PL.dic").to_string_lossy()))); - assert_eq!(file("pt"), Some(json!(dir.join("pt_BR.dic").to_string_lossy()))); - assert_eq!(file("id"), Some(json!(dir.join("id_ID.dic").to_string_lossy()))); + // A language reports the folder's file, or none while a dictionary another test gave with + // install_dictionary() is in use (those tests run concurrently); the folder is searched first + // either way. + let reported = |got: Option<&serde_json::Value>, name: &str| { + let want = dir.join(format!("{name}.dic")); + assert!(got.is_some_and(|f| f.is_null() || *f == json!(want.to_string_lossy())), "{name}: {dicts:?}"); + assert_eq!(wordcraft_proof::hunspell::find(&[name]).map(|(_, dic)| dic), Some(want)); + }; + reported(file("pl").as_ref(), "pl_PL"); + reported(file("pt").as_ref(), "pt_BR"); + reported(file("id").as_ref(), "id_ID"); // Each Spanish variety reports its own file (Spain, Mexico, Latin America, Uruguay, in order). let spanish: Vec = dicts.iter().filter(|d| d["language"] == "es").map(|d| d["file"].clone()).collect(); assert_eq!(spanish.len(), 4); - assert_eq!(spanish.first(), Some(&json!(dir.join("es_ES.dic").to_string_lossy()))); - assert_eq!(spanish.last(), Some(&json!(dir.join("es_UY.dic").to_string_lossy()))); + reported(spanish.first(), "es_ES"); + reported(spanish.last(), "es_UY"); // Polish text is checked against the folder's dictionary. run(&mut s, "document.setText", json!({"text": "rzeka płynie żeka"})); run(&mut s, "select.all", json!({})); diff --git a/crates/proof/src/cs/mod.rs b/crates/proof/src/cs/mod.rs index fee7c561c..55e21aef6 100644 --- a/crates/proof/src/cs/mod.rs +++ b/crates/proof/src/cs/mod.rs @@ -7,83 +7,47 @@ pub(crate) mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Czech dictionary and whether it came from [`install_dictionary`] rather than a file on -/// disk (it then has no file of its own to report from [`installed_path`], even if a matching -/// file also happens to exist): one lock, so a reload and an install can't interleave their two -/// fields into an inconsistent combination. -static DICT: RwLock = RwLock::new(DictState { dict: None, in_memory: false }); - -struct DictState { - dict: Option>, - in_memory: bool, -} - /// File names of a Czech Hunspell dictionary. const NAMES: &[&str] = &["cs_CZ", "cs-CZ", "cs"]; +/// The Czech dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Czech"); + /// The Czech dictionary: the installed Hunspell `cs_CZ` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available (Czech spelling is off /// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).dict.as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.dict.get_or_insert(loaded).clone() -} - -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Czech spelling: no Hunspell cs_CZ dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Czech spelling: {e}"); - AffixDict::default() - }) + DICT.get() } /// Use this Czech dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to /// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - slot.dict = Some(Arc::new(d)); - slot.in_memory = true; - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - slot.dict = None; - slot.in_memory = false; + DICT.reload(); } -/// The `.dic` file of the installed Czech dictionary, searching the dictionaries folder and -/// the system locations now (File › Options › Proofing shows it, #3226); `None` when there is -/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported, even if a -/// matching file also happens to exist. +/// The `.dic` file of the installed Czech dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - if DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).in_memory { - return None; - } - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Czech dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `c` a letter of the Latin script (the only one the Czech dictionary covers)? diff --git a/crates/proof/src/es/mod.rs b/crates/proof/src/es/mod.rs index 94c6511ab..253642c83 100644 --- a/crates/proof/src/es/mod.rs +++ b/crates/proof/src/es/mod.rs @@ -16,16 +16,19 @@ mod suggest; pub(crate) mod test_dict; pub mod voseo; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::ProofLang; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The dictionary of each variety (Spain, Mexico, Latin America, Uruguay): `None` until first -/// use, then the installed one (or an empty one). -static DICTS: [RwLock>>; 4] = [const { RwLock::new(None) }; 4]; +/// The dictionary of each variety, found on first use (see [`DictSlot`]). +static SPAIN_DICT: DictSlot = DictSlot::new(SPAIN, "Spanish (Spain)"); +static MEXICO_DICT: DictSlot = DictSlot::new(MEXICO, "Spanish (Mexico)"); +static LATIN_AMERICA_DICT: DictSlot = DictSlot::new(LATIN_AMERICA, "Spanish (Latin America)"); +static URUGUAY_DICT: DictSlot = DictSlot::new(URUGUAY, "Spanish (Uruguay)"); /// File names of a Spanish Hunspell dictionary for Spain, most specific first (`es_ANY` is the /// generic one RLA-ES publishes). @@ -40,79 +43,53 @@ const LATIN_AMERICA: &[&str] = &["es_419", "es-419", "es_ANY", "es", "es_US", "e /// or generic one. const URUGUAY: &[&str] = &["es_UY", "es-UY", "es_AR", "es-AR", "es_419", "es-419", "es_ANY", "es"]; -/// `lang`'s variety: its slot in [`DICTS`], its English name (for the log) and its dictionary's -/// file names; Spain for any other language. -fn variety(lang: ProofLang) -> (usize, &'static str, &'static [&'static str]) { +/// `lang`'s variety's dictionary; Spain's for any other language. +fn slot(lang: ProofLang) -> &'static DictSlot { match lang { - ProofLang::EsMx => (1, "Spanish (Mexico)", MEXICO), - ProofLang::Es419 => (2, "Spanish (Latin America)", LATIN_AMERICA), - ProofLang::EsUy => (3, "Spanish (Uruguay)", URUGUAY), - _ => (0, "Spanish (Spain)", SPAIN), + ProofLang::EsMx => &MEXICO_DICT, + ProofLang::Es419 => &LATIN_AMERICA_DICT, + ProofLang::EsUy => &URUGUAY_DICT, + _ => &SPAIN_DICT, } } /// The Hunspell name each variety's dictionary is known by (`es_ES`, `es_MX`, `es_419`, /// `es_UY`): the first of its file names, used for the status-bar hint and load errors. pub fn dictionary_name(lang: ProofLang) -> &'static str { - variety(lang).2.first().copied().unwrap_or("es_ES") + slot(lang).name() } /// The Spanish dictionary of `lang`'s variety: the installed one (read on first use), one given /// with [`install_dictionary`], or an empty one when none is available (spelling is off then; /// see [`has_dictionary`]). pub fn dictionary(lang: ProofLang) -> Arc { - let Some(slot) = DICTS.get(variety(lang).0) else { return Arc::new(AffixDict::default()) }; - if let Some(d) = slot.read().unwrap_or_else(|e| e.into_inner()).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed(lang)); - let mut slot = slot.write().unwrap_or_else(|e| e.into_inner()); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() -} - -fn load_installed(lang: ProofLang) -> AffixDict { - let (_, name, files) = variety(lang); - let key = dictionary_name(lang); - let Some((aff, dic)) = crate::hunspell::find(files) else { - crate::hunspell::clear_load_error(key); - log::info!("{name} spelling: no Hunspell {key} dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(key, &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("{name} spelling: {e}"); - AffixDict::default() - }) + slot(lang).get() } /// Use this dictionary for `lang`'s variety from now on (Hunspell `.aff` and `.dic` text, /// decoded to UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(lang: ProofLang, aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - if let Some(slot) = DICTS.get(variety(lang).0) { - *slot.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(d)); - } - Ok(()) + slot(lang).install(aff, dic) } /// Forget every variety's loaded dictionary, so the next check looks for an installed one again /// (after [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - for slot in &DICTS { - *slot.write().unwrap_or_else(|e| e.into_inner()) = None; + for slot in [&SPAIN_DICT, &MEXICO_DICT, &LATIN_AMERICA_DICT, &URUGUAY_DICT] { + slot.reload(); } } /// The `.dic` file of the Hunspell dictionary `lang`'s variety finds in the dictionaries folder -/// or the system locations now (File › Options › Proofing shows it); `None` when there is none. -/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// or the system locations now (File › Options › Proofing shows it); `None` when there is none, +/// or when one given with [`install_dictionary`] is in use. pub fn installed_path(lang: ProofLang) -> Option { - crate::hunspell::find(variety(lang).2).map(|(_, dic)| dic) + slot(lang).installed_path() } /// Is a dictionary available for `lang`'s variety (spelling is checked)? pub fn has_dictionary(lang: ProofLang) -> bool { - !dictionary(lang).is_empty() + slot(lang).has_dictionary() } /// Is `c` a letter of the Latin script (the only one the Spanish dictionaries cover)? @@ -200,7 +177,7 @@ mod tests { std::fs::write(dir.join(format!("{name}.aff")), test_dict::AFF).unwrap(); std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); for lang in VARIETIES { - let found = crate::hunspell::find_in(&dir, variety(lang).2); + let found = crate::hunspell::find_in(&dir, slot(lang).names()); assert_eq!(found.is_some(), found_by.contains(&lang), "{name}: {lang:?}"); if let Some((aff, dic)) = found { assert!(is_correct_with(&crate::hunspell::load(&aff, &dic).unwrap(), "canción", lang)); @@ -215,7 +192,7 @@ mod tests { std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); } for (lang, name) in [(es, "es_ES"), (mx, "es_MX"), (la, "es_419"), (uy, "es_UY")] { - assert_eq!(crate::hunspell::find_in(&dir, variety(lang).2).map(|(_, dic)| dic), Some(dir.join(format!("{name}.dic")))); + assert_eq!(crate::hunspell::find_in(&dir, slot(lang).names()).map(|(_, dic)| dic), Some(dir.join(format!("{name}.dic")))); assert_eq!(dictionary_name(lang), name); } std::fs::remove_dir_all(dir).unwrap(); diff --git a/crates/proof/src/hr/mod.rs b/crates/proof/src/hr/mod.rs index ee9d58e92..6372a47d2 100644 --- a/crates/proof/src/hr/mod.rs +++ b/crates/proof/src/hr/mod.rs @@ -9,68 +9,47 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Croatian dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of a Croatian Hunspell dictionary. const NAMES: &[&str] = &["hr_HR", "hr-HR", "hr"]; +/// The Croatian dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Croatian"); + /// The Croatian dictionary: the installed Hunspell `hr_HR` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available (Croatian spelling is off /// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() -} - -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Croatian spelling: no Hunspell hr_HR dictionary installed"); - return AffixDict::default(); - }; - - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Croatian spelling: {e}"); - AffixDict::default() - }) + DICT.get() } /// Use this Croatian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to /// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the Hunspell `hr_HR` dictionary found in the dictionaries folder or the -/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. -/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Croatian dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Croatian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `c` a letter of the Latin script (the only one the Croatian dictionary covers)? diff --git a/crates/proof/src/id/mod.rs b/crates/proof/src/id/mod.rs index 012babc0e..65b820dfc 100644 --- a/crates/proof/src/id/mod.rs +++ b/crates/proof/src/id/mod.rs @@ -9,74 +9,54 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Indonesian dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of an Indonesian Hunspell dictionary (`in` is the old ISO 639 code, still used by /// Java locales). const NAMES: &[&str] = &["id_ID", "id-ID", "id", "in_ID"]; -/// Clitics PUEBI joins with a hyphen to a capitalized word, an abbreviation or a number -/// (`se-Indonesia`, `KTP-nya`, `Jakarta-lah`); they aren't dictionary words on their own. -const CLITICS: &[&str] = &["se", "nya", "ku", "mu", "lah", "kah", "pun"]; +/// The Indonesian dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Indonesian"); /// The Indonesian dictionary: the installed Hunspell `id_ID` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available (Indonesian spelling is off /// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() -} - -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Indonesian spelling: no Hunspell id_ID dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Indonesian spelling: {e}"); - AffixDict::default() - }) + DICT.get() } /// Use this Indonesian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to /// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the Hunspell `id_ID` dictionary found in the dictionaries folder or the -/// system locations now (File › Options › Proofing shows it); `None` when there is none. A -/// dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Indonesian dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is an Indonesian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } +/// Clitics PUEBI joins with a hyphen to a capitalized word, an abbreviation or a number +/// (`se-Indonesia`, `KTP-nya`, `Jakarta-lah`); they aren't dictionary words on their own. +const CLITICS: &[&str] = &["se", "nya", "ku", "mu", "lah", "kah", "pun"]; + /// Is `c` a letter of the Latin script (the only one the Indonesian dictionary covers)? fn is_latin_letter(c: char) -> bool { c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) diff --git a/crates/proof/src/it/mod.rs b/crates/proof/src/it/mod.rs index 027006a69..168883e17 100644 --- a/crates/proof/src/it/mod.rs +++ b/crates/proof/src/it/mod.rs @@ -18,67 +18,47 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Italian dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of an Italian Hunspell dictionary (Italy first, then Switzerland). const NAMES: &[&str] = &["it_IT", "it_CH", "it-IT", "it"]; +/// The Italian dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Italian"); + /// The Italian dictionary: the installed Hunspell `it_IT` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available (Italian spelling is off /// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() -} - -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Italian spelling: no Hunspell it_IT dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Italian spelling: {e}"); - AffixDict::default() - }) + DICT.get() } /// Use this Italian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to /// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the Hunspell `it_IT` dictionary found in the dictionaries folder or the -/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. -/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Italian dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is an Italian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `c` a letter of the Latin script (the only one the Italian dictionary covers)? diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index 47216d72a..7469c4d9b 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -65,6 +65,7 @@ pub mod patterns; pub mod pl; pub mod pt; pub mod ru; +mod slot; pub mod tr; pub mod vi; @@ -1095,36 +1096,35 @@ mod tests { let before = dictionary_generation(); set_dictionaries_folder(Some(dir.clone())); assert!(dictionary_generation() > before, "cached checks are dropped"); - // Turkish follows the four below; Russian, Czech, Indonesian and the Spanishes follow it. + // Every language is listed, Russian and Czech too (#3226: they were once missing, so + // neither showed up in File › Options › Proofing). A language reports the folder's file, + // or none while a dictionary given with install_dictionary() is in use (#3252 review); + // the languages' own tests install theirs concurrently, so an exact path would race them. + // The folder is searched first whatever is in use. let found = installed_dictionaries(); - assert_eq!(found.get(4), Some(&(ProofLang::Tr, Some(dir.join("tr_TR.dic"))))); + let names = ["pl_PL", "pt_BR", "it_IT", "hr_HR", "tr_TR", "ru_RU", "cs_CZ", "id_ID", "es_ES", "es_MX", "es_419", "es_UY"]; assert_eq!( - found[7..], + found.iter().map(|(lang, _)| *lang).collect::>(), [ - (ProofLang::Id, Some(dir.join("id_ID.dic"))), - (ProofLang::Es, Some(dir.join("es_ES.dic"))), - (ProofLang::EsMx, Some(dir.join("es_MX.dic"))), - (ProofLang::Es419, Some(dir.join("es_419.dic"))), - (ProofLang::EsUy, Some(dir.join("es_UY.dic"))), + ProofLang::Pl, + ProofLang::Pt, + ProofLang::It, + ProofLang::Hr, + ProofLang::Tr, + ProofLang::Ru, + ProofLang::Cs, + ProofLang::Id, + ProofLang::Es, + ProofLang::EsMx, + ProofLang::Es419, + ProofLang::EsUy ] ); - assert_eq!( - found[..4], - [ - (ProofLang::Pl, Some(dir.join("pl_PL.dic"))), - (ProofLang::Pt, Some(dir.join("pt_BR.dic"))), - (ProofLang::It, Some(dir.join("it_IT.dic"))), - (ProofLang::Hr, Some(dir.join("hr_HR.dic"))), - ] - ); - // #3226: Russian and Czech were missing from installed_dictionaries() entirely (no - // installed_path() in either module), so neither ever showed up in File › Options › - // Proofing, installed or not. Their languages, not their exact path, is what belongs - // here: unlike the four above, installed_path() for Russian and Czech also reflects - // whether install_dictionary() gave one directly (#3252 review), which ru's and cs's own - // test modules do, concurrently, in tests of their own — asserting an exact path here - // would race them for no reason this test needs to care about. - assert_eq!(found[5..7].iter().map(|(lang, _)| *lang).collect::>(), [ProofLang::Ru, ProofLang::Cs], "{found:?}"); + for ((lang, path), name) in found.iter().zip(names) { + let file = dir.join(format!("{name}.dic")); + assert!(path.is_none() || path.as_ref() == Some(&file), "{lang:?}: {path:?}"); + assert_eq!(hunspell::find(&[name]).map(|(_, dic)| dic), Some(file), "{lang:?}"); + } assert!(is_correct_in("chrząszcz", ProofLang::Pl) && !is_correct_in("chrzonszcz", ProofLang::Pl)); assert!(is_correct_in("tenés", ProofLang::EsUy) && !is_correct_in("prueva", ProofLang::EsUy)); assert!(is_correct_in("дом", ProofLang::Ru) && !is_correct_in("дум", ProofLang::Ru)); diff --git a/crates/proof/src/pl/mod.rs b/crates/proof/src/pl/mod.rs index 18c968feb..4d79986d9 100644 --- a/crates/proof/src/pl/mod.rs +++ b/crates/proof/src/pl/mod.rs @@ -17,67 +17,47 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Polish dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of a Polish Hunspell dictionary. const NAMES: &[&str] = &["pl_PL", "pl-PL", "pl"]; +/// The Polish dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Polish"); + /// The Polish dictionary: the installed Hunspell `pl_PL` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Polish spelling is off then; -/// see [`has_dictionary`]). +/// [`install_dictionary`], or an empty one when none is available (Polish spelling is off +/// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() + DICT.get() } -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Polish spelling: no Hunspell pl_PL dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Polish spelling: {e}"); - AffixDict::default() - }) -} - -/// Use this Polish dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to UTF-8), -/// for example one the user picked. On error the current dictionary stays. +/// Use this Polish dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to +/// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the installed Polish dictionary, searching the dictionaries folder and -/// the system locations now (File › Options › Proofing shows it, #1063); `None` when there is -/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Polish dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Polish dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Endings Polish adds after an apostrophe to foreign names whose last letter isn't pronounced diff --git a/crates/proof/src/pt/mod.rs b/crates/proof/src/pt/mod.rs index de4198259..860cf4579 100644 --- a/crates/proof/src/pt/mod.rs +++ b/crates/proof/src/pt/mod.rs @@ -16,67 +16,47 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Portuguese dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of a Portuguese Hunspell dictionary. const NAMES: &[&str] = &["pt_BR", "pt-BR", "pt"]; +/// The Portuguese dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Portuguese"); + /// The Portuguese dictionary: the installed Hunspell `pt_BR` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available (Portuguese spelling is off /// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.get_or_insert(loaded).clone() -} - -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Portuguese spelling: no Hunspell pt_BR dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Portuguese spelling: {e}"); - AffixDict::default() - }) + DICT.get() } /// Use this Portuguese dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to /// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the installed Portuguese dictionary, searching the dictionaries folder and -/// the system locations now (File › Options › Proofing shows it, #1063); `None` when there is -/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Portuguese dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Portuguese dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `c` a letter of the Latin script (the only one the Portuguese dictionary covers)? diff --git a/crates/proof/src/ru/mod.rs b/crates/proof/src/ru/mod.rs index d5035e6a3..e46bf305f 100644 --- a/crates/proof/src/ru/mod.rs +++ b/crates/proof/src/ru/mod.rs @@ -28,83 +28,47 @@ mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use crate::affix::AffixDict; +use crate::slot::DictSlot; pub use suggest::suggest; -/// The Russian dictionary and whether it came from [`install_dictionary`] rather than a file on -/// disk (it then has no file of its own to report from [`installed_path`], even if a matching -/// file also happens to exist): one lock, so a reload and an install can't interleave their two -/// fields into an inconsistent combination. -static DICT: RwLock = RwLock::new(DictState { dict: None, in_memory: false }); - -struct DictState { - dict: Option>, - in_memory: bool, -} - /// File names of a Russian Hunspell dictionary. const NAMES: &[&str] = &["ru_RU", "ru-RU", "ru"]; +/// The Russian dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Russian"); + /// The Russian dictionary: the installed Hunspell `ru_RU` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Russian spelling is off then; -/// see [`has_dictionary`]). +/// [`install_dictionary`], or an empty one when none is available (Russian spelling is off +/// then; see [`has_dictionary`]). pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).dict.as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - // Another thread (or an install) may have filled it meanwhile: that one wins. - slot.dict.get_or_insert(loaded).clone() + DICT.get() } -fn load_installed() -> AffixDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Russian spelling: no Hunspell ru_RU dictionary installed"); - return AffixDict::default(); - }; - crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("Russian spelling: {e}"); - AffixDict::default() - }) -} - -/// Use this Russian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to UTF-8), -/// for example one the user picked. On error the current dictionary stays. +/// Use this Russian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to +/// UTF-8), for example one the user picked. On error the current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = AffixDict::from_hunspell(aff, dic, &[])?; - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - slot.dict = Some(Arc::new(d)); - slot.in_memory = true; - Ok(()) + DICT.install(aff, dic) } /// Forget the loaded dictionary, so the next check looks for an installed one again (after /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). pub fn reload_dictionary() { - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - slot.dict = None; - slot.in_memory = false; + DICT.reload(); } -/// The `.dic` file of the installed Russian dictionary, searching the dictionaries folder and -/// the system locations now (File › Options › Proofing shows it, #3226); `None` when there is -/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported, even if a -/// matching file also happens to exist. +/// The `.dic` file of the installed Russian dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - if DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).in_memory { - return None; - } - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Russian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `c` a letter of the Cyrillic script (the only one the Russian dictionary covers)? diff --git a/crates/proof/src/slot.rs b/crates/proof/src/slot.rs new file mode 100644 index 000000000..3c0f2bd35 --- /dev/null +++ b/crates/proof/src/slot.rs @@ -0,0 +1,150 @@ +//! The installed Hunspell dictionary of a proofing language, in one shape for every language: +//! found on first use, replaced with [`DictSlot::install`] and forgotten with [`DictSlot::reload`]. + +use std::path::{Path, PathBuf}; +use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +use crate::affix::AffixDict; + +/// A dictionary a language reads: the affix engine's ([`AffixDict`]) or Turkish's own. +pub(crate) trait Dictionary: Default + Send + Sync { + /// The installed `.aff` and `.dic` files, read. + fn load(aff: &Path, dic: &Path) -> Result; + /// Hunspell `.aff` and `.dic` text (decoded to UTF-8), parsed. + fn parse(aff: &str, dic: &str) -> Result; + /// No words: the language has no spelling marks. + fn is_empty(&self) -> bool; +} + +impl Dictionary for AffixDict { + fn load(aff: &Path, dic: &Path) -> Result { + crate::hunspell::load(aff, dic) + } + + fn parse(aff: &str, dic: &str) -> Result { + AffixDict::from_hunspell(aff, dic, &[]) + } + + fn is_empty(&self) -> bool { + AffixDict::is_empty(self) + } +} + +/// The dictionary in use and whether it came from [`DictSlot::install`] rather than a file on +/// disk (it then has no file to report, even if a matching file also happens to exist): one lock, +/// so a reload and an install can't interleave their two fields. +struct State { + dict: Option>, + in_memory: bool, +} + +/// One language's dictionary. +pub(crate) struct DictSlot { + /// File names the dictionary may have, the usual one first (also the key of its load error). + names: &'static [&'static str], + /// The language's English name, for the log. + english: &'static str, + state: RwLock>, +} + +impl DictSlot { + pub(crate) const fn new(names: &'static [&'static str], english: &'static str) -> Self { + DictSlot { names, english, state: RwLock::new(State { dict: None, in_memory: false }) } + } + + fn read(&self) -> RwLockReadGuard<'_, State> { + self.state.read().unwrap_or_else(PoisonError::into_inner) + } + + fn write(&self) -> RwLockWriteGuard<'_, State> { + self.state.write().unwrap_or_else(PoisonError::into_inner) + } + + /// The Hunspell name the dictionary is known by (`pl_PL`): the first of its file names. + pub(crate) fn name(&self) -> &'static str { + self.names.first().copied().unwrap_or_default() + } + + /// The file names the dictionary may have, the usual one first. + #[cfg(test)] + pub(crate) fn names(&self) -> &'static [&'static str] { + self.names + } + + /// The dictionary: the installed one (read on first use), one given with [`Self::install`], or + /// an empty one when none is available (the language's spelling is off then). + pub(crate) fn get(&self) -> Arc { + if let Some(d) = self.read().dict.as_ref() { + return d.clone(); + } + let loaded = Arc::new(self.load_installed()); + // Another thread (or an install) may have filled it meanwhile: that one wins. + self.write().dict.get_or_insert(loaded).clone() + } + + fn load_installed(&self) -> D { + let name = self.name(); + let Some((aff, dic)) = crate::hunspell::find(self.names) else { + crate::hunspell::clear_load_error(name); + log::info!("{} spelling: no Hunspell {name} dictionary installed", self.english); + return D::default(); + }; + crate::hunspell::note_load(name, &dic, D::load(&aff, &dic)).unwrap_or_else(|e| { + log::warn!("{} spelling: {e}", self.english); + D::default() + }) + } + + /// Use this dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to UTF-8), for + /// example one the user picked. On error the current dictionary stays. + pub(crate) fn install(&self, aff: &str, dic: &str) -> Result<(), String> { + let d = D::parse(aff, dic)?; + let mut slot = self.write(); + slot.dict = Some(Arc::new(d)); + slot.in_memory = true; + Ok(()) + } + + /// Forget the dictionary, so the next check looks for an installed one again (after + /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). + pub(crate) fn reload(&self) { + let mut slot = self.write(); + slot.dict = None; + slot.in_memory = false; + } + + /// The `.dic` file of the installed dictionary, searching the dictionaries folder and the + /// system locations now (File › Options › Proofing shows it); `None` when there is none, or + /// when the dictionary in use was given with [`Self::install`] and so has no file. + pub(crate) fn installed_path(&self) -> Option { + if self.read().in_memory { + return None; + } + crate::hunspell::find(self.names).map(|(_, dic)| dic) + } + + /// Is a dictionary available (spelling is checked)? + pub(crate) fn has_dictionary(&self) -> bool { + !self.get().is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static SLOT: DictSlot = DictSlot::new(&["zz_SLOT_TEST"], "Test"); + + #[test] + fn an_installed_dictionary_has_no_file_until_reloaded() { + assert!(!SLOT.has_dictionary(), "nothing installed under this name"); + assert!(SLOT.install("FLAG long\n", "1\nkot\n").is_err(), "unsupported files are refused"); + assert!(!SLOT.has_dictionary(), "a refused install keeps the current one"); + SLOT.install("", "1\nkot\n").unwrap(); + assert!(SLOT.has_dictionary() && SLOT.get().check("kot")); + assert_eq!(SLOT.installed_path(), None); + SLOT.reload(); + assert!(!SLOT.has_dictionary(), "reloaded from disk, where there is none"); + assert_eq!(SLOT.name(), "zz_SLOT_TEST"); + } +} diff --git a/crates/proof/src/tr/dict.rs b/crates/proof/src/tr/dict.rs index ed57cbca7..b47ba7c1e 100644 --- a/crates/proof/src/tr/dict.rs +++ b/crates/proof/src/tr/dict.rs @@ -26,6 +26,21 @@ pub struct TrDict { max_suffix_len: usize, } +impl crate::slot::Dictionary for TrDict { + fn load(aff: &std::path::Path, dic: &std::path::Path) -> Result { + let read = |p: &std::path::Path| std::fs::read_to_string(p).map_err(|e| e.to_string()); + TrDict::from_hunspell(&read(aff)?, &read(dic)?) + } + + fn parse(aff: &str, dic: &str) -> Result { + TrDict::from_hunspell(aff, dic) + } + + fn is_empty(&self) -> bool { + TrDict::is_empty(self) + } +} + impl TrDict { /// Returns true if empty (no dictionary loaded). pub fn is_empty(&self) -> bool { diff --git a/crates/proof/src/tr/mod.rs b/crates/proof/src/tr/mod.rs index ab3fe92d7..eb5f0ea52 100644 --- a/crates/proof/src/tr/mod.rs +++ b/crates/proof/src/tr/mod.rs @@ -18,70 +18,45 @@ pub mod suggest; #[cfg(test)] pub(crate) mod test_dict; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; + +use crate::slot::DictSlot; pub use dict::TrDict; pub use suggest::suggest; -/// The Turkish dictionary: `None` until first use, then the installed one (or an empty one). -static DICT: RwLock>> = RwLock::new(None); - /// File names of a Turkish Hunspell dictionary. const NAMES: &[&str] = &["tr_TR", "tr-TR", "tr"]; +/// The Turkish dictionary, found on first use (see [`DictSlot`]). +static DICT: DictSlot = DictSlot::new(NAMES, "Turkish"); + /// The Turkish dictionary: the installed Hunspell `tr_TR` (read on first use), one given with /// [`install_dictionary`], or an empty one when none is available. pub fn dictionary() -> Arc { - if let Some(d) = DICT.read().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - return d.clone(); - } - let loaded = Arc::new(load_installed()); - let mut slot = DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner); - slot.get_or_insert(loaded).clone() + DICT.get() } -fn load_installed() -> TrDict { - let Some((aff, dic)) = crate::hunspell::find(NAMES) else { - crate::hunspell::clear_load_error(NAMES[0]); - log::info!("Turkish spelling: no Hunspell tr_TR dictionary installed"); - return TrDict::default(); - }; - let read = |p: &std::path::Path| std::fs::read_to_string(p); - match (read(&aff), read(&dic)) { - (Ok(aff_text), Ok(dic_text)) => crate::hunspell::note_load(NAMES[0], &dic, TrDict::from_hunspell(&aff_text, &dic_text)).unwrap_or_else(|e| { - log::warn!("Turkish spelling: {e}"); - TrDict::default() - }), - (a, d) => { - let reason = a.err().or(d.err()).map_or_else(String::new, |e| e.to_string()); - let _ = crate::hunspell::note_load::<()>(NAMES[0], &dic, Err(reason)); - TrDict::default() - } - } -} - -/// Use this Turkish dictionary from now on. +/// Use this Turkish dictionary from now on (Hunspell `.aff` and `.dic` text). On error the +/// current dictionary stays. pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - let d = TrDict::from_hunspell(aff, dic)?; - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::new(d)); - Ok(()) + DICT.install(aff, dic) } -/// Reload the dictionary on next check. +/// Forget the loaded dictionary, so the next check looks for an installed one again. pub fn reload_dictionary() { - *DICT.write().unwrap_or_else(std::sync::PoisonError::into_inner) = None; + DICT.reload(); } -/// The `.dic` file of the Hunspell `tr_TR` dictionary found in the dictionaries folder or the -/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. -/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +/// The `.dic` file of the installed Turkish dictionary (File › Options › Proofing shows it); +/// `None` when there is none, or when one given with [`install_dictionary`] is in use. pub fn installed_path() -> Option { - crate::hunspell::find(NAMES).map(|(_, dic)| dic) + DICT.installed_path() } /// Is a Turkish dictionary available? pub fn has_dictionary() -> bool { - !dictionary().is_empty() + DICT.has_dictionary() } /// Is `word` spelled correctly in Turkish?