309 lines
9.8 KiB
Rust
309 lines
9.8 KiB
Rust
use editor::{Editor, SelectionEffects, movement};
|
|
use gpui::{Context, Window, actions};
|
|
use language::Point;
|
|
|
|
use crate::{
|
|
Mode, Vim,
|
|
motion::{Motion, MotionKind},
|
|
};
|
|
|
|
actions!(
|
|
vim,
|
|
[
|
|
/// Substitutes characters in the current selection.
|
|
Substitute,
|
|
/// Substitutes the entire line.
|
|
SubstituteLine
|
|
]
|
|
);
|
|
|
|
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|
Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
|
|
vim.start_recording(cx);
|
|
let count = Vim::take_count(cx);
|
|
Vim::take_forced_motion(cx);
|
|
vim.substitute(count, vim.mode == Mode::VisualLine, window, cx);
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| {
|
|
vim.start_recording(cx);
|
|
if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
|
|
vim.switch_mode(Mode::VisualLine, false, window, cx)
|
|
}
|
|
let count = Vim::take_count(cx);
|
|
Vim::take_forced_motion(cx);
|
|
vim.substitute(count, true, window, cx)
|
|
});
|
|
}
|
|
|
|
impl Vim {
|
|
pub fn substitute(
|
|
&mut self,
|
|
count: Option<usize>,
|
|
line_mode: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.store_visual_marks(window, cx);
|
|
self.update_editor(cx, |vim, editor, cx| {
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
let text_layout_details = editor.text_layout_details(window);
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.move_with(|map, selection| {
|
|
if selection.start == selection.end {
|
|
Motion::Right.expand_selection(
|
|
map,
|
|
selection,
|
|
count,
|
|
&text_layout_details,
|
|
false,
|
|
);
|
|
}
|
|
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,
|
|
&text_layout_details,
|
|
false,
|
|
);
|
|
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
|
display_lines: false,
|
|
})
|
|
.move_point(
|
|
map,
|
|
selection.start,
|
|
selection.goal,
|
|
None,
|
|
&text_layout_details,
|
|
) {
|
|
selection.start = point;
|
|
}
|
|
}
|
|
})
|
|
});
|
|
let kind = if line_mode {
|
|
MotionKind::Linewise
|
|
} else {
|
|
MotionKind::Exclusive
|
|
};
|
|
vim.copy_selections_content(editor, kind, window, cx);
|
|
let selections = editor.selections.all::<Point>(cx).into_iter();
|
|
let edits = selections.map(|selection| (selection.start..selection.end, ""));
|
|
editor.edit(edits, cx);
|
|
});
|
|
});
|
|
self.switch_mode(Mode::Insert, true, window, 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
|
|
"});
|
|
}
|
|
}
|