substitute.rs

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