M1: Save no longer overwrites changes another app made to the open file (#1330)

With a file open in the desktop app, figcraft-cli could change it on disk;
the next Save (or auto-save) wrote the app's copy over it and said
"Saved", losing the CLI's changes.

Each tab now remembers its file's size and modification time when it
opens, reverts or saves it (the Stamp library files already use), and
Save and auto-save refuse while the file on disk differs, pointing to
Revert to saved or Save as. Scripts pass `file.save {overwrite: true}`.
Save as and saves to an explicit path are unchanged.
This commit is contained in:
Ridzzz0Alam
2026-10-26 11:04:01 +01:00
parent 63d926876f
commit a648700fcc
5 changed files with 124 additions and 15 deletions
@@ -0,0 +1,76 @@
//! Saving never silently replaces what another app wrote to the open file since it was opened or
//! saved here (#1330): Save and auto-save refuse, `overwrite` and Revert to saved settle it.
use serde_json::json;
use crate::Session;
use crate::cmd::file::FileSnapshot;
fn temp_file(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("figcraft-disk-changes-{}-{name}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir.join("TESTE.figcraft").to_string_lossy().into_owned()
}
/// What the file on disk names its first page.
fn page_on_disk(path: &str) -> String {
let mut s = Session::new();
s.execute("file.open", &json!({"path": path})).unwrap();
let ds = s.ds().unwrap();
ds.doc.node(ds.page).unwrap().name.clone()
}
/// The app has `path` open with an unsaved edit, after figcraft-cli renamed its page on disk.
fn app_and_cli(path: &str) -> Session {
let mut setup = Session::new();
setup.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
setup.execute("file.save", &json!({"path": path})).unwrap();
let mut app = Session::new();
app.execute("file.open", &json!({"path": path})).unwrap();
let mut cli = Session::new();
cli.execute("file.open", &json!({"path": path})).unwrap();
cli.execute("page.rename", &json!({"name": "CLI VENCEU"})).unwrap();
cli.execute("file.save", &json!({})).unwrap();
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
app
}
#[test]
fn saving_over_changes_made_on_disk_is_refused_until_asked() {
let path = temp_file("save");
let mut app = app_and_cli(&path);
let err = app.execute("file.save", &json!({})).unwrap_err().to_string();
assert!(err.contains("changed on disk"), "{err}");
assert_eq!(page_on_disk(&path), "CLI VENCEU", "the CLI's change is still in the file");
assert!(app.ds().unwrap().is_dirty(), "the app's edit stays unsaved");
// Asked to, the save replaces it; from then on the file is the app's again.
app.execute("file.save", &json!({"overwrite": true})).unwrap();
assert_ne!(page_on_disk(&path), "CLI VENCEU");
app.execute("shape.rectangle", &json!({"x": 40, "y": 0, "width": 10, "height": 10})).unwrap();
app.execute("file.save", &json!({})).unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn reverting_loads_the_changes_made_on_disk_and_saves_again() {
let path = temp_file("revert");
let mut app = app_and_cli(&path);
app.execute("file.revert", &json!({})).unwrap();
let ds = app.ds().unwrap();
assert_eq!(ds.doc.node(ds.page).unwrap().name, "CLI VENCEU");
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
app.execute("file.save", &json!({})).unwrap();
assert_eq!(page_on_disk(&path), "CLI VENCEU", "saved on top of the CLI's change");
let _ = std::fs::remove_file(&path);
}
#[test]
fn auto_save_leaves_changes_made_on_disk_alone() {
let path = temp_file("auto");
let mut app = app_and_cli(&path);
let snapshot = FileSnapshot::of(app.ds().unwrap());
let bytes = snapshot.encode().unwrap();
assert!(snapshot.save(&mut app, &bytes).is_err());
assert_eq!(page_on_disk(&path), "CLI VENCEU");
let _ = std::fs::remove_file(&path);
}