Rebuild vim search experience on refactored code
This commit is contained in:
parent
232d14a3ae
commit
b4b0f622de
8 changed files with 226 additions and 116 deletions
|
@ -357,7 +357,7 @@
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "buffer_search::FocusEditor",
|
"enter": "vim::SearchSubmit",
|
||||||
"escape": "buffer_search::Dismiss"
|
"escape": "buffer_search::Dismiss"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,13 +330,13 @@ impl AssistantPanel {
|
||||||
|
|
||||||
fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
|
fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx));
|
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, None, cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx));
|
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, None, cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -937,7 +937,9 @@ impl SearchableItem for Editor {
|
||||||
) {
|
) {
|
||||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||||
let range = self.range_for_match(&matches[index]);
|
let range = self.range_for_match(&matches[index]);
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| s.select_ranges([range]));
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
s.select_ranges([range]);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
|
@ -945,11 +947,12 @@ impl SearchableItem for Editor {
|
||||||
matches: &Vec<Range<Anchor>>,
|
matches: &Vec<Range<Anchor>>,
|
||||||
mut current_index: usize,
|
mut current_index: usize,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
count: Option<usize>,
|
||||||
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 cursor = self.selections.newest_anchor().head();
|
||||||
if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
|
if count.is_none() && matches[current_index].start.cmp(&cursor, &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;
|
||||||
|
@ -957,22 +960,19 @@ impl SearchableItem for Editor {
|
||||||
current_index -= 1;
|
current_index -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
|
} else if count.is_none() && matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
|
||||||
if direction == Direction::Next {
|
if direction == Direction::Next {
|
||||||
current_index = 0;
|
current_index = 0;
|
||||||
}
|
}
|
||||||
} else if direction == Direction::Prev {
|
} else if direction == Direction::Prev {
|
||||||
if current_index == 0 {
|
let count = count.unwrap_or(1) % matches.len();
|
||||||
current_index = matches.len() - 1;
|
if current_index >= count {
|
||||||
|
current_index = current_index - count;
|
||||||
} else {
|
} else {
|
||||||
current_index -= 1;
|
current_index = matches.len() - (count - current_index);
|
||||||
}
|
}
|
||||||
} else if direction == Direction::Next {
|
} else if direction == Direction::Next {
|
||||||
if current_index == matches.len() - 1 {
|
current_index = (current_index + count.unwrap_or(1)) % matches.len()
|
||||||
current_index = 0
|
|
||||||
} else {
|
|
||||||
current_index += 1;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
current_index
|
current_index
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,6 @@ pub struct BufferSearchBar {
|
||||||
pub query_editor: ViewHandle<Editor>,
|
pub query_editor: ViewHandle<Editor>,
|
||||||
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
||||||
active_match_index: Option<usize>,
|
active_match_index: Option<usize>,
|
||||||
pending_match_direction: Option<Direction>,
|
|
||||||
active_searchable_item_subscription: Option<Subscription>,
|
active_searchable_item_subscription: Option<Subscription>,
|
||||||
seachable_items_with_matches:
|
seachable_items_with_matches:
|
||||||
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
||||||
|
@ -254,7 +253,6 @@ impl BufferSearchBar {
|
||||||
default_options: SearchOptions::NONE,
|
default_options: SearchOptions::NONE,
|
||||||
search_options: SearchOptions::NONE,
|
search_options: SearchOptions::NONE,
|
||||||
pending_search: None,
|
pending_search: None,
|
||||||
pending_match_direction: None,
|
|
||||||
query_contains_error: false,
|
query_contains_error: false,
|
||||||
dismissed: true,
|
dismissed: true,
|
||||||
}
|
}
|
||||||
|
@ -281,12 +279,9 @@ impl BufferSearchBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
if self.active_searchable_item.is_none() {
|
||||||
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
self.dismissed = false;
|
self.dismissed = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
cx.emit(Event::UpdateLocation);
|
cx.emit(Event::UpdateLocation);
|
||||||
|
@ -296,44 +291,53 @@ impl BufferSearchBar {
|
||||||
pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let search = self
|
let search = self
|
||||||
.query_suggestion(cx)
|
.query_suggestion(cx)
|
||||||
.map(|suggestion| self.search(&suggestion, self.default_options, cx));
|
.map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
|
||||||
|
|
||||||
if let Some(search) = search {
|
if let Some(search) = search {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
search.await?;
|
search.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
|
||||||
if let Some(match_ix) = this.active_match_index {
|
|
||||||
if let Some(active_searchable_item) = this.active_searchable_item.as_ref() {
|
|
||||||
if let Some(matches) = this
|
|
||||||
.seachable_items_with_matches
|
|
||||||
.get(&active_searchable_item.downgrade())
|
|
||||||
{
|
|
||||||
active_searchable_item.activate_match(match_ix, matches, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(match_ix) = self.active_match_index {
|
||||||
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
||||||
|
if let Some(matches) = self
|
||||||
|
.seachable_items_with_matches
|
||||||
|
.get(&active_searchable_item.downgrade())
|
||||||
|
{
|
||||||
|
active_searchable_item.activate_match(match_ix, matches, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.query_editor.update(cx, |query_editor, cx| {
|
self.query_editor.update(cx, |query_editor, cx| {
|
||||||
query_editor.select_all(&Default::default(), cx);
|
query_editor.select_all(&Default::default(), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_suggestion(&self, cx: &mut ViewContext<Self>) -> Option<String> {
|
pub fn query(&self, cx: &WindowContext) -> String {
|
||||||
Some(self.active_searchable_item.as_ref()?.query_suggestion(cx))
|
self.query_editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search(
|
pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
|
||||||
|
self.active_searchable_item
|
||||||
|
.as_ref()
|
||||||
|
.map(|searchable_item| searchable_item.query_suggestion(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: &str,
|
query: &str,
|
||||||
options: SearchOptions,
|
options: Option<SearchOptions>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> oneshot::Receiver<()> {
|
) -> oneshot::Receiver<()> {
|
||||||
|
let options = options.unwrap_or(self.default_options);
|
||||||
if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
|
if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
|
||||||
self.query_editor.update(cx, |query_editor, cx| {
|
self.query_editor.update(cx, |query_editor, cx| {
|
||||||
query_editor.buffer().update(cx, |query_buffer, cx| {
|
query_editor.buffer().update(cx, |query_buffer, cx| {
|
||||||
|
@ -499,7 +503,7 @@ impl BufferSearchBar {
|
||||||
cx.propagate_action();
|
cx.propagate_action();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
|
pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
||||||
cx.focus(active_editor.as_any());
|
cx.focus(active_editor.as_any());
|
||||||
}
|
}
|
||||||
|
@ -512,23 +516,37 @@ impl BufferSearchBar {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_search_options(
|
||||||
|
&mut self,
|
||||||
|
search_options: SearchOptions,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.search_options = search_options;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
|
fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
|
||||||
self.select_match(Direction::Next, cx);
|
self.select_match(Direction::Next, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
||||||
self.select_match(Direction::Prev, cx);
|
self.select_match(Direction::Prev, None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
pub fn select_match(
|
||||||
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
|
count: Option<usize>,
|
||||||
|
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
|
.seachable_items_with_matches
|
||||||
.get(&searchable_item.downgrade())
|
.get(&searchable_item.downgrade())
|
||||||
{
|
{
|
||||||
let new_match_index =
|
let new_match_index = searchable_item
|
||||||
searchable_item.match_index_for_direction(matches, index, direction, cx);
|
.match_index_for_direction(matches, index, direction, count, cx);
|
||||||
searchable_item.update_matches(matches, cx);
|
searchable_item.update_matches(matches, cx);
|
||||||
searchable_item.activate_match(new_match_index, matches, cx);
|
searchable_item.activate_match(new_match_index, matches, cx);
|
||||||
}
|
}
|
||||||
|
@ -563,15 +581,12 @@ impl BufferSearchBar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let editor::Event::Edited { .. } = event {
|
if let editor::Event::Edited { .. } = event {
|
||||||
let query = self.query_editor.read(cx).text(cx);
|
|
||||||
let search = self.search(&query, self.search_options, cx);
|
|
||||||
self.query_contains_error = false;
|
self.query_contains_error = false;
|
||||||
self.clear_matches(cx);
|
self.clear_matches(cx);
|
||||||
let search = self.update_matches(cx);
|
let search = self.update_matches(cx);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
search.await?;
|
search.await?;
|
||||||
this.update(&mut cx, |this, cx| this.select_match(Direction::Next, cx))?;
|
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
@ -611,7 +626,6 @@ impl BufferSearchBar {
|
||||||
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
self.active_match_index.take();
|
self.active_match_index.take();
|
||||||
self.pending_match_direction.take();
|
|
||||||
active_searchable_item.clear_matches(cx);
|
active_searchable_item.clear_matches(cx);
|
||||||
let _ = done_tx.send(());
|
let _ = done_tx.send(());
|
||||||
} else {
|
} else {
|
||||||
|
@ -733,10 +747,9 @@ mod tests {
|
||||||
// Search for a string that appears with different casing.
|
// Search for a string that appears with different casing.
|
||||||
// By default, search is case-insensitive.
|
// By default, search is case-insensitive.
|
||||||
search_bar
|
search_bar
|
||||||
.update(cx, |search_bar, cx| {
|
.update(cx, |search_bar, cx| search_bar.search("us", None, cx))
|
||||||
search_bar.search("us", search_bar.default_options, cx)
|
.await
|
||||||
})
|
.unwrap();
|
||||||
.await;
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.all_background_highlights(cx),
|
editor.all_background_highlights(cx),
|
||||||
|
@ -770,10 +783,10 @@ mod tests {
|
||||||
|
|
||||||
// Search for a string that appears both as a whole word and
|
// Search for a string that appears both as a whole word and
|
||||||
// within other words. By default, all results are found.
|
// within other words. By default, all results are found.
|
||||||
search_bar.update(cx, |search_bar, cx| {
|
search_bar
|
||||||
search_bar.search("or", search_bar.default_options, cx);
|
.update(cx, |search_bar, cx| search_bar.search("or", None, cx))
|
||||||
});
|
.await
|
||||||
editor.next_notification(cx).await;
|
.unwrap();
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.all_background_highlights(cx),
|
editor.all_background_highlights(cx),
|
||||||
|
|
|
@ -627,7 +627,7 @@ impl ProjectSearchView {
|
||||||
if let Some(index) = self.active_match_index {
|
if let Some(index) = self.active_match_index {
|
||||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||||
let new_index = self.results_editor.update(cx, |editor, cx| {
|
let new_index = self.results_editor.update(cx, |editor, cx| {
|
||||||
editor.match_index_for_direction(&match_ranges, index, direction, cx)
|
editor.match_index_for_direction(&match_ranges, index, direction, None, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let range_to_select = match_ranges[new_index].clone();
|
let range_to_select = match_ranges[new_index].clone();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use gpui::{impl_actions, AppContext, ViewContext};
|
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
||||||
use search::{BufferSearchBar, SearchOptions};
|
use search::{buffer_search, BufferSearchBar, SearchOptions};
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use workspace::{searchable::Direction, Workspace};
|
use workspace::{searchable::Direction, Pane, Workspace};
|
||||||
|
|
||||||
use crate::Vim;
|
use crate::{state::SearchState, Vim};
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -26,11 +26,14 @@ pub(crate) struct Search {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
|
impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
|
||||||
|
actions!(vim, [SearchSubmit]);
|
||||||
|
|
||||||
pub(crate) fn init(cx: &mut AppContext) {
|
pub(crate) fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(move_to_next);
|
cx.add_action(move_to_next);
|
||||||
cx.add_action(move_to_prev);
|
cx.add_action(move_to_prev);
|
||||||
cx.add_action(search);
|
cx.add_action(search);
|
||||||
|
cx.add_action(search_submit);
|
||||||
|
cx.add_action(search_deploy);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
|
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
|
||||||
|
@ -43,19 +46,68 @@ fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewCon
|
||||||
|
|
||||||
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
|
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
pane.update(cx, |pane, cx| {
|
let direction = if action.backwards {
|
||||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
Direction::Prev
|
||||||
search_bar.update(cx, |search_bar, cx| {
|
} else {
|
||||||
let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
|
Direction::Next
|
||||||
let direction = if action.backwards {
|
};
|
||||||
Direction::Prev
|
Vim::update(cx, |vim, cx| {
|
||||||
} else {
|
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||||
Direction::Next
|
pane.update(cx, |pane, cx| {
|
||||||
};
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.select_match(direction, cx);
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
// search_bar.show_with_options(true, false, options, cx);
|
if !search_bar.show(cx) {
|
||||||
})
|
return;
|
||||||
}
|
}
|
||||||
|
let query = search_bar.query(cx);
|
||||||
|
|
||||||
|
search_bar.select_query(cx);
|
||||||
|
cx.focus_self();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
search_bar.set_search_options(
|
||||||
|
SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vim.state.search = SearchState {
|
||||||
|
direction,
|
||||||
|
count,
|
||||||
|
initial_query: query,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
|
||||||
|
Vim::update(cx, |vim, _| vim.state.search = Default::default());
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
let pane = workspace.active_pane().clone();
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
let mut state = &mut vim.state.search;
|
||||||
|
let mut count = state.count;
|
||||||
|
|
||||||
|
// in the case that the query has changed, the search bar
|
||||||
|
// will have selected the next match already.
|
||||||
|
if (search_bar.query(cx) != state.initial_query)
|
||||||
|
&& state.direction == Direction::Next
|
||||||
|
{
|
||||||
|
count = count.saturating_sub(1);
|
||||||
|
}
|
||||||
|
search_bar.select_match(state.direction, Some(count), cx);
|
||||||
|
state.count = 1;
|
||||||
|
search_bar.focus_editor(&Default::default(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,18 +119,32 @@ pub fn move_to_internal(
|
||||||
) {
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
|
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, |search_bar, cx| {
|
let search = search_bar.update(cx, |search_bar, cx| {
|
||||||
// let mut options = SearchOptions::CASE_SENSITIVE;
|
let mut options = SearchOptions::CASE_SENSITIVE;
|
||||||
// options.set(SearchOptions::WHOLE_WORD, whole_word);
|
options.set(SearchOptions::WHOLE_WORD, whole_word);
|
||||||
// search_bar.show(false, false, cx);
|
if search_bar.show(cx) {
|
||||||
// let word = search_bar.query_suggestion();
|
search_bar
|
||||||
// search_bar.show()
|
.query_suggestion(cx)
|
||||||
// search_bar.search(word, options)
|
.map(|query| search_bar.search(&query, Some(options), cx))
|
||||||
|
} else {
|
||||||
// search_bar.select_word_under_cursor(direction, options, cx);
|
None
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(search) = search {
|
||||||
|
let search_bar = search_bar.downgrade();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
search.await?;
|
||||||
|
search_bar.update(&mut cx, |search_bar, cx| {
|
||||||
|
search_bar.select_match(direction, Some(count), cx)
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
vim.clear_operator(cx);
|
vim.clear_operator(cx);
|
||||||
|
@ -100,15 +166,6 @@ mod test {
|
||||||
deterministic: Arc<gpui::executor::Deterministic>,
|
deterministic: Arc<gpui::executor::Deterministic>,
|
||||||
) {
|
) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
let search_bar = cx.workspace(|workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.active_pane()
|
|
||||||
.read(cx)
|
|
||||||
.toolbar()
|
|
||||||
.read(cx)
|
|
||||||
.item_of_type::<BufferSearchBar>()
|
|
||||||
.expect("Buffer search bar should be deployed")
|
|
||||||
});
|
|
||||||
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||||
|
|
||||||
cx.simulate_keystrokes(["*"]);
|
cx.simulate_keystrokes(["*"]);
|
||||||
|
@ -127,6 +184,10 @@ mod test {
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["2", "*"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||||
|
|
||||||
cx.simulate_keystrokes(["g", "*"]);
|
cx.simulate_keystrokes(["g", "*"]);
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||||
|
@ -140,7 +201,10 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_search(cx: &mut gpui::TestAppContext) {
|
async fn test_search(
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
deterministic: Arc<gpui::executor::Deterministic>,
|
||||||
|
) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||||
|
@ -160,8 +224,7 @@ mod test {
|
||||||
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
|
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
|
||||||
});
|
});
|
||||||
|
|
||||||
// wait for the query editor change event to fire.
|
deterministic.run_until_parked();
|
||||||
search_bar.next_notification(&cx).await;
|
|
||||||
|
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
let highlights = editor.all_background_highlights(cx);
|
let highlights = editor.all_background_highlights(cx);
|
||||||
|
@ -173,30 +236,49 @@ mod test {
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.simulate_keystrokes(["enter"]);
|
cx.simulate_keystrokes(["enter"]);
|
||||||
|
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
||||||
|
|
||||||
// n to go to next/N to go to previous
|
// n to go to next/N to go to previous
|
||||||
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
|
||||||
cx.simulate_keystrokes(["n"]);
|
cx.simulate_keystrokes(["n"]);
|
||||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||||
cx.simulate_keystrokes(["shift-n"]);
|
cx.simulate_keystrokes(["shift-n"]);
|
||||||
|
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
||||||
|
|
||||||
// ?<enter> to go to previous
|
// ?<enter> to go to previous
|
||||||
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
|
||||||
cx.simulate_keystrokes(["?", "enter"]);
|
cx.simulate_keystrokes(["?", "enter"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||||
cx.simulate_keystrokes(["?", "enter"]);
|
cx.simulate_keystrokes(["?", "enter"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||||
|
|
||||||
// /<enter> to go to next
|
// /<enter> to go to next
|
||||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
|
||||||
cx.simulate_keystrokes(["/", "enter"]);
|
cx.simulate_keystrokes(["/", "enter"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||||
|
|
||||||
// ?{search}<enter> to search backwards
|
// ?{search}<enter> to search backwards
|
||||||
cx.simulate_keystrokes(["?", "b", "enter"]);
|
cx.simulate_keystrokes(["?", "b", "enter"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
// wait for the query editor change event to fire.
|
|
||||||
search_bar.next_notification(&cx).await;
|
|
||||||
|
|
||||||
cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||||
|
|
||||||
|
// works with counts
|
||||||
|
cx.simulate_keystrokes(["4", "/", "c"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.simulate_keystrokes(["enter"]);
|
||||||
|
cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
|
||||||
|
|
||||||
|
// check that searching resumes from cursor, not previous match
|
||||||
|
cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes(["/", "d"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.simulate_keystrokes(["enter"]);
|
||||||
|
cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
|
||||||
|
cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
|
||||||
|
cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes(["/", "b"]);
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.simulate_keystrokes(["enter"]);
|
||||||
|
cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use gpui::keymap_matcher::KeymapContext;
|
use gpui::keymap_matcher::KeymapContext;
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use workspace::searchable::Direction;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
|
@ -38,6 +39,23 @@ pub enum Operator {
|
||||||
pub struct VimState {
|
pub struct VimState {
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub operator_stack: Vec<Operator>,
|
pub operator_stack: Vec<Operator>,
|
||||||
|
pub search: SearchState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SearchState {
|
||||||
|
pub direction: Direction,
|
||||||
|
pub count: usize,
|
||||||
|
pub initial_query: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SearchState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
direction: Direction::Next,
|
||||||
|
count: 1,
|
||||||
|
initial_query: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VimState {
|
impl VimState {
|
||||||
|
|
|
@ -50,26 +50,21 @@ pub trait SearchableItem: Item {
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
matches: &Vec<Self::Match>,
|
matches: &Vec<Self::Match>,
|
||||||
mut current_index: usize,
|
current_index: usize,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
count: Option<usize>,
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match direction {
|
match direction {
|
||||||
Direction::Prev => {
|
Direction::Prev => {
|
||||||
if current_index == 0 {
|
let count = count.unwrap_or(1) % matches.len();
|
||||||
matches.len() - 1
|
if current_index >= count {
|
||||||
|
current_index - count
|
||||||
} else {
|
} else {
|
||||||
current_index - 1
|
matches.len() - (count - current_index)
|
||||||
}
|
|
||||||
}
|
|
||||||
Direction::Next => {
|
|
||||||
current_index += 1;
|
|
||||||
if current_index == matches.len() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
current_index
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Direction::Next => (current_index + count.unwrap_or(1)) % matches.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
|
@ -107,6 +102,7 @@ pub trait SearchableItemHandle: ItemHandle {
|
||||||
matches: &Vec<Box<dyn Any + Send>>,
|
matches: &Vec<Box<dyn Any + Send>>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
count: Option<usize>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> usize;
|
) -> usize;
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
|
@ -170,11 +166,12 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
||||||
matches: &Vec<Box<dyn Any + Send>>,
|
matches: &Vec<Box<dyn Any + Send>>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
count: Option<usize>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let matches = downcast_matches(matches);
|
let matches = downcast_matches(matches);
|
||||||
self.update(cx, |this, cx| {
|
self.update(cx, |this, cx| {
|
||||||
this.match_index_for_direction(&matches, current_index, direction, cx)
|
this.match_index_for_direction(&matches, current_index, direction, count, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue