From a11665ecc7c5c2e6fa4edf8ac8dbba6434bd27b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Mar 2022 17:04:39 +0200 Subject: [PATCH] Render project search query editor in toolbar --- crates/search/src/buffer_search.rs | 42 +-- crates/search/src/project_search.rs | 465 ++++++++++++++++------------ crates/search/src/search.rs | 7 +- crates/zed/assets/themes/_base.toml | 2 +- crates/zed/src/zed.rs | 8 +- 5 files changed, 307 insertions(+), 217 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ad4736f387..af6e6116c5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -19,26 +19,30 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), - Binding::new("escape", Dismiss, Some("SearchBar")), - Binding::new("cmd-f", FocusEditor, Some("SearchBar")), - Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")), + Binding::new("escape", Dismiss, Some("BufferSearchBar")), + Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")), + Binding::new( + "enter", + SelectMatch(Direction::Next), + Some("BufferSearchBar"), + ), Binding::new( "shift-enter", SelectMatch(Direction::Prev), - Some("SearchBar"), + Some("BufferSearchBar"), ), Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")), Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")), ]); - cx.add_action(SearchBar::deploy); - cx.add_action(SearchBar::dismiss); - cx.add_action(SearchBar::focus_editor); - cx.add_action(SearchBar::toggle_search_option); - cx.add_action(SearchBar::select_match); - cx.add_action(SearchBar::select_match_on_pane); + cx.add_action(BufferSearchBar::deploy); + cx.add_action(BufferSearchBar::dismiss); + cx.add_action(BufferSearchBar::focus_editor); + cx.add_action(BufferSearchBar::toggle_search_option); + cx.add_action(BufferSearchBar::select_match); + cx.add_action(BufferSearchBar::select_match_on_pane); } -pub struct SearchBar { +pub struct BufferSearchBar { query_editor: ViewHandle, active_editor: Option>, active_match_index: Option, @@ -52,13 +56,13 @@ pub struct SearchBar { dismissed: bool, } -impl Entity for SearchBar { +impl Entity for BufferSearchBar { type Event = (); } -impl View for SearchBar { +impl View for BufferSearchBar { fn ui_name() -> &'static str { - "SearchBar" + "BufferSearchBar" } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -132,7 +136,7 @@ impl View for SearchBar { } } -impl ToolbarItemView for SearchBar { +impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { cx.notify(); self.active_editor_subscription.take(); @@ -151,7 +155,7 @@ impl ToolbarItemView for SearchBar { } } -impl SearchBar { +impl BufferSearchBar { pub fn new(cx: &mut ViewContext) -> Self { let query_editor = cx.add_view(|cx| Editor::auto_height(2, Some(|theme| theme.search.editor.clone()), cx)); @@ -295,7 +299,7 @@ impl SearchBar { } fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) { return; } @@ -354,7 +358,7 @@ impl SearchBar { } fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx)); } } @@ -548,7 +552,7 @@ mod tests { }); let search_bar = cx.add_view(Default::default(), |cx| { - let mut search_bar = SearchBar::new(cx); + let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar }); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1415b16da0..2d3bbc5f27 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -6,8 +6,8 @@ use collections::HashMap; use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, - ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, WeakModelHandle, WeakViewHandle, + ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, + ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; use project::{search::SearchQuery, Project}; use std::{ @@ -16,7 +16,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemNavHistory, Settings, Workspace}; +use workspace::{Item, ItemNavHistory, Pane, Settings, ToolbarItemView, Workspace}; action!(Deploy); action!(Search); @@ -31,29 +31,21 @@ struct ActiveSearches(HashMap, WeakViewHandle, } +pub struct ProjectSearchBar { + active_project_search: Option>, + subscription: Option, +} + impl Entity for ProjectSearch { type Event = (); } @@ -154,7 +151,7 @@ impl View for ProjectSearchView { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let model = &self.model.read(cx); - let results = if model.match_ranges.is_empty() { + if model.match_ranges.is_empty() { let theme = &cx.global::().theme; let text = if self.query_editor.read(cx).text(cx).is_empty() { "" @@ -173,12 +170,7 @@ impl View for ProjectSearchView { ChildView::new(&self.results_editor) .flexible(1., true) .boxed() - }; - - Flex::column() - .with_child(self.render_query_editor(cx)) - .with_child(results) - .boxed() + } } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -401,45 +393,12 @@ impl ProjectSearchView { } } - fn search(&mut self, _: &Search, cx: &mut ViewContext) { + fn search(&mut self, cx: &mut ViewContext) { if let Some(query) = self.build_search_query(cx) { self.model.update(cx, |model, cx| model.search(query, cx)); } } - fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = search_view.update(cx, |search_view, cx| { - let new_query = search_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = search_view.model.read(cx).active_query.clone() { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), cx); - }); - search_view.regex = old_query.is_regex(); - search_view.whole_word = old_query.whole_word(); - search_view.case_sensitive = old_query.case_sensitive(); - } - } - new_query - }); - if let Some(new_query) = new_query { - let model = cx.add_model(|cx| { - let mut model = ProjectSearch::new(workspace.project().clone(), cx); - model.search(new_query, cx); - model - }); - workspace.add_item( - Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), - cx, - ); - } - } - } - fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { let text = self.query_editor.read(cx).text(cx); if self.regex { @@ -460,22 +419,7 @@ impl ProjectSearchView { } } - fn toggle_search_option( - &mut self, - ToggleSearchOption(option): &ToggleSearchOption, - cx: &mut ViewContext, - ) { - let value = match option { - SearchOption::WholeWord => &mut self.whole_word, - SearchOption::CaseSensitive => &mut self.case_sensitive, - SearchOption::Regex => &mut self.regex, - }; - *value = !*value; - self.search(&Search, cx); - cx.notify(); - } - - fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { let model = self.model.read(cx); let results_editor = self.results_editor.read(cx); @@ -494,26 +438,6 @@ impl ProjectSearchView { } } - fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { - if self.query_editor.is_focused(cx) { - if !self.model.read(cx).match_ranges.is_empty() { - self.focus_results_editor(cx); - } - } else { - self.focus_query_editor(cx); - } - } - - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { - if self.query_editor.is_focused(cx) { - if !self.model.read(cx).match_ranges.is_empty() { - self.focus_results_editor(cx); - } - } else { - cx.propagate_action() - } - } - fn focus_query_editor(&self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { query_editor.select_all(&SelectAll, cx); @@ -562,97 +486,123 @@ impl ProjectSearchView { cx.notify(); } } +} - fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let editor_container = if self.query_contains_error { - theme.search.invalid_editor +impl ProjectSearchBar { + pub fn new() -> Self { + Self { + active_project_search: Default::default(), + subscription: Default::default(), + } + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| search_view.search(cx)); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + search_view.regex = old_query.is_regex(); + search_view.whole_word = old_query.whole_word(); + search_view.case_sensitive = old_query.case_sensitive(); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectSearch::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.add_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), + cx, + ); + } + } + } + + fn select_match( + pane: &mut Pane, + &SelectMatch(direction): &SelectMatch, + cx: &mut ViewContext, + ) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |search_view, cx| { + search_view.select_match(direction, cx); + }); } else { - theme.search.editor.container - }; - Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .flexible(1., true) - .boxed(), - ) - .with_children(self.active_match_index.map(|match_ix| { - Label::new( - format!( - "{}/{}", - match_ix + 1, - self.model.read(cx).match_ranges.len() - ), - theme.search.match_index.text.clone(), - ) - .contained() - .with_style(theme.search.match_index.container) - .aligned() - .boxed() - })) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_max_width(theme.search.max_editor_width) - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .aligned() - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) - .contained() - .with_style(theme.search.option_button_group) - .aligned() - .boxed(), - ) - .contained() - .with_style(theme.workspace.toolbar.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("project search") + cx.propagate_action(); + } } - fn render_option_button( - &self, - icon: &str, - option: SearchOption, - cx: &mut RenderContext, - ) -> ElementBox { - let is_active = self.is_option_enabled(option); - MouseEventHandler::new::(option as usize, cx, |state, cx| { - let theme = &cx.global::().theme.search; - let style = match (is_active, state.hovered) { - (false, false) => &theme.option_button, - (false, true) => &theme.hovered_option_button, - (true, false) => &theme.active_option_button, - (true, true) => &theme.active_hovered_option_button, - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() + fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.is_focused(cx) { + if !search_view.model.read(cx).match_ranges.is_empty() { + search_view.focus_results_editor(cx); + } + } else { + search_view.focus_query_editor(cx); + } + }); + } else { + cx.propagate_action(); + } } - fn is_option_enabled(&self, option: SearchOption) -> bool { - match option { - SearchOption::WholeWord => self.whole_word, - SearchOption::CaseSensitive => self.case_sensitive, - SearchOption::Regex => self.regex, + fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.is_focused(cx) { + if !search_view.model.read(cx).match_ranges.is_empty() { + search_view.focus_results_editor(cx); + } + } else { + cx.propagate_action(); + } + }); + } else { + cx.propagate_action(); + } + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + let value = match option { + SearchOption::WholeWord => &mut search_view.whole_word, + SearchOption::CaseSensitive => &mut search_view.case_sensitive, + SearchOption::Regex => &mut search_view.regex, + }; + *value = !*value; + search_view.search(cx); + }); + cx.notify(); } } @@ -679,6 +629,139 @@ impl ProjectSearchView { .with_cursor_style(CursorStyle::PointingHand) .boxed() } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let is_active = self.is_option_enabled(option, cx); + MouseEventHandler::new::(option as usize, cx, |state, cx| { + let theme = &cx.global::().theme.search; + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool { + if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + match option { + SearchOption::WholeWord => search.whole_word, + SearchOption::CaseSensitive => search.case_sensitive, + SearchOption::Regex => search.regex, + } + } else { + false + } + } +} + +impl Entity for ProjectSearchBar { + type Event = (); +} + +impl View for ProjectSearchBar { + fn ui_name() -> &'static str { + "ProjectSearchBar" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + let theme = cx.global::().theme.clone(); + let editor_container = if search.query_contains_error { + theme.search.invalid_editor + } else { + theme.search.editor.container + }; + Flex::row() + .with_child( + Flex::row() + .with_child( + ChildView::new(&search.query_editor) + .flexible(1., true) + .boxed(), + ) + .with_children(search.active_match_index.map(|match_ix| { + Label::new( + format!( + "{}/{}", + match_ix + 1, + search.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + .boxed() + })) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.search.max_editor_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button( + "Case", + SearchOption::CaseSensitive, + cx, + )) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.search.option_button_group) + .aligned() + .boxed(), + ) + .contained() + .with_style(theme.workspace.toolbar.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("project search") + } else { + Empty::new().boxed() + } + } +} + +impl ToolbarItemView for ProjectSearchBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::ItemHandle>, + cx: &mut ViewContext, + ) { + self.subscription = None; + self.active_project_search = None; + if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); + self.active_project_search = Some(search); + } + cx.notify(); + } } #[cfg(test)] @@ -728,7 +811,7 @@ mod tests { search_view .query_editor .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(&Search, cx); + search_view.search(cx); }); search_view.next_notification(&cx).await; search_view.update(cx, |search_view, cx| { @@ -765,7 +848,7 @@ mod tests { [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] ); - search_view.select_match(&SelectMatch(Direction::Next), cx); + search_view.select_match(Direction::Next, cx); }); search_view.update(cx, |search_view, cx| { @@ -776,7 +859,7 @@ mod tests { .update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] ); - search_view.select_match(&SelectMatch(Direction::Next), cx); + search_view.select_match(Direction::Next, cx); }); search_view.update(cx, |search_view, cx| { @@ -787,7 +870,7 @@ mod tests { .update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] ); - search_view.select_match(&SelectMatch(Direction::Next), cx); + search_view.select_match(Direction::Next, cx); }); search_view.update(cx, |search_view, cx| { @@ -798,7 +881,7 @@ mod tests { .update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] ); - search_view.select_match(&SelectMatch(Direction::Prev), cx); + search_view.select_match(Direction::Prev, cx); }); search_view.update(cx, |search_view, cx| { @@ -809,7 +892,7 @@ mod tests { .update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] ); - search_view.select_match(&SelectMatch(Direction::Prev), cx); + search_view.select_match(Direction::Prev, cx); }); search_view.update(cx, |search_view, cx| { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index b2543fe261..e1ef1357ce 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,13 +1,14 @@ -pub use buffer_search::SearchBar; +pub use buffer_search::BufferSearchBar; use editor::{Anchor, MultiBufferSnapshot}; use gpui::{action, MutableAppContext}; +pub use project_search::ProjectSearchBar; use std::{ cmp::{self, Ordering}, ops::Range, }; -mod buffer_search; -mod project_search; +pub mod buffer_search; +pub mod project_search; pub fn init(cx: &mut MutableAppContext) { buffer_search::init(cx); diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 73ffaa8898..06e9c4f3b3 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -361,7 +361,7 @@ tab_icon_spacing = 4 tab_summary_spacing = 10 [search] -max_editor_width = 400 +max_editor_width = 250 match_background = "$state.highlighted_line" results_status = { extends = "$text.0", size = 18 } tab_icon_width = 14 diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bfa5b3024d..84cf163b76 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -22,7 +22,7 @@ pub use lsp; use project::Project; pub use project::{self, fs}; use project_panel::ProjectPanel; -use search::SearchBar; +use search::{BufferSearchBar, ProjectSearchBar}; use std::{path::PathBuf, sync::Arc}; pub use workspace; use workspace::{AppState, Settings, Workspace, WorkspaceParams}; @@ -113,8 +113,10 @@ pub fn build_workspace( let breadcrumbs = cx.add_view(|_| Breadcrumbs::new()); toolbar.add_left_item(breadcrumbs, cx); - let search_bar = cx.add_view(|cx| SearchBar::new(cx)); - toolbar.add_right_item(search_bar, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_right_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_right_item(project_search_bar, cx); }) }); })