substitute.rs

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