substitute.rs

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