substitute.rs

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