@@ -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
})
}
@@ -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]
@@ -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,
);