Merge pull request #2328 from zed-industries/breadcrumbs-outline-toggle

Julia created

Open symbol outline when clicking on editor breadcrumbs

Change summary

Cargo.lock                                |  1 
crates/breadcrumbs/Cargo.toml             |  1 
crates/breadcrumbs/src/breadcrumbs.rs     | 66 +++++++++++++++++++-----
crates/editor/src/items.rs                | 12 +++-
crates/terminal_view/src/terminal_view.rs |  2 
crates/theme/src/theme.rs                 |  3 
crates/workspace/src/pane.rs              | 10 +++
crates/workspace/src/toolbar.rs           | 13 ++++
styles/src/styleTree/app.ts               |  6 --
styles/src/styleTree/workspace.ts         | 17 +++++
10 files changed, 102 insertions(+), 29 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -785,6 +785,7 @@ dependencies = [
  "gpui",
  "itertools",
  "language",
+ "outline",
  "project",
  "search",
  "settings",

crates/breadcrumbs/Cargo.toml 🔗

@@ -18,6 +18,7 @@ search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+outline = { path = "../outline" }
 itertools = "0.10"
 
 [dev-dependencies]

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{
-    elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    elements::*, AppContext, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
+    ViewHandle,
 };
 use itertools::Itertools;
 use search::ProjectSearchView;
@@ -14,6 +15,7 @@ pub enum Event {
 }
 
 pub struct Breadcrumbs {
+    pane_focused: bool,
     active_item: Option<Box<dyn ItemHandle>>,
     project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -22,6 +24,7 @@ pub struct Breadcrumbs {
 impl Breadcrumbs {
     pub fn new() -> Self {
         Self {
+            pane_focused: false,
             active_item: Default::default(),
             subscription: Default::default(),
             project_search: Default::default(),
@@ -39,24 +42,53 @@ impl View for Breadcrumbs {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let active_item = match &self.active_item {
+            Some(active_item) => active_item,
+            None => return Empty::new().boxed(),
+        };
+        let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
         let theme = cx.global::<Settings>().theme.clone();
-        if let Some(breadcrumbs) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.breadcrumbs(&theme, cx))
-        {
-            Flex::row()
-                .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
-                    Label::new(" 〉 ", theme.breadcrumbs.text.clone()).boxed()
-                }))
-                .contained()
-                .with_style(theme.breadcrumbs.container)
+        let style = &theme.workspace.breadcrumbs;
+
+        let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
+            Some(breadcrumbs) => breadcrumbs,
+            None => return Empty::new().boxed(),
+        };
+
+        let crumbs = Flex::row()
+            .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
+                Label::new(" 〉 ", style.default.text.clone()).boxed()
+            }))
+            .constrained()
+            .with_height(theme.workspace.breadcrumb_height)
+            .contained();
+
+        if not_editor || !self.pane_focused {
+            return crumbs
+                .with_style(style.default.container)
                 .aligned()
                 .left()
-                .boxed()
-        } else {
-            Empty::new().boxed()
+                .boxed();
         }
+
+        MouseEventHandler::<Breadcrumbs>::new(0, cx, |state, _| {
+            let style = style.style_for(state, false);
+            crumbs.with_style(style.container).boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(outline::Toggle);
+        })
+        .with_tooltip::<Breadcrumbs, _>(
+            0,
+            "Show symbol outline".to_owned(),
+            Some(Box::new(outline::Toggle)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .left()
+        .boxed()
     }
 }
 
@@ -103,4 +135,8 @@ impl ToolbarItemView for Breadcrumbs {
             current_location
         }
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut gpui::MutableAppContext) {
+        self.pane_focused = pane_focused;
+    }
 }

crates/editor/src/items.rs 🔗

@@ -747,11 +747,15 @@ impl Item for Editor {
             .map(|path| path.to_string_lossy().to_string())
             .unwrap_or_else(|| "untitled".to_string());
 
-        let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
+        let filename_label = Label::new(filename, theme.workspace.breadcrumbs.default.text.clone());
+        let mut breadcrumbs = vec![filename_label.boxed()];
         breadcrumbs.extend(symbols.into_iter().map(|symbol| {
-            Text::new(symbol.text, theme.breadcrumbs.text.clone())
-                .with_highlights(symbol.highlight_ranges)
-                .boxed()
+            Text::new(
+                symbol.text,
+                theme.workspace.breadcrumbs.default.text.clone(),
+            )
+            .with_highlights(symbol.highlight_ranges)
+            .boxed()
         }));
         Some(breadcrumbs)
     }

crates/terminal_view/src/terminal_view.rs 🔗

@@ -612,7 +612,7 @@ impl Item for TerminalView {
     fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
         Some(vec![Text::new(
             self.terminal().read(cx).breadcrumb_text.clone(),
-            theme.breadcrumbs.text.clone(),
+            theme.workspace.breadcrumbs.default.text.clone(),
         )
         .boxed()])
     }

crates/theme/src/theme.rs 🔗

@@ -30,7 +30,6 @@ pub struct Theme {
     pub editor: Editor,
     pub search: Search,
     pub project_diagnostics: ProjectDiagnostics,
-    pub breadcrumbs: ContainedText,
     pub shared_screen: ContainerStyle,
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
@@ -62,6 +61,8 @@ pub struct Workspace {
     pub sidebar: Sidebar,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
+    pub breadcrumb_height: f32,
+    pub breadcrumbs: Interactive<ContainedText>,
     pub disconnected_overlay: ContainedText,
     pub modal: ContainerStyle,
     pub notification: ContainerStyle,

crates/workspace/src/pane.rs 🔗

@@ -1603,6 +1603,10 @@ impl View for Pane {
     }
 
     fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(true, cx);
+        });
+
         if let Some(active_item) = self.active_item() {
             if cx.is_self_focused() {
                 // Pane was focused directly. We need to either focus a view inside the active item,
@@ -1626,6 +1630,12 @@ impl View for Pane {
         }
     }
 
+    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(false, cx);
+        });
+    }
+
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut keymap = Self::default_keymap_context();
         if self.docked.is_some() {

crates/workspace/src/toolbar.rs 🔗

@@ -20,6 +20,8 @@ pub trait ToolbarItemView: View {
     ) -> ToolbarItemLocation {
         current_location
     }
+
+    fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut MutableAppContext) {}
 }
 
 trait ToolbarItemViewHandle {
@@ -30,6 +32,7 @@ trait ToolbarItemViewHandle {
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
     ) -> ToolbarItemLocation;
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext);
 }
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -260,6 +263,12 @@ impl Toolbar {
         }
     }
 
+    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        for (toolbar_item, _) in self.items.iter_mut() {
+            toolbar_item.pane_focus_update(pane_focused, cx);
+        }
+    }
+
     pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
         self.items
             .iter()
@@ -289,6 +298,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
             this.set_active_pane_item(active_pane_item, cx)
         })
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
+    }
 }
 
 impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {

styles/src/styleTree/app.ts 🔗

@@ -44,12 +44,6 @@ export default function app(colorScheme: ColorScheme): Object {
         contactList: contactList(colorScheme),
         search: search(colorScheme),
         sharedScreen: sharedScreen(colorScheme),
-        breadcrumbs: {
-            ...text(colorScheme.highest, "sans", "variant"),
-            padding: {
-                left: 6,
-            },
-        },
         updateNotification: updateNotification(colorScheme),
         simpleMessageNotification: simpleMessageNotification(colorScheme),
         tooltip: tooltip(colorScheme),

styles/src/styleTree/workspace.ts 🔗

@@ -276,9 +276,22 @@ export default function workspace(colorScheme: ColorScheme) {
             },
             padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
+        breadcrumbHeight: 24,
         breadcrumbs: {
-            ...text(layer, "mono", "variant"),
-            padding: { left: 6 },
+            ...text(colorScheme.highest, "sans", "variant"),
+            cornerRadius: 6,
+            padding: {
+                left: 6,
+                right: 6,
+            },
+            hover: {
+                color: foreground(colorScheme.highest, "on", "hovered"),
+                background: background(
+                    colorScheme.highest,
+                    "on",
+                    "hovered"
+                ),
+            },
         },
         disconnectedOverlay: {
             ...text(layer, "sans"),