Render the search UI on a separate row from the breadcrumbs

- In project search, render it above the breadcrumbs
- In buffer search, render it below

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2022-03-31 10:36:39 -06:00
parent 0453dd1101
commit 8bfac63e0d
9 changed files with 207 additions and 125 deletions

1
Cargo.lock generated
View File

@ -727,6 +727,7 @@ dependencies = [
"editor", "editor",
"gpui", "gpui",
"language", "language",
"search",
"theme", "theme",
"workspace", "workspace",
] ]

View File

@ -12,6 +12,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
search = { path = "../search" }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View File

@ -3,9 +3,10 @@ use gpui::{
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle, elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
}; };
use language::{BufferSnapshot, OutlineItem}; use language::{BufferSnapshot, OutlineItem};
use search::ProjectSearchView;
use std::borrow::Cow; use std::borrow::Cow;
use theme::SyntaxTheme; use theme::SyntaxTheme;
use workspace::{ItemHandle, Settings, ToolbarItemView}; use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
pub struct Breadcrumbs { pub struct Breadcrumbs {
editor: Option<ViewHandle<Editor>>, editor: Option<ViewHandle<Editor>>,
@ -83,17 +84,29 @@ impl ToolbarItemView for Breadcrumbs {
&mut self, &mut self,
active_pane_item: Option<&dyn ItemHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) -> ToolbarItemLocation {
cx.notify();
self.editor_subscription = None; self.editor_subscription = None;
self.editor = None; self.editor = None;
if let Some(editor) = active_pane_item.and_then(|i| i.act_as::<Editor>(cx)) { if let Some(item) = active_pane_item {
self.editor_subscription = Some(cx.subscribe(&editor, |_, _, event, cx| match event { if let Some(editor) = item.act_as::<Editor>(cx) {
editor::Event::BufferEdited => cx.notify(), self.editor_subscription =
editor::Event::SelectionsChanged { local } if *local => cx.notify(), Some(cx.subscribe(&editor, |_, _, event, cx| match event {
_ => {} editor::Event::BufferEdited => cx.notify(),
})); editor::Event::SelectionsChanged { local } if *local => cx.notify(),
self.editor = Some(editor); _ => {}
}));
self.editor = Some(editor);
if item.downcast::<ProjectSearchView>().is_some() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::PrimaryLeft
}
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::Hidden
} }
cx.notify();
} }
} }

View File

@ -8,13 +8,17 @@ use gpui::{
use language::OffsetRangeExt; use language::OffsetRangeExt;
use project::search::SearchQuery; use project::search::SearchQuery;
use std::ops::Range; use std::ops::Range;
use workspace::{ItemHandle, Pane, Settings, ToolbarItemView}; use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
action!(Deploy, bool); action!(Deploy, bool);
action!(Dismiss); action!(Dismiss);
action!(FocusEditor); action!(FocusEditor);
action!(ToggleSearchOption, SearchOption); action!(ToggleSearchOption, SearchOption);
pub enum Event {
UpdateLocation,
}
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings([ cx.add_bindings([
Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
@ -57,7 +61,7 @@ pub struct BufferSearchBar {
} }
impl Entity for BufferSearchBar { impl Entity for BufferSearchBar {
type Event = (); type Event = Event;
} }
impl View for BufferSearchBar { impl View for BufferSearchBar {
@ -70,70 +74,66 @@ impl View for BufferSearchBar {
} }
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.dismissed || self.active_editor.is_none() { let theme = cx.global::<Settings>().theme.clone();
Empty::new().boxed() let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else { } else {
let theme = cx.global::<Settings>().theme.clone(); theme.search.editor.input.container
let editor_container = if self.query_contains_error { };
theme.search.invalid_editor Flex::row()
} else { .with_child(
theme.search.editor.input.container Flex::row()
}; .with_child(ChildView::new(&self.query_editor).flex(1., true).boxed())
Flex::row() .with_children(self.active_editor.as_ref().and_then(|editor| {
.with_child( let matches = self.editors_with_matches.get(&editor.downgrade())?;
Flex::row() let message = if let Some(match_ix) = self.active_match_index {
.with_child(ChildView::new(&self.query_editor).flex(1., true).boxed()) format!("{}/{}", match_ix + 1, matches.len())
.with_children(self.active_editor.as_ref().and_then(|editor| { } else {
let matches = self.editors_with_matches.get(&editor.downgrade())?; "No matches".to_string()
let message = if let Some(match_ix) = self.active_match_index { };
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some( Some(
Label::new(message, theme.search.match_index.text.clone()) Label::new(message, theme.search.match_index.text.clone())
.contained() .contained()
.with_style(theme.search.match_index.container) .with_style(theme.search.match_index.container)
.aligned() .aligned()
.boxed(), .boxed(),
) )
})) }))
.contained() .contained()
.with_style(editor_container) .with_style(editor_container)
.aligned() .aligned()
.constrained() .constrained()
.with_max_width(theme.search.editor.max_width) .with_max_width(theme.search.editor.max_width)
.boxed(), .boxed(),
) )
.with_child( .with_child(
Flex::row() Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx)) .with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx)) .with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned() .aligned()
.boxed(), .boxed(),
) )
.with_child( .with_child(
Flex::row() Flex::row()
.with_child(self.render_search_option( .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
"Case", .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
SearchOption::CaseSensitive, .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
cx, .contained()
)) .with_style(theme.search.option_button_group)
.with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) .aligned()
.with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) .boxed(),
.contained() )
.with_style(theme.search.option_button_group) .named("search bar")
.aligned()
.boxed(),
)
.named("search bar")
}
} }
} }
impl ToolbarItemView for BufferSearchBar { impl ToolbarItemView for BufferSearchBar {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) { fn set_active_pane_item(
&mut self,
item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify(); cx.notify();
self.active_editor_subscription.take(); self.active_editor_subscription.take();
self.active_editor.take(); self.active_editor.take();
@ -145,9 +145,21 @@ impl ToolbarItemView for BufferSearchBar {
Some(cx.subscribe(&editor, Self::on_active_editor_event)); Some(cx.subscribe(&editor, Self::on_active_editor_event));
self.active_editor = Some(editor); self.active_editor = Some(editor);
self.update_matches(false, cx); self.update_matches(false, cx);
return; if !self.dismissed {
return ToolbarItemLocation::Secondary;
}
} }
} }
ToolbarItemLocation::Hidden
}
fn location_for_event(&self, _: &Self::Event, _: ToolbarItemLocation) -> ToolbarItemLocation {
if self.active_editor.is_some() && !self.dismissed {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
} }
} }
@ -186,6 +198,7 @@ impl BufferSearchBar {
if let Some(active_editor) = self.active_editor.as_ref() { if let Some(active_editor) = self.active_editor.as_ref() {
cx.focus(active_editor); cx.focus(active_editor);
} }
cx.emit(Event::UpdateLocation);
cx.notify(); cx.notify();
} }
@ -234,6 +247,7 @@ impl BufferSearchBar {
self.dismissed = false; self.dismissed = false;
cx.notify(); cx.notify();
cx.emit(Event::UpdateLocation);
true true
} }

View File

@ -16,7 +16,9 @@ use std::{
path::PathBuf, path::PathBuf,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{Item, ItemNavHistory, Pane, Settings, ToolbarItemView, Workspace}; use workspace::{
Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
};
action!(Deploy); action!(Deploy);
action!(Search); action!(Search);
@ -56,7 +58,7 @@ struct ProjectSearch {
active_query: Option<SearchQuery>, active_query: Option<SearchQuery>,
} }
struct ProjectSearchView { pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>, model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>, query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>, results_editor: ViewHandle<Editor>,
@ -136,7 +138,7 @@ impl ProjectSearch {
} }
} }
enum ViewEvent { pub enum ViewEvent {
UpdateTab, UpdateTab,
} }
@ -748,14 +750,17 @@ impl ToolbarItemView for ProjectSearchBar {
&mut self, &mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>, active_pane_item: Option<&dyn workspace::ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) -> ToolbarItemLocation {
cx.notify();
self.subscription = None; self.subscription = None;
self.active_project_search = None; self.active_project_search = None;
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) { if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
self.active_project_search = Some(search); self.active_project_search = Some(search);
ToolbarItemLocation::PrimaryLeft
} else {
ToolbarItemLocation::Hidden
} }
cx.notify();
} }
} }

View File

@ -1,7 +1,7 @@
pub use buffer_search::BufferSearchBar; pub use buffer_search::BufferSearchBar;
use editor::{Anchor, MultiBufferSnapshot}; use editor::{Anchor, MultiBufferSnapshot};
use gpui::{action, MutableAppContext}; use gpui::{action, MutableAppContext};
pub use project_search::ProjectSearchBar; pub use project_search::{ProjectSearchBar, ProjectSearchView};
use std::{ use std::{
cmp::{self, Ordering}, cmp::{self, Ordering},
ops::Range, ops::Range,

View File

@ -9,22 +9,38 @@ pub trait ToolbarItemView: View {
&mut self, &mut self,
active_pane_item: Option<&dyn crate::ItemHandle>, active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
); ) -> ToolbarItemLocation;
fn location_for_event(
&self,
_event: &Self::Event,
current_location: ToolbarItemLocation,
) -> ToolbarItemLocation {
current_location
}
} }
trait ToolbarItemViewHandle { trait ToolbarItemViewHandle {
fn id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle; fn to_any(&self) -> AnyViewHandle;
fn set_active_pane_item( fn set_active_pane_item(
&self, &self,
active_pane_item: Option<&dyn ItemHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
); ) -> ToolbarItemLocation;
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ToolbarItemLocation {
Hidden,
PrimaryLeft,
PrimaryRight,
Secondary,
} }
pub struct Toolbar { pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>, active_pane_item: Option<Box<dyn ItemHandle>>,
left_items: Vec<Box<dyn ToolbarItemViewHandle>>, items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
right_items: Vec<Box<dyn ToolbarItemViewHandle>>,
} }
impl Entity for Toolbar { impl Entity for Toolbar {
@ -38,26 +54,50 @@ impl View for Toolbar {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.workspace.toolbar; let theme = &cx.global::<Settings>().theme.workspace.toolbar;
Flex::row()
.with_children(self.left_items.iter().map(|i| { let mut primary_left_items = Vec::new();
ChildView::new(i.as_ref()) let mut primary_right_items = Vec::new();
.aligned() let mut secondary_item = None;
.contained()
.with_margin_right(theme.item_spacing) for (item, position) in &self.items {
.boxed() match position {
})) ToolbarItemLocation::Hidden => {}
.with_children(self.right_items.iter().map(|i| { ToolbarItemLocation::PrimaryLeft => primary_left_items.push(item),
ChildView::new(i.as_ref()) ToolbarItemLocation::PrimaryRight => primary_right_items.push(item),
.aligned() ToolbarItemLocation::Secondary => secondary_item = Some(item),
.contained() }
.with_margin_left(theme.item_spacing) }
.flex_float()
Flex::column()
.with_child(
Flex::row()
.with_children(primary_left_items.iter().map(|i| {
ChildView::new(i.as_ref())
.aligned()
.contained()
.with_margin_right(theme.item_spacing)
.boxed()
}))
.with_children(primary_right_items.iter().map(|i| {
ChildView::new(i.as_ref())
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.flex_float()
.boxed()
}))
.constrained()
.with_height(theme.height)
.boxed(),
)
.with_children(secondary_item.map(|item| {
ChildView::new(item.as_ref())
.constrained()
.with_height(theme.height)
.boxed() .boxed()
})) }))
.contained() .contained()
.with_style(theme.container) .with_style(theme.container)
.constrained()
.with_height(theme.height)
.boxed() .boxed()
} }
} }
@ -66,49 +106,58 @@ impl Toolbar {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
active_pane_item: None, active_pane_item: None,
left_items: Default::default(), items: Default::default(),
right_items: Default::default(),
} }
} }
pub fn add_left_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>) pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where where
T: 'static + ToolbarItemView, T: 'static + ToolbarItemView,
{ {
item.set_active_pane_item(self.active_pane_item.as_deref(), cx); let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
self.left_items.push(Box::new(item)); cx.subscribe(&item, |this, item, event, cx| {
cx.notify(); if let Some((_, current_location)) =
} this.items.iter_mut().find(|(i, _)| i.id() == item.id())
{
pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>) let new_location = item.read(cx).location_for_event(event, *current_location);
where if new_location != *current_location {
T: 'static + ToolbarItemView, *current_location = new_location;
{ cx.notify();
item.set_active_pane_item(self.active_pane_item.as_deref(), cx); }
self.right_items.push(Box::new(item)); }
})
.detach();
self.items.push((Box::new(item), dbg!(location)));
cx.notify(); cx.notify();
} }
pub fn set_active_pane_item( pub fn set_active_pane_item(
&mut self, &mut self,
item: Option<&dyn ItemHandle>, pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.active_pane_item = item.map(|item| item.boxed_clone()); self.active_pane_item = pane_item.map(|item| item.boxed_clone());
for tool in self.left_items.iter().chain(&self.right_items) { for (toolbar_item, current_location) in self.items.iter_mut() {
tool.set_active_pane_item(item, cx); let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
} }
} }
pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> { pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
self.left_items self.items
.iter() .iter()
.chain(&self.right_items) .find_map(|(item, _)| item.to_any().downcast())
.find_map(|tool| tool.to_any().downcast())
} }
} }
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> { impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
fn to_any(&self) -> AnyViewHandle { fn to_any(&self) -> AnyViewHandle {
self.into() self.into()
} }
@ -117,10 +166,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
&self, &self,
active_pane_item: Option<&dyn ItemHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) { ) -> ToolbarItemLocation {
self.update(cx, |this, cx| { self.update(cx, |this, cx| {
this.set_active_pane_item(active_pane_item, cx) this.set_active_pane_item(active_pane_item, cx)
}); })
} }
} }

View File

@ -48,7 +48,7 @@ use std::{
}, },
}; };
use theme::{Theme, ThemeRegistry}; use theme::{Theme, ThemeRegistry};
pub use toolbar::ToolbarItemView; pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt; use util::ResultExt;
type ProjectItemBuilders = HashMap< type ProjectItemBuilders = HashMap<

View File

@ -111,12 +111,11 @@ pub fn build_workspace(
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| { pane.toolbar().update(cx, |toolbar, cx| {
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
toolbar.add_left_item(breadcrumbs, cx); toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
toolbar.add_right_item(buffer_search_bar, cx); toolbar.add_item(buffer_search_bar, cx);
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
toolbar.add_right_item(project_search_bar, cx); toolbar.add_item(project_search_bar, cx);
}) })
}); });
}) })