From 72cf1a9b3ccf3b60c97de75287ed3b26ac559018 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:56:56 +0530 Subject: [PATCH] Queued tab activations keep the file access scope they were asked in A document asked for again while a dialog was open was queued by path only, and opened later in the scope of the frame that ran the queue. A request confined to automation roots could then read a file outside them, if the document was closed meanwhile and its path became a link out of the roots. Each queued activation now carries its ScopeHandle and runs in it, as background opens already do. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/io/open.rs | 5 +++-- crates/ui-egui/src/opening.rs | 8 ++++---- crates/ui-egui/src/tests/home.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/io/open.rs b/crates/ui-egui/src/io/open.rs index 370fc9c06..d9d2b470a 100644 --- a/crates/ui-egui/src/io/open.rs +++ b/crates/ui-egui/src/io/open.rs @@ -282,8 +282,9 @@ pub(crate) fn open_file(app: &mut VectorcraftApp, path: &str, background: bool) // a drag acts on the active document, its tab comes to the front once that ends // ([`crate::opening::poll`]), as a file that finishes opening then does. if document_index_for_path(app, path).is_some() && !crate::opening::may_switch(app) { - if !app.opening.activate.iter().any(|p| same_open_path(p, path)) { - app.opening.activate.push(path.to_string()); + if !app.opening.activate.iter().any(|(p, _)| same_open_path(p, path)) { + let scope = vectorcraft_engine::file_access::ScopeHandle::capture(); + app.opening.activate.push((path.to_string(), scope)); } return Ok(Value::Null); } diff --git a/crates/ui-egui/src/opening.rs b/crates/ui-egui/src/opening.rs index 37eca5b62..e23463493 100644 --- a/crates/ui-egui/src/opening.rs +++ b/crates/ui-egui/src/opening.rs @@ -30,8 +30,8 @@ pub struct Pending { pub struct Opening { pub pending: Vec, /// Open documents asked for again while the active one couldn't change: their tabs come to - /// the front once it can ([`may_switch`]). - pub activate: Vec, + /// the front once it can ([`may_switch`]), each in the file access scope it was asked in. + pub activate: Vec<(String, ScopeHandle)>, } /// Is a long operation running that should show the busy cursor and status spinner (background @@ -106,9 +106,9 @@ pub fn poll(app: &mut VectorcraftApp) { if !may_switch(app) { return; } - for path in std::mem::take(&mut app.opening.activate) { + for (path, scope) in std::mem::take(&mut app.opening.activate) { // Closed meanwhile, it opens again; a failure is reported to the user there. - let _ = crate::io::open_reporting(app, &path); + let _ = scope.run(|| crate::io::open_reporting(app, &path)); } let mut i = 0; while let Some(p) = app.opening.pending.get(i) { diff --git a/crates/ui-egui/src/tests/home.rs b/crates/ui-egui/src/tests/home.rs index 9474408b7..96f50716d 100644 --- a/crates/ui-egui/src/tests/home.rs +++ b/crates/ui-egui/src/tests/home.rs @@ -409,3 +409,32 @@ fn reopening_while_a_dialog_is_open_waits_for_it_to_close() { assert_eq!((app.session.documents().len(), app.session.active_index()), (2, Some(0))); let _ = std::fs::remove_dir_all(d); } + +/// A document asked for under automation roots while a dialog is open comes to the front, or +/// opens again, in those roots once the dialog closes, not in the scope of the frame that runs it +/// (#5369): closed meanwhile and now a link to a file outside the roots, it is not read. +#[cfg(unix)] +#[test] +fn a_queued_reopen_keeps_the_roots_it_was_asked_in() { + use vectorcraft_engine::file_access; + let (mut app, d, _) = saved_and_another("queued-roots"); + let (inside, outside) = (d.join("inside"), d.join("outside")); + std::fs::create_dir_all(&inside).unwrap(); + std::fs::create_dir_all(&outside).unwrap(); + let path = inside.join("doc.vectorcraft").to_string_lossy().to_string(); + app.run("document.activate", json!({"index": 0})).unwrap(); + app.run("file.saveAs", json!({"path": path})).unwrap(); + app.run("document.activate", json!({"index": 1})).unwrap(); + app.ui.dialog = Some(crate::state::Dialog::new(crate::dialogs::save_options::KIND, json!({}))); + let roots = file_access::AutomationRoots::new(&[inside.as_path()], &[]).unwrap(); + assert_eq!(file_access::confine(roots.as_ref(), || crate::io::open_reporting(&mut app, &path)).unwrap(), serde_json::Value::Null); + // Closed, and its file swapped for a link to one outside the roots. + app.session.execute("file.close", &json!({"index": 0})).unwrap(); + let away = outside.join("doc.vectorcraft"); + std::fs::rename(&path, &away).unwrap(); + std::os::unix::fs::symlink(&away, &path).unwrap(); + app.ui.dialog = None; + crate::opening::poll(&mut app); + assert_eq!(app.session.documents().len(), 1, "the file outside the roots is not opened"); + let _ = std::fs::remove_dir_all(d); +}