vim: add */#/g*/g# for jumping to next word
As in vim, this toggles the normal search experience.
This commit is contained in:
parent
96ce0bb783
commit
dbec2ed1f1
4 changed files with 152 additions and 4 deletions
|
@ -101,6 +101,8 @@
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Normal"
|
"Normal"
|
||||||
],
|
],
|
||||||
|
"*": "vim::MoveToNext",
|
||||||
|
"#": "vim::MoveToPrev",
|
||||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
"1": [
|
"1": [
|
||||||
"vim::Number",
|
"vim::Number",
|
||||||
|
@ -240,7 +242,19 @@
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Normal"
|
"Normal"
|
||||||
],
|
],
|
||||||
"d": "editor::GoToDefinition"
|
"d": "editor::GoToDefinition",
|
||||||
|
"*": [
|
||||||
|
"vim::MoveToNext",
|
||||||
|
{
|
||||||
|
"partialWord": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"#": [
|
||||||
|
"vim::MoveToPrev",
|
||||||
|
{
|
||||||
|
"partialWord": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -65,6 +65,7 @@ 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>>>,
|
||||||
|
@ -252,6 +253,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
@ -285,10 +287,10 @@ impl BufferSearchBar {
|
||||||
&mut self,
|
&mut self,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
suggest_query: bool,
|
suggest_query: bool,
|
||||||
search_option: SearchOptions,
|
search_options: SearchOptions,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.search_options = search_option;
|
self.search_options = search_options;
|
||||||
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
||||||
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
||||||
} else {
|
} else {
|
||||||
|
@ -486,6 +488,17 @@ impl BufferSearchBar {
|
||||||
self.select_match(Direction::Prev, cx);
|
self.select_match(Direction::Prev, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_word_under_cursor(
|
||||||
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
|
options: SearchOptions,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.active_match_index = None;
|
||||||
|
self.pending_match_direction = Some(direction);
|
||||||
|
self.show_with_options(false, true, options, 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() {
|
||||||
|
@ -567,6 +580,7 @@ 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);
|
||||||
} else {
|
} else {
|
||||||
let query = if self.search_options.contains(SearchOptions::REGEX) {
|
let query = if self.search_options.contains(SearchOptions::REGEX) {
|
||||||
|
@ -614,7 +628,15 @@ impl BufferSearchBar {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_searchable_item.update_matches(matches, cx);
|
active_searchable_item.update_matches(matches, cx);
|
||||||
if select_closest_match {
|
if select_closest_match {
|
||||||
if let Some(match_ix) = this.active_match_index {
|
if let Some(mut match_ix) = this.active_match_index {
|
||||||
|
if let Some(direction) = this.pending_match_direction.take()
|
||||||
|
{
|
||||||
|
match_ix += match direction {
|
||||||
|
Direction::Next => 1,
|
||||||
|
Direction::Prev => matches.len() - 1,
|
||||||
|
};
|
||||||
|
match_ix = match_ix % matches.len();
|
||||||
|
}
|
||||||
active_searchable_item
|
active_searchable_item
|
||||||
.activate_match(match_ix, matches, cx);
|
.activate_match(match_ix, matches, cx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod case;
|
||||||
mod change;
|
mod change;
|
||||||
mod delete;
|
mod delete;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
|
mod search;
|
||||||
mod substitute;
|
mod substitute;
|
||||||
mod yank;
|
mod yank;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ use self::{
|
||||||
case::change_case,
|
case::change_case,
|
||||||
change::{change_motion, change_object},
|
change::{change_motion, change_object},
|
||||||
delete::{delete_motion, delete_object},
|
delete::{delete_motion, delete_object},
|
||||||
|
search::{move_to_next, move_to_prev},
|
||||||
substitute::substitute,
|
substitute::substitute,
|
||||||
yank::{yank_motion, yank_object},
|
yank::{yank_motion, yank_object},
|
||||||
};
|
};
|
||||||
|
@ -57,6 +59,8 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(insert_line_above);
|
cx.add_action(insert_line_above);
|
||||||
cx.add_action(insert_line_below);
|
cx.add_action(insert_line_below);
|
||||||
cx.add_action(change_case);
|
cx.add_action(change_case);
|
||||||
|
cx.add_action(move_to_next);
|
||||||
|
cx.add_action(move_to_prev);
|
||||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
|
|
108
crates/vim/src/normal/search.rs
Normal file
108
crates/vim/src/normal/search.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use gpui::{impl_actions, ViewContext};
|
||||||
|
use search::{BufferSearchBar, SearchOptions};
|
||||||
|
use serde_derive::Deserialize;
|
||||||
|
use workspace::{searchable::Direction, Workspace};
|
||||||
|
|
||||||
|
use crate::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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_actions!(vim, [MoveToNext, MoveToPrev]);
|
||||||
|
|
||||||
|
pub(crate) fn move_to_next(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
action: &MoveToNext,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to_prev(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
action: &MoveToPrev,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
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 options = SearchOptions::CASE_SENSITIVE;
|
||||||
|
options.set(SearchOptions::WHOLE_WORD, whole_word);
|
||||||
|
search_bar.select_word_under_cursor(direction, options, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vim.clear_operator(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use search::BufferSearchBar;
|
||||||
|
|
||||||
|
use crate::{state::Mode, test::VimTestContext};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
|
||||||
|
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.simulate_keystrokes(["*"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["*"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["#"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["#"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["g", "*"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
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", "#"]);
|
||||||
|
search_bar.next_notification(&cx).await;
|
||||||
|
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue