Dedupe rendering logic for breadcrumbtext segments.

KyleBarton created

Change summary

crates/breadcrumbs/src/breadcrumbs.rs | 180 +------------------
crates/editor/src/editor.rs           |   1 
crates/editor/src/element.rs          | 259 +++++++++++++++++-----------
crates/workspace/src/item.rs          |   1 
4 files changed, 173 insertions(+), 268 deletions(-)

Detailed changes

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,16 +1,10 @@
-use editor::Editor;
-use gpui::{
-    Context, Element, EventEmitter, Focusable, FontWeight, IntoElement, ParentElement, Render,
-    StyledText, Subscription, Window,
-};
-use itertools::Itertools;
-use settings::Settings;
-use std::cmp;
+use editor::render_breadcrumb_text;
+use gpui::{Context, EventEmitter, IntoElement, Render, Subscription, Window};
 use theme::ActiveTheme;
-use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*};
+use ui::prelude::*;
 use workspace::{
-    TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
-    item::{BreadcrumbText, ItemEvent, ItemHandle},
+    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
+    item::{ItemEvent, ItemHandle},
 };
 
 pub struct Breadcrumbs {
@@ -37,129 +31,30 @@ impl Breadcrumbs {
 
 impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 
-// Potential idea:
-// - Rename this to "BreadcrumbToolbar" or something
-// - Create a wrapping "Breadcrumb" struct for Vec<BreadcrumbText>
-// - Implement render for _that_ breadcrumb struct.
-// - Call that from here to eliminate much of the logic.
-// - This will change the Item interface, so do it only after you're happy with the features thus far
 impl Render for Breadcrumbs {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        const MAX_SEGMENTS: usize = 12;
-
         let element = h_flex()
             .id("breadcrumb-container")
             .flex_grow()
             .h_8()
             .overflow_x_scroll()
             .text_ui(cx);
-
         let Some(active_item) = self.active_item.as_ref() else {
-            return element;
+            return element.into_any_element();
         };
 
-        // Begin - logic we should copy/move
-        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
-            return element;
+        let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
+            return element.into_any_element();
         };
-
-        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
-        let suffix_start_ix = cmp::max(
-            prefix_end_ix,
-            segments.len().saturating_sub(MAX_SEGMENTS / 2),
-        );
-
-        if suffix_start_ix > prefix_end_ix {
-            segments.splice(
-                prefix_end_ix..suffix_start_ix,
-                Some(BreadcrumbText {
-                    text: "⋯".into(),
-                    highlights: None,
-                    font: None,
-                }),
-            );
-        }
-
-        let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
-            let mut text_style = window.text_style();
-            if let Some(ref font) = segment.font {
-                text_style.font_family = font.family.clone();
-                text_style.font_features = font.features.clone();
-                text_style.font_style = font.style;
-                text_style.font_weight = font.weight;
-            }
-            text_style.color = Color::Muted.color(cx);
-
-            if index == 0
-                && !TabBarSettings::get_global(cx).show
-                && active_item.is_dirty(cx)
-                && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
-            {
-                return styled_element;
-            }
-
-            StyledText::new(segment.text.replace('\n', "⏎"))
-                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
-                .into_any()
-        });
-        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
-            Label::new("›").color(Color::Placeholder).into_any_element()
-        });
-
-        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
-
         let prefix_element = active_item.breadcrumb_prefix(window, cx);
-
-        let breadcrumbs = if let Some(prefix) = prefix_element {
-            h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
-        } else {
-            breadcrumbs_stack
-        };
-
-        match active_item
-            .downcast::<Editor>()
-            .map(|editor| editor.downgrade())
-        {
-            Some(editor) => element.child(
-                ButtonLike::new("toggle outline view")
-                    .child(breadcrumbs)
-                    .style(ButtonStyle::Transparent)
-                    .on_click({
-                        let editor = editor.clone();
-                        move |_, window, cx| {
-                            if let Some((editor, callback)) = editor
-                                .upgrade()
-                                .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
-                            {
-                                callback(editor.to_any_view(), window, cx);
-                            }
-                        }
-                    })
-                    .tooltip(move |_window, cx| {
-                        if let Some(editor) = editor.upgrade() {
-                            let focus_handle = editor.read(cx).focus_handle(cx);
-                            Tooltip::for_action_in(
-                                "Show Symbol Outline",
-                                &zed_actions::outline::ToggleOutline,
-                                &focus_handle,
-                                cx,
-                            )
-                        } else {
-                            Tooltip::for_action(
-                                "Show Symbol Outline",
-                                &zed_actions::outline::ToggleOutline,
-                                cx,
-                            )
-                        }
-                    }),
-            ),
-            None => element
-                // Match the height and padding of the `ButtonLike` in the other arm.
-                .h(rems_from_px(22.))
-                .pl_1()
-                .child(breadcrumbs),
-        }
-        // End
+        render_breadcrumb_text(
+            segments,
+            prefix_element,
+            Box::new(active_item.as_ref()),
+            window,
+            cx,
+        )
+        .into_any_element()
     }
 }
 
@@ -208,46 +103,3 @@ impl ToolbarItemView for Breadcrumbs {
         self.pane_focused = pane_focused;
     }
 }
-
-fn apply_dirty_filename_style(
-    segment: &BreadcrumbText,
-    text_style: &gpui::TextStyle,
-    cx: &mut Context<Breadcrumbs>,
-) -> Option<gpui::AnyElement> {
-    let text = segment.text.replace('\n', "⏎");
-
-    let filename_position = std::path::Path::new(&segment.text)
-        .file_name()
-        .and_then(|f| {
-            let filename_str = f.to_string_lossy();
-            segment.text.rfind(filename_str.as_ref())
-        })?;
-
-    let bold_weight = FontWeight::BOLD;
-    let default_color = Color::Default.color(cx);
-
-    if filename_position == 0 {
-        let mut filename_style = text_style.clone();
-        filename_style.font_weight = bold_weight;
-        filename_style.color = default_color;
-
-        return Some(
-            StyledText::new(text)
-                .with_default_highlights(&filename_style, [])
-                .into_any(),
-        );
-    }
-
-    let highlight_style = gpui::HighlightStyle {
-        font_weight: Some(bold_weight),
-        color: Some(default_color),
-        ..Default::default()
-    };
-
-    let highlight = vec![(filename_position..text.len(), highlight_style)];
-    Some(
-        StyledText::new(text)
-            .with_default_highlights(text_style, highlight)
-            .into_any(),
-    )
-}

crates/editor/src/editor.rs 🔗

@@ -58,6 +58,7 @@ pub use editor_settings::{
 };
 pub use element::{
     CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
+    render_breadcrumb_text,
 };
 pub use git::blame::BlameRenderer;
 pub use hover_popover::hover_markdown_style;

crates/editor/src/element.rs 🔗

@@ -41,7 +41,7 @@ use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatu
 use gpui::{
     Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
     Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
-    DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
+    DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, FontWeight,
     GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
     KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,
     MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,
@@ -3880,7 +3880,7 @@ impl EditorElement {
         let editor = self.editor.read(cx);
         let multi_buffer = editor.buffer.read(cx);
         let is_read_only = self.editor.read(cx).read_only(cx);
-        let weak_editor = self.editor.downgrade();
+        let editor_handle: Box<&dyn ItemHandle> = Box::new(&self.editor);
 
         let breadcrumbs = if is_selected {
             editor.breadcrumbs_inner(cx.theme(), cx)
@@ -4100,10 +4100,10 @@ impl EditorElement {
                                         )
                                     })
                                     .when_some(breadcrumbs, |then, breadcrumbs| {
-                                        then.child(self.render_breadcrumb_text(
+                                        then.child(render_breadcrumb_text(
                                             breadcrumbs,
-                                            None, // TODO gotta figure this out somehow
-                                            weak_editor,
+                                            None,
+                                            editor_handle,
                                             window,
                                             cx,
                                         ))
@@ -4261,105 +4261,6 @@ impl EditorElement {
             })
     }
 
-    // TODO This has too much code in common with Breadcrumb::render. We should find a way to DRY it.
-    fn render_breadcrumb_text(
-        &self,
-        mut segments: Vec<BreadcrumbText>,
-        prefix: Option<gpui::AnyElement>,
-        editor: WeakEntity<Editor>,
-        window: &mut Window,
-        cx: &App,
-    ) -> impl IntoElement {
-        const MAX_SEGMENTS: usize = 12;
-
-        let element = h_flex().id("breadcrumb-container").flex_grow().text_ui(cx);
-
-        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
-        let suffix_start_ix = cmp::max(
-            prefix_end_ix,
-            segments.len().saturating_sub(MAX_SEGMENTS / 2),
-        );
-
-        if suffix_start_ix > prefix_end_ix {
-            segments.splice(
-                prefix_end_ix..suffix_start_ix,
-                Some(BreadcrumbText {
-                    text: "⋯".into(),
-                    highlights: None,
-                    font: None,
-                }),
-            );
-        }
-
-        let highlighted_segments = segments.into_iter().enumerate().map(|(_index, segment)| {
-            let mut text_style = window.text_style();
-            if let Some(ref font) = segment.font {
-                text_style.font_family = font.family.clone();
-                text_style.font_features = font.features.clone();
-                text_style.font_style = font.style;
-                text_style.font_weight = font.weight;
-            }
-            text_style.color = Color::Muted.color(cx);
-
-            // TODO this shouldn't apply here, but will in the formal breadcrumb (e.g. singleton buffer). Need to resolve the difference.
-            // if index == 0
-            //     && !TabBarSettings::get_global(cx).show
-            //     && active_item.is_dirty(cx)
-            //     && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
-            // {
-            //     return styled_element;
-            // }
-
-            StyledText::new(segment.text.replace('\n', "⏎"))
-                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
-                .into_any()
-        });
-        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
-            Label::new("›").color(Color::Placeholder).into_any_element()
-        });
-
-        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
-
-        let breadcrumbs = if let Some(prefix) = prefix {
-            h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
-        } else {
-            breadcrumbs_stack
-        };
-        element.child(
-            ButtonLike::new("toggle outline view")
-                .child(breadcrumbs)
-                .style(ButtonStyle::Transparent)
-                .on_click({
-                    let editor = editor.clone();
-                    move |_, window, cx| {
-                        if let Some((editor, callback)) = editor
-                            .upgrade()
-                            .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
-                        {
-                            callback(editor.to_any_view(), window, cx);
-                        }
-                    }
-                })
-                .tooltip(move |_window, cx| {
-                    if let Some(editor) = editor.upgrade() {
-                        let focus_handle = editor.read(cx).focus_handle(cx);
-                        Tooltip::for_action_in(
-                            "Show Symbol Outline",
-                            &zed_actions::outline::ToggleOutline,
-                            &focus_handle,
-                            cx,
-                        )
-                    } else {
-                        Tooltip::for_action(
-                            "Show Symbol Outline",
-                            &zed_actions::outline::ToggleOutline,
-                            cx,
-                        )
-                    }
-                }),
-        )
-    }
-
     fn render_blocks(
         &self,
         rows: Range<DisplayRow>,
@@ -7997,6 +7898,156 @@ impl EditorElement {
     }
 }
 
+pub fn render_breadcrumb_text(
+    mut segments: Vec<BreadcrumbText>,
+    prefix: Option<gpui::AnyElement>,
+    active_item: Box<&dyn ItemHandle>,
+    window: &mut Window,
+    cx: &App,
+) -> impl IntoElement {
+    const MAX_SEGMENTS: usize = 12;
+
+    let element = h_flex().id("breadcrumb-container").flex_grow().text_ui(cx);
+
+    let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
+    let suffix_start_ix = cmp::max(
+        prefix_end_ix,
+        segments.len().saturating_sub(MAX_SEGMENTS / 2),
+    );
+
+    if suffix_start_ix > prefix_end_ix {
+        segments.splice(
+            prefix_end_ix..suffix_start_ix,
+            Some(BreadcrumbText {
+                text: "⋯".into(),
+                highlights: None,
+                font: None,
+            }),
+        );
+    }
+
+    let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
+        let mut text_style = window.text_style();
+        if let Some(ref font) = segment.font {
+            text_style.font_family = font.family.clone();
+            text_style.font_features = font.features.clone();
+            text_style.font_style = font.style;
+            text_style.font_weight = font.weight;
+        }
+        text_style.color = Color::Muted.color(cx);
+
+        if index == 0
+            && !workspace::TabBarSettings::get_global(cx).show
+            && active_item.is_dirty(cx)
+            && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
+        {
+            return styled_element;
+        }
+
+        StyledText::new(segment.text.replace('\n', "⏎"))
+            .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
+            .into_any()
+    });
+    let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
+        Label::new("›").color(Color::Placeholder).into_any_element()
+    });
+
+    let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
+
+    let breadcrumbs = if let Some(prefix) = prefix {
+        h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
+    } else {
+        breadcrumbs_stack
+    };
+
+    match active_item
+        .downcast::<Editor>()
+        .map(|editor| editor.downgrade())
+    {
+        Some(editor) => element.child(
+            ButtonLike::new("toggle outline view")
+                .child(breadcrumbs)
+                .style(ButtonStyle::Transparent)
+                .on_click({
+                    let editor = editor.clone();
+                    move |_, window, cx| {
+                        if let Some((editor, callback)) = editor
+                            .upgrade()
+                            .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
+                        {
+                            callback(editor.to_any_view(), window, cx);
+                        }
+                    }
+                })
+                .tooltip(move |_window, cx| {
+                    if let Some(editor) = editor.upgrade() {
+                        let focus_handle = editor.read(cx).focus_handle(cx);
+                        Tooltip::for_action_in(
+                            "Show Symbol Outline",
+                            &zed_actions::outline::ToggleOutline,
+                            &focus_handle,
+                            cx,
+                        )
+                    } else {
+                        Tooltip::for_action(
+                            "Show Symbol Outline",
+                            &zed_actions::outline::ToggleOutline,
+                            cx,
+                        )
+                    }
+                }),
+        ),
+        None => element
+            // Match the height and padding of the `ButtonLike` in the other arm.
+            .h(rems_from_px(22.))
+            .pl_1()
+            .child(breadcrumbs),
+    }
+}
+
+fn apply_dirty_filename_style(
+    segment: &BreadcrumbText,
+    text_style: &gpui::TextStyle,
+    cx: &App,
+) -> Option<gpui::AnyElement> {
+    let text = segment.text.replace('\n', "⏎");
+
+    let filename_position = std::path::Path::new(&segment.text)
+        .file_name()
+        .and_then(|f| {
+            let filename_str = f.to_string_lossy();
+            segment.text.rfind(filename_str.as_ref())
+        })?;
+
+    let bold_weight = FontWeight::BOLD;
+    let default_color = Color::Default.color(cx);
+
+    if filename_position == 0 {
+        let mut filename_style = text_style.clone();
+        filename_style.font_weight = bold_weight;
+        filename_style.color = default_color;
+
+        return Some(
+            StyledText::new(text)
+                .with_default_highlights(&filename_style, [])
+                .into_any(),
+        );
+    }
+
+    let highlight_style = gpui::HighlightStyle {
+        font_weight: Some(bold_weight),
+        color: Some(default_color),
+        ..Default::default()
+    };
+
+    let highlight = vec![(filename_position..text.len(), highlight_style)];
+    Some(
+        StyledText::new(text)
+            .with_default_highlights(text_style, highlight)
+            .into_any(),
+    )
+}
+
 fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
     file_status.map_or(Color::Default, |status| {
         if status.is_conflicted() {

crates/workspace/src/item.rs 🔗

@@ -124,6 +124,7 @@ pub enum ItemEvent {
 }
 
 // TODO: Combine this with existing HighlightedText struct?
+#[derive(Debug)]
 pub struct BreadcrumbText {
     pub text: String,
     pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,