substitute.rs

  1use gpui::WindowContext;
  2use language::Point;
  3
  4use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
  5
  6pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
  7    let line_mode = vim.state().mode == Mode::VisualLine;
  8    vim.update_active_editor(cx, |editor, cx| {
  9        editor.set_clip_at_line_ends(false, cx);
 10        editor.transact(cx, |editor, cx| {
 11            editor.change_selections(None, cx, |s| {
 12                s.move_with(|map, selection| {
 13                    if selection.start == selection.end {
 14                        Motion::Right.expand_selection(map, selection, count, true);
 15                    }
 16                    if line_mode {
 17                        Motion::CurrentLine.expand_selection(map, selection, None, false);
 18                        if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
 19                            map,
 20                            selection.start,
 21                            selection.goal,
 22                            None,
 23                        ) {
 24                            selection.start = point;
 25                        }
 26                    }
 27                })
 28            });
 29            copy_selections_content(editor, line_mode, cx);
 30            let selections = editor.selections.all::<Point>(cx).into_iter();
 31            let edits = selections.map(|selection| (selection.start..selection.end, ""));
 32            editor.edit(edits, cx);
 33        });
 34    });
 35    vim.switch_mode(Mode::Insert, true, cx);
 36}
 37
 38#[cfg(test)]
 39mod test {
 40    use crate::{
 41        state::Mode,
 42        test::{NeovimBackedTestContext, VimTestContext},
 43    };
 44    use indoc::indoc;
 45
 46    #[gpui::test]
 47    async fn test_substitute(cx: &mut gpui::TestAppContext) {
 48        let mut cx = VimTestContext::new(cx, true).await;
 49
 50        // supports a single cursor
 51        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
 52        cx.simulate_keystrokes(["s", "x"]);
 53        cx.assert_editor_state("xˇbc\n");
 54
 55        // supports a selection
 56        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
 57        cx.assert_editor_state("a«bcˇ»\n");
 58        cx.simulate_keystrokes(["s", "x"]);
 59        cx.assert_editor_state("axˇ\n");
 60
 61        // supports counts
 62        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
 63        cx.simulate_keystrokes(["2", "s", "x"]);
 64        cx.assert_editor_state("xˇc\n");
 65
 66        // supports multiple cursors
 67        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
 68        cx.simulate_keystrokes(["2", "s", "x"]);
 69        cx.assert_editor_state("axˇdexˇg\n");
 70
 71        // does not read beyond end of line
 72        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
 73        cx.simulate_keystrokes(["5", "s", "x"]);
 74        cx.assert_editor_state("\n");
 75
 76        // it handles multibyte characters
 77        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
 78        cx.simulate_keystrokes(["4", "s"]);
 79        cx.assert_editor_state("ˇ\n");
 80
 81        // should transactionally undo selection changes
 82        cx.simulate_keystrokes(["escape", "u"]);
 83        cx.assert_editor_state("ˇcàfé\n");
 84
 85        // it handles visual line mode
 86        cx.set_state(
 87            indoc! {"
 88            alpha
 89              beˇta
 90            gamma"},
 91            Mode::Normal,
 92        );
 93        cx.simulate_keystrokes(["shift-v", "s"]);
 94        cx.assert_editor_state(indoc! {"
 95            alpha
 96              ˇ
 97            gamma"});
 98    }
 99
100    #[gpui::test]
101    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
102        let mut cx = NeovimBackedTestContext::new(cx).await;
103
104        cx.set_shared_state("The quick ˇbrown").await;
105        cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
106        cx.assert_shared_state("The quick ˇ").await;
107
108        cx.set_shared_state(indoc! {"
109            The ˇquick brown
110            fox jumps over
111            the lazy dog"})
112            .await;
113        cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
114        cx.assert_shared_state(indoc! {"
115            The ˇver
116            the lazy dog"})
117            .await;
118
119        let cases = cx.each_marked_position(indoc! {"
120            The ˇquick brown
121            fox jumps ˇover
122            the ˇlazy dog"});
123        for initial_state in cases {
124            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
125                .await;
126            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
127                .await;
128        }
129    }
130
131    #[gpui::test]
132    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
133        let mut cx = NeovimBackedTestContext::new(cx)
134            .await
135            .binding(["shift-v", "c"]);
136        cx.assert(indoc! {"
137            The quˇick brown
138            fox jumps over
139            the lazy dog"})
140            .await;
141        // Test pasting code copied on change
142        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
143        cx.assert_state_matches().await;
144
145        cx.assert_all(indoc! {"
146            The quick brown
147            fox juˇmps over
148            the laˇzy dog"})
149            .await;
150        let mut cx = cx.binding(["shift-v", "j", "c"]);
151        cx.assert(indoc! {"
152            The quˇick brown
153            fox jumps over
154            the lazy dog"})
155            .await;
156        // Test pasting code copied on delete
157        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
158        cx.assert_state_matches().await;
159
160        cx.assert_all(indoc! {"
161            The quick brown
162            fox juˇmps over
163            the laˇzy dog"})
164            .await;
165    }
166}