Fix #3455: focus open tab from Home/Open Recent only

## Summary
Home › Recent Files and File › Open Recent switch to an already-open document tab instead of calling document.open again. document.open is unchanged for File › Open, MCP, and save-and-reopen tests.

## Tests
- home_recent_reopen_focuses_an_already_open_document (ui-egui)
- Removed engine open_same_path_activates_existing_document

Closes storytold/vectorcraft#3455

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-10-22 11:01:58 +07:00
committed by bflatastic
co-authored by nrq6993-a11y
parent 11eb27b127
commit e4db022f49
6 changed files with 68 additions and 45 deletions
-24
View File
@@ -369,11 +369,6 @@ pub fn open_bytes(s: &mut Session, name: &str, bytes: &[u8], path: Option<String
/// [`open_bytes`] with the `document.open` options in `p` ([`LoadOptions::from_params`]).
pub fn open_bytes_with(s: &mut Session, name: &str, bytes: &[u8], path: Option<String>, p: &Value) -> Result<Value> {
if let Some(path) = path.as_deref()
&& let Some(index) = document_index_for_path(s, path)
{
return activate_open_document(s, index);
}
let update_links = s.prefs.update_links == "automatically";
open_read(s, read_for_open(name, bytes, path, p, update_links)?)
}
@@ -709,22 +704,3 @@ fn same_file(a: &str, b: &str) -> bool {
_ => a == b,
}
}
/// The open document whose saved path is the same file as `path`, if any.
fn document_index_for_path(s: &Session, path: &str) -> Option<usize> {
s.documents().iter().position(|st| st.path.as_deref().is_some_and(|p| same_file(p, path)))
}
/// Bring an already-open document to the front instead of reading the file again (Home › Recent).
fn activate_open_document(s: &mut Session, index: usize) -> Result<Value> {
s.set_active(index);
let st = &s.documents()[index];
Ok(json!({
"index": index,
"title": st.title(),
"format": st.format,
"warnings": [],
"restored": false,
"activated": true,
}))
}
-19
View File
@@ -363,25 +363,6 @@ fn templates_open_untitled() {
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn open_same_path_activates_existing_document() {
let mut s = Session::new();
let dir = tmp_dir("reopen");
let path = dir.join("doc.vectorcraft").to_string_lossy().to_string();
let mut fresh = Session::new();
fresh.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
std::fs::write(&path, b64(&fresh.execute("document.serialize", &json!({})).unwrap())).unwrap();
s.execute("document.open", &json!({"path": path})).unwrap();
assert_eq!(s.documents().len(), 1);
s.execute("shape.rectangle", &json!({"x": 1, "y": 2, "width": 10, "height": 10})).unwrap();
let r = s.execute("document.open", &json!({"path": path})).unwrap();
assert_eq!(r["activated"], true);
assert_eq!(s.documents().len(), 1);
assert_eq!(s.active_index(), Some(0));
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 1);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn open_exts_cover_every_readable_format() {
for f in FORMATS.iter().filter(|f| f.read) {
+1 -1
View File
@@ -3094,7 +3094,7 @@ fn recent(app: &mut VectorcraftApp, ui: &mut Ui) {
for path in files {
if recent_row(ui, &path).clicked() {
// A failure is reported to the user there.
let _ = crate::io::open_reporting(app, &path);
let _ = crate::io::open_recent_reporting(app, &path);
}
}
}
+38
View File
@@ -214,6 +214,44 @@ pub fn open_reporting(app: &mut VectorcraftApp, path: &str) -> Result<Value, Str
r
}
/// Home › Recent Files and File › Open Recent: focus an already-open tab for that path instead of
/// calling `document.open` again (#3455).
pub fn open_recent_reporting(app: &mut VectorcraftApp, path: &str) -> Result<Value, String> {
if let Some(r) = try_activate_open_document(app, path)? {
return Ok(r);
}
open_reporting(app, path)
}
fn try_activate_open_document(app: &mut VectorcraftApp, path: &str) -> Result<Option<Value>, String> {
let Some(index) = document_index_for_path(app, path) else {
return Ok(None);
};
let st = &app.session.documents()[index];
let r = json!({
"index": index,
"title": st.title(),
"format": st.format,
"warnings": [],
"restored": false,
"activated": true,
});
app.run("document.activate", json!({ "index": index }))?;
opened(app, &r);
Ok(Some(r))
}
fn document_index_for_path(app: &VectorcraftApp, path: &str) -> Option<usize> {
app.session.documents().iter().position(|st| st.path.as_deref().is_some_and(|p| same_open_path(p, path)))
}
fn same_open_path(a: &str, b: &str) -> bool {
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
fn read(app: &VectorcraftApp, path: &str) -> Result<Vec<u8>, String> {
app.services.read.as_ref().ok_or("no file reader")?(path)
}
+1 -1
View File
@@ -1266,7 +1266,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"file.cancelExport" => Ok(io::cancel_exports(app)),
"file.exportInBackground" => Ok(io::hide_exports(app)),
id if id.starts_with("file.openRecent") => match recent_slot(app, id).cloned() {
Some(path) => io::open_reporting(app, &path),
Some(path) => io::open_recent_reporting(app, &path),
None => Err("no such recent file".into()),
},
"type.findFont" => {
+28
View File
@@ -297,3 +297,31 @@ fn home_scrolls_in_a_short_window() {
visible_home_texts(&mut app, &ctx, size, 4.1, vec![press(false)]);
assert_eq!(app.session.documents().len(), 1, "the card opened its preset");
}
/// Home › Recent Files and File › Open Recent focus an open tab instead of opening a second copy
/// (#3455); `document.open` is unchanged.
#[test]
fn home_recent_reopen_focuses_an_already_open_document() {
let services = crate::Services {
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
..Default::default()
};
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), services);
let d = std::env::temp_dir().join(format!("vc-home-reopen-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let path = d.join("doc.vectorcraft").to_string_lossy().to_string();
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run("shape.rectangle", json!({"x": 1, "y": 2, "width": 10, "height": 10})).unwrap();
app.run("file.saveAs", json!({"path": path})).unwrap();
app.run("file.new", json!({})).unwrap();
assert_eq!(app.session.documents().len(), 2);
assert_eq!(app.session.active_index(), Some(1));
let r = crate::io::open_recent_reporting(&mut app, &path).unwrap();
assert_eq!(r["activated"], true);
assert_eq!(app.session.documents().len(), 2);
assert_eq!(app.session.active_index(), Some(0));
assert_eq!(app.session.doc().unwrap().doc.layers[0].children().unwrap().len(), 1);
let _ = std::fs::remove_dir_all(d);
}