mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Text: AutoCorrect options, case rules and your own entries
AutoCorrect gains PowerPoint's options: correct TWo INitial CApitals, capitalize the first letter of sentences and the names of days, fix accidental cAPS LOCK, and replace 1/2, 1/4, 3/4 with fraction characters. "word..." becomes "word…", "word," and "word." are corrected too, and the built-in list grows from 13 to ~70 entries. The case rules apply only to typing, not pasted text. New text.autocorrect sets the options and adds or deletes replace entries (your own win over the built-in list; deleted built-ins stay deleted) and returns the list. Preferences ▸ AutoCorrect Options… opens an AutoCorrect dialog with the options and the editable list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(noundo "text.edit", "Edit Text", [], Some("Enter"), "{id?, at?: [paragraph, char], end?: bool, cell?: [row, col], notes?: bool}", has_slide, edit),
|
||||
cmd!(noundo "text.exit", "Stop Editing Text", [], None, "{}", has_doc, exit),
|
||||
cmd!(
|
||||
noundo "text.autocorrect",
|
||||
"AutoCorrect Options",
|
||||
["Tools"],
|
||||
None,
|
||||
"{on?, twoInitials?, sentenceCaps?, days?, capsLock?, fractions?: bool, add?: [from, to], remove?: from} → the options and the replacement list",
|
||||
always,
|
||||
autocorrect_options
|
||||
),
|
||||
cmd!("text.insert", "Type Text", [], None, "{text} (\\n = new paragraph, \\u000b = line break)", has_text, insert),
|
||||
cmd!("text.delete", "Delete Text", [], Some("Backspace"), "{dir?: backward|forward|wordBackward|wordForward}", has_text, delete),
|
||||
// The input method's composition: shown in place, an undo step only once committed.
|
||||
@@ -294,7 +303,7 @@ pub(crate) fn insert_text(s: &mut Session, text: &str, props: Option<RunProps>)
|
||||
t.caret = c;
|
||||
c
|
||||
})?;
|
||||
autocorrect(s)?;
|
||||
autocorrect(s, text.chars().count() == 1)?;
|
||||
Ok(json!({"caret": [pos.0, pos.1]}))
|
||||
}
|
||||
|
||||
@@ -312,24 +321,151 @@ fn unlinked_at_link_end(body: &TextBody, at: Pos) -> Option<RunProps> {
|
||||
Some(props)
|
||||
}
|
||||
|
||||
const AUTOCORRECT: &[(&str, &str)] = &[
|
||||
/// Built-in AutoCorrect replacements (common slips and symbols).
|
||||
pub const AUTOCORRECT: &[(&str, &str)] = &[
|
||||
("teh", "the"),
|
||||
("adn", "and"),
|
||||
("recieve", "receive"),
|
||||
("recieved", "received"),
|
||||
("seperate", "separate"),
|
||||
("occured", "occurred"),
|
||||
("occurence", "occurrence"),
|
||||
("thier", "their"),
|
||||
("wich", "which"),
|
||||
("acheive", "achieve"),
|
||||
("accross", "across"),
|
||||
("adress", "address"),
|
||||
("alot", "a lot"),
|
||||
("appearence", "appearance"),
|
||||
("arguement", "argument"),
|
||||
("basicly", "basically"),
|
||||
("beggining", "beginning"),
|
||||
("beleive", "believe"),
|
||||
("calender", "calendar"),
|
||||
("commitee", "committee"),
|
||||
("definately", "definitely"),
|
||||
("dont", "don't"),
|
||||
("doesnt", "doesn't"),
|
||||
("didnt", "didn't"),
|
||||
("cant", "can't"),
|
||||
("wont", "won't"),
|
||||
("isnt", "isn't"),
|
||||
("embarass", "embarrass"),
|
||||
("enviroment", "environment"),
|
||||
("existance", "existence"),
|
||||
("foward", "forward"),
|
||||
("freind", "friend"),
|
||||
("goverment", "government"),
|
||||
("happend", "happened"),
|
||||
("i", "I"),
|
||||
("im", "I'm"),
|
||||
("ive", "I've"),
|
||||
("immediatly", "immediately"),
|
||||
("independant", "independent"),
|
||||
("knowlege", "knowledge"),
|
||||
("libary", "library"),
|
||||
("neccessary", "necessary"),
|
||||
("necesary", "necessary"),
|
||||
("noticable", "noticeable"),
|
||||
("occassion", "occasion"),
|
||||
("posession", "possession"),
|
||||
("prefered", "preferred"),
|
||||
("publically", "publicly"),
|
||||
("realy", "really"),
|
||||
("recomend", "recommend"),
|
||||
("refered", "referred"),
|
||||
("relevent", "relevant"),
|
||||
("succesful", "successful"),
|
||||
("tommorow", "tomorrow"),
|
||||
("tomorow", "tomorrow"),
|
||||
("truely", "truly"),
|
||||
("untill", "until"),
|
||||
("wierd", "weird"),
|
||||
("youre", "you're"),
|
||||
("(c)", "©"),
|
||||
("(r)", "®"),
|
||||
("(tm)", "™"),
|
||||
("(e)", "€"),
|
||||
("...", "…"),
|
||||
("->", "→"),
|
||||
("<-", "←"),
|
||||
("=>", "⇒"),
|
||||
("--", "–"),
|
||||
];
|
||||
|
||||
/// After a space or punctuation, replace a just-typed autocorrect entry.
|
||||
fn autocorrect(s: &mut Session) -> Result<()> {
|
||||
const DAYS: &[&str] = &["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
|
||||
|
||||
/// The replacement for `word` from your entries, then the built-in list.
|
||||
fn replacement(prefs: &crate::Prefs, word: &str) -> Option<String> {
|
||||
if let Some((_, to)) = prefs.ac_entries.iter().find(|(k, _)| k == word) {
|
||||
return Some(to.clone());
|
||||
}
|
||||
if let Some((_, to)) = prefs.ac_entries.iter().find(|(k, _)| k.eq_ignore_ascii_case(word)) {
|
||||
return Some(to.clone());
|
||||
}
|
||||
let (k, to) = AUTOCORRECT.iter().find(|(k, _)| k.eq_ignore_ascii_case(word))?;
|
||||
if prefs.ac_removed.iter().any(|r| r.eq_ignore_ascii_case(k)) {
|
||||
return None;
|
||||
}
|
||||
// "i" only on its own, lower case.
|
||||
if *k == "i" && word != "i" {
|
||||
return None;
|
||||
}
|
||||
Some(if word.chars().next().is_some_and(char::is_uppercase) { ed::change_case(to, "sentence") } else { to.to_string() })
|
||||
}
|
||||
|
||||
/// What a just-typed `word` becomes (`None`: unchanged). `sentence_start`: nothing but spaces
|
||||
/// or a sentence's end (`.`, `!`, `?`) before it in the paragraph. `typing`: one character was
|
||||
/// typed (the case rules apply only then).
|
||||
pub fn correct_word(prefs: &crate::Prefs, word: &str, sentence_start: bool, typing: bool) -> Option<String> {
|
||||
let mut out = replacement(prefs, word);
|
||||
if typing && out.is_none() {
|
||||
let chars: Vec<char> = word.chars().collect();
|
||||
let letters = chars.iter().all(|c| c.is_alphabetic());
|
||||
if prefs.ac_fractions {
|
||||
out = match word {
|
||||
"1/2" => Some("½".into()),
|
||||
"1/4" => Some("¼".into()),
|
||||
"3/4" => Some("¾".into()),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if out.is_none() && letters && chars.len() >= 3 {
|
||||
let upper = |c: &char| c.is_uppercase();
|
||||
let rest_lower = chars.get(2..).is_some_and(|r| r.iter().all(|c| c.is_lowercase()));
|
||||
let rest_upper = chars.get(1..).is_some_and(|r| r.iter().all(upper));
|
||||
if prefs.ac_two_initials && chars.first().is_some_and(upper) && chars.get(1).is_some_and(upper) && rest_lower {
|
||||
out =
|
||||
Some(chars.iter().enumerate().map(|(i, c)| if i == 1 { c.to_lowercase().collect::<String>() } else { c.to_string() }).collect());
|
||||
} else if prefs.ac_caps_lock && chars.first().is_some_and(|c| c.is_lowercase()) && rest_upper {
|
||||
out = Some(
|
||||
chars
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| if i == 0 { c.to_uppercase().collect::<String>() } else { c.to_lowercase().collect() })
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if out.is_none() && prefs.ac_days && DAYS.contains(&word) {
|
||||
out = Some(ed::change_case(word, "sentence"));
|
||||
}
|
||||
}
|
||||
if typing && prefs.ac_sentence_caps && sentence_start {
|
||||
let w = out.clone().unwrap_or_else(|| word.to_string());
|
||||
let mut cs = w.chars();
|
||||
if let Some(f) = cs.next()
|
||||
&& f.is_lowercase()
|
||||
&& w.chars().all(char::is_alphabetic)
|
||||
{
|
||||
out = Some(f.to_uppercase().chain(cs).collect());
|
||||
}
|
||||
}
|
||||
out.filter(|o| o != word)
|
||||
}
|
||||
|
||||
/// After a space or punctuation, correct the word just typed.
|
||||
fn autocorrect(s: &mut Session, typing: bool) -> Result<()> {
|
||||
if !s.prefs.autocorrect {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -341,14 +477,31 @@ fn autocorrect(s: &mut Session) -> Result<()> {
|
||||
if end == 0 || !line.get(end - 1).is_some_and(|c| c.is_whitespace() || ".,;:!?".contains(*c)) {
|
||||
return Ok(());
|
||||
}
|
||||
let word_end = end - 1;
|
||||
let mut word_end = end - 1;
|
||||
let mut start = word_end;
|
||||
while start > 0 && line.get(start - 1).is_some_and(|c| !c.is_whitespace()) {
|
||||
start -= 1;
|
||||
}
|
||||
let word: String = line.get(start..word_end).map(|w| w.iter().collect()).unwrap_or_default();
|
||||
let Some((_, rep)) = AUTOCORRECT.iter().find(|(k, _)| k.eq_ignore_ascii_case(&word)) else { return Ok(()) };
|
||||
let rep = if word.chars().next().is_some_and(char::is_uppercase) { ed::change_case(rep, "sentence") } else { rep.to_string() };
|
||||
// "word," and "word." also count: the punctuation was typed before the space.
|
||||
let mut word: String = line.get(start..word_end).map(|w| w.iter().collect()).unwrap_or_default();
|
||||
if replacement(&s.prefs, &word).is_none() {
|
||||
let trimmed = word.trim_end_matches(|c: char| ".,;:!?".contains(c));
|
||||
if !trimmed.is_empty() && trimmed.len() < word.len() && !word.ends_with("...") {
|
||||
word_end -= word.chars().count() - trimmed.chars().count();
|
||||
word = trimmed.to_string();
|
||||
}
|
||||
}
|
||||
let before = line.get(..start).map(|b| b.iter().rev().find(|c| !c.is_whitespace()).copied()).unwrap_or(None);
|
||||
let sentence_start = before.is_none_or(|c| ".!?".contains(c));
|
||||
// "word..." becomes "word…", the word corrected too.
|
||||
let ellipsis = word.chars().count() > 3 && word.ends_with("...") && replacement(&s.prefs, "...").is_some();
|
||||
let rep = if ellipsis {
|
||||
let stem = word.trim_end_matches('.');
|
||||
Some(format!("{}…", correct_word(&s.prefs, stem, sentence_start, typing).unwrap_or_else(|| stem.to_string())))
|
||||
} else {
|
||||
correct_word(&s.prefs, &word, sentence_start, typing)
|
||||
};
|
||||
let Some(rep) = rep else { return Ok(()) };
|
||||
let delta = rep.chars().count() as isize - word.chars().count() as isize;
|
||||
let st = s.doc_mut()?;
|
||||
let mut d = (*st.doc).clone();
|
||||
@@ -367,6 +520,65 @@ fn autocorrect(s: &mut Session) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn autocorrect_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pr = &mut s.prefs;
|
||||
for (key, slot) in [
|
||||
("on", &mut pr.autocorrect),
|
||||
("twoInitials", &mut pr.ac_two_initials),
|
||||
("sentenceCaps", &mut pr.ac_sentence_caps),
|
||||
("days", &mut pr.ac_days),
|
||||
("capsLock", &mut pr.ac_caps_lock),
|
||||
("fractions", &mut pr.ac_fractions),
|
||||
] {
|
||||
if let Some(v) = p.get(key).and_then(Value::as_bool) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
if let Some(Value::Array(a)) = p.get("add") {
|
||||
let (Some(from), Some(to)) = (a.first().and_then(Value::as_str), a.get(1).and_then(Value::as_str)) else {
|
||||
return Err(bad("text.autocorrect", "`add` is [from, to]"));
|
||||
};
|
||||
let from = from.trim();
|
||||
if from.is_empty() || from.chars().any(char::is_whitespace) || from.chars().count() > 64 || to.chars().count() > 255 {
|
||||
return Err(bad("text.autocorrect", "`from` is one word (up to 64 characters), `to` up to 255"));
|
||||
}
|
||||
pr.ac_entries.retain(|(k, _)| k != from);
|
||||
pr.ac_removed.retain(|k| !k.eq_ignore_ascii_case(from));
|
||||
if pr.ac_entries.len() >= 2000 {
|
||||
return Err(bad("text.autocorrect", "the list is full"));
|
||||
}
|
||||
pr.ac_entries.push((from.to_string(), to.to_string()));
|
||||
}
|
||||
if let Some(from) = str_param(p, "remove") {
|
||||
let had = pr.ac_entries.len();
|
||||
pr.ac_entries.retain(|(k, _)| k != from);
|
||||
if had == pr.ac_entries.len()
|
||||
&& AUTOCORRECT.iter().any(|(k, _)| k.eq_ignore_ascii_case(from))
|
||||
&& !pr.ac_removed.iter().any(|r| r.eq_ignore_ascii_case(from))
|
||||
{
|
||||
pr.ac_removed.push(from.to_string());
|
||||
}
|
||||
}
|
||||
let list: Vec<(String, String)> = pr
|
||||
.ac_entries
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(
|
||||
AUTOCORRECT
|
||||
.iter()
|
||||
.filter(|(k, _)| {
|
||||
!pr.ac_removed.iter().any(|r| r.eq_ignore_ascii_case(k)) && !pr.ac_entries.iter().any(|(e, _)| e.eq_ignore_ascii_case(k))
|
||||
})
|
||||
.map(|(k, v)| (k.to_string(), v.to_string())),
|
||||
)
|
||||
.collect();
|
||||
Ok(json!({
|
||||
"on": pr.autocorrect, "twoInitials": pr.ac_two_initials, "sentenceCaps": pr.ac_sentence_caps,
|
||||
"days": pr.ac_days, "capsLock": pr.ac_caps_lock, "fractions": pr.ac_fractions,
|
||||
"entries": list,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn paste_body(s: &mut Session, src: &TextBody) -> Result<Value> {
|
||||
let pos = edit_text(s, |body, t| {
|
||||
let (a, b) = t.ordered();
|
||||
|
||||
@@ -247,6 +247,18 @@ pub struct Prefs {
|
||||
pub duplicate_offset: f64,
|
||||
pub autocorrect: bool,
|
||||
pub smart_quotes: bool,
|
||||
/// AutoCorrect options (Tools ▸ AutoCorrect): fix TWo INitial CApitals, capitalize the first
|
||||
/// letter of sentences and the names of days, fix accidental cAPS lOCK, and replace 1/2, 1/4
|
||||
/// and 3/4 with fraction characters.
|
||||
pub ac_two_initials: bool,
|
||||
pub ac_sentence_caps: bool,
|
||||
pub ac_days: bool,
|
||||
pub ac_caps_lock: bool,
|
||||
pub ac_fractions: bool,
|
||||
/// Your own "replace … with …" entries; they win over the built-in list.
|
||||
pub ac_entries: Vec<(String, String)>,
|
||||
/// Built-in entries you deleted.
|
||||
pub ac_removed: Vec<String>,
|
||||
pub author: String,
|
||||
pub undo_limit: usize,
|
||||
/// Minutes between AutoRecover saves of unsaved presentations.
|
||||
@@ -262,6 +274,13 @@ impl Default for Prefs {
|
||||
duplicate_offset: 12.0,
|
||||
autocorrect: true,
|
||||
smart_quotes: true,
|
||||
ac_two_initials: true,
|
||||
ac_sentence_caps: true,
|
||||
ac_days: true,
|
||||
ac_caps_lock: true,
|
||||
ac_fractions: true,
|
||||
ac_entries: Vec::new(),
|
||||
ac_removed: Vec::new(),
|
||||
author: "Presenter".into(),
|
||||
undo_limit: 500,
|
||||
recovery_minutes: 1.0,
|
||||
|
||||
@@ -835,3 +835,40 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
|
||||
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
|
||||
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autocorrect_options_as_you_type() {
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
let typed = |s: &mut Session, text: &str| -> String {
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
s.execute("text.selectAll", &json!({})).ok();
|
||||
s.execute("text.delete", &json!({"dir": "backward"})).ok();
|
||||
for c in text.chars() {
|
||||
s.execute("text.insert", &json!({"text": c.to_string()})).unwrap();
|
||||
}
|
||||
s.execute("text.get", &json!({"id": title})).unwrap()["text"].as_str().unwrap().to_string()
|
||||
};
|
||||
assert_eq!(typed(&mut s, "hello world. see you monday, i think. "), "Hello world. See you Monday, I think. ");
|
||||
assert_eq!(typed(&mut s, "THe tHIS 1/2 recieved... "), "The This ½ received… ");
|
||||
// Options turn rules off.
|
||||
s.execute("text.autocorrect", &json!({"sentenceCaps": false, "days": false})).unwrap();
|
||||
assert_eq!(typed(&mut s, "hello monday "), "hello monday ");
|
||||
// Your own entries, and deleting a built-in one.
|
||||
s.execute("text.autocorrect", &json!({"add": ["dc", "DeckCraft"], "remove": "teh"})).unwrap();
|
||||
assert_eq!(typed(&mut s, "dc teh "), "DeckCraft teh ");
|
||||
let v = s.execute("text.autocorrect", &json!({})).unwrap();
|
||||
let entries = v["entries"].as_array().unwrap();
|
||||
assert!(entries.iter().any(|e| e[0] == "dc"));
|
||||
assert!(!entries.iter().any(|e| e[0] == "teh"));
|
||||
assert!(s.execute("text.autocorrect", &json!({"add": ["two words", "x"]})).is_err());
|
||||
// Off entirely.
|
||||
s.execute("text.autocorrect", &json!({"on": false})).unwrap();
|
||||
assert_eq!(typed(&mut s, "dc adn "), "dc adn ");
|
||||
// Pasting a whole string applies only the replacement list, not the case rules.
|
||||
s.execute("text.autocorrect", &json!({"on": true, "sentenceCaps": true})).unwrap();
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
s.execute("text.selectAll", &json!({})).ok();
|
||||
s.execute("text.insert", &json!({"text": "monday notes "})).unwrap();
|
||||
assert_eq!(s.execute("text.get", &json!({"id": title})).unwrap()["text"], "monday notes ");
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ fn title(id: &str) -> &'static str {
|
||||
"paragraph" => "Paragraph",
|
||||
"about" => "About DeckCraft",
|
||||
"preferences" => "Preferences",
|
||||
"autocorrect" => "AutoCorrect",
|
||||
"export" => "Export",
|
||||
"symbol" => "Symbol",
|
||||
"equation" => "Equation",
|
||||
@@ -708,6 +709,48 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
let (ok, _) = buttons(ui, "Close");
|
||||
ok
|
||||
}
|
||||
"autocorrect" => {
|
||||
let pr = &mut app.session.prefs;
|
||||
ui.checkbox(&mut pr.ac_two_initials, "Correct TWo INitial CApitals");
|
||||
ui.checkbox(&mut pr.ac_sentence_caps, "Capitalize first letter of sentences");
|
||||
ui.checkbox(&mut pr.ac_days, "Capitalize names of days");
|
||||
ui.checkbox(&mut pr.ac_caps_lock, "Correct accidental use of cAPS LOCK key");
|
||||
ui.checkbox(&mut pr.ac_fractions, "Replace fractions (1/2) with fraction characters (½)");
|
||||
ui.checkbox(&mut pr.autocorrect, "Replace text as you type");
|
||||
ui.add_space(4.0);
|
||||
let mut from = d.get("from", "");
|
||||
let mut to = d.get("to", "");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Replace");
|
||||
ui.add(egui::TextEdit::singleline(&mut from).desired_width(90.0));
|
||||
ui.label("With");
|
||||
ui.add(egui::TextEdit::singleline(&mut to).desired_width(130.0));
|
||||
if ui.add_enabled(!from.trim().is_empty(), egui::Button::new("Add")).clicked() {
|
||||
run(app, "text.autocorrect", json!({"add": [from.trim(), to]}));
|
||||
from.clear();
|
||||
to.clear();
|
||||
}
|
||||
});
|
||||
d.fields.insert("from".into(), from.clone());
|
||||
d.fields.insert("to".into(), to);
|
||||
let list = app.session.execute("text.autocorrect", &json!({})).ok().and_then(|v| v.get("entries").cloned());
|
||||
let entries: Vec<(String, String)> = list.and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default();
|
||||
let q = from.trim().to_lowercase();
|
||||
egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| {
|
||||
egui::Grid::new("ac-list").striped(true).show(ui, |ui| {
|
||||
for (k, v) in entries.iter().filter(|(k, _)| q.is_empty() || k.to_lowercase().starts_with(&q)) {
|
||||
ui.label(k);
|
||||
ui.label(v);
|
||||
if ui.small_button("Delete").clicked() {
|
||||
run(app, "text.autocorrect", json!({"remove": k}));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
let (ok, cancel) = buttons(ui, "OK");
|
||||
ok || cancel
|
||||
}
|
||||
"preferences" => {
|
||||
ui.label(egui::RichText::new("General").font(theme::bold(13.0)));
|
||||
let mut dark = app.ui.brightness == theme::Brightness::Dark;
|
||||
@@ -722,7 +765,15 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
ui.checkbox(&mut app.session.prefs.smart_guides, "Show smart guides");
|
||||
ui.checkbox(&mut app.session.prefs.snap_to_grid, "Snap objects to grid");
|
||||
ui.checkbox(&mut app.session.prefs.smart_quotes, "Replace straight quotes with smart quotes");
|
||||
ui.checkbox(&mut app.session.prefs.autocorrect, "AutoCorrect as you type");
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut app.session.prefs.autocorrect, "AutoCorrect as you type");
|
||||
if ui.button("AutoCorrect Options…").clicked() {
|
||||
*d = Dialog::new("autocorrect");
|
||||
}
|
||||
});
|
||||
if d.id != "preferences" {
|
||||
return false;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name (comments):");
|
||||
ui.text_edit_singleline(&mut app.session.prefs.author);
|
||||
|
||||
+4
-1
@@ -176,7 +176,9 @@ Detail in [animation-parity.md](animation-parity.md). 10–16 h; the pane is a *
|
||||
|
||||
### Proofing: spelling, thesaurus, AutoCorrect
|
||||
- **Evidence:** `review.spelling` checks a small built-in list of frequent misspellings
|
||||
(`crates/engine/src/cmd/review.rs`); no dictionaries, no thesaurus, no AutoCorrect.
|
||||
(`crates/engine/src/cmd/review.rs`); no dictionaries, no thesaurus. AutoCorrect has PowerPoint's options (two initial capitals,
|
||||
sentence and day capitals, caps lock, fractions, an editable replace list); no AutoFormat of
|
||||
ordinals or automatic bulleted lists, no undo of a single correction.
|
||||
- **Estimate:** 8–14 h (Hunspell-format dictionaries under an open licence per language).
|
||||
|
||||
### Equations
|
||||
@@ -256,6 +258,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | AutoCorrect options: case rules, fractions, ellipsis, a larger built-in list and your own entries (`text.autocorrect`, Tools ▸ AutoCorrect) |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
| 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines |
|
||||
| 2026-10-12 | minor | EMF/WMF text, pattern and hatch brushes drawn; legacy OLE and ActiveX previews shown; the SVG gap keeps only EMF clip regions |
|
||||
|
||||
@@ -156,7 +156,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
|
||||
| Text | Find and replace, replace fonts | P1 | P |
|
||||
| Text | Spelling check | P1 | P |
|
||||
| Text | Thesaurus | P2 | M |
|
||||
| Text | AutoCorrect and smart quotes | P2 | M |
|
||||
| Text | AutoCorrect and smart quotes | P2 | Y |
|
||||
| Text | Symbols and emoji | P1 | D |
|
||||
| Text | Equations | P2 | M |
|
||||
| Text | Hyperlinks (URL, slide, email) | P1 | D |
|
||||
|
||||
Reference in New Issue
Block a user