From 7aab0b202cdabc6f4f2e02b7ee08a2f766d16b45 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Mon, 2 Nov 2026 00:07:50 +0000 Subject: [PATCH] =?UTF-8?q?FU.3:=20language=20packs=20=E2=80=94=20the=20st?= =?UTF-8?q?atus=20bar's=20pack=20lookup=20is=20cached=20by=20index=20gener?= =?UTF-8?q?ation,=20not=20copied=20every=20frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/engine/src/packs.rs | 60 +++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/packs.rs b/crates/engine/src/packs.rs index f411f8969c..4556f35390 100644 --- a/crates/engine/src/packs.rs +++ b/crates/engine/src/packs.rs @@ -630,12 +630,47 @@ pub fn proofable(tag: &str) -> bool { pub fn missing_dictionary(tag: Option<&str>) -> Option<&'static str> { let name = wordcraft_proof::missing_dictionary(tag)?; let lang = wordcraft_proof::ProofLang::from_tag(tag); - if lang.pack_locale().is_some() && pack_for_tag(tag.unwrap_or(""), &available()).is_none() { + if lang.pack_locale().is_some() && !has_pack_for_tag(tag.unwrap_or("")) { return None; } Some(name) } +/// The last answer of whether the index has a pack for a tag, for the index generation it was +/// worked out for: the status bar asks every frame while the caret is in a pack language, and +/// working it out copies the whole index. +#[derive(Default)] +struct PackLookup { + /// (index generation, tag, has a pack). + entry: Option<(u64, String, bool)>, +} + +impl PackLookup { + /// The remembered answer for `tag` at `generation`, else `compute`'s (then remembered). + fn get(&mut self, generation: u64, tag: &str, compute: impl FnOnce() -> bool) -> bool { + if let Some((g, t, has)) = &self.entry + && *g == generation + && t == tag + { + return *has; + } + let has = compute(); + self.entry = Some((generation, tag.to_string(), has)); + has + } +} + +static PACK_LOOKUP: Mutex = Mutex::new(PackLookup { entry: None }); + +/// Whether the index has a pack for `tag` ([`pack_for_tag`]), worked out again only when the +/// index changes ([`index_generation`]) or the tag does. +fn has_pack_for_tag(tag: &str) -> bool { + // Read before [`available`], which may fill the index and bump it: the answer is then worked + // out once more, never kept for a list it wasn't worked out from. + let generation = index_generation(); + PACK_LOOKUP.lock().unwrap_or_else(PoisonError::into_inner).get(generation, tag, || pack_for_tag(tag, &available()).is_some()) +} + /// The packs the document's text asks for that are available and not installed: what the /// missing-pack bar offers. Never a language without a pack, nor one proofing can't use. pub fn missing(s: &Session) -> Vec { @@ -764,6 +799,29 @@ mod tests { } } + /// FU.3: the status bar's "(no dictionary)" asks every frame whether the index has a pack + /// for the caret's language; the answer is reused until the index changes (a new + /// generation) or the tag does, so the index isn't copied every frame. + #[test] + fn the_pack_lookup_is_reused_until_the_index_changes() { + let mut lookup = super::PackLookup::default(); + let mut computed = 0; + let mut ask = |lookup: &mut super::PackLookup, generation: u64, tag: &str, answer: bool| { + lookup.get(generation, tag, || { + computed += 1; + answer + }) + }; + assert!(ask(&mut lookup, 1, "ja-JP", true)); + // Same generation and tag: the remembered answer, even where the list would now say no. + for _ in 0..10 { + assert!(ask(&mut lookup, 1, "ja-JP", false)); + } + assert!(!ask(&mut lookup, 2, "ja-JP", false), "a new generation works it out again"); + assert!(ask(&mut lookup, 2, "th-TH", true), "another tag too"); + assert_eq!(computed, 3); + } + #[test] fn the_default_build_embeds_only_english() { const CARRY_OVER: [&str; 5] = ["de", "pl", "pt", "it", "ru"];