substitute.rs

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