Draft quick actions bar

Kirill Bulatov created

Change summary

crates/editor/src/editor.rs           |  33 ++++++
crates/editor/src/inlay_hint_cache.rs |   2 
crates/search/src/buffer_search.rs    |   2 
crates/theme/src/theme.rs             |   1 
crates/zed/src/quick_action_bar.rs    | 143 ++++++++++++++++++++++++++++
crates/zed/src/zed.rs                 |   7 +
styles/src/style_tree/workspace.ts    |   4 
7 files changed, 189 insertions(+), 3 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -302,10 +302,11 @@ actions!(
         Hover,
         Format,
         ToggleSoftWrap,
+        ToggleInlays,
         RevealInFinder,
         CopyPath,
         CopyRelativePath,
-        CopyHighlightJson
+        CopyHighlightJson,
     ]
 );
 
@@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::toggle_soft_wrap);
+    cx.add_action(Editor::toggle_inlays);
     cx.add_action(Editor::reveal_in_finder);
     cx.add_action(Editor::copy_path);
     cx.add_action(Editor::copy_relative_path);
@@ -1238,6 +1240,7 @@ enum GotoDefinitionKind {
 
 #[derive(Debug, Clone)]
 enum InlayRefreshReason {
+    Toggled(bool),
     SettingsChange(InlayHintSettings),
     NewLinesShown,
     BufferEdited(HashSet<Arc<Language>>),
@@ -2669,12 +2672,40 @@ impl Editor {
         }
     }
 
+    pub fn toggle_inlays(&mut self, _: &ToggleInlays, cx: &mut ViewContext<Self>) {
+        self.inlay_hint_cache.enabled = !self.inlay_hint_cache.enabled;
+        self.refresh_inlays(
+            InlayRefreshReason::Toggled(self.inlay_hint_cache.enabled),
+            cx,
+        )
+    }
+
+    pub fn inlays_enabled(&self) -> bool {
+        self.inlay_hint_cache.enabled
+    }
+
     fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
         if self.project.is_none() || self.mode != EditorMode::Full {
             return;
         }
 
         let (invalidate_cache, required_languages) = match reason {
+            InlayRefreshReason::Toggled(enabled) => {
+                if enabled {
+                    (InvalidationStrategy::RefreshRequested, None)
+                } else {
+                    self.inlay_hint_cache.clear();
+                    self.splice_inlay_hints(
+                        self.visible_inlay_hints(cx)
+                            .iter()
+                            .map(|inlay| inlay.id)
+                            .collect(),
+                        Vec::new(),
+                        cx,
+                    );
+                    return;
+                }
+            }
             InlayRefreshReason::SettingsChange(new_settings) => {
                 match self.inlay_hint_cache.update_settings(
                     &self.buffer,

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -380,7 +380,7 @@ impl InlayHintCache {
         }
     }
 
-    fn clear(&mut self) {
+    pub fn clear(&mut self) {
         self.version += 1;
         self.update_tasks.clear();
         self.hints.clear();

crates/search/src/buffer_search.rs 🔗

@@ -553,7 +553,7 @@ impl BufferSearchBar {
         .into_any()
     }
 
-    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+    pub fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
         let mut propagate_action = true;
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
             search_bar.update(cx, |search_bar, cx| {

crates/theme/src/theme.rs 🔗

@@ -399,6 +399,7 @@ pub struct Toolbar {
     pub height: f32,
     pub item_spacing: f32,
     pub nav_button: Interactive<IconButton>,
+    pub toggleable_tool: Toggleable<Interactive<IconButton>>,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

crates/zed/src/quick_action_bar.rs 🔗

@@ -0,0 +1,143 @@
+use editor::Editor;
+use gpui::{
+    elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
+    platform::{CursorStyle, MouseButton},
+    Action, AnyElement, Element, Entity, EventContext, View, ViewContext, ViewHandle,
+};
+
+use search::{buffer_search, BufferSearchBar};
+use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
+
+pub struct QuickActionBar {
+    workspace: ViewHandle<Workspace>,
+    active_item: Option<Box<dyn ItemHandle>>,
+}
+
+impl QuickActionBar {
+    pub fn new(workspace: ViewHandle<Workspace>) -> Self {
+        Self {
+            workspace,
+            active_item: None,
+        }
+    }
+
+    fn active_editor(&self) -> Option<ViewHandle<Editor>> {
+        self.active_item
+            .as_ref()
+            .and_then(|item| item.downcast::<Editor>())
+    }
+}
+
+impl Entity for QuickActionBar {
+    type Event = ();
+}
+
+impl View for QuickActionBar {
+    fn ui_name() -> &'static str {
+        "QuickActionsBar"
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+        let Some(editor) = self.active_editor() else { return Empty::new().into_any(); };
+
+        let inlays_enabled = editor.read(cx).inlays_enabled();
+        let mut bar = Flex::row().with_child(render_quick_action_bar_button(
+            0,
+            "icons/hamburger_15.svg",
+            inlays_enabled,
+            (
+                "Toggle inlays".to_string(),
+                Some(Box::new(editor::ToggleInlays)),
+            ),
+            cx,
+            |this, cx| {
+                if let Some(editor) = this.active_editor() {
+                    editor.update(cx, |editor, cx| {
+                        editor.toggle_inlays(&editor::ToggleInlays, cx);
+                    });
+                }
+            },
+        ));
+
+        if editor.read(cx).buffer().read(cx).is_singleton() {
+            let search_action = buffer_search::Deploy { focus: true };
+
+            // TODO kb: this opens the search bar in a differently focused pane (should be the same) + should be toggleable
+            let pane = self.workspace.read(cx).active_pane().clone();
+            bar = bar.with_child(render_quick_action_bar_button(
+                1,
+                "icons/magnifying_glass_12.svg",
+                false,
+                (
+                    "Search in buffer".to_string(),
+                    Some(Box::new(search_action.clone())),
+                ),
+                cx,
+                move |_, cx| {
+                    pane.update(cx, |pane, cx| {
+                        BufferSearchBar::deploy(pane, &search_action, cx);
+                    });
+                },
+            ));
+        }
+
+        bar.into_any()
+    }
+}
+
+fn render_quick_action_bar_button<
+    F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
+>(
+    index: usize,
+    icon: &'static str,
+    toggled: bool,
+    tooltip: (String, Option<Box<dyn Action>>),
+    cx: &mut ViewContext<QuickActionBar>,
+    on_click: F,
+) -> AnyElement<QuickActionBar> {
+    enum QuickActionBarButton {}
+
+    let theme = theme::current(cx);
+    let (tooltip_text, action) = tooltip;
+
+    MouseEventHandler::<QuickActionBarButton, _>::new(index, cx, |mouse_state, _| {
+        let style = theme
+            .workspace
+            .toolbar
+            .toggleable_tool
+            .in_state(toggled)
+            .style_for(mouse_state);
+        Svg::new(icon)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.icon_width)
+            .aligned()
+            .constrained()
+            .with_width(style.button_width)
+            .with_height(style.button_width)
+    })
+    .with_cursor_style(CursorStyle::PointingHand)
+    .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+    .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+    .into_any_named("quick action bar button")
+}
+
+impl ToolbarItemView for QuickActionBar {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        _: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        match active_pane_item {
+            Some(active_item) => {
+                dbg!("@@@@@@@@@@ TODO kb", active_item.id());
+                self.active_item = Some(active_item.boxed_clone());
+                ToolbarItemLocation::PrimaryRight { flex: None }
+            }
+            None => {
+                self.active_item = None;
+                ToolbarItemLocation::Hidden
+            }
+        }
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -5,6 +5,8 @@ pub mod only_instance;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
+mod quick_action_bar;
+
 use ai::AssistantPanel;
 use anyhow::Context;
 use assets::Assets;
@@ -30,6 +32,7 @@ use gpui::{
 pub use lsp;
 pub use project;
 use project_panel::ProjectPanel;
+use quick_action_bar::QuickActionBar;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
@@ -255,12 +258,16 @@ pub fn initialize_workspace(
         workspace_handle.update(&mut cx, |workspace, cx| {
             let workspace_handle = cx.handle();
             cx.subscribe(&workspace_handle, {
+                let workspace_handle = workspace_handle.clone();
                 move |workspace, _, event, cx| {
                     if let workspace::Event::PaneAdded(pane) = event {
                         pane.update(cx, |pane, cx| {
                             pane.toolbar().update(cx, |toolbar, cx| {
                                 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
                                 toolbar.add_item(breadcrumbs, cx);
+                                let quick_action_bar =
+                                    cx.add_view(|_| QuickActionBar::new(workspace_handle.clone()));
+                                toolbar.add_item(quick_action_bar, cx);
                                 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
                                 toolbar.add_item(buffer_search_bar, cx);
                                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());

styles/src/style_tree/workspace.ts 🔗

@@ -12,6 +12,7 @@ import tabBar from "./tab_bar"
 import { interactive } from "../element"
 import { titlebar } from "./titlebar"
 import { useTheme } from "../theme"
+import { toggleable_icon_button } from "../component/icon_button"
 
 export default function workspace(): any {
     const theme = useTheme()
@@ -149,6 +150,9 @@ export default function workspace(): any {
                     },
                 },
             }),
+            toggleable_tool: toggleable_icon_button(theme, {
+                active_color: "accent",
+            }),
             padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
         breadcrumb_height: 24,