Merge PR #3252: Russian and Czech dictionaries are reported, not just checked (#3226)

This commit is contained in:
aiongg
2026-10-22 01:19:47 +00:00
4 changed files with 83 additions and 12 deletions
+29 -6
View File
@@ -13,8 +13,16 @@ use crate::affix::AffixDict;
pub use suggest::suggest;
/// The Czech dictionary: `None` until first use, then the installed one (or an empty one).
static DICT: RwLock<Option<Arc<AffixDict>>> = RwLock::new(None);
/// 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<DictState> = RwLock::new(DictState { dict: None, in_memory: false });
struct DictState {
dict: Option<Arc<AffixDict>>,
in_memory: bool,
}
/// File names of a Czech Hunspell dictionary.
const NAMES: &[&str] = &["cs_CZ", "cs-CZ", "cs"];
@@ -23,13 +31,13 @@ const NAMES: &[&str] = &["cs_CZ", "cs-CZ", "cs"];
/// [`install_dictionary`], or an empty one when none is available (Czech spelling is off
/// then; see [`has_dictionary`]).
pub fn dictionary() -> Arc<AffixDict> {
if let Some(d) = DICT.read().unwrap_or_else(|e| e.into_inner()).as_ref() {
if let Some(d) = DICT.read().unwrap_or_else(|e| e.into_inner()).dict.as_ref() {
return d.clone();
}
let loaded = Arc::new(load_installed());
let mut slot = DICT.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()
slot.dict.get_or_insert(loaded).clone()
}
fn load_installed() -> AffixDict {
@@ -47,14 +55,29 @@ fn load_installed() -> AffixDict {
/// 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(|e| e.into_inner()) = Some(Arc::new(d));
let mut slot = DICT.write().unwrap_or_else(|e| e.into_inner());
slot.dict = Some(Arc::new(d));
slot.in_memory = true;
Ok(())
}
/// 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(|e| e.into_inner()) = None;
let mut slot = DICT.write().unwrap_or_else(|e| e.into_inner());
slot.dict = None;
slot.in_memory = false;
}
/// 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.
pub fn installed_path() -> Option<std::path::PathBuf> {
if DICT.read().unwrap_or_else(|e| e.into_inner()).in_memory {
return None;
}
crate::hunspell::find(NAMES).map(|(_, dic)| dic)
}
/// Is a Czech dictionary available (spelling is checked)?
+20
View File
@@ -151,6 +151,8 @@ pub fn set_dictionaries_folder(dir: Option<std::path::PathBuf>) {
it::reload_dictionary();
hr::reload_dictionary();
tr::reload_dictionary();
ru::reload_dictionary();
cs::reload_dictionary();
id::reload_dictionary();
DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed);
}
@@ -164,6 +166,8 @@ pub fn installed_dictionaries() -> Vec<(ProofLang, Option<std::path::PathBuf>)>
(ProofLang::It, it::installed_path()),
(ProofLang::Hr, hr::installed_path()),
(ProofLang::Tr, tr::installed_path()),
(ProofLang::Ru, ru::installed_path()),
(ProofLang::Cs, cs::installed_path()),
(ProofLang::Id, id::installed_path()),
]
}
@@ -977,11 +981,18 @@ mod tests {
("pt_BR", pt::test_dict::AFF, pt::test_dict::DIC),
("it_IT", it::test_dict::AFF, it::test_dict::DIC),
("hr_HR", hr::test_dict::AFF, hr::test_dict::DIC),
("ru_RU", ru::test_dict::AFF, ru::test_dict::DIC),
("cs_CZ", cs::test_dict::AFF, cs::test_dict::DIC),
("id_ID", id::test_dict::AFF, id::test_dict::DIC),
] {
std::fs::write(dir.join(format!("{name}.aff")), aff).unwrap();
std::fs::write(dir.join(format!("{name}.dic")), dic).unwrap();
}
// #3252 review: force Russian and Czech to cache whatever they find before the folder is
// set (empty, with no folder and nothing installed on CI), so the folder change below has
// a stale cache to actually invalidate, not just an untouched `None`.
ru::dictionary();
cs::dictionary();
let before = dictionary_generation();
set_dictionaries_folder(Some(dir.clone()));
assert!(dictionary_generation() > before, "cached checks are dropped");
@@ -998,7 +1009,16 @@ mod tests {
(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::<Vec<_>>(), [ProofLang::Ru, ProofLang::Cs], "{found:?}");
assert!(is_correct_in("chrząszcz", ProofLang::Pl) && !is_correct_in("chrzonszcz", ProofLang::Pl));
assert!(is_correct_in("дом", ProofLang::Ru) && !is_correct_in("дум", ProofLang::Ru));
}
/// #1741: a list item may be a fragment, so its first word needs no capital in any language
+29 -6
View File
@@ -34,8 +34,16 @@ use crate::affix::AffixDict;
pub use suggest::suggest;
/// The Russian dictionary: `None` until first use, then the installed one (or an empty one).
static DICT: RwLock<Option<Arc<AffixDict>>> = RwLock::new(None);
/// 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<DictState> = RwLock::new(DictState { dict: None, in_memory: false });
struct DictState {
dict: Option<Arc<AffixDict>>,
in_memory: bool,
}
/// File names of a Russian Hunspell dictionary.
const NAMES: &[&str] = &["ru_RU", "ru-RU", "ru"];
@@ -44,13 +52,13 @@ const NAMES: &[&str] = &["ru_RU", "ru-RU", "ru"];
/// [`install_dictionary`], or an empty one when none is available (Russian spelling is off then;
/// see [`has_dictionary`]).
pub fn dictionary() -> Arc<AffixDict> {
if let Some(d) = DICT.read().unwrap_or_else(|e| e.into_inner()).as_ref() {
if let Some(d) = DICT.read().unwrap_or_else(|e| e.into_inner()).dict.as_ref() {
return d.clone();
}
let loaded = Arc::new(load_installed());
let mut slot = DICT.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()
slot.dict.get_or_insert(loaded).clone()
}
fn load_installed() -> AffixDict {
@@ -68,14 +76,29 @@ fn load_installed() -> AffixDict {
/// 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(|e| e.into_inner()) = Some(Arc::new(d));
let mut slot = DICT.write().unwrap_or_else(|e| e.into_inner());
slot.dict = Some(Arc::new(d));
slot.in_memory = true;
Ok(())
}
/// 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(|e| e.into_inner()) = None;
let mut slot = DICT.write().unwrap_or_else(|e| e.into_inner());
slot.dict = None;
slot.in_memory = false;
}
/// 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.
pub fn installed_path() -> Option<std::path::PathBuf> {
if DICT.read().unwrap_or_else(|e| e.into_inner()).in_memory {
return None;
}
crate::hunspell::find(NAMES).map(|(_, dic)| dic)
}
/// Is a Russian dictionary available (spelling is checked)?