mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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:
parent
dc5e2f1c24
commit
ad5d015490
9 changed files with 308 additions and 182 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue