From 95a769eedc44bcfb33a18e95b9c61b49e41bfd61 Mon Sep 17 00:00:00 2001 From: Eduardo Soares Date: Mon, 12 Oct 2026 22:55:19 -0300 Subject: [PATCH] Open: opening an already-open file focuses its tab (#1620) Co-authored-by: GLM --- crates/ui-egui/src/last_session.rs | 2 +- crates/ui-egui/src/lib.rs | 16 ++++++++++++++++ crates/ui-egui/tests/editing.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/last_session.rs b/crates/ui-egui/src/last_session.rs index bf64ea7ab..7bc4833c0 100644 --- a/crates/ui-egui/src/last_session.rs +++ b/crates/ui-egui/src/last_session.rs @@ -52,7 +52,7 @@ impl LastSession { /// `path` made absolute against the current directory, without resolving links (and without /// Windows' `\\?\` prefix, which `canonicalize` adds). #[cfg(not(target_arch = "wasm32"))] -fn absolute(path: &str) -> String { +pub(crate) fn absolute(path: &str) -> String { std::path::absolute(path).map_or_else(|_| path.to_string(), |p| p.to_string_lossy().into_owned()) } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 91d534802..f34fd63de 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1208,8 +1208,24 @@ impl PdfCraftApp { } } + /// The tab already showing the file at `path`, if any: its position in `views`. Paths are + /// compared after `absolute` normalization, so another spelling of the same file matches + /// (#1620). + #[cfg(not(target_arch = "wasm32"))] + fn view_showing(&self, path: &str) -> Option { + let path = crate::last_session::absolute(path); + self.views + .iter() + .position(|v| self.session.get(v.id).and_then(|d| d.path.as_deref()).map(crate::last_session::absolute).as_deref() == Some(path.as_str())) + } + #[cfg(not(target_arch = "wasm32"))] pub fn open_path(&mut self, path: &str) { + if let Some(i) = self.view_showing(path) { + self.startup_superseded = true; + self.active = Some(i); + return; + } let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string()); match std::fs::read(path) { Ok(bytes) => { diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 17990d2fb..6a177831d 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -653,6 +653,36 @@ fn the_last_session_is_off_by_default_and_skips_missing_and_duplicate_files() { assert!(later.views.is_empty(), "neither opens from the session"); } +#[test] +fn opening_the_same_path_again_focuses_its_tab() { + let pdfs = TempPdfs::new("dedup-open"); + let a = pdfs.file("a.pdf", 1); + let b = pdfs.file("b.pdf", 1); + let mut h = session_harness(|_| {}); + h.state_mut().open_path(&a); + h.state_mut().open_path(&b); + h.run_steps(2); + assert_eq!(h.state().views.len(), 2, "two different files open two tabs"); + h.state_mut().open_path(&a); + h.run_steps(2); + assert_eq!(h.state().views.len(), 2, "opening an open file again adds no tab"); + assert_eq!(h.state().active, Some(0), "the existing tab comes to the front"); + assert_eq!(open_paths(h.state()), vec![a, b], "the tab order is untouched"); +} + +#[test] +fn opening_the_same_file_with_a_dot_component_focuses_its_tab() { + let pdfs = TempPdfs::new("dedup-dot"); + let a = pdfs.file("a.pdf", 1); + let dotted = format!("{}/./a.pdf", pdfs.0.display()); + let mut h = session_harness(|_| {}); + h.state_mut().open_path(&a); + h.state_mut().open_path(&dotted); + h.run_steps(2); + assert_eq!(h.state().views.len(), 1, "another spelling of the same file matches"); + assert_eq!(h.state().active, Some(0)); +} + #[test] fn save_prompt_stays_inside_the_screen_for_a_long_filename() { // Issue #161: an unwrapped title carrying a long filename widened the centered modal past