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