cli: Fix -n behavior and refactor open options (#53939)

This fixes a regression where `zed -n .` in a subdirectory of an
already-open
project would redirect to the parent window instead of creating a new
one.
The root cause was that commit 66d2cb20c9 ("Adjust `zed -n` behavior")
made
`-n` run the worktree matching loop with subdirectory matching enabled,
when
previously `-n` skipped matching entirely.

## Changes

### Bug fix
- **Restore `-n` to always create a new window.** No worktree matching,
no
exceptions. This matches the behavior from when `-n` was first
introduced.

### New `--classic` flag
- Adds a hidden `--classic` CLI flag that explicitly selects the
pre-sidebar
default behavior: new window for directories, reuse existing window for
  files already in an open worktree.
- The `cli_default_open_behavior` setting now toggles between `-e` (add
to
sidebar) and `--classic` behavior. When set to `new_window`, the classic
  logic is used instead of unconditionally opening a new window.

### Refactor CLI open options
Replaces the old grab-bag of `open_new_workspace: Option<bool>`,
`force_existing_window: bool`, `classic: bool`, and `reuse: bool` with:

- **`cli::CliOpenBehavior` enum** — a single enum on the IPC boundary
with
variants `Default`, `AlwaysNew`, `Add`, `ExistingWindow`, `Classic`, and
  `Reuse`.
- **`workspace::WorkspaceMatching` enum** — describes how to match paths
against existing worktrees (`None`, `MatchExact`, `MatchSubdirectory`).
- **`workspace::OpenOptions`** — uses `WorkspaceMatching` plus a simple
  `add_dirs_to_sidebar: bool` instead of overlapping boolean flags.

The translation from CLI enum to workspace options happens in
`open_listener.rs`, keeping both layers clean and independent.

Release Notes:

- N/A
This commit is contained in:
Eric Holk 2026-04-14 21:58:04 -07:00 committed by GitHub
parent dc5e2f1c24
commit ad5d015490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 308 additions and 182 deletions

View file

@ -145,9 +145,10 @@
// an explicit `-e` (existing window) or `-n` (new window) flag.
//
// May take 2 values:
// 1. Add to the existing Zed window
// 1. Open directories as a new workspace in the current Zed window's sidebar
// "cli_default_open_behavior": "existing_window"
// 2. Open a new Zed window
// 2. Open directories in a new window (reuse existing windows for files
// that are already part of an open project)
// "cli_default_open_behavior": "new_window"
"cli_default_open_behavior": "existing_window",
// Whether to attempt to restore previous file's state when opening it again.

View file

@ -9,10 +9,45 @@ pub struct IpcHandshake {
pub responses: ipc::IpcReceiver<CliResponse>,
}
/// Controls how CLI paths are opened — whether to reuse existing windows,
/// create new ones, or add to the sidebar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OpenBehavior {
/// Consult the user's `cli_default_open_behavior` setting to choose between
/// `ExistingWindow` or `Classic`.
#[default]
Default,
/// Always create a new window. No matching against existing worktrees.
/// Corresponds to `zed -n`.
AlwaysNew,
/// Match broadly including subdirectories, and fall back to any existing
/// window if no worktree matched. Corresponds to `zed -a`.
Add,
/// Open directories as a new workspace in the current Zed window's sidebar.
/// Reuse existing windows for files in open worktrees.
/// Corresponds to `zed -e`.
ExistingWindow,
/// New window for directories, reuse existing window for files in open
/// worktrees. The classic pre-sidebar behavior.
/// Corresponds to `zed --classic`.
Classic,
/// Replace the content of an existing window with a new workspace.
/// Corresponds to `zed -r`.
Reuse,
}
/// The setting-level enum for configuring default behavior. This only has
/// two values because the other modes are always explicitly requested via
/// CLI flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CliOpenBehavior {
pub enum CliBehaviorSetting {
/// Open directories as a new workspace in the current Zed window's sidebar.
ExistingWindow,
/// Classic behavior: open directories in a new window, but reuse an
/// existing window when opening files that are already part of an open
/// project.
NewWindow,
}
@ -25,16 +60,14 @@ pub enum CliRequest {
diff_all: bool,
wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
#[serde(default)]
force_existing_window: bool,
reuse: bool,
open_behavior: OpenBehavior,
env: Option<HashMap<String, String>>,
user_data_dir: Option<String>,
dev_container: bool,
},
SetOpenBehavior {
behavior: CliOpenBehavior,
behavior: CliBehaviorSetting,
},
}

View file

@ -67,17 +67,20 @@ struct Args {
#[arg(short, long)]
wait: bool,
/// Add files to the currently open workspace
#[arg(short, long, overrides_with_all = ["new", "reuse", "existing"])]
#[arg(short, long, overrides_with_all = ["new", "reuse", "existing", "classic"])]
add: bool,
/// Create a new workspace
#[arg(short, long, overrides_with_all = ["add", "reuse", "existing"])]
#[arg(short, long, overrides_with_all = ["add", "reuse", "existing", "classic"])]
new: bool,
/// Reuse an existing window, replacing its workspace
#[arg(short, long, overrides_with_all = ["add", "new", "existing"], hide = true)]
#[arg(short, long, overrides_with_all = ["add", "new", "existing", "classic"], hide = true)]
reuse: bool,
/// Open in existing Zed window
#[arg(short = 'e', long = "existing", overrides_with_all = ["add", "new", "reuse"])]
#[arg(short = 'e', long = "existing", overrides_with_all = ["add", "new", "reuse", "classic"])]
existing: bool,
/// Use the classic open behavior: new window for directories, reuse for files
#[arg(long, hide = true, overrides_with_all = ["add", "new", "reuse", "existing"])]
classic: bool,
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
/// This overrides the default platform-specific data directory location:
#[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
@ -538,16 +541,20 @@ fn main() -> Result<()> {
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
let open_new_workspace = if args.new {
Some(true)
let open_behavior = if args.new {
cli::OpenBehavior::AlwaysNew
} else if args.add {
Some(false)
cli::OpenBehavior::Add
} else if args.existing {
cli::OpenBehavior::ExistingWindow
} else if args.classic {
cli::OpenBehavior::Classic
} else if args.reuse {
cli::OpenBehavior::Reuse
} else {
None
cli::OpenBehavior::Default
};
let force_existing_window = args.existing;
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
@ -676,9 +683,7 @@ fn main() -> Result<()> {
diff_all: diff_all_mode,
wsl,
wait: args.wait,
open_new_workspace,
force_existing_window,
reuse: args.reuse,
open_behavior,
env,
user_data_dir: user_data_dir_for_thread,
dev_container: args.dev_container,
@ -697,7 +702,7 @@ fn main() -> Result<()> {
}
CliResponse::PromptOpenBehavior => {
let behavior = prompt_open_behavior()
.unwrap_or(cli::CliOpenBehavior::ExistingWindow);
.unwrap_or(cli::CliBehaviorSetting::ExistingWindow);
tx.send(CliRequest::SetOpenBehavior { behavior })?;
}
}
@ -796,15 +801,18 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
/// Shows an interactive prompt asking the user to choose the default open
/// behavior for `zed <path>`. Returns `None` if the prompt cannot be shown
/// (e.g. stdin is not a terminal) or the user cancels.
fn prompt_open_behavior() -> Option<cli::CliOpenBehavior> {
fn prompt_open_behavior() -> Option<cli::CliBehaviorSetting> {
if !std::io::stdin().is_terminal() {
return None;
}
let blue = console::Style::new().blue();
let items = [
format!("Add to existing Zed window ({})", blue.apply_to("zed -e")),
format!("Open a new window ({})", blue.apply_to("zed -n")),
format!(
"Add to existing Zed window ({})",
blue.apply_to("zed --existing")
),
format!("Open a new window ({})", blue.apply_to("zed --classic")),
];
let prompt = format!(
@ -821,9 +829,9 @@ fn prompt_open_behavior() -> Option<cli::CliOpenBehavior> {
.ok()?;
Some(if selection == 0 {
cli::CliOpenBehavior::ExistingWindow
cli::CliBehaviorSetting::ExistingWindow
} else {
cli::CliOpenBehavior::NewWindow
cli::CliBehaviorSetting::NewWindow
})
}

View file

@ -400,11 +400,12 @@ impl CloseWindowWhenNoItems {
)]
#[serde(rename_all = "snake_case")]
pub enum CliDefaultOpenBehavior {
/// Add to the existing Zed window as a new workspace.
/// Open directories as a new workspace in the current Zed window's sidebar.
#[default]
#[strum(serialize = "Add to Existing Window")]
ExistingWindow,
/// Open a new Zed window.
/// Open directories in a new window, but reuse an existing window when
/// opening files that are already part of an open project.
#[strum(serialize = "Open a New Window")]
NewWindow,
}

View file

@ -143,7 +143,7 @@ fn general_page() -> SettingsPage {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "CLI Default Open Behavior",
description: "How `zed <path>` opens directories when no `-e` or `-n` flag is specified.",
description: "How `zed <path>` opens directories when no flag is specified.",
field: Box::new(SettingField {
json_path: Some("cli_default_open_behavior"),
pick: |settings_content| {

View file

@ -9259,35 +9259,31 @@ pub async fn find_existing_workspace(
let mut open_visible = OpenVisible::All;
let mut best_match = None;
cx.update(|cx| {
for window in workspace_windows_for_location(location, cx) {
if let Ok(multi_workspace) = window.read(cx) {
for workspace in multi_workspace.workspaces() {
let project = workspace.read(cx).project.read(cx);
let m = project.visibility_for_paths(
abs_paths,
open_options.open_new_workspace == None,
cx,
);
if m > best_match {
existing = Some((window, workspace.clone()));
best_match = m;
} else if best_match.is_none() && open_options.open_new_workspace == Some(false)
{
existing = Some((window, workspace.clone()))
if open_options.workspace_matching != WorkspaceMatching::None {
cx.update(|cx| {
for window in workspace_windows_for_location(location, cx) {
if let Ok(multi_workspace) = window.read(cx) {
for workspace in multi_workspace.workspaces() {
let project = workspace.read(cx).project.read(cx);
let m = project.visibility_for_paths(
abs_paths,
open_options.workspace_matching != WorkspaceMatching::MatchSubdirectory,
cx,
);
if m > best_match {
existing = Some((window, workspace.clone()));
best_match = m;
} else if best_match.is_none()
&& open_options.workspace_matching
== WorkspaceMatching::MatchSubdirectory
{
existing = Some((window, workspace.clone()))
}
}
}
}
}
});
});
// With -n, only reuse a window if the path is genuinely contained
// within an existing worktree (don't fall back to any arbitrary window).
if open_options.open_new_workspace == Some(true) && best_match.is_none() {
existing = None;
}
if open_options.open_new_workspace != Some(true) {
let all_paths_are_files = existing
.as_ref()
.and_then(|(_, target_workspace)| {
@ -9310,11 +9306,7 @@ pub async fn find_existing_workspace(
})
.unwrap_or(false);
if open_options.open_new_workspace.is_none()
&& existing.is_some()
&& open_options.wait
&& all_paths_are_files
{
if open_options.wait && existing.is_some() && all_paths_are_files {
cx.update(|cx| {
let windows = workspace_windows_for_location(location, cx);
let window = cx
@ -9335,12 +9327,32 @@ pub async fn find_existing_workspace(
(existing, open_visible)
}
#[derive(Default, Clone)]
/// Controls whether to reuse an existing workspace whose worktrees contain the
/// given paths, and how broadly to match.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum WorkspaceMatching {
/// Always open a new workspace. No matching against existing worktrees.
None,
/// Match paths against existing worktree roots and files within them.
#[default]
MatchExact,
/// Match paths against existing worktrees including subdirectories, and
/// fall back to any existing window if no worktree matched.
///
/// For example, `zed -a foo/bar` will activate the `bar` workspace if it
/// exists, otherwise it will open a new window with `foo/bar` as the root.
MatchSubdirectory,
}
#[derive(Clone)]
pub struct OpenOptions {
pub visible: Option<OpenVisible>,
pub focus: Option<bool>,
pub open_new_workspace: Option<bool>,
pub force_existing_window: bool,
pub workspace_matching: WorkspaceMatching,
/// Whether to add unmatched directories to the existing window's sidebar
/// rather than opening a new window. Defaults to true, matching the default
/// `cli_default_open_behavior` setting.
pub add_dirs_to_sidebar: bool,
pub wait: bool,
pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
pub open_mode: OpenMode,
@ -9348,9 +9360,25 @@ pub struct OpenOptions {
pub open_in_dev_container: bool,
}
impl Default for OpenOptions {
fn default() -> Self {
Self {
visible: None,
focus: None,
workspace_matching: WorkspaceMatching::default(),
add_dirs_to_sidebar: true,
wait: false,
requesting_window: None,
open_mode: OpenMode::default(),
env: None,
open_in_dev_container: false,
}
}
}
impl OpenOptions {
fn should_reuse_existing_window(&self) -> bool {
self.open_new_workspace.is_none() && self.open_mode != OpenMode::NewWindow
self.workspace_matching != WorkspaceMatching::None && self.open_mode != OpenMode::NewWindow
}
}
@ -9541,11 +9569,7 @@ pub fn open_paths(
&& existing.is_none()
&& open_options.requesting_window.is_none()
{
let use_existing_window = open_options.force_existing_window
|| cx.update(|cx| {
WorkspaceSettings::get_global(cx).cli_default_open_behavior
== settings::CliDefaultOpenBehavior::ExistingWindow
});
let use_existing_window = open_options.add_dirs_to_sidebar;
if use_existing_window {
let target_window = cx.update(|cx| {

View file

@ -2022,7 +2022,7 @@ pub fn open_new_ssh_project_from_project(
paths,
app_state,
workspace::OpenOptions {
open_new_workspace: Some(true),
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
@ -2583,13 +2583,13 @@ mod tests {
})
.unwrap();
// Opening with -n (open_new_workspace: Some(true)) still creates a new window.
// Opening with -n (reuse_worktrees: false) still creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/e"))],
app_state,
workspace::OpenOptions {
open_new_workspace: Some(true),
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
@ -2630,7 +2630,7 @@ mod tests {
&[PathBuf::from(path!("/root/a"))],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(false),
workspace_matching: workspace::WorkspaceMatching::MatchSubdirectory,
..Default::default()
},
cx,
@ -2640,29 +2640,13 @@ mod tests {
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a file inside the existing worktree with -n reuses the window.
// Opening a file inside the existing worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/dir/c"))],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(true),
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a path NOT in any existing worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/b"))],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(true),
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
@ -2671,6 +2655,22 @@ mod tests {
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
// Opening a path NOT in any existing worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/b"))],
app_state.clone(),
workspace::OpenOptions {
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 3);
}
#[gpui::test]
@ -2723,29 +2723,13 @@ mod tests {
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a directory already in a worktree with -n reuses the window.
// Opening a directory already in a worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root/dir2"))],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(true),
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
// Opening a directory NOT in any worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root"))],
app_state.clone(),
workspace::OpenOptions {
open_new_workspace: Some(true),
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
@ -2754,6 +2738,22 @@ mod tests {
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 2);
// Opening a directory NOT in any worktree with -n creates a new window.
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/root"))],
app_state.clone(),
workspace::OpenOptions {
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
cx,
)
})
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 3);
}
#[gpui::test]

View file

@ -462,9 +462,7 @@ pub async fn handle_cli_connection(
diff_all,
wait,
wsl,
mut open_new_workspace,
mut force_existing_window,
reuse,
mut open_behavior,
env,
user_data_dir: _,
dev_container,
@ -499,7 +497,7 @@ pub async fn handle_cli_connection(
return;
}
if open_new_workspace.is_none() && !force_existing_window && !reuse {
if open_behavior == cli::OpenBehavior::Default {
match resolve_open_behavior(
&paths,
&app_state,
@ -510,10 +508,10 @@ pub async fn handle_cli_connection(
.await
{
Some(settings::CliDefaultOpenBehavior::ExistingWindow) => {
force_existing_window = true;
open_behavior = cli::OpenBehavior::ExistingWindow;
}
Some(settings::CliDefaultOpenBehavior::NewWindow) => {
open_new_workspace = Some(true);
open_behavior = cli::OpenBehavior::Classic;
}
None => {}
}
@ -525,9 +523,7 @@ pub async fn handle_cli_connection(
paths,
diff_paths,
diff_all,
open_new_workspace,
force_existing_window,
reuse,
open_behavior,
responses.as_ref(),
wait,
dev_container,
@ -629,10 +625,10 @@ async fn resolve_open_behavior(
if let Some(CliRequest::SetOpenBehavior { behavior }) = requests.next().await {
let behavior = match behavior {
cli::CliOpenBehavior::ExistingWindow => {
cli::CliBehaviorSetting::ExistingWindow => {
settings::CliDefaultOpenBehavior::ExistingWindow
}
cli::CliOpenBehavior::NewWindow => settings::CliDefaultOpenBehavior::NewWindow,
cli::CliBehaviorSetting::NewWindow => settings::CliDefaultOpenBehavior::NewWindow,
};
let fs = app_state.fs.clone();
@ -652,9 +648,7 @@ async fn open_workspaces(
paths: Vec<String>,
diff_paths: Vec<[String; 2]>,
diff_all: bool,
open_new_workspace: Option<bool>,
force_existing_window: bool,
reuse: bool,
open_behavior: cli::OpenBehavior,
responses: &dyn CliResponseSink,
wait: bool,
dev_container: bool,
@ -662,7 +656,7 @@ async fn open_workspaces(
env: Option<collections::HashMap<String, String>>,
cx: &mut AsyncApp,
) -> Result<()> {
if paths.is_empty() && diff_paths.is_empty() && open_new_workspace != Some(true) {
if paths.is_empty() && diff_paths.is_empty() && open_behavior != cli::OpenBehavior::AlwaysNew {
return restore_or_create_workspace(app_state, cx).await;
}
@ -702,21 +696,33 @@ async fn open_workspaces(
for (location, workspace_paths) in grouped_locations {
// If reuse flag is passed, open a new workspace in an existing window.
let (open_new_workspace, replace_window) = if reuse {
(
Some(true),
cx.update(|cx| {
workspace::workspace_windows_for_location(&location, cx)
.into_iter()
.next()
}),
)
let replace_window = if open_behavior == cli::OpenBehavior::Reuse {
cx.update(|cx| {
workspace::workspace_windows_for_location(&location, cx)
.into_iter()
.next()
})
} else {
(open_new_workspace, None)
None
};
let open_options = workspace::OpenOptions {
open_new_workspace,
force_existing_window,
workspace_matching: match open_behavior {
cli::OpenBehavior::AlwaysNew | cli::OpenBehavior::Reuse => {
workspace::WorkspaceMatching::None
}
cli::OpenBehavior::Add => workspace::WorkspaceMatching::MatchSubdirectory,
_ => workspace::WorkspaceMatching::MatchExact,
},
add_dirs_to_sidebar: match open_behavior {
cli::OpenBehavior::ExistingWindow => true,
// For the default value, we consult the settings to decide
// whether to open in a new window or existing window.
cli::OpenBehavior::Default => cx.update(|cx| {
workspace::WorkspaceSettings::get_global(cx).cli_default_open_behavior
== settings::CliDefaultOpenBehavior::ExistingWindow
}),
_ => false,
},
requesting_window: replace_window,
wait,
env: env.clone(),
@ -1215,7 +1221,7 @@ mod tests {
assert_eq!(cx.windows().len(), 0);
// First open the workspace directory
open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await;
open_workspace_file(path!("/root/dir1"), <_>::default(), app_state.clone(), cx).await;
assert_eq!(cx.windows().len(), 1);
let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
@ -1228,7 +1234,13 @@ mod tests {
.unwrap();
// Now open a file inside that workspace
open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await;
open_workspace_file(
path!("/root/dir1/file1.txt"),
<_>::default(),
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
multi_workspace
@ -1239,16 +1251,19 @@ mod tests {
})
.unwrap();
// Opening a file inside the existing worktree with -n reuses the window.
// Opening a file inside the existing worktree with -n creates a new window.
open_workspace_file(
path!("/root/dir1/file1.txt"),
Some(true),
workspace::OpenOptions {
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
assert_eq!(cx.windows().len(), 2);
}
#[gpui::test]
@ -1319,7 +1334,13 @@ mod tests {
assert_eq!(cx.windows().len(), 0);
// Test case 1: Open a single file that does not exist yet
open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await;
open_workspace_file(
path!("/root/file5.txt"),
<_>::default(),
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
let multi_workspace_1 = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
@ -1333,7 +1354,16 @@ mod tests {
// Test case 2: Open a single file that does not exist yet,
// but tell Zed to add it to the current workspace
open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await;
open_workspace_file(
path!("/root/file6.txt"),
workspace::OpenOptions {
workspace_matching: workspace::WorkspaceMatching::MatchSubdirectory,
..Default::default()
},
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
multi_workspace_1
@ -1347,7 +1377,16 @@ mod tests {
// Test case 3: Open a single file that does not exist yet,
// but tell Zed to NOT add it to the current workspace
open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await;
open_workspace_file(
path!("/root/file7.txt"),
workspace::OpenOptions {
workspace_matching: workspace::WorkspaceMatching::None,
..Default::default()
},
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 2);
let multi_workspace_2 = cx.windows()[1].downcast::<MultiWorkspace>().unwrap();
@ -1363,7 +1402,7 @@ mod tests {
async fn open_workspace_file(
path: &str,
open_new_workspace: Option<bool>,
open_options: workspace::OpenOptions,
app_state: Arc<AppState>,
cx: &TestAppContext,
) {
@ -1377,10 +1416,7 @@ mod tests {
workspace_paths,
vec![],
false,
workspace::OpenOptions {
open_new_workspace,
..Default::default()
},
open_options,
&response_sink,
&app_state,
&mut cx,
@ -1657,7 +1693,7 @@ mod tests {
Vec::new(),
false,
workspace::OpenOptions {
open_new_workspace: Some(true), // Force new window
workspace_matching: workspace::WorkspaceMatching::None, // Force new window
..Default::default()
},
&response_sink,
@ -1679,7 +1715,7 @@ mod tests {
})
.unwrap();
// Now use --add flag (open_new_workspace = Some(false)) to add a new file
// Now use --add flag (open_behavior = OpenBehavior::Add) to add a new file
// It should open in the focused window (window2), not an arbitrary window
let new_file_path = if cfg!(windows) {
"C:\\root\\new_file.txt"
@ -1703,7 +1739,7 @@ mod tests {
Vec::new(),
false,
workspace::OpenOptions {
open_new_workspace: Some(false), // --add flag
workspace_matching: workspace::WorkspaceMatching::MatchSubdirectory, // --add flag
..Default::default()
},
&response_sink,
@ -1855,11 +1891,7 @@ mod tests {
.unwrap();
}
fn make_cli_open_request(
paths: Vec<String>,
open_new_workspace: Option<bool>,
force_existing_window: bool,
) -> CliRequest {
fn make_cli_open_request(paths: Vec<String>, open_behavior: cli::OpenBehavior) -> CliRequest {
CliRequest::Open {
paths,
urls: vec![],
@ -1867,9 +1899,7 @@ mod tests {
diff_all: false,
wsl: None,
wait: false,
open_new_workspace,
force_existing_window,
reuse: false,
open_behavior,
env: None,
user_data_dir: None,
dev_container: false,
@ -1886,7 +1916,7 @@ mod tests {
cx: &mut TestAppContext,
app_state: Arc<AppState>,
open_request: CliRequest,
prompt_response: Option<cli::CliOpenBehavior>,
prompt_response: Option<cli::CliBehaviorSetting>,
) -> (i32, bool) {
cx.executor().allow_parking();
@ -1915,7 +1945,7 @@ mod tests {
CliResponse::PromptOpenBehavior => {
prompt_called_for_thread.store(true, std::sync::atomic::Ordering::SeqCst);
let behavior =
prompt_response.unwrap_or(cli::CliOpenBehavior::ExistingWindow);
prompt_response.unwrap_or(cli::CliBehaviorSetting::ExistingWindow);
request_tx
.unbounded_send(CliRequest::SetOpenBehavior { behavior })
.map_err(|error| anyhow::anyhow!("{error}"))?;
@ -1955,7 +1985,10 @@ mod tests {
let (status, prompt_shown) = run_cli_with_zed_handler(
cx,
app_state,
make_cli_open_request(vec![path!("/project/file.txt").to_string()], None, false),
make_cli_open_request(
vec![path!("/project/file.txt").to_string()],
cli::OpenBehavior::Default,
),
None,
);
@ -1983,14 +2016,23 @@ mod tests {
.await;
// Create an existing window so the prompt triggers
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
open_workspace_file(
path!("/project_a"),
Default::default(),
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
let (status, prompt_shown) = run_cli_with_zed_handler(
cx,
app_state.clone(),
make_cli_open_request(vec![path!("/project_b").to_string()], None, false),
Some(cli::CliOpenBehavior::ExistingWindow),
make_cli_open_request(
vec![path!("/project_b").to_string()],
cli::OpenBehavior::Default,
),
Some(cli::CliBehaviorSetting::ExistingWindow),
);
assert_eq!(status, 0);
@ -2024,14 +2066,23 @@ mod tests {
.await;
// Create an existing window with project_a
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
open_workspace_file(
path!("/project_a"),
Default::default(),
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
let (status, prompt_shown) = run_cli_with_zed_handler(
cx,
app_state.clone(),
make_cli_open_request(vec![path!("/project_b").to_string()], None, false),
Some(cli::CliOpenBehavior::NewWindow),
make_cli_open_request(
vec![path!("/project_b").to_string()],
cli::OpenBehavior::Default,
),
Some(cli::CliBehaviorSetting::NewWindow),
);
assert_eq!(status, 0);
@ -2072,13 +2123,16 @@ mod tests {
.await;
// Create an existing window
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
open_workspace_file(path!("/project"), Default::default(), app_state.clone(), cx).await;
assert_eq!(cx.windows().len(), 1);
let (status, prompt_shown) = run_cli_with_zed_handler(
cx,
app_state,
make_cli_open_request(vec![path!("/project/file.txt").to_string()], None, false),
make_cli_open_request(
vec![path!("/project/file.txt").to_string()],
cli::OpenBehavior::Default,
),
None,
);
@ -2100,7 +2154,7 @@ mod tests {
.await;
// Create an existing window
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
open_workspace_file(path!("/project"), Default::default(), app_state.clone(), cx).await;
assert_eq!(cx.windows().len(), 1);
let (status, prompt_shown) = run_cli_with_zed_handler(
@ -2108,8 +2162,7 @@ mod tests {
app_state,
make_cli_open_request(
vec![path!("/project/file.txt").to_string()],
None,
true, // -e flag: force existing window
cli::OpenBehavior::ExistingWindow, // -e flag: force existing window
),
None,
);
@ -2135,7 +2188,13 @@ mod tests {
.await;
// Create an existing window
open_workspace_file(path!("/project_a"), None, app_state.clone(), cx).await;
open_workspace_file(
path!("/project_a"),
Default::default(),
app_state.clone(),
cx,
)
.await;
assert_eq!(cx.windows().len(), 1);
let (status, prompt_shown) = run_cli_with_zed_handler(
@ -2143,8 +2202,7 @@ mod tests {
app_state,
make_cli_open_request(
vec![path!("/project_b/file.txt").to_string()],
Some(true), // -n flag: force new window
false,
cli::OpenBehavior::AlwaysNew, // -n flag: force new window
),
None,
);
@ -2172,14 +2230,17 @@ mod tests {
.await;
// Open the project directory as a workspace
open_workspace_file(path!("/project"), None, app_state.clone(), cx).await;
open_workspace_file(path!("/project"), Default::default(), app_state.clone(), cx).await;
assert_eq!(cx.windows().len(), 1);
// Opening a file inside the already-open workspace should not prompt
let (status, prompt_shown) = run_cli_with_zed_handler(
cx,
app_state,
make_cli_open_request(vec![path!("/project/src/main.rs").to_string()], None, false),
make_cli_open_request(
vec![path!("/project/src/main.rs").to_string()],
cli::OpenBehavior::Default,
),
None,
);

View file

@ -158,9 +158,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
diff_all: false,
wait: false,
wsl: args.wsl.clone(),
open_new_workspace: None,
force_existing_window: false,
reuse: false,
open_behavior: Default::default(),
env: None,
user_data_dir: args.user_data_dir.clone(),
dev_container: args.dev_container,
@ -189,7 +187,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
}
CliResponse::PromptOpenBehavior => {
tx.send(CliRequest::SetOpenBehavior {
behavior: cli::CliOpenBehavior::ExistingWindow,
behavior: cli::CliBehaviorSetting::ExistingWindow,
})?;
}
}