Release notes: - vim: add Visual Block mode ([#984](https://github.com/zed-industries/community/issues/984)), ([#1415](https://github.com/zed-industries/community/issues/1415)). - vim: add support for `a<object>` and `i<object>` in visual modes - vim: fix scroll shortcuts (`ctrl-{f,b,d,u,e,y}`) in visual modes - allow `shift-enter` to type a newline.
302 lines
11 KiB
Rust
302 lines
11 KiB
Rust
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
|
use search::{buffer_search, BufferSearchBar, SearchMode, 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, cx);
|
|
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
|
}
|
|
vim.workspace_state.search = SearchState {
|
|
direction,
|
|
count,
|
|
initial_query: query.clone(),
|
|
};
|
|
});
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// 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.workspace_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 state = &mut vim.workspace_state.search;
|
|
let mut count = state.count;
|
|
let direction = state.direction;
|
|
|
|
// 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)
|
|
}
|
|
state.count = 1;
|
|
search_bar.select_match(direction, count, cx);
|
|
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(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);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_non_vim_search(
|
|
cx: &mut gpui::TestAppContext,
|
|
deterministic: Arc<gpui::executor::Deterministic>,
|
|
) {
|
|
let mut cx = VimTestContext::new(cx, false).await;
|
|
cx.set_state("ˇone one one one", Mode::Normal);
|
|
cx.simulate_keystrokes(["cmd-f"]);
|
|
deterministic.run_until_parked();
|
|
|
|
cx.assert_editor_state("«oneˇ» one one one");
|
|
cx.simulate_keystrokes(["enter"]);
|
|
cx.assert_editor_state("one «oneˇ» one one");
|
|
cx.simulate_keystrokes(["shift-enter"]);
|
|
cx.assert_editor_state("«oneˇ» one one one");
|
|
}
|
|
}
|