substitute.rs

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