diff --git a/crates/proof/src/hunspell.rs b/crates/proof/src/hunspell.rs index 4cf75210..274af9eb 100644 --- a/crates/proof/src/hunspell.rs +++ b/crates/proof/src/hunspell.rs @@ -105,7 +105,7 @@ fn search_tiers(lo_exts: Option<&[String]>) -> (Vec, Vec) { system.extend(lo_dirs(&base.join("LibreOffice").join("share").join("extensions").to_string_lossy())); } } - system.extend(lo_drive_dirs(lo_exts, Path::is_dir)); + system.extend(lo_drive_dirs(lo_exts, || LO_DRIVE_BASES.get_or_init(|| lo_drive_bases(Path::is_dir)))); } else if cfg!(target_os = "macos") { if let Some(h) = &home { // An app started from Finder or the Dock doesn't see `WORDCRAFT_DICTIONARIES`; this @@ -136,22 +136,25 @@ fn search_tiers(lo_exts: Option<&[String]>) -> (Vec, Vec) { } /// LibreOffice's language folders (`:\Program Files\LibreOffice\share\extensions\`) -/// on the drives C to Z where `is_dir` finds LibreOffice installed. No drive is probed without +/// on the drives C to Z where LibreOffice is installed (`bases`). No drive is probed without /// `lo_exts` (no LibreOffice folders are wanted then). -fn lo_drive_dirs(lo_exts: Option<&[String]>, is_dir: impl Fn(&Path) -> bool) -> Vec { +fn lo_drive_dirs<'a>(lo_exts: Option<&[String]>, bases: impl FnOnce() -> &'a [PathBuf]) -> Vec { let Some(exts) = lo_exts else { return Vec::new() }; - let mut out = Vec::new(); - for drive in 'C'..='Z' { - for pf in ["Program Files", "Program Files (x86)"] { - let base = PathBuf::from(format!(r"{drive}:\{pf}\LibreOffice\share\extensions")); - if is_dir(&base) { - out.extend(exts.iter().map(|e| base.join(e))); - } - } - } - out + bases().iter().flat_map(|base| exts.iter().map(|e| base.join(e))).collect() } +/// The `LibreOffice\share\extensions` folders on drives C to Z (48 probes). +fn lo_drive_bases(is_dir: impl Fn(&Path) -> bool) -> Vec { + ('C'..='Z') + .flat_map(|drive| ["Program Files", "Program Files (x86)"].map(|pf| PathBuf::from(format!(r"{drive}:\{pf}\LibreOffice\share\extensions")))) + .filter(|base| is_dir(base)) + .collect() +} + +/// [`lo_drive_bases`], probed once per process. +// ponytail: a LibreOffice installed while the app runs is seen after a restart. +static LO_DRIVE_BASES: std::sync::OnceLock> = std::sync::OnceLock::new(); + /// The user's data folder on Linux and the BSDs: `$XDG_DATA_HOME` when it is an absolute path (the /// XDG Base Directory spec ignores a relative one), else `~/.local/share`. fn xdg_data_home(xdg: Option, home: Option<&Path>) -> Option { @@ -627,11 +630,20 @@ mod tests { probes.set(probes.get() + 1); false }; - assert!(lo_drive_dirs(None, count).is_empty()); + // craft-libs#495 review: the drives are probed once, not on every lookup. + let cache = std::sync::OnceLock::new(); + let cached = || cache.get_or_init(|| lo_drive_bases(count)).as_slice(); + assert!(lo_drive_dirs(None, cached).is_empty()); assert_eq!(probes.get(), 0); let exts = ["dict-tr".to_string()]; - let found = lo_drive_dirs(Some(&exts), |p: &Path| p.to_string_lossy().starts_with(r"D:\Program Files\")); - assert_eq!(found, [PathBuf::from(r"D:\Program Files\LibreOffice\share\extensions").join("dict-tr")]); + assert!(lo_drive_dirs(Some(&exts), cached).is_empty()); + assert!(lo_drive_dirs(Some(&exts), cached).is_empty()); + assert_eq!(probes.get(), 48); + let found = lo_drive_bases(|p: &Path| p.to_string_lossy().starts_with(r"D:\Program Files\")); + assert_eq!( + lo_drive_dirs(Some(&exts), || found.as_slice()), + [PathBuf::from(r"D:\Program Files\LibreOffice\share\extensions").join("dict-tr")] + ); } /// Another region's Language pack never beats the system's dictionary of the right region diff --git a/crates/proof/src/lang.rs b/crates/proof/src/lang.rs index 85f5b6e6..993e4030 100644 --- a/crates/proof/src/lang.rs +++ b/crates/proof/src/lang.rs @@ -147,6 +147,9 @@ impl ProofLang { } else { ProofLang::De } + } else if tag.split_once('@').is_some_and(|(_, m)| m.eq_ignore_ascii_case("latin")) { + // POSIX `sr_RS@latin`: the Latin-script language (`sr-latn`). + PackLocale::new(&format!("{primary}-latn")).map_or(ProofLang::Other, ProofLang::Pack) } else { PackLocale::new(tag).map_or(ProofLang::Other, ProofLang::Pack) } @@ -446,6 +449,9 @@ mod tests { // craft-libs#482 review: a POSIX charset or modifier with no region. assert_tags(&["en.UTF-8", "en@euro"], ProofLang::En); assert_tags(&["de.UTF-8"], ProofLang::De); + let sr_latn = ProofLang::Pack(PackLocale::new("sr-latn").unwrap()); + assert_tags(&["sr@latin", "sr_RS@latin", "sr_RS.UTF-8@latin", "sr-Latn"], sr_latn); + assert_tags(&["sr", "sr@cyrillic"], ProofLang::Pack(PackLocale::new("sr").unwrap())); assert_tags(&["pt", "pt-BR", "PT_br", "pt-BR.UTF-8", "pt-AO", "por"], ProofLang::Pt); let pt_pt = ProofLang::Pack(PackLocale::new("pt-pt").unwrap()); assert_tags(&[" pt-PT ", "pt_PT.UTF-8", "por-PT"], pt_pt);