Introduced ItemEvent and to_item_events function to Item trait which converts the Item's events into a standard ItemEvent similar to how SearchableItems work.

Add breadcrumb_location and breadcrumbs functions to item trait which handles rendering of the breadcrumb elements
Change breadcrumb toolbar to use these new functions rather than having hard coded breadcrumb logic
Add breadcrumb support to the terminal tabs

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
K Simmons 2022-09-06 16:05:36 -07:00
parent ab81093ef5
commit 31ecb2f7bc
8 changed files with 149 additions and 99 deletions

View File

@ -1,46 +1,29 @@
use editor::Editor;
use gpui::{
elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
ViewHandle,
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use itertools::Itertools;
use project::Project;
use search::ProjectSearchView;
use settings::Settings;
use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
use workspace::{ItemEvent, ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub enum Event {
UpdateLocation,
}
pub struct Breadcrumbs {
project: ModelHandle<Project>,
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,
active_item: Default::default(),
subscriptions: 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.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 {
@ -53,42 +36,16 @@ 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();
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()
}))
// .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()
@ -107,39 +64,25 @@ impl ToolbarItemView for Breadcrumbs {
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.subscriptions.clear();
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.active_item = 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
}
@ -151,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
}

View File

@ -27,6 +27,7 @@ use util::TryFutureExt;
use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
ToolbarItemLocation,
};
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@ -476,17 +477,71 @@ impl Item for Editor {
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
let mut result = Vec::new();
match event {
Event::Closed => vec![ItemEvent::CloseItem],
Event::Saved | Event::DirtyChanged | Event::TitleChanged => vec![ItemEvent::UpdateTab],
Event::BufferEdited => vec![ItemEvent::Edit],
_ => Vec::new(),
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 as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
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)
}
}
impl ProjectItem for Editor {

View File

@ -189,7 +189,9 @@ 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_to_search_events(

View File

@ -329,11 +329,23 @@ impl Item for ProjectSearchView {
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
match event {
ViewEvent::UpdateTab => vec![ItemEvent::UpdateTab],
ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab],
ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
_ => Vec::new(),
}
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
if self.has_matches() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
self.results_editor.breadcrumbs(theme, cx)
}
}
impl ProjectSearchView {

View File

@ -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()))

View File

@ -9,7 +9,7 @@ use gpui::{
};
use util::truncate_and_trailoff;
use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
use workspace::{Item, ItemEvent, Workspace};
use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath};
@ -363,13 +363,37 @@ impl Item for TerminalContainer {
Some(Box::new(handle.clone()))
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
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 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()])
}
}
impl SearchableItem for TerminalContainer {

View File

@ -339,7 +339,7 @@ pub trait Item: View {
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::Hidden
}
fn breadcrumbs(&self, _theme: &Theme) -> Option<Vec<ElementBox>> {
fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
None
}
}
@ -437,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;
@ -476,8 +481,7 @@ 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>>;
}
@ -500,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)
}
@ -762,7 +778,7 @@ 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)
}
@ -771,7 +787,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
self.read(cx).breadcrumbs(theme)
self.read(cx).breadcrumbs(theme, cx)
}
}

View File

@ -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);