Merge pull request #705 from zed-industries/breadcrumbs

Antonio Scandurra created

Introduce breadcrumbs

Change summary

Cargo.lock                                    |  14 
crates/breadcrumbs/Cargo.toml                 |  22 
crates/breadcrumbs/src/breadcrumbs.rs         | 146 ++++++
crates/chat_panel/src/chat_panel.rs           |   2 
crates/contacts_panel/src/contacts_panel.rs   |   2 
crates/editor/src/multi_buffer.rs             |  27 +
crates/file_finder/src/file_finder.rs         |  36 
crates/gpui/src/color.rs                      |   4 
crates/gpui/src/elements.rs                   |  11 
crates/gpui/src/elements/flex.rs              | 118 +++--
crates/language/src/buffer.rs                 |  34 +
crates/language/src/outline.rs                |   2 
crates/language/src/tests.rs                  | 146 +++++-
crates/outline/src/outline.rs                 |   6 
crates/project_symbols/src/project_symbols.rs |   6 
crates/search/src/buffer_search.rs            | 274 ++++++-----
crates/search/src/project_search.rs           | 483 ++++++++++++--------
crates/search/src/search.rs                   |  11 
crates/text/src/anchor.rs                     |   2 
crates/theme/src/theme.rs                     |   5 
crates/theme_selector/src/theme_selector.rs   |   6 
crates/workspace/src/pane.rs                  | 139 -----
crates/workspace/src/pane_group.rs            |   2 
crates/workspace/src/sidebar.rs               |   2 
crates/workspace/src/status_bar.rs            |   2 
crates/workspace/src/toolbar.rs               | 193 ++++++++
crates/workspace/src/workspace.rs             |  39 +
crates/zed/Cargo.toml                         |   1 
crates/zed/assets/themes/_base.toml           |  20 
crates/zed/src/zed.rs                         |  17 
30 files changed, 1,206 insertions(+), 566 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -719,6 +719,19 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "breadcrumbs"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "editor",
+ "gpui",
+ "language",
+ "search",
+ "theme",
+ "workspace",
+]
+
 [[package]]
 name = "brotli"
 version = "3.3.0"
@@ -5963,6 +5976,7 @@ dependencies = [
  "async-compression",
  "async-recursion",
  "async-trait",
+ "breadcrumbs",
  "chat_panel",
  "client",
  "clock",

crates/breadcrumbs/Cargo.toml 🔗

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

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -0,0 +1,146 @@
+use editor::{Anchor, Editor};
+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, ToolbarItemLocation, ToolbarItemView};
+
+pub enum Event {
+    UpdateLocation,
+}
+
+pub struct Breadcrumbs {
+    editor: Option<ViewHandle<Editor>>,
+    project_search: Option<ViewHandle<ProjectSearchView>>,
+    subscriptions: Vec<Subscription>,
+}
+
+impl Breadcrumbs {
+    pub fn new() -> Self {
+        Self {
+            editor: Default::default(),
+            subscriptions: Default::default(),
+            project_search: Default::default(),
+        }
+    }
+
+    fn active_symbols(
+        &self,
+        theme: &SyntaxTheme,
+        cx: &AppContext,
+    ) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
+        let editor = self.editor.as_ref()?.read(cx);
+        let cursor = editor.newest_anchor_selection().head();
+        let (buffer, symbols) = editor
+            .buffer()
+            .read(cx)
+            .read(cx)
+            .symbols_containing(cursor, Some(theme))?;
+        Some((buffer, symbols))
+    }
+}
+
+impl Entity for Breadcrumbs {
+    type Event = Event;
+}
+
+impl View for Breadcrumbs {
+    fn ui_name() -> &'static str {
+        "Breadcrumbs"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
+        let (buffer, symbols) =
+            if let Some((buffer, symbols)) = self.active_symbols(&theme.editor.syntax, cx) {
+                (buffer, symbols)
+            } else {
+                return Empty::new().boxed();
+            };
+
+        let filename = if let Some(path) = buffer.path() {
+            path.to_string_lossy()
+        } else {
+            Cow::Borrowed("untitled")
+        };
+
+        Flex::row()
+            .with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed())
+            .with_children(symbols.into_iter().flat_map(|symbol| {
+                [
+                    Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed(),
+                    Text::new(symbol.text, theme.breadcrumbs.text.clone())
+                        .with_highlights(symbol.highlight_ranges)
+                        .boxed(),
+                ]
+            }))
+            .contained()
+            .with_style(theme.breadcrumbs.container)
+            .aligned()
+            .left()
+            .boxed()
+    }
+}
+
+impl ToolbarItemView for Breadcrumbs {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        cx.notify();
+        self.subscriptions.clear();
+        self.editor = None;
+        self.project_search = None;
+        if let Some(item) = active_pane_item {
+            if let Some(editor) = item.act_as::<Editor>(cx) {
+                self.subscriptions
+                    .push(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(project_search) = item.downcast::<ProjectSearchView>() {
+                    self.subscriptions
+                        .push(cx.subscribe(&project_search, |_, _, _, cx| {
+                            cx.emit(Event::UpdateLocation);
+                        }));
+                    self.project_search = Some(project_search.clone());
+
+                    if project_search.read(cx).has_matches() {
+                        ToolbarItemLocation::Secondary
+                    } else {
+                        ToolbarItemLocation::Hidden
+                    }
+                } else {
+                    ToolbarItemLocation::PrimaryLeft { flex: None }
+                }
+            } else {
+                ToolbarItemLocation::Hidden
+            }
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
+
+    fn location_for_event(
+        &self,
+        _: &Event,
+        current_location: ToolbarItemLocation,
+        cx: &AppContext,
+    ) -> ToolbarItemLocation {
+        if let Some(project_search) = self.project_search.as_ref() {
+            if project_search.read(cx).has_matches() {
+                ToolbarItemLocation::Secondary
+            } else {
+                ToolbarItemLocation::Hidden
+            }
+        } else {
+            current_location
+        }
+    }
+}

crates/chat_panel/src/chat_panel.rs 🔗

@@ -219,7 +219,7 @@ impl ChatPanel {
             Empty::new().boxed()
         };
 
-        Flexible::new(1., true, messages).boxed()
+        FlexItem::new(messages).flex(1., true).boxed()
     }
 
     fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {

crates/editor/src/multi_buffer.rs 🔗

@@ -2264,6 +2264,33 @@ impl MultiBufferSnapshot {
         ))
     }
 
+    pub fn symbols_containing<T: ToOffset>(
+        &self,
+        offset: T,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
+        let anchor = self.anchor_before(offset);
+        let excerpt_id = anchor.excerpt_id();
+        let excerpt = self.excerpt(excerpt_id)?;
+        Some((
+            excerpt.buffer.clone(),
+            excerpt
+                .buffer
+                .symbols_containing(anchor.text_anchor, theme)
+                .into_iter()
+                .flatten()
+                .map(|item| OutlineItem {
+                    depth: item.depth,
+                    range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
+                        ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
+                    text: item.text,
+                    highlight_ranges: item.highlight_ranges,
+                    name_ranges: item.name_ranges,
+                })
+                .collect(),
+        ))
+    }
+
     fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
         let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
         cursor.seek(&Some(excerpt_id), Bias::Left, &());

crates/file_finder/src/file_finder.rs 🔗

@@ -78,7 +78,11 @@ impl View for FileFinder {
                                 .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )
-                        .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+                        .with_child(
+                            FlexItem::new(self.render_matches(cx))
+                                .flex(1., false)
+                                .boxed(),
+                        )
                         .boxed(),
                 )
                 .with_style(settings.theme.selector.container)
@@ -166,23 +170,19 @@ impl FileFinder {
                 //     .boxed(),
                 // )
                 .with_child(
-                    Flexible::new(
-                        1.0,
-                        false,
-                        Flex::column()
-                            .with_child(
-                                Label::new(file_name.to_string(), style.label.clone())
-                                    .with_highlights(file_name_positions)
-                                    .boxed(),
-                            )
-                            .with_child(
-                                Label::new(full_path, style.label.clone())
-                                    .with_highlights(full_path_positions)
-                                    .boxed(),
-                            )
-                            .boxed(),
-                    )
-                    .boxed(),
+                    Flex::column()
+                        .with_child(
+                            Label::new(file_name.to_string(), style.label.clone())
+                                .with_highlights(file_name_positions)
+                                .boxed(),
+                        )
+                        .with_child(
+                            Label::new(full_path, style.label.clone())
+                                .with_highlights(full_path_positions)
+                                .boxed(),
+                        )
+                        .flex(1., false)
+                        .boxed(),
                 )
                 .boxed(),
         )

crates/gpui/src/color.rs 🔗

@@ -41,6 +41,10 @@ impl Color {
         Self(ColorU::from_u32(0x0000ffff))
     }
 
+    pub fn yellow() -> Self {
+        Self(ColorU::from_u32(0x00ffffff))
+    }
+
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
         Self(ColorU::new(r, g, b, a))
     }

crates/gpui/src/elements.rs 🔗

@@ -139,11 +139,18 @@ pub trait Element {
         Expanded::new(self.boxed())
     }
 
-    fn flexible(self, flex: f32, expanded: bool) -> Flexible
+    fn flex(self, flex: f32, expanded: bool) -> FlexItem
     where
         Self: 'static + Sized,
     {
-        Flexible::new(flex, expanded, self.boxed())
+        FlexItem::new(self.boxed()).flex(flex, expanded)
+    }
+
+    fn flex_float(self) -> FlexItem
+    where
+        Self: 'static + Sized,
+    {
+        FlexItem::new(self.boxed()).float()
     }
 }
 

crates/gpui/src/elements/flex.rs 🔗

@@ -34,7 +34,7 @@ impl Flex {
 
     fn layout_flex_children(
         &mut self,
-        expanded: bool,
+        layout_expanded: bool,
         constraint: SizeConstraint,
         remaining_space: &mut f32,
         remaining_flex: &mut f32,
@@ -44,32 +44,33 @@ impl Flex {
         let cross_axis = self.axis.invert();
         for child in &mut self.children {
             if let Some(metadata) = child.metadata::<FlexParentData>() {
-                if metadata.expanded != expanded {
-                    continue;
-                }
+                if let Some((flex, expanded)) = metadata.flex {
+                    if expanded != layout_expanded {
+                        continue;
+                    }
 
-                let flex = metadata.flex;
-                let child_max = if *remaining_flex == 0.0 {
-                    *remaining_space
-                } else {
-                    let space_per_flex = *remaining_space / *remaining_flex;
-                    space_per_flex * flex
-                };
-                let child_min = if expanded { child_max } else { 0. };
-                let child_constraint = match self.axis {
-                    Axis::Horizontal => SizeConstraint::new(
-                        vec2f(child_min, constraint.min.y()),
-                        vec2f(child_max, constraint.max.y()),
-                    ),
-                    Axis::Vertical => SizeConstraint::new(
-                        vec2f(constraint.min.x(), child_min),
-                        vec2f(constraint.max.x(), child_max),
-                    ),
-                };
-                let child_size = child.layout(child_constraint, cx);
-                *remaining_space -= child_size.along(self.axis);
-                *remaining_flex -= flex;
-                *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+                    let child_max = if *remaining_flex == 0.0 {
+                        *remaining_space
+                    } else {
+                        let space_per_flex = *remaining_space / *remaining_flex;
+                        space_per_flex * flex
+                    };
+                    let child_min = if expanded { child_max } else { 0. };
+                    let child_constraint = match self.axis {
+                        Axis::Horizontal => SizeConstraint::new(
+                            vec2f(child_min, constraint.min.y()),
+                            vec2f(child_max, constraint.max.y()),
+                        ),
+                        Axis::Vertical => SizeConstraint::new(
+                            vec2f(constraint.min.x(), child_min),
+                            vec2f(constraint.max.x(), child_max),
+                        ),
+                    };
+                    let child_size = child.layout(child_constraint, cx);
+                    *remaining_space -= child_size.along(self.axis);
+                    *remaining_flex -= flex;
+                    *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+                }
             }
         }
     }
@@ -82,7 +83,7 @@ impl Extend<ElementBox> for Flex {
 }
 
 impl Element for Flex {
-    type LayoutState = bool;
+    type LayoutState = f32;
     type PaintState = ();
 
     fn layout(
@@ -96,8 +97,11 @@ impl Element for Flex {
         let cross_axis = self.axis.invert();
         let mut cross_axis_max: f32 = 0.0;
         for child in &mut self.children {
-            if let Some(metadata) = child.metadata::<FlexParentData>() {
-                *total_flex.get_or_insert(0.) += metadata.flex;
+            if let Some(flex) = child
+                .metadata::<FlexParentData>()
+                .and_then(|metadata| metadata.flex.map(|(flex, _)| flex))
+            {
+                *total_flex.get_or_insert(0.) += flex;
             } else {
                 let child_constraint = match self.axis {
                     Axis::Horizontal => SizeConstraint::new(
@@ -115,12 +119,12 @@ impl Element for Flex {
             }
         }
 
+        let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
         let mut size = if let Some(mut remaining_flex) = total_flex {
-            if constraint.max_along(self.axis).is_infinite() {
+            if remaining_space.is_infinite() {
                 panic!("flex contains flexible children but has an infinite constraint along the flex axis");
             }
 
-            let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
             self.layout_flex_children(
                 false,
                 constraint,
@@ -156,38 +160,47 @@ impl Element for Flex {
             size.set_y(size.y().max(constraint.min.y()));
         }
 
-        let mut overflowing = false;
         if size.x() > constraint.max.x() {
             size.set_x(constraint.max.x());
-            overflowing = true;
         }
         if size.y() > constraint.max.y() {
             size.set_y(constraint.max.y());
-            overflowing = true;
         }
 
-        (size, overflowing)
+        (size, remaining_space)
     }
 
     fn paint(
         &mut self,
         bounds: RectF,
         visible_bounds: RectF,
-        overflowing: &mut Self::LayoutState,
+        remaining_space: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        if *overflowing {
+        let overflowing = *remaining_space < 0.;
+        if overflowing {
             cx.scene.push_layer(Some(bounds));
         }
         let mut child_origin = bounds.origin();
         for child in &mut self.children {
+            if *remaining_space > 0. {
+                if let Some(metadata) = child.metadata::<FlexParentData>() {
+                    if metadata.float {
+                        match self.axis {
+                            Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0),
+                            Axis::Vertical => child_origin += vec2f(0.0, *remaining_space),
+                        }
+                        *remaining_space = 0.;
+                    }
+                }
+            }
             child.paint(child_origin, visible_bounds, cx);
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
                 Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
             }
         }
-        if *overflowing {
+        if overflowing {
             cx.scene.pop_layer();
         }
     }
@@ -224,25 +237,38 @@ impl Element for Flex {
 }
 
 struct FlexParentData {
-    flex: f32,
-    expanded: bool,
+    flex: Option<(f32, bool)>,
+    float: bool,
 }
 
-pub struct Flexible {
+pub struct FlexItem {
     metadata: FlexParentData,
     child: ElementBox,
 }
 
-impl Flexible {
-    pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
-        Flexible {
-            metadata: FlexParentData { flex, expanded },
+impl FlexItem {
+    pub fn new(child: ElementBox) -> Self {
+        FlexItem {
+            metadata: FlexParentData {
+                flex: None,
+                float: false,
+            },
             child,
         }
     }
+
+    pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
+        self.metadata.flex = Some((flex, expanded));
+        self
+    }
+
+    pub fn float(mut self) -> Self {
+        self.metadata.float = true;
+        self
+    }
 }
 
-impl Element for Flexible {
+impl Element for FlexItem {
     type LayoutState = ();
     type PaintState = ();
 

crates/language/src/buffer.rs 🔗

@@ -1674,6 +1674,32 @@ impl BufferSnapshot {
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
+        self.outline_items_containing(0..self.len(), theme)
+            .map(Outline::new)
+    }
+
+    pub fn symbols_containing<T: ToOffset>(
+        &self,
+        position: T,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
+        let position = position.to_offset(&self);
+        let mut items =
+            self.outline_items_containing(position.saturating_sub(1)..position + 1, theme)?;
+        let mut prev_depth = None;
+        items.retain(|item| {
+            let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
+            prev_depth = Some(item.depth);
+            result
+        });
+        Some(items)
+    }
+
+    fn outline_items_containing(
+        &self,
+        range: Range<usize>,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
         let tree = self.tree.as_ref()?;
         let grammar = self
             .language
@@ -1681,6 +1707,7 @@ impl BufferSnapshot {
             .and_then(|language| language.grammar.as_ref())?;
 
         let mut cursor = QueryCursorHandle::new();
+        cursor.set_byte_range(range);
         let matches = cursor.matches(
             &grammar.outline_query,
             tree.root_node(),
@@ -1773,12 +1800,7 @@ impl BufferSnapshot {
                 })
             })
             .collect::<Vec<_>>();
-
-        if items.is_empty() {
-            None
-        } else {
-            Some(Outline::new(items))
-        }
+        Some(items)
     }
 
     pub fn enclosing_bracket_ranges<T: ToOffset>(

crates/language/src/outline.rs 🔗

@@ -10,7 +10,7 @@ pub struct Outline<T> {
     path_candidate_prefixes: Vec<usize>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct OutlineItem<T> {
     pub depth: usize,
     pub range: Range<T>,

crates/language/src/tests.rs 🔗

@@ -282,36 +282,6 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_outline(cx: &mut gpui::TestAppContext) {
-    let language = Arc::new(
-        rust_lang()
-            .with_outline_query(
-                r#"
-                (struct_item
-                    "struct" @context
-                    name: (_) @name) @item
-                (enum_item
-                    "enum" @context
-                    name: (_) @name) @item
-                (enum_variant
-                    name: (_) @name) @item
-                (field_declaration
-                    name: (_) @name) @item
-                (impl_item
-                    "impl" @context
-                    trait: (_) @name
-                    "for" @context
-                    type: (_) @name) @item
-                (function_item
-                    "fn" @context
-                    name: (_) @name) @item
-                (mod_item
-                    "mod" @context
-                    name: (_) @name) @item
-                "#,
-            )
-            .unwrap(),
-    );
-
     let text = r#"
         struct Person {
             name: String,
@@ -339,7 +309,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
     let outline = buffer
         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
         .unwrap();
@@ -413,6 +384,93 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     }
 }
 
+#[gpui::test]
+async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
+    let text = r#"
+        impl Person {
+            fn one() {
+                1
+            }
+
+            fn two() {
+                2
+            }fn three() {
+                3
+            }
+        }
+    "#
+    .unindent();
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+    // point is at the start of an item
+    assert_eq!(
+        symbols_containing(Point::new(1, 4), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in the middle of an item
+    assert_eq!(
+        symbols_containing(Point::new(2, 8), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is at the end of an item
+    assert_eq!(
+        symbols_containing(Point::new(3, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in between two adjacent items
+    assert_eq!(
+        symbols_containing(Point::new(7, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
+        ]
+    );
+
+    fn symbols_containing<'a>(
+        position: Point,
+        snapshot: &'a BufferSnapshot,
+    ) -> Vec<(String, Range<Point>)> {
+        snapshot
+            .symbols_containing(position, None)
+            .unwrap()
+            .into_iter()
+            .map(|item| {
+                (
+                    item.text,
+                    item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
+                )
+            })
+            .collect()
+    }
+}
+
 #[gpui::test]
 fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
     let buffer = cx.add_model(|cx| {
@@ -889,6 +947,32 @@ fn rust_lang() -> Language {
         "#,
     )
     .unwrap()
+    .with_outline_query(
+        r#"
+        (struct_item
+            "struct" @context
+            name: (_) @name) @item
+        (enum_item
+            "enum" @context
+            name: (_) @name) @item
+        (enum_variant
+            name: (_) @name) @item
+        (field_declaration
+            name: (_) @name) @item
+        (impl_item
+            "impl" @context
+            trait: (_)? @name
+            "for"? @context
+            type: (_) @name) @item
+        (function_item
+            "fn" @context
+            name: (_) @name) @item
+        (mod_item
+            "mod" @context
+            name: (_) @name) @item
+        "#,
+    )
+    .unwrap()
 }
 
 fn empty(point: Point) -> Range<Point> {

crates/outline/src/outline.rs 🔗

@@ -77,7 +77,11 @@ impl View for OutlineView {
                     .with_style(settings.theme.selector.input_editor.container)
                     .boxed(),
             )
-            .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+            .with_child(
+                FlexItem::new(self.render_matches(cx))
+                    .flex(1.0, false)
+                    .boxed(),
+            )
             .contained()
             .with_style(settings.theme.selector.container)
             .constrained()

crates/project_symbols/src/project_symbols.rs 🔗

@@ -76,7 +76,11 @@ impl View for ProjectSymbolsView {
                     .with_style(settings.theme.selector.input_editor.container)
                     .boxed(),
             )
-            .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+            .with_child(
+                FlexItem::new(self.render_matches(cx))
+                    .flex(1., false)
+                    .boxed(),
+            )
             .contained()
             .with_style(settings.theme.selector.container)
             .constrained()

crates/search/src/buffer_search.rs 🔗

@@ -2,43 +2,52 @@ use crate::{active_match_index, match_index_for_direction, Direction, SearchOpti
 use collections::HashMap;
 use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
 use gpui::{
-    action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
-    RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
+    MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakViewHandle,
 };
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
+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")),
         Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
-        Binding::new("escape", Dismiss, Some("SearchBar")),
-        Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
-        Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
+        Binding::new("escape", Dismiss, Some("BufferSearchBar")),
+        Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
+        Binding::new(
+            "enter",
+            SelectMatch(Direction::Next),
+            Some("BufferSearchBar"),
+        ),
         Binding::new(
             "shift-enter",
             SelectMatch(Direction::Prev),
-            Some("SearchBar"),
+            Some("BufferSearchBar"),
         ),
         Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
         Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
     ]);
-    cx.add_action(SearchBar::deploy);
-    cx.add_action(SearchBar::dismiss);
-    cx.add_action(SearchBar::focus_editor);
-    cx.add_action(SearchBar::toggle_search_option);
-    cx.add_action(SearchBar::select_match);
-    cx.add_action(SearchBar::select_match_on_pane);
+    cx.add_action(BufferSearchBar::deploy);
+    cx.add_action(BufferSearchBar::dismiss);
+    cx.add_action(BufferSearchBar::focus_editor);
+    cx.add_action(BufferSearchBar::toggle_search_option);
+    cx.add_action(BufferSearchBar::select_match);
+    cx.add_action(BufferSearchBar::select_match_on_pane);
 }
 
-struct SearchBar {
+pub struct BufferSearchBar {
     query_editor: ViewHandle<Editor>,
     active_editor: Option<ViewHandle<Editor>>,
     active_match_index: Option<usize>,
@@ -52,13 +61,13 @@ struct SearchBar {
     dismissed: bool,
 }
 
-impl Entity for SearchBar {
-    type Event = ();
+impl Entity for BufferSearchBar {
+    type Event = Event;
 }
 
-impl View for SearchBar {
+impl View for BufferSearchBar {
     fn ui_name() -> &'static str {
-        "SearchBar"
+        "BufferSearchBar"
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@@ -74,61 +83,69 @@ impl View for SearchBar {
         };
         Flex::row()
             .with_child(
-                ChildView::new(&self.query_editor)
+                Flex::row()
+                    .with_child(
+                        ChildView::new(&self.query_editor)
+                            .aligned()
+                            .left()
+                            .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_min_width(theme.search.editor.min_width)
                     .with_max_width(theme.search.editor.max_width)
+                    .flex(1., false)
                     .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)
+                    .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_nav_button("<", Direction::Prev, cx))
-                    .with_child(self.render_nav_button(">", Direction::Next, cx))
+                    .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(),
             )
-            .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(theme.search.container)
-            .constrained()
-            .with_height(theme.workspace.toolbar.height)
             .named("search bar")
     }
 }
 
-impl Toolbar for SearchBar {
-    fn active_item_changed(
+impl ToolbarItemView for BufferSearchBar {
+    fn set_active_pane_item(
         &mut self,
-        item: Option<Box<dyn ItemHandle>>,
+        item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
-    ) -> bool {
+    ) -> ToolbarItemLocation {
+        cx.notify();
         self.active_editor_subscription.take();
         self.active_editor.take();
         self.pending_search.take();
@@ -139,26 +156,31 @@ impl Toolbar for SearchBar {
                     Some(cx.subscribe(&editor, Self::on_active_editor_event));
                 self.active_editor = Some(editor);
                 self.update_matches(false, cx);
-                return true;
+                if !self.dismissed {
+                    return ToolbarItemLocation::Secondary;
+                }
             }
         }
-        false
+
+        ToolbarItemLocation::Hidden
     }
 
-    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        self.dismissed = true;
-        for (editor, _) in &self.editors_with_matches {
-            if let Some(editor) = editor.upgrade(cx) {
-                editor.update(cx, |editor, cx| {
-                    editor.clear_background_highlights::<Self>(cx)
-                });
-            }
+    fn location_for_event(
+        &self,
+        _: &Self::Event,
+        _: ToolbarItemLocation,
+        _: &AppContext,
+    ) -> ToolbarItemLocation {
+        if self.active_editor.is_some() && !self.dismissed {
+            ToolbarItemLocation::Secondary
+        } else {
+            ToolbarItemLocation::Hidden
         }
     }
 }
 
-impl SearchBar {
-    fn new(cx: &mut ViewContext<Self>) -> Self {
+impl BufferSearchBar {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
         let query_editor = cx.add_view(|cx| {
             Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
         });
@@ -176,10 +198,75 @@ impl SearchBar {
             regex: false,
             pending_search: None,
             query_contains_error: false,
-            dismissed: false,
+            dismissed: true,
         }
     }
 
+    fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+        self.dismissed = true;
+        for (editor, _) in &self.editors_with_matches {
+            if let Some(editor) = editor.upgrade(cx) {
+                editor.update(cx, |editor, cx| {
+                    editor.clear_background_highlights::<Self>(cx)
+                });
+            }
+        }
+        if let Some(active_editor) = self.active_editor.as_ref() {
+            cx.focus(active_editor);
+        }
+        cx.emit(Event::UpdateLocation);
+        cx.notify();
+    }
+
+    fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
+        let editor = if let Some(editor) = self.active_editor.clone() {
+            editor
+        } else {
+            return false;
+        };
+
+        let display_map = editor
+            .update(cx, |editor, cx| editor.snapshot(cx))
+            .display_snapshot;
+        let selection = editor
+            .read(cx)
+            .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
+
+        let mut text: String;
+        if selection.start == selection.end {
+            let point = selection.start.to_display_point(&display_map);
+            let range = editor::movement::surrounding_word(&display_map, point);
+            let range = range.start.to_offset(&display_map, Bias::Left)
+                ..range.end.to_offset(&display_map, Bias::Right);
+            text = display_map.buffer_snapshot.text_for_range(range).collect();
+            if text.trim().is_empty() {
+                text = String::new();
+            }
+        } else {
+            text = display_map
+                .buffer_snapshot
+                .text_for_range(selection.start..selection.end)
+                .collect();
+        }
+
+        if !text.is_empty() {
+            self.set_query(&text, cx);
+        }
+
+        if focus {
+            let query_editor = self.query_editor.clone();
+            query_editor.update(cx, |query_editor, cx| {
+                query_editor.select_all(&editor::SelectAll, cx);
+            });
+            cx.focus_self();
+        }
+
+        self.dismissed = false;
+        cx.notify();
+        cx.emit(Event::UpdateLocation);
+        true
+    }
+
     fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
             query_editor.buffer().update(cx, |query_buffer, cx| {
@@ -238,61 +325,13 @@ impl SearchBar {
         .boxed()
     }
 
-    fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
-        workspace.active_pane().update(cx, |pane, cx| {
-            pane.show_toolbar(cx, |cx| SearchBar::new(cx));
-
-            if let Some(search_bar) = pane
-                .active_toolbar()
-                .and_then(|toolbar| toolbar.downcast::<Self>())
-            {
-                search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
-                let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
-                let display_map = editor
-                    .update(cx, |editor, cx| editor.snapshot(cx))
-                    .display_snapshot;
-                let selection = editor
-                    .read(cx)
-                    .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
-
-                let mut text: String;
-                if selection.start == selection.end {
-                    let point = selection.start.to_display_point(&display_map);
-                    let range = editor::movement::surrounding_word(&display_map, point);
-                    let range = range.start.to_offset(&display_map, Bias::Left)
-                        ..range.end.to_offset(&display_map, Bias::Right);
-                    text = display_map.buffer_snapshot.text_for_range(range).collect();
-                    if text.trim().is_empty() {
-                        text = String::new();
-                    }
-                } else {
-                    text = display_map
-                        .buffer_snapshot
-                        .text_for_range(selection.start..selection.end)
-                        .collect();
-                }
-
-                if !text.is_empty() {
-                    search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
-                }
-
-                if *focus {
-                    let query_editor = search_bar.read(cx).query_editor.clone();
-                    query_editor.update(cx, |query_editor, cx| {
-                        query_editor.select_all(&editor::SelectAll, cx);
-                    });
-                    cx.focus(&search_bar);
-                }
-            } else {
-                cx.propagate_action();
+    fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
+                return;
             }
-        });
-    }
-
-    fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
-        if pane.toolbar::<SearchBar>().is_some() {
-            pane.dismiss_toolbar(cx);
         }
+        cx.propagate_action();
     }
 
     fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
@@ -346,7 +385,7 @@ impl SearchBar {
     }
 
     fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
-        if let Some(search_bar) = pane.toolbar::<SearchBar>() {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
             search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
         }
     }
@@ -540,8 +579,9 @@ mod tests {
         });
 
         let search_bar = cx.add_view(Default::default(), |cx| {
-            let mut search_bar = SearchBar::new(cx);
-            search_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
+            let mut search_bar = BufferSearchBar::new(cx);
+            search_bar.set_active_pane_item(Some(&editor), cx);
+            search_bar.show(false, cx);
             search_bar
         });
 

crates/search/src/project_search.rs 🔗

@@ -6,8 +6,8 @@ use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
     action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
-    ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakModelHandle, WeakViewHandle,
+    ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
+    ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use project::{search::SearchQuery, Project};
 use std::{
@@ -16,7 +16,9 @@ use std::{
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{Item, ItemNavHistory, Settings, Workspace};
+use workspace::{
+    Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
 
 action!(Deploy);
 action!(Search);
@@ -31,29 +33,21 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
 pub fn init(cx: &mut MutableAppContext) {
     cx.set_global(ActiveSearches::default());
     cx.add_bindings([
-        Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
-        Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
+        Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
+        Binding::new("cmd-f", ToggleFocus, Some("Pane")),
         Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
-        Binding::new("enter", Search, Some("ProjectSearchView")),
-        Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
-        Binding::new(
-            "cmd-g",
-            SelectMatch(Direction::Next),
-            Some("ProjectSearchView"),
-        ),
-        Binding::new(
-            "cmd-shift-G",
-            SelectMatch(Direction::Prev),
-            Some("ProjectSearchView"),
-        ),
+        Binding::new("enter", Search, Some("ProjectSearchBar")),
+        Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
+        Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
+        Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
     ]);
     cx.add_action(ProjectSearchView::deploy);
-    cx.add_action(ProjectSearchView::search);
-    cx.add_action(ProjectSearchView::search_in_new);
-    cx.add_action(ProjectSearchView::toggle_search_option);
-    cx.add_action(ProjectSearchView::select_match);
-    cx.add_action(ProjectSearchView::toggle_focus);
-    cx.capture_action(ProjectSearchView::tab);
+    cx.add_action(ProjectSearchBar::search);
+    cx.add_action(ProjectSearchBar::search_in_new);
+    cx.add_action(ProjectSearchBar::toggle_search_option);
+    cx.add_action(ProjectSearchBar::select_match);
+    cx.add_action(ProjectSearchBar::toggle_focus);
+    cx.capture_action(ProjectSearchBar::tab);
 }
 
 struct ProjectSearch {
@@ -64,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>,
@@ -75,6 +69,11 @@ struct ProjectSearchView {
     active_match_index: Option<usize>,
 }
 
+pub struct ProjectSearchBar {
+    active_project_search: Option<ViewHandle<ProjectSearchView>>,
+    subscription: Option<Subscription>,
+}
+
 impl Entity for ProjectSearch {
     type Event = ();
 }
@@ -139,7 +138,7 @@ impl ProjectSearch {
     }
 }
 
-enum ViewEvent {
+pub enum ViewEvent {
     UpdateTab,
 }
 
@@ -154,7 +153,7 @@ impl View for ProjectSearchView {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let model = &self.model.read(cx);
-        let results = if model.match_ranges.is_empty() {
+        if model.match_ranges.is_empty() {
             let theme = &cx.global::<Settings>().theme;
             let text = if self.query_editor.read(cx).text(cx).is_empty() {
                 ""
@@ -167,18 +166,11 @@ impl View for ProjectSearchView {
                 .aligned()
                 .contained()
                 .with_background_color(theme.editor.background)
-                .flexible(1., true)
+                .flex(1., true)
                 .boxed()
         } else {
-            ChildView::new(&self.results_editor)
-                .flexible(1., true)
-                .boxed()
-        };
-
-        Flex::column()
-            .with_child(self.render_query_editor(cx))
-            .with_child(results)
-            .boxed()
+            ChildView::new(&self.results_editor).flex(1., true).boxed()
+        }
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@@ -402,45 +394,12 @@ impl ProjectSearchView {
         }
     }
 
-    fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
+    fn search(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(query) = self.build_search_query(cx) {
             self.model.update(cx, |model, cx| model.search(query, cx));
         }
     }
 
-    fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
-        if let Some(search_view) = workspace
-            .active_item(cx)
-            .and_then(|item| item.downcast::<ProjectSearchView>())
-        {
-            let new_query = search_view.update(cx, |search_view, cx| {
-                let new_query = search_view.build_search_query(cx);
-                if new_query.is_some() {
-                    if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
-                        search_view.query_editor.update(cx, |editor, cx| {
-                            editor.set_text(old_query.as_str(), cx);
-                        });
-                        search_view.regex = old_query.is_regex();
-                        search_view.whole_word = old_query.whole_word();
-                        search_view.case_sensitive = old_query.case_sensitive();
-                    }
-                }
-                new_query
-            });
-            if let Some(new_query) = new_query {
-                let model = cx.add_model(|cx| {
-                    let mut model = ProjectSearch::new(workspace.project().clone(), cx);
-                    model.search(new_query, cx);
-                    model
-                });
-                workspace.add_item(
-                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
-                    cx,
-                );
-            }
-        }
-    }
-
     fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
         let text = self.query_editor.read(cx).text(cx);
         if self.regex {
@@ -461,22 +420,7 @@ impl ProjectSearchView {
         }
     }
 
-    fn toggle_search_option(
-        &mut self,
-        ToggleSearchOption(option): &ToggleSearchOption,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let value = match option {
-            SearchOption::WholeWord => &mut self.whole_word,
-            SearchOption::CaseSensitive => &mut self.case_sensitive,
-            SearchOption::Regex => &mut self.regex,
-        };
-        *value = !*value;
-        self.search(&Search, cx);
-        cx.notify();
-    }
-
-    fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
+    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
             let model = self.model.read(cx);
             let results_editor = self.results_editor.read(cx);
@@ -495,26 +439,6 @@ impl ProjectSearchView {
         }
     }
 
-    fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
-        if self.query_editor.is_focused(cx) {
-            if !self.model.read(cx).match_ranges.is_empty() {
-                self.focus_results_editor(cx);
-            }
-        } else {
-            self.focus_query_editor(cx);
-        }
-    }
-
-    fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
-        if self.query_editor.is_focused(cx) {
-            if !self.model.read(cx).match_ranges.is_empty() {
-                self.focus_results_editor(cx);
-            }
-        } else {
-            cx.propagate_action()
-        }
-    }
-
     fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
             query_editor.select_all(&SelectAll, cx);
@@ -564,61 +488,151 @@ impl ProjectSearchView {
         }
     }
 
-    fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.global::<Settings>().theme.clone();
-        let editor_container = if self.query_contains_error {
-            theme.search.invalid_editor
+    pub fn has_matches(&self) -> bool {
+        self.active_match_index.is_some()
+    }
+}
+
+impl ProjectSearchBar {
+    pub fn new() -> Self {
+        Self {
+            active_project_search: Default::default(),
+            subscription: Default::default(),
+        }
+    }
+
+    fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| search_view.search(cx));
+        }
+    }
+
+    fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
+        if let Some(search_view) = workspace
+            .active_item(cx)
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            let new_query = search_view.update(cx, |search_view, cx| {
+                let new_query = search_view.build_search_query(cx);
+                if new_query.is_some() {
+                    if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
+                        search_view.query_editor.update(cx, |editor, cx| {
+                            editor.set_text(old_query.as_str(), cx);
+                        });
+                        search_view.regex = old_query.is_regex();
+                        search_view.whole_word = old_query.whole_word();
+                        search_view.case_sensitive = old_query.case_sensitive();
+                    }
+                }
+                new_query
+            });
+            if let Some(new_query) = new_query {
+                let model = cx.add_model(|cx| {
+                    let mut model = ProjectSearch::new(workspace.project().clone(), cx);
+                    model.search(new_query, cx);
+                    model
+                });
+                workspace.add_item(
+                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
+                    cx,
+                );
+            }
+        }
+    }
+
+    fn select_match(
+        pane: &mut Pane,
+        &SelectMatch(direction): &SelectMatch,
+        cx: &mut ViewContext<Pane>,
+    ) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |search_view, cx| {
+                search_view.select_match(direction, cx);
+            });
         } else {
-            theme.search.editor.input.container
-        };
-        Flex::row()
-            .with_child(
-                ChildView::new(&self.query_editor)
-                    .contained()
-                    .with_style(editor_container)
-                    .aligned()
-                    .constrained()
-                    .with_max_width(theme.search.editor.max_width)
-                    .boxed(),
-            )
-            .with_child(
-                Flex::row()
-                    .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx))
-                    .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
-                    .with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
-                    .contained()
-                    .with_style(theme.search.option_button_group)
-                    .aligned()
-                    .boxed(),
-            )
-            .with_children({
-                self.active_match_index.into_iter().flat_map(|match_ix| {
-                    [
-                        Flex::row()
-                            .with_child(self.render_nav_button("<", Direction::Prev, cx))
-                            .with_child(self.render_nav_button(">", Direction::Next, cx))
-                            .aligned()
-                            .boxed(),
-                        Label::new(
-                            format!(
-                                "{}/{}",
-                                match_ix + 1,
-                                self.model.read(cx).match_ranges.len()
-                            ),
-                            theme.search.match_index.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.search.match_index.container)
-                        .aligned()
-                        .boxed(),
-                    ]
-                })
-            })
-            .contained()
-            .with_style(theme.search.container)
-            .constrained()
-            .with_height(theme.workspace.toolbar.height)
-            .named("project search")
+            cx.propagate_action();
+        }
+    }
+
+    fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |search_view, cx| {
+                if search_view.query_editor.is_focused(cx) {
+                    if !search_view.model.read(cx).match_ranges.is_empty() {
+                        search_view.focus_results_editor(cx);
+                    }
+                } else {
+                    search_view.focus_query_editor(cx);
+                }
+            });
+        } else {
+            cx.propagate_action();
+        }
+    }
+
+    fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                if search_view.query_editor.is_focused(cx) {
+                    if !search_view.model.read(cx).match_ranges.is_empty() {
+                        search_view.focus_results_editor(cx);
+                    }
+                } else {
+                    cx.propagate_action();
+                }
+            });
+        } else {
+            cx.propagate_action();
+        }
+    }
+
+    fn toggle_search_option(
+        &mut self,
+        ToggleSearchOption(option): &ToggleSearchOption,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                let value = match option {
+                    SearchOption::WholeWord => &mut search_view.whole_word,
+                    SearchOption::CaseSensitive => &mut search_view.case_sensitive,
+                    SearchOption::Regex => &mut search_view.regex,
+                };
+                *value = !*value;
+                search_view.search(cx);
+            });
+            cx.notify();
+        }
+    }
+
+    fn render_nav_button(
+        &self,
+        icon: &str,
+        direction: Direction,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        enum NavButton {}
+        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
+            let theme = &cx.global::<Settings>().theme.search;
+            let style = if state.hovered {
+                &theme.hovered_option_button
+            } else {
+                &theme.option_button
+            };
+            Label::new(icon.to_string(), style.text.clone())
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+        .with_cursor_style(CursorStyle::PointingHand)
+        .boxed()
     }
 
     fn render_option_button(
@@ -627,8 +641,8 @@ impl ProjectSearchView {
         option: SearchOption,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
-        let is_active = self.is_option_enabled(option);
-        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+        let is_active = self.is_option_enabled(option, cx);
+        MouseEventHandler::new::<ProjectSearchBar, _, _>(option as usize, cx, |state, cx| {
             let theme = &cx.global::<Settings>().theme.search;
             let style = match (is_active, state.hovered) {
                 (false, false) => &theme.option_button,
@@ -646,36 +660,121 @@ impl ProjectSearchView {
         .boxed()
     }
 
-    fn is_option_enabled(&self, option: SearchOption) -> bool {
-        match option {
-            SearchOption::WholeWord => self.whole_word,
-            SearchOption::CaseSensitive => self.case_sensitive,
-            SearchOption::Regex => self.regex,
+    fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
+        if let Some(search) = self.active_project_search.as_ref() {
+            let search = search.read(cx);
+            match option {
+                SearchOption::WholeWord => search.whole_word,
+                SearchOption::CaseSensitive => search.case_sensitive,
+                SearchOption::Regex => search.regex,
+            }
+        } else {
+            false
         }
     }
+}
 
-    fn render_nav_button(
-        &self,
-        icon: &str,
-        direction: Direction,
-        cx: &mut RenderContext<Self>,
-    ) -> ElementBox {
-        enum NavButton {}
-        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
-            let theme = &cx.global::<Settings>().theme.search;
-            let style = if state.hovered {
-                &theme.hovered_option_button
+impl Entity for ProjectSearchBar {
+    type Event = ();
+}
+
+impl View for ProjectSearchBar {
+    fn ui_name() -> &'static str {
+        "ProjectSearchBar"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        if let Some(search) = self.active_project_search.as_ref() {
+            let search = search.read(cx);
+            let theme = cx.global::<Settings>().theme.clone();
+            let editor_container = if search.query_contains_error {
+                theme.search.invalid_editor
             } else {
-                &theme.option_button
+                theme.search.editor.input.container
             };
-            Label::new(icon.to_string(), style.text.clone())
+            Flex::row()
+                .with_child(
+                    Flex::row()
+                        .with_child(
+                            ChildView::new(&search.query_editor)
+                                .aligned()
+                                .left()
+                                .flex(1., true)
+                                .boxed(),
+                        )
+                        .with_children(search.active_match_index.map(|match_ix| {
+                            Label::new(
+                                format!(
+                                    "{}/{}",
+                                    match_ix + 1,
+                                    search.model.read(cx).match_ranges.len()
+                                ),
+                                theme.search.match_index.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.search.match_index.container)
+                            .aligned()
+                            .boxed()
+                        }))
+                        .contained()
+                        .with_style(editor_container)
+                        .aligned()
+                        .constrained()
+                        .with_min_width(theme.search.editor.min_width)
+                        .with_max_width(theme.search.editor.max_width)
+                        .flex(1., false)
+                        .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_option_button(
+                            "Case",
+                            SearchOption::CaseSensitive,
+                            cx,
+                        ))
+                        .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
+                        .with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
+                        .contained()
+                        .with_style(theme.search.option_button_group)
+                        .aligned()
+                        .boxed(),
+                )
                 .contained()
-                .with_style(style.container)
-                .boxed()
-        })
-        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
-        .with_cursor_style(CursorStyle::PointingHand)
-        .boxed()
+                .with_style(theme.search.container)
+                .aligned()
+                .left()
+                .named("project search")
+        } else {
+            Empty::new().boxed()
+        }
+    }
+}
+
+impl ToolbarItemView for ProjectSearchBar {
+    fn set_active_pane_item(
+        &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 {
+                flex: Some((1., false)),
+            }
+        } else {
+            ToolbarItemLocation::Hidden
+        }
     }
 }
 
@@ -726,7 +825,7 @@ mod tests {
             search_view
                 .query_editor
                 .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
-            search_view.search(&Search, cx);
+            search_view.search(cx);
         });
         search_view.next_notification(&cx).await;
         search_view.update(cx, |search_view, cx| {
@@ -763,7 +862,7 @@ mod tests {
                 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
             );
 
-            search_view.select_match(&SelectMatch(Direction::Next), cx);
+            search_view.select_match(Direction::Next, cx);
         });
 
         search_view.update(cx, |search_view, cx| {
@@ -774,7 +873,7 @@ mod tests {
                     .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
             );
-            search_view.select_match(&SelectMatch(Direction::Next), cx);
+            search_view.select_match(Direction::Next, cx);
         });
 
         search_view.update(cx, |search_view, cx| {
@@ -785,7 +884,7 @@ mod tests {
                     .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
             );
-            search_view.select_match(&SelectMatch(Direction::Next), cx);
+            search_view.select_match(Direction::Next, cx);
         });
 
         search_view.update(cx, |search_view, cx| {
@@ -796,7 +895,7 @@ mod tests {
                     .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
             );
-            search_view.select_match(&SelectMatch(Direction::Prev), cx);
+            search_view.select_match(Direction::Prev, cx);
         });
 
         search_view.update(cx, |search_view, cx| {
@@ -807,7 +906,7 @@ mod tests {
                     .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
             );
-            search_view.select_match(&SelectMatch(Direction::Prev), cx);
+            search_view.select_match(Direction::Prev, cx);
         });
 
         search_view.update(cx, |search_view, cx| {

crates/search/src/search.rs 🔗

@@ -1,13 +1,14 @@
+pub use buffer_search::BufferSearchBar;
+use editor::{Anchor, MultiBufferSnapshot};
+use gpui::{action, MutableAppContext};
+pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
     ops::Range,
 };
 
-use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{action, MutableAppContext};
-
-mod buffer_search;
-mod project_search;
+pub mod buffer_search;
+pub mod project_search;
 
 pub fn init(cx: &mut MutableAppContext) {
     buffer_search::init(cx);

crates/text/src/anchor.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::Result;
 use std::{cmp::Ordering, fmt::Debug, ops::Range};
 use sum_tree::Bias;
 
-#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
     pub timestamp: clock::Local,
     pub offset: usize,

crates/theme/src/theme.rs 🔗

@@ -26,6 +26,7 @@ pub struct Theme {
     pub editor: Editor,
     pub search: Search,
     pub project_diagnostics: ProjectDiagnostics,
+    pub breadcrumbs: ContainedText,
 }
 
 #[derive(Deserialize, Default)]
@@ -94,7 +95,10 @@ pub struct Tab {
 
 #[derive(Clone, Deserialize, Default)]
 pub struct Toolbar {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
     pub height: f32,
+    pub item_spacing: f32,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -119,6 +123,7 @@ pub struct Search {
 pub struct FindEditor {
     #[serde(flatten)]
     pub input: FieldEditor,
+    pub min_width: f32,
     pub max_width: f32,
 }
 

crates/theme_selector/src/theme_selector.rs 🔗

@@ -310,7 +310,11 @@ impl View for ThemeSelector {
                                 .with_style(theme.selector.input_editor.container)
                                 .boxed(),
                         )
-                        .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
+                        .with_child(
+                            FlexItem::new(self.render_matches(cx))
+                                .flex(1., false)
+                                .boxed(),
+                        )
                         .boxed(),
                 )
                 .with_style(theme.selector.container)

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,5 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
 use collections::{HashMap, VecDeque};
 use gpui::{
     action,
@@ -7,16 +7,11 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
-    AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
 use project::{ProjectEntryId, ProjectPath};
-use std::{
-    any::{Any, TypeId},
-    cell::RefCell,
-    cmp, mem,
-    rc::Rc,
-};
+use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
 use util::ResultExt;
 
 action!(Split, SplitDirection);
@@ -101,28 +96,7 @@ pub struct Pane {
     items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
     nav_history: Rc<RefCell<NavHistory>>,
-    toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
-    active_toolbar_type: Option<TypeId>,
-    active_toolbar_visible: bool,
-}
-
-pub trait Toolbar: View {
-    fn active_item_changed(
-        &mut self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut ViewContext<Self>,
-    ) -> bool;
-    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
-}
-
-trait ToolbarHandle {
-    fn active_item_changed(
-        &self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut MutableAppContext,
-    ) -> bool;
-    fn on_dismiss(&self, cx: &mut MutableAppContext);
-    fn to_any(&self) -> AnyViewHandle;
+    toolbar: ViewHandle<Toolbar>,
 }
 
 pub struct ItemNavHistory {
@@ -158,14 +132,12 @@ pub struct NavigationEntry {
 }
 
 impl Pane {
-    pub fn new() -> Self {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
         Self {
             items: Vec::new(),
             active_item_index: 0,
             nav_history: Default::default(),
-            toolbars: Default::default(),
-            active_toolbar_type: Default::default(),
-            active_toolbar_visible: false,
+            toolbar: cx.add_view(|_| Toolbar::new()),
         }
     }
 
@@ -402,7 +374,7 @@ impl Pane {
                 self.items[prev_active_item_ix].deactivated(cx);
                 cx.emit(Event::ActivateItem { local });
             }
-            self.update_active_toolbar(cx);
+            self.update_toolbar(cx);
             if local {
                 self.focus_active_item(cx);
                 self.activate(cx);
@@ -487,7 +459,7 @@ impl Pane {
             self.focus_active_item(cx);
             self.activate(cx);
         }
-        self.update_active_toolbar(cx);
+        self.update_toolbar(cx);
 
         cx.notify();
     }
@@ -502,63 +474,18 @@ impl Pane {
         cx.emit(Event::Split(direction));
     }
 
-    pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
-    where
-        F: FnOnce(&mut ViewContext<V>) -> V,
-        V: Toolbar,
-    {
-        let type_id = TypeId::of::<V>();
-        if self.active_toolbar_type != Some(type_id) {
-            self.dismiss_toolbar(cx);
-
-            let active_item = self.active_item();
-            self.toolbars
-                .entry(type_id)
-                .or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
-
-            self.active_toolbar_type = Some(type_id);
-            self.active_toolbar_visible =
-                self.toolbars[&type_id].active_item_changed(active_item, cx);
-            cx.notify();
-        }
-    }
-
-    pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
-            self.toolbars
-                .get_mut(&active_toolbar_type)
-                .unwrap()
-                .on_dismiss(cx);
-            self.active_toolbar_visible = false;
-            self.focus_active_item(cx);
-            cx.notify();
-        }
+    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
+        &self.toolbar
     }
 
-    pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
-        self.toolbars
-            .get(&TypeId::of::<T>())
-            .and_then(|toolbar| toolbar.to_any().downcast())
-    }
-
-    pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
-        let type_id = self.active_toolbar_type?;
-        let toolbar = self.toolbars.get(&type_id)?;
-        if self.active_toolbar_visible {
-            Some(toolbar.to_any())
-        } else {
-            None
-        }
-    }
-
-    fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
-        let active_item = self.items.get(self.active_item_index);
-        for (toolbar_type_id, toolbar) in &self.toolbars {
-            let visible = toolbar.active_item_changed(active_item.cloned(), cx);
-            if Some(*toolbar_type_id) == self.active_toolbar_type {
-                self.active_toolbar_visible = visible;
-            }
-        }
+    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
+        let active_item = self
+            .items
+            .get(self.active_item_index)
+            .map(|item| item.as_ref());
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.set_active_pane_item(active_item, cx);
+        });
     }
 
     fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -685,7 +612,7 @@ impl Pane {
                 Empty::new()
                     .contained()
                     .with_border(theme.workspace.tab.container.border)
-                    .flexible(0., true)
+                    .flex(0., true)
                     .named("filler"),
             );
 
@@ -713,12 +640,8 @@ impl View for Pane {
         EventHandler::new(if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
-                .with_children(
-                    self.active_toolbar()
-                        .as_ref()
-                        .map(|view| ChildView::new(view).boxed()),
-                )
-                .with_child(ChildView::new(active_item).flexible(1., true).boxed())
+                .with_child(ChildView::new(&self.toolbar).boxed())
+                .with_child(ChildView::new(active_item).flex(1., true).boxed())
                 .boxed()
         } else {
             Empty::new().boxed()
@@ -740,24 +663,6 @@ impl View for Pane {
     }
 }
 
-impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
-    fn active_item_changed(
-        &self,
-        item: Option<Box<dyn ItemHandle>>,
-        cx: &mut MutableAppContext,
-    ) -> bool {
-        self.update(cx, |this, cx| this.active_item_changed(item, cx))
-    }
-
-    fn on_dismiss(&self, cx: &mut MutableAppContext) {
-        self.update(cx, |this, cx| this.on_dismiss(cx));
-    }
-
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
-    }
-}
-
 impl ItemNavHistory {
     pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
         Self {

crates/workspace/src/pane_group.rs 🔗

@@ -248,7 +248,7 @@ impl PaneAxis {
                     member = Container::new(member).with_border(border).boxed();
                 }
 
-                Flexible::new(1.0, true, member).boxed()
+                FlexItem::new(member).flex(1.0, true).boxed()
             }))
             .boxed()
     }

crates/workspace/src/sidebar.rs 🔗

@@ -138,7 +138,7 @@ impl Sidebar {
                     let width = self.width.clone();
                     move |size, _| *width.borrow_mut() = size.x()
                 })
-                .flexible(1., false)
+                .flex(1., false)
                 .boxed(),
             );
             if matches!(self.side, Side::Left) {

crates/workspace/src/status_bar.rs 🔗

@@ -47,12 +47,12 @@ impl View for StatusBar {
                     .with_margin_right(theme.item_spacing)
                     .boxed()
             }))
-            .with_child(Empty::new().flexible(1., true).boxed())
             .with_children(self.right_items.iter().map(|i| {
                 ChildView::new(i.as_ref())
                     .aligned()
                     .contained()
                     .with_margin_left(theme.item_spacing)
+                    .flex_float()
                     .boxed()
             }))
             .contained()

crates/workspace/src/toolbar.rs 🔗

@@ -0,0 +1,193 @@
+use crate::{ItemHandle, Settings};
+use gpui::{
+    elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
+    View, ViewContext, ViewHandle,
+};
+
+pub trait ToolbarItemView: View {
+    fn set_active_pane_item(
+        &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,
+        _cx: &AppContext,
+    ) -> 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)]
+pub enum ToolbarItemLocation {
+    Hidden,
+    PrimaryLeft { flex: Option<(f32, bool)> },
+    PrimaryRight { flex: Option<(f32, bool)> },
+    Secondary,
+}
+
+pub struct Toolbar {
+    active_pane_item: Option<Box<dyn ItemHandle>>,
+    items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
+}
+
+impl Entity for Toolbar {
+    type Event = ();
+}
+
+impl View for Toolbar {
+    fn ui_name() -> &'static str {
+        "Toolbar"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &cx.global::<Settings>().theme.workspace.toolbar;
+
+        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 { flex } => {
+                    let left_item = ChildView::new(item.as_ref())
+                        .aligned()
+                        .contained()
+                        .with_margin_right(theme.item_spacing);
+                    if let Some((flex, expanded)) = flex {
+                        primary_left_items.push(left_item.flex(flex, expanded).boxed());
+                    } else {
+                        primary_left_items.push(left_item.boxed());
+                    }
+                }
+                ToolbarItemLocation::PrimaryRight { flex } => {
+                    let right_item = ChildView::new(item.as_ref())
+                        .aligned()
+                        .contained()
+                        .with_margin_left(theme.item_spacing)
+                        .flex_float();
+                    if let Some((flex, expanded)) = flex {
+                        primary_right_items.push(right_item.flex(flex, expanded).boxed());
+                    } else {
+                        primary_right_items.push(right_item.boxed());
+                    }
+                }
+                ToolbarItemLocation::Secondary => {
+                    secondary_item = Some(
+                        ChildView::new(item.as_ref())
+                            .constrained()
+                            .with_height(theme.height)
+                            .boxed(),
+                    );
+                }
+            }
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::row()
+                    .with_children(primary_left_items)
+                    .with_children(primary_right_items)
+                    .constrained()
+                    .with_height(theme.height)
+                    .boxed(),
+            )
+            .with_children(secondary_item)
+            .contained()
+            .with_style(theme.container)
+            .boxed()
+    }
+}
+
+impl Toolbar {
+    pub fn new() -> Self {
+        Self {
+            active_pane_item: None,
+            items: Default::default(),
+        }
+    }
+
+    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+    where
+        T: 'static + ToolbarItemView,
+    {
+        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, cx);
+                if new_location != *current_location {
+                    *current_location = new_location;
+                    cx.notify();
+                }
+            }
+        })
+        .detach();
+        self.items.push((Box::new(item), location));
+        cx.notify();
+    }
+
+    pub fn set_active_pane_item(
+        &mut self,
+        pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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.items
+            .iter()
+            .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()
+    }
+
+    fn set_active_pane_item(
+        &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)
+        })
+    }
+}
+
+impl Into<AnyViewHandle> for &dyn ToolbarItemViewHandle {
+    fn into(self) -> AnyViewHandle {
+        self.to_any()
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -5,6 +5,7 @@ pub mod pane_group;
 pub mod settings;
 pub mod sidebar;
 mod status_bar;
+mod toolbar;
 
 use anyhow::{anyhow, Context, Result};
 use client::{
@@ -47,6 +48,7 @@ use std::{
     },
 };
 use theme::{Theme, ThemeRegistry};
+pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::ResultExt;
 
 type ProjectItemBuilders = HashMap<
@@ -650,6 +652,10 @@ impl WorkspaceParams {
     }
 }
 
+pub enum Event {
+    PaneAdded(ViewHandle<Pane>),
+}
+
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     client: Arc<Client>,
@@ -716,7 +722,7 @@ impl Workspace {
         })
         .detach();
 
-        let pane = cx.add_view(|_| Pane::new());
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);
@@ -729,6 +735,7 @@ impl Workspace {
         })
         .detach();
         cx.focus(&pane);
+        cx.emit(Event::PaneAdded(pane.clone()));
 
         let status_bar = cx.add_view(|cx| StatusBar::new(&pane, cx));
         let mut current_user = params.user_store.read(cx).watch_current_user().clone();
@@ -1047,7 +1054,7 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|_| Pane::new());
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);
@@ -1061,6 +1068,7 @@ impl Workspace {
         .detach();
         self.panes.push(pane.clone());
         self.activate_pane(pane.clone(), cx);
+        cx.emit(Event::PaneAdded(pane.clone()));
         pane
     }
 
@@ -1916,7 +1924,7 @@ impl Workspace {
 }
 
 impl Entity for Workspace {
-    type Event = ();
+    type Event = Event;
 }
 
 impl View for Workspace {
@@ -1938,36 +1946,35 @@ impl View for Workspace {
                                 if let Some(element) =
                                     self.left_sidebar.render_active_item(&theme, cx)
                                 {
-                                    content.add_child(Flexible::new(0.8, false, element).boxed());
+                                    content
+                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
                                 }
                                 content.add_child(
                                     Flex::column()
                                         .with_child(
-                                            Flexible::new(
-                                                1.,
-                                                true,
-                                                self.center.render(
-                                                    &theme,
-                                                    &self.follower_states_by_leader,
-                                                    self.project.read(cx).collaborators(),
-                                                ),
-                                            )
+                                            FlexItem::new(self.center.render(
+                                                &theme,
+                                                &self.follower_states_by_leader,
+                                                self.project.read(cx).collaborators(),
+                                            ))
+                                            .flex(1., true)
                                             .boxed(),
                                         )
                                         .with_child(ChildView::new(&self.status_bar).boxed())
-                                        .flexible(1., true)
+                                        .flex(1., true)
                                         .boxed(),
                                 );
                                 if let Some(element) =
                                     self.right_sidebar.render_active_item(&theme, cx)
                                 {
-                                    content.add_child(Flexible::new(0.8, false, element).boxed());
+                                    content
+                                        .add_child(FlexItem::new(element).flex(0.8, false).boxed());
                                 }
                                 content.add_child(self.right_sidebar.render(&theme, cx));
                                 content.boxed()
                             })
                             .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
-                            .flexible(1.0, true)
+                            .flex(1.0, true)
                             .boxed(),
                     )
                     .contained()

crates/zed/Cargo.toml 🔗

@@ -29,6 +29,7 @@ test-support = [
 ]
 
 [dependencies]
+breadcrumbs = { path = "../breadcrumbs" }
 chat_panel = { path = "../chat_panel" }
 collections = { path = "../collections" }
 client = { path = "../client" }

crates/zed/assets/themes/_base.toml 🔗

@@ -85,7 +85,15 @@ diagnostic_message = "$text.2"
 lsp_message = "$text.2"
 
 [workspace.toolbar]
-height = 44
+background = "$surface.1"
+border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
+height = 34
+item_spacing = 8
+padding = { left = 16, right = 8, top = 4, bottom = 4 }
+
+[breadcrumbs]
+extends = "$text.1"
+padding = { left = 6 }
 
 [panel]
 padding = { top = 12, left = 12, bottom = 12, right = 12 }
@@ -354,7 +362,6 @@ tab_summary_spacing = 10
 
 [search]
 match_background = "$state.highlighted_line"
-background = "$surface.1"
 results_status = { extends = "$text.0", size = 18 }
 tab_icon_width = 14
 tab_icon_spacing = 4
@@ -384,15 +391,16 @@ extends = "$search.option_button"
 background = "$surface.2"
 
 [search.match_index]
-extends = "$text.1"
+extends = "$text.2"
 padding = 6
 
 [search.editor]
-max_width = 400
+min_width = 200
+max_width = 500
 background = "$surface.0"
 corner_radius = 6
-padding = { left = 13, right = 13, top = 3, bottom = 3 }
-margin = { top = 5, bottom = 5, left = 5, right = 5 }
+padding = { left = 14, right = 14, top = 3, bottom = 3 }
+margin = { right = 5 }
 text = "$text.0"
 placeholder_text = "$text.2"
 selection = "$selection.host"

crates/zed/src/zed.rs 🔗

@@ -4,6 +4,7 @@ pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
+use breadcrumbs::Breadcrumbs;
 use chat_panel::ChatPanel;
 pub use client;
 pub use contacts_panel;
@@ -21,6 +22,7 @@ pub use lsp;
 use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
+use search::{BufferSearchBar, ProjectSearchBar};
 use std::{path::PathBuf, sync::Arc};
 pub use workspace;
 use workspace::{AppState, Settings, Workspace, WorkspaceParams};
@@ -104,6 +106,21 @@ pub fn build_workspace(
     app_state: &Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
 ) -> Workspace {
+    cx.subscribe(&cx.handle(), |_, _, event, cx| {
+        let workspace::Event::PaneAdded(pane) = event;
+        pane.update(cx, |pane, cx| {
+            pane.toolbar().update(cx, |toolbar, cx| {
+                let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
+                toolbar.add_item(breadcrumbs, cx);
+                let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
+                toolbar.add_item(buffer_search_bar, cx);
+                let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
+                toolbar.add_item(project_search_bar, cx);
+            })
+        });
+    })
+    .detach();
+
     let workspace_params = WorkspaceParams {
         project,
         client: app_state.client.clone(),