cli: Add --reuse flag for replacing workspace in existing window (#38131)
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
This commit is contained in:
@@ -17,6 +17,7 @@ pub enum CliRequest {
|
||||
wsl: Option<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
reuse: bool,
|
||||
env: Option<HashMap<String, String>>,
|
||||
user_data_dir: Option<String>,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
})?;
|
||||
|
||||
@@ -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<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
open_new_workspace: Option<bool>,
|
||||
reuse: bool,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
wait: bool,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -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<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
open_new_workspace: Option<bool>,
|
||||
reuse: bool,
|
||||
wait: bool,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
env: Option<&HashMap<String, String>>,
|
||||
@@ -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::<CliResponse>().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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user