@@ -371,6 +371,7 @@
"Replace"
],
"s": "vim::Substitute",
+ "shift-s": "vim::SubstituteLine",
"> >": "editor::Indent",
"< <": "editor::Outdent",
"ctrl-pagedown": "pane::ActivateNextItem",
@@ -446,6 +447,7 @@
}
],
"s": "vim::Substitute",
+ "shift-s": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
"shift-i": [
@@ -27,7 +27,6 @@ use self::{
case::change_case,
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
- substitute::substitute,
yank::{yank_motion, yank_object},
};
@@ -44,7 +43,6 @@ actions!(
ChangeToEndOfLine,
DeleteToEndOfLine,
Yank,
- Substitute,
ChangeCase,
]
);
@@ -56,13 +54,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(insert_line_above);
cx.add_action(insert_line_below);
cx.add_action(change_case);
+ substitute::init(cx);
search::init(cx);
- cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
- Vim::update(cx, |vim, cx| {
- let times = vim.pop_number_operator(cx);
- substitute(vim, times, cx);
- })
- });
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
@@ -1,10 +1,32 @@
-use gpui::WindowContext;
+use editor::movement;
+use gpui::{actions, AppContext, WindowContext};
use language::Point;
+use workspace::Workspace;
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
-pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
- let line_mode = vim.state().mode == Mode::VisualLine;
+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| {
@@ -14,6 +36,11 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
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,
@@ -166,4 +193,68 @@ mod test {
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;
+ }
}
@@ -0,0 +1,29 @@
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"v"}
+{"Key":"k"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"oˇ\nthe lazy dog\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\n","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":"v"}
+{"Key":"$"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}}
+{"SetOption":{"value":"shiftwidth=4"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}}
+{"Key":">"}
+{"Key":">"}
+{"Key":"shift-s"}
+{"Key":"o"}
+{"Get":{"state":"The quick brown\n oˇ\nthe lazy dog\n","mode":"Insert"}}