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.assert_shared_state("The quick ˇ").await;
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.assert_shared_state(indoc! {"
161            The ˇver
162            the lazy dog"})
163            .await;
164
165        let cases = cx.each_marked_position(indoc! {"
166            The ˇquick brown
167            fox jumps ˇover
168            the ˇlazy dog"});
169        for initial_state in cases {
170            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
171                .await;
172            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
173                .await;
174        }
175    }
176
177    #[gpui::test]
178    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
179        let mut cx = NeovimBackedTestContext::new(cx)
180            .await
181            .binding(["shift-v", "c"]);
182        cx.assert(indoc! {"
183            The quˇick brown
184            fox jumps over
185            the lazy dog"})
186            .await;
187        // Test pasting code copied on change
188        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
189        cx.assert_state_matches().await;
190
191        cx.assert_all(indoc! {"
192            The quick brown
193            fox juˇmps over
194            the laˇzy dog"})
195            .await;
196        let mut cx = cx.binding(["shift-v", "j", "c"]);
197        cx.assert(indoc! {"
198            The quˇick brown
199            fox jumps over
200            the lazy dog"})
201            .await;
202        // Test pasting code copied on delete
203        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
204        cx.assert_state_matches().await;
205
206        cx.assert_all(indoc! {"
207            The quick brown
208            fox juˇmps over
209            the laˇzy dog"})
210            .await;
211    }
212
213    #[gpui::test]
214    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
215        let mut cx = NeovimBackedTestContext::new(cx).await;
216
217        let initial_state = indoc! {"
218                    The quick brown
219                    fox juˇmps over
220                    the lazy dog
221                    "};
222
223        // normal mode
224        cx.set_shared_state(initial_state).await;
225        cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
226        cx.assert_shared_state(indoc! {"
227            The quick brown
228229            the lazy dog
230            "})
231            .await;
232
233        // visual mode
234        cx.set_shared_state(initial_state).await;
235        cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
236            .await;
237        cx.assert_shared_state(indoc! {"
238239            the lazy dog
240            "})
241            .await;
242
243        // visual block mode
244        cx.set_shared_state(initial_state).await;
245        cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
246            .await;
247        cx.assert_shared_state(indoc! {"
248            The quick brown
249250            "})
251            .await;
252
253        // visual mode including newline
254        cx.set_shared_state(initial_state).await;
255        cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
256            .await;
257        cx.assert_shared_state(indoc! {"
258            The quick brown
259260            the lazy dog
261            "})
262            .await;
263
264        // indentation
265        cx.set_neovim_option("shiftwidth=4").await;
266        cx.set_shared_state(initial_state).await;
267        cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
268            .await;
269        cx.assert_shared_state(indoc! {"
270            The quick brown
271272            the lazy dog
273            "})
274            .await;
275    }
276}