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