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 <[email protected]>
Цей коміт міститься в:
Deng Liuтаl0ng-ai authored and GitHub committed 2026-11-09 16:27:45 +08:00
1 джерело cb2f249db5
коміт 4bc08a1084
1 file changed
+54 -25
+54 -25
Переглянути файл
@@ -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<OpenStep> {
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")])]);
}