WIP pull breadcrumb rendering out into item trait

K Simmons created

Change summary

Cargo.lock                                     |   1 
crates/breadcrumbs/Cargo.toml                  |   1 
crates/breadcrumbs/src/breadcrumbs.rs          | 113 +++++++++--------
crates/diagnostics/src/diagnostics.rs          |   8 
crates/editor/src/items.rs                     |  22 +--
crates/search/src/buffer_search.rs             |  21 +-
crates/search/src/project_search.rs            |  17 +-
crates/terminal/src/terminal_container_view.rs |  18 +-
crates/workspace/src/searchable.rs             |   4 
crates/workspace/src/workspace.rs              | 130 +++++++++++--------
10 files changed, 176 insertions(+), 159 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -613,6 +613,7 @@ dependencies = [
  "collections",
  "editor",
  "gpui",
+ "itertools",
  "language",
  "project",
  "search",

crates/breadcrumbs/Cargo.toml 🔗

@@ -17,6 +17,7 @@ search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+itertools = "0.10"
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,13 +1,12 @@
-use editor::{Anchor, Editor};
+use editor::Editor;
 use gpui::{
     elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
     ViewHandle,
 };
-use language::{Buffer, OutlineItem};
+use itertools::Itertools;
 use project::Project;
 use search::ProjectSearchView;
 use settings::Settings;
-use theme::SyntaxTheme;
 use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub enum Event {
@@ -16,7 +15,7 @@ pub enum Event {
 
 pub struct Breadcrumbs {
     project: ModelHandle<Project>,
-    editor: Option<ViewHandle<Editor>>,
+    active_item: Option<Box<dyn ItemHandle>>,
     project_search: Option<ViewHandle<ProjectSearchView>>,
     subscriptions: Vec<Subscription>,
 }
@@ -25,24 +24,23 @@ impl Breadcrumbs {
     pub fn new(project: ModelHandle<Project>) -> Self {
         Self {
             project,
-            editor: Default::default(),
+            active_item: Default::default(),
             subscriptions: Default::default(),
             project_search: Default::default(),
         }
     }
-
-    fn active_symbols(
-        &self,
-        theme: &SyntaxTheme,
-        cx: &AppContext,
-    ) -> Option<(ModelHandle<Buffer>, Vec<OutlineItem<Anchor>>)> {
-        let editor = self.editor.as_ref()?.read(cx);
-        let cursor = editor.selections.newest_anchor().head();
-        let multibuffer = &editor.buffer().read(cx);
-        let (buffer_id, symbols) = multibuffer.symbols_containing(cursor, Some(theme), cx)?;
-        let buffer = multibuffer.buffer(buffer_id)?;
-        Some((buffer, symbols))
-    }
+    // fn active_symbols(
+    //     &self,
+    //     theme: &SyntaxTheme,
+    //     cx: &AppContext,
+    // ) -> Option<(ModelHandle<Buffer>, Vec<OutlineItem<Anchor>>)> {
+    //     let editor = self.active_item.as_ref()?.read(cx);
+    //     let cursor = editor.selections.newest_anchor().head();
+    //     let multibuffer = &editor.buffer().read(cx);
+    //     let (buffer_id, symbols) = multibuffer.symbols_containing(cursor, Some(theme), cx)?;
+    //     let buffer = multibuffer.buffer(buffer_id)?;
+    //     Some((buffer, symbols))
+    // }
 }
 
 impl Entity for Breadcrumbs {
@@ -55,41 +53,50 @@ impl View for Breadcrumbs {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        // let (buffer, symbols) =
+        //     if let Some((buffer, symbols)) = self.active_symbols(&theme.editor.syntax, cx) {
+        //         (buffer, symbols)
+        //     } else {
+        //         return Empty::new().boxed();
+        //     };
+        // let buffer = buffer.read(cx);
+        // let filename = if let Some(file) = buffer.file() {
+        //     if file.path().file_name().is_none()
+        //         || self.project.read(cx).visible_worktrees(cx).count() > 1
+        //     {
+        //         file.full_path(cx).to_string_lossy().to_string()
+        //     } else {
+        //         file.path().to_string_lossy().to_string()
+        //     }
+        // } else {
+        //     "untitled".to_string()
+        // };
+
         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 buffer = buffer.read(cx);
-        let filename = if let Some(file) = buffer.file() {
-            if file.path().file_name().is_none()
-                || self.project.read(cx).visible_worktrees(cx).count() > 1
-            {
-                file.full_path(cx).to_string_lossy().to_string()
-            } else {
-                file.path().to_string_lossy().to_string()
-            }
+        if let Some(breadcrumbs) = self
+            .active_item
+            .and_then(|item| item.breadcrumbs(&theme, cx))
+        {
+            Flex::row()
+                .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
+                    Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed()
+                }))
+                // .with_child(Label::new(filename, theme.breadcrumbs.text.clone()).boxed())
+                // .with_children(symbols.into_iter().flat_map(|symbol| {
+                //     [
+                //         Text::new(symbol.text, theme.breadcrumbs.text.clone())
+                //             .with_highlights(symbol.highlight_ranges)
+                //             .boxed(),
+                //     ]
+                // }))
+                .contained()
+                .with_style(theme.breadcrumbs.container)
+                .aligned()
+                .left()
+                .boxed()
         } else {
-            "untitled".to_string()
-        };
-
-        Flex::row()
-            .with_child(Label::new(filename, 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()
+            Empty::new().boxed()
+        }
     }
 }
 
@@ -101,7 +108,7 @@ impl ToolbarItemView for Breadcrumbs {
     ) -> ToolbarItemLocation {
         cx.notify();
         self.subscriptions.clear();
-        self.editor = None;
+        self.active_item = None;
         self.project_search = None;
         if let Some(item) = active_pane_item {
             if let Some(editor) = item.act_as::<Editor>(cx) {
@@ -114,7 +121,7 @@ impl ToolbarItemView for Breadcrumbs {
                         editor::Event::SelectionsChanged { local } if *local => cx.notify(),
                         _ => {}
                     }));
-                self.editor = Some(editor);
+                self.active_item = Some(editor);
                 if let Some(project_search) = item.downcast::<ProjectSearchView>() {
                     self.subscriptions
                         .push(cx.subscribe(&project_search, |_, _, _, cx| {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -566,12 +566,8 @@ impl workspace::Item for ProjectDiagnosticsEditor {
         unreachable!()
     }
 
-    fn should_update_tab_on_event(event: &Event) -> bool {
-        Editor::should_update_tab_on_event(event)
-    }
-
-    fn is_edit_event(event: &Self::Event) -> bool {
-        Editor::is_edit_event(event)
+    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+        Editor::to_item_events(event)
     }
 
     fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {

crates/editor/src/items.rs 🔗

@@ -26,7 +26,7 @@ use text::{Point, Selection};
 use util::TryFutureExt;
 use workspace::{
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
-    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
+    FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
 };
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@@ -475,19 +475,13 @@ impl Item for Editor {
         })
     }
 
-    fn should_close_item_on_event(event: &Event) -> bool {
-        matches!(event, Event::Closed)
-    }
-
-    fn should_update_tab_on_event(event: &Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::DirtyChanged | Event::TitleChanged
-        )
-    }
-
-    fn is_edit_event(event: &Self::Event) -> bool {
-        matches!(event, Event::BufferEdited)
+    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+        match event {
+            Event::Closed => vec![ItemEvent::CloseItem],
+            Event::Saved | Event::DirtyChanged | Event::TitleChanged => vec![ItemEvent::UpdateTab],
+            Event::BufferEdited => vec![ItemEvent::Edit],
+            _ => Vec::new(),
+        }
     }
 
     fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {

crates/search/src/buffer_search.rs 🔗

@@ -191,16 +191,17 @@ impl ToolbarItemView for BufferSearchBar {
 
         if let Some(searchable_item_handle) = item.and_then(|item| item.as_searchable(cx)) {
             let handle = cx.weak_handle();
-            self.active_searchable_item_subscription = Some(searchable_item_handle.subscribe(
-                cx,
-                Box::new(move |search_event, cx| {
-                    if let Some(this) = handle.upgrade(cx) {
-                        this.update(cx, |this, cx| {
-                            this.on_active_searchable_item_event(search_event, cx)
-                        });
-                    }
-                }),
-            ));
+            self.active_searchable_item_subscription =
+                Some(searchable_item_handle.subscribe_to_search_events(
+                    cx,
+                    Box::new(move |search_event, cx| {
+                        if let Some(this) = handle.upgrade(cx) {
+                            this.update(cx, |this, cx| {
+                                this.on_active_searchable_item_event(search_event, cx)
+                            });
+                        }
+                    }),
+                ));
 
             self.active_searchable_item = Some(searchable_item_handle);
             self.update_matches(false, cx);

crates/search/src/project_search.rs 🔗

@@ -24,7 +24,8 @@ use std::{
 use util::ResultExt as _;
 use workspace::{
     searchable::{Direction, SearchableItem, SearchableItemHandle},
-    Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
+    Item, ItemEvent, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView,
+    Workspace,
 };
 
 actions!(project_search, [SearchInNew, ToggleFocus]);
@@ -326,15 +327,11 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
-    fn should_update_tab_on_event(event: &ViewEvent) -> bool {
-        matches!(event, ViewEvent::UpdateTab)
-    }
-
-    fn is_edit_event(event: &Self::Event) -> bool {
-        if let ViewEvent::EditorEvent(editor_event) = event {
-            Editor::is_edit_event(editor_event)
-        } else {
-            false
+    fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
+        match event {
+            ViewEvent::UpdateTab => vec![ItemEvent::UpdateTab],
+            ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
+            _ => Vec::new(),
         }
     }
 }

crates/terminal/src/terminal_container_view.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
 };
 use util::truncate_and_trailoff;
 use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
-use workspace::{Item, Workspace};
+use workspace::{Item, ItemEvent, Workspace};
 
 use crate::TerminalSize;
 use project::{LocalWorktree, Project, ProjectPath};
@@ -359,17 +359,17 @@ impl Item for TerminalContainer {
         false
     }
 
-    fn should_update_tab_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::TitleChanged | &Event::Wakeup)
-    }
-
-    fn should_close_item_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::CloseTerminal)
-    }
-
     fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(handle.clone()))
     }
+
+    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+        match event {
+            Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab],
+            Event::CloseTerminal => vec![ItemEvent::CloseItem],
+            _ => vec![],
+        }
+    }
 }
 
 impl SearchableItem for TerminalContainer {

crates/workspace/src/searchable.rs 🔗

@@ -88,7 +88,7 @@ pub trait SearchableItemHandle: ItemHandle {
     fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
     fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
     fn supported_options(&self) -> SearchOptions;
-    fn subscribe(
+    fn subscribe_to_search_events(
         &self,
         cx: &mut MutableAppContext,
         handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
@@ -134,7 +134,7 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         T::supported_options()
     }
 
-    fn subscribe(
+    fn subscribe_to_search_events(
         &self,
         cx: &mut MutableAppContext,
         handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,

crates/workspace/src/workspace.rs 🔗

@@ -267,6 +267,14 @@ pub struct AppState {
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
 }
 
+#[derive(Eq, PartialEq, Hash)]
+pub enum ItemEvent {
+    CloseItem,
+    UpdateTab,
+    UpdateBreadcrumbs,
+    Edit,
+}
+
 pub trait Item: View {
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
@@ -311,15 +319,7 @@ pub trait Item: View {
         project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
-    fn should_close_item_on_event(_: &Self::Event) -> bool {
-        false
-    }
-    fn should_update_tab_on_event(_: &Self::Event) -> bool {
-        false
-    }
-    fn is_edit_event(_: &Self::Event) -> bool {
-        false
-    }
+    fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
     fn act_as_type(
         &self,
         type_id: TypeId,
@@ -335,6 +335,13 @@ pub trait Item: View {
     fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         None
     }
+
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        ToolbarItemLocation::Hidden
+    }
+    fn breadcrumbs(&self, _theme: &Theme) -> Option<Vec<ElementBox>> {
+        None
+    }
 }
 
 pub trait ProjectItem: Item {
@@ -470,6 +477,9 @@ pub trait ItemHandle: 'static + fmt::Debug {
         callback: Box<dyn FnOnce(&mut MutableAppContext)>,
     ) -> gpui::Subscription;
     fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+
+    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
+    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
 }
 
 pub trait WeakItemHandle {
@@ -605,47 +615,53 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                         }
                     }
 
-                    if T::should_close_item_on_event(event) {
-                        Pane::close_item(workspace, pane, item.id(), cx).detach_and_log_err(cx);
-                        return;
-                    }
-
-                    if T::should_update_tab_on_event(event) {
-                        pane.update(cx, |_, cx| {
-                            cx.emit(pane::Event::ChangeItemTitle);
-                            cx.notify();
-                        });
-                    }
-
-                    if T::is_edit_event(event) {
-                        if let Autosave::AfterDelay { milliseconds } =
-                            cx.global::<Settings>().autosave
-                        {
-                            let prev_autosave = pending_autosave
-                                .take()
-                                .unwrap_or_else(|| Task::ready(Some(())));
-                            let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
-                            let prev_cancel_tx =
-                                mem::replace(&mut cancel_pending_autosave, cancel_tx);
-                            let project = workspace.project.downgrade();
-                            let _ = prev_cancel_tx.send(());
-                            pending_autosave = Some(cx.spawn_weak(|_, mut cx| async move {
-                                let mut timer = cx
-                                    .background()
-                                    .timer(Duration::from_millis(milliseconds))
-                                    .fuse();
-                                prev_autosave.await;
-                                futures::select_biased! {
-                                    _ = cancel_rx => return None,
-                                    _ = timer => {}
+                    for item_event in T::to_item_events(event).into_iter() {
+                        match item_event {
+                            ItemEvent::CloseItem => {
+                                Pane::close_item(workspace, pane, item.id(), cx)
+                                    .detach_and_log_err(cx);
+                                return;
+                            }
+                            ItemEvent::UpdateTab => {
+                                pane.update(cx, |_, cx| {
+                                    cx.emit(pane::Event::ChangeItemTitle);
+                                    cx.notify();
+                                });
+                            }
+                            ItemEvent::Edit => {
+                                if let Autosave::AfterDelay { milliseconds } =
+                                    cx.global::<Settings>().autosave
+                                {
+                                    let prev_autosave = pending_autosave
+                                        .take()
+                                        .unwrap_or_else(|| Task::ready(Some(())));
+                                    let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
+                                    let prev_cancel_tx =
+                                        mem::replace(&mut cancel_pending_autosave, cancel_tx);
+                                    let project = workspace.project.downgrade();
+                                    let _ = prev_cancel_tx.send(());
+                                    let item = item.clone();
+                                    pending_autosave =
+                                        Some(cx.spawn_weak(|_, mut cx| async move {
+                                            let mut timer = cx
+                                                .background()
+                                                .timer(Duration::from_millis(milliseconds))
+                                                .fuse();
+                                            prev_autosave.await;
+                                            futures::select_biased! {
+                                                _ = cancel_rx => return None,
+                                                    _ = timer => {}
+                                            }
+
+                                            let project = project.upgrade(&cx)?;
+                                            cx.update(|cx| Pane::autosave_item(&item, project, cx))
+                                                .await
+                                                .log_err();
+                                            None
+                                        }));
                                 }
-
-                                let project = project.upgrade(&cx)?;
-                                cx.update(|cx| Pane::autosave_item(&item, project, cx))
-                                    .await
-                                    .log_err();
-                                None
-                            }));
+                            }
+                            _ => {}
                         }
                     }
                 }));
@@ -749,6 +765,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
         self.read(cx).as_searchable(self)
     }
+
+    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
+        self.read(cx).breadcrumb_location()
+    }
+
+    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+        self.read(cx).breadcrumbs(theme)
+    }
 }
 
 impl From<Box<dyn ItemHandle>> for AnyViewHandle {
@@ -3590,12 +3614,8 @@ mod tests {
             Task::ready(Ok(()))
         }
 
-        fn should_update_tab_on_event(_: &Self::Event) -> bool {
-            true
-        }
-
-        fn is_edit_event(event: &Self::Event) -> bool {
-            matches!(event, TestItemEvent::Edit)
+        fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
+            vec![ItemEvent::UpdateTab, ItemEvent::Edit]
         }
     }
 }