Merge pull request #2253 from zed-industries/joseph/z-226-add-terminal-popup-menu

Petros Amoiridis created

Add terminal pop up menu

Change summary

Cargo.lock                                    |   1 
crates/feedback/src/deploy_feedback_button.rs |  11 +
crates/gpui/src/app.rs                        |   6 
crates/project/src/terminals.rs               |   4 
crates/terminal/src/terminal.rs               |  33 +++
crates/terminal_view/src/terminal_button.rs   | 176 +++++++++++++++++++++
crates/terminal_view/src/terminal_view.rs     |  40 ----
crates/workspace/Cargo.toml                   |   1 
crates/workspace/src/pane.rs                  |   2 
crates/workspace/src/terminal_button.rs       |  92 ----------
crates/workspace/src/workspace.rs             |   4 
crates/zed/src/zed.rs                         |   6 
12 files changed, 242 insertions(+), 134 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8349,6 +8349,7 @@ dependencies = [
  "serde_json",
  "settings",
  "smallvec",
+ "terminal",
  "theme",
  "util",
  "uuid 1.2.2",

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -25,10 +25,10 @@ impl View for DeployFeedbackButton {
 
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
         let active = self.active;
+        let theme = cx.global::<Settings>().theme.clone();
         Stack::new()
             .with_child(
-                MouseEventHandler::<Self>::new(0, cx, |state, cx| {
-                    let theme = &cx.global::<Settings>().theme;
+                MouseEventHandler::<Self>::new(0, cx, |state, _| {
                     let style = &theme
                         .workspace
                         .status_bar
@@ -54,6 +54,13 @@ impl View for DeployFeedbackButton {
                         cx.dispatch_action(GiveFeedback)
                     }
                 })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Give Feedback".into(),
+                    Some(Box::new(GiveFeedback)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
                 .boxed(),
             )
             .boxed()

crates/gpui/src/app.rs 🔗

@@ -2757,6 +2757,12 @@ impl AppContext {
         Some(self.views.get(&(window_id, view_id))?.ui_name())
     }
 
+    pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
+        self.views
+            .get(&(window_id, view_id))
+            .map(|view| view.as_any().type_id())
+    }
+
     pub fn background(&self) -> &Arc<executor::Background> {
         &self.background
     }

crates/project/src/terminals.rs 🔗

@@ -58,6 +58,10 @@ impl Project {
             terminal
         }
     }
+
+    pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
+        &self.terminals.local_handles
+    }
 }
 
 // TODO: Add a few tests for adding and removing terminal tabs

crates/terminal/src/terminal.rs 🔗

@@ -32,6 +32,7 @@ use mappings::mouse::{
 
 use procinfo::LocalProcessInfo;
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
+use util::truncate_and_trailoff;
 
 use std::{
     cmp::min,
@@ -1169,6 +1170,38 @@ impl Terminal {
             all_search_matches(&term, &searcher).collect()
         })
     }
+
+    pub fn title(&self) -> String {
+        self.foreground_process_info
+            .as_ref()
+            .map(|fpi| {
+                format!(
+                    "{} — {}",
+                    truncate_and_trailoff(
+                        &fpi.cwd
+                            .file_name()
+                            .map(|name| name.to_string_lossy().to_string())
+                            .unwrap_or_default(),
+                        25
+                    ),
+                    truncate_and_trailoff(
+                        &{
+                            format!(
+                                "{}{}",
+                                fpi.name,
+                                if fpi.argv.len() >= 1 {
+                                    format!(" {}", (&fpi.argv[1..]).join(" "))
+                                } else {
+                                    "".to_string()
+                                }
+                            )
+                        },
+                        25
+                    )
+                )
+            })
+            .unwrap_or_else(|| "Terminal".to_string())
+    }
 }
 
 impl Drop for Terminal {

crates/terminal_view/src/terminal_button.rs 🔗

@@ -0,0 +1,176 @@
+use context_menu::{ContextMenu, ContextMenuItem};
+use gpui::{
+    elements::*, geometry::vector::Vector2F, impl_internal_actions, CursorStyle, Element,
+    ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
+};
+use settings::Settings;
+use std::any::TypeId;
+use terminal::Terminal;
+use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
+
+use crate::TerminalView;
+
+#[derive(Clone, PartialEq)]
+pub struct FocusTerminal {
+    terminal_handle: WeakModelHandle<Terminal>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct DeployTerminalMenu {
+    position: Vector2F,
+}
+
+impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(TerminalButton::deploy_terminal_menu);
+    cx.add_action(TerminalButton::focus_terminal);
+}
+
+pub struct TerminalButton {
+    workspace: WeakViewHandle<Workspace>,
+    popup_menu: ViewHandle<ContextMenu>,
+}
+
+impl Entity for TerminalButton {
+    type Event = ();
+}
+
+impl View for TerminalButton {
+    fn ui_name() -> &'static str {
+        "TerminalButton"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
+        let workspace = self.workspace.upgrade(cx);
+        let project = match workspace {
+            Some(workspace) => workspace.read(cx).project().read(cx),
+            None => return Empty::new().boxed(),
+        };
+
+        let focused_view = cx.focused_view_id(cx.window_id());
+        let active = focused_view
+            .map(|view_id| {
+                cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
+            })
+            .unwrap_or(false);
+
+        let has_terminals = !project.local_terminal_handles().is_empty();
+        let theme = cx.global::<Settings>().theme.clone();
+
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<Self>::new(0, cx, {
+                    let theme = theme.clone();
+                    move |state, _| {
+                        let style = theme
+                            .workspace
+                            .status_bar
+                            .sidebar_buttons
+                            .item
+                            .style_for(state, active);
+
+                        Svg::new("icons/terminal_12.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, move |e, cx| {
+                    if has_terminals {
+                        cx.dispatch_action(DeployTerminalMenu {
+                            position: e.region.upper_right(),
+                        });
+                    } else {
+                        if !active {
+                            cx.dispatch_action(FocusDock);
+                        }
+                    };
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Show Terminal".into(),
+                    Some(Box::new(FocusDock)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .boxed(),
+            )
+            .with_child(ChildView::new(&self.popup_menu, cx).boxed())
+            .boxed()
+    }
+}
+
+impl TerminalButton {
+    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        // When terminal moves, redraw so that the icon and toggle status matches.
+        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
+        Self {
+            workspace: workspace.downgrade(),
+            popup_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Window);
+                menu
+            }),
+        }
+    }
+
+    pub fn deploy_terminal_menu(
+        &mut self,
+        action: &DeployTerminalMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
+
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let project = workspace.read(cx).project().read(cx);
+            let local_terminal_handles = project.local_terminal_handles();
+
+            if !local_terminal_handles.is_empty() {
+                menu_options.push(ContextMenuItem::Separator)
+            }
+
+            for local_terminal_handle in local_terminal_handles {
+                if let Some(terminal) = local_terminal_handle.upgrade(cx) {
+                    menu_options.push(ContextMenuItem::item(
+                        terminal.read(cx).title(),
+                        FocusTerminal {
+                            terminal_handle: local_terminal_handle.clone(),
+                        },
+                    ))
+                }
+            }
+        }
+
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(action.position, AnchorCorner::BottomRight, menu_options, cx);
+        });
+    }
+
+    pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            workspace.update(cx, |workspace, cx| {
+                let terminal = workspace
+                    .items_of_type::<TerminalView>(cx)
+                    .find(|terminal| {
+                        terminal.read(cx).model().downgrade() == action.terminal_handle
+                    });
+                if let Some(terminal) = terminal {
+                    workspace.activate_item(&terminal, cx);
+                }
+            });
+        }
+    }
+}
+
+impl StatusItemView for TerminalButton {
+    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+}

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1,4 +1,5 @@
 mod persistence;
+pub mod terminal_button;
 pub mod terminal_element;
 
 use std::{
@@ -30,7 +31,7 @@ use terminal::{
     },
     Event, Terminal,
 };
-use util::{truncate_and_trailoff, ResultExt};
+use util::ResultExt;
 use workspace::{
     item::{Item, ItemEvent},
     notifications::NotifyResultExt,
@@ -177,8 +178,8 @@ impl TerminalView {
         }
     }
 
-    pub fn handle(&self) -> ModelHandle<Terminal> {
-        self.terminal.clone()
+    pub fn model(&self) -> &ModelHandle<Terminal> {
+        &self.terminal
     }
 
     pub fn has_new_content(&self) -> bool {
@@ -547,38 +548,7 @@ impl Item for TerminalView {
         tab_theme: &theme::Tab,
         cx: &gpui::AppContext,
     ) -> ElementBox {
-        let title = self
-            .terminal()
-            .read(cx)
-            .foreground_process_info
-            .as_ref()
-            .map(|fpi| {
-                format!(
-                    "{} — {}",
-                    truncate_and_trailoff(
-                        &fpi.cwd
-                            .file_name()
-                            .map(|name| name.to_string_lossy().to_string())
-                            .unwrap_or_default(),
-                        25
-                    ),
-                    truncate_and_trailoff(
-                        &{
-                            format!(
-                                "{}{}",
-                                fpi.name,
-                                if fpi.argv.len() >= 1 {
-                                    format!(" {}", (&fpi.argv[1..]).join(" "))
-                                } else {
-                                    "".to_string()
-                                }
-                            )
-                        },
-                        25
-                    )
-                )
-            })
-            .unwrap_or_else(|| "Terminal".to_string());
+        let title = self.terminal().read(cx).title();
 
         Flex::row()
             .with_child(

crates/workspace/Cargo.toml 🔗

@@ -32,6 +32,7 @@ language = { path = "../language" }
 menu = { path = "../menu" }
 project = { path = "../project" }
 settings = { path = "../settings" }
+terminal = { path = "../terminal" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 async-recursion = "1.0.0"

crates/workspace/src/pane.rs 🔗

@@ -168,8 +168,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
     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(Pane::deploy_new_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });

crates/workspace/src/terminal_button.rs 🔗

@@ -1,92 +0,0 @@
-use gpui::{
-    elements::{Empty, MouseEventHandler, Svg},
-    CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
-    ViewHandle, WeakViewHandle,
-};
-use settings::Settings;
-
-use crate::{dock::FocusDock, item::ItemHandle, StatusItemView, Workspace};
-
-pub struct TerminalButton {
-    workspace: WeakViewHandle<Workspace>,
-}
-
-impl TerminalButton {
-    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
-        // When terminal moves, redraw so that the icon and toggle status matches.
-        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
-
-        Self {
-            workspace: workspace.downgrade(),
-        }
-    }
-}
-
-impl Entity for TerminalButton {
-    type Event = ();
-}
-
-impl View for TerminalButton {
-    fn ui_name() -> &'static str {
-        "TerminalButton"
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
-        let workspace = self.workspace.upgrade(cx);
-
-        if workspace.is_none() {
-            return Empty::new().boxed();
-        }
-
-        let focused_view = cx.focused_view_id(cx.window_id());
-
-        // FIXME: Don't hardcode "Terminal" in here
-        let active = focused_view
-            .map(|view| cx.view_ui_name(cx.window_id(), view) == Some("Terminal"))
-            .unwrap_or(false);
-
-        // let workspace = workspace.unwrap();
-        let theme = cx.global::<Settings>().theme.clone();
-
-        MouseEventHandler::<Self>::new(0, cx, {
-            let theme = theme.clone();
-            move |state, _| {
-                let style = theme
-                    .workspace
-                    .status_bar
-                    .sidebar_buttons
-                    .item
-                    .style_for(state, active);
-
-                Svg::new("icons/terminal_12.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, move |_, cx| {
-            if !active {
-                cx.dispatch_action(FocusDock);
-            }
-        })
-        .with_tooltip::<Self, _>(
-            0,
-            "Show Terminal".into(),
-            Some(Box::new(FocusDock)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .boxed()
-    }
-}
-
-impl StatusItemView for TerminalButton {
-    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        cx.notify();
-    }
-}

crates/workspace/src/workspace.rs 🔗

@@ -12,7 +12,6 @@ pub mod searchable;
 pub mod shared_screen;
 pub mod sidebar;
 mod status_bar;
-pub mod terminal_button;
 mod toolbar;
 
 pub use smallvec;
@@ -58,7 +57,6 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use terminal_button::TerminalButton;
 
 use crate::{
     notifications::simple_message_notification::{MessageNotification, OsOpen},
@@ -666,7 +664,6 @@ impl Workspace {
         let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
         let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
         let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
-        let toggle_terminal = cx.add_view(|cx| TerminalButton::new(handle.clone(), cx));
         let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
         let right_sidebar_buttons =
             cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
@@ -675,7 +672,6 @@ impl Workspace {
             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);
-            status_bar.add_right_item(toggle_terminal, cx);
             status_bar
         });
 

crates/zed/src/zed.rs 🔗

@@ -31,6 +31,7 @@ use serde::Deserialize;
 use serde_json::to_string_pretty;
 use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
 use std::{borrow::Cow, env, path::Path, str, sync::Arc};
+use terminal_view::terminal_button::{self, TerminalButton};
 use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode};
 use uuid::Uuid;
 pub use workspace;
@@ -72,6 +73,7 @@ actions!(
 const MIN_FONT_SIZE: f32 = 6.0;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
+    terminal_button::init(cx);
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
         cx.platform().hide();
@@ -336,6 +338,7 @@ pub fn initialize_workspace(
         )
     });
 
+    let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
     let activity_indicator =
@@ -347,6 +350,9 @@ 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);
+        if **cx.default_global::<StaffMode>() {
+            status_bar.add_right_item(toggle_terminal, cx);
+        }
         status_bar.add_right_item(feedback_button, cx);
         status_bar.add_right_item(active_buffer_language, cx);
         status_bar.add_right_item(cursor_position, cx);