Vim search (#2657)
This PR makes searching in vim mode significantly more like vim. I re-used search to implement "go to next instance of word under cursor" as this is how it works in vim (for integration with other search-related keyboard shortcuts) and to avoid having to rewrite all the logic to be vim-specific; but that did mean I had to make some changes to the way search works (in particular to allow different searches to run with specific options). Release Notes: - vim: `<enter>` in search now puts you back in normal mode ([#1583](https://github.com/zed-industries/community/issues/1583)) - vim: `?` now works to search backwards. - vim: jumping to definitions or search results keeps you in normal mode ([#1284](https://github.com/zed-industries/community/issues/1284)) ([#1514](https://github.com/zed-industries/community/issues/1514)) - vim: `n`/`N` are now supported to jump to next/previous match after a search ([#1583](https://github.com/zed-industries/community/issues/1583)) - vim: `*`/`#`/`g*`/`g#` are now supported to jump to the next/previous occurrence of the word under the cursor. - vim: `gD` now jumps to type definition
This commit is contained in:
Generated
+1
@@ -6469,6 +6469,7 @@ name = "search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
|
||||
+31
-6
@@ -60,6 +60,8 @@
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"n": "search::SelectNextMatch",
|
||||
"shift-n": "search::SelectPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
@@ -103,6 +105,8 @@
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"1": [
|
||||
"vim::Number",
|
||||
@@ -197,10 +201,11 @@
|
||||
"p": "vim::Paste",
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"/": [
|
||||
"buffer_search::Deploy",
|
||||
"/": "vim::Search",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"focus": true
|
||||
"backwards": true,
|
||||
}
|
||||
],
|
||||
"ctrl-f": "vim::PageDown",
|
||||
@@ -238,7 +243,20 @@
|
||||
"h": "editor::Hover",
|
||||
"t": "pane::ActivateNextItem",
|
||||
"shift-t": "pane::ActivatePrevItem",
|
||||
"d": "editor::GoToDefinition"
|
||||
"d": "editor::GoToDefinition",
|
||||
"shift-d": "editor::GoToTypeDefinition",
|
||||
"*": [
|
||||
"vim::MoveToNext",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"#": [
|
||||
"vim::MoveToPrev",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -310,8 +328,8 @@
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"> >": "editor::Indent",
|
||||
"< <": "editor::Outdent"
|
||||
">": "editor::Indent",
|
||||
"<": "editor::Outdent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -336,5 +354,12 @@
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -298,12 +298,22 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
|
||||
let mut propagate_action = true;
|
||||
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
||||
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
|
||||
return;
|
||||
}
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.show(cx) {
|
||||
search_bar.search_suggested(cx);
|
||||
if action.focus {
|
||||
search_bar.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
propagate_action = false
|
||||
}
|
||||
});
|
||||
}
|
||||
if propagate_action {
|
||||
cx.propagate_action();
|
||||
}
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
||||
fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
|
||||
@@ -320,13 +330,13 @@ impl AssistantPanel {
|
||||
|
||||
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>() {
|
||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx));
|
||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
|
||||
}
|
||||
}
|
||||
|
||||
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>() {
|
||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx));
|
||||
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -549,6 +549,7 @@ pub struct Editor {
|
||||
pending_rename: Option<RenameState>,
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
collapse_matches: bool,
|
||||
workspace: Option<(WeakViewHandle<Workspace>, i64)>,
|
||||
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
||||
input_enabled: bool,
|
||||
@@ -1381,6 +1382,7 @@ impl Editor {
|
||||
searchable: true,
|
||||
override_text_style: None,
|
||||
cursor_shape: Default::default(),
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
keymap_context_layers: Default::default(),
|
||||
input_enabled: true,
|
||||
@@ -1520,6 +1522,17 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
return range.start..range.start;
|
||||
}
|
||||
range.clone()
|
||||
}
|
||||
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
@@ -6261,6 +6274,7 @@ impl Editor {
|
||||
.to_offset(definition.target.buffer.read(cx));
|
||||
|
||||
if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
|
||||
let range = self.range_for_match(&range);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
@@ -6277,6 +6291,7 @@ impl Editor {
|
||||
// When selecting a definition in a different buffer, disable the nav history
|
||||
// to avoid creating a history entry at the previous cursor location.
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
let range = target_editor.range_for_match(&range);
|
||||
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
|
||||
+40
-35
@@ -946,21 +946,27 @@ impl SearchableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
s.select_ranges([range]);
|
||||
})
|
||||
}
|
||||
|
||||
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));
|
||||
let mut ranges = Vec::new();
|
||||
for m in &matches {
|
||||
ranges.push(self.range_for_match(&m))
|
||||
}
|
||||
self.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
}
|
||||
|
||||
fn match_index_for_direction(
|
||||
&mut self,
|
||||
matches: &Vec<Range<Anchor>>,
|
||||
mut current_index: usize,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
count: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> usize {
|
||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||
@@ -969,40 +975,39 @@ impl SearchableItem for Editor {
|
||||
} else {
|
||||
matches[current_index].start
|
||||
};
|
||||
if matches[current_index]
|
||||
.start
|
||||
.cmp(¤t_index_position, &buffer)
|
||||
.is_gt()
|
||||
{
|
||||
if direction == Direction::Prev {
|
||||
if current_index == 0 {
|
||||
current_index = matches.len() - 1;
|
||||
|
||||
let mut count = count % matches.len();
|
||||
if count == 0 {
|
||||
return current_index;
|
||||
}
|
||||
match direction {
|
||||
Direction::Next => {
|
||||
if matches[current_index]
|
||||
.start
|
||||
.cmp(¤t_index_position, &buffer)
|
||||
.is_gt()
|
||||
{
|
||||
count = count - 1
|
||||
}
|
||||
|
||||
(current_index + count) % matches.len()
|
||||
}
|
||||
Direction::Prev => {
|
||||
if matches[current_index]
|
||||
.end
|
||||
.cmp(¤t_index_position, &buffer)
|
||||
.is_lt()
|
||||
{
|
||||
count = count - 1;
|
||||
}
|
||||
|
||||
if current_index >= count {
|
||||
current_index - count
|
||||
} else {
|
||||
current_index -= 1;
|
||||
matches.len() - (count - current_index)
|
||||
}
|
||||
}
|
||||
} else if matches[current_index]
|
||||
.end
|
||||
.cmp(¤t_index_position, &buffer)
|
||||
.is_lt()
|
||||
{
|
||||
if direction == Direction::Next {
|
||||
current_index = 0;
|
||||
}
|
||||
} else if direction == Direction::Prev {
|
||||
if current_index == 0 {
|
||||
current_index = matches.len() - 1;
|
||||
} else {
|
||||
current_index -= 1;
|
||||
}
|
||||
} else if direction == Direction::Next {
|
||||
if current_index == matches.len() - 1 {
|
||||
current_index = 0
|
||||
} else {
|
||||
current_index += 1;
|
||||
}
|
||||
};
|
||||
current_index
|
||||
}
|
||||
}
|
||||
|
||||
fn find_matches(
|
||||
|
||||
@@ -9,6 +9,7 @@ path = "src/search.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1"
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
+223
-106
@@ -1,15 +1,17 @@
|
||||
use crate::{
|
||||
SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
|
||||
SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
|
||||
ToggleRegex, ToggleWholeWord,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
@@ -44,20 +46,19 @@ pub fn init(cx: &mut AppContext) {
|
||||
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);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
|
||||
}
|
||||
|
||||
fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
|
||||
fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
|
||||
cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.show(cx) {
|
||||
search_bar.toggle_search_option(option, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.propagate_action();
|
||||
});
|
||||
@@ -71,9 +72,8 @@ pub struct BufferSearchBar {
|
||||
searchable_items_with_matches:
|
||||
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
||||
pending_search: Option<Task<()>>,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
regex: bool,
|
||||
search_options: SearchOptions,
|
||||
default_options: SearchOptions,
|
||||
query_contains_error: bool,
|
||||
dismissed: bool,
|
||||
}
|
||||
@@ -156,19 +156,19 @@ impl View for BufferSearchBar {
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.case,
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.word,
|
||||
"Word",
|
||||
SearchOption::WholeWord,
|
||||
SearchOptions::WHOLE_WORD,
|
||||
cx,
|
||||
))
|
||||
.with_children(self.render_search_option(
|
||||
supported_options.regex,
|
||||
"Regex",
|
||||
SearchOption::Regex,
|
||||
SearchOptions::REGEX,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
@@ -212,7 +212,7 @@ impl ToolbarItemView for BufferSearchBar {
|
||||
));
|
||||
|
||||
self.active_searchable_item = Some(searchable_item_handle);
|
||||
self.update_matches(false, cx);
|
||||
let _ = self.update_matches(cx);
|
||||
if !self.dismissed {
|
||||
return ToolbarItemLocation::Secondary;
|
||||
}
|
||||
@@ -253,9 +253,8 @@ impl BufferSearchBar {
|
||||
active_searchable_item_subscription: None,
|
||||
active_match_index: None,
|
||||
searchable_items_with_matches: Default::default(),
|
||||
case_sensitive: false,
|
||||
whole_word: false,
|
||||
regex: false,
|
||||
default_options: SearchOptions::NONE,
|
||||
search_options: SearchOptions::NONE,
|
||||
pending_search: None,
|
||||
query_contains_error: false,
|
||||
dismissed: true,
|
||||
@@ -282,48 +281,86 @@ impl BufferSearchBar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
|
||||
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
||||
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
||||
} else {
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
return false;
|
||||
};
|
||||
|
||||
if suggest_query {
|
||||
let text = searchable_item.query_suggestion(cx);
|
||||
if !text.is_empty() {
|
||||
self.set_query(&text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
if focus {
|
||||
let query_editor = self.query_editor.clone();
|
||||
query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&editor::SelectAll, cx);
|
||||
});
|
||||
cx.focus_self();
|
||||
}
|
||||
|
||||
self.dismissed = false;
|
||||
cx.notify();
|
||||
cx.emit(Event::UpdateLocation);
|
||||
true
|
||||
}
|
||||
|
||||
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||
pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let search = self
|
||||
.query_suggestion(cx)
|
||||
.map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
|
||||
|
||||
if let Some(search) = search {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
search.await?;
|
||||
this.update(&mut cx, |this, cx| this.activate_current_match(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
|
||||
.searchable_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>) {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.buffer().update(cx, |query_buffer, cx| {
|
||||
let len = query_buffer.len(cx);
|
||||
query_buffer.edit([(0..len, query)], None, cx);
|
||||
});
|
||||
query_editor.select_all(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn query(&self, cx: &WindowContext) -> String {
|
||||
self.query_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
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,
|
||||
query: &str,
|
||||
options: Option<SearchOptions>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let options = options.unwrap_or(self.default_options);
|
||||
if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.buffer().update(cx, |query_buffer, cx| {
|
||||
let len = query_buffer.len(cx);
|
||||
query_buffer.edit([(0..len, query)], None, cx);
|
||||
});
|
||||
});
|
||||
self.search_options = options;
|
||||
self.query_contains_error = false;
|
||||
self.clear_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_search_option(
|
||||
&self,
|
||||
option_supported: bool,
|
||||
icon: &'static str,
|
||||
option: SearchOption,
|
||||
option: SearchOptions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
if !option_supported {
|
||||
@@ -331,9 +368,9 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let is_active = self.is_search_option_enabled(option);
|
||||
let is_active = self.search_options.contains(option);
|
||||
Some(
|
||||
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.search
|
||||
@@ -349,7 +386,7 @@ impl BufferSearchBar {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self>(
|
||||
option as usize,
|
||||
option.bits as usize,
|
||||
format!("Toggle {}", option.label()),
|
||||
Some(option.to_toggle_action()),
|
||||
tooltip_style,
|
||||
@@ -471,12 +508,23 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
let mut propagate_action = true;
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
|
||||
return;
|
||||
}
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.show(cx) {
|
||||
search_bar.search_suggested(cx);
|
||||
if action.focus {
|
||||
search_bar.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
propagate_action = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if propagate_action {
|
||||
cx.propagate_action();
|
||||
}
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
||||
fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
|
||||
@@ -489,37 +537,34 @@ impl BufferSearchBar {
|
||||
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() {
|
||||
cx.focus(active_editor.as_any());
|
||||
}
|
||||
}
|
||||
|
||||
fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
|
||||
match search_option {
|
||||
SearchOption::WholeWord => self.whole_word,
|
||||
SearchOption::CaseSensitive => self.case_sensitive,
|
||||
SearchOption::Regex => self.regex,
|
||||
}
|
||||
fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
|
||||
self.search_options.toggle(search_option);
|
||||
self.default_options = self.search_options;
|
||||
let _ = self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
|
||||
let value = match search_option {
|
||||
SearchOption::WholeWord => &mut self.whole_word,
|
||||
SearchOption::CaseSensitive => &mut self.case_sensitive,
|
||||
SearchOption::Regex => &mut self.regex,
|
||||
};
|
||||
*value = !*value;
|
||||
self.update_matches(false, cx);
|
||||
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>) {
|
||||
self.select_match(Direction::Next, cx);
|
||||
self.select_match(Direction::Next, 1, cx);
|
||||
}
|
||||
|
||||
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
||||
self.select_match(Direction::Prev, cx);
|
||||
self.select_match(Direction::Prev, 1, cx);
|
||||
}
|
||||
|
||||
fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
|
||||
@@ -536,15 +581,15 @@ impl BufferSearchBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
||||
if let Some(matches) = self
|
||||
.searchable_items_with_matches
|
||||
.get(&searchable_item.downgrade())
|
||||
{
|
||||
let new_match_index =
|
||||
searchable_item.match_index_for_direction(matches, index, direction, cx);
|
||||
let new_match_index = searchable_item
|
||||
.match_index_for_direction(matches, index, direction, count, cx);
|
||||
searchable_item.update_matches(matches, cx);
|
||||
searchable_item.activate_match(new_match_index, matches, cx);
|
||||
}
|
||||
@@ -588,17 +633,23 @@ impl BufferSearchBar {
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let editor::Event::BufferEdited { .. } = event {
|
||||
if let editor::Event::Edited { .. } = event {
|
||||
self.query_contains_error = false;
|
||||
self.clear_matches(cx);
|
||||
self.update_matches(true, cx);
|
||||
cx.notify();
|
||||
let search = self.update_matches(cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
search.await?;
|
||||
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
|
||||
SearchEvent::MatchesInvalidated => {
|
||||
let _ = self.update_matches(cx);
|
||||
}
|
||||
SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
|
||||
}
|
||||
}
|
||||
@@ -621,19 +672,21 @@ impl BufferSearchBar {
|
||||
.extend(active_item_matches);
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let query = self.query_editor.read(cx).text(cx);
|
||||
self.pending_search.take();
|
||||
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
||||
if query.is_empty() {
|
||||
self.active_match_index.take();
|
||||
active_searchable_item.clear_matches(cx);
|
||||
let _ = done_tx.send(());
|
||||
} else {
|
||||
let query = if self.regex {
|
||||
let query = if self.search_options.contains(SearchOptions::REGEX) {
|
||||
match SearchQuery::regex(
|
||||
query,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
) {
|
||||
@@ -641,14 +694,14 @@ impl BufferSearchBar {
|
||||
Err(_) => {
|
||||
self.query_contains_error = true;
|
||||
cx.notify();
|
||||
return;
|
||||
return done_rx;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SearchQuery::text(
|
||||
query,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
@@ -673,12 +726,7 @@ impl BufferSearchBar {
|
||||
.get(&active_searchable_item.downgrade())
|
||||
.unwrap();
|
||||
active_searchable_item.update_matches(matches, cx);
|
||||
if select_closest_match {
|
||||
if let Some(match_ix) = this.active_match_index {
|
||||
active_searchable_item
|
||||
.activate_match(match_ix, matches, cx);
|
||||
}
|
||||
}
|
||||
let _ = done_tx.send(());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -687,6 +735,7 @@ impl BufferSearchBar {
|
||||
}));
|
||||
}
|
||||
}
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -714,8 +763,7 @@ mod tests {
|
||||
use language::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_simple(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
|
||||
crate::project_search::tests::init_test(cx);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
@@ -738,16 +786,23 @@ mod tests {
|
||||
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.show(cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
(editor, search_bar)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_simple(cx: &mut TestAppContext) {
|
||||
let (editor, search_bar) = init_test(cx);
|
||||
|
||||
// Search for a string that appears with different casing.
|
||||
// By default, search is case-insensitive.
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.set_query("us", cx);
|
||||
});
|
||||
editor.next_notification(cx).await;
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| search_bar.search("us", None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
@@ -766,7 +821,7 @@ mod tests {
|
||||
|
||||
// Switch to a case sensitive search.
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
|
||||
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
|
||||
});
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -781,10 +836,10 @@ mod tests {
|
||||
|
||||
// Search for a string that appears both as a whole word and
|
||||
// within other words. By default, all results are found.
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.set_query("or", cx);
|
||||
});
|
||||
editor.next_notification(cx).await;
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| search_bar.search("or", None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
@@ -823,7 +878,7 @@ mod tests {
|
||||
|
||||
// Switch to a whole word search.
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.toggle_search_option(SearchOption::WholeWord, cx);
|
||||
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
});
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -1025,6 +1080,65 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_option_handling(cx: &mut TestAppContext) {
|
||||
let (editor, search_bar) = init_test(cx);
|
||||
|
||||
// show with options should make current search case sensitive
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| {
|
||||
search_bar.show(cx);
|
||||
search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
&[(
|
||||
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
|
||||
Color::red(),
|
||||
)]
|
||||
);
|
||||
});
|
||||
|
||||
// search_suggested should restore default options
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.search_suggested(cx);
|
||||
assert_eq!(search_bar.search_options, SearchOptions::NONE)
|
||||
});
|
||||
|
||||
// toggling a search option should update the defaults
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| {
|
||||
search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
|
||||
});
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
&[(
|
||||
DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
|
||||
Color::red(),
|
||||
),]
|
||||
);
|
||||
});
|
||||
|
||||
// defaults should still include whole word
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.search_suggested(cx);
|
||||
assert_eq!(
|
||||
search_bar.search_options,
|
||||
SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_select_all_matches(cx: &mut TestAppContext) {
|
||||
crate::project_search::tests::init_test(cx);
|
||||
@@ -1052,15 +1166,18 @@ mod tests {
|
||||
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.show(cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.set_query("a", cx);
|
||||
search_bar.activate_current_match(cx);
|
||||
});
|
||||
|
||||
editor.next_notification(cx).await;
|
||||
let initial_selections = editor.update(cx, |editor, cx| {
|
||||
let initial_selections = editor.selections.display_ranges(cx);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleWholeWord,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -51,12 +51,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
|
||||
}
|
||||
|
||||
fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
|
||||
fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
|
||||
cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
|
||||
if search_bar.update(cx, |search_bar, cx| {
|
||||
@@ -89,9 +89,7 @@ pub struct ProjectSearchView {
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
results_editor: ViewHandle<Editor>,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
regex: bool,
|
||||
search_options: SearchOptions,
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
active_match_index: Option<usize>,
|
||||
search_id: usize,
|
||||
@@ -408,9 +406,7 @@ impl ProjectSearchView {
|
||||
let project;
|
||||
let excerpts;
|
||||
let mut query_text = String::new();
|
||||
let mut regex = false;
|
||||
let mut case_sensitive = false;
|
||||
let mut whole_word = false;
|
||||
let mut options = SearchOptions::NONE;
|
||||
|
||||
{
|
||||
let model = model.read(cx);
|
||||
@@ -418,9 +414,7 @@ impl ProjectSearchView {
|
||||
excerpts = model.excerpts.clone();
|
||||
if let Some(active_query) = model.active_query.as_ref() {
|
||||
query_text = active_query.as_str().to_string();
|
||||
regex = active_query.is_regex();
|
||||
case_sensitive = active_query.case_sensitive();
|
||||
whole_word = active_query.whole_word();
|
||||
options = SearchOptions::from_query(active_query);
|
||||
}
|
||||
}
|
||||
cx.observe(&model, |this, _, cx| this.model_changed(cx))
|
||||
@@ -496,9 +490,7 @@ impl ProjectSearchView {
|
||||
model,
|
||||
query_editor,
|
||||
results_editor,
|
||||
case_sensitive,
|
||||
whole_word,
|
||||
regex,
|
||||
search_options: options,
|
||||
panels_with_errors: HashSet::new(),
|
||||
active_match_index: None,
|
||||
query_editor_was_focused: false,
|
||||
@@ -594,11 +586,11 @@ impl ProjectSearchView {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if self.regex {
|
||||
if self.search_options.contains(SearchOptions::REGEX) {
|
||||
match SearchQuery::regex(
|
||||
text,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
@@ -615,8 +607,8 @@ impl ProjectSearchView {
|
||||
} else {
|
||||
Some(SearchQuery::text(
|
||||
text,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
included_files,
|
||||
excluded_files,
|
||||
))
|
||||
@@ -635,7 +627,7 @@ impl ProjectSearchView {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||
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, 1, cx)
|
||||
});
|
||||
|
||||
let range_to_select = match_ranges[new_index].clone();
|
||||
@@ -676,7 +668,6 @@ impl ProjectSearchView {
|
||||
self.active_match_index = None;
|
||||
} else {
|
||||
self.active_match_index = Some(0);
|
||||
self.select_match(Direction::Next, cx);
|
||||
self.update_match_index(cx);
|
||||
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
|
||||
let is_new_search = self.search_id != prev_search_id;
|
||||
@@ -768,9 +759,7 @@ impl ProjectSearchBar {
|
||||
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();
|
||||
search_view.search_options = SearchOptions::from_query(&old_query);
|
||||
}
|
||||
}
|
||||
new_query
|
||||
@@ -858,15 +847,10 @@ impl ProjectSearchBar {
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
|
||||
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
|
||||
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_options.toggle(option);
|
||||
search_view.search(cx);
|
||||
});
|
||||
cx.notify();
|
||||
@@ -923,12 +907,12 @@ impl ProjectSearchBar {
|
||||
fn render_option_button(
|
||||
&self,
|
||||
icon: &'static str,
|
||||
option: SearchOption,
|
||||
option: SearchOptions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let is_active = self.is_option_enabled(option, cx);
|
||||
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
|
||||
MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.search
|
||||
@@ -944,7 +928,7 @@ impl ProjectSearchBar {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self>(
|
||||
option as usize,
|
||||
option.bits as usize,
|
||||
format!("Toggle {}", option.label()),
|
||||
Some(option.to_toggle_action()),
|
||||
tooltip_style,
|
||||
@@ -953,14 +937,9 @@ impl ProjectSearchBar {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
|
||||
fn is_option_enabled(&self, option: SearchOptions, 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,
|
||||
}
|
||||
search.read(cx).search_options.contains(option)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -1051,17 +1030,17 @@ impl View for ProjectSearchBar {
|
||||
Flex::row()
|
||||
.with_child(self.render_option_button(
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button(
|
||||
"Word",
|
||||
SearchOption::WholeWord,
|
||||
SearchOptions::WHOLE_WORD,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button(
|
||||
"Regex",
|
||||
SearchOption::Regex,
|
||||
SearchOptions::REGEX,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
|
||||
+29
-14
@@ -1,5 +1,7 @@
|
||||
use bitflags::bitflags;
|
||||
pub use buffer_search::BufferSearchBar;
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use project::search::SearchQuery;
|
||||
pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
||||
|
||||
pub mod buffer_search;
|
||||
@@ -22,27 +24,40 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum SearchOption {
|
||||
WholeWord,
|
||||
CaseSensitive,
|
||||
Regex,
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
pub struct SearchOptions: u8 {
|
||||
const NONE = 0b000;
|
||||
const WHOLE_WORD = 0b001;
|
||||
const CASE_SENSITIVE = 0b010;
|
||||
const REGEX = 0b100;
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchOption {
|
||||
impl SearchOptions {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SearchOption::WholeWord => "Match Whole Word",
|
||||
SearchOption::CaseSensitive => "Match Case",
|
||||
SearchOption::Regex => "Use Regular Expression",
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "Match Whole Word",
|
||||
SearchOptions::CASE_SENSITIVE => "Match Case",
|
||||
SearchOptions::REGEX => "Use Regular Expression",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_toggle_action(&self) -> Box<dyn Action> {
|
||||
match self {
|
||||
SearchOption::WholeWord => Box::new(ToggleWholeWord),
|
||||
SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive),
|
||||
SearchOption::Regex => Box::new(ToggleRegex),
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
|
||||
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
||||
SearchOptions::REGEX => Box::new(ToggleRegex),
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_query(query: &SearchQuery) -> SearchOptions {
|
||||
let mut options = SearchOptions::NONE;
|
||||
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
||||
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
||||
options.set(SearchOptions::REGEX, query.is_regex());
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod scroll;
|
||||
mod search;
|
||||
mod substitute;
|
||||
mod yank;
|
||||
|
||||
@@ -57,6 +58,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
search::init(cx);
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let times = vim.pop_number_operator(cx);
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
||||
use search::{buffer_search, BufferSearchBar, SearchOptions};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{searchable::Direction, Pane, Workspace};
|
||||
|
||||
use crate::{state::SearchState, Vim};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MoveToNext {
|
||||
#[serde(default)]
|
||||
partial_word: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MoveToPrev {
|
||||
#[serde(default)]
|
||||
partial_word: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub(crate) struct Search {
|
||||
#[serde(default)]
|
||||
backwards: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
|
||||
actions!(vim, [SearchSubmit]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(move_to_next);
|
||||
cx.add_action(move_to_prev);
|
||||
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>) {
|
||||
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
|
||||
}
|
||||
|
||||
fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
|
||||
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
|
||||
}
|
||||
|
||||
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let direction = if action.backwards {
|
||||
Direction::Prev
|
||||
} else {
|
||||
Direction::Next
|
||||
};
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
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| {
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||
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, count, cx);
|
||||
state.count = 1;
|
||||
search_bar.focus_editor(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_internal(
|
||||
workspace: &mut Workspace,
|
||||
direction: Direction,
|
||||
whole_word: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
let mut options = SearchOptions::CASE_SENSITIVE;
|
||||
options.set(SearchOptions::WHOLE_WORD, whole_word);
|
||||
if search_bar.show(cx) {
|
||||
search_bar
|
||||
.query_suggestion(cx)
|
||||
.map(|query| search_bar.search(&query, Some(options), cx))
|
||||
} else {
|
||||
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, count, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
vim.clear_operator(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::DisplayPoint;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_next(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["#"]);
|
||||
deterministic.run_until_parked();
|
||||
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", "*"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes(["g", "#"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "c", "c"]);
|
||||
|
||||
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")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
highlights[0].0
|
||||
)
|
||||
});
|
||||
|
||||
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
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-n"]);
|
||||
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
|
||||
|
||||
// ?<enter> to go to previous
|
||||
cx.simulate_keystrokes(["?", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
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
|
||||
cx.simulate_keystrokes(["/", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
|
||||
|
||||
// ?{search}<enter> to search backwards
|
||||
cx.simulate_keystrokes(["?", "b", "enter"]);
|
||||
deterministic.run_until_parked();
|
||||
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 language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
@@ -38,6 +39,23 @@ pub enum Operator {
|
||||
pub struct VimState {
|
||||
pub mode: Mode,
|
||||
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 {
|
||||
|
||||
+44
-2
@@ -5,6 +5,7 @@ mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
use command_palette::CommandPalette;
|
||||
use editor::DisplayPoint;
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_binding_test_context::*;
|
||||
@@ -96,7 +97,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
cx.assert_editor_state("aa\nbˇb\ncc");
|
||||
|
||||
// works in visuial mode
|
||||
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
|
||||
cx.simulate_keystrokes(["shift-v", "down", ">"]);
|
||||
cx.assert_editor_state("aa\n b«b\n cˇ»c");
|
||||
}
|
||||
|
||||
@@ -153,3 +154,44 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
|
||||
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
|
||||
cx.assert_state("aˇbc\n", Mode::Insert);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["/", "c", "c"]);
|
||||
|
||||
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")
|
||||
});
|
||||
|
||||
search_bar.read_with(cx.cx, |bar, cx| {
|
||||
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
|
||||
});
|
||||
|
||||
// wait for the query editor change event to fire.
|
||||
search_bar.next_notification(&cx).await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
highlights[0].0
|
||||
)
|
||||
});
|
||||
cx.simulate_keystrokes(["enter"]);
|
||||
|
||||
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["n"]);
|
||||
cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-n"]);
|
||||
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> {
|
||||
self.cx.set_state(text)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||
self.assert_editor_state(text);
|
||||
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||
|
||||
@@ -295,11 +295,15 @@ impl Vim {
|
||||
if self.enabled && editor.mode() == EditorMode::Full {
|
||||
editor.set_cursor_shape(cursor_shape, cx);
|
||||
editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
|
||||
editor.set_collapse_matches(true);
|
||||
editor.set_input_enabled(!state.vim_controlled());
|
||||
editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
|
||||
let context_layer = state.keymap_context_layer();
|
||||
editor.set_keymap_context_layer::<Self>(context_layer, cx);
|
||||
} else {
|
||||
// Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
|
||||
// but we need collapse_matches to persist when the search bar is focused.
|
||||
editor.set_collapse_matches(false);
|
||||
Self::unhook_vim_settings(editor, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,26 +55,21 @@ pub trait SearchableItem: Item {
|
||||
fn match_index_for_direction(
|
||||
&mut self,
|
||||
matches: &Vec<Self::Match>,
|
||||
mut current_index: usize,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
count: usize,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> usize {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
if current_index == 0 {
|
||||
matches.len() - 1
|
||||
let count = count % matches.len();
|
||||
if current_index >= count {
|
||||
current_index - count
|
||||
} else {
|
||||
current_index - 1
|
||||
}
|
||||
}
|
||||
Direction::Next => {
|
||||
current_index += 1;
|
||||
if current_index == matches.len() {
|
||||
0
|
||||
} else {
|
||||
current_index
|
||||
matches.len() - (count - current_index)
|
||||
}
|
||||
}
|
||||
Direction::Next => (current_index + count) % matches.len(),
|
||||
}
|
||||
}
|
||||
fn find_matches(
|
||||
@@ -113,6 +108,7 @@ pub trait SearchableItemHandle: ItemHandle {
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
count: usize,
|
||||
cx: &mut WindowContext,
|
||||
) -> usize;
|
||||
fn find_matches(
|
||||
@@ -183,11 +179,12 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
|
||||
matches: &Vec<Box<dyn Any + Send>>,
|
||||
current_index: usize,
|
||||
direction: Direction,
|
||||
count: usize,
|
||||
cx: &mut WindowContext,
|
||||
) -> usize {
|
||||
let matches = downcast_matches(matches);
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user