substitute.rs

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