Detailed changes
@@ -101,6 +101,8 @@
"vim::SwitchMode",
"Normal"
],
+ "*": "vim::MoveToNext",
+ "#": "vim::MoveToPrev",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
@@ -240,7 +242,19 @@
"vim::SwitchMode",
"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>,
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>,
+ pending_match_direction: Option<Direction>,
active_searchable_item_subscription: Option<Subscription>,
seachable_items_with_matches:
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
@@ -252,6 +253,7 @@ impl BufferSearchBar {
default_options: SearchOptions::NONE,
search_options: SearchOptions::NONE,
pending_search: None,
+ pending_match_direction: None,
query_contains_error: false,
dismissed: true,
}
@@ -285,10 +287,10 @@ impl BufferSearchBar {
&mut self,
focus: bool,
suggest_query: bool,
- search_option: SearchOptions,
+ search_options: SearchOptions,
cx: &mut ViewContext<Self>,
) -> bool {
- self.search_options = search_option;
+ self.search_options = search_options;
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
} else {
@@ -486,6 +488,17 @@ impl BufferSearchBar {
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>) {
if let Some(index) = self.active_match_index {
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 query.is_empty() {
self.active_match_index.take();
+ self.pending_match_direction.take();
active_searchable_item.clear_matches(cx);
} else {
let query = if self.search_options.contains(SearchOptions::REGEX) {
@@ -614,7 +628,15 @@ impl BufferSearchBar {
.unwrap();
active_searchable_item.update_matches(matches, cx);
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
.activate_match(match_ix, matches, cx);
}
@@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod scroll;
+mod search;
mod substitute;
mod yank;
@@ -27,6 +28,7 @@ use self::{
case::change_case,
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
+ search::{move_to_next, move_to_prev},
substitute::substitute,
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_below);
cx.add_action(change_case);
+ cx.add_action(move_to_next);
+ cx.add_action(move_to_prev);
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
@@ -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);
+ }
+}