substitute.rs

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