Change open paths to replace the existing window if dispatched from a window

Mikayla Maki and Max created

co-authored-by: Max <max@zed.dev>

Change summary

crates/journal/src/journal.rs     |   2 
crates/workspace/src/workspace.rs | 206 +++++++++++++++++++-------------
crates/zed/src/main.rs            |   6 
crates/zed/src/zed.rs             |  37 +++++
4 files changed, 162 insertions(+), 89 deletions(-)

Detailed changes

crates/journal/src/journal.rs 🔗

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

crates/workspace/src/workspace.rs 🔗

@@ -44,7 +44,8 @@ use gpui::{
     platform::{CursorStyle, WindowOptions},
     AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
-    SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
+    SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowBounds,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::LanguageRegistry;
@@ -188,12 +189,54 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     dock::init(cx);
     notifications::init(cx);
 
-    cx.add_global_action(open);
+    cx.add_global_action(|_: &Open, cx: &mut MutableAppContext| {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
+
+        cx.spawn(|mut cx| async move {
+            if let Some(paths) = paths.recv().await.flatten() {
+                cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
+            }
+        })
+        .detach();
+    });
+    cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
+
+        let handle = cx.handle().downgrade();
+        cx.spawn(|_, mut cx| async move {
+            if let Some(paths) = paths.recv().await.flatten() {
+                cx.update(|cx| {
+                    cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
+                })
+            }
+        })
+        .detach();
+    });
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
         move |action: &OpenPaths, cx: &mut MutableAppContext| {
             if let Some(app_state) = app_state.upgrade() {
-                open_paths(&action.paths, &app_state, cx).detach();
+                open_paths(&action.paths, &app_state, None, cx).detach();
+            }
+        }
+    });
+    cx.add_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |_, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
+            if let Some(app_state) = app_state.upgrade() {
+                let window_id = cx.window_id();
+                let action = action.clone();
+                cx.as_mut().defer(move |cx| {
+                    open_paths(&action.paths, &app_state, Some(window_id), cx).detach();
+                })
             }
         }
     });
@@ -488,6 +531,7 @@ pub struct Workspace {
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
+    _window_subscriptions: [Subscription; 3],
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<()>,
 }
@@ -519,10 +563,6 @@ impl Workspace {
         dock_default_factory: DockDefaultItemFactory,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
-
-        cx.observe_window_activation(Self::on_window_activation_changed)
-            .detach();
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&project, move |this, _, event, cx| {
             match event {
@@ -629,6 +669,28 @@ impl Workspace {
             active_call = Some((call, subscriptions));
         }
 
+        let subscriptions = [
+            cx.observe_fullscreen(|_, _, cx| cx.notify()),
+            cx.observe_window_activation(Self::on_window_activation_changed),
+            cx.observe_window_bounds(move |_, mut bounds, display, cx| {
+                // Transform fixed bounds to be stored in terms of the containing display
+                if let WindowBounds::Fixed(mut window_bounds) = bounds {
+                    if let Some(screen) = cx.platform().screen_by_id(display) {
+                        let screen_bounds = screen.bounds();
+                        window_bounds
+                            .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
+                        window_bounds
+                            .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
+                        bounds = WindowBounds::Fixed(window_bounds);
+                    }
+                }
+
+                cx.background()
+                    .spawn(DB.set_window_bounds(workspace_id, bounds, display))
+                    .detach_and_log_err(cx);
+            }),
+        ];
+
         let mut this = Workspace {
             modal: None,
             weak_self: weak_handle.clone(),
@@ -660,6 +722,7 @@ impl Workspace {
             _observe_current_user,
             _apply_leader_updates,
             leader_updates_tx,
+            _window_subscriptions: subscriptions,
         };
         this.project_remote_id_changed(project.read(cx).remote_id(), cx);
         cx.defer(|this, cx| this.update_window_title(cx));
@@ -676,6 +739,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
+        requesting_window_id: Option<usize>,
         cx: &mut MutableAppContext,
     ) -> Task<(
         ViewHandle<Workspace>,
@@ -731,42 +795,9 @@ impl Workspace {
                         ))
                     });
 
-            let (bounds, display) = if let Some(bounds) = window_bounds_override {
-                (Some(bounds), None)
-            } else {
-                serialized_workspace
-                    .as_ref()
-                    .and_then(|serialized_workspace| {
-                        let display = serialized_workspace.display?;
-                        let mut bounds = serialized_workspace.bounds?;
-
-                        // Stored bounds are relative to the containing display.
-                        // So convert back to global coordinates if that screen still exists
-                        if let WindowBounds::Fixed(mut window_bounds) = bounds {
-                            if let Some(screen) = cx.platform().screen_by_id(display) {
-                                let screen_bounds = screen.bounds();
-                                window_bounds.set_origin_x(
-                                    window_bounds.origin_x() + screen_bounds.origin_x(),
-                                );
-                                window_bounds.set_origin_y(
-                                    window_bounds.origin_y() + screen_bounds.origin_y(),
-                                );
-                                bounds = WindowBounds::Fixed(window_bounds);
-                            } else {
-                                // Screen no longer exists. Return none here.
-                                return None;
-                            }
-                        }
-
-                        Some((bounds, display))
-                    })
-                    .unzip()
-            };
-
-            // Use the serialized workspace to construct the new window
-            let (_, workspace) = cx.add_window(
-                (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
-                |cx| {
+            let build_workspace =
+                |cx: &mut ViewContext<Workspace>,
+                 serialized_workspace: Option<SerializedWorkspace>| {
                     let mut workspace = Workspace::new(
                         serialized_workspace,
                         workspace_id,
@@ -775,29 +806,53 @@ impl Workspace {
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                    cx.observe_window_bounds(move |_, mut bounds, display, cx| {
-                        // Transform fixed bounds to be stored in terms of the containing display
-                        if let WindowBounds::Fixed(mut window_bounds) = bounds {
-                            if let Some(screen) = cx.platform().screen_by_id(display) {
-                                let screen_bounds = screen.bounds();
-                                window_bounds.set_origin_x(
-                                    window_bounds.origin_x() - screen_bounds.origin_x(),
-                                );
-                                window_bounds.set_origin_y(
-                                    window_bounds.origin_y() - screen_bounds.origin_y(),
-                                );
-                                bounds = WindowBounds::Fixed(window_bounds);
+                    workspace
+                };
+
+            let workspace = if let Some(window_id) = requesting_window_id {
+                cx.update(|cx| {
+                    cx.replace_root_view(window_id, |cx| build_workspace(cx, serialized_workspace))
+                })
+            } else {
+                let (bounds, display) = if let Some(bounds) = window_bounds_override {
+                    (Some(bounds), None)
+                } else {
+                    serialized_workspace
+                        .as_ref()
+                        .and_then(|serialized_workspace| {
+                            let display = serialized_workspace.display?;
+                            let mut bounds = serialized_workspace.bounds?;
+
+                            // Stored bounds are relative to the containing display.
+                            // So convert back to global coordinates if that screen still exists
+                            if let WindowBounds::Fixed(mut window_bounds) = bounds {
+                                if let Some(screen) = cx.platform().screen_by_id(display) {
+                                    let screen_bounds = screen.bounds();
+                                    window_bounds.set_origin_x(
+                                        window_bounds.origin_x() + screen_bounds.origin_x(),
+                                    );
+                                    window_bounds.set_origin_y(
+                                        window_bounds.origin_y() + screen_bounds.origin_y(),
+                                    );
+                                    bounds = WindowBounds::Fixed(window_bounds);
+                                } else {
+                                    // Screen no longer exists. Return none here.
+                                    return None;
+                                }
                             }
-                        }
 
-                        cx.background()
-                            .spawn(DB.set_window_bounds(workspace_id, bounds, display))
-                            .detach_and_log_err(cx);
-                    })
-                    .detach();
-                    workspace
-                },
-            );
+                            Some((bounds, display))
+                        })
+                        .unzip()
+                };
+
+                // Use the serialized workspace to construct the new window
+                cx.add_window(
+                    (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
+                    |cx| build_workspace(cx, serialized_workspace),
+                )
+                .1
+            };
 
             notify_if_database_failed(&workspace, &mut cx);
 
@@ -893,7 +948,7 @@ impl Workspace {
         if self.project.read(cx).is_local() {
             Task::Ready(Some(callback(self, cx)))
         } else {
-            let task = Self::new_local(Vec::new(), app_state.clone(), cx);
+            let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
             cx.spawn(|_vh, mut cx| async move {
                 let (workspace, _) = task.await;
                 workspace.update(&mut cx, callback)
@@ -2809,21 +2864,6 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-fn open(_: &Open, cx: &mut MutableAppContext) {
-    let mut paths = cx.prompt_for_paths(PathPromptOptions {
-        files: true,
-        directories: true,
-        multiple: true,
-    });
-
-    cx.spawn(|mut cx| async move {
-        if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
-        }
-    })
-    .detach();
-}
-
 pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
 
 pub fn activate_workspace_for_project(
@@ -2850,6 +2890,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
+    requesting_window_id: Option<usize>,
     cx: &mut MutableAppContext,
 ) -> Task<(
     ViewHandle<Workspace>,
@@ -2880,7 +2921,8 @@ pub fn open_paths(
                     .contains(&false);
 
             cx.update(|cx| {
-                let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
+                let task =
+                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
 
                 cx.spawn(|mut cx| async move {
                     let (workspace, items) = task.await;
@@ -2904,7 +2946,7 @@ pub fn open_new(
     cx: &mut MutableAppContext,
     init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
 ) -> Task<()> {
-    let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
+    let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
     cx.spawn(|mut cx| async move {
         let (workspace, opened_paths) = task.await;
 

crates/zed/src/main.rs 🔗

@@ -216,7 +216,7 @@ fn main() {
                 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
                     .detach();
             } else if let Ok(Some(paths)) = open_paths_rx.try_next() {
-                cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
                     .detach();
             } else {
                 cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
@@ -237,7 +237,7 @@ fn main() {
                 let app_state = app_state.clone();
                 async move {
                     while let Some(paths) = open_paths_rx.next().await {
-                        cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                        cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
                             .detach();
                     }
                 }
@@ -603,7 +603,7 @@ async fn handle_cli_connection(
                     paths
                 };
                 let (workspace, items) = cx
-                    .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
                     .await;
 
                 let mut errored = false;

crates/zed/src/zed.rs 🔗

@@ -728,6 +728,10 @@ mod tests {
                         "ca": null,
                         "cb": null,
                     },
+                    "d": {
+                        "da": null,
+                        "db": null,
+                    },
                 }),
             )
             .await;
@@ -736,13 +740,14 @@ mod tests {
             open_paths(
                 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
                 &app_state,
+                None,
                 cx,
             )
         })
         .await;
         assert_eq!(cx.window_ids().len(), 1);
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
@@ -756,11 +761,37 @@ mod tests {
             open_paths(
                 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
                 &app_state,
+                None,
+                cx,
+            )
+        })
+        .await;
+        assert_eq!(cx.window_ids().len(), 2);
+
+        // Replace existing windows
+        let window_id = cx.window_ids()[0];
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+                &app_state,
+                Some(window_id),
                 cx,
             )
         })
         .await;
         assert_eq!(cx.window_ids().len(), 2);
+        let workspace_1 = cx.root_view::<Workspace>(window_id).unwrap();
+        workspace_1.read_with(cx, |workspace, cx| {
+            assert_eq!(
+                workspace
+                    .worktrees(cx)
+                    .map(|w| w.read(cx).abs_path())
+                    .collect::<Vec<_>>(),
+                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+            );
+            assert!(workspace.left_sidebar().read(cx).is_open());
+            assert!(workspace.active_pane().is_focused(cx));
+        });
     }
 
     #[gpui::test]
@@ -772,7 +803,7 @@ mod tests {
             .insert_tree("/root", json!({"a": "hey"}))
             .await;
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
 
@@ -810,7 +841,7 @@ mod tests {
         assert!(!cx.is_window_edited(workspace.window_id()));
 
         // Opening the buffer again doesn't impact the window's edited state.
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace