substitute.rs

  1use editor::movement;
  2use gpui::{actions, AppContext, WindowContext};
  3use language::Point;
  4use workspace::Workspace;
  5
  6use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
  7
  8actions!(vim, [Substitute, SubstituteLine]);
  9
 10pub(crate) fn init(cx: &mut AppContext) {
 11    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
 12        Vim::update(cx, |vim, cx| {
 13            let count = vim.pop_number_operator(cx);
 14            substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
 15        })
 16    });
 17
 18    cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
 19        Vim::update(cx, |vim, cx| {
 20            if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
 21                vim.switch_mode(Mode::VisualLine, false, cx)
 22            }
 23            let count = vim.pop_number_operator(cx);
 24            substitute(vim, count, true, cx)
 25        })
 26    });
 27}
 28
 29pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
 30    vim.update_active_editor(cx, |editor, cx| {
 31        editor.set_clip_at_line_ends(false, cx);
 32        editor.transact(cx, |editor, cx| {
 33            editor.change_selections(None, cx, |s| {
 34                s.move_with(|map, selection| {
 35                    if selection.start == selection.end {
 36                        Motion::Right.expand_selection(map, selection, count, true);
 37                    }
 38                    if line_mode {
 39                        // in Visual mode when the selection contains the newline at the end
 40                        // of the line, we should exclude it.
 41                        if !selection.is_empty() && selection.end.column() == 0 {
 42                            selection.end = movement::left(map, selection.end);
 43                        }
 44                        Motion::CurrentLine.expand_selection(map, selection, None, false);
 45                        if let Some((point, _)) = (Motion::FirstNonWhitespace {
 46                            display_lines: false,
 47                        })
 48                        .move_point(
 49                            map,
 50                            selection.start,
 51                            selection.goal,
 52                            None,
 53                        ) {
 54                            selection.start = point;
 55                        }
 56                    }
 57                })
 58            });
 59            copy_selections_content(editor, line_mode, cx);
 60            let selections = editor.selections.all::<Point>(cx).into_iter();
 61            let edits = selections.map(|selection| (selection.start..selection.end, ""));
 62            editor.edit(edits, cx);
 63        });
 64    });
 65    vim.switch_mode(Mode::Insert, true, cx);
 66}
 67
 68#[cfg(test)]
 69mod test {
 70    use crate::{
 71        state::Mode,
 72        test::{NeovimBackedTestContext, VimTestContext},
 73    };
 74    use indoc::indoc;
 75
 76    #[gpui::test]
 77    async fn test_substitute(cx: &mut gpui::TestAppContext) {
 78        let mut cx = VimTestContext::new(cx, true).await;
 79
 80        // supports a single cursor
 81        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
 82        cx.simulate_keystrokes(["s", "x"]);
 83        cx.assert_editor_state("xˇbc\n");
 84
 85        // supports a selection
 86        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
 87        cx.assert_editor_state("a«bcˇ»\n");
 88        cx.simulate_keystrokes(["s", "x"]);
 89        cx.assert_editor_state("axˇ\n");
 90
 91        // supports counts
 92        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
 93        cx.simulate_keystrokes(["2", "s", "x"]);
 94        cx.assert_editor_state("xˇc\n");
 95
 96        // supports multiple cursors
 97        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
 98        cx.simulate_keystrokes(["2", "s", "x"]);
 99        cx.assert_editor_state("axˇdexˇg\n");
100
101        // does not read beyond end of line
102        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
103        cx.simulate_keystrokes(["5", "s", "x"]);
104        cx.assert_editor_state("\n");
105
106        // it handles multibyte characters
107        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
108        cx.simulate_keystrokes(["4", "s"]);
109        cx.assert_editor_state("ˇ\n");
110
111        // should transactionally undo selection changes
112        cx.simulate_keystrokes(["escape", "u"]);
113        cx.assert_editor_state("ˇcàfé\n");
114
115        // it handles visual line mode
116        cx.set_state(
117            indoc! {"
118            alpha
119              beˇta
120            gamma"},
121            Mode::Normal,
122        );
123        cx.simulate_keystrokes(["shift-v", "s"]);
124        cx.assert_editor_state(indoc! {"
125            alpha
126              ˇ
127            gamma"});
128    }
129
130    #[gpui::test]
131    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
132        let mut cx = NeovimBackedTestContext::new(cx).await;
133
134        cx.set_shared_state("The quick ˇbrown").await;
135        cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
136        cx.assert_shared_state("The quick ˇ").await;
137
138        cx.set_shared_state(indoc! {"
139            The ˇquick brown
140            fox jumps over
141            the lazy dog"})
142            .await;
143        cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
144        cx.assert_shared_state(indoc! {"
145            The ˇver
146            the lazy dog"})
147            .await;
148
149        let cases = cx.each_marked_position(indoc! {"
150            The ˇquick brown
151            fox jumps ˇover
152            the ˇlazy dog"});
153        for initial_state in cases {
154            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
155                .await;
156            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
157                .await;
158        }
159    }
160
161    #[gpui::test]
162    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
163        let mut cx = NeovimBackedTestContext::new(cx)
164            .await
165            .binding(["shift-v", "c"]);
166        cx.assert(indoc! {"
167            The quˇick brown
168            fox jumps over
169            the lazy dog"})
170            .await;
171        // Test pasting code copied on change
172        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
173        cx.assert_state_matches().await;
174
175        cx.assert_all(indoc! {"
176            The quick brown
177            fox juˇmps over
178            the laˇzy dog"})
179            .await;
180        let mut cx = cx.binding(["shift-v", "j", "c"]);
181        cx.assert(indoc! {"
182            The quˇick brown
183            fox jumps over
184            the lazy dog"})
185            .await;
186        // Test pasting code copied on delete
187        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
188        cx.assert_state_matches().await;
189
190        cx.assert_all(indoc! {"
191            The quick brown
192            fox juˇmps over
193            the laˇzy dog"})
194            .await;
195    }
196
197    #[gpui::test]
198    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
199        let mut cx = NeovimBackedTestContext::new(cx).await;
200
201        let initial_state = indoc! {"
202                    The quick brown
203                    fox juˇmps over
204                    the lazy dog
205                    "};
206
207        // normal mode
208        cx.set_shared_state(initial_state).await;
209        cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
210        cx.assert_shared_state(indoc! {"
211            The quick brown
212213            the lazy dog
214            "})
215            .await;
216
217        // visual mode
218        cx.set_shared_state(initial_state).await;
219        cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
220            .await;
221        cx.assert_shared_state(indoc! {"
222223            the lazy dog
224            "})
225            .await;
226
227        // visual block mode
228        cx.set_shared_state(initial_state).await;
229        cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
230            .await;
231        cx.assert_shared_state(indoc! {"
232            The quick brown
233234            "})
235            .await;
236
237        // visual mode including newline
238        cx.set_shared_state(initial_state).await;
239        cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
240            .await;
241        cx.assert_shared_state(indoc! {"
242            The quick brown
243244            the lazy dog
245            "})
246            .await;
247
248        // indentation
249        cx.set_neovim_option("shiftwidth=4").await;
250        cx.set_shared_state(initial_state).await;
251        cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
252            .await;
253        cx.assert_shared_state(indoc! {"
254            The quick brown
255256            the lazy dog
257            "})
258            .await;
259    }
260}