Merge upstream main a930276 into feat/ime: this branch's IME model replaces text.compose

#84 landed on main squashed (0f4f382b0), so the merge was resolved against #84's
tip as the base: main's version of #84 stands and only this branch's IME changes
are applied on top.

Conflicts and how each was resolved:
- crates/engine/src/cmd/text.rs: removed main's text.compose command, compose_text
  and its compose_tests; text.insert keeps main's reset of the typing format after a
  successful edit, skipped while an IME preview is open; reselection bumps the
  revision.
- crates/engine/src/lib.rs: removed Session.composition and the hook that ended it
  in execute_unguarded; set_active/add_document end the composition and reset the
  Find scope; both test modules kept.
- crates/engine/src/tooling.rs: removed main's composition() and its
  end_composition (the opposite semantics); begin_interaction no longer ends a
  composition.
- crates/engine/src/cmd/edit.rs, file.rs, guard.rs, recovery.rs: main's code with
  bump_revision in place of revision arithmetic; Revert takes the fresh state's
  revision.
- crates/ui-egui/src/canvas.rs: removed main's IME block, composition highlight and
  remembered caret rect; this branch's ime_event/ime_output stay. Main's dead-key
  test is ported to these events (the accent stays as typed when the caret moves).
- crates/ui-egui/src/lib.rs, menus.rs: both sides' fields and checks.
- apps/designcraft/src/main.rs, apps/designcraft-web/src/web.rs: both sides.
- docs/control-protocol.md: ui.ime row plus main's ui.click/ui.drag params.

Also: the remaining `revision += 1` sites on main use bump_revision, and two tests
compare revisions with `>`; the IME candidate window follows the caret on type on a
path; IME status lines in ROADMAP.md, gaps, ui-, localization- and target-app-parity
docs and roadmap.md.
This commit is contained in:
aiongg
2026-10-15 06:23:59 +00:00
257 changed files with 44669 additions and 3177 deletions
+34 -6
View File
@@ -1,5 +1,7 @@
//! Window › Utilities › Scripts: run a command script (see [`crate::script`]) as one undo step.
use std::sync::Arc;
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, cmd, str_param};
@@ -8,15 +10,23 @@ use crate::Result;
fn run(s: &mut crate::Session, p: &Value) -> Result<Value> {
let text = str_param(p, "text").ok_or_else(|| bad("script.run", "`text` required"))?;
let steps = crate::script::parse(text).map_err(|e| bad("script.run", e))?;
// The steps' own undo entries collapse into this command's single one.
let uid = s.active().map(|d| d.uid);
let before = s.active().map_or(0, |d| d.history.undo.len());
// The steps' own undo entries collapse into this command's single one. A step that fails
// rolls the successful prefix back (document, selection and history), so a half-run script
// never strands edits that can't be undone (#709).
let before = s.active().map(|d| (d.uid, d.doc.clone(), d.selection.clone(), d.history.clone()));
let report = s.run_script(&steps);
if let Some(st) = s.active_mut()
&& Some(st.uid) == uid
&& st.history.undo.len() > before
&& let Some((uid, doc, selection, history)) = &before
&& st.uid == *uid
{
st.history.undo.truncate(before);
if report.failed.is_some() {
st.doc = Arc::clone(doc);
st.selection = selection.clone();
st.history = history.clone();
st.bump_revision();
} else if st.history.undo.len() > history.undo.len() {
st.history.undo.truncate(history.undo.len());
}
}
let out = json!({"results": report.results, "steps": steps.len()});
match report.failed {
@@ -56,4 +66,22 @@ mod tests {
assert_eq!(count(&s), 0, "one undo removes the whole script's work");
assert!(s.execute("script.run", &json!({"text": "nope.command"})).is_err());
}
/// A script that fails after a successful step leaves the document and its history exactly as
/// they were: the half-run prefix is not stranded without an undo entry (#709).
#[test]
fn a_failed_script_rolls_the_successful_prefix_back() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let count = |s: &Session| s.doc().unwrap().doc.spreads[0].items.len();
let err = s.execute("script.run", &json!({"text": "frame.create {\"rect\": [72, 72, 200, 200]}\nnope.command"})).unwrap_err();
assert!(err.to_string().contains("step 1") && err.to_string().contains("nope.command"), "{err}");
assert_eq!(count(&s), 0, "the successful prefix is rolled back");
assert!(s.execute("edit.undo", &json!({})).is_err(), "nothing was left to undo");
// The session keeps working and its undo history is as it was.
s.execute("frame.create", &json!({"rect": [72, 72, 200, 200]})).unwrap();
assert_eq!(count(&s), 1);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(count(&s), 0);
}
}