mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 08:16:24 +00:00
`file.open` on a path already open in a tab switched to it and returned its index, even when another process had changed the file since, so the caller kept looking at the old copy with nothing saying so (#997). The tab's disk stamp from #1330 (`DocState::on_disk`) now tells: - the result says `stale: true` when the file on disk differs from what the tab read or last saved; - `reload: true` reads the file again into the tab, as Revert to saved does (refused while the tab has unsaved changes or is in a live session), and the result says `reloaded: true`. MCP's `open_file` takes `reload` and passes it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.3 KiB
Rust
111 lines
5.3 KiB
Rust
//! 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);
|
|
}
|
|
|
|
/// The name of the page the active tab shows.
|
|
fn page_in_tab(s: &Session) -> String {
|
|
let ds = s.ds().unwrap();
|
|
ds.doc.node(ds.page).unwrap().name.clone()
|
|
}
|
|
|
|
/// Opening a file that is already open says when it changed on disk since the tab read or saved
|
|
/// it, and `reload` reads it again into the tab, unless the tab has unsaved changes (#997).
|
|
#[test]
|
|
fn opening_an_open_file_says_it_is_stale_and_reloads_on_request() {
|
|
let path = temp_file("reopen");
|
|
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();
|
|
let index = app.execute("file.open", &json!({"path": path})).unwrap()["index"].clone();
|
|
let before = page_in_tab(&app);
|
|
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index}), "unchanged: not stale");
|
|
let mut cli = Session::new();
|
|
cli.execute("file.open", &json!({"path": path})).unwrap();
|
|
cli.execute("page.rename", &json!({"name": "Renamed on disk by the CLI"})).unwrap();
|
|
cli.execute("file.save", &json!({})).unwrap();
|
|
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index, "stale": true}));
|
|
assert_eq!(page_in_tab(&app), before, "without reload the tab keeps what it read");
|
|
assert_eq!(app.execute("file.open", &json!({"path": path, "reload": true})).unwrap(), json!({"index": index, "reloaded": true}));
|
|
assert_eq!(page_in_tab(&app), "Renamed on disk by the CLI");
|
|
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index}), "read again: no longer stale");
|
|
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
|
|
let e = app.execute("file.open", &json!({"path": path, "reload": true})).unwrap_err().to_string();
|
|
assert!(e.contains("unsaved changes"), "{e}");
|
|
assert!(app.ds().unwrap().is_dirty(), "the unsaved edit is still there");
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|