Merge pull request #1613 from zed-industries/breadcrumbs-refactoring

K Simmons created

Terminal Breadcrumbs

Change summary

Cargo.lock                                     |   1 
crates/breadcrumbs/Cargo.toml                  |   1 
crates/breadcrumbs/src/breadcrumbs.rs          | 138 +++++------------
crates/diagnostics/src/diagnostics.rs          |   8 
crates/editor/src/items.rs                     |  73 ++++++++-
crates/search/src/buffer_search.rs             |  25 +-
crates/search/src/project_search.rs            |  23 ++
crates/terminal/src/terminal.rs                |   3 
crates/terminal/src/terminal_container_view.rs |  38 ++++
crates/workspace/src/searchable.rs             |   4 
crates/workspace/src/workspace.rs              | 150 ++++++++++++-------
crates/zed/src/zed.rs                          |   3 
12 files changed, 267 insertions(+), 200 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,48 +1,29 @@
-use editor::{Anchor, Editor};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
-    ViewHandle,
+    elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
 };
-use language::{Buffer, OutlineItem};
-use project::Project;
+use itertools::Itertools;
 use search::ProjectSearchView;
 use settings::Settings;
-use theme::SyntaxTheme;
-use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemEvent, ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub enum Event {
     UpdateLocation,
 }
 
 pub struct Breadcrumbs {
-    project: ModelHandle<Project>,
-    editor: Option<ViewHandle<Editor>>,
+    active_item: Option<Box<dyn ItemHandle>>,
     project_search: Option<ViewHandle<ProjectSearchView>>,
-    subscriptions: Vec<Subscription>,
+    subscription: Option<Subscription>,
 }
 
 impl Breadcrumbs {
-    pub fn new(project: ModelHandle<Project>) -> Self {
+    pub fn new() -> Self {
         Self {
-            project,
-            editor: Default::default(),
-            subscriptions: Default::default(),
+            active_item: Default::default(),
+            subscription: 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))
-    }
 }
 
 impl Entity for Breadcrumbs {
@@ -56,40 +37,23 @@ impl View for 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 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
+            .as_ref()
+            .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()
+                }))
+                .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()
+        }
     }
 }
 
@@ -100,39 +64,25 @@ impl ToolbarItemView for Breadcrumbs {
         cx: &mut ViewContext<Self>,
     ) -> 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) {
-                self.subscriptions
-                    .push(cx.subscribe(&editor, |_, _, event, cx| match event {
-                        editor::Event::BufferEdited
-                        | editor::Event::TitleChanged
-                        | editor::Event::Saved
-                        | editor::Event::Reparsed => cx.notify(),
-                        editor::Event::SelectionsChanged { local } if *local => cx.notify(),
-                        _ => {}
-                    }));
-                self.editor = Some(editor);
-                if let Some(project_search) = item.downcast::<ProjectSearchView>() {
-                    self.subscriptions
-                        .push(cx.subscribe(&project_search, |_, _, _, cx| {
-                            cx.emit(Event::UpdateLocation);
-                        }));
-                    self.project_search = Some(project_search.clone());
-
-                    if project_search.read(cx).has_matches() {
-                        ToolbarItemLocation::Secondary
-                    } else {
-                        ToolbarItemLocation::Hidden
+            let this = cx.weak_handle();
+            self.subscription = Some(item.subscribe_to_item_events(
+                cx,
+                Box::new(move |event, cx| {
+                    if let Some(this) = this.upgrade(cx) {
+                        if let ItemEvent::UpdateBreadcrumbs = event {
+                            this.update(cx, |_, cx| {
+                                cx.emit(Event::UpdateLocation);
+                                cx.notify();
+                            });
+                        }
                     }
-                } else {
-                    ToolbarItemLocation::PrimaryLeft { flex: None }
-                }
-            } else {
-                ToolbarItemLocation::Hidden
-            }
+                }),
+            ));
+            self.active_item = Some(item.boxed_clone());
+            item.breadcrumb_location(cx)
         } else {
             ToolbarItemLocation::Hidden
         }
@@ -144,12 +94,8 @@ impl ToolbarItemView for Breadcrumbs {
         current_location: ToolbarItemLocation,
         cx: &AppContext,
     ) -> ToolbarItemLocation {
-        if let Some(project_search) = self.project_search.as_ref() {
-            if project_search.read(cx).has_matches() {
-                ToolbarItemLocation::Secondary
-            } else {
-                ToolbarItemLocation::Hidden
-            }
+        if let Some(active_item) = self.active_item.as_ref() {
+            active_item.breadcrumb_location(cx)
         } else {
             current_location
         }

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,8 @@ 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,
+    ToolbarItemLocation,
 };
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@@ -475,23 +476,71 @@ impl Item for Editor {
         })
     }
 
-    fn should_close_item_on_event(event: &Event) -> bool {
-        matches!(event, Event::Closed)
+    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
+        let mut result = Vec::new();
+        match event {
+            Event::Closed => result.push(ItemEvent::CloseItem),
+            Event::Saved | Event::TitleChanged => {
+                result.push(ItemEvent::UpdateTab);
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::Reparsed => {
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::SelectionsChanged { local } if *local => {
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::DirtyChanged => {
+                result.push(ItemEvent::UpdateTab);
+            }
+            Event::BufferEdited => {
+                result.push(ItemEvent::Edit);
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            _ => {}
+        }
+        result
     }
 
-    fn should_update_tab_on_event(event: &Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::DirtyChanged | Event::TitleChanged
-        )
+    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(handle.clone()))
     }
 
-    fn is_edit_event(event: &Self::Event) -> bool {
-        matches!(event, Event::BufferEdited)
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        ToolbarItemLocation::PrimaryLeft { flex: None }
     }
 
-    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(handle.clone()))
+    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+        let cursor = self.selections.newest_anchor().head();
+        let multibuffer = &self.buffer().read(cx);
+        let (buffer_id, symbols) =
+            multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
+        let buffer = multibuffer.buffer(buffer_id)?;
+
+        let buffer = buffer.read(cx);
+        let filename = if let Some(file) = buffer.file() {
+            if file.path().file_name().is_none()
+                || self
+                    .project
+                    .as_ref()
+                    .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                    .unwrap_or_default()
+            {
+                file.full_path(cx).to_string_lossy().to_string()
+            } else {
+                file.path().to_string_lossy().to_string()
+            }
+        } else {
+            "untitled".to_string()
+        };
+
+        let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
+        breadcrumbs.extend(symbols.into_iter().map(|symbol| {
+            Text::new(symbol.text, theme.breadcrumbs.text.clone())
+                .with_highlights(symbol.highlight_ranges)
+                .boxed()
+        }));
+        Some(breadcrumbs)
     }
 }
 

crates/search/src/buffer_search.rs 🔗

@@ -189,18 +189,21 @@ impl ToolbarItemView for BufferSearchBar {
         self.active_searchable_item.take();
         self.pending_search.take();
 
-        if let Some(searchable_item_handle) = item.and_then(|item| item.as_searchable(cx)) {
+        if let Some(searchable_item_handle) =
+            item.and_then(|item| item.to_searchable_item_handle(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,17 +327,25 @@ 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 to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
+        match event {
+            ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab],
+            ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
+            _ => Vec::new(),
+        }
     }
 
-    fn is_edit_event(event: &Self::Event) -> bool {
-        if let ViewEvent::EditorEvent(editor_event) = event {
-            Editor::is_edit_event(editor_event)
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        if self.has_matches() {
+            ToolbarItemLocation::Secondary
         } else {
-            false
+            ToolbarItemLocation::Hidden
         }
     }
+
+    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+        self.results_editor.breadcrumbs(theme, cx)
+    }
 }
 
 impl ProjectSearchView {

crates/terminal/src/terminal.rs 🔗

@@ -83,6 +83,7 @@ const DEBUG_LINE_HEIGHT: f32 = 5.;
 #[derive(Clone, Copy, Debug)]
 pub enum Event {
     TitleChanged,
+    BreadcrumbsChanged,
     CloseTerminal,
     Bell,
     Wakeup,
@@ -494,9 +495,11 @@ impl Terminal {
         match event {
             AlacTermEvent::Title(title) => {
                 self.breadcrumb_text = title.to_string();
+                cx.emit(Event::BreadcrumbsChanged);
             }
             AlacTermEvent::ResetTitle => {
                 self.breadcrumb_text = String::new();
+                cx.emit(Event::BreadcrumbsChanged);
             }
             AlacTermEvent::ClipboardStore(_, data) => {
                 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))

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, ToolbarItemLocation, Workspace};
 
 use crate::TerminalSize;
 use project::{LocalWorktree, Project, ProjectPath};
@@ -359,16 +359,40 @@ impl Item for TerminalContainer {
         false
     }
 
-    fn should_update_tab_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::TitleChanged | &Event::Wakeup)
+    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(handle.clone()))
     }
 
-    fn should_close_item_on_event(event: &Self::Event) -> bool {
-        matches!(event, &Event::CloseTerminal)
+    fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
+        match event {
+            Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs],
+            Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab],
+            Event::CloseTerminal => vec![ItemEvent::CloseItem],
+            _ => vec![],
+        }
     }
 
-    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(handle.clone()))
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        if self.connected().is_some() {
+            ToolbarItemLocation::PrimaryLeft { flex: None }
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
+
+    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
+        let connected = self.connected()?;
+
+        Some(vec![Text::new(
+            connected
+                .read(cx)
+                .terminal()
+                .read(cx)
+                .breadcrumb_text
+                .to_string(),
+            theme.breadcrumbs.text.clone(),
+        )
+        .boxed()])
     }
 }
 

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, _cx: &AppContext) -> Option<Vec<ElementBox>> {
+        None
+    }
 }
 
 pub trait ProjectItem: Item {
@@ -430,6 +437,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
 }
 
 pub trait ItemHandle: 'static + fmt::Debug {
+    fn subscribe_to_item_events(
+        &self,
+        cx: &mut MutableAppContext,
+        handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
+    ) -> gpui::Subscription;
     fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
     fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
         -> ElementBox;
@@ -469,7 +481,9 @@ pub trait ItemHandle: 'static + fmt::Debug {
         cx: &mut MutableAppContext,
         callback: Box<dyn FnOnce(&mut MutableAppContext)>,
     ) -> gpui::Subscription;
-    fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+    fn to_searchable_item_handle(&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 {
@@ -490,6 +504,18 @@ impl dyn ItemHandle {
 }
 
 impl<T: Item> ItemHandle for ViewHandle<T> {
+    fn subscribe_to_item_events(
+        &self,
+        cx: &mut MutableAppContext,
+        handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
+    ) -> gpui::Subscription {
+        cx.subscribe(self, move |_, event, cx| {
+            for item_event in T::to_item_events(event) {
+                handler(item_event, cx)
+            }
+        })
+    }
+
     fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
         self.read(cx).tab_description(detail, cx)
     }
@@ -605,47 +631,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
-                            }));
+                            }
+                            _ => {}
                         }
                     }
                 }));
@@ -746,9 +778,17 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         cx.observe_release(self, move |_, cx| callback(cx))
     }
 
-    fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+    fn to_searchable_item_handle(&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, cx)
+    }
 }
 
 impl From<Box<dyn ItemHandle>> for AnyViewHandle {
@@ -3590,12 +3630,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]
         }
     }
 }

crates/zed/src/zed.rs 🔗

@@ -225,12 +225,11 @@ pub fn initialize_workspace(
     cx: &mut ViewContext<Workspace>,
 ) {
     cx.subscribe(&cx.handle(), {
-        let project = workspace.project().clone();
         move |_, _, event, cx| {
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, cx| {
                     pane.toolbar().update(cx, |toolbar, cx| {
-                        let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone()));
+                        let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
                         toolbar.add_item(breadcrumbs, cx);
                         let buffer_search_bar = cx.add_view(BufferSearchBar::new);
                         toolbar.add_item(buffer_search_bar, cx);