From 387873ae05e515033ed02451fe78b8b05d0c094c Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 04:20:33 +0200 Subject: [PATCH] Zotero: a pending citation no longer edits a replacement document (#470) (#580) Co-authored-by: Brandon Thomas --- crates/ui-egui/src/zotero.rs | 32 +++++++++++++++++++++++++++++++- docs/zotero.md | 4 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/zotero.rs b/crates/ui-egui/src/zotero.rs index a8dc1a4ef..0437df573 100644 --- a/crates/ui-egui/src/zotero.rs +++ b/crates/ui-egui/src/zotero.rs @@ -38,6 +38,8 @@ pub struct ZoteroLink { alert: Option, /// The command in flight. pub busy: Option, + /// The document the command in flight started in; its calls are refused once another is open. + document: u64, /// How the last command ended. pub last: Option, /// Zotero's calls in the command in flight (logged when it ends). @@ -96,6 +98,7 @@ fn start(app: &mut WordApp, cmd: Command, ctx: Option) -> Result< if let Some(b) = app.zotero.busy { return Err(format!("Zotero is still working on {}; finish in Zotero's window first", b.wire_name())); } + let document = app.session.document_id(); let (call_tx, call_rx) = channel::(); let (done_tx, done_rx) = channel(); let wake = ctx.clone(); @@ -123,6 +126,7 @@ fn start(app: &mut WordApp, cmd: Command, ctx: Option) -> Result< z.calls = Some(call_rx); z.done = Some(done_rx); z.busy = Some(cmd); + z.document = document; log::info!("Zotero: {} started", cmd.wire_name()); app.status(tl!("Waiting for Zotero…")); Ok(json!({"started": cmd.wire_name()})) @@ -156,7 +160,7 @@ pub fn poll(app: &mut WordApp, ctx: &egui::Context) { // One alert at a time: Zotero sends nothing else while it waits for the answer. while app.zotero.alert.is_none() { let Some(p) = app.zotero.calls.as_ref().and_then(|rx| rx.try_recv().ok()) else { break }; - if p.call.method == "Document_displayAlert" { + if p.call.method == "Document_displayAlert" && app.session.document_id() == app.zotero.document { let text = p.call.args.get(1).and_then(Value::as_str).unwrap_or("").to_string(); let buttons = p.call.args.get(3).and_then(Value::as_i64).unwrap_or(0); app.zotero.methods.push(p.call.method.clone()); @@ -164,6 +168,11 @@ pub fn poll(app: &mut WordApp, ctx: &egui::Context) { continue; } app.zotero.methods.push(p.call.method.clone()); + if app.session.document_id() != app.zotero.document { + // New or Open replaced the document this command started in: its edits must not land in the new one. + let _ = p.reply.send(Err("WordCraft replaced the document this command started in".into())); + continue; + } let rev = app.session.rev(); let mut host = AppHost { ctx }; let r = app.zotero.bridge.handle(&mut app.session, &mut host, &p.call); @@ -313,6 +322,27 @@ mod tests { assert_eq!(app.session.doc.plain_text(StoryRef::Body), "Cite: "); } + #[test] + fn a_pending_citation_does_not_insert_into_a_replacement_document() { + let (addr, server) = fake_zotero(vec![ + json!(["Application_getActiveDocument", [3]]), + json!(["Document_insertField", [1, "ReferenceMark", 0]]), + json!(["Field_setCode", [1, 1, "ITEM CSL_CITATION {}"]]), + json!(["Field_setText", [1, 1, "(Sample, 2020)", false]]), + json!(["Document_complete", [1]]), + ]); + let mut app = WordApp::new(Session::new(Document::new()), Services::default()); + app.zotero.options.addr = addr; + let ctx = egui::Context::default(); + app.ctx = Some(ctx.clone()); + app.run("ui.zotero.addEditCitation", json!({})).unwrap(); + app.run("file.new", json!({"template": "blank"})).unwrap(); + pump(&mut app, &ctx, |a| a.zotero.busy.is_none()); + server.join().unwrap(); + assert_eq!(app.session.doc.plain_text(StoryRef::Body), ""); + assert!(!app.session.can_undo(), "the old command left no undo step in the new document"); + } + #[test] fn no_zotero_is_a_clear_error() { let free = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap(); diff --git a/docs/zotero.md b/docs/zotero.md index 4041691d3..f05b62c6e 100644 --- a/docs/zotero.md +++ b/docs/zotero.md @@ -21,7 +21,9 @@ Citations (each runs one of the commands below). They run in the app (and through the control channel and MCP bridged to the app). A command returns at once with `{"started": …}`; Zotero then works in its own window and WordCraft answers -it in the background. All changes from one command are one undo step. +it in the background. All changes from one command are one undo step. If New or Open replaces +the document while a command is running, Zotero's remaining calls for it get an error and the +new document is left as it is. From a terminal, against a file: