312 lines · 9.8 KB · Rust
RawDownload
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>(&editor.display_snapshot(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            "});    }}