forked from 1b-agent/opencadstudio-upstream
feat(text): native OS spell-checker for text_audit (Windows/macOS/Linux)
Add cross-platform SystemSpeller abstraction with three backends: - Windows: direct COM vtable calls to ISpellChecker (Win32 Spell API), no external crate — uses the same hand-rolled FFI pattern as file_association.rs. Handles ALL-CAPS words by also testing lowercase. - macOS: NSSpellChecker via objc2/objc2-app-kit (already in deps). - Linux: Hunspell .dic file reader + hunspell CLI pipe fallback. text_audit gains three new parameters: system_spellcheck: true — activate OS spell-checker language: en-US — BCP-47 language tag (default: OS default) suggest: true — include spelling suggestions The agent dictionary and system speller work as a layered pipeline: words matching the agent dictionary are accepted immediately; only remaining words are forwarded to the OS checker. Response includes a system_speller object (enabled, available, backend, language) and an unrecognized_words array with handle, position, and optional suggestions. Integration test: text_audit_system_spellcheck_flags_misspelled_words (20/20 p1_tests pass). Release build verified.
This commit is contained in:
1 parent
97ea84d99a
commit
77fbb79917
6 files changed
+1001
-14
No files matched your search
+101
-13
@@ -955,6 +955,20 @@ impl OpenCADStudio {
|
||||
.map(|w| TextMatcher::new(w.clone(), false, true, true))
|
||||
.collect();
|
||||
|
||||
let use_system_speller = req["system_spellcheck"]
|
||||
.as_bool()
|
||||
.or_else(|| req["use_system_speller"].as_bool())
|
||||
.or_else(|| req["system_speller"].as_bool())
|
||||
.unwrap_or(false);
|
||||
let language_param = req["language"].as_str();
|
||||
let want_suggestions = req["suggest"].as_bool().unwrap_or(true);
|
||||
|
||||
let system_speller = if use_system_speller {
|
||||
Some(super::spellcheck::SystemSpeller::new(language_param))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut pair_hit_counts = vec![0usize; pair_matchers.len()];
|
||||
let mut check_hit_counts = vec![0usize; check_matchers.len()];
|
||||
|
||||
@@ -1090,21 +1104,39 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Dictionary check
|
||||
if !dict_matchers.is_empty() {
|
||||
// 3. Dictionary & System Spell-check
|
||||
if !dict_matchers.is_empty() || system_speller.is_some() {
|
||||
for word in plain_text.split(|c: char| !c.is_alphabetic()) {
|
||||
let w = word.trim();
|
||||
if w.len() >= 3 && !dict_matchers.iter().any(|dm| dm.matches(w)) {
|
||||
if unrecognized_words.len() < limit
|
||||
&& !unrecognized_words
|
||||
.iter()
|
||||
.any(|item: &Value| item["word"].as_str() == Some(w))
|
||||
{
|
||||
unrecognized_words.push(json!({
|
||||
"word": w,
|
||||
"handle": format!("{:X}", handle.value()),
|
||||
"position": pos,
|
||||
}));
|
||||
if w.len() >= 3 {
|
||||
let in_agent_dict = dict_matchers.iter().any(|dm| dm.matches(w));
|
||||
if in_agent_dict {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (is_system_valid, suggestions) = if let Some(ref speller) = system_speller {
|
||||
speller.check_word(w, want_suggestions)
|
||||
} else {
|
||||
(false, Vec::new())
|
||||
};
|
||||
|
||||
if !is_system_valid {
|
||||
entity_had_match = true;
|
||||
if unrecognized_words.len() < limit
|
||||
&& !unrecognized_words
|
||||
.iter()
|
||||
.any(|item: &Value| item["word"].as_str() == Some(w))
|
||||
{
|
||||
let mut item = json!({
|
||||
"word": w,
|
||||
"handle": format!("{:X}", handle.value()),
|
||||
"position": pos,
|
||||
});
|
||||
if !suggestions.is_empty() {
|
||||
item["suggestions"] = json!(suggestions);
|
||||
}
|
||||
unrecognized_words.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1157,6 +1189,44 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !dict_matchers.is_empty() || system_speller.is_some() {
|
||||
for word in plain.split(|c: char| !c.is_alphabetic()) {
|
||||
let w = word.trim();
|
||||
if w.len() >= 3 {
|
||||
let in_agent_dict = dict_matchers.iter().any(|dm| dm.matches(w));
|
||||
if in_agent_dict {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (is_system_valid, suggestions) = if let Some(ref speller) = system_speller {
|
||||
speller.check_word(w, want_suggestions)
|
||||
} else {
|
||||
(false, Vec::new())
|
||||
};
|
||||
|
||||
if !is_system_valid {
|
||||
entity_had_match = true;
|
||||
if unrecognized_words.len() < limit
|
||||
&& !unrecognized_words
|
||||
.iter()
|
||||
.any(|item: &Value| item["word"].as_str() == Some(w))
|
||||
{
|
||||
let mut item = json!({
|
||||
"word": w,
|
||||
"handle": format!("{:X}", handle.value()),
|
||||
"attribute_tag": attr.tag,
|
||||
"position": [insert.insert_point.x, insert.insert_point.y, insert.insert_point.z],
|
||||
});
|
||||
if !suggestions.is_empty() {
|
||||
item["suggestions"] = json!(suggestions);
|
||||
}
|
||||
unrecognized_words.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1180,6 +1250,22 @@ impl OpenCADStudio {
|
||||
.map(|(idx, _)| check_terms[idx].clone())
|
||||
.collect();
|
||||
|
||||
let speller_info = if let Some(ref speller) = system_speller {
|
||||
json!({
|
||||
"enabled": true,
|
||||
"available": speller.is_available(),
|
||||
"backend": speller.backend_name(),
|
||||
"language": speller.language(),
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"enabled": false,
|
||||
"available": false,
|
||||
"backend": "none",
|
||||
"language": null,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"summary": {
|
||||
@@ -1190,7 +1276,9 @@ impl OpenCADStudio {
|
||||
"pairs_unmatched": unmatched_pairs.len(),
|
||||
"check_terms_total": check_terms.len(),
|
||||
"check_terms_found": check_terms.len() - unmatched_check_terms.len(),
|
||||
"unrecognized_words_count": unrecognized_words.len(),
|
||||
},
|
||||
"system_speller": speller_info,
|
||||
"unmatched_pairs": unmatched_pairs,
|
||||
"unmatched_check_terms": unmatched_check_terms,
|
||||
"simulated_changes": simulated_changes,
|
||||
|
||||
Reference in new issue
Block a user