Render the search UI on a separate row from the breadcrumbs

Nathan Sobo , Antonio Scandurra , and Max Brunsfeld created

- In project search, render it above the breadcrumbs
- In buffer search, render it below

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

Cargo.lock                            |   1 
crates/breadcrumbs/Cargo.toml         |   1 
crates/breadcrumbs/src/breadcrumbs.rs |  33 ++++--
crates/search/src/buffer_search.rs    | 138 +++++++++++++++-------------
crates/search/src/project_search.rs   |  15 ++-
crates/search/src/search.rs           |   2 
crates/workspace/src/toolbar.rs       | 135 +++++++++++++++++++---------
crates/workspace/src/workspace.rs     |   2 
crates/zed/src/zed.rs                 |   7 
9 files changed, 208 insertions(+), 126 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -727,6 +727,7 @@ dependencies = [
  "editor",
  "gpui",
  "language",
+ "search",
  "theme",
  "workspace",
 ]

crates/breadcrumbs/Cargo.toml 🔗

@@ -12,6 +12,7 @@ collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+search = { path = "../search" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -3,9 +3,10 @@ use gpui::{
     elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
 };
 use language::{BufferSnapshot, OutlineItem};
+use search::ProjectSearchView;
 use std::borrow::Cow;
 use theme::SyntaxTheme;
-use workspace::{ItemHandle, Settings, ToolbarItemView};
+use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
 
 pub struct Breadcrumbs {
     editor: Option<ViewHandle<Editor>>,
@@ -83,17 +84,29 @@ impl ToolbarItemView for Breadcrumbs {
         &mut self,
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
-    ) {
+    ) -> ToolbarItemLocation {
+        cx.notify();
         self.editor_subscription = None;
         self.editor = None;
-        if let Some(editor) = active_pane_item.and_then(|i| i.act_as::<Editor>(cx)) {
-            self.editor_subscription = Some(cx.subscribe(&editor, |_, _, event, cx| match event {
-                editor::Event::BufferEdited => cx.notify(),
-                editor::Event::SelectionsChanged { local } if *local => cx.notify(),
-                _ => {}
-            }));
-            self.editor = Some(editor);
+        if let Some(item) = active_pane_item {
+            if let Some(editor) = item.act_as::<Editor>(cx) {
+                self.editor_subscription =
+                    Some(cx.subscribe(&editor, |_, _, event, cx| match event {
+                        editor::Event::BufferEdited => cx.notify(),
+                        editor::Event::SelectionsChanged { local } if *local => cx.notify(),
+                        _ => {}
+                    }));
+                self.editor = Some(editor);
+                if item.downcast::<ProjectSearchView>().is_some() {
+                    ToolbarItemLocation::Secondary
+                } else {
+                    ToolbarItemLocation::PrimaryLeft
+                }
+            } else {
+                ToolbarItemLocation::Hidden
+            }
+        } else {
+            ToolbarItemLocation::Hidden
         }
-        cx.notify();
     }
 }

crates/search/src/buffer_search.rs 🔗

@@ -8,13 +8,17 @@ use gpui::{
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, ToolbarItemView};
+use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
 
 action!(Deploy, bool);
 action!(Dismiss);
 action!(FocusEditor);
 action!(ToggleSearchOption, SearchOption);
 
+pub enum Event {
+    UpdateLocation,
+}
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([
         Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
@@ -57,7 +61,7 @@ pub struct BufferSearchBar {
 }
 
 impl Entity for BufferSearchBar {
-    type Event = ();
+    type Event = Event;
 }
 
 impl View for BufferSearchBar {
@@ -70,70 +74,66 @@ impl View for BufferSearchBar {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        if self.dismissed || self.active_editor.is_none() {
-            Empty::new().boxed()
+        let theme = cx.global::<Settings>().theme.clone();
+        let editor_container = if self.query_contains_error {
+            theme.search.invalid_editor
         } else {
-            let theme = cx.global::<Settings>().theme.clone();
-            let editor_container = if self.query_contains_error {
-                theme.search.invalid_editor
-            } else {
-                theme.search.editor.input.container
-            };
-            Flex::row()
-                .with_child(
-                    Flex::row()
-                        .with_child(ChildView::new(&self.query_editor).flex(1., true).boxed())
-                        .with_children(self.active_editor.as_ref().and_then(|editor| {
-                            let matches = self.editors_with_matches.get(&editor.downgrade())?;
-                            let message = if let Some(match_ix) = self.active_match_index {
-                                format!("{}/{}", match_ix + 1, matches.len())
-                            } else {
-                                "No matches".to_string()
-                            };
-
-                            Some(
-                                Label::new(message, theme.search.match_index.text.clone())
-                                    .contained()
-                                    .with_style(theme.search.match_index.container)
-                                    .aligned()
-                                    .boxed(),
-                            )
-                        }))
-                        .contained()
-                        .with_style(editor_container)
-                        .aligned()
-                        .constrained()
-                        .with_max_width(theme.search.editor.max_width)
-                        .boxed(),
-                )
-                .with_child(
-                    Flex::row()
-                        .with_child(self.render_nav_button("<", Direction::Prev, cx))
-                        .with_child(self.render_nav_button(">", Direction::Next, cx))
-                        .aligned()
-                        .boxed(),
-                )
-                .with_child(
-                    Flex::row()
-                        .with_child(self.render_search_option(
-                            "Case",
-                            SearchOption::CaseSensitive,
-                            cx,
-                        ))
-                        .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
-                        .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
-                        .contained()
-                        .with_style(theme.search.option_button_group)
-                        .aligned()
-                        .boxed(),
-                )
-                .named("search bar")
-        }
+            theme.search.editor.input.container
+        };
+        Flex::row()
+            .with_child(
+                Flex::row()
+                    .with_child(ChildView::new(&self.query_editor).flex(1., true).boxed())
+                    .with_children(self.active_editor.as_ref().and_then(|editor| {
+                        let matches = self.editors_with_matches.get(&editor.downgrade())?;
+                        let message = if let Some(match_ix) = self.active_match_index {
+                            format!("{}/{}", match_ix + 1, matches.len())
+                        } else {
+                            "No matches".to_string()
+                        };
+
+                        Some(
+                            Label::new(message, theme.search.match_index.text.clone())
+                                .contained()
+                                .with_style(theme.search.match_index.container)
+                                .aligned()
+                                .boxed(),
+                        )
+                    }))
+                    .contained()
+                    .with_style(editor_container)
+                    .aligned()
+                    .constrained()
+                    .with_max_width(theme.search.editor.max_width)
+                    .boxed(),
+            )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_nav_button("<", Direction::Prev, cx))
+                    .with_child(self.render_nav_button(">", Direction::Next, cx))
+                    .aligned()
+                    .boxed(),
+            )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
+                    .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
+                    .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
+                    .contained()
+                    .with_style(theme.search.option_button_group)
+                    .aligned()
+                    .boxed(),
+            )
+            .named("search bar")
     }
 }
 
 impl ToolbarItemView for BufferSearchBar {
-    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+    fn set_active_pane_item(
+        &mut self,
+        item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
         cx.notify();
         self.active_editor_subscription.take();
         self.active_editor.take();
@@ -145,9 +145,21 @@ impl ToolbarItemView for BufferSearchBar {
                     Some(cx.subscribe(&editor, Self::on_active_editor_event));
                 self.active_editor = Some(editor);
                 self.update_matches(false, cx);
-                return;
+                if !self.dismissed {
+                    return ToolbarItemLocation::Secondary;
+                }
             }
         }
+
+        ToolbarItemLocation::Hidden
+    }
+
+    fn location_for_event(&self, _: &Self::Event, _: ToolbarItemLocation) -> ToolbarItemLocation {
+        if self.active_editor.is_some() && !self.dismissed {
+            ToolbarItemLocation::Secondary
+        } else {
+            ToolbarItemLocation::Hidden
+        }
     }
 }
 
@@ -186,6 +198,7 @@ impl BufferSearchBar {
         if let Some(active_editor) = self.active_editor.as_ref() {
             cx.focus(active_editor);
         }
+        cx.emit(Event::UpdateLocation);
         cx.notify();
     }
 
@@ -234,6 +247,7 @@ impl BufferSearchBar {
 
         self.dismissed = false;
         cx.notify();
+        cx.emit(Event::UpdateLocation);
         true
     }
 

crates/search/src/project_search.rs 🔗

@@ -16,7 +16,9 @@ use std::{
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{Item, ItemNavHistory, Pane, Settings, ToolbarItemView, Workspace};
+use workspace::{
+    Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
 
 action!(Deploy);
 action!(Search);
@@ -56,7 +58,7 @@ struct ProjectSearch {
     active_query: Option<SearchQuery>,
 }
 
-struct ProjectSearchView {
+pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
@@ -136,7 +138,7 @@ impl ProjectSearch {
     }
 }
 
-enum ViewEvent {
+pub enum ViewEvent {
     UpdateTab,
 }
 
@@ -748,14 +750,17 @@ impl ToolbarItemView for ProjectSearchBar {
         &mut self,
         active_pane_item: Option<&dyn workspace::ItemHandle>,
         cx: &mut ViewContext<Self>,
-    ) {
+    ) -> ToolbarItemLocation {
+        cx.notify();
         self.subscription = None;
         self.active_project_search = None;
         if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
             self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
             self.active_project_search = Some(search);
+            ToolbarItemLocation::PrimaryLeft
+        } else {
+            ToolbarItemLocation::Hidden
         }
-        cx.notify();
     }
 }
 

crates/search/src/search.rs 🔗

@@ -1,7 +1,7 @@
 pub use buffer_search::BufferSearchBar;
 use editor::{Anchor, MultiBufferSnapshot};
 use gpui::{action, MutableAppContext};
-pub use project_search::ProjectSearchBar;
+pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
     ops::Range,

crates/workspace/src/toolbar.rs 🔗

@@ -9,22 +9,38 @@ pub trait ToolbarItemView: View {
         &mut self,
         active_pane_item: Option<&dyn crate::ItemHandle>,
         cx: &mut ViewContext<Self>,
-    );
+    ) -> ToolbarItemLocation;
+
+    fn location_for_event(
+        &self,
+        _event: &Self::Event,
+        current_location: ToolbarItemLocation,
+    ) -> ToolbarItemLocation {
+        current_location
+    }
 }
 
 trait ToolbarItemViewHandle {
+    fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
     fn set_active_pane_item(
         &self,
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
-    );
+    ) -> ToolbarItemLocation;
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ToolbarItemLocation {
+    Hidden,
+    PrimaryLeft,
+    PrimaryRight,
+    Secondary,
 }
 
 pub struct Toolbar {
     active_pane_item: Option<Box<dyn ItemHandle>>,
-    left_items: Vec<Box<dyn ToolbarItemViewHandle>>,
-    right_items: Vec<Box<dyn ToolbarItemViewHandle>>,
+    items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 }
 
 impl Entity for Toolbar {
@@ -38,26 +54,50 @@ impl View for Toolbar {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = &cx.global::<Settings>().theme.workspace.toolbar;
-        Flex::row()
-            .with_children(self.left_items.iter().map(|i| {
-                ChildView::new(i.as_ref())
-                    .aligned()
-                    .contained()
-                    .with_margin_right(theme.item_spacing)
-                    .boxed()
-            }))
-            .with_children(self.right_items.iter().map(|i| {
-                ChildView::new(i.as_ref())
-                    .aligned()
-                    .contained()
-                    .with_margin_left(theme.item_spacing)
-                    .flex_float()
+
+        let mut primary_left_items = Vec::new();
+        let mut primary_right_items = Vec::new();
+        let mut secondary_item = None;
+
+        for (item, position) in &self.items {
+            match position {
+                ToolbarItemLocation::Hidden => {}
+                ToolbarItemLocation::PrimaryLeft => primary_left_items.push(item),
+                ToolbarItemLocation::PrimaryRight => primary_right_items.push(item),
+                ToolbarItemLocation::Secondary => secondary_item = Some(item),
+            }
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::row()
+                    .with_children(primary_left_items.iter().map(|i| {
+                        ChildView::new(i.as_ref())
+                            .aligned()
+                            .contained()
+                            .with_margin_right(theme.item_spacing)
+                            .boxed()
+                    }))
+                    .with_children(primary_right_items.iter().map(|i| {
+                        ChildView::new(i.as_ref())
+                            .aligned()
+                            .contained()
+                            .with_margin_left(theme.item_spacing)
+                            .flex_float()
+                            .boxed()
+                    }))
+                    .constrained()
+                    .with_height(theme.height)
+                    .boxed(),
+            )
+            .with_children(secondary_item.map(|item| {
+                ChildView::new(item.as_ref())
+                    .constrained()
+                    .with_height(theme.height)
                     .boxed()
             }))
             .contained()
             .with_style(theme.container)
-            .constrained()
-            .with_height(theme.height)
             .boxed()
     }
 }
@@ -66,49 +106,58 @@ impl Toolbar {
     pub fn new() -> Self {
         Self {
             active_pane_item: None,
-            left_items: Default::default(),
-            right_items: Default::default(),
+            items: Default::default(),
         }
     }
 
-    pub fn add_left_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
-    where
-        T: 'static + ToolbarItemView,
-    {
-        item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
-        self.left_items.push(Box::new(item));
-        cx.notify();
-    }
-
-    pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
     where
         T: 'static + ToolbarItemView,
     {
-        item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
-        self.right_items.push(Box::new(item));
+        let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
+        cx.subscribe(&item, |this, item, event, cx| {
+            if let Some((_, current_location)) =
+                this.items.iter_mut().find(|(i, _)| i.id() == item.id())
+            {
+                let new_location = item.read(cx).location_for_event(event, *current_location);
+                if new_location != *current_location {
+                    *current_location = new_location;
+                    cx.notify();
+                }
+            }
+        })
+        .detach();
+        self.items.push((Box::new(item), dbg!(location)));
         cx.notify();
     }
 
     pub fn set_active_pane_item(
         &mut self,
-        item: Option<&dyn ItemHandle>,
+        pane_item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.active_pane_item = item.map(|item| item.boxed_clone());
-        for tool in self.left_items.iter().chain(&self.right_items) {
-            tool.set_active_pane_item(item, cx);
+        self.active_pane_item = pane_item.map(|item| item.boxed_clone());
+        for (toolbar_item, current_location) in self.items.iter_mut() {
+            let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
+            if new_location != *current_location {
+                *current_location = new_location;
+                cx.notify();
+            }
         }
     }
 
     pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
-        self.left_items
+        self.items
             .iter()
-            .chain(&self.right_items)
-            .find_map(|tool| tool.to_any().downcast())
+            .find_map(|(item, _)| item.to_any().downcast())
     }
 }
 
 impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
     fn to_any(&self) -> AnyViewHandle {
         self.into()
     }
@@ -117,10 +166,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
         &self,
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
-    ) {
+    ) -> ToolbarItemLocation {
         self.update(cx, |this, cx| {
             this.set_active_pane_item(active_pane_item, cx)
-        });
+        })
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -48,7 +48,7 @@ use std::{
     },
 };
 use theme::{Theme, ThemeRegistry};
-pub use toolbar::ToolbarItemView;
+pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::ResultExt;
 
 type ProjectItemBuilders = HashMap<

crates/zed/src/zed.rs 🔗

@@ -111,12 +111,11 @@ pub fn build_workspace(
         pane.update(cx, |pane, cx| {
             pane.toolbar().update(cx, |toolbar, cx| {
                 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
-                toolbar.add_left_item(breadcrumbs, cx);
-
+                toolbar.add_item(breadcrumbs, cx);
                 let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
-                toolbar.add_right_item(buffer_search_bar, cx);
+                toolbar.add_item(buffer_search_bar, cx);
                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
-                toolbar.add_right_item(project_search_bar, cx);
+                toolbar.add_item(project_search_bar, cx);
             })
         });
     })