Detailed changes
@@ -73,8 +73,17 @@
],
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
- "n": "search::SelectNextMatch",
- "shift-n": "search::SelectPrevMatch",
+ "/": "vim::Search",
+ "?": [
+ "vim::Search",
+ {
+ "backwards": true
+ }
+ ],
+ "*": "vim::MoveToNext",
+ "#": "vim::MoveToPrev",
+ "n": "vim::MoveToNextMatch",
+ "shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"f": [
"vim::PushOperator",
@@ -351,15 +360,6 @@
],
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
- "/": "vim::Search",
- "?": [
- "vim::Search",
- {
- "backwards": true
- }
- ],
- "*": "vim::MoveToNext",
- "#": "vim::MoveToPrev",
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -48,7 +48,6 @@ fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
.upgrade()
.is_some_and(|previous| previous == editor.clone())
{
- vim.sync_vim_settings(cx);
vim.clear_operator(cx);
}
}
@@ -3,7 +3,8 @@ use editor::{
movement::{
self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
},
- Bias, DisplayPoint, ToOffset,
+ scroll::Autoscroll,
+ Anchor, Bias, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
@@ -20,7 +21,7 @@ use crate::{
Vim,
};
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Motion {
Left,
Backspace,
@@ -96,6 +97,14 @@ pub enum Motion {
WindowTop,
WindowMiddle,
WindowBottom,
+
+ // we don't have a good way to run a search syncronously, so
+ // we handle search motions by running the search async and then
+ // calling back into motion with this
+ ZedSearchResult {
+ prior_selections: Vec<Range<Anchor>>,
+ new_selections: Vec<Range<Anchor>>,
+ },
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -379,6 +388,34 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
});
}
+pub(crate) fn search_motion(m: Motion, cx: &mut WindowContext) {
+ if let Motion::ZedSearchResult {
+ prior_selections, ..
+ } = &m
+ {
+ match Vim::read(cx).state().mode {
+ Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
+ if !prior_selections.is_empty() {
+ Vim::update(cx, |vim, cx| {
+ vim.update_active_editor(cx, |_, editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(prior_selections.iter().cloned())
+ })
+ });
+ });
+ }
+ }
+ Mode::Normal | Mode::Replace | Mode::Insert => {
+ if Vim::read(cx).active_operator().is_none() {
+ return;
+ }
+ }
+ }
+ }
+
+ motion(m, cx)
+}
+
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
Vim::read(cx).active_operator()
@@ -453,7 +490,8 @@ impl Motion {
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFind { .. }
- | RepeatFindReversed { .. } => false,
+ | RepeatFindReversed { .. }
+ | ZedSearchResult { .. } => false,
}
}
@@ -491,7 +529,8 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
- | NextLineStart => false,
+ | NextLineStart
+ | ZedSearchResult { .. } => false,
}
}
@@ -529,7 +568,8 @@ impl Motion {
| NextSubwordStart { .. }
| PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
- | FindBackward { .. } => false,
+ | FindBackward { .. }
+ | ZedSearchResult { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
motion.inclusive()
}
@@ -720,6 +760,18 @@ impl Motion {
WindowTop => window_top(map, point, &text_layout_details, times - 1),
WindowMiddle => window_middle(map, point, &text_layout_details),
WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
+ ZedSearchResult { new_selections, .. } => {
+ // There will be only one selection, as
+ // Search::SelectNextMatch selects a single match.
+ if let Some(new_selection) = new_selections.first() {
+ (
+ new_selection.start.to_display_point(map),
+ SelectionGoal::None,
+ )
+ } else {
+ return None;
+ }
+ }
};
(new_point != point || infallible).then_some((new_point, goal))
@@ -734,6 +786,33 @@ impl Motion {
expand_to_surrounding_newline: bool,
text_layout_details: &TextLayoutDetails,
) -> Option<Range<DisplayPoint>> {
+ if let Motion::ZedSearchResult {
+ prior_selections,
+ new_selections,
+ } = self
+ {
+ if let Some((prior_selection, new_selection)) =
+ prior_selections.first().zip(new_selections.first())
+ {
+ let start = prior_selection
+ .start
+ .to_display_point(map)
+ .min(new_selection.start.to_display_point(map));
+ let end = new_selection
+ .end
+ .to_display_point(map)
+ .max(prior_selection.end.to_display_point(map));
+
+ if start < end {
+ return Some(start..end);
+ } else {
+ return Some(end..start);
+ }
+ } else {
+ return None;
+ }
+ }
+
if let Some((new_head, goal)) = self.move_point(
map,
selection.head(),
@@ -4,7 +4,7 @@ use serde_derive::Deserialize;
use workspace::{searchable::Direction, Workspace};
use crate::{
- motion::Motion,
+ motion::{search_motion, Motion},
normal::move_cursor,
state::{Mode, SearchState},
Vim,
@@ -49,7 +49,7 @@ struct Replacement {
is_case_sensitive: bool,
}
-actions!(vim, [SearchSubmit]);
+actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
impl_actions!(
vim,
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
@@ -58,6 +58,8 @@ impl_actions!(
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(move_to_next);
workspace.register_action(move_to_prev);
+ workspace.register_action(move_to_next_match);
+ workspace.register_action(move_to_prev_match);
workspace.register_action(search);
workspace.register_action(search_submit);
workspace.register_action(search_deploy);
@@ -74,6 +76,22 @@ fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewCon
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
}
+fn move_to_next_match(
+ workspace: &mut Workspace,
+ _: &MoveToNextMatch,
+ cx: &mut ViewContext<Workspace>,
+) {
+ move_to_match_internal(workspace, Direction::Next, cx)
+}
+
+fn move_to_prev_match(
+ workspace: &mut Workspace,
+ _: &MoveToPrevMatch,
+ cx: &mut ViewContext<Workspace>,
+) {
+ move_to_match_internal(workspace, Direction::Prev, cx)
+}
+
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
let direction = if action.backwards {
@@ -83,6 +101,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
};
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx).unwrap_or(1);
+ let prior_selections = vim.editor_selections(cx);
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| {
@@ -102,6 +121,9 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
direction,
count,
initial_query: query.clone(),
+ prior_selections,
+ prior_operator: vim.active_operator(),
+ prior_mode: vim.state().mode,
};
});
}
@@ -116,6 +138,7 @@ fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewCont
}
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
+ let mut motion = None;
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
@@ -135,10 +158,60 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
state.count = 1;
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx);
+
+ let prior_selections = state.prior_selections.drain(..).collect();
+ let prior_mode = state.prior_mode;
+ let prior_operator = state.prior_operator.take();
+ let new_selections = vim.editor_selections(cx);
+
+ if prior_mode != vim.state().mode {
+ vim.switch_mode(prior_mode, true, cx);
+ }
+ if let Some(operator) = prior_operator {
+ vim.push_operator(operator, cx);
+ };
+ motion = Some(Motion::ZedSearchResult {
+ prior_selections,
+ new_selections,
+ });
});
}
});
- })
+ });
+
+ if let Some(motion) = motion {
+ search_motion(motion, cx)
+ }
+}
+
+pub fn move_to_match_internal(
+ workspace: &mut Workspace,
+ direction: Direction,
+ cx: &mut ViewContext<Workspace>,
+) {
+ let mut motion = None;
+ Vim::update(cx, |vim, cx| {
+ let pane = workspace.active_pane().clone();
+ let count = vim.take_count(cx).unwrap_or(1);
+ let prior_selections = vim.editor_selections(cx);
+
+ 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| {
+ search_bar.select_match(direction, count, cx);
+
+ let new_selections = vim.editor_selections(cx);
+ motion = Some(Motion::ZedSearchResult {
+ prior_selections,
+ new_selections,
+ });
+ })
+ }
+ })
+ });
+ if let Some(motion) = motion {
+ search_motion(motion, cx);
+ }
}
pub fn move_to_internal(
@@ -150,6 +223,7 @@ pub fn move_to_internal(
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
let count = vim.take_count(cx).unwrap_or(1);
+ let prior_selections = vim.editor_selections(cx);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
@@ -159,6 +233,8 @@ pub fn move_to_internal(
return None;
}
let Some(query) = search_bar.query_suggestion(cx) else {
+ vim.clear_operator(cx);
+ let _ = search_bar.search("", None, cx);
return None;
};
let mut query = regex::escape(&query);
@@ -174,7 +250,17 @@ pub fn move_to_internal(
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
- search_bar.select_match(direction, count, cx)
+ search_bar.select_match(direction, count, cx);
+
+ let new_selections =
+ Vim::update(cx, |vim, cx| vim.editor_selections(cx));
+ search_motion(
+ Motion::ZedSearchResult {
+ prior_selections,
+ new_selections,
+ },
+ cx,
+ )
})?;
anyhow::Ok(())
})
@@ -186,8 +272,6 @@ pub fn move_to_internal(
if vim.state().mode.is_visual() {
vim.switch_mode(Mode::Normal, false, cx)
}
-
- vim.clear_operator(cx);
});
}
@@ -362,6 +446,7 @@ fn parse_replace_all(query: &str) -> Replacement {
#[cfg(test)]
mod test {
use editor::DisplayPoint;
+ use indoc::indoc;
use search::BufferSearchBar;
use crate::{
@@ -508,4 +593,62 @@ mod test {
cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await;
cx.assert_shared_mode(Mode::Normal).await;
}
+
+ #[gpui::test]
+ async fn test_d_search(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
+ cx.simulate_shared_keystrokes(["d", "/", "c", "d"]).await;
+ cx.simulate_shared_keystrokes(["enter"]).await;
+ cx.assert_shared_state("ˇcd a.c. abcd").await;
+ }
+
+ #[gpui::test]
+ async fn test_v_search(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
+ cx.simulate_shared_keystrokes(["v", "/", "c", "d"]).await;
+ cx.simulate_shared_keystrokes(["enter"]).await;
+ cx.assert_shared_state("«a.c. abcˇ»d a.c. abcd").await;
+
+ cx.set_shared_state("a a aˇ a a a").await;
+ cx.simulate_shared_keystrokes(["v", "/", "a"]).await;
+ cx.simulate_shared_keystrokes(["enter"]).await;
+ cx.assert_shared_state("a a a« aˇ» a a").await;
+ cx.simulate_shared_keystrokes(["/", "enter"]).await;
+ cx.assert_shared_state("a a a« a aˇ» a").await;
+ cx.simulate_shared_keystrokes(["?", "enter"]).await;
+ cx.assert_shared_state("a a a« aˇ» a a").await;
+ cx.simulate_shared_keystrokes(["?", "enter"]).await;
+ cx.assert_shared_state("a a «ˇa »a a a").await;
+ cx.simulate_shared_keystrokes(["/", "enter"]).await;
+ cx.assert_shared_state("a a a« aˇ» a a").await;
+ cx.simulate_shared_keystrokes(["/", "enter"]).await;
+ cx.assert_shared_state("a a a« a aˇ» a").await;
+ }
+
+ #[gpui::test]
+ async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {
+ "ˇone two
+ three four
+ five six
+ "
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "j", "/", "f"])
+ .await;
+ cx.simulate_shared_keystrokes(["enter"]).await;
+ cx.assert_shared_state(indoc! {
+ "«one twoˇ»
+ «three fˇ»our
+ five six
+ "
+ })
+ .await;
+ }
}
@@ -138,21 +138,15 @@ impl Clone for ReplayableAction {
}
}
-#[derive(Clone)]
+#[derive(Clone, Default, Debug)]
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(),
- }
- }
+ pub prior_selections: Vec<Range<Anchor>>,
+ pub prior_operator: Option<Operator>,
+ pub prior_mode: Mode,
}
impl EditorState {
@@ -4,12 +4,22 @@ use gpui::WindowContext;
use language::BracketPair;
use serde::Deserialize;
use std::sync::Arc;
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType {
Motion(Motion),
Object(Object),
}
+// This exists so that we can have Deserialize on Operators, but not on Motions.
+impl<'de> Deserialize<'de> for SurroundsType {
+ fn deserialize<D>(_: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Err(serde::de::Error::custom("Cannot deserialize SurroundsType"))
+ }
+}
+
pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
@@ -21,7 +21,7 @@ use collections::HashMap;
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
use editor::{
movement::{self, FindRange},
- Editor, EditorEvent, EditorMode,
+ Anchor, Editor, EditorEvent, EditorMode,
};
use gpui::{
actions, impl_actions, Action, AppContext, EntityId, FocusableView, Global, KeystrokeEvent,
@@ -295,6 +295,18 @@ impl Vim {
Some(editor.update(cx, |editor, cx| update(self, editor, cx)))
}
+ fn editor_selections(&mut self, cx: &mut WindowContext) -> Vec<Range<Anchor>> {
+ self.update_active_editor(cx, |_, editor, _| {
+ editor
+ .selections
+ .disjoint_anchors()
+ .iter()
+ .map(|selection| selection.tail()..selection.head())
+ .collect()
+ })
+ .unwrap_or_default()
+ }
+
/// When doing an action that modifies the buffer, we start recording so that `.`
/// will replay the action.
pub fn start_recording(&mut self, cx: &mut WindowContext) {
@@ -0,0 +1,7 @@
+{"Put":{"state":"ˇa.c. abcd a.c. abcd"}}
+{"Key":"d"}
+{"Key":"/"}
+{"Key":"c"}
+{"Key":"d"}
+{"Key":"enter"}
+{"Get":{"state":"ˇcd a.c. abcd","mode":"Normal"}}
@@ -0,0 +1,28 @@
+{"Put":{"state":"ˇa.c. abcd a.c. abcd"}}
+{"Key":"v"}
+{"Key":"/"}
+{"Key":"c"}
+{"Key":"d"}
+{"Key":"enter"}
+{"Get":{"state":"«a.c. abcˇ»d a.c. abcd","mode":"Visual"}}
+{"Put":{"state":"a a aˇ a a a"}}
+{"Key":"v"}
+{"Key":"/"}
+{"Key":"a"}
+{"Key":"enter"}
+{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}}
+{"Key":"/"}
+{"Key":"enter"}
+{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}}
+{"Key":"?"}
+{"Key":"enter"}
+{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}}
+{"Key":"?"}
+{"Key":"enter"}
+{"Get":{"state":"a a «ˇa »a a a","mode":"Visual"}}
+{"Key":"/"}
+{"Key":"enter"}
+{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}}
+{"Key":"/"}
+{"Key":"enter"}
+{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}}
@@ -0,0 +1,7 @@
+{"Put":{"state":"ˇone two\nthree four\nfive six\n"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"/"}
+{"Key":"f"}
+{"Key":"enter"}
+{"Get":{"state":"«one twoˇ»\n«three fˇ»our\nfive six\n","mode":"VisualBlock"}}
@@ -18,9 +18,10 @@ pub enum SearchEvent {
ActiveMatchChanged,
}
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Direction {
Prev,
+ #[default]
Next,
}