Make zed foo/ add to active window and open sidebar (#53254)

Changes the default CLI behavior when opening directories:

- `zed foo/` now adds `foo/` as a new workspace in the active window's
MultiWorkspace and opens the sidebar, instead of creating a new window.
If `bar/` was previously open, both `foo/` and `bar/` are retained as
separate workspaces in the sidebar.
- `zed -n foo/` continues to open a new window without the sidebar
(unchanged behavior).
- `zed -a foo/` continues to add to the existing workspace (unchanged
behavior).

The new behavior is gated on `multi_workspace_enabled` (the `agent-v2`
feature flag). Without the flag, directories open in a new window as
before, avoiding a situation where the workspace gets replaced with no
sidebar to switch back.

The change is contained to `open_paths()` in
`crates/workspace/src/workspace.rs`. When no CLI flag is specified and
no existing workspace matches the paths, we now look for the active
window and set `requesting_window` so that `Workspace::new_local` adds
the new workspace to it rather than creating a new window.

Release Notes:

- N/A
This commit is contained in:
Eric Holk authored and GitHub committed 2026-04-06 18:43:14 +00:00
1 parent 7748047051
commit d22f8bf2a7
2 files changed
+62 -37

No files matched your search

+32 -4
View File
@@ -9325,7 +9325,7 @@ pub fn open_workspace_by_id(
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: Arc<AppState>,
open_options: OpenOptions,
mut open_options: OpenOptions,
cx: &mut App,
) -> Task<anyhow::Result<OpenResult>> {
let abs_paths = abs_paths.to_vec();
@@ -9350,10 +9350,9 @@ pub fn open_paths(
let all_metadatas = futures::future::join_all(all_paths)
.await
.into_iter()
.filter_map(|result| result.ok().flatten())
.collect::<Vec<_>>();
.filter_map(|result| result.ok().flatten());
if all_metadatas.iter().all(|file| !file.is_dir) {
if all_metadatas.into_iter().all(|file| !file.is_dir) {
cx.update(|cx| {
let windows = workspace_windows_for_location(
&SerializedWorkspaceLocation::Local,
@@ -9375,6 +9374,35 @@ pub fn open_paths(
}
}
// Fallback for directories: when no flag is specified and no existing
// workspace matched, add the directory as a new workspace in the
// active window's MultiWorkspace (instead of opening a new window).
if open_options.open_new_workspace.is_none() && existing.is_none() {
let target_window = cx.update(|cx| {
let windows = workspace_windows_for_location(
&SerializedWorkspaceLocation::Local,
cx,
);
let window = cx
.active_window()
.and_then(|window| window.downcast::<MultiWorkspace>())
.filter(|window| windows.contains(window))
.or_else(|| windows.into_iter().next());
window.filter(|window| {
window.read(cx).is_ok_and(|mw| mw.multi_workspace_enabled(cx))
})
});
if let Some(window) = target_window {
open_options.requesting_window = Some(window);
window
.update(cx, |multi_workspace, _, cx| {
multi_workspace.open_sidebar(cx);
})
.log_err();
}
}
let open_in_dev_container = open_options.open_in_dev_container;
let result = if let Some((existing, target_workspace)) = existing {
+30 -33
View File
@@ -2606,18 +2606,33 @@ mod tests {
})
.await
.unwrap();
assert_eq!(cx.read(|cx| cx.windows().len()), 2);
// Replace existing windows
let window = cx
.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
assert_eq!(cx.read(|cx| cx.windows().len()), 1);
cx.run_until_parked();
multi_workspace_1
.update(cx, |multi_workspace, _window, cx| {
assert_eq!(multi_workspace.workspaces().len(), 2);
assert!(multi_workspace.sidebar_open());
let workspace = multi_workspace.workspace().read(cx);
assert_eq!(
workspace
.worktrees(cx)
.map(|w| w.read(cx).abs_path())
.collect::<Vec<_>>(),
&[
Path::new(path!("/root/c")).into(),
Path::new(path!("/root/d")).into(),
]
);
})
.unwrap();
// Opening with -n (open_new_workspace: Some(true)) still creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/e"))],
app_state,
workspace::OpenOptions {
requesting_window: Some(window),
open_new_workspace: Some(true),
..Default::default()
},
cx,
@@ -2627,23 +2642,6 @@ mod tests {
.unwrap();
cx.background_executor.run_until_parked();
assert_eq!(cx.read(|cx| cx.windows().len()), 2);
let multi_workspace_1 = cx
.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
.unwrap();
multi_workspace_1
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace().read(cx);
assert_eq!(
workspace
.worktrees(cx)
.map(|w| w.read(cx).abs_path())
.collect::<Vec<_>>(),
&[Path::new(path!("/root/e")).into()]
);
assert!(workspace.right_dock().read(cx).is_open());
assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
})
.unwrap();
}
#[gpui::test]
@@ -2724,7 +2722,6 @@ mod tests {
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let window1 = cx.update(|cx| cx.active_window().unwrap());
cx.update(|cx| {
open_paths(
@@ -2738,6 +2735,8 @@ mod tests {
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a directory with default options adds to the existing window
// rather than creating a new one.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/dir2"))],
@@ -2748,25 +2747,23 @@ mod tests {
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
let window2 = cx.update(|cx| cx.active_window().unwrap());
assert!(window1 != window2);
cx.update_window(window1, |_, window, _| window.activate_window())
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a directory with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/dir2/c"))],
&[PathBuf::from(path!("/root/dir2"))],
app_state.clone(),
workspace::OpenOptions::default(),
workspace::OpenOptions {
open_new_workspace: Some(true),
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
// should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
}
#[gpui::test]