From ad5d0154903f2c386bdd5b5df5020eca98e6672f Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Tue, 14 Apr 2026 21:58:04 -0700 Subject: [PATCH] cli: Fix -n behavior and refactor open options (#53939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`, `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 --- assets/settings/default.json | 5 +- crates/cli/src/cli.rs | 43 ++++- crates/cli/src/main.rs | 46 +++-- crates/settings_content/src/workspace.rs | 5 +- crates/settings_ui/src/page_data.rs | 2 +- crates/workspace/src/workspace.rs | 102 ++++++---- crates/zed/src/zed.rs | 28 +-- crates/zed/src/zed/open_listener.rs | 201 +++++++++++++------- crates/zed/src/zed/windows_only_instance.rs | 6 +- 9 files changed, 282 insertions(+), 156 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 68a9f2324912db7c1724c81cefd60ecbc41bf4b1..944ab72b98545bf616224e99a0984c17e1ab3b5c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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. diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index ea7e42beb4e22d7743bc5caade972f8f9f889925..d59eabc2ac23787550b8db1c4df25083d96298a8 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -9,10 +9,45 @@ pub struct IpcHandshake { pub responses: ipc::IpcReceiver, } +/// 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, wait: bool, - open_new_workspace: Option, #[serde(default)] - force_existing_window: bool, - reuse: bool, + open_behavior: OpenBehavior, env: Option>, user_data_dir: Option, dev_container: bool, }, SetOpenBehavior { - behavior: CliOpenBehavior, + behavior: CliBehaviorSetting, }, } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d46b90fdb77dc5b1fca87133cf72ce265d2701c1..9c6ec08219437ce1da4b1252f7ee75e7d11bd780 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -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::::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 { /// Shows an interactive prompt asking the user to choose the default open /// behavior for `zed `. Returns `None` if the prompt cannot be shown /// (e.g. stdin is not a terminal) or the user cancels. -fn prompt_open_behavior() -> Option { +fn prompt_open_behavior() -> Option { 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 { .ok()?; Some(if selection == 0 { - cli::CliOpenBehavior::ExistingWindow + cli::CliBehaviorSetting::ExistingWindow } else { - cli::CliOpenBehavior::NewWindow + cli::CliBehaviorSetting::NewWindow }) } diff --git a/crates/settings_content/src/workspace.rs b/crates/settings_content/src/workspace.rs index cebc73550f268f0be5385b5eb41928898db67585..3e4f3b03de5c615132a9ad9b65e569a3a4cf8ee2 100644 --- a/crates/settings_content/src/workspace.rs +++ b/crates/settings_content/src/workspace.rs @@ -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, } diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index a4bee8803b7a47581e0bd197cf6c3d723e2dbfe5..2d2d24a6ec2157affd57b7798b4ed83da2dd5325 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -143,7 +143,7 @@ fn general_page() -> SettingsPage { }), SettingsPageItem::SettingItem(SettingItem { title: "CLI Default Open Behavior", - description: "How `zed ` opens directories when no `-e` or `-n` flag is specified.", + description: "How `zed ` opens directories when no flag is specified.", field: Box::new(SettingField { json_path: Some("cli_default_open_behavior"), pick: |settings_content| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8c8e927cd5904c7594fecd5e15e33b936102fd06..4256945ad01ddbd0dabc7502ada3edfa7a5e3120 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, pub focus: Option, - pub open_new_workspace: Option, - 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>, 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| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 848efaffbb8e0abbf8e5099b0b8bf7da6aa8f90f..48d6501f737155ad96bfc2f0d865c5a19b186c7e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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,13 +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), + workspace_matching: workspace::WorkspaceMatching::None, ..Default::default() }, cx, @@ -2654,7 +2654,7 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); + 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| { @@ -2662,7 +2662,7 @@ mod tests { &[PathBuf::from(path!("/root/b"))], app_state.clone(), workspace::OpenOptions { - open_new_workspace: Some(true), + workspace_matching: workspace::WorkspaceMatching::None, ..Default::default() }, cx, @@ -2670,7 +2670,7 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); + assert_eq!(cx.update(|cx| cx.windows().len()), 3); } #[gpui::test] @@ -2723,13 +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), + workspace_matching: workspace::WorkspaceMatching::None, ..Default::default() }, cx, @@ -2737,7 +2737,7 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); + 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| { @@ -2745,7 +2745,7 @@ mod tests { &[PathBuf::from(path!("/root"))], app_state.clone(), workspace::OpenOptions { - open_new_workspace: Some(true), + workspace_matching: workspace::WorkspaceMatching::None, ..Default::default() }, cx, @@ -2753,7 +2753,7 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); + assert_eq!(cx.update(|cx| cx.windows().len()), 3); } #[gpui::test] diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index aa55ef543802a7271f2a3c935b14b6cedd26e767..e0094cb6556302de17deef1c397de5470128c333 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -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, diff_paths: Vec<[String; 2]>, diff_all: bool, - open_new_workspace: Option, - 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>, 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::().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::().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::().unwrap(); @@ -1363,7 +1402,7 @@ mod tests { async fn open_workspace_file( path: &str, - open_new_workspace: Option, + open_options: workspace::OpenOptions, app_state: Arc, 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, - open_new_workspace: Option, - force_existing_window: bool, - ) -> CliRequest { + fn make_cli_open_request(paths: Vec, 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, open_request: CliRequest, - prompt_response: Option, + prompt_response: Option, ) -> (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, ); diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index efc0e9e999d05d4d2dfe4969f82679e909f3ea06..022773f362f52cc696de974e8b855ff56376653a 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -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, })?; } }