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
 98                    .selections
 99                    .all::<Point>(&editor.display_snapshot(cx))
100                    .into_iter();
101                let edits = selections.map(|selection| (selection.start..selection.end, ""));
102                editor.edit(edits, cx);
103            });
104        });
105        self.switch_mode(Mode::Insert, true, window, cx);
106    }
107}
108
109#[cfg(test)]
110mod test {
111    use crate::{
112        state::Mode,
113        test::{NeovimBackedTestContext, VimTestContext},
114    };
115    use indoc::indoc;
116
117    #[gpui::test]
118    async fn test_substitute(cx: &mut gpui::TestAppContext) {
119        let mut cx = VimTestContext::new(cx, true).await;
120
121        // supports a single cursor
122        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
123        cx.simulate_keystrokes("s x");
124        cx.assert_editor_state("xˇbc\n");
125
126        // supports a selection
127        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
128        cx.assert_editor_state("a«bcˇ»\n");
129        cx.simulate_keystrokes("s x");
130        cx.assert_editor_state("axˇ\n");
131
132        // supports counts
133        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
134        cx.simulate_keystrokes("2 s x");
135        cx.assert_editor_state("xˇc\n");
136
137        // supports multiple cursors
138        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
139        cx.simulate_keystrokes("2 s x");
140        cx.assert_editor_state("axˇdexˇg\n");
141
142        // does not read beyond end of line
143        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
144        cx.simulate_keystrokes("5 s x");
145        cx.assert_editor_state("\n");
146
147        // it handles multibyte characters
148        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
149        cx.simulate_keystrokes("4 s");
150        cx.assert_editor_state("ˇ\n");
151
152        // should transactionally undo selection changes
153        cx.simulate_keystrokes("escape u");
154        cx.assert_editor_state("ˇcàfé\n");
155
156        // it handles visual line mode
157        cx.set_state(
158            indoc! {"
159            alpha
160              beˇta
161            gamma"},
162            Mode::Normal,
163        );
164        cx.simulate_keystrokes("shift-v s");
165        cx.assert_editor_state(indoc! {"
166            alpha
167              ˇ
168            gamma"});
169    }
170
171    #[gpui::test]
172    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
173        let mut cx = NeovimBackedTestContext::new(cx).await;
174
175        cx.set_shared_state("The quick ˇbrown").await;
176        cx.simulate_shared_keystrokes("v w c").await;
177        cx.shared_state().await.assert_eq("The quick ˇ");
178
179        cx.set_shared_state(indoc! {"
180            The ˇquick brown
181            fox jumps over
182            the lazy dog"})
183            .await;
184        cx.simulate_shared_keystrokes("v w j c").await;
185        cx.shared_state().await.assert_eq(indoc! {"
186            The ˇver
187            the lazy dog"});
188
189        cx.simulate_at_each_offset(
190            "v w j c",
191            indoc! {"
192                    The ˇquick brown
193                    fox jumps ˇover
194                    the ˇlazy dog"},
195        )
196        .await
197        .assert_matches();
198        cx.simulate_at_each_offset(
199            "v w k c",
200            indoc! {"
201                    The ˇquick brown
202                    fox jumps ˇover
203                    the ˇlazy dog"},
204        )
205        .await
206        .assert_matches();
207    }
208
209    #[gpui::test]
210    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
211        let mut cx = NeovimBackedTestContext::new(cx).await;
212        cx.simulate(
213            "shift-v c",
214            indoc! {"
215            The quˇick brown
216            fox jumps over
217            the lazy dog"},
218        )
219        .await
220        .assert_matches();
221        // Test pasting code copied on change
222        cx.simulate_shared_keystrokes("escape j p").await;
223        cx.shared_state().await.assert_matches();
224
225        cx.simulate_at_each_offset(
226            "shift-v c",
227            indoc! {"
228            The quick brown
229            fox juˇmps over
230            the laˇzy dog"},
231        )
232        .await
233        .assert_matches();
234        cx.simulate(
235            "shift-v j c",
236            indoc! {"
237            The quˇick brown
238            fox jumps over
239            the lazy dog"},
240        )
241        .await
242        .assert_matches();
243        // Test pasting code copied on delete
244        cx.simulate_shared_keystrokes("escape j p").await;
245        cx.shared_state().await.assert_matches();
246
247        cx.simulate_at_each_offset(
248            "shift-v j c",
249            indoc! {"
250            The quick brown
251            fox juˇmps over
252            the laˇzy dog"},
253        )
254        .await
255        .assert_matches();
256    }
257
258    #[gpui::test]
259    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
260        let mut cx = NeovimBackedTestContext::new(cx).await;
261
262        let initial_state = indoc! {"
263                    The quick brown
264                    fox juˇmps over
265                    the lazy dog
266                    "};
267
268        // normal mode
269        cx.set_shared_state(initial_state).await;
270        cx.simulate_shared_keystrokes("shift-s o").await;
271        cx.shared_state().await.assert_eq(indoc! {"
272            The quick brown
273274            the lazy dog
275            "});
276
277        // visual mode
278        cx.set_shared_state(initial_state).await;
279        cx.simulate_shared_keystrokes("v k shift-s o").await;
280        cx.shared_state().await.assert_eq(indoc! {"
281282            the lazy dog
283            "});
284
285        // visual block mode
286        cx.set_shared_state(initial_state).await;
287        cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
288        cx.shared_state().await.assert_eq(indoc! {"
289            The quick brown
290291            "});
292
293        // visual mode including newline
294        cx.set_shared_state(initial_state).await;
295        cx.simulate_shared_keystrokes("v $ shift-s o").await;
296        cx.shared_state().await.assert_eq(indoc! {"
297            The quick brown
298299            the lazy dog
300            "});
301
302        // indentation
303        cx.set_neovim_option("shiftwidth=4").await;
304        cx.set_shared_state(initial_state).await;
305        cx.simulate_shared_keystrokes("> > shift-s o").await;
306        cx.shared_state().await.assert_eq(indoc! {"
307            The quick brown
308309            the lazy dog
310            "});
311    }
312}