Add `editor::ToggleInlayHints` command and a bar to toggle editor-related items (#2839)

Kirill Bulatov created

Closes
https://linear.app/zed-industries/issue/Z-2735/make-inlay-hints-discoverable
Deals with https://github.com/zed-industries/community/issues/1764

Adds `editor::ToggleInlayHints` command and a new panel with two
buttons for 
* toggling hints on/off for every editor separately (overrides settings)

Would benefit from a new icon.

* toggling buffer search on/off 

Does not have a keybinding shown in its tooltip, that is a separate
issue with the way `KeystrokeLabel` gets these for panels not in the
view directly.

Release Notes:

- Adds `editor::ToggleInlayHints` command and a bar to toggle
editor-related items

Change summary

Cargo.lock                                      |  12 +
assets/icons/inlay_hint.svg                     |   5 
crates/collab/src/tests/integration_tests.rs    |   4 
crates/editor/src/editor.rs                     |  60 +++++-
crates/editor/src/inlay_hint_cache.rs           | 143 +++++++++++++++
crates/editor/src/scroll.rs                     |   6 
crates/project/src/project.rs                   |   8 
crates/quick_action_bar/Cargo.toml              |  22 ++
crates/quick_action_bar/src/quick_action_bar.rs | 163 +++++++++++++++++++
crates/search/src/buffer_search.rs              |  25 +-
crates/theme/src/theme.rs                       |   1 
crates/zed/Cargo.toml                           |   1 
crates/zed/src/zed.rs                           |   6 
styles/src/style_tree/workspace.ts              |   6 
14 files changed, 423 insertions(+), 39 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5714,6 +5714,17 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "quick_action_bar"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "gpui",
+ "search",
+ "theme",
+ "workspace",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.32"
@@ -9922,6 +9933,7 @@ dependencies = [
  "project",
  "project_panel",
  "project_symbols",
+ "quick_action_bar",
  "rand 0.8.5",
  "recent_projects",
  "regex",

assets/icons/inlay_hint.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="3" cy="9" r="1" fill="black"/>
+<circle cx="3" cy="5" r="1" fill="black"/>
+<path d="M7 3H10M13 3H10M10 3C10 3 10 11 10 11.5" stroke="black" stroke-width="1.25"/>
+</svg>

crates/collab/src/tests/integration_tests.rs 🔗

@@ -7867,7 +7867,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .insert_tree(
             "/a",
             json!({
-                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
                 "other.rs": "// Test file",
             }),
         )
@@ -8177,7 +8177,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .insert_tree(
             "/a",
             json!({
-                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
                 "other.rs": "// Test file",
             }),
         )

crates/editor/src/editor.rs 🔗

@@ -302,10 +302,11 @@ actions!(
         Hover,
         Format,
         ToggleSoftWrap,
+        ToggleInlayHints,
         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_inlay_hints);
     cx.add_action(Editor::reveal_in_finder);
     cx.add_action(Editor::copy_path);
     cx.add_action(Editor::copy_relative_path);
@@ -1237,7 +1239,8 @@ enum GotoDefinitionKind {
 }
 
 #[derive(Debug, Clone)]
-enum InlayRefreshReason {
+enum InlayHintRefreshReason {
+    Toggle(bool),
     SettingsChange(InlayHintSettings),
     NewLinesShown,
     BufferEdited(HashSet<Arc<Language>>),
@@ -1354,8 +1357,8 @@ impl Editor {
                     }));
                 }
                 project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
-                    if let project::Event::RefreshInlays = event {
-                        editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+                    if let project::Event::RefreshInlayHints = event {
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
                     };
                 }));
             }
@@ -2669,13 +2672,41 @@ impl Editor {
         }
     }
 
-    fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+    pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
+            cx,
+        );
+    }
+
+    pub fn inlay_hints_enabled(&self) -> bool {
+        self.inlay_hint_cache.enabled
+    }
+
+    fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
         if self.project.is_none() || self.mode != EditorMode::Full {
             return;
         }
 
         let (invalidate_cache, required_languages) = match reason {
-            InlayRefreshReason::SettingsChange(new_settings) => {
+            InlayHintRefreshReason::Toggle(enabled) => {
+                self.inlay_hint_cache.enabled = 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;
+                }
+            }
+            InlayHintRefreshReason::SettingsChange(new_settings) => {
                 match self.inlay_hint_cache.update_settings(
                     &self.buffer,
                     new_settings,
@@ -2693,11 +2724,13 @@ impl Editor {
                     ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
                 }
             }
-            InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
-            InlayRefreshReason::BufferEdited(buffer_languages) => {
+            InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+            InlayHintRefreshReason::BufferEdited(buffer_languages) => {
                 (InvalidationStrategy::BufferEdited, Some(buffer_languages))
             }
-            InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
+            InlayHintRefreshReason::RefreshRequested => {
+                (InvalidationStrategy::RefreshRequested, None)
+            }
         };
 
         if let Some(InlaySplice {
@@ -2774,6 +2807,7 @@ impl Editor {
         self.display_map.update(cx, |display_map, cx| {
             display_map.splice_inlays(to_remove, to_insert, cx);
         });
+        cx.notify();
     }
 
     fn trigger_on_type_formatting(
@@ -7696,8 +7730,8 @@ impl Editor {
                         .cloned()
                         .collect::<HashSet<_>>();
                     if !languages_affected.is_empty() {
-                        self.refresh_inlays(
-                            InlayRefreshReason::BufferEdited(languages_affected),
+                        self.refresh_inlay_hints(
+                            InlayHintRefreshReason::BufferEdited(languages_affected),
                             cx,
                         );
                     }
@@ -7735,8 +7769,8 @@ impl Editor {
 
     fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
         self.refresh_copilot_suggestions(true, cx);
-        self.refresh_inlays(
-            InlayRefreshReason::SettingsChange(inlay_hint_settings(
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
                 self.selections.newest_anchor().head(),
                 &self.buffer.read(cx).snapshot(cx),
                 cx,

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -24,7 +24,7 @@ pub struct InlayHintCache {
     hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
     allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
     version: usize,
-    enabled: bool,
+    pub(super) enabled: bool,
     update_tasks: HashMap<ExcerptId, TasksForRanges>,
 }
 
@@ -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();
@@ -2001,7 +2001,7 @@ mod tests {
         });
     }
 
-    #[gpui::test]
+    #[gpui::test(iterations = 10)]
     async fn test_multiple_excerpts_large_multibuffer(
         deterministic: Arc<Deterministic>,
         cx: &mut gpui::TestAppContext,
@@ -2335,10 +2335,12 @@ mod tests {
 all hints should be invalidated and requeried for all of its visible excerpts"
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                last_scroll_update_version + expected_layers.len(),
-                "Due to every excerpt having one hint, cache should update per new excerpt received"
+
+            let current_cache_version = editor.inlay_hint_cache().version;
+            let minimum_expected_version = last_scroll_update_version + expected_layers.len();
+            assert!(
+                current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
+                "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
             );
         });
     }
@@ -2683,6 +2685,127 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         });
     }
 
+    #[gpui::test]
+    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: false,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().start_waiting();
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should display inlays after toggle despite them disabled in settings"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                1,
+                "First toggle should be cache's first update"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear hints after 2nd toggle"
+            );
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 2);
+        });
+
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should query LSP hints for the 2nd time after enabling hints in settings"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 3);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear hints after enabling in settings and a 3rd toggle"
+            );
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 4);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 5);
+        });
+    }
+
     pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
         cx.foreground().forbid_parking();
 
@@ -2759,6 +2882,12 @@ all hints should be invalidated and requeried for all of its visible excerpts"
             .downcast::<Editor>()
             .unwrap();
 
+        editor.update(cx, |editor, cx| {
+            assert!(cached_hint_labels(editor).is_empty());
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 0);
+        });
+
         ("/a/main.rs", editor, fake_server)
     }
 

crates/editor/src/scroll.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
     ToPoint,
 };
 
@@ -301,7 +301,7 @@ impl Editor {
             cx.spawn(|editor, mut cx| async move {
                 editor
                     .update(&mut cx, |editor, cx| {
-                        editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
                     })
                     .ok()
             })
@@ -333,7 +333,7 @@ impl Editor {
             cx,
         );
 
-        self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

crates/project/src/project.rs 🔗

@@ -282,7 +282,7 @@ pub enum Event {
         new_peer_id: proto::PeerId,
     },
     CollaboratorLeft(proto::PeerId),
-    RefreshInlays,
+    RefreshInlayHints,
 }
 
 pub enum LanguageServerState {
@@ -2872,7 +2872,7 @@ impl Project {
                         .upgrade(&cx)
                         .ok_or_else(|| anyhow!("project dropped"))?;
                     this.update(&mut cx, |project, cx| {
-                        cx.emit(Event::RefreshInlays);
+                        cx.emit(Event::RefreshInlayHints);
                         project.remote_id().map(|project_id| {
                             project.client.send(proto::RefreshInlayHints { project_id })
                         })
@@ -3436,7 +3436,7 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) {
         if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
-            cx.emit(Event::RefreshInlays);
+            cx.emit(Event::RefreshInlayHints);
             status.pending_work.remove(&token);
             cx.notify();
         }
@@ -6810,7 +6810,7 @@ impl Project {
         mut cx: AsyncAppContext,
     ) -> Result<proto::Ack> {
         this.update(&mut cx, |_, cx| {
-            cx.emit(Event::RefreshInlays);
+            cx.emit(Event::RefreshInlayHints);
         });
         Ok(proto::Ack {})
     }

crates/quick_action_bar/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "quick_action_bar"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/quick_action_bar.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+search = { path = "../search" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -0,0 +1,163 @@
+use editor::Editor;
+use gpui::{
+    elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
+    platform::{CursorStyle, MouseButton},
+    Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
+};
+
+use search::{buffer_search, BufferSearchBar};
+use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
+
+pub struct QuickActionBar {
+    buffer_search_bar: ViewHandle<BufferSearchBar>,
+    active_item: Option<Box<dyn ItemHandle>>,
+    _inlay_hints_enabled_subscription: Option<Subscription>,
+}
+
+impl QuickActionBar {
+    pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
+        Self {
+            buffer_search_bar,
+            active_item: None,
+            _inlay_hints_enabled_subscription: 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 inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+        let mut bar = Flex::row().with_child(render_quick_action_bar_button(
+            0,
+            "icons/inlay_hint.svg",
+            inlay_hints_enabled,
+            (
+                "Toggle Inlay Hints".to_string(),
+                Some(Box::new(editor::ToggleInlayHints)),
+            ),
+            cx,
+            |this, cx| {
+                if let Some(editor) = this.active_editor() {
+                    editor.update(cx, |editor, cx| {
+                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+                    });
+                }
+            },
+        ));
+
+        if editor.read(cx).buffer().read(cx).is_singleton() {
+            let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
+            let search_action = buffer_search::Deploy { focus: true };
+
+            bar = bar.with_child(render_quick_action_bar_button(
+                1,
+                "icons/magnifying_glass.svg",
+                search_bar_shown,
+                (
+                    "Buffer Search".to_string(),
+                    Some(Box::new(search_action.clone())),
+                ),
+                cx,
+                move |this, cx| {
+                    this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
+                        if search_bar_shown {
+                            buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
+                        } else {
+                            buffer_search_bar.deploy(&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::new::<QuickActionBarButton, _>(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)
+            .contained()
+            .with_style(style.container)
+    })
+    .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>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        match active_pane_item {
+            Some(active_item) => {
+                self.active_item = Some(active_item.boxed_clone());
+                self._inlay_hints_enabled_subscription.take();
+
+                if let Some(editor) = active_item.downcast::<Editor>() {
+                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    self._inlay_hints_enabled_subscription =
+                        Some(cx.observe(&editor, move |_, editor, cx| {
+                            let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                            if inlay_hints_enabled != new_inlay_hints_enabled {
+                                inlay_hints_enabled = new_inlay_hints_enabled;
+                                cx.notify();
+                            }
+                        }));
+                }
+
+                ToolbarItemLocation::PrimaryRight { flex: None }
+            }
+            None => {
+                self.active_item = None;
+                ToolbarItemLocation::Hidden
+            }
+        }
+    }
+}

crates/search/src/buffer_search.rs 🔗

@@ -36,7 +36,7 @@ pub enum Event {
 }
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(BufferSearchBar::deploy);
+    cx.add_action(BufferSearchBar::deploy_bar);
     cx.add_action(BufferSearchBar::dismiss);
     cx.add_action(BufferSearchBar::focus_editor);
     cx.add_action(BufferSearchBar::select_next_match);
@@ -327,6 +327,19 @@ impl BufferSearchBar {
         cx.notify();
     }
 
+    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
+        if self.show(cx) {
+            self.search_suggested(cx);
+            if deploy.focus {
+                self.select_query(cx);
+                cx.focus_self();
+            }
+            return true;
+        }
+
+        false
+    }
+
     pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if self.active_searchable_item.is_none() {
             return false;
@@ -553,21 +566,15 @@ impl BufferSearchBar {
         .into_any()
     }
 
-    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+    fn deploy_bar(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| {
-                if search_bar.show(cx) {
-                    search_bar.search_suggested(cx);
-                    if action.focus {
-                        search_bar.select_query(cx);
-                        cx.focus_self();
-                    }
+                if search_bar.deploy(action, cx) {
                     propagate_action = false;
                 }
             });
         }
-
         if propagate_action {
             cx.propagate_action();
         }

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/Cargo.toml 🔗

@@ -54,6 +54,7 @@ plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { path = "../project" }
 project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
+quick_action_bar = { path = "../quick_action_bar" }
 recent_projects = { path = "../recent_projects" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }

crates/zed/src/zed.rs 🔗

@@ -30,6 +30,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;
@@ -262,7 +263,10 @@ pub fn initialize_workspace(
                                 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
                                 toolbar.add_item(breadcrumbs, cx);
                                 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
-                                toolbar.add_item(buffer_search_bar, cx);
+                                toolbar.add_item(buffer_search_bar.clone(), cx);
+                                let quick_action_bar =
+                                    cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
+                                toolbar.add_item(quick_action_bar, cx);
                                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                                 toolbar.add_item(project_search_bar, cx);
                                 let submit_feedback_button =

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,11 @@ export default function workspace(): any {
                     },
                 },
             }),
+            toggleable_tool: toggleable_icon_button(theme, {
+                margin: { left: 8 },
+                variant: "ghost",
+                active_color: "accent",
+            }),
             padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
         breadcrumb_height: 24,