diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 905aa328f2..6b512d950f 100644 --- a/crates/collab/src/integration_tests.rs +++ b/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 { 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( diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 9b51415069..c12e68a854 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/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); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index fde304cd35..672730cf22 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/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(), diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 271843dc69..7387ec8fdc 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ce31e68dae..7c7e3391d5 100644 --- a/crates/editor/src/editor.rs +++ b/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| { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 43e50829f5..74b82a20bd 100644 --- a/crates/editor/src/test.rs +++ b/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) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 768e58407e..aa2174b959 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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::().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 diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bc314d81be..afd911233b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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); diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 2180883ad4..309a11d41f 100644 --- a/crates/terminal/src/modal.rs +++ b/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) { +pub fn deploy_modal(_workspace: &mut Workspace, _: &DeployModal, _cx: &mut ViewContext) { // 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, - event: &Event, - cx: &mut ViewContext, + _event: &Event, + _cx: &mut ViewContext, ) { // Dismiss the modal if the terminal quit // if let Event::CloseTerminal = event { diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index bee78e3ce0..f9ee6e8082 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/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) } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 2be2eb8d6f..0e77b05ba2 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/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| { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index e0a8ee4bb5..f1e8aa8539 100644 --- a/crates/workspace/src/dock.rs +++ b/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 { + 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) -> Box; + pub struct Dock { - position: Option, + position: DockPosition, pane: ViewHandle, + default_item_factory: DefaultItemFactory, } impl Dock { - pub fn new(cx: &mut ViewContext) -> Self { - let pane = cx.add_view(Pane::new); + pub fn new(cx: &mut ViewContext, 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 render(&self, _theme: &Theme, position: DockPosition) -> Option { - if self.position.is_some() && self.position.unwrap() == position { - Some(ChildView::new(self.pane.clone()).boxed()) - } else { - None + pub fn pane(&self) -> ViewHandle { + self.pane.clone() + } + + fn hide(&mut self) { + self.position = self.position.hide(); + } + + fn ensure_not_empty(workspace: &mut Workspace, cx: &mut ViewContext) { + 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 struct ToggleDock { - dock: Arc, -} + fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext) { + // Shift-escape ON + // Get or insert the dock's last focused terminal + // Open the dock in fullscreen + // Focus that terminal -impl ToggleDock { - pub fn new(dock: Arc, _cx: &mut ViewContext) -> Self { - Self { dock } + // 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, + ) { + // 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 { + self.position + .visible() + .filter(|current_anchor| *current_anchor == anchor) + .map(|_| ChildView::new(self.pane.clone()).boxed()) } } -impl Entity for ToggleDock { +pub struct ToggleDockButton { + workspace: WeakViewHandle, +} + +impl ToggleDockButton { + pub fn new(workspace: WeakViewHandle, _cx: &mut ViewContext) -> Self { + Self { workspace } + } +} + +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 + 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); - // 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!() + MouseEventHandler::new::(0, cx, |state, cx| { + let theme = &cx + .global::() + .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>, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 671f6a9f92..fc95deca90 100644 --- a/crates/workspace/src/pane.rs +++ b/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>, toolbar: ViewHandle, context_menu: ViewHandle, + is_dock: bool, } pub struct ItemNavHistory { @@ -235,7 +255,7 @@ pub enum ReorderBehavior { } impl Pane { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(is_dock: bool, cx: &mut ViewContext) -> 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.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.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 diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index e05e0fb5ff..8102bb7bb2 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/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); - } + 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) - } - 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(); - } + } + workspace + }); } - }) - } - - Ok(()) + 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(); + } + } + }) } - }); + + Ok(()) + } + }); Self { project_id, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cb8d57ad1f..56b771020d 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, 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, pub build_window_options: fn() -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), + 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, cx: &mut ViewContext) -> Self { + pub fn new( + project: ModelHandle, + dock_default_factory: DefaultItemFactory, + cx: &mut ViewContext, + ) -> 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(¢er_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(¢er_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(¢er_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) -> ViewHandle { - 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, cx: &mut ViewContext) { + Pane::add_item(self, &self.dock.pane(), item, true, true, None, cx); + } + pub fn open_path( &mut self, path: impl Into, @@ -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, 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, + ) -> Box { + 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(); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fc3616d592..3bfd5e6e1a 100644 --- a/crates/zed/src/main.rs +++ b/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, +) -> Box { + let strategy = cx + .global::() + .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) +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a8f10a1579..103db1b515 100644 --- a/crates/zed/src/zed.rs +++ b/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));