WIP: Start on `TerminalPanel`

Antonio Scandurra created

Change summary

crates/terminal_view/src/terminal_panel.rs | 76 ++++++++++++++++++++
crates/terminal_view/src/terminal_view.rs  |  1 
crates/theme/src/theme.rs                  |  1 
crates/workspace/src/dock.rs               |  4 
crates/workspace/src/pane.rs               |  9 +-
crates/workspace/src/workspace.rs          | 87 +++++++++++++----------
crates/zed/src/zed.rs                      | 18 +++-
styles/src/styleTree/statusBar.ts          |  1 
8 files changed, 149 insertions(+), 48 deletions(-)

Detailed changes

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -0,0 +1,76 @@
+use gpui::{elements::*, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle};
+use project::Project;
+use settings::{Settings, WorkingDirectory};
+use util::ResultExt;
+use workspace::{dock::Panel, Pane, Workspace};
+
+use crate::TerminalView;
+
+pub struct TerminalPanel {
+    project: ModelHandle<Project>,
+    pane: ViewHandle<Pane>,
+    workspace: WeakViewHandle<Workspace>,
+}
+
+impl TerminalPanel {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            project: workspace.project().clone(),
+            pane: cx.add_view(|cx| {
+                Pane::new(
+                    workspace.weak_handle(),
+                    workspace.app_state().background_actions,
+                    cx,
+                )
+            }),
+            workspace: workspace.weak_handle(),
+        }
+    }
+}
+
+impl Entity for TerminalPanel {
+    type Event = ();
+}
+
+impl View for TerminalPanel {
+    fn ui_name() -> &'static str {
+        "TerminalPanel"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+        ChildView::new(&self.pane, cx).into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if self.pane.read(cx).items_len() == 0 {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                let working_directory_strategy = cx
+                    .global::<Settings>()
+                    .terminal_overrides
+                    .working_directory
+                    .clone()
+                    .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+                let working_directory = crate::get_working_directory(
+                    workspace.read(cx),
+                    cx,
+                    working_directory_strategy,
+                );
+                let window_id = cx.window_id();
+                if let Some(terminal) = self.project.update(cx, |project, cx| {
+                    project
+                        .create_terminal(working_directory, window_id, cx)
+                        .log_err()
+                }) {
+                    workspace.update(cx, |workspace, cx| {
+                        let terminal = Box::new(cx.add_view(|cx| {
+                            TerminalView::new(terminal, workspace.database_id(), cx)
+                        }));
+                        Pane::add_item(workspace, &self.pane, terminal, true, true, None, cx);
+                    });
+                }
+            }
+        }
+    }
+}
+
+impl Panel for TerminalPanel {}

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1,6 +1,7 @@
 mod persistence;
 pub mod terminal_button;
 pub mod terminal_element;
+pub mod terminal_panel;
 
 use context_menu::{ContextMenu, ContextMenuItem};
 use dirs::home_dir;

crates/theme/src/theme.rs 🔗

@@ -343,6 +343,7 @@ pub struct StatusBar {
 #[derive(Deserialize, Default)]
 pub struct StatusBarPanelButtons {
     pub group_left: ContainerStyle,
+    pub group_bottom: ContainerStyle,
     pub group_right: ContainerStyle,
     pub button: Interactive<PanelButton>,
     pub badge: ContainerStyle,

crates/workspace/src/dock.rs 🔗

@@ -20,7 +20,6 @@ pub trait Panel: View {
 }
 
 pub trait PanelHandle {
-
     fn id(&self) -> usize;
     fn should_show_badge(&self, cx: &WindowContext) -> bool;
     fn is_focused(&self, cx: &WindowContext) -> bool;
@@ -64,6 +63,7 @@ pub struct Dock {
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
 pub enum DockPosition {
     Left,
+    Bottom,
     Right,
 }
 
@@ -71,6 +71,7 @@ impl DockPosition {
     fn to_resizable_side(self) -> Side {
         match self {
             Self::Left => Side::Right,
+            Self::Bottom => Side::Bottom,
             Self::Right => Side::Left,
         }
     }
@@ -243,6 +244,7 @@ impl View for PanelButtons {
         let dock_position = dock.position;
         let group_style = match dock_position {
             DockPosition::Left => theme.group_left,
+            DockPosition::Bottom => theme.group_bottom,
             DockPosition::Right => theme.group_right,
         };
 

crates/workspace/src/pane.rs 🔗

@@ -2,9 +2,7 @@ mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
 use crate::{
-    item::WeakItemHandle,
-    toolbar::Toolbar,
-    Item, NewFile, NewSearch, NewTerminal, Workspace,
+    item::WeakItemHandle, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, Workspace,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -485,7 +483,7 @@ impl Pane {
         }
     }
 
-    pub(crate) fn add_item(
+    pub fn add_item(
         workspace: &mut Workspace,
         pane: &ViewHandle<Pane>,
         item: Box<dyn ItemHandle>,
@@ -1594,7 +1592,8 @@ impl Pane {
                 "icons/split_12.svg",
                 cx,
                 |pane, cx| pane.deploy_split_menu(cx),
-                self.tab_bar_context_menu.handle_if_kind(TabBarContextMenuKind::Split),
+                self.tab_bar_context_menu
+                    .handle_if_kind(TabBarContextMenuKind::Split),
             ))
             .contained()
             .with_style(theme.workspace.tab_bar.pane_button_container)

crates/workspace/src/workspace.rs 🔗

@@ -1,3 +1,4 @@
+pub mod dock;
 /// NOTE: Focus only 'takes' after an update has flushed_effects.
 ///
 /// This may cause issues when you're trying to write tests that use workspace focus to add items at
@@ -9,7 +10,6 @@ pub mod pane_group;
 mod persistence;
 pub mod searchable;
 pub mod shared_screen;
-pub mod dock;
 mod status_bar;
 mod toolbar;
 
@@ -60,6 +60,7 @@ use crate::{
     notifications::simple_message_notification::MessageNotification,
     persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
 };
+use dock::{Dock, DockPosition, PanelButtons, TogglePanel};
 use lazy_static::lazy_static;
 use notifications::{NotificationHandle, NotifyResultExt};
 pub use pane::*;
@@ -74,7 +75,6 @@ use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings::{Autosave, Settings};
 use shared_screen::SharedScreen;
-use dock::{Dock, PanelButtons, DockPosition, TogglePanel};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeRegistry};
@@ -443,6 +443,7 @@ pub struct Workspace {
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_dock: ViewHandle<Dock>,
+    bottom_dock: ViewHandle<Dock>,
     right_dock: ViewHandle<Dock>,
     panes: Vec<ViewHandle<Pane>>,
     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
@@ -526,8 +527,8 @@ impl Workspace {
 
         let weak_handle = cx.weak_handle();
 
-        let center_pane = cx
-            .add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
+        let center_pane =
+            cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
         cx.subscribe(&center_pane, Self::handle_pane_event).detach();
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
@@ -563,14 +564,18 @@ impl Workspace {
         cx.emit_global(WorkspaceCreated(weak_handle.clone()));
 
         let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
+        let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
         let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
         let left_dock_buttons =
             cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
+        let bottom_dock_buttons =
+            cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
         let right_dock_buttons =
             cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
         let status_bar = cx.add_view(|cx| {
             let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
             status_bar.add_left_item(left_dock_buttons, cx);
+            status_bar.add_right_item(bottom_dock_buttons, cx);
             status_bar.add_right_item(right_dock_buttons, cx);
             status_bar
         });
@@ -621,8 +626,9 @@ impl Workspace {
             titlebar_item: None,
             notifications: Default::default(),
             remote_entity_subscription: None,
-            left_dock: left_dock,
-            right_dock: right_dock,
+            left_dock,
+            bottom_dock,
+            right_dock,
             project: project.clone(),
             leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
@@ -817,6 +823,10 @@ impl Workspace {
         &self.left_dock
     }
 
+    pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
+        &self.bottom_dock
+    }
+
     pub fn right_dock(&self) -> &ViewHandle<Dock> {
         &self.right_dock
     }
@@ -1309,6 +1319,7 @@ impl Workspace {
     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
         let dock = match dock_side {
             DockPosition::Left => &mut self.left_dock,
+            DockPosition::Bottom => &mut self.bottom_dock,
             DockPosition::Right => &mut self.right_dock,
         };
         dock.update(cx, |dock, cx| {
@@ -1325,6 +1336,7 @@ impl Workspace {
     pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
         let dock = match action.dock_position {
             DockPosition::Left => &mut self.left_dock,
+            DockPosition::Bottom => &mut self.bottom_dock,
             DockPosition::Right => &mut self.right_dock,
         };
         let active_item = dock.update(cx, move |dock, cx| {
@@ -1361,6 +1373,7 @@ impl Workspace {
     ) {
         let dock = match dock_position {
             DockPosition::Left => &mut self.left_dock,
+            DockPosition::Bottom => &mut self.bottom_dock,
             DockPosition::Right => &mut self.right_dock,
         };
         let active_item = dock.update(cx, |dock, cx| {
@@ -1387,13 +1400,8 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|cx| {
-            Pane::new(
-                self.weak_handle(),
-                self.app_state.background_actions,
-                cx,
-            )
-        });
+        let pane =
+            cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
         cx.subscribe(&pane, Self::handle_pane_event).detach();
         self.panes.push(pane.clone());
         cx.focus(&pane);
@@ -1560,7 +1568,7 @@ impl Workspace {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
             self.active_item_path_changed(cx);
-                self.last_active_center_pane = Some(pane.downgrade());
+            self.last_active_center_pane = Some(pane.downgrade());
             cx.notify();
         }
 
@@ -2470,13 +2478,12 @@ impl Workspace {
         cx: &mut AppContext,
     ) {
         cx.spawn(|mut cx| async move {
-            let (project, old_center_pane) =
-                workspace.read_with(&cx, |workspace, _| {
-                    (
-                        workspace.project().clone(),
-                        workspace.last_active_center_pane.clone(),
-                    )
-                })?;
+            let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
+                (
+                    workspace.project().clone(),
+                    workspace.last_active_center_pane.clone(),
+                )
+            })?;
 
             // Traverse the splits tree and add to things
             let center_group = serialized_workspace
@@ -2615,22 +2622,28 @@ impl View for Workspace {
                                         },
                                     )
                                     .with_child(
-                                        FlexItem::new(
-                                            Flex::column()
-                                                .with_child(
-                                                    FlexItem::new(self.center.render(
-                                                        &project,
-                                                        &theme,
-                                                        &self.follower_states_by_leader,
-                                                        self.active_call(),
-                                                        self.active_pane(),
-                                                        &self.app_state,
-                                                        cx,
-                                                    ))
-                                                    .flex(1., true),
-                                                )
-                                        )
-                                        .flex(1., true),
+                                        Flex::column()
+                                            .with_child(
+                                                FlexItem::new(self.center.render(
+                                                    &project,
+                                                    &theme,
+                                                    &self.follower_states_by_leader,
+                                                    self.active_call(),
+                                                    self.active_pane(),
+                                                    &self.app_state,
+                                                    cx,
+                                                ))
+                                                .flex(1., true),
+                                            )
+                                            .with_children(
+                                                if self.bottom_dock.read(cx).active_item().is_some()
+                                                {
+                                                    Some(ChildView::new(&self.bottom_dock, cx))
+                                                } else {
+                                                    None
+                                                },
+                                            )
+                                            .flex(1., true),
                                     )
                                     .with_children(
                                         if self.right_dock.read(cx).active_item().is_some() {

crates/zed/src/zed.rs 🔗

@@ -31,12 +31,12 @@ use serde::Deserialize;
 use serde_json::to_string_pretty;
 use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
 use std::{borrow::Cow, str, sync::Arc};
-use terminal_view::terminal_button::TerminalButton;
+use terminal_view::terminal_panel::TerminalPanel;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
 pub use workspace;
 use workspace::{
-    create_and_open_local_file, open_new, dock::DockPosition, AppState, NewFile, NewWindow,
+    create_and_open_local_file, dock::DockPosition, open_new, AppState, NewFile, NewWindow,
     Workspace,
 };
 
@@ -318,10 +318,19 @@ pub fn initialize_workspace(
             "Project Panel".to_string(),
             project_panel,
             cx,
-        )
+        );
+    });
+
+    let terminal_panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
+    workspace.bottom_dock().update(cx, |dock, cx| {
+        dock.add_item(
+            "icons/terminal_12.svg",
+            "Terminals".to_string(),
+            terminal_panel,
+            cx,
+        );
     });
 
-    let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
     let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
@@ -335,7 +344,6 @@ pub fn initialize_workspace(
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(activity_indicator, cx);
-        status_bar.add_right_item(toggle_terminal, cx);
         status_bar.add_right_item(feedback_button, cx);
         status_bar.add_right_item(copilot, cx);
         status_bar.add_right_item(active_buffer_language, cx);

styles/src/styleTree/statusBar.ts 🔗

@@ -95,6 +95,7 @@ export default function statusBar(colorScheme: ColorScheme) {
         },
         panelButtons: {
             groupLeft: {},
+            groupBottom: {},
             groupRight: {},
             button: {
                 ...statusContainer,