Add --add/--new to control CLI behaviour (#9202)

Conrad Irwin created

When neither is specified, if you open a directory you get a new
workspace, otherwise files are added to your existing workspace.

With --new files are always opened in a new workspace
With --add directories are always added to an existing workspace

Fixes #9076
Fixes #4861
Fixes #5370

Release Notes:

- Added `-n/--new` and `-a/--add` to the zed CLI. When neither is
specified, if you open a directory you get a new workspace, otherwise
files are added to your existing workspace. With `--new` files are
always opened in a new workspace, with `--add` directories are always
added to an existing workspace.
([#9076](https://github.com/zed-industries/zed/issues/9096),
[#4861](https://github.com/zed-industries/zed/issues/4861),
[#5370](https://github.com/zed-industries/zed/issues/5370)).

Change summary

crates/cli/src/cli.rs                         |  11 
crates/cli/src/main.rs                        |  16 +
crates/journal/src/journal.rs                 |   9 
crates/project/src/project.rs                 |  26 +-
crates/recent_projects/src/recent_projects.rs |  13 +
crates/workspace/src/workspace.rs             |  84 +++++++-
crates/zed/src/main.rs                        |  50 ++--
crates/zed/src/open_listener.rs               |  66 ++++--
crates/zed/src/zed.rs                         | 197 ++++++++++++++++++--
9 files changed, 369 insertions(+), 103 deletions(-)

Detailed changes

crates/cli/src/cli.rs 🔗

@@ -9,12 +9,11 @@ pub struct IpcHandshake {
 
 #[derive(Debug, Serialize, Deserialize)]
 pub enum CliRequest {
-    // The filed is named `path` for compatibility, but now CLI can request
-    // opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
-    //
-    // Since Zed CLI has to be installed separately, there can be situations when old CLI is
-    // querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
-    Open { paths: Vec<String>, wait: bool },
+    Open {
+        paths: Vec<String>,
+        wait: bool,
+        open_new_workspace: Option<bool>,
+    },
 }
 
 #[derive(Debug, Serialize, Deserialize)]

crates/cli/src/main.rs 🔗

@@ -12,12 +12,18 @@ use std::{
 };
 use util::paths::PathLikeWithPosition;
 
-#[derive(Parser)]
+#[derive(Parser, Debug)]
 #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
 struct Args {
     /// Wait for all of the given paths to be opened/closed before exiting.
     #[clap(short, long)]
     wait: bool,
+    /// Add files to the currently open workspace
+    #[clap(short, long, overrides_with = "new")]
+    add: bool,
+    /// Create a new workspace
+    #[clap(short, long, overrides_with = "add")]
+    new: bool,
     /// A sequence of space-separated paths that you want to open.
     ///
     /// Use `path:line:row` syntax to open a file at a specific location.
@@ -67,6 +73,13 @@ fn main() -> Result<()> {
     }
 
     let (tx, rx) = bundle.launch()?;
+    let open_new_workspace = if args.new {
+        Some(true)
+    } else if args.add {
+        Some(false)
+    } else {
+        None
+    };
 
     tx.send(CliRequest::Open {
         paths: args
@@ -81,6 +94,7 @@ fn main() -> Result<()> {
             })
             .collect::<Result<_>>()?,
         wait: args.wait,
+        open_new_workspace,
     })?;
 
     while let Ok(response) = rx.recv() {

crates/journal/src/journal.rs 🔗

@@ -102,7 +102,14 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut WindowContext) {
     cx.spawn(|mut cx| async move {
         let (journal_dir, entry_path) = create_entry.await?;
         let (workspace, _) = cx
-            .update(|cx| workspace::open_paths(&[journal_dir], app_state, None, cx))?
+            .update(|cx| {
+                workspace::open_paths(
+                    &[journal_dir],
+                    app_state,
+                    workspace::OpenOptions::default(),
+                    cx,
+                )
+            })?
             .await?;
 
         let opened = workspace

crates/project/src/project.rs 🔗

@@ -1145,18 +1145,24 @@ impl Project {
             .map(|worktree| worktree.read(cx).id())
     }
 
-    pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
-        paths.iter().all(|path| self.contains_path(path, cx))
+    pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
+        paths
+            .iter()
+            .map(|path| self.visibility_for_path(path, cx))
+            .max()
+            .flatten()
     }
 
-    pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
-        for worktree in self.worktrees() {
-            let worktree = worktree.read(cx).as_local();
-            if worktree.map_or(false, |w| w.contains_abs_path(path)) {
-                return true;
-            }
-        }
-        false
+    pub fn visibility_for_path(&self, path: &Path, cx: &AppContext) -> Option<bool> {
+        self.worktrees()
+            .filter_map(|worktree| {
+                let worktree = worktree.read(cx);
+                worktree
+                    .as_local()?
+                    .contains_abs_path(path)
+                    .then(|| worktree.is_visible())
+            })
+            .max()
     }
 
     pub fn create_entry(

crates/recent_projects/src/recent_projects.rs 🔗

@@ -493,9 +493,16 @@ mod tests {
                 }),
             )
             .await;
-        cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], app_state, None, cx))
-            .await
-            .unwrap();
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/dir/main.ts")],
+                app_state,
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
 
         let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());

crates/workspace/src/workspace.rs 🔗

@@ -262,7 +262,8 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
                 cx.spawn(move |cx| async move {
                     if let Some(paths) = paths.await.log_err().flatten() {
                         cx.update(|cx| {
-                            open_paths(&paths, app_state, None, cx).detach_and_log_err(cx)
+                            open_paths(&paths, app_state, OpenOptions::default(), cx)
+                                .detach_and_log_err(cx)
                         })
                         .ok();
                     }
@@ -1414,8 +1415,18 @@ impl Workspace {
         let app_state = self.app_state.clone();
 
         cx.spawn(|_, mut cx| async move {
-            cx.update(|cx| open_paths(&paths, app_state, window_to_replace, cx))?
-                .await?;
+            cx.update(|cx| {
+                open_paths(
+                    &paths,
+                    app_state,
+                    OpenOptions {
+                        replace_window: window_to_replace,
+                        ..Default::default()
+                    },
+                    cx,
+                )
+            })?
+            .await?;
             Ok(())
         })
     }
@@ -4361,6 +4372,13 @@ pub async fn get_any_active_workspace(
 
 fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
     cx.update(|cx| {
+        if let Some(workspace_window) = cx
+            .active_window()
+            .and_then(|window| window.downcast::<Workspace>())
+        {
+            return Some(workspace_window);
+        }
+
         for window in cx.windows() {
             if let Some(workspace_window) = window.downcast::<Workspace>() {
                 workspace_window
@@ -4375,11 +4393,17 @@ fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandl
     .flatten()
 }
 
+#[derive(Default)]
+pub struct OpenOptions {
+    pub open_new_workspace: Option<bool>,
+    pub replace_window: Option<WindowHandle<Workspace>>,
+}
+
 #[allow(clippy::type_complexity)]
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: Arc<AppState>,
-    requesting_window: Option<WindowHandle<Workspace>>,
+    open_options: OpenOptions,
     cx: &mut AppContext,
 ) -> Task<
     anyhow::Result<(
@@ -4388,24 +4412,62 @@ pub fn open_paths(
     )>,
 > {
     let abs_paths = abs_paths.to_vec();
-    // Open paths in existing workspace if possible
-    let existing = activate_workspace_for_project(cx, {
-        let abs_paths = abs_paths.clone();
-        move |project, cx| project.contains_paths(&abs_paths, cx)
-    });
+    let mut existing = None;
+    let mut best_match = None;
+    let mut open_visible = OpenVisible::All;
+
+    if open_options.open_new_workspace != Some(true) {
+        for window in cx.windows() {
+            let Some(handle) = window.downcast::<Workspace>() else {
+                continue;
+            };
+            if let Ok(workspace) = handle.read(cx) {
+                let m = workspace
+                    .project()
+                    .read(cx)
+                    .visibility_for_paths(&abs_paths, cx);
+                if m > best_match {
+                    existing = Some(handle);
+                    best_match = m;
+                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
+                    existing = Some(handle)
+                }
+            }
+        }
+    }
+
     cx.spawn(move |mut cx| async move {
+        if open_options.open_new_workspace.is_none() && existing.is_none() {
+            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
+            if futures::future::join_all(all_files)
+                .await
+                .into_iter()
+                .filter_map(|result| result.ok().flatten())
+                .all(|file| !file.is_dir)
+            {
+                existing = activate_any_workspace_window(&mut cx);
+                open_visible = OpenVisible::None;
+            }
+        }
+
         if let Some(existing) = existing {
             Ok((
                 existing,
                 existing
                     .update(&mut cx, |workspace, cx| {
-                        workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
+                        cx.activate_window();
+                        workspace.open_paths(abs_paths, open_visible, None, cx)
                     })?
                     .await,
             ))
         } else {
             cx.update(move |cx| {
-                Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
+                Workspace::new_local(
+                    abs_paths,
+                    app_state.clone(),
+                    open_options.replace_window,
+                    cx,
+                )
             })?
             .await
         }

crates/zed/src/main.rs 🔗

@@ -264,24 +264,14 @@ fn main() {
         cx.set_menus(app_menus());
         initialize_workspace(app_state.clone(), cx);
 
-        if stdout_is_a_pty() {
-            // todo(linux): unblock this
-            #[cfg(not(target_os = "linux"))]
-            upload_panics_and_crashes(http.clone(), cx);
-            cx.activate(true);
-            let urls = collect_url_args(cx);
-            if !urls.is_empty() {
-                listener.open_urls(urls)
-            }
-        } else {
-            upload_panics_and_crashes(http.clone(), cx);
-            // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
-            // of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
-            if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
-                && !listener.triggered.load(Ordering::Acquire)
-            {
-                listener.open_urls(collect_url_args(cx))
-            }
+        // todo(linux): unblock this
+        upload_panics_and_crashes(http.clone(), cx);
+
+        cx.activate(true);
+
+        let urls = collect_url_args(cx);
+        if !urls.is_empty() {
+            listener.open_urls(urls)
         }
 
         let mut triggered_authentication = false;
@@ -339,8 +329,13 @@ fn handle_open_request(
     if !request.open_paths.is_empty() {
         let app_state = app_state.clone();
         task = Some(cx.spawn(|mut cx| async move {
-            let (_window, results) =
-                open_paths_with_positions(&request.open_paths, app_state, &mut cx).await?;
+            let (_window, results) = open_paths_with_positions(
+                &request.open_paths,
+                app_state,
+                workspace::OpenOptions::default(),
+                &mut cx,
+            )
+            .await?;
             for result in results.into_iter().flatten() {
                 if let Err(err) = result {
                     log::error!("Error opening path: {err}",);
@@ -441,9 +436,16 @@ async fn installation_id() -> Result<(String, bool)> {
 async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppContext) {
     async_maybe!({
         if let Some(location) = workspace::last_opened_workspace_paths().await {
-            cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
-                .await
-                .log_err();
+            cx.update(|cx| {
+                workspace::open_paths(
+                    location.paths().as_ref(),
+                    app_state,
+                    workspace::OpenOptions::default(),
+                    cx,
+                )
+            })?
+            .await
+            .log_err();
         } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
             cx.update(|cx| show_welcome_view(app_state, cx)).log_err();
         } else {
@@ -901,7 +903,7 @@ fn collect_url_args(cx: &AppContext) -> Vec<String> {
         .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) {
             Ok(path) => Some(format!("file://{}", path.to_string_lossy())),
             Err(error) => {
-                if arg.starts_with("file://") {
+                if arg.starts_with("file://") || arg.starts_with("zed-cli://") {
                     Some(arg)
                 } else if let Some(_) = parse_zed_link(&arg, cx) {
                     Some(arg)

crates/zed/src/open_listener.rs 🔗

@@ -11,11 +11,10 @@ use futures::{FutureExt, SinkExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
 use language::{Bias, Point};
 use std::path::Path;
-use std::sync::atomic::Ordering;
+use std::path::PathBuf;
 use std::sync::Arc;
 use std::thread;
 use std::time::Duration;
-use std::{path::PathBuf, sync::atomic::AtomicBool};
 use util::paths::PathLikeWithPosition;
 use util::ResultExt;
 use workspace::item::ItemHandle;
@@ -89,7 +88,6 @@ impl OpenRequest {
 
 pub struct OpenListener {
     tx: UnboundedSender<Vec<String>>,
-    pub triggered: AtomicBool,
 }
 
 struct GlobalOpenListener(Arc<OpenListener>);
@@ -107,17 +105,10 @@ impl OpenListener {
 
     pub fn new() -> (Self, UnboundedReceiver<Vec<String>>) {
         let (tx, rx) = mpsc::unbounded();
-        (
-            OpenListener {
-                tx,
-                triggered: AtomicBool::new(false),
-            },
-            rx,
-        )
+        (OpenListener { tx }, rx)
     }
 
     pub fn open_urls(&self, urls: Vec<String>) {
-        self.triggered.store(true, Ordering::Release);
         self.tx
             .unbounded_send(urls)
             .map_err(|_| anyhow!("no listener for open requests"))
@@ -157,6 +148,7 @@ fn connect_to_cli(
 pub async fn open_paths_with_positions(
     path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
     app_state: Arc<AppState>,
+    open_options: workspace::OpenOptions,
     cx: &mut AsyncAppContext,
 ) -> Result<(
     WindowHandle<Workspace>,
@@ -180,7 +172,7 @@ pub async fn open_paths_with_positions(
         .collect::<Vec<_>>();
 
     let (workspace, items) = cx
-        .update(|cx| workspace::open_paths(&paths, app_state, None, cx))?
+        .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))?
         .await?;
 
     for (item, path) in items.iter().zip(&paths) {
@@ -215,22 +207,30 @@ pub async fn handle_cli_connection(
 ) {
     if let Some(request) = requests.next().await {
         match request {
-            CliRequest::Open { paths, wait } => {
+            CliRequest::Open {
+                paths,
+                wait,
+                open_new_workspace,
+            } => {
                 let paths = if paths.is_empty() {
-                    workspace::last_opened_workspace_paths()
-                        .await
-                        .map(|location| {
-                            location
-                                .paths()
-                                .iter()
-                                .map(|path| PathLikeWithPosition {
-                                    path_like: path.clone(),
-                                    row: None,
-                                    column: None,
-                                })
-                                .collect::<Vec<_>>()
-                        })
-                        .unwrap_or_default()
+                    if open_new_workspace == Some(true) {
+                        vec![]
+                    } else {
+                        workspace::last_opened_workspace_paths()
+                            .await
+                            .map(|location| {
+                                location
+                                    .paths()
+                                    .iter()
+                                    .map(|path| PathLikeWithPosition {
+                                        path_like: path.clone(),
+                                        row: None,
+                                        column: None,
+                                    })
+                                    .collect::<Vec<_>>()
+                            })
+                            .unwrap_or_default()
+                    }
                 } else {
                     paths
                         .into_iter()
@@ -250,7 +250,17 @@ pub async fn handle_cli_connection(
 
                 let mut errored = false;
 
-                match open_paths_with_positions(&paths, app_state, &mut cx).await {
+                match open_paths_with_positions(
+                    &paths,
+                    app_state,
+                    workspace::OpenOptions {
+                        open_new_workspace,
+                        ..Default::default()
+                    },
+                    &mut cx,
+                )
+                .await
+                {
                     Ok((workspace, items)) => {
                         let mut item_release_futures = Vec::new();
 

crates/zed/src/zed.rs 🔗

@@ -905,6 +905,10 @@ mod tests {
                         "da": null,
                         "db": null,
                     },
+                    "e": {
+                        "ea": null,
+                        "eb": null,
+                    }
                 }),
             )
             .await;
@@ -913,7 +917,7 @@ mod tests {
             open_paths(
                 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
                 app_state.clone(),
-                None,
+                workspace::OpenOptions::default(),
                 cx,
             )
         })
@@ -921,9 +925,16 @@ mod tests {
         .unwrap();
         assert_eq!(cx.read(|cx| cx.windows().len()), 1);
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
-            .await
-            .unwrap();
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/a")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
         assert_eq!(cx.read(|cx| cx.windows().len()), 1);
         let workspace_1 = cx
             .read(|cx| cx.windows()[0].downcast::<Workspace>())
@@ -942,9 +953,9 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
                 app_state.clone(),
-                None,
+                workspace::OpenOptions::default(),
                 cx,
             )
         })
@@ -958,9 +969,12 @@ mod tests {
             .unwrap();
         cx.update(|cx| {
             open_paths(
-                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+                &[PathBuf::from("/root/e")],
                 app_state,
-                Some(window),
+                workspace::OpenOptions {
+                    replace_window: Some(window),
+                    ..Default::default()
+                },
                 cx,
             )
         })
@@ -978,7 +992,7 @@ mod tests {
                         .worktrees(cx)
                         .map(|w| w.read(cx).abs_path())
                         .collect::<Vec<_>>(),
-                    &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+                    &[Path::new("/root/e").into()]
                 );
                 assert!(workspace.left_dock().read(cx).is_open());
                 assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
@@ -986,6 +1000,123 @@ mod tests {
             .unwrap();
     }
 
+    #[gpui::test]
+    async fn test_open_add_new(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}}))
+            .await;
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/dir")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/a")],
+                app_state.clone(),
+                workspace::OpenOptions {
+                    open_new_workspace: Some(false),
+                    ..Default::default()
+                },
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/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()), 2);
+    }
+
+    #[gpui::test]
+    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}))
+            .await;
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/dir1/a")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+        let window1 = cx.update(|cx| cx.active_window().unwrap());
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/dir2/c")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/dir2")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
+        let window2 = cx.update(|cx| cx.active_window().unwrap());
+        assert!(window1 != window2);
+        cx.update_window(window1, |_, cx| cx.activate_window())
+            .unwrap();
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/dir2/c")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
+        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
+        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
+    }
+
     #[gpui::test]
     async fn test_window_edit_state(cx: &mut TestAppContext) {
         let executor = cx.executor();
@@ -996,9 +1127,16 @@ mod tests {
             .insert_tree("/root", json!({"a": "hey"}))
             .await;
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state.clone(), None, cx))
-            .await
-            .unwrap();
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/a")],
+                app_state.clone(),
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
 
         // When opening the workspace, the window is not in a edited state.
@@ -1063,9 +1201,16 @@ mod tests {
         assert!(!window_is_edited(window, cx));
 
         // Opening the buffer again doesn't impact the window's edited state.
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], app_state, None, cx))
-            .await
-            .unwrap();
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/a")],
+                app_state,
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
         let editor = window
             .read_with(cx, |workspace, cx| {
                 workspace
@@ -1292,9 +1437,16 @@ mod tests {
             )
             .await;
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], app_state, None, cx))
-            .await
-            .unwrap();
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/dir1/")],
+                app_state,
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
         let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
         let workspace = window.root(cx).unwrap();
@@ -1526,7 +1678,14 @@ mod tests {
             Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
         ];
         let (opened_workspace, new_items) = cx
-            .update(|cx| workspace::open_paths(&paths_to_open, app_state, None, cx))
+            .update(|cx| {
+                workspace::open_paths(
+                    &paths_to_open,
+                    app_state,
+                    workspace::OpenOptions::default(),
+                    cx,
+                )
+            })
             .await
             .unwrap();