substitute.rs

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