WIP dock split button and default item

K Simmons created

Change summary

crates/collab/src/integration_tests.rs             |  14 
crates/command_palette/src/command_palette.rs      |   3 
crates/contacts_panel/src/contacts_panel.rs        |   3 
crates/diagnostics/src/diagnostics.rs              |   3 
crates/editor/src/editor.rs                        |   2 
crates/editor/src/test.rs                          |   3 
crates/file_finder/src/file_finder.rs              |  18 
crates/project_panel/src/project_panel.rs          |   6 
crates/terminal/src/modal.rs                       |  18 
crates/terminal/src/tests/terminal_test_context.rs |   4 
crates/vim/src/vim_test_context.rs                 |   3 
crates/workspace/src/dock.rs                       | 215 ++++++++++++---
crates/workspace/src/pane.rs                       |  66 ++++
crates/workspace/src/waiting_room.rs               | 142 +++++-----
crates/workspace/src/workspace.rs                  |  79 +++--
crates/zed/src/main.rs                             |  25 +
crates/zed/src/zed.rs                              |  24 +
17 files changed, 435 insertions(+), 193 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -298,7 +298,8 @@ async fn test_host_disconnect(
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), true, cx)
@@ -2786,7 +2787,8 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_window_b, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), true, cx)
@@ -3001,7 +3003,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_window_b, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), true, cx)
@@ -5224,6 +5227,7 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: Default::default,
             initialize_workspace: |_, _, _| unimplemented!(),
+            default_item_factory: |_, _| unimplemented!(),
         });
 
         Channel::init(&client);
@@ -5459,7 +5463,9 @@ impl TestClient {
         cx: &mut TestAppContext,
     ) -> ViewHandle<Workspace> {
         let (_, root_view) = cx.add_window(|_| EmptyView);
-        cx.add_view(&root_view, |cx| Workspace::new(project.clone(), cx))
+        cx.add_view(&root_view, |cx| {
+            Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
+        })
     }
 
     async fn simulate_host(

crates/command_palette/src/command_palette.rs 🔗

@@ -350,7 +350,8 @@ mod tests {
         });
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let editor = cx.add_view(&workspace, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -1247,7 +1247,8 @@ mod tests {
             .0
             .read_with(cx, |worktree, _| worktree.id().to_proto());
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = cx.add_view(&workspace, |cx| {
             ContactsPanel::new(
                 user_store.clone(),

crates/diagnostics/src/diagnostics.rs 🔗

@@ -776,7 +776,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {

crates/editor/src/editor.rs 🔗

@@ -7100,7 +7100,7 @@ mod tests {
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
         use workspace::Item;
-        let (_, pane) = cx.add_window(Default::default(), Pane::new);
+        let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(false, cx));
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
 
         cx.add_view(&pane, |cx| {

crates/editor/src/test.rs 🔗

@@ -364,7 +364,8 @@ impl<'a> EditorLspTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { file_name: "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)

crates/file_finder/src/file_finder.rs 🔗

@@ -316,7 +316,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -370,7 +371,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -444,7 +446,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
         finder
@@ -468,7 +471,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -520,7 +524,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -558,7 +563,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
         finder

crates/project_panel/src/project_panel.rs 🔗

@@ -1243,7 +1243,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1335,7 +1336,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);

crates/terminal/src/modal.rs 🔗

@@ -1,15 +1,9 @@
 use gpui::{ModelHandle, ViewContext};
-use settings::{Settings, WorkingDirectory};
-use workspace::{dock::Dock, Workspace};
+use workspace::Workspace;
 
-use crate::{
-    terminal_container_view::{
-        get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
-    },
-    Event, Terminal,
-};
+use crate::{terminal_container_view::DeployModal, Event, Terminal};
 
-pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
+pub fn deploy_modal(_workspace: &mut Workspace, _: &DeployModal, _cx: &mut ViewContext<Workspace>) {
     // let window = cx.window_id();
 
     // // Pull the terminal connection out of the global if it has been stored
@@ -62,10 +56,10 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
 }
 
 pub fn on_event(
-    workspace: &mut Workspace,
+    _workspace: &mut Workspace,
     _: ModelHandle<Terminal>,
-    event: &Event,
-    cx: &mut ViewContext<Workspace>,
+    _event: &Event,
+    _cx: &mut ViewContext<Workspace>,
 ) {
     // Dismiss the modal if the terminal quit
     // if let Event::CloseTerminal = event {

crates/terminal/src/tests/terminal_test_context.rs 🔗

@@ -21,7 +21,9 @@ impl<'a> TerminalTestContext<'a> {
         let params = self.cx.update(AppState::test);
 
         let project = Project::test(params.fs.clone(), [], self.cx).await;
-        let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) = self
+            .cx
+            .add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         (project, workspace)
     }

crates/vim/src/vim_test_context.rs 🔗

@@ -39,7 +39,8 @@ impl<'a> VimTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         // Setup search toolbars
         workspace.update(cx, |workspace, cx| {

crates/workspace/src/dock.rs 🔗

@@ -1,83 +1,210 @@
-use std::sync::Arc;
-
-use gpui::{elements::ChildView, Element, ElementBox, Entity, View, ViewContext, ViewHandle};
+use gpui::{
+    actions,
+    elements::{ChildView, MouseEventHandler, Svg},
+    impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
+    MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use serde::Deserialize;
+use settings::Settings;
 use theme::Theme;
 
-use crate::{Pane, StatusItemView, Workspace};
+use crate::{pane, ItemHandle, Pane, StatusItemView, Workspace};
 
-#[derive(PartialEq, Eq, Default, Copy, Clone)]
-pub enum DockPosition {
+#[derive(PartialEq, Clone, Deserialize)]
+pub struct MoveDock(pub DockAnchor);
+
+#[derive(PartialEq, Clone)]
+pub struct AddDefaultItemToDock;
+
+actions!(workspace, [ToggleDock]);
+impl_internal_actions!(workspace, [MoveDock, AddDefaultItemToDock]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(Dock::toggle);
+    cx.add_action(Dock::move_dock);
+}
+
+#[derive(PartialEq, Eq, Default, Copy, Clone, Deserialize)]
+pub enum DockAnchor {
     #[default]
     Bottom,
     Right,
-    Fullscreen,
+    Expanded,
+}
+
+#[derive(Copy, Clone)]
+pub enum DockPosition {
+    Shown(DockAnchor),
+    Hidden(DockAnchor),
+}
+
+impl Default for DockPosition {
+    fn default() -> Self {
+        DockPosition::Hidden(Default::default())
+    }
+}
+
+impl DockPosition {
+    fn toggle(self) -> Self {
+        match self {
+            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
+            DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
+        }
+    }
+
+    fn visible(&self) -> Option<DockAnchor> {
+        match self {
+            DockPosition::Shown(anchor) => Some(*anchor),
+            DockPosition::Hidden(_) => None,
+        }
+    }
+
+    fn hide(self) -> Self {
+        match self {
+            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
+            DockPosition::Hidden(_) => self,
+        }
+    }
 }
 
+pub type DefaultItemFactory =
+    fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
+
 pub struct Dock {
-    position: Option<DockPosition>,
+    position: DockPosition,
     pane: ViewHandle<Pane>,
+    default_item_factory: DefaultItemFactory,
 }
 
 impl Dock {
-    pub fn new(cx: &mut ViewContext<Workspace>) -> Self {
-        let pane = cx.add_view(Pane::new);
+    pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
+        let pane = cx.add_view(|cx| Pane::new(true, cx));
+
+        cx.subscribe(&pane.clone(), |workspace, _, event, cx| {
+            if let pane::Event::Remove = event {
+                workspace.dock.hide();
+                cx.notify();
+            }
+        })
+        .detach();
+
         Self {
             pane,
-            position: None,
+            position: Default::default(),
+            default_item_factory,
+        }
+    }
+
+    pub fn pane(&self) -> ViewHandle<Pane> {
+        self.pane.clone()
+    }
+
+    fn hide(&mut self) {
+        self.position = self.position.hide();
+    }
+
+    fn ensure_not_empty(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        let pane = workspace.dock.pane.clone();
+        if !pane.read(cx).items().next().is_none() {
+            let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
+            Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
         }
     }
 
-    pub fn render(&self, _theme: &Theme, position: DockPosition) -> Option<ElementBox> {
-        if self.position.is_some() && self.position.unwrap() == position {
-            Some(ChildView::new(self.pane.clone()).boxed())
-        } else {
-            None
+    fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
+        // Shift-escape ON
+        // Get or insert the dock's last focused terminal
+        // Open the dock in fullscreen
+        // Focus that terminal
+
+        // Shift-escape OFF
+        // Close the dock
+        // Return focus to center
+
+        // Behaviors:
+        // If the dock is shown, hide it
+        // If the dock is hidden, show it
+        // If the dock was full screen, open it in last position (bottom or right)
+        // If the dock was bottom or right, re-open it in that context (and with the previous % width)
+
+        workspace.dock.position = workspace.dock.position.toggle();
+        if workspace.dock.position.visible().is_some() {
+            Self::ensure_not_empty(workspace, cx);
         }
+        cx.notify();
+    }
+
+    fn move_dock(
+        workspace: &mut Workspace,
+        &MoveDock(new_anchor): &MoveDock,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        // Clear the previous position if the dock is not visible.
+        workspace.dock.position = DockPosition::Shown(new_anchor);
+        Self::ensure_not_empty(workspace, cx);
+        cx.notify();
+    }
+
+    pub fn render(&self, _theme: &Theme, anchor: DockAnchor) -> Option<ElementBox> {
+        self.position
+            .visible()
+            .filter(|current_anchor| *current_anchor == anchor)
+            .map(|_| ChildView::new(self.pane.clone()).boxed())
     }
 }
 
-pub struct ToggleDock {
-    dock: Arc<Dock>,
+pub struct ToggleDockButton {
+    workspace: WeakViewHandle<Workspace>,
 }
 
-impl ToggleDock {
-    pub fn new(dock: Arc<Dock>, _cx: &mut ViewContext<Self>) -> Self {
-        Self { dock }
+impl ToggleDockButton {
+    pub fn new(workspace: WeakViewHandle<Workspace>, _cx: &mut ViewContext<Self>) -> Self {
+        Self { workspace }
     }
 }
 
-impl Entity for ToggleDock {
+impl Entity for ToggleDockButton {
     type Event = ();
 }
 
-impl View for ToggleDock {
+impl View for ToggleDockButton {
     fn ui_name() -> &'static str {
         "Dock Toggle"
     }
-    // Shift-escape ON
-    // Get or insert the dock's last focused terminal
-    // Open the dock in fullscreen
-    // Focus that terminal
-
-    // Shift-escape OFF
-    // Close the dock
-    // Return focus to center
-
-    // Behaviors:
-    // If the dock is shown, hide it
-    // If the dock is hidden, show it
-    // If the dock was full screen, open it in last position (bottom or right)
-    // If the dock was bottom or right, re-open it in that context (and with the previous % width)
-    // On hover, change color and background
-    // On shown, change color and background
-    // On hidden, change color and background
-    // Show tool tip
-    fn render(&mut self, _cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        todo!()
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let dock_is_open = self
+            .workspace
+            .upgrade(cx)
+            .map(|workspace| workspace.read(cx).dock.position.visible().is_some())
+            .unwrap_or(false);
+
+        MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
+            let theme = &cx
+                .global::<Settings>()
+                .theme
+                .workspace
+                .status_bar
+                .sidebar_buttons;
+            let style = theme.item.style_for(state, dock_is_open);
+
+            Svg::new("icons/terminal_16.svg")
+                .with_color(style.icon_color)
+                .constrained()
+                .with_width(style.icon_size)
+                .with_height(style.icon_size)
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ToggleDock))
+        // TODO: Add tooltip
+        .boxed()
     }
 }
 
-impl StatusItemView for ToggleDock {
+impl StatusItemView for ToggleDockButton {
     fn set_active_pane_item(
         &mut self,
         _active_pane_item: Option<&dyn crate::ItemHandle>,

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,9 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
+use crate::{
+    dock::{DockAnchor, MoveDock},
+    toolbar::Toolbar,
+    Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
+};
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
@@ -76,13 +80,27 @@ pub struct DeploySplitMenu {
     position: Vector2F,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct DeployDockMenu {
+    position: Vector2F,
+}
+
 #[derive(Clone, PartialEq)]
 pub struct DeployNewMenu {
     position: Vector2F,
 }
 
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
-impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
+impl_internal_actions!(
+    pane,
+    [
+        CloseItem,
+        DeploySplitMenu,
+        DeployNewMenu,
+        DeployDockMenu,
+        MoveItem
+    ]
+);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -141,6 +159,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
     cx.add_action(Pane::deploy_split_menu);
     cx.add_action(Pane::deploy_new_menu);
+    cx.add_action(Pane::deploy_dock_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -186,6 +205,7 @@ pub struct Pane {
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
     context_menu: ViewHandle<ContextMenu>,
+    is_dock: bool,
 }
 
 pub struct ItemNavHistory {
@@ -235,7 +255,7 @@ pub enum ReorderBehavior {
 }
 
 impl Pane {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(is_dock: bool, cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
         let context_menu = cx.add_view(ContextMenu::new);
         Self {
@@ -254,6 +274,7 @@ impl Pane {
             })),
             toolbar: cx.add_view(|_| Toolbar::new(handle)),
             context_menu,
+            is_dock,
         }
     }
 
@@ -976,6 +997,20 @@ impl Pane {
         });
     }
 
+    fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
+        self.context_menu.update(cx, |menu, cx| {
+            menu.show(
+                action.position,
+                vec![
+                    ContextMenuItem::item("Move Dock Right", MoveDock(DockAnchor::Right)),
+                    ContextMenuItem::item("Move Dock Bottom", MoveDock(DockAnchor::Bottom)),
+                    ContextMenuItem::item("Move Dock Maximized", MoveDock(DockAnchor::Expanded)),
+                ],
+                cx,
+            );
+        });
+    }
+
     fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
         self.context_menu.update(cx, |menu, cx| {
             menu.show(
@@ -1320,6 +1355,8 @@ impl View for Pane {
 
         let this = cx.handle();
 
+        let is_dock = self.is_dock;
+
         Stack::new()
             .with_child(
                 EventHandler::new(if let Some(active_item) = self.active_item() {
@@ -1382,10 +1419,16 @@ impl View for Pane {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(MouseButton::Left, |e, cx| {
-                                        cx.dispatch_action(DeploySplitMenu {
-                                            position: e.position,
-                                        });
+                                    .on_down(MouseButton::Left, move |e, cx| {
+                                        if is_dock {
+                                            cx.dispatch_action(DeployDockMenu {
+                                                position: e.position,
+                                            });
+                                        } else {
+                                            cx.dispatch_action(DeploySplitMenu {
+                                                position: e.position,
+                                            });
+                                        }
                                     })
                                     .boxed(),
                                 ])
@@ -1570,7 +1613,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, crate::tests::default_item_factory, cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1658,7 +1702,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, crate::tests::default_item_factory, cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1734,7 +1779,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, crate::tests::default_item_factory, cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view

crates/workspace/src/waiting_room.rs 🔗

@@ -74,82 +74,84 @@ impl WaitingRoom {
     ) -> Self {
         let project_id = contact.projects[project_index].id;
         let client = app_state.client.clone();
-        let _join_task =
-            cx.spawn_weak({
-                let contact = contact.clone();
-                |this, mut cx| async move {
-                    let project = Project::remote(
-                        project_id,
-                        app_state.client.clone(),
-                        app_state.user_store.clone(),
-                        app_state.project_store.clone(),
-                        app_state.languages.clone(),
-                        app_state.fs.clone(),
-                        cx.clone(),
-                    )
-                    .await;
+        let _join_task = cx.spawn_weak({
+            let contact = contact.clone();
+            |this, mut cx| async move {
+                let project = Project::remote(
+                    project_id,
+                    app_state.client.clone(),
+                    app_state.user_store.clone(),
+                    app_state.project_store.clone(),
+                    app_state.languages.clone(),
+                    app_state.fs.clone(),
+                    cx.clone(),
+                )
+                .await;
 
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| {
-                            this.waiting = false;
-                            match project {
-                                Ok(project) => {
-                                    cx.replace_root_view(|cx| {
-                                        let mut workspace = Workspace::new(project, cx);
-                                        (app_state.initialize_workspace)(
-                                            &mut workspace,
-                                            &app_state,
-                                            cx,
-                                        );
-                                        workspace.toggle_sidebar(Side::Left, cx);
-                                        if let Some((host_peer_id, _)) =
-                                            workspace.project.read(cx).collaborators().iter().find(
-                                                |(_, collaborator)| collaborator.replica_id == 0,
-                                            )
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.waiting = false;
+                        match project {
+                            Ok(project) => {
+                                cx.replace_root_view(|cx| {
+                                    let mut workspace =
+                                        Workspace::new(project, app_state.default_item_factory, cx);
+                                    (app_state.initialize_workspace)(
+                                        &mut workspace,
+                                        &app_state,
+                                        cx,
+                                    );
+                                    workspace.toggle_sidebar(Side::Left, cx);
+                                    if let Some((host_peer_id, _)) = workspace
+                                        .project
+                                        .read(cx)
+                                        .collaborators()
+                                        .iter()
+                                        .find(|(_, collaborator)| collaborator.replica_id == 0)
+                                    {
+                                        if let Some(follow) = workspace
+                                            .toggle_follow(&ToggleFollow(*host_peer_id), cx)
                                         {
-                                            if let Some(follow) = workspace
-                                                .toggle_follow(&ToggleFollow(*host_peer_id), cx)
-                                            {
-                                                follow.detach_and_log_err(cx);
-                                            }
-                                        }
-                                        workspace
-                                    });
-                                }
-                                Err(error) => {
-                                    let login = &contact.user.github_login;
-                                    let message = match error {
-                                        project::JoinProjectError::HostDeclined => {
-                                            format!("@{} declined your request.", login)
+                                            follow.detach_and_log_err(cx);
                                         }
-                                        project::JoinProjectError::HostClosedProject => {
-                                            format!(
-                                                "@{} closed their copy of {}.",
-                                                login,
-                                                humanize_list(
-                                                    &contact.projects[project_index]
-                                                        .visible_worktree_root_names
-                                                )
+                                    }
+                                    workspace
+                                });
+                            }
+                            Err(error) => {
+                                let login = &contact.user.github_login;
+                                let message = match error {
+                                    project::JoinProjectError::HostDeclined => {
+                                        format!("@{} declined your request.", login)
+                                    }
+                                    project::JoinProjectError::HostClosedProject => {
+                                        format!(
+                                            "@{} closed their copy of {}.",
+                                            login,
+                                            humanize_list(
+                                                &contact.projects[project_index]
+                                                    .visible_worktree_root_names
                                             )
-                                        }
-                                        project::JoinProjectError::HostWentOffline => {
-                                            format!("@{} went offline.", login)
-                                        }
-                                        project::JoinProjectError::Other(error) => {
-                                            log::error!("error joining project: {}", error);
-                                            "An error occurred.".to_string()
-                                        }
-                                    };
-                                    this.message = message;
-                                    cx.notify();
-                                }
+                                        )
+                                    }
+                                    project::JoinProjectError::HostWentOffline => {
+                                        format!("@{} went offline.", login)
+                                    }
+                                    project::JoinProjectError::Other(error) => {
+                                        log::error!("error joining project: {}", error);
+                                        "An error occurred.".to_string()
+                                    }
+                                };
+                                this.message = message;
+                                cx.notify();
                             }
-                        })
-                    }
-
-                    Ok(())
+                        }
+                    })
                 }
-            });
+
+                Ok(())
+            }
+        });
 
         Self {
             project_id,

crates/workspace/src/workspace.rs 🔗

@@ -1,9 +1,9 @@
-pub mod dock;
 /// NOTE: Focus only 'takes' after an update has flushed_effects. Pane sends an event in on_focus_in
 /// which the workspace uses to change the activated pane.
 ///
 /// This may cause issues when you're trying to write tests that use workspace focus to add items at
 /// specific locations.
+pub mod dock;
 pub mod pane;
 pub mod pane_group;
 pub mod searchable;
@@ -18,7 +18,7 @@ use client::{
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockPosition, ToggleDock};
+use dock::{DefaultItemFactory, Dock, DockAnchor, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
@@ -147,6 +147,7 @@ impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
+    dock::init(cx);
 
     cx.add_global_action(open);
     cx.add_global_action({
@@ -262,6 +263,7 @@ pub struct AppState {
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options: fn() -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
+    pub default_item_factory: DefaultItemFactory,
 }
 
 #[derive(Eq, PartialEq, Hash)]
@@ -867,6 +869,7 @@ impl AppState {
             project_store,
             initialize_workspace: |_, _, _| {},
             build_window_options: Default::default,
+            default_item_factory: |_, _| unimplemented!(),
         })
     }
 }
@@ -920,7 +923,11 @@ enum FollowerItem {
 }
 
 impl Workspace {
-    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        dock_default_factory: DefaultItemFactory,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
 
         cx.observe_window_activation(Self::on_window_activation_changed)
@@ -947,14 +954,14 @@ impl Workspace {
         })
         .detach();
 
-        let pane = cx.add_view(Pane::new);
-        let pane_id = pane.id();
-        cx.subscribe(&pane, move |this, _, event, cx| {
+        let center_pane = cx.add_view(|cx| Pane::new(false, cx));
+        let pane_id = center_pane.id();
+        cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
         })
         .detach();
-        cx.focus(&pane);
-        cx.emit(Event::PaneAdded(pane.clone()));
+        cx.focus(&center_pane);
+        cx.emit(Event::PaneAdded(center_pane.clone()));
 
         let fs = project.read(cx).fs().clone();
         let user_store = project.read(cx).user_store();
@@ -977,19 +984,18 @@ impl Workspace {
         });
 
         let weak_self = cx.weak_handle();
-
         cx.emit_global(WorkspaceCreated(weak_self.clone()));
 
-        let dock = Dock::new(cx);
+        let dock = Dock::new(cx, dock_default_factory);
 
         let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
         let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
         let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
-        let toggle_dock = cx.add_view(|cx| ToggleDock::new(Arc::new(dock), cx));
+        let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(weak_self.clone(), cx));
         let right_sidebar_buttons =
             cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
         let status_bar = cx.add_view(|cx| {
-            let mut status_bar = StatusBar::new(&pane.clone(), cx);
+            let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
             status_bar.add_left_item(left_sidebar_buttons, cx);
             status_bar.add_right_item(right_sidebar_buttons, cx);
             status_bar.add_right_item(toggle_dock, cx);
@@ -1003,11 +1009,11 @@ impl Workspace {
         let mut this = Workspace {
             modal: None,
             weak_self,
-            center: PaneGroup::new(pane.clone()),
+            center: PaneGroup::new(center_pane.clone()),
             dock,
-            panes: vec![pane.clone()],
+            panes: vec![center_pane.clone()],
             panes_by_item: Default::default(),
-            active_pane: pane.clone(),
+            active_pane: center_pane.clone(),
             status_bar,
             notifications: Default::default(),
             client,
@@ -1081,6 +1087,7 @@ impl Workspace {
                         app_state.fs.clone(),
                         cx,
                     ),
+                    app_state.default_item_factory,
                     cx,
                 );
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -1532,7 +1539,7 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(Pane::new);
+        let pane = cx.add_view(|cx| Pane::new(false, cx));
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -1549,6 +1556,10 @@ impl Workspace {
         Pane::add_item(self, &active_pane, item, true, true, None, cx);
     }
 
+    pub fn add_item_to_dock(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        Pane::add_item(self, &self.dock.pane(), item, true, true, None, cx);
+    }
+
     pub fn open_path(
         &mut self,
         path: impl Into<ProjectPath>,
@@ -2573,7 +2584,7 @@ impl View for Workspace {
                                                 )
                                                 .with_children(
                                                     self.dock
-                                                        .render(&theme, DockPosition::Bottom)
+                                                        .render(&theme, DockAnchor::Bottom)
                                                         .map(|dock| {
                                                             FlexItem::new(dock)
                                                                 .flex(1., true)
@@ -2587,7 +2598,7 @@ impl View for Workspace {
                                     )
                                     .with_children(
                                         self.dock
-                                            .render(&theme, DockPosition::Right)
+                                            .render(&theme, DockAnchor::Right)
                                             .map(|dock| FlexItem::new(dock).flex(1., true).boxed()),
                                     )
                                     .with_children(
@@ -2603,7 +2614,7 @@ impl View for Workspace {
                                     )
                                     .boxed()
                             })
-                            .with_children(self.dock.render(&theme, DockPosition::Fullscreen))
+                            .with_children(self.dock.render(&theme, DockAnchor::Expanded))
                             .with_children(self.modal.as_ref().map(|m| {
                                 ChildView::new(m)
                                     .contained()
@@ -2811,7 +2822,7 @@ pub fn open_paths(
                     cx,
                 );
                 new_project = Some(project.clone());
-                let mut workspace = Workspace::new(project, cx);
+                let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
                     workspace.toggle_sidebar(Side::Left, cx);
@@ -2872,6 +2883,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
                 app_state.fs.clone(),
                 cx,
             ),
+            app_state.default_item_factory,
             cx,
         );
         (app_state.initialize_workspace)(&mut workspace, app_state, cx);
@@ -2889,6 +2901,13 @@ mod tests {
     use project::{FakeFs, Project, ProjectEntryId};
     use serde_json::json;
 
+    pub fn default_item_factory(
+        _workspace: &mut Workspace,
+        _cx: &mut ViewContext<Workspace>,
+    ) -> Box<dyn ItemHandle> {
+        unimplemented!();
+    }
+
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
@@ -2896,7 +2915,8 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
 
         // Adding an item with no ambiguity renders the tab without detail.
         let item1 = cx.add_view(&workspace, |_| {
@@ -2960,7 +2980,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
@@ -3056,7 +3077,8 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
 
         // When there are no dirty items, there's nothing to do.
         let item1 = cx.add_view(&workspace, |_| TestItem::new());
@@ -3096,7 +3118,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item1 = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();
@@ -3191,7 +3214,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
@@ -3292,7 +3316,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();
@@ -3409,7 +3434,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();

crates/zed/src/main.rs 🔗

@@ -19,20 +19,21 @@ use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
+use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
 use isahc::{config::Configurable, AsyncBody, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::{Fs, ProjectStore};
 use serde_json::json;
-use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
+use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory};
 use smol::process::Command;
 use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration};
+use terminal::terminal_container_view::{get_working_directory, TerminalContainer};
 
 use theme::ThemeRegistry;
 use util::{ResultExt, TryFutureExt};
-use workspace::{self, AppState, NewFile, OpenPaths};
+use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
 use zed::{
     self, build_window_options,
     fs::RealFs,
@@ -148,6 +149,7 @@ fn main() {
             fs,
             build_window_options,
             initialize_workspace,
+            default_item_factory,
         });
         auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
         workspace::init(app_state.clone(), cx);
@@ -591,3 +593,20 @@ async fn handle_cli_connection(
         }
     }
 }
+
+pub fn default_item_factory(
+    workspace: &mut Workspace,
+    cx: &mut ViewContext<Workspace>,
+) -> Box<dyn ItemHandle> {
+    let strategy = cx
+        .global::<Settings>()
+        .terminal_overrides
+        .working_directory
+        .clone()
+        .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+
+    let working_directory = get_working_directory(workspace, cx, strategy);
+
+    let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+    Box::new(terminal_handle)
+}

crates/zed/src/zed.rs 🔗

@@ -723,7 +723,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -842,7 +843,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1001,7 +1003,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1043,7 +1046,8 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
@@ -1132,7 +1136,8 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, NewFile);
@@ -1185,7 +1190,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1258,7 +1264,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1522,7 +1529,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));