substitute.rs

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