use editor::movement;use gpui::{actions, AppContext, WindowContext};use language::Point;use workspace::Workspace; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; actions!(vim, [Substitute, SubstituteLine]); pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let count = vim.pop_number_operator(cx); substitute(vim, count, vim.state().mode == Mode::VisualLine, cx); }) }); cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| { Vim::update(cx, |vim, cx| { if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { vim.switch_mode(Mode::VisualLine, false, cx) } let count = vim.pop_number_operator(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, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, 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); } 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); if let Some((point, _)) = (Motion::FirstNonWhitespace { display_lines: false, }) .move_point( map, selection.start, selection.goal, None, ) { selection.start = point; } } }) }); copy_selections_content(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.assert_shared_state("The quick ˇ").await; 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.assert_shared_state(indoc! {" The ˇver the lazy dog"}) .await; let cases = cx.each_marked_position(indoc! {" The ˇquick brown fox jumps ˇover the ˇlazy dog"}); for initial_state in cases { cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"]) .await; cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"]) .await; } } #[gpui::test] async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx) .await .binding(["shift-v", "c"]); cx.assert(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; // Test pasting code copied on change cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; cx.assert_state_matches().await; cx.assert_all(indoc! {" The quick brown fox juˇmps over the laˇzy dog"}) .await; let mut cx = cx.binding(["shift-v", "j", "c"]); cx.assert(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; // Test pasting code copied on delete cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; cx.assert_state_matches().await; cx.assert_all(indoc! {" The quick brown fox juˇmps over the laˇzy dog"}) .await; } #[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.assert_shared_state(indoc! {" The quick brown oˇ the lazy dog "}) .await; // visual mode cx.set_shared_state(initial_state).await; cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"]) .await; cx.assert_shared_state(indoc! {" oˇ the lazy dog "}) .await; // visual block mode cx.set_shared_state(initial_state).await; cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"]) .await; cx.assert_shared_state(indoc! {" The quick brown oˇ "}) .await; // visual mode including newline cx.set_shared_state(initial_state).await; cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"]) .await; cx.assert_shared_state(indoc! {" The quick brown oˇ the lazy dog "}) .await; // indentation cx.set_neovim_option("shiftwidth=4").await; cx.set_shared_state(initial_state).await; cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"]) .await; cx.assert_shared_state(indoc! {" The quick brown oˇ the lazy dog "}) .await; }}
use editor::movement;
use gpui::{actions, AppContext, WindowContext};
use language::Point;
use workspace::Workspace;
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
actions!(vim, [Substitute, SubstituteLine]);
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
let count = vim.pop_number_operator(cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
Vim::update(cx, |vim, cx| {
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
vim.switch_mode(Mode::VisualLine, false, cx)
}
let count = vim.pop_number_operator(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, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, 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);
}
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);
if let Some((point, _)) = (Motion::FirstNonWhitespace {
display_lines: false,
})
.move_point(
map,
selection.start,
selection.goal,
None,
) {
selection.start = point;
}
}
})
});
copy_selections_content(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.assert_shared_state("The quick ˇ").await;
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.assert_shared_state(indoc! {"
The ˇver
the lazy dog"})
.await;
let cases = cx.each_marked_position(indoc! {"
The ˇquick brown
fox jumps ˇover
the ˇlazy dog"});
for initial_state in cases {
cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
.await;
cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
.await;
}
}
#[gpui::test]
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["shift-v", "c"]);
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"})
.await;
// Test pasting code copied on change
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
cx.assert_state_matches().await;
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the laˇzy dog"})
.await;
let mut cx = cx.binding(["shift-v", "j", "c"]);
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"})
.await;
// Test pasting code copied on delete
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
cx.assert_state_matches().await;
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the laˇzy dog"})
.await;
}
#[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.assert_shared_state(indoc! {"
The quick brown
oˇ
the lazy dog
"})
.await;
// visual mode
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
.await;
cx.assert_shared_state(indoc! {"
oˇ
the lazy dog
"})
.await;
// visual block mode
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
.await;
cx.assert_shared_state(indoc! {"
The quick brown
oˇ
"})
.await;
// visual mode including newline
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
.await;
cx.assert_shared_state(indoc! {"
The quick brown
oˇ
the lazy dog
"})
.await;
// indentation
cx.set_neovim_option("shiftwidth=4").await;
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
.await;
cx.assert_shared_state(indoc! {"
The quick brown
oˇ
the lazy dog
"})
.await;
}
}