
Release Notes: - vim: Added support for multicursor registers (#11687) - vim: Added support for the `"/` register
287 lines
9.2 KiB
Rust
287 lines
9.2 KiB
Rust
use editor::movement;
|
|
use gpui::{actions, ViewContext, WindowContext};
|
|
use language::Point;
|
|
use workspace::Workspace;
|
|
|
|
use crate::{motion::Motion, normal::yank::copy_selections_content, Mode, Vim};
|
|
|
|
actions!(vim, [Substitute, SubstituteLine]);
|
|
|
|
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
|
workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| {
|
|
Vim::update(cx, |vim, cx| {
|
|
vim.start_recording(cx);
|
|
let count = vim.take_count(cx);
|
|
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
|
})
|
|
});
|
|
|
|
workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
|
Vim::update(cx, |vim, cx| {
|
|
vim.start_recording(cx);
|
|
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
|
vim.switch_mode(Mode::VisualLine, false, cx)
|
|
}
|
|
let count = vim.take_count(cx);
|
|
substitute(vim, count, true, cx)
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
|
|
vim.update_active_editor(cx, |vim, editor, cx| {
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
editor.transact(cx, |editor, cx| {
|
|
let text_layout_details = editor.text_layout_details(cx);
|
|
editor.change_selections(None, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
if selection.start == selection.end {
|
|
Motion::Right.expand_selection(
|
|
map,
|
|
selection,
|
|
count,
|
|
true,
|
|
&text_layout_details,
|
|
);
|
|
}
|
|
if line_mode {
|
|
// in Visual mode when the selection contains the newline at the end
|
|
// of the line, we should exclude it.
|
|
if !selection.is_empty() && selection.end.column() == 0 {
|
|
selection.end = movement::left(map, selection.end);
|
|
}
|
|
Motion::CurrentLine.expand_selection(
|
|
map,
|
|
selection,
|
|
None,
|
|
false,
|
|
&text_layout_details,
|
|
);
|
|
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
|
display_lines: false,
|
|
})
|
|
.move_point(
|
|
map,
|
|
selection.start,
|
|
selection.goal,
|
|
None,
|
|
&text_layout_details,
|
|
) {
|
|
selection.start = point;
|
|
}
|
|
}
|
|
})
|
|
});
|
|
copy_selections_content(vim, editor, line_mode, cx);
|
|
let selections = editor.selections.all::<Point>(cx).into_iter();
|
|
let edits = selections.map(|selection| (selection.start..selection.end, ""));
|
|
editor.edit(edits, cx);
|
|
});
|
|
});
|
|
vim.switch_mode(Mode::Insert, true, cx);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{
|
|
state::Mode,
|
|
test::{NeovimBackedTestContext, VimTestContext},
|
|
};
|
|
use indoc::indoc;
|
|
|
|
#[gpui::test]
|
|
async fn test_substitute(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
// supports a single cursor
|
|
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
|
cx.simulate_keystrokes("s x");
|
|
cx.assert_editor_state("xˇbc\n");
|
|
|
|
// supports a selection
|
|
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
|
|
cx.assert_editor_state("a«bcˇ»\n");
|
|
cx.simulate_keystrokes("s x");
|
|
cx.assert_editor_state("axˇ\n");
|
|
|
|
// supports counts
|
|
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
|
cx.simulate_keystrokes("2 s x");
|
|
cx.assert_editor_state("xˇc\n");
|
|
|
|
// supports multiple cursors
|
|
cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
|
|
cx.simulate_keystrokes("2 s x");
|
|
cx.assert_editor_state("axˇdexˇg\n");
|
|
|
|
// does not read beyond end of line
|
|
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
|
cx.simulate_keystrokes("5 s x");
|
|
cx.assert_editor_state("xˇ\n");
|
|
|
|
// it handles multibyte characters
|
|
cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
|
|
cx.simulate_keystrokes("4 s");
|
|
cx.assert_editor_state("ˇ\n");
|
|
|
|
// should transactionally undo selection changes
|
|
cx.simulate_keystrokes("escape u");
|
|
cx.assert_editor_state("ˇcàfé\n");
|
|
|
|
// it handles visual line mode
|
|
cx.set_state(
|
|
indoc! {"
|
|
alpha
|
|
beˇta
|
|
gamma"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("shift-v s");
|
|
cx.assert_editor_state(indoc! {"
|
|
alpha
|
|
ˇ
|
|
gamma"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("The quick ˇbrown").await;
|
|
cx.simulate_shared_keystrokes("v w c").await;
|
|
cx.shared_state().await.assert_eq("The quick ˇ");
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v w j c").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The ˇver
|
|
the lazy dog"});
|
|
|
|
cx.simulate_at_each_offset(
|
|
"v w j c",
|
|
indoc! {"
|
|
The ˇquick brown
|
|
fox jumps ˇover
|
|
the ˇlazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(
|
|
"v w k c",
|
|
indoc! {"
|
|
The ˇquick brown
|
|
fox jumps ˇover
|
|
the ˇlazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"shift-v c",
|
|
indoc! {"
|
|
The quˇick brown
|
|
fox jumps over
|
|
the lazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
// Test pasting code copied on change
|
|
cx.simulate_shared_keystrokes("escape j p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.simulate_at_each_offset(
|
|
"shift-v c",
|
|
indoc! {"
|
|
The quick brown
|
|
fox juˇmps over
|
|
the laˇzy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"shift-v j c",
|
|
indoc! {"
|
|
The quˇick brown
|
|
fox jumps over
|
|
the lazy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
// Test pasting code copied on delete
|
|
cx.simulate_shared_keystrokes("escape j p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.simulate_at_each_offset(
|
|
"shift-v j c",
|
|
indoc! {"
|
|
The quick brown
|
|
fox juˇmps over
|
|
the laˇzy dog"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
let initial_state = indoc! {"
|
|
The quick brown
|
|
fox juˇmps over
|
|
the lazy dog
|
|
"};
|
|
|
|
// normal mode
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("shift-s o").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
oˇ
|
|
the lazy dog
|
|
"});
|
|
|
|
// visual mode
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("v k shift-s o").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
oˇ
|
|
the lazy dog
|
|
"});
|
|
|
|
// visual block mode
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
oˇ
|
|
"});
|
|
|
|
// visual mode including newline
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("v $ shift-s o").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
oˇ
|
|
the lazy dog
|
|
"});
|
|
|
|
// indentation
|
|
cx.set_neovim_option("shiftwidth=4").await;
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("> > shift-s o").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
oˇ
|
|
the lazy dog
|
|
"});
|
|
}
|
|
}
|