vim: Support gn command and remap gn to gl (#9982)

Release Notes:

- Resolves #4273

@algora-pbc /claim #4273

This is a work-in-progress. The process for `gn` command is:

- maintain updated vim.workspace_state.search.initial_query
- modify editor.select_next_state with
vim.workspace_state.search.initial_query
- use editor.select_next()
- merge selections
- set editor.select_next_state to previous state

To make this possible, several private members and editor structures are
made public. `gN` is not yet implemented and the cursor still does not
jump to the next selection in the first use.

Maybe there is an better way to do this?

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
joaquin30 2024-04-05 21:23:37 -05:00 committed by GitHub
parent fe4b345603
commit bf9b443b4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 311 additions and 89 deletions

View File

@ -137,8 +137,10 @@
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g x": "editor::OpenUrl",
"g n": "vim::SelectNext",
"g shift-n": "vim::SelectPrevious",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
"g shift-l": "vim::SelectPrevious",
"g >": [
"editor::SelectNext",
{

View File

@ -963,7 +963,7 @@ impl BufferSearchBar {
done_rx
}
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
pub fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
let new_index = self
.active_searchable_item
.as_ref()

View File

@ -39,6 +39,7 @@ ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
schemars.workspace = true
util.workspace = true
[dev-dependencies]
command_palette.workspace = true

View File

@ -1,4 +1,3 @@
use anyhow::Result;
use std::sync::Arc;
use collections::HashMap;
@ -10,10 +9,13 @@ use editor::{
};
use gpui::{actions, ViewContext, WindowContext};
use language::{Point, Selection, SelectionGoal};
use workspace::Workspace;
use search::BufferSearchBar;
use util::ResultExt;
use workspace::{searchable::Direction, Workspace};
use crate::{
motion::{start_of_line, Motion},
normal::substitute::substitute,
object::Object,
state::{Mode, Operator},
utils::{copy_selections_content, yank_selections_content},
@ -31,6 +33,8 @@ actions!(
OtherEnd,
SelectNext,
SelectPrevious,
SelectNextMatch,
SelectPreviousMatch,
]
);
@ -47,14 +51,29 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
},
);
workspace.register_action(other_end);
workspace.register_action(delete);
workspace.register_action(yank);
workspace.register_action(|workspace, action, cx| {
select_next(workspace, action, cx).ok();
workspace.register_action(|_, _: &VisualDelete, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
delete(vim, cx);
});
});
workspace.register_action(|workspace, action, cx| {
select_previous(workspace, action, cx).ok();
workspace.register_action(|_, _: &VisualYank, cx| {
Vim::update(cx, |vim, cx| {
yank(vim, cx);
});
});
workspace.register_action(select_next);
workspace.register_action(select_previous);
workspace.register_action(|workspace, _: &SelectNextMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Next, cx);
});
});
workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| {
Vim::update(cx, |vim, cx| {
select_match(workspace, vim, Direction::Prev, cx);
});
});
}
@ -333,70 +352,65 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
});
}
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = editor.selections.line_mode;
pub fn delete(vim: &mut Vim, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = editor.selections.line_mode;
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if line_mode {
let mut position = selection.head();
if !selection.reversed {
position = movement::left(map, position);
}
original_columns.insert(selection.id, position.to_point(map).column);
}
selection.goal = SelectionGoal::None;
});
});
copy_selections_content(vim, editor, line_mode, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map);
if let Some(column) = original_columns.get(&selection.id) {
cursor.column = *column
}
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
if vim.state().mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
})
});
vim.switch_mode(Mode::Normal, true, cx);
});
}
pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let line_mode = editor.selections.line_mode;
yank_selections_content(vim, editor, line_mode, cx);
editor.change_selections(None, cx, |s| {
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if line_mode {
selection.start = start_of_line(map, false, selection.start);
};
selection.collapse_to(selection.start, SelectionGoal::None)
let mut position = selection.head();
if !selection.reversed {
position = movement::left(map, position);
}
original_columns.insert(selection.id, position.to_point(map).column);
}
selection.goal = SelectionGoal::None;
});
});
copy_selections_content(vim, editor, line_mode, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map);
if let Some(column) = original_columns.get(&selection.id) {
cursor.column = *column
}
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
if vim.state().mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
});
vim.switch_mode(Mode::Normal, true, cx);
})
});
vim.switch_mode(Mode::Normal, true, cx);
}
pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
vim.update_active_editor(cx, |vim, editor, cx| {
let line_mode = editor.selections.line_mode;
yank_selections_content(vim, editor, line_mode, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if line_mode {
selection.start = start_of_line(map, false, selection.start);
};
selection.collapse_to(selection.start, SelectionGoal::None)
});
if vim.state().mode == Mode::VisualBlock {
s.select_anchors(vec![s.first_anchor()])
}
});
});
vim.switch_mode(Mode::Normal, true, cx);
}
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
@ -442,48 +456,112 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
});
}
pub fn select_next(
_: &mut Workspace,
_: &SelectNext,
cx: &mut ViewContext<Workspace>,
) -> Result<()> {
pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
for _ in 0..count {
match editor.select_next(&Default::default(), cx) {
Err(a) => return Err(a),
_ => {}
if editor
.select_next(&Default::default(), cx)
.log_err()
.is_none()
{
break;
}
}
Ok(())
})
})
.unwrap_or(Ok(()))
});
}
pub fn select_previous(
_: &mut Workspace,
_: &SelectPrevious,
cx: &mut ViewContext<Workspace>,
) -> Result<()> {
pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count =
vim.take_count(cx)
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
vim.update_active_editor(cx, |_, editor, cx| {
for _ in 0..count {
match editor.select_previous(&Default::default(), cx) {
Err(a) => return Err(a),
_ => {}
if editor
.select_previous(&Default::default(), cx)
.log_err()
.is_none()
{
break;
}
}
Ok(())
})
})
.unwrap_or(Ok(()))
});
}
pub fn select_match(
workspace: &mut Workspace,
vim: &mut Vim,
direction: Direction,
cx: &mut WindowContext,
) {
let count = vim.take_count(cx).unwrap_or(1);
let pane = workspace.active_pane().clone();
let vim_is_normal = vim.state().mode == Mode::Normal;
let mut start_selection = 0usize;
let mut end_selection = 0usize;
vim.update_active_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false);
});
if vim_is_normal {
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| {
// without update_match_index there is a bug when the cursor is before the first match
search_bar.update_match_index(cx);
search_bar.select_match(direction.opposite(), 1, cx);
});
}
});
}
vim.update_active_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
start_selection = latest.start;
end_selection = latest.end;
});
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.update_match_index(cx);
search_bar.select_match(direction, count, cx);
});
}
});
vim.update_active_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
if vim_is_normal {
start_selection = latest.start;
end_selection = latest.end;
} else {
start_selection = start_selection.min(latest.start);
end_selection = end_selection.max(latest.end);
}
if direction == Direction::Prev {
std::mem::swap(&mut start_selection, &mut end_selection);
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([start_selection..end_selection]);
});
editor.set_collapse_matches(true);
});
match vim.maybe_pop_operator() {
Some(Operator::Change) => substitute(vim, None, false, cx),
Some(Operator::Delete) => {
vim.stop_recording();
delete(vim, cx)
}
Some(Operator::Yank) => yank(vim, cx),
_ => {} // Ignoring other operators
};
}
#[cfg(test)]
@ -1052,4 +1130,69 @@ mod test {
cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
assert_eq!(cx.mode(), Mode::VisualBlock);
}
#[gpui::test]
async fn test_gn(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("aaˇ aa aa aa aa").await;
cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
.await;
cx.assert_shared_state("aa ˇaa aa aa aa").await;
cx.simulate_shared_keystrokes(["g", "n"]).await;
cx.assert_shared_state("aa «aaˇ» aa aa aa").await;
cx.simulate_shared_keystrokes(["g", "n"]).await;
cx.assert_shared_state("aa «aa aaˇ» aa aa").await;
cx.simulate_shared_keystrokes(["escape", "d", "g", "n"])
.await;
cx.assert_shared_state("aa aa ˇ aa aa").await;
cx.set_shared_state("aaˇ aa aa aa aa").await;
cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
.await;
cx.assert_shared_state("aa ˇaa aa aa aa").await;
cx.simulate_shared_keystrokes(["3", "g", "n"]).await;
cx.assert_shared_state("aa aa aa «aaˇ» aa").await;
cx.set_shared_state("aaˇ aa aa aa aa").await;
cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
.await;
cx.assert_shared_state("aa ˇaa aa aa aa").await;
cx.simulate_shared_keystrokes(["g", "shift-n"]).await;
cx.assert_shared_state("aa «ˇaa» aa aa aa").await;
cx.simulate_shared_keystrokes(["g", "shift-n"]).await;
cx.assert_shared_state("«ˇaa aa» aa aa aa").await;
}
#[gpui::test]
async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("aaˇ aa aa aa aa").await;
cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
.await;
cx.assert_shared_state("aa ˇaa aa aa aa").await;
cx.simulate_shared_keystrokes(["d", "g", "n"]).await;
cx.assert_shared_state("aa ˇ aa aa aa").await;
cx.simulate_shared_keystrokes(["."]).await;
cx.assert_shared_state("aa ˇ aa aa").await;
cx.simulate_shared_keystrokes(["."]).await;
cx.assert_shared_state("aa ˇ aa").await;
}
#[gpui::test]
async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("aaˇ aa aa aa aa").await;
cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
.await;
cx.assert_shared_state("aa ˇaa aa aa aa").await;
cx.simulate_shared_keystrokes(["c", "g", "n", "x", "escape"])
.await;
cx.assert_shared_state("aa ˇx aa aa aa").await;
cx.simulate_shared_keystrokes(["."]).await;
cx.assert_shared_state("aa x ˇx aa aa").await;
}
}

View File

@ -0,0 +1,14 @@
{"Put":{"state":"aaˇ aa aa aa aa"}}
{"Key":"/"}
{"Key":"a"}
{"Key":"a"}
{"Key":"enter"}
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
{"Key":"c"}
{"Key":"g"}
{"Key":"n"}
{"Key":"x"}
{"Key":"escape"}
{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}}
{"Key":"."}
{"Get":{"state":"aa x ˇx aa aa","mode":"Normal"}}

View File

@ -0,0 +1,14 @@
{"Put":{"state":"aaˇ aa aa aa aa"}}
{"Key":"/"}
{"Key":"a"}
{"Key":"a"}
{"Key":"enter"}
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
{"Key":"d"}
{"Key":"g"}
{"Key":"n"}
{"Get":{"state":"aa ˇ aa aa aa","mode":"Normal"}}
{"Key":"."}
{"Get":{"state":"aa ˇ aa aa","mode":"Normal"}}
{"Key":"."}
{"Get":{"state":"aa ˇ aa","mode":"Normal"}}

View File

@ -0,0 +1,39 @@
{"Put":{"state":"aaˇ aa aa aa aa"}}
{"Key":"/"}
{"Key":"a"}
{"Key":"a"}
{"Key":"enter"}
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
{"Key":"g"}
{"Key":"n"}
{"Get":{"state":"aa «aaˇ» aa aa aa","mode":"Visual"}}
{"Key":"g"}
{"Key":"n"}
{"Get":{"state":"aa «aa aaˇ» aa aa","mode":"Visual"}}
{"Key":"escape"}
{"Key":"d"}
{"Key":"g"}
{"Key":"n"}
{"Get":{"state":"aa aa ˇ aa aa","mode":"Normal"}}
{"Put":{"state":"aaˇ aa aa aa aa"}}
{"Key":"/"}
{"Key":"a"}
{"Key":"a"}
{"Key":"enter"}
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
{"Key":"3"}
{"Key":"g"}
{"Key":"n"}
{"Get":{"state":"aa aa aa «aaˇ» aa","mode":"Visual"}}
{"Put":{"state":"aaˇ aa aa aa aa"}}
{"Key":"/"}
{"Key":"a"}
{"Key":"a"}
{"Key":"enter"}
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
{"Key":"g"}
{"Key":"shift-n"}
{"Get":{"state":"aa «ˇaa» aa aa aa","mode":"Visual"}}
{"Key":"g"}
{"Key":"shift-n"}
{"Get":{"state":"«ˇaa aa» aa aa aa","mode":"Visual"}}

View File

@ -24,6 +24,15 @@ pub enum Direction {
Next,
}
impl Direction {
pub fn opposite(&self) -> Self {
match self {
Direction::Prev => Direction::Next,
Direction::Next => Direction::Prev,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SearchOptions {
pub case: bool,