Show breadcrumbs in the toolbar

Antonio Scandurra created

Change summary

Cargo.lock                            | 13 +++
crates/breadcrumbs/Cargo.toml         | 21 ++++++
crates/breadcrumbs/src/breadcrumbs.rs | 99 +++++++++++++++++++++++++++++
crates/theme/src/theme.rs             |  6 +
crates/zed/Cargo.toml                 |  1 
crates/zed/assets/themes/_base.toml   |  3 
crates/zed/src/zed.rs                 |  4 +
7 files changed, 147 insertions(+)

Detailed changes

Cargo.lock 🔗

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

crates/breadcrumbs/Cargo.toml 🔗

@@ -0,0 +1,21 @@
+[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" }
+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,99 @@
+use editor::{Anchor, Editor};
+use gpui::{
+    elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
+};
+use language::{BufferSnapshot, OutlineItem};
+use std::borrow::Cow;
+use theme::SyntaxTheme;
+use workspace::{ItemHandle, Settings, ToolbarItemView};
+
+pub struct Breadcrumbs {
+    editor: Option<ViewHandle<Editor>>,
+    editor_subscription: Option<Subscription>,
+}
+
+impl Breadcrumbs {
+    pub fn new() -> Self {
+        Self {
+            editor: Default::default(),
+            editor_subscription: 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))?;
+        if buffer.path().is_none() && symbols.is_empty() {
+            None
+        } else {
+            Some((buffer, symbols))
+        }
+    }
+}
+
+impl Entity for Breadcrumbs {
+    type 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(),
+                ]
+            }))
+            .boxed()
+    }
+}
+
+impl ToolbarItemView for Breadcrumbs {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor_subscription = None;
+        self.editor = None;
+        if let Some(editor) = active_pane_item.and_then(|i| i.act_as::<Editor>(cx)) {
+            self.editor_subscription = Some(cx.subscribe(&editor, |_, _, event, cx| match event {
+                editor::Event::BufferEdited => cx.notify(),
+                editor::Event::SelectionsChanged { local } if *local => cx.notify(),
+                _ => {}
+            }));
+            self.editor = Some(editor);
+        }
+        cx.notify();
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -26,6 +26,7 @@ pub struct Theme {
     pub editor: Editor,
     pub search: Search,
     pub project_diagnostics: ProjectDiagnostics,
+    pub breadcrumbs: Breadcrumbs,
 }
 
 #[derive(Deserialize, Default)]
@@ -271,6 +272,11 @@ pub struct ProjectDiagnostics {
     pub tab_summary_spacing: f32,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct Breadcrumbs {
+    pub text: TextStyle,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct Editor {
     pub text_color: Color,

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 🔗

@@ -92,6 +92,9 @@ item_spacing = 8
 padding.left = 8
 padding.right = 8
 
+[breadcrumbs]
+text = "$text.1"
+
 [panel]
 padding = { top = 12, left = 12, bottom = 12, right = 12 }
 

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;
@@ -109,6 +110,9 @@ pub fn build_workspace(
         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_left_item(breadcrumbs, cx);
+
                 let search_bar = cx.add_view(|cx| SearchBar::new(cx));
                 toolbar.add_right_item(search_bar, cx);
             })