From 4b489f4ce9ae3edbf28e11f4def0dc28099c33d9 Mon Sep 17 00:00:00 2001 From: Mateo Noel Rabines <39866695+mateonoel2@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:41:13 -0500 Subject: [PATCH] cli: Add `--reuse` flag for replacing workspace in existing window (#38131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #ISSUE it is was still in [discussion](https://github.com/zed-industries/zed/discussions/37983) Release Notes: - Added: `--reuse` (`-r`) CLI flag to replace the workspace in an existing window instead of opening a new one This PR adds a new `--reuse` (`-r`) CLI flag that allows users to replace the workspace in an existing Zed window instead of opening a new one or adding files to the current workspace. ### What it does The `--reuse` flag finds an available local workspace window and replaces its workspace with the newly specified paths. This provides a third workspace opening mode alongside the existing `--add` and `--new` flags. ### Implementation Details - **CLI Flag**: Added `--reuse` (`-r`) flag with proper mutual exclusion with `--add` and `--new` - **Window Replacement**: Uses the existing `replace_window` option in `workspace::OpenOptions` - **Window Selection**: Reuses the first available local workspace window - **Fallback Behavior**: When no existing windows are found, creates a new window - **Test Coverage**: Added comprehensive test for the reuse functionality ### Behavior - `zed -r file.txt` - Replaces the workspace in an available window with `file.txt` - If no windows are open, creates a new window (same as default behavior) - Mutually exclusive with `-a/--add` and `-n/--new` flags - Works with multiple files and directories ### Files Changed - `crates/cli/src/cli.rs` - Added `reuse` field to `CliRequest::Open` - `crates/cli/src/main.rs` - Added CLI argument definition and parsing - `crates/zed/src/zed/open_listener.rs` - Implemented reuse logic and added tests - `crates/zed/src/zed/windows_only_instance.rs` - Updated for Windows compatibility ### Testing - ✅ Unit tests pass - ✅ Manual testing confirms expected behavior: - Works when no windows are open - Replaces workspace in existing window - Maintains compatibility with existing `-a` and `-n` flags - Proper help text display ## Manual testing #### In this first video we do a couple of tests: * **1**: What happens if we use the -r flag when there are no windows open? - works as expected. It opens the files in a new window. * **2**: Does it work as expected if there is already a window open. Does it overrides the workspace? - yes it does. When opening a different file it overrides the current window instead of creating a new one. * **3**: Does the -n flag still works as expected? - yes, it creates the project in a new window * **4**: What about the -a flag? - yes, on the last accessed page * **5**: we do the replace command. It overrides the first opened window, do we want this behavior? - It is good enough that it overrides one of the opened windows with the new project. It still makes the user automatically go to the window with the specified files * **6**: we use the -r command again replacing the workspace with a new one. - this indeed worked as expected https://github.com/user-attachments/assets/f1cd7f4b-f4af-4da2-a755-c0be7ce96c0d #### In here the we check how the --help flag now displays the new command. (Description was later updated) https://github.com/user-attachments/assets/a8a7a288-d926-431b-a9f9-a8c3d909a2ec --- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 8 +- crates/zed/src/zed/open_listener.rs | 126 +++++++++++++++++++- crates/zed/src/zed/windows_only_instance.rs | 1 + 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 79a10fa2b0936b44d9500fd9990ffa4c6ac62e85..fbd7e2693a74598f3840afa5f4a99c86e96f2357 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -17,6 +17,7 @@ pub enum CliRequest { wsl: Option, wait: bool, open_new_workspace: Option, + reuse: bool, env: Option>, user_data_dir: Option, }, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4c25cf1c9d701369c7ce18a1cb70b8073da161e5..64a342a332f2c1b896afe58dda0e7156304e8116 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -62,11 +62,14 @@ struct Args { #[arg(short, long)] wait: bool, /// Add files to the currently open workspace - #[arg(short, long, overrides_with = "new")] + #[arg(short, long, overrides_with_all = ["new", "reuse"])] add: bool, /// Create a new workspace - #[arg(short, long, overrides_with = "add")] + #[arg(short, long, overrides_with_all = ["add", "reuse"])] new: bool, + /// Reuse an existing window, replacing its workspace + #[arg(short, long, overrides_with_all = ["add", "new"])] + reuse: 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`.")] @@ -374,6 +377,7 @@ fn main() -> Result<()> { wsl, wait: args.wait, open_new_workspace, + reuse: args.reuse, env, user_data_dir: user_data_dir_for_thread, })?; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index a8a998b6580269de150280c432c329cf59c30c22..3e0250825860aa358bb43125267dd4be8299b736 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -328,6 +328,7 @@ pub async fn handle_cli_connection( wait, wsl, open_new_workspace, + reuse, env, user_data_dir: _, } => { @@ -363,6 +364,7 @@ pub async fn handle_cli_connection( paths, diff_paths, open_new_workspace, + reuse, &responses, wait, app_state.clone(), @@ -382,6 +384,7 @@ async fn open_workspaces( paths: Vec, diff_paths: Vec<[String; 2]>, open_new_workspace: Option, + reuse: bool, responses: &IpcSender, wait: bool, app_state: Arc, @@ -441,6 +444,7 @@ async fn open_workspaces( workspace_paths, diff_paths.clone(), open_new_workspace, + reuse, wait, responses, env.as_ref(), @@ -487,6 +491,7 @@ async fn open_local_workspace( workspace_paths: Vec, diff_paths: Vec<[String; 2]>, open_new_workspace: Option, + reuse: bool, wait: bool, responses: &IpcSender, env: Option<&HashMap>, @@ -497,12 +502,30 @@ async fn open_local_workspace( let paths_with_position = derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await; + + // Handle reuse flag by finding existing window to replace + let replace_window = if reuse { + cx.update(|cx| workspace::local_workspace_windows(cx).into_iter().next()) + .ok() + .flatten() + } else { + None + }; + + // For reuse, force new workspace creation but with replace_window set + let effective_open_new_workspace = if reuse { + Some(true) + } else { + open_new_workspace + }; + match open_paths_with_positions( &paths_with_position, &diff_paths, app_state.clone(), workspace::OpenOptions { - open_new_workspace, + open_new_workspace: effective_open_new_workspace, + replace_window, env: env.cloned(), ..Default::default() }, @@ -614,7 +637,9 @@ mod tests { }; use editor::Editor; use gpui::TestAppContext; + use language::LineEnding; use remote::SshConnectionOptions; + use rope::Rope; use serde_json::json; use std::sync::Arc; use util::path; @@ -780,6 +805,7 @@ mod tests { vec![], open_new_workspace, false, + false, &response_tx, None, &app_state, @@ -791,4 +817,102 @@ mod tests { assert!(!errored); } + + #[gpui::test] + async fn test_reuse_flag_functionality(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let root_dir = if cfg!(windows) { "C:\\root" } else { "/root" }; + let file1_path = if cfg!(windows) { + "C:\\root\\file1.txt" + } else { + "/root/file1.txt" + }; + let file2_path = if cfg!(windows) { + "C:\\root\\file2.txt" + } else { + "/root/file2.txt" + }; + + app_state.fs.create_dir(Path::new(root_dir)).await.unwrap(); + app_state + .fs + .create_file(Path::new(file1_path), Default::default()) + .await + .unwrap(); + app_state + .fs + .save( + Path::new(file1_path), + &Rope::from("content1"), + LineEnding::Unix, + ) + .await + .unwrap(); + app_state + .fs + .create_file(Path::new(file2_path), Default::default()) + .await + .unwrap(); + app_state + .fs + .save( + Path::new(file2_path), + &Rope::from("content2"), + LineEnding::Unix, + ) + .await + .unwrap(); + + // First, open a workspace normally + let (response_tx, _response_rx) = ipc::channel::().unwrap(); + let workspace_paths = vec![file1_path.to_string()]; + + let _errored = cx + .spawn({ + let app_state = app_state.clone(); + let response_tx = response_tx.clone(); + |mut cx| async move { + open_local_workspace( + workspace_paths, + vec![], + None, + false, + false, + &response_tx, + None, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + // Now test the reuse functionality - should replace the existing workspace + let workspace_paths_reuse = vec![file1_path.to_string()]; + + let errored_reuse = cx + .spawn({ + let app_state = app_state.clone(); + let response_tx = response_tx.clone(); + |mut cx| async move { + open_local_workspace( + workspace_paths_reuse, + vec![], + None, // open_new_workspace will be overridden by reuse logic + true, // reuse = true + false, + &response_tx, + None, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + assert!(!errored_reuse); + } } diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 45f3cd158bb38156a0981f01e5331dc0aead91c9..f3eab154415814d60e2b06f5823d47006b1c367c 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -158,6 +158,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { wait: false, wsl: args.wsl.clone(), open_new_workspace: None, + reuse: false, env: None, user_data_dir: args.user_data_dir.clone(), }