mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 11:06:58 +03:00
vim: Allow search with operators & visual mode (#10226)
Fixes: #4346 Release Notes: - vim: Add search motions (`/,?,n,N,*,#`) in visual modes and as targets for operators like `d`,`c`,`y` ([#4346](https://github.com/zed-industries/zed/issues/4346)).
This commit is contained in:
parent
f9bf60f017
commit
f327118e06
@ -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) {
|
||||
|
7
crates/vim/test_data/test_d_search.json
Normal file
7
crates/vim/test_data/test_d_search.json
Normal file
@ -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"}}
|
28
crates/vim/test_data/test_v_search.json
Normal file
28
crates/vim/test_data/test_v_search.json
Normal file
@ -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"}}
|
7
crates/vim/test_data/test_visual_block_search.json
Normal file
7
crates/vim/test_data/test_visual_block_search.json
Normal file
@ -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,
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user