replace.rs

  1use crate::{
  2    motion::{self},
  3    state::Mode,
  4    Vim,
  5};
  6use editor::{display_map::ToDisplayPoint, Bias, ToPoint};
  7use gpui::{actions, ViewContext, WindowContext};
  8use language::{AutoindentMode, Point};
  9use std::ops::Range;
 10use std::sync::Arc;
 11use workspace::Workspace;
 12
 13actions!(vim, [ToggleReplace, UndoReplace]);
 14
 15pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 16    workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext<Workspace>| {
 17        Vim::update(cx, |vim, cx| {
 18            vim.update_state(|state| state.replacements = vec![]);
 19            vim.switch_mode(Mode::Replace, false, cx);
 20        });
 21    });
 22
 23    workspace.register_action(|_, _: &UndoReplace, cx: &mut ViewContext<Workspace>| {
 24        Vim::update(cx, |vim, cx| {
 25            if vim.state().mode != Mode::Replace {
 26                return;
 27            }
 28            let count = vim.take_count(cx);
 29            undo_replace(vim, count, cx)
 30        });
 31    });
 32}
 33
 34pub(crate) fn multi_replace(text: Arc<str>, cx: &mut WindowContext) {
 35    Vim::update(cx, |vim, cx| {
 36        vim.update_active_editor(cx, |vim, editor, cx| {
 37            editor.transact(cx, |editor, cx| {
 38                editor.set_clip_at_line_ends(false, cx);
 39                let map = editor.snapshot(cx);
 40                let display_selections = editor.selections.all::<Point>(cx);
 41
 42                // Handles all string that require manipulation, including inserts and replaces
 43                let edits = display_selections
 44                    .into_iter()
 45                    .map(|selection| {
 46                        let is_new_line = text.as_ref() == "\n";
 47                        let mut range = selection.range();
 48                        // "\n" need to be handled separately, because when a "\n" is typing,
 49                        // we don't do a replace, we need insert a "\n"
 50                        if !is_new_line {
 51                            range.end.column += 1;
 52                            range.end = map.buffer_snapshot.clip_point(range.end, Bias::Right);
 53                        }
 54                        let replace_range = map.buffer_snapshot.anchor_before(range.start)
 55                            ..map.buffer_snapshot.anchor_after(range.end);
 56                        let current_text = map
 57                            .buffer_snapshot
 58                            .text_for_range(replace_range.clone())
 59                            .collect();
 60                        vim.update_state(|state| {
 61                            state
 62                                .replacements
 63                                .push((replace_range.clone(), current_text))
 64                        });
 65                        (replace_range, text.clone())
 66                    })
 67                    .collect::<Vec<_>>();
 68
 69                editor.buffer().update(cx, |buffer, cx| {
 70                    buffer.edit(
 71                        edits.clone(),
 72                        Some(AutoindentMode::Block {
 73                            original_indent_columns: Vec::new(),
 74                        }),
 75                        cx,
 76                    );
 77                });
 78
 79                editor.change_selections(None, cx, |s| {
 80                    s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
 81                });
 82                editor.set_clip_at_line_ends(true, cx);
 83            });
 84        });
 85    });
 86}
 87
 88fn undo_replace(vim: &mut Vim, maybe_times: Option<usize>, cx: &mut WindowContext) {
 89    vim.update_active_editor(cx, |vim, editor, cx| {
 90        editor.transact(cx, |editor, cx| {
 91            editor.set_clip_at_line_ends(false, cx);
 92            let map = editor.snapshot(cx);
 93            let selections = editor.selections.all::<Point>(cx);
 94            let mut new_selections = vec![];
 95            let edits: Vec<(Range<Point>, String)> = selections
 96                .into_iter()
 97                .filter_map(|selection| {
 98                    let end = selection.head();
 99                    let start = motion::backspace(
100                        &map,
101                        end.to_display_point(&map),
102                        maybe_times.unwrap_or(1),
103                    )
104                    .to_point(&map);
105                    new_selections.push(
106                        map.buffer_snapshot.anchor_before(start)
107                            ..map.buffer_snapshot.anchor_before(start),
108                    );
109
110                    let mut undo = None;
111                    let edit_range = start..end;
112                    for (i, (range, inverse)) in vim.state().replacements.iter().rev().enumerate() {
113                        if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
114                            && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
115                        {
116                            undo = Some(inverse.clone());
117                            vim.update_state(|state| {
118                                state.replacements.remove(state.replacements.len() - i - 1);
119                            });
120                            break;
121                        }
122                    }
123                    Some((edit_range, undo?))
124                })
125                .collect::<Vec<_>>();
126
127            editor.buffer().update(cx, |buffer, cx| {
128                buffer.edit(edits, None, cx);
129            });
130
131            editor.change_selections(None, cx, |s| {
132                s.select_ranges(new_selections);
133            });
134            editor.set_clip_at_line_ends(true, cx);
135        });
136    });
137}
138
139#[cfg(test)]
140mod test {
141    use indoc::indoc;
142
143    use crate::{
144        state::Mode,
145        test::{NeovimBackedTestContext, VimTestContext},
146    };
147
148    #[gpui::test]
149    async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
150        let mut cx = VimTestContext::new(cx, true).await;
151        cx.simulate_keystroke("shift-r");
152        assert_eq!(cx.mode(), Mode::Replace);
153        cx.simulate_keystroke("escape");
154        assert_eq!(cx.mode(), Mode::Normal);
155    }
156
157    #[gpui::test]
158    async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
159        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
160
161        // test normal replace
162        cx.set_shared_state(indoc! {"
163            ˇThe quick brown
164            fox jumps over
165            the lazy dog."})
166            .await;
167        cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
168            .await;
169        cx.assert_shared_state(indoc! {"
170            Oneˇ quick brown
171            fox jumps over
172            the lazy dog."})
173            .await;
174        assert_eq!(Mode::Replace, cx.neovim_mode().await);
175
176        // test replace with line ending
177        cx.set_shared_state(indoc! {"
178            The quick browˇn
179            fox jumps over
180            the lazy dog."})
181            .await;
182        cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
183            .await;
184        cx.assert_shared_state(indoc! {"
185            The quick browOneˇ
186            fox jumps over
187            the lazy dog."})
188            .await;
189
190        // test replace with blank line
191        cx.set_shared_state(indoc! {"
192        The quick brown
193        ˇ
194        fox jumps over
195        the lazy dog."})
196            .await;
197        cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
198            .await;
199        cx.assert_shared_state(indoc! {"
200            The quick brown
201            Oneˇ
202            fox jumps over
203            the lazy dog."})
204            .await;
205
206        // test replace with multi cursor
207        cx.set_shared_state(indoc! {"
208            ˇThe quick brown
209            fox jumps over
210            the lazy ˇdog."})
211            .await;
212        cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
213            .await;
214        cx.assert_shared_state(indoc! {"
215            Oneˇ quick brown
216            fox jumps over
217            the lazy Oneˇ."})
218            .await;
219
220        // test replace with newline
221        cx.set_shared_state(indoc! {"
222            The quˇick brown
223            fox jumps over
224            the lazy dog."})
225            .await;
226        cx.simulate_shared_keystrokes(["shift-r", "enter", "O", "n", "e"])
227            .await;
228        cx.assert_shared_state(indoc! {"
229            The qu
230            Oneˇ brown
231            fox jumps over
232            the lazy dog."})
233            .await;
234
235        // test replace with multi cursor and newline
236        cx.set_shared_state(indoc! {"
237            ˇThe quick brown
238            fox jumps over
239            the lazy ˇdog."})
240            .await;
241        cx.simulate_shared_keystrokes(["shift-r", "O", "n", "e"])
242            .await;
243        cx.assert_shared_state(indoc! {"
244            Oneˇ quick brown
245            fox jumps over
246            the lazy Oneˇ."})
247            .await;
248        cx.simulate_shared_keystrokes(["enter", "T", "w", "o"])
249            .await;
250        cx.assert_shared_state(indoc! {"
251            One
252            Twoˇck brown
253            fox jumps over
254            the lazy One
255            Twoˇ"})
256            .await;
257    }
258
259    #[gpui::test]
260    async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
261        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
262
263        const UNDO_REPLACE_EXAMPLES: &[&'static str] = &[
264            // replace undo with single line
265            "ˇThe quick brown fox jumps over the lazy dog.",
266            // replace undo with ending line
267            indoc! {"
268                The quick browˇn
269                fox jumps over
270                the lazy dog."
271            },
272            // replace undo with empty line
273            indoc! {"
274                The quick brown
275                ˇ
276                fox jumps over
277                the lazy dog."
278            },
279            // replace undo with multi cursor
280            indoc! {"
281                The quick browˇn
282                fox jumps over
283                the lazy ˇdog."
284            },
285        ];
286
287        for example in UNDO_REPLACE_EXAMPLES {
288            // normal undo
289            cx.assert_binding_matches(
290                [
291                    "shift-r",
292                    "O",
293                    "n",
294                    "e",
295                    "backspace",
296                    "backspace",
297                    "backspace",
298                ],
299                example,
300            )
301            .await;
302            // undo with new line
303            cx.assert_binding_matches(
304                [
305                    "shift-r",
306                    "O",
307                    "enter",
308                    "e",
309                    "backspace",
310                    "backspace",
311                    "backspace",
312                ],
313                example,
314            )
315            .await;
316            cx.assert_binding_matches(
317                [
318                    "shift-r",
319                    "O",
320                    "enter",
321                    "n",
322                    "enter",
323                    "e",
324                    "backspace",
325                    "backspace",
326                    "backspace",
327                    "backspace",
328                    "backspace",
329                ],
330                example,
331            )
332            .await;
333        }
334    }
335
336    #[gpui::test]
337    async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
338        let mut cx = VimTestContext::new(cx, true).await;
339        cx.set_state("ˇabcˇabcabc", Mode::Normal);
340        cx.simulate_keystrokes(["shift-r", "1", "2", "3", "4"]);
341        cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
342        assert_eq!(cx.mode(), Mode::Replace);
343        cx.simulate_keystrokes([
344            "backspace",
345            "backspace",
346            "backspace",
347            "backspace",
348            "backspace",
349        ]);
350        cx.assert_state("ˇabˇcabcabc", Mode::Replace);
351    }
352
353    #[gpui::test]
354    async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
355        let mut cx = VimTestContext::new(cx, true).await;
356
357        cx.set_state("ˇaaaa", Mode::Normal);
358        cx.simulate_keystrokes(["0", "shift-r", "b", "b", "b", "escape", "u"]);
359        cx.assert_state("ˇaaaa", Mode::Normal);
360    }
361}