FU.3: language packs — the status bar's pack lookup is cached by index generation, not copied every frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
Martin Ball
2026-11-02 01:52:21 +00:00
co-authored by Claude Opus 5.5
parent c4d7948e68
commit 7aab0b202c
+59 -1
View File
@@ -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<PackLookup> = 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<Pack> {
@@ -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"];