forked from 1b-agent/fieldcraft-upstream
Undo: a new model is untitled, and undoing it brings the old file back (#214)
geometry.open with "model": "new" cleared the model but kept the session's file path, so File > Save wrote the new model over the previous one. The path and the revision saved there (which document the session is) were not part of the undo state. A step now records the document when its command changed it: the new model is untitled and unsaved; undo restores the old file and its clean state, redo the untitled model (or the file it was saved to since). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
2188d891ff
commit
06b98f01c7
3 files changed
+90
-4
No files matched your search
@@ -218,7 +218,7 @@ impl Session {
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.chars().take(100).collect()))?;
|
||||
// Session commands (open, save, undo, redo) change the session only in their last step,
|
||||
// after everything that can fail, so they need no copy of the model to restore.
|
||||
let before = (spec.kind == CommandKind::Model).then(|| self.snapshot());
|
||||
let before = (spec.kind == CommandKind::Model).then(|| (self.snapshot(), self.document()));
|
||||
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, params)));
|
||||
let result = match outcome {
|
||||
Ok(r) => r,
|
||||
@@ -228,10 +228,15 @@ impl Session {
|
||||
Err(EngineError::Internal { command: id.to_string(), message })
|
||||
}
|
||||
};
|
||||
if let Some(before) = before {
|
||||
if let Some((mut before, document)) = before {
|
||||
// A command that makes the session another document (a new model) keeps the old
|
||||
// one in its undo step.
|
||||
if self.document() != document {
|
||||
before.document = Some(document);
|
||||
}
|
||||
if result.is_err() {
|
||||
self.restore(before);
|
||||
} else if self.differs_from(&before) {
|
||||
} else if before.document.is_some() || self.differs_from(&before) {
|
||||
self.journal.push((id.to_string(), params.clone()));
|
||||
self.revision = next_revision();
|
||||
self.record_undo(before);
|
||||
@@ -716,4 +721,53 @@ mod tests {
|
||||
assert!(s.execute("geometry.open", &json!({"path": path, "model": "discard"})).is_err());
|
||||
assert!(s.design.is_none() && s.model == defined.model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_model_is_untitled_and_undo_brings_the_file_back() {
|
||||
let (dir, design) = sample_design_file(&Plate::default());
|
||||
let file = dir.0.join("a.fieldcraft").to_string_lossy().into_owned();
|
||||
let mut s = defined_model("Seal", "no-such-face");
|
||||
s.execute("file.save_as", &json!({"path": file})).unwrap();
|
||||
assert!(!s.is_dirty());
|
||||
|
||||
s.execute("geometry.open", &json!({"path": design, "model": "new"})).unwrap();
|
||||
assert_eq!(s.path, None, "a new model must not save over the old model's file");
|
||||
assert!(s.is_dirty());
|
||||
assert!(s.execute("file.save", &json!({})).is_err(), "save asks for a file");
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), Some(file.as_str()));
|
||||
assert!(!s.is_dirty(), "undo returns to the saved state of the old file");
|
||||
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.path, None);
|
||||
assert!(s.is_dirty());
|
||||
|
||||
// Saving the new model and then editing it: undoing the edit keeps the new file.
|
||||
let b = dir.0.join("b.fieldcraft").to_string_lossy().into_owned();
|
||||
s.execute("file.save_as", &json!({"path": b})).unwrap();
|
||||
s.execute("model.material.create", &json!({"name": "Al", "elastic": {"type": "isotropic", "young": 70000, "poisson": 0.33}})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), Some(b.as_str()));
|
||||
assert!(!s.is_dirty());
|
||||
// Undoing the open after that save returns to the old file; redo to the saved new one.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), Some(file.as_str()));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), Some(b.as_str()));
|
||||
assert!(!s.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeping_the_model_keeps_its_file_and_a_sample_has_none() {
|
||||
let (dir, design) = sample_design_file(&Plate::default());
|
||||
let file = dir.0.join("a.fieldcraft").to_string_lossy().into_owned();
|
||||
let mut s = defined_model("Seal", "no-such-face");
|
||||
s.execute("file.save_as", &json!({"path": file})).unwrap();
|
||||
s.execute("geometry.open", &json!({"path": design})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), Some(file.as_str()));
|
||||
s.execute("file.sample", &json!({"name": "cantilever"})).unwrap();
|
||||
assert_eq!(s.path, None, "a sample must not save over the last opened file");
|
||||
assert!(s.execute("file.save", &json!({})).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user