From 4bc08a1084701f1e6a07f1e54ab07ed83019a74d Mon Sep 17 00:00:00 2001 From: Deng Liu Date: Mon, 9 Nov 2026 00:27:45 -0800 Subject: [PATCH] fix(windows): open loose external files in the focused project window (#923) * fix(windows): route loose external files to active project window * fix(windows): open loose files in the focused window only Loose files join the focused window when it holds a local project or LightEdit. A Welcome or remote window, or a project window in the background, keeps the previous routing: the LightEdit window, else a free window, else a new one. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> --- src-tauri/src/windows.rs | 79 +++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/src-tauri/src/windows.rs b/src-tauri/src/windows.rs index 423952fc3..601f0cb2a 100644 --- a/src-tauri/src/windows.rs +++ b/src-tauri/src/windows.rs @@ -946,10 +946,11 @@ enum ProjectTarget { } /// Routes canonical `paths` (with whether each is a folder) over `windows` (the preferred -/// free one first). A folder focuses the window that has it open, else takes a free window, +/// one first). A folder focuses the window that has it open, else takes a free window, /// else a new one. A file inside a local project being opened (the deepest root if nested) -/// opens in that project's window; the others all join the LightEdit window, else a free -/// window or a new one switches to LightEdit for them. +/// opens in that project's window; the others join the preferred window when it holds a local +/// project or LightEdit, else the LightEdit window, else a free window or a new one switches to +/// LightEdit for them. pub fn plan_open(paths: Vec<(PathBuf, bool)>, windows: &[(String, Holds)]) -> Vec { let mut free = windows.iter().filter(|(_, h)| *h == Holds::Nothing).map(|(l, _)| l.clone()); let (mut dirs, mut files) = (Vec::new(), Vec::new()); @@ -1009,13 +1010,23 @@ pub fn plan_open(paths: Vec<(PathBuf, bool)>, windows: &[(String, Holds)]) -> Ve None => loose.push(file), } } + let preferred = windows.first().filter(|(_, h)| matches!(h, Holds::Project(_) | Holds::LightEdit)); + let light = windows.iter().find(|(_, h)| *h == Holds::LightEdit); + match preferred.or(light) { + Some((label, _)) if !loose.is_empty() => { + let files = std::mem::take(&mut loose); + match in_windows.iter_mut().find(|(l, _)| l == label) { + Some((_, list)) => list.extend(files), + None => in_windows.push((label.clone(), files)), + } + } + _ => {} + } steps.extend(in_windows.into_iter().map(|(label, files)| OpenStep::Files(label, files))); if !loose.is_empty() { - let light = windows.iter().find(|(_, h)| *h == Holds::LightEdit).map(|(l, _)| l.clone()); - steps.push(match (light, free.next()) { - (Some(label), _) => OpenStep::Files(label, loose), - (None, Some(label)) => OpenStep::Launch(label, path_launch_of(None, loose)), - (None, None) => OpenStep::New(None, loose), + steps.push(match free.next() { + Some(label) => OpenStep::Launch(label, path_launch_of(None, loose)), + None => OpenStep::New(None, loose), }); } steps @@ -2023,11 +2034,13 @@ mod tests { assert_eq!( steps, [ - OpenStep::Files("main".into(), vec![p("/proj/src/a.rs"), p("/proj/e.rs")]), + // `/projects` is not inside `/proj`; loose files join the focused project window. + OpenStep::Files( + "main".into(), + vec![p("/proj/src/a.rs"), p("/proj/e.rs"), p("/x/b.txt"), p("/projects/d.rs")], + ), // The deepest root wins. OpenStep::Files("project-2".into(), vec![p("/proj/sub/c.rs")]), - // `/projects` is not inside `/proj`. - OpenStep::Files("project-1".into(), vec![p("/x/b.txt"), p("/projects/d.rs")]), ] ); } @@ -2088,18 +2101,13 @@ mod tests { #[test] fn files_opened_from_a_window_go_to_their_project_window_else_light_edit() { - let windows = [ - w("main", Holds::Nothing), - w("project-1", Holds::Project(p("/proj"))), - w("project-2", Holds::Nothing), - w("project-3", Holds::Other), - ]; - let files = vec![p("/proj/src/a.rs"), p("/x/b.txt")]; - // The Welcome window turns into LightEdit itself; another free window doesn't take the files. - let (steps, here) = plan_open_files("main", files.clone(), &windows); - assert_eq!(steps, [OpenStep::Files("project-1".into(), vec![p("/proj/src/a.rs")])]); + let welcome_only = [w("main", Holds::Nothing), w("project-2", Holds::Nothing), w("project-3", Holds::Other)]; + // With no local project or LightEdit window, the Welcome window turns into LightEdit itself. + let (steps, here) = plan_open_files("main", vec![p("/x/b.txt")], &welcome_only); + assert!(steps.is_empty()); assert_eq!(here, [p("/x/b.txt")]); // A LightEdit window keeps loose files, even with another one open. + let files = vec![p("/proj/src/a.rs"), p("/x/b.txt")]; let light = [w("main", Holds::LightEdit), w("project-1", Holds::Project(p("/proj"))), w("project-2", Holds::LightEdit)]; let (steps, here) = plan_open_files("main", files.clone(), &light); @@ -2109,8 +2117,20 @@ mod tests { let others = [w("main", Holds::Nothing), w("project-2", Holds::LightEdit)]; let (steps, here) = plan_open_files("main", vec![p("/x/b.txt")], &others); assert_eq!((steps, here), (vec![OpenStep::Files("project-2".into(), vec![p("/x/b.txt")])], vec![])); + // A project window in the background doesn't take them from a Welcome window. + let with_proj = [ + w("main", Holds::Nothing), + w("project-1", Holds::Project(p("/proj"))), + w("project-2", Holds::Nothing), + w("project-3", Holds::Other), + ]; + let (steps, here) = plan_open_files("main", files.clone(), &with_proj); + assert_eq!(steps, [OpenStep::Files("project-1".into(), vec![p("/proj/src/a.rs")])]); + assert_eq!(here, [p("/x/b.txt")]); // A remote project window gets a new LightEdit window, never a free one or its own. - let (steps, here) = plan_open_files("project-3", files, &windows); + let remote = + [w("project-3", Holds::Other), w("main", Holds::Nothing), w("project-1", Holds::Project(p("/proj")))]; + let (steps, here) = plan_open_files("project-3", files, &remote); assert_eq!( steps, [OpenStep::Files("project-1".into(), vec![p("/proj/src/a.rs")]), OpenStep::New(None, vec![p("/x/b.txt")])] @@ -2119,13 +2139,22 @@ mod tests { } #[test] - fn files_opened_while_running_take_a_free_window_else_a_new_one() { - let free = [w("main", Holds::Project(p("/proj"))), w("project-1", Holds::Nothing)]; + fn files_opened_while_running_join_the_focused_window_else_a_free_one_else_a_new_one() { + // The focused local project window takes them, even with a LightEdit window open. + let project = + [w("main", Holds::Project(p("/proj"))), w("project-1", Holds::Nothing), w("project-2", Holds::LightEdit)]; + assert_eq!( + plan_open(vec![(p("/x/a.txt"), false)], &project), + [OpenStep::Files("main".into(), vec![p("/x/a.txt")])] + ); + // A project window in the background doesn't. + let free = + [w("main", Holds::Other), w("project-2", Holds::Project(p("/proj"))), w("project-1", Holds::Nothing)]; assert_eq!( plan_open(vec![(p("/x/a.txt"), false)], &free), [OpenStep::Launch("project-1".into(), path_launch_of(None, vec![p("/x/a.txt")]))] ); - let busy = [w("main", Holds::Project(p("/proj"))), w("project-1", Holds::Other)]; + let busy = [w("main", Holds::Other)]; assert_eq!(plan_open(vec![(p("/x/a.txt"), false)], &busy), [OpenStep::New(None, vec![p("/x/a.txt")])]); }