breadcrumbs.rs

  1use editor::Editor;
  2use gpui::{
  3    Context, Element, EventEmitter, Focusable, FontWeight, IntoElement, ParentElement, Render,
  4    StyledText, Subscription, Window,
  5};
  6use itertools::Itertools;
  7use settings::Settings;
  8use std::{any::Any, cmp};
  9use theme::ActiveTheme;
 10use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*};
 11use workspace::{
 12    TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 13    item::{BreadcrumbText, ItemBufferKind, ItemEvent, ItemHandle},
 14};
 15
 16pub struct Breadcrumbs {
 17    pane_focused: bool,
 18    active_item: Option<Box<dyn ItemHandle>>,
 19    subscription: Option<Subscription>,
 20}
 21
 22impl Default for Breadcrumbs {
 23    fn default() -> Self {
 24        Self::new()
 25    }
 26}
 27
 28impl Breadcrumbs {
 29    pub fn new() -> Self {
 30        Self {
 31            pane_focused: false,
 32            active_item: Default::default(),
 33            subscription: Default::default(),
 34        }
 35    }
 36}
 37
 38impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 39
 40impl Render for Breadcrumbs {
 41    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 42        const MAX_SEGMENTS: usize = 12;
 43
 44        let element = h_flex()
 45            .id("breadcrumb-container")
 46            .flex_grow()
 47            .overflow_x_scroll()
 48            .text_ui(cx);
 49
 50        let Some(active_item) = self.active_item.as_ref() else {
 51            return element;
 52        };
 53
 54        // Begin - logic we should copy/move
 55        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 56            return element;
 57        };
 58
 59        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 60        let suffix_start_ix = cmp::max(
 61            prefix_end_ix,
 62            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 63        );
 64
 65        if suffix_start_ix > prefix_end_ix {
 66            segments.splice(
 67                prefix_end_ix..suffix_start_ix,
 68                Some(BreadcrumbText {
 69                    text: "".into(),
 70                    highlights: None,
 71                    font: None,
 72                }),
 73            );
 74        }
 75
 76        let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
 77            let mut text_style = window.text_style();
 78            if let Some(ref font) = segment.font {
 79                text_style.font_family = font.family.clone();
 80                text_style.font_features = font.features.clone();
 81                text_style.font_style = font.style;
 82                text_style.font_weight = font.weight;
 83            }
 84            text_style.color = Color::Muted.color(cx);
 85
 86            if index == 0
 87                && !TabBarSettings::get_global(cx).show
 88                && active_item.is_dirty(cx)
 89                && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
 90            {
 91                return styled_element;
 92            }
 93
 94            StyledText::new(segment.text.replace('\n', ""))
 95                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
 96                .into_any()
 97        });
 98        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 99            Label::new("").color(Color::Placeholder).into_any_element()
100        });
101
102        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
103
104        let prefix_element = active_item.breadcrumb_prefix(window, cx);
105
106        let breadcrumbs = if let Some(prefix) = prefix_element {
107            h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
108        } else {
109            breadcrumbs_stack
110        };
111
112        match active_item
113            .downcast::<Editor>()
114            .map(|editor| editor.downgrade())
115        {
116            Some(editor) => element.child(
117                ButtonLike::new("toggle outline view")
118                    .child(breadcrumbs)
119                    .style(ButtonStyle::Transparent)
120                    .on_click({
121                        let editor = editor.clone();
122                        move |_, window, cx| {
123                            if let Some((editor, callback)) = editor
124                                .upgrade()
125                                .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
126                            {
127                                callback(editor.to_any_view(), window, cx);
128                            }
129                        }
130                    })
131                    .tooltip(move |_window, cx| {
132                        if let Some(editor) = editor.upgrade() {
133                            let focus_handle = editor.read(cx).focus_handle(cx);
134                            Tooltip::for_action_in(
135                                "Show Symbol Outline",
136                                &zed_actions::outline::ToggleOutline,
137                                &focus_handle,
138                                cx,
139                            )
140                        } else {
141                            Tooltip::for_action(
142                                "Show Symbol Outline",
143                                &zed_actions::outline::ToggleOutline,
144                                cx,
145                            )
146                        }
147                    }),
148            ),
149            None => element
150                // Match the height and padding of the `ButtonLike` in the other arm.
151                .h(rems_from_px(22.))
152                .pl_1()
153                .child(breadcrumbs),
154        }
155        // End
156    }
157}
158
159impl ToolbarItemView for Breadcrumbs {
160    fn set_active_pane_item(
161        &mut self,
162        active_pane_item: Option<&dyn ItemHandle>,
163        window: &mut Window,
164        cx: &mut Context<Self>,
165    ) -> ToolbarItemLocation {
166        cx.notify();
167        self.active_item = None;
168
169        let Some(item) = active_pane_item else {
170            return ToolbarItemLocation::Hidden;
171        };
172
173        let this = cx.entity().downgrade();
174        self.subscription = Some(item.subscribe_to_item_events(
175            window,
176            cx,
177            Box::new(move |event, _, cx| {
178                if let ItemEvent::UpdateBreadcrumbs = event {
179                    this.update(cx, |this, cx| {
180                        cx.notify();
181                        if let Some(active_item) = this.active_item.as_ref() {
182                            cx.emit(ToolbarItemEvent::ChangeLocation(
183                                active_item.breadcrumb_location(cx),
184                            ))
185                        }
186                    })
187                    .ok();
188                }
189            }),
190        ));
191        self.active_item = Some(item.boxed_clone());
192        item.breadcrumb_location(cx)
193    }
194
195    fn pane_focus_update(
196        &mut self,
197        pane_focused: bool,
198        _window: &mut Window,
199        _: &mut Context<Self>,
200    ) {
201        self.pane_focused = pane_focused;
202    }
203}
204
205fn apply_dirty_filename_style(
206    segment: &BreadcrumbText,
207    text_style: &gpui::TextStyle,
208    cx: &mut Context<Breadcrumbs>,
209) -> Option<gpui::AnyElement> {
210    let text = segment.text.replace('\n', "");
211
212    let filename_position = std::path::Path::new(&segment.text)
213        .file_name()
214        .and_then(|f| {
215            let filename_str = f.to_string_lossy();
216            segment.text.rfind(filename_str.as_ref())
217        })?;
218
219    let bold_weight = FontWeight::BOLD;
220    let default_color = Color::Default.color(cx);
221
222    if filename_position == 0 {
223        let mut filename_style = text_style.clone();
224        filename_style.font_weight = bold_weight;
225        filename_style.color = default_color;
226
227        return Some(
228            StyledText::new(text)
229                .with_default_highlights(&filename_style, [])
230                .into_any(),
231        );
232    }
233
234    let highlight_style = gpui::HighlightStyle {
235        font_weight: Some(bold_weight),
236        color: Some(default_color),
237        ..Default::default()
238    };
239
240    let highlight = vec![(filename_position..text.len(), highlight_style)];
241    Some(
242        StyledText::new(text)
243            .with_default_highlights(text_style, highlight)
244            .into_any(),
245    )
246}