Select all matches (#2717)
Closes https://github.com/zed-industries/community/issues/75 Closes https://github.com/zed-industries/community/issues/1749 The PR * changes keybindings for `Editor && mode == auto_height` context: before, `alt-enter` and `alt-shift-enter` added new lines in such editors, including the one from buffer search. New bindings are the same as in `Editor && mode == full` context. * adds `search::SelectAllMatches` action and binds it to `Alt + Enter` by default, to select all matches of a buffer search The behavior mimics VSCode: we do not move the screen even if all selections are out of the visible range (Cmd+G will navigate there) and allow reselecting the results from both pane and search field, as long as the search is not dismissed. Release Notes: - Added `search::SelectAllMatches` (`Alt + Enter` default) action to place carets and select all buffer search results ([#75](https://github.com/zed-industries/community/issues/75), [#1749](https://github.com/zed-industries/community/issues/1749)).
This commit is contained in:
commit
cde5b3952d
12 changed files with 337 additions and 31 deletions
|
@ -194,8 +194,8 @@
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == auto_height",
|
"context": "Editor && mode == auto_height",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-enter": "editor::Newline",
|
"shift-enter": "editor::Newline",
|
||||||
"cmd-alt-enter": "editor::NewlineBelow"
|
"cmd-shift-enter": "editor::NewlineBelow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -221,7 +221,8 @@
|
||||||
"escape": "buffer_search::Dismiss",
|
"escape": "buffer_search::Dismiss",
|
||||||
"tab": "buffer_search::FocusEditor",
|
"tab": "buffer_search::FocusEditor",
|
||||||
"enter": "search::SelectNextMatch",
|
"enter": "search::SelectNextMatch",
|
||||||
"shift-enter": "search::SelectPrevMatch"
|
"shift-enter": "search::SelectPrevMatch",
|
||||||
|
"alt-enter": "search::SelectAllMatches"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -242,6 +243,7 @@
|
||||||
"cmd-f": "project_search::ToggleFocus",
|
"cmd-f": "project_search::ToggleFocus",
|
||||||
"cmd-g": "search::SelectNextMatch",
|
"cmd-g": "search::SelectNextMatch",
|
||||||
"cmd-shift-g": "search::SelectPrevMatch",
|
"cmd-shift-g": "search::SelectPrevMatch",
|
||||||
|
"alt-enter": "search::SelectAllMatches",
|
||||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||||
"alt-cmd-w": "search::ToggleWholeWord",
|
"alt-cmd-w": "search::ToggleWholeWord",
|
||||||
"alt-cmd-r": "search::ToggleRegex"
|
"alt-cmd-r": "search::ToggleRegex"
|
||||||
|
|
|
@ -887,10 +887,20 @@ pub(crate) enum BufferSearchHighlights {}
|
||||||
impl SearchableItem for Editor {
|
impl SearchableItem for Editor {
|
||||||
type Match = Range<Anchor>;
|
type Match = Range<Anchor>;
|
||||||
|
|
||||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
|
fn to_search_event(
|
||||||
|
&mut self,
|
||||||
|
event: &Self::Event,
|
||||||
|
_: &mut ViewContext<Self>,
|
||||||
|
) -> Option<SearchEvent> {
|
||||||
match event {
|
match event {
|
||||||
Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
|
Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
|
||||||
Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged),
|
Event::SelectionsChanged { .. } => {
|
||||||
|
if self.selections.disjoint_anchors().len() == 1 {
|
||||||
|
Some(SearchEvent::ActiveMatchChanged)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -941,6 +951,11 @@ impl SearchableItem for Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.unfold_ranges(matches.clone(), false, false, cx);
|
||||||
|
self.change_selections(None, cx, |s| s.select_ranges(matches));
|
||||||
|
}
|
||||||
|
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
matches: &Vec<Range<Anchor>>,
|
matches: &Vec<Range<Anchor>>,
|
||||||
|
@ -949,8 +964,16 @@ impl SearchableItem for Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||||
let cursor = self.selections.newest_anchor().head();
|
let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
|
||||||
if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
|
self.selections.newest_anchor().head()
|
||||||
|
} else {
|
||||||
|
matches[current_index].start
|
||||||
|
};
|
||||||
|
if matches[current_index]
|
||||||
|
.start
|
||||||
|
.cmp(¤t_index_position, &buffer)
|
||||||
|
.is_gt()
|
||||||
|
{
|
||||||
if direction == Direction::Prev {
|
if direction == Direction::Prev {
|
||||||
if current_index == 0 {
|
if current_index == 0 {
|
||||||
current_index = matches.len() - 1;
|
current_index = matches.len() - 1;
|
||||||
|
@ -958,7 +981,11 @@ impl SearchableItem for Editor {
|
||||||
current_index -= 1;
|
current_index -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
|
} else if matches[current_index]
|
||||||
|
.end
|
||||||
|
.cmp(¤t_index_position, &buffer)
|
||||||
|
.is_lt()
|
||||||
|
{
|
||||||
if direction == Direction::Next {
|
if direction == Direction::Next {
|
||||||
current_index = 0;
|
current_index = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,13 @@ use crate::{
|
||||||
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
|
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PendingSelection {
|
pub struct PendingSelection {
|
||||||
pub selection: Selection<Anchor>,
|
pub selection: Selection<Anchor>,
|
||||||
pub mode: SelectMode,
|
pub mode: SelectMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SelectionsCollection {
|
pub struct SelectionsCollection {
|
||||||
display_map: ModelHandle<DisplayMap>,
|
display_map: ModelHandle<DisplayMap>,
|
||||||
buffer: ModelHandle<MultiBuffer>,
|
buffer: ModelHandle<MultiBuffer>,
|
||||||
|
|
|
@ -362,8 +362,13 @@ impl Item for FeedbackEditor {
|
||||||
impl SearchableItem for FeedbackEditor {
|
impl SearchableItem for FeedbackEditor {
|
||||||
type Match = Range<Anchor>;
|
type Match = Range<Anchor>;
|
||||||
|
|
||||||
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
|
fn to_search_event(
|
||||||
Editor::to_search_event(event)
|
&mut self,
|
||||||
|
event: &Self::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<workspace::searchable::SearchEvent> {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.to_search_event(event, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -391,6 +396,11 @@ impl SearchableItem for FeedbackEditor {
|
||||||
.update(cx, |editor, cx| editor.activate_match(index, matches, cx))
|
.update(cx, |editor, cx| editor.activate_match(index, matches, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |e, cx| e.select_matches(matches, cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: project::search::SearchQuery,
|
query: project::search::SearchQuery,
|
||||||
|
|
|
@ -467,8 +467,13 @@ impl Item for LspLogView {
|
||||||
impl SearchableItem for LspLogView {
|
impl SearchableItem for LspLogView {
|
||||||
type Match = <Editor as SearchableItem>::Match;
|
type Match = <Editor as SearchableItem>::Match;
|
||||||
|
|
||||||
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
|
fn to_search_event(
|
||||||
Editor::to_search_event(event)
|
&mut self,
|
||||||
|
event: &Self::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<workspace::searchable::SearchEvent> {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.to_search_event(event, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -494,6 +499,11 @@ impl SearchableItem for LspLogView {
|
||||||
.update(cx, |e, cx| e.activate_match(index, matches, cx))
|
.update(cx, |e, cx| e.activate_match(index, matches, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |e, cx| e.select_matches(matches, cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: project::search::SearchQuery,
|
query: project::search::SearchQuery,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
|
||||||
ToggleWholeWord,
|
ToggleRegex, ToggleWholeWord,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
|
@ -39,8 +39,10 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(BufferSearchBar::focus_editor);
|
cx.add_action(BufferSearchBar::focus_editor);
|
||||||
cx.add_action(BufferSearchBar::select_next_match);
|
cx.add_action(BufferSearchBar::select_next_match);
|
||||||
cx.add_action(BufferSearchBar::select_prev_match);
|
cx.add_action(BufferSearchBar::select_prev_match);
|
||||||
|
cx.add_action(BufferSearchBar::select_all_matches);
|
||||||
cx.add_action(BufferSearchBar::select_next_match_on_pane);
|
cx.add_action(BufferSearchBar::select_next_match_on_pane);
|
||||||
cx.add_action(BufferSearchBar::select_prev_match_on_pane);
|
cx.add_action(BufferSearchBar::select_prev_match_on_pane);
|
||||||
|
cx.add_action(BufferSearchBar::select_all_matches_on_pane);
|
||||||
cx.add_action(BufferSearchBar::handle_editor_cancel);
|
cx.add_action(BufferSearchBar::handle_editor_cancel);
|
||||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||||
|
@ -66,7 +68,7 @@ pub struct BufferSearchBar {
|
||||||
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
||||||
active_match_index: Option<usize>,
|
active_match_index: Option<usize>,
|
||||||
active_searchable_item_subscription: Option<Subscription>,
|
active_searchable_item_subscription: Option<Subscription>,
|
||||||
seachable_items_with_matches:
|
searchable_items_with_matches:
|
||||||
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
||||||
pending_search: Option<Task<()>>,
|
pending_search: Option<Task<()>>,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
@ -118,7 +120,7 @@ impl View for BufferSearchBar {
|
||||||
.with_children(self.active_searchable_item.as_ref().and_then(
|
.with_children(self.active_searchable_item.as_ref().and_then(
|
||||||
|searchable_item| {
|
|searchable_item| {
|
||||||
let matches = self
|
let matches = self
|
||||||
.seachable_items_with_matches
|
.searchable_items_with_matches
|
||||||
.get(&searchable_item.downgrade())?;
|
.get(&searchable_item.downgrade())?;
|
||||||
let message = if let Some(match_ix) = self.active_match_index {
|
let message = if let Some(match_ix) = self.active_match_index {
|
||||||
format!("{}/{}", match_ix + 1, matches.len())
|
format!("{}/{}", match_ix + 1, matches.len())
|
||||||
|
@ -146,6 +148,7 @@ impl View for BufferSearchBar {
|
||||||
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))
|
||||||
|
.with_child(self.render_action_button("Select All", cx))
|
||||||
.aligned(),
|
.aligned(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -249,7 +252,7 @@ impl BufferSearchBar {
|
||||||
active_searchable_item: None,
|
active_searchable_item: None,
|
||||||
active_searchable_item_subscription: None,
|
active_searchable_item_subscription: None,
|
||||||
active_match_index: None,
|
active_match_index: None,
|
||||||
seachable_items_with_matches: Default::default(),
|
searchable_items_with_matches: Default::default(),
|
||||||
case_sensitive: false,
|
case_sensitive: false,
|
||||||
whole_word: false,
|
whole_word: false,
|
||||||
regex: false,
|
regex: false,
|
||||||
|
@ -265,7 +268,7 @@ impl BufferSearchBar {
|
||||||
|
|
||||||
pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||||
self.dismissed = true;
|
self.dismissed = true;
|
||||||
for searchable_item in self.seachable_items_with_matches.keys() {
|
for searchable_item in self.searchable_items_with_matches.keys() {
|
||||||
if let Some(searchable_item) =
|
if let Some(searchable_item) =
|
||||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
||||||
{
|
{
|
||||||
|
@ -401,6 +404,37 @@ impl BufferSearchBar {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_action_button(
|
||||||
|
&self,
|
||||||
|
icon: &'static str,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> AnyElement<Self> {
|
||||||
|
let tooltip = "Select All Matches";
|
||||||
|
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||||
|
let action_type_id = 0_usize;
|
||||||
|
|
||||||
|
enum ActionButton {}
|
||||||
|
MouseEventHandler::<ActionButton, _>::new(action_type_id, cx, |state, cx| {
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
let style = theme.search.action_button.style_for(state);
|
||||||
|
Label::new(icon, style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.select_all_matches(&SelectAllMatches, cx)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.with_tooltip::<ActionButton>(
|
||||||
|
action_type_id,
|
||||||
|
tooltip.to_string(),
|
||||||
|
Some(Box::new(SelectAllMatches)),
|
||||||
|
tooltip_style,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_close_button(
|
fn render_close_button(
|
||||||
&self,
|
&self,
|
||||||
theme: &theme::Search,
|
theme: &theme::Search,
|
||||||
|
@ -488,11 +522,25 @@ impl BufferSearchBar {
|
||||||
self.select_match(Direction::Prev, cx);
|
self.select_match(Direction::Prev, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
|
||||||
|
if !self.dismissed {
|
||||||
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
||||||
|
if let Some(matches) = self
|
||||||
|
.searchable_items_with_matches
|
||||||
|
.get(&searchable_item.downgrade())
|
||||||
|
{
|
||||||
|
searchable_item.select_matches(matches, cx);
|
||||||
|
self.focus_editor(&FocusEditor, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(index) = self.active_match_index {
|
if let Some(index) = self.active_match_index {
|
||||||
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
||||||
if let Some(matches) = self
|
if let Some(matches) = self
|
||||||
.seachable_items_with_matches
|
.searchable_items_with_matches
|
||||||
.get(&searchable_item.downgrade())
|
.get(&searchable_item.downgrade())
|
||||||
{
|
{
|
||||||
let new_match_index =
|
let new_match_index =
|
||||||
|
@ -524,6 +572,16 @@ impl BufferSearchBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_all_matches_on_pane(
|
||||||
|
pane: &mut Pane,
|
||||||
|
action: &SelectAllMatches,
|
||||||
|
cx: &mut ViewContext<Pane>,
|
||||||
|
) {
|
||||||
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
|
search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_query_editor_event(
|
fn on_query_editor_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: ViewHandle<Editor>,
|
_: ViewHandle<Editor>,
|
||||||
|
@ -547,7 +605,7 @@ impl BufferSearchBar {
|
||||||
|
|
||||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let mut active_item_matches = None;
|
let mut active_item_matches = None;
|
||||||
for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
|
for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
|
||||||
if let Some(searchable_item) =
|
if let Some(searchable_item) =
|
||||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
||||||
{
|
{
|
||||||
|
@ -559,7 +617,7 @@ impl BufferSearchBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.seachable_items_with_matches
|
self.searchable_items_with_matches
|
||||||
.extend(active_item_matches);
|
.extend(active_item_matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,13 +663,13 @@ impl BufferSearchBar {
|
||||||
if let Some(active_searchable_item) =
|
if let Some(active_searchable_item) =
|
||||||
WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
|
WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
|
||||||
{
|
{
|
||||||
this.seachable_items_with_matches
|
this.searchable_items_with_matches
|
||||||
.insert(active_searchable_item.downgrade(), matches);
|
.insert(active_searchable_item.downgrade(), matches);
|
||||||
|
|
||||||
this.update_match_index(cx);
|
this.update_match_index(cx);
|
||||||
if !this.dismissed {
|
if !this.dismissed {
|
||||||
let matches = this
|
let matches = this
|
||||||
.seachable_items_with_matches
|
.searchable_items_with_matches
|
||||||
.get(&active_searchable_item.downgrade())
|
.get(&active_searchable_item.downgrade())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_searchable_item.update_matches(matches, cx);
|
active_searchable_item.update_matches(matches, cx);
|
||||||
|
@ -637,7 +695,7 @@ impl BufferSearchBar {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|searchable_item| {
|
.and_then(|searchable_item| {
|
||||||
let matches = self
|
let matches = self
|
||||||
.seachable_items_with_matches
|
.searchable_items_with_matches
|
||||||
.get(&searchable_item.downgrade())?;
|
.get(&searchable_item.downgrade())?;
|
||||||
searchable_item.active_match_index(matches, cx)
|
searchable_item.active_match_index(matches, cx)
|
||||||
});
|
});
|
||||||
|
@ -966,4 +1024,133 @@ mod tests {
|
||||||
assert_eq!(search_bar.active_match_index, Some(2));
|
assert_eq!(search_bar.active_match_index, Some(2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_search_select_all_matches(cx: &mut TestAppContext) {
|
||||||
|
crate::project_search::tests::init_test(cx);
|
||||||
|
|
||||||
|
let buffer_text = r#"
|
||||||
|
A regular expression (shortened as regex or regexp;[1] also referred to as
|
||||||
|
rational expression[2][3]) is a sequence of characters that specifies a search
|
||||||
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
||||||
|
for "find" or "find and replace" operations on strings, or for input validation.
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
let expected_query_matches_count = buffer_text
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.to_ascii_lowercase() == 'a')
|
||||||
|
.count();
|
||||||
|
assert!(
|
||||||
|
expected_query_matches_count > 1,
|
||||||
|
"Should pick a query with multiple results"
|
||||||
|
);
|
||||||
|
let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
|
||||||
|
let (window_id, _root_view) = cx.add_window(|_| EmptyView);
|
||||||
|
|
||||||
|
let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||||
|
|
||||||
|
let search_bar = cx.add_view(window_id, |cx| {
|
||||||
|
let mut search_bar = BufferSearchBar::new(cx);
|
||||||
|
search_bar.set_active_pane_item(Some(&editor), cx);
|
||||||
|
search_bar.show(false, true, cx);
|
||||||
|
search_bar
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
search_bar.set_query("a", cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.next_notification(cx).await;
|
||||||
|
let initial_selections = editor.update(cx, |editor, cx| {
|
||||||
|
let initial_selections = editor.selections.display_ranges(cx);
|
||||||
|
assert_eq!(
|
||||||
|
initial_selections.len(), 1,
|
||||||
|
"Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
|
||||||
|
);
|
||||||
|
initial_selections
|
||||||
|
});
|
||||||
|
search_bar.update(cx, |search_bar, _| {
|
||||||
|
assert_eq!(search_bar.active_match_index, Some(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
search_bar.select_all_matches(&SelectAllMatches, cx);
|
||||||
|
let all_selections =
|
||||||
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
|
||||||
|
assert_eq!(
|
||||||
|
all_selections.len(),
|
||||||
|
expected_query_matches_count,
|
||||||
|
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
search_bar.update(cx, |search_bar, _| {
|
||||||
|
assert_eq!(
|
||||||
|
search_bar.active_match_index,
|
||||||
|
Some(0),
|
||||||
|
"Match index should not change after selecting all matches"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
search_bar.select_next_match(&SelectNextMatch, cx);
|
||||||
|
let all_selections =
|
||||||
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
|
||||||
|
assert_eq!(
|
||||||
|
all_selections.len(),
|
||||||
|
1,
|
||||||
|
"On next match, should deselect items and select the next match"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
all_selections, initial_selections,
|
||||||
|
"Next match should be different from the first selection"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
search_bar.update(cx, |search_bar, _| {
|
||||||
|
assert_eq!(
|
||||||
|
search_bar.active_match_index,
|
||||||
|
Some(1),
|
||||||
|
"Match index should be updated to the next one"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
search_bar.select_all_matches(&SelectAllMatches, cx);
|
||||||
|
let all_selections =
|
||||||
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
|
||||||
|
assert_eq!(
|
||||||
|
all_selections.len(),
|
||||||
|
expected_query_matches_count,
|
||||||
|
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
search_bar.update(cx, |search_bar, _| {
|
||||||
|
assert_eq!(
|
||||||
|
search_bar.active_match_index,
|
||||||
|
Some(1),
|
||||||
|
"Match index should not change after selecting all matches"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
search_bar.select_prev_match(&SelectPrevMatch, cx);
|
||||||
|
let all_selections =
|
||||||
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
|
||||||
|
assert_eq!(
|
||||||
|
all_selections.len(),
|
||||||
|
1,
|
||||||
|
"On previous match, should deselect items and select the previous item"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_selections, initial_selections,
|
||||||
|
"Previous match should be the same as the first selection"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
search_bar.update(cx, |search_bar, _| {
|
||||||
|
assert_eq!(
|
||||||
|
search_bar.active_match_index,
|
||||||
|
Some(0),
|
||||||
|
"Match index should be updated to the previous one"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@ actions!(
|
||||||
ToggleCaseSensitive,
|
ToggleCaseSensitive,
|
||||||
ToggleRegex,
|
ToggleRegex,
|
||||||
SelectNextMatch,
|
SelectNextMatch,
|
||||||
SelectPrevMatch
|
SelectPrevMatch,
|
||||||
|
SelectAllMatches,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -908,6 +908,21 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_matches(&mut self, matches: Vec<RangeInclusive<Point>>) {
|
||||||
|
let matches_to_select = self
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.filter(|self_match| matches.contains(self_match))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for match_to_select in matches_to_select {
|
||||||
|
self.set_selection(Some((
|
||||||
|
make_selection(&match_to_select),
|
||||||
|
*match_to_select.end(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
|
fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
|
||||||
self.events
|
self.events
|
||||||
.push_back(InternalEvent::SetSelection(selection));
|
.push_back(InternalEvent::SetSelection(selection));
|
||||||
|
|
|
@ -647,7 +647,11 @@ impl SearchableItem for TerminalView {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert events raised by this item into search-relevant events (if applicable)
|
/// Convert events raised by this item into search-relevant events (if applicable)
|
||||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
|
fn to_search_event(
|
||||||
|
&mut self,
|
||||||
|
event: &Self::Event,
|
||||||
|
_: &mut ViewContext<Self>,
|
||||||
|
) -> Option<SearchEvent> {
|
||||||
match event {
|
match event {
|
||||||
Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
|
Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
|
||||||
Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
|
Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
|
||||||
|
@ -682,6 +686,13 @@ impl SearchableItem for TerminalView {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add selections for all matches given.
|
||||||
|
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.terminal()
|
||||||
|
.update(cx, |term, _| term.select_matches(matches));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all of the matches for this query, should be done on the background
|
/// Get all of the matches for this query, should be done on the background
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
@ -379,6 +379,7 @@ pub struct Search {
|
||||||
pub invalid_include_exclude_editor: ContainerStyle,
|
pub invalid_include_exclude_editor: ContainerStyle,
|
||||||
pub include_exclude_inputs: ContainedText,
|
pub include_exclude_inputs: ContainedText,
|
||||||
pub option_button: Toggleable<Interactive<ContainedText>>,
|
pub option_button: Toggleable<Interactive<ContainedText>>,
|
||||||
|
pub action_button: Interactive<ContainedText>,
|
||||||
pub match_background: Color,
|
pub match_background: Color,
|
||||||
pub match_index: ContainedText,
|
pub match_index: ContainedText,
|
||||||
pub results_status: TextStyle,
|
pub results_status: TextStyle,
|
||||||
|
|
|
@ -37,7 +37,11 @@ pub trait SearchableItem: Item {
|
||||||
regex: true,
|
regex: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
|
fn to_search_event(
|
||||||
|
&mut self,
|
||||||
|
event: &Self::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<SearchEvent>;
|
||||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
||||||
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
|
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
|
||||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
||||||
|
@ -47,6 +51,7 @@ pub trait SearchableItem: Item {
|
||||||
matches: Vec<Self::Match>,
|
matches: Vec<Self::Match>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
);
|
);
|
||||||
|
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
matches: &Vec<Self::Match>,
|
matches: &Vec<Self::Match>,
|
||||||
|
@ -102,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle {
|
||||||
matches: &Vec<Box<dyn Any + Send>>,
|
matches: &Vec<Box<dyn Any + Send>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
);
|
);
|
||||||
|
fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&self,
|
&self,
|
||||||
matches: &Vec<Box<dyn Any + Send>>,
|
matches: &Vec<Box<dyn Any + Send>>,
|
||||||
|
@ -139,8 +145,9 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
|
handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
|
||||||
) -> Subscription {
|
) -> Subscription {
|
||||||
cx.subscribe(self, move |_, event, cx| {
|
cx.subscribe(self, move |handle, event, cx| {
|
||||||
if let Some(search_event) = T::to_search_event(event) {
|
let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx));
|
||||||
|
if let Some(search_event) = search_event {
|
||||||
handler(search_event, cx)
|
handler(search_event, cx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -165,6 +172,12 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
||||||
let matches = downcast_matches(matches);
|
let matches = downcast_matches(matches);
|
||||||
self.update(cx, |this, cx| this.activate_match(index, matches, cx));
|
self.update(cx, |this, cx| this.activate_match(index, matches, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
|
||||||
|
let matches = downcast_matches(matches);
|
||||||
|
self.update(cx, |this, cx| this.select_matches(matches, cx));
|
||||||
|
}
|
||||||
|
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&self,
|
&self,
|
||||||
matches: &Vec<Box<dyn Any + Send>>,
|
matches: &Vec<Box<dyn Any + Send>>,
|
||||||
|
|
|
@ -83,6 +83,35 @@ export default function search(): any {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
action_button: interactive({
|
||||||
|
base: {
|
||||||
|
...text(theme.highest, "mono", "on"),
|
||||||
|
background: background(theme.highest, "on"),
|
||||||
|
corner_radius: 6,
|
||||||
|
border: border(theme.highest, "on"),
|
||||||
|
margin: {
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
bottom: 2,
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
top: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
...text(theme.highest, "mono", "on", "hovered"),
|
||||||
|
background: background(theme.highest, "on", "hovered"),
|
||||||
|
border: border(theme.highest, "on", "hovered"),
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
...text(theme.highest, "mono", "on", "pressed"),
|
||||||
|
background: background(theme.highest, "on", "pressed"),
|
||||||
|
border: border(theme.highest, "on", "pressed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
editor,
|
editor,
|
||||||
invalid_editor: {
|
invalid_editor: {
|
||||||
...editor,
|
...editor,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue