Files
vectorcraft/crates/engine/src/cmd/fileio/batch.rs
T
dotCooCoo 6cd57cfa1b Undo groups: an open undo group also groups the edits in a document opened or created before its end
A document opened or created between edit.undoGroup.begin and
edit.undoGroup.end opens an undo group of its own with the begun
group's name, so an atomic command_batch that starts with file.new or
document.open is one undo step in the new document. The group opens
after the command that opens the document: when that command also
edits it (a command.batch), those edits stay the command's own undo
step, and a canceled group keeps them, so the journal still replays.
Both commands also run with no document open, and a command.batch that
fails rolls back a begun group. Switching to a document that was
already open does not open a group in it.
2026-10-24 20:10:04 +07:00

114 lines
5.6 KiB
Rust

//! `command.batch`: several commands as one undo step (in each document they edit).
use serde_json::{Value, json};
use super::super::*;
use crate::{DocState, EngineError};
pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
let mut cmds = p.get("commands").and_then(Value::as_array).ok_or_else(|| bad("command.batch", "missing commands"))?.clone();
let label = str_param(p, "label").unwrap_or("Batch").to_string();
// The active artboard the app passes, for the steps that act on it.
let artboard = p.get("artboard").and_then(Value::as_u64);
// What an error rolls back besides the documents: session-level state a step may change
// (paint defaults, drawing mode, clipboard, the Untitled-N count, a begun undo group).
let saved = (s.paint.clone(), s.fill_active, s.draw_mode, s.draw_inside, s.clipboard.clone(), s.untitled_counter, s.explicit_undo_group.clone());
// A drag or typing still in progress is a step of its own, as before Undo: the batch's steps
// would otherwise run inside it and take its baseline as theirs (#1697).
if s.active().is_some() {
super::super::edit::settle_interaction(s)?;
}
// The documents as they are now, whole: what a failed step brings back. Their art, history,
// file, format and saved state share their data with the open ones until a step changes them.
let start = s.docs.clone();
let active = s.active;
s.batch_stash = Some(vec![]);
let r = run_steps(s, &label, artboard, &mut cmds);
let stash = s.batch_stash.take().unwrap_or_default();
let results = match r {
Ok(results) => results,
Err(e) => {
roll_back(s, start, &stash);
s.active = active;
(s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter, s.explicit_undo_group) = saved;
s.pending_paint = None;
return Err(e);
}
};
// Each document the steps edited keeps them as one undo step.
s.remember_pending_paint();
for st in &mut s.docs {
if let Some(it) = st.interaction.take() {
st.keep_interaction(it);
}
}
// The batch journals itself, whichever document it ends in.
let mut entry = p.clone();
if let Some(o) = entry.as_object_mut() {
o.insert("commands".into(), Value::Array(cmds));
}
s.journal.push(("command.batch".into(), entry));
Ok(json!({ "results": results }))
}
/// Run the steps → their results. Each runs inside an interaction of the document active when it
/// starts (one for all the steps in that document), so a step may open, switch or close documents.
/// A step that acts on the active artboard ([`crate::ON_ACTIVE_ARTBOARD`]) and names none gets
/// `artboard`, as the Artboards panel shows it in the document the step runs in: past that
/// document's last artboard, the last one. A step that resizes the active artboard
/// ([`crate::FIT_ACTIVE_ARTBOARD`]) and names no `index` gets that artboard as its `index`.
fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Value]) -> Result<Vec<Value>> {
let mut results = Vec::with_capacity(cmds.len());
for c in cmds {
let id = c.get("command").and_then(Value::as_str).unwrap_or("").to_string();
if id == "command.batch" {
return Err(bad("command.batch", "batches cannot nest"));
}
if s.active().is_some() {
s.begin_interaction(label)?;
}
let mut params = c.get("params").cloned().unwrap_or(json!({}));
let key = if crate::ON_ACTIVE_ARTBOARD.contains(&id.as_str()) {
Some("artboard")
} else {
crate::FIT_ACTIVE_ARTBOARD.contains(&id.as_str()).then_some("index")
};
if let Some(a) = artboard
&& let Some(key) = key
&& params.get(key).is_none()
&& let Some(last) = s.active().and_then(|d| d.doc.artboards.len().checked_sub(1))
&& let Some(o) = params.as_object_mut()
{
o.insert(key.into(), json!(usize::try_from(a).map_or(last, |a| a.min(last))));
}
let (v, noted) =
s.execute_step(&id, &params).map_err(|e| EngineError::Other(format!("batch step {} (`{id}`) failed: {e}", results.len())))?;
// The step's journal entry records what it took from the preferences or the clock (a
// save's date, a new document's), as a command of its own would.
if noted != params
&& let Some(c) = c.as_object_mut()
{
c.insert("params".into(), noted);
}
results.push(v);
}
Ok(results)
}
/// The documents as they were before the batch (`start`), whatever the steps did to them: opened
/// ones close, closed or replaced ones (`stash`) come back, and each has its art, selection,
/// history, file, format and saved state again (#1697: undoing the batch's interaction alone left
/// a Save As step's new file and an Undo step's history in place). Files the steps wrote stay
/// written. Lock Guides, Lock Slices and Hide Slices stay as the steps left them, and the guides
/// and slices they keep from being selected are deselected.
fn roll_back(s: &mut Session, start: Vec<DocState>, stash: &[DocState]) {
let open = std::mem::take(&mut s.docs);
s.docs = start;
for st in &mut s.docs {
// Past the newest revision the document reached, so the restored one redraws.
let newest = open.iter().chain(stash).filter(|d| d.uid == st.uid).map(|d| d.revision).max().unwrap_or(st.revision);
st.revision = newest.saturating_add(1);
crate::deselect_hidden(&s.menu, &st.doc, &mut st.selection);
}
}