replace.rs

  1use crate::{
  2    Vim,
  3    motion::{self, Motion},
  4    object::Object,
  5    state::Mode,
  6};
  7use editor::{
  8    Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint,
  9    display_map::ToDisplayPoint,
 10};
 11use gpui::{Context, Window, actions};
 12use language::{Point, SelectionGoal};
 13use std::ops::Range;
 14use std::sync::Arc;
 15
 16actions!(
 17    vim,
 18    [
 19        /// Toggles replace mode.
 20        ToggleReplace,
 21        /// Undoes the last replacement.
 22        UndoReplace
 23    ]
 24);
 25
 26pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 27    Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| {
 28        vim.replacements = vec![];
 29        vim.start_recording(cx);
 30        vim.switch_mode(Mode::Replace, false, window, cx);
 31    });
 32
 33    Vim::action(editor, cx, |vim, _: &UndoReplace, window, cx| {
 34        if vim.mode != Mode::Replace {
 35            return;
 36        }
 37        let count = Vim::take_count(cx);
 38        Vim::take_forced_motion(cx);
 39        vim.undo_replace(count, window, cx)
 40    });
 41}
 42
 43struct VimExchange;
 44
 45impl Vim {
 46    pub(crate) fn multi_replace(
 47        &mut self,
 48        text: Arc<str>,
 49        window: &mut Window,
 50        cx: &mut Context<Self>,
 51    ) {
 52        self.update_editor(cx, |vim, editor, cx| {
 53            editor.transact(window, cx, |editor, window, cx| {
 54                editor.set_clip_at_line_ends(false, cx);
 55                let map = editor.snapshot(window, cx);
 56                let display_selections = editor.selections.all::<Point>(cx);
 57
 58                // Handles all string that require manipulation, including inserts and replaces
 59                let edits = display_selections
 60                    .into_iter()
 61                    .map(|selection| {
 62                        let is_new_line = text.as_ref() == "\n";
 63                        let mut range = selection.range();
 64                        // "\n" need to be handled separately, because when a "\n" is typing,
 65                        // we don't do a replace, we need insert a "\n"
 66                        if !is_new_line {
 67                            range.end.column += 1;
 68                            range.end = map.buffer_snapshot().clip_point(range.end, Bias::Right);
 69                        }
 70                        let replace_range = map.buffer_snapshot().anchor_before(range.start)
 71                            ..map.buffer_snapshot().anchor_after(range.end);
 72                        let current_text = map
 73                            .buffer_snapshot()
 74                            .text_for_range(replace_range.clone())
 75                            .collect();
 76                        vim.replacements.push((replace_range.clone(), current_text));
 77                        (replace_range, text.clone())
 78                    })
 79                    .collect::<Vec<_>>();
 80
 81                editor.edit_with_block_indent(edits.clone(), Vec::new(), cx);
 82
 83                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 84                    s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
 85                });
 86                editor.set_clip_at_line_ends(true, cx);
 87            });
 88        });
 89    }
 90
 91    fn undo_replace(
 92        &mut self,
 93        maybe_times: Option<usize>,
 94        window: &mut Window,
 95        cx: &mut Context<Self>,
 96    ) {
 97        self.update_editor(cx, |vim, editor, cx| {
 98            editor.transact(window, cx, |editor, window, cx| {
 99                editor.set_clip_at_line_ends(false, cx);
100                let map = editor.snapshot(window, cx);
101                let selections = editor.selections.all::<Point>(cx);
102                let mut new_selections = vec![];
103                let edits: Vec<(Range<Point>, String)> = selections
104                    .into_iter()
105                    .filter_map(|selection| {
106                        let end = selection.head();
107                        let start = motion::wrapping_left(
108                            &map,
109                            end.to_display_point(&map),
110                            maybe_times.unwrap_or(1),
111                        )
112                        .to_point(&map);
113                        new_selections.push(
114                            map.buffer_snapshot().anchor_before(start)
115                                ..map.buffer_snapshot().anchor_before(start),
116                        );
117
118                        let mut undo = None;
119                        let edit_range = start..end;
120                        for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
121                            if range.start.to_point(&map.buffer_snapshot()) <= edit_range.start
122                                && range.end.to_point(&map.buffer_snapshot()) >= edit_range.end
123                            {
124                                undo = Some(inverse.clone());
125                                vim.replacements.remove(vim.replacements.len() - i - 1);
126                                break;
127                            }
128                        }
129                        Some((edit_range, undo?))
130                    })
131                    .collect::<Vec<_>>();
132
133                editor.edit(edits, cx);
134
135                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
136                    s.select_ranges(new_selections);
137                });
138                editor.set_clip_at_line_ends(true, cx);
139            });
140        });
141    }
142
143    pub fn exchange_object(
144        &mut self,
145        object: Object,
146        around: bool,
147        window: &mut Window,
148        cx: &mut Context<Self>,
149    ) {
150        self.stop_recording(cx);
151        self.update_editor(cx, |vim, editor, cx| {
152            editor.set_clip_at_line_ends(false, cx);
153            let mut selection = editor.selections.newest_display(cx);
154            let snapshot = editor.snapshot(window, cx);
155            object.expand_selection(&snapshot, &mut selection, around, None);
156            let start = snapshot
157                .buffer_snapshot()
158                .anchor_before(selection.start.to_point(&snapshot));
159            let end = snapshot
160                .buffer_snapshot()
161                .anchor_before(selection.end.to_point(&snapshot));
162            let new_range = start..end;
163            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
164            editor.set_clip_at_line_ends(true, cx);
165        });
166    }
167
168    pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
169        self.stop_recording(cx);
170        self.update_editor(cx, |vim, editor, cx| {
171            let selection = editor.selections.newest_anchor();
172            let new_range = selection.start..selection.end;
173            let snapshot = editor.snapshot(window, cx);
174            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
175        });
176        self.switch_mode(Mode::Normal, false, window, cx);
177    }
178
179    pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
180        self.stop_recording(cx);
181        self.update_editor(cx, |_, editor, cx| {
182            editor.clear_background_highlights::<VimExchange>(cx);
183        });
184        self.clear_operator(window, cx);
185    }
186
187    pub fn exchange_motion(
188        &mut self,
189        motion: Motion,
190        times: Option<usize>,
191        forced_motion: bool,
192        window: &mut Window,
193        cx: &mut Context<Self>,
194    ) {
195        self.stop_recording(cx);
196        self.update_editor(cx, |vim, editor, cx| {
197            editor.set_clip_at_line_ends(false, cx);
198            let text_layout_details = editor.text_layout_details(window);
199            let mut selection = editor.selections.newest_display(cx);
200            let snapshot = editor.snapshot(window, cx);
201            motion.expand_selection(
202                &snapshot,
203                &mut selection,
204                times,
205                &text_layout_details,
206                forced_motion,
207            );
208            let start = snapshot
209                .buffer_snapshot()
210                .anchor_before(selection.start.to_point(&snapshot));
211            let end = snapshot
212                .buffer_snapshot()
213                .anchor_before(selection.end.to_point(&snapshot));
214            let new_range = start..end;
215            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
216            editor.set_clip_at_line_ends(true, cx);
217        });
218    }
219
220    pub fn exchange_impl(
221        &self,
222        new_range: Range<Anchor>,
223        editor: &mut Editor,
224        snapshot: &EditorSnapshot,
225        window: &mut Window,
226        cx: &mut Context<Editor>,
227    ) {
228        if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
229            let previous_range = ranges[0].clone();
230
231            let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot());
232            let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot());
233            let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot());
234            let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot());
235
236            let text_for = |range: Range<Anchor>| {
237                snapshot
238                    .buffer_snapshot()
239                    .text_for_range(range)
240                    .collect::<String>()
241            };
242
243            let mut final_cursor_position = None;
244
245            if previous_range_end < new_range_start || new_range_end < previous_range_start {
246                let previous_text = text_for(previous_range.clone());
247                let new_text = text_for(new_range.clone());
248                final_cursor_position = Some(new_range.start.to_display_point(snapshot));
249
250                editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
251            } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
252            {
253                final_cursor_position = Some(new_range.start.to_display_point(snapshot));
254                editor.edit([(new_range, text_for(previous_range))], cx);
255            } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
256            {
257                final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
258                editor.edit([(previous_range, text_for(new_range))], cx);
259            }
260
261            if let Some(position) = final_cursor_position {
262                editor.change_selections(Default::default(), window, cx, |s| {
263                    s.move_with(|_map, selection| {
264                        selection.collapse_to(position, SelectionGoal::None);
265                    });
266                })
267            }
268        } else {
269            let ranges = [new_range];
270            editor.highlight_background::<VimExchange>(
271                &ranges,
272                |theme| theme.colors().editor_document_highlight_read_background,
273                cx,
274            );
275        }
276    }
277}
278
279#[cfg(test)]
280mod test {
281    use indoc::indoc;
282
283    use crate::{
284        state::Mode,
285        test::{NeovimBackedTestContext, VimTestContext},
286    };
287
288    #[gpui::test]
289    async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
290        let mut cx = VimTestContext::new(cx, true).await;
291        cx.simulate_keystrokes("shift-r");
292        assert_eq!(cx.mode(), Mode::Replace);
293        cx.simulate_keystrokes("escape");
294        assert_eq!(cx.mode(), Mode::Normal);
295    }
296
297    #[gpui::test]
298    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
299    async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
300        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
301
302        // test normal replace
303        cx.set_shared_state(indoc! {"
304            ˇThe quick brown
305            fox jumps over
306            the lazy dog."})
307            .await;
308        cx.simulate_shared_keystrokes("shift-r O n e").await;
309        cx.shared_state().await.assert_eq(indoc! {"
310            Oneˇ quick brown
311            fox jumps over
312            the lazy dog."});
313
314        // test replace with line ending
315        cx.set_shared_state(indoc! {"
316            The quick browˇn
317            fox jumps over
318            the lazy dog."})
319            .await;
320        cx.simulate_shared_keystrokes("shift-r O n e").await;
321        cx.shared_state().await.assert_eq(indoc! {"
322            The quick browOneˇ
323            fox jumps over
324            the lazy dog."});
325
326        // test replace with blank line
327        cx.set_shared_state(indoc! {"
328        The quick brown
329        ˇ
330        fox jumps over
331        the lazy dog."})
332            .await;
333        cx.simulate_shared_keystrokes("shift-r O n e").await;
334        cx.shared_state().await.assert_eq(indoc! {"
335            The quick brown
336            Oneˇ
337            fox jumps over
338            the lazy dog."});
339
340        // test replace with newline
341        cx.set_shared_state(indoc! {"
342            The quˇick brown
343            fox jumps over
344            the lazy dog."})
345            .await;
346        cx.simulate_shared_keystrokes("shift-r enter O n e").await;
347        cx.shared_state().await.assert_eq(indoc! {"
348            The qu
349            Oneˇ brown
350            fox jumps over
351            the lazy dog."});
352
353        // test replace with multi cursor and newline
354        cx.set_state(
355            indoc! {"
356            ˇThe quick brown
357            fox jumps over
358            the lazy ˇdog."},
359            Mode::Normal,
360        );
361        cx.simulate_keystrokes("shift-r O n e");
362        cx.assert_state(
363            indoc! {"
364            Oneˇ quick brown
365            fox jumps over
366            the lazy Oneˇ."},
367            Mode::Replace,
368        );
369        cx.simulate_keystrokes("enter T w o");
370        cx.assert_state(
371            indoc! {"
372            One
373            Twoˇck brown
374            fox jumps over
375            the lazy One
376            Twoˇ"},
377            Mode::Replace,
378        );
379    }
380
381    #[gpui::test]
382    async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
383        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
384
385        cx.set_shared_state("ˇhello\n").await;
386        cx.simulate_shared_keystrokes("3 shift-r - escape").await;
387        cx.shared_state().await.assert_eq("--ˇ-lo\n");
388
389        cx.set_shared_state("ˇhello\n").await;
390        cx.simulate_shared_keystrokes("3 shift-r a b c escape")
391            .await;
392        cx.shared_state().await.assert_eq("abcabcabˇc\n");
393    }
394
395    #[gpui::test]
396    async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
397        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
398
399        cx.set_shared_state("ˇhello world\n").await;
400        cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
401            .await;
402        cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
403    }
404
405    #[gpui::test]
406    async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
407        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
408
409        const UNDO_REPLACE_EXAMPLES: &[&str] = &[
410            // replace undo with single line
411            "ˇThe quick brown fox jumps over the lazy dog.",
412            // replace undo with ending line
413            indoc! {"
414                The quick browˇn
415                fox jumps over
416                the lazy dog."
417            },
418            // replace undo with empty line
419            indoc! {"
420                The quick brown
421                ˇ
422                fox jumps over
423                the lazy dog."
424            },
425        ];
426
427        for example in UNDO_REPLACE_EXAMPLES {
428            // normal undo
429            cx.simulate("shift-r O n e backspace backspace backspace", example)
430                .await
431                .assert_matches();
432            // undo with new line
433            cx.simulate("shift-r O enter e backspace backspace backspace", example)
434                .await
435                .assert_matches();
436            cx.simulate(
437                "shift-r O enter n enter e backspace backspace backspace backspace backspace",
438                example,
439            )
440            .await
441            .assert_matches();
442        }
443    }
444
445    #[gpui::test]
446    async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
447        let mut cx = VimTestContext::new(cx, true).await;
448        cx.set_state("ˇabcˇabcabc", Mode::Normal);
449        cx.simulate_keystrokes("shift-r 1 2 3 4");
450        cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
451        assert_eq!(cx.mode(), Mode::Replace);
452        cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
453        cx.assert_state("ˇabˇcabcabc", Mode::Replace);
454    }
455
456    #[gpui::test]
457    async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
458        let mut cx = VimTestContext::new(cx, true).await;
459
460        cx.set_state("ˇaaaa", Mode::Normal);
461        cx.simulate_keystrokes("0 shift-r b b b escape u");
462        cx.assert_state("ˇaaaa", Mode::Normal);
463    }
464
465    #[gpui::test]
466    async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
467        let mut cx = VimTestContext::new(cx, true).await;
468
469        cx.set_state("ˇhello world", Mode::Normal);
470        cx.simulate_keystrokes("c x i w w c x i w");
471        cx.assert_state("world ˇhello", Mode::Normal);
472    }
473
474    #[gpui::test]
475    async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
476        let mut cx = VimTestContext::new(cx, true).await;
477
478        cx.set_state("ˇhello world", Mode::Normal);
479        cx.simulate_keystrokes("c x x w c x i w");
480        cx.assert_state("ˇworld", Mode::Normal);
481
482        // the focus should still be at the start of the word if we reverse the
483        // order of selections (smaller -> larger)
484        cx.set_state("ˇhello world", Mode::Normal);
485        cx.simulate_keystrokes("c x i w c x x");
486        cx.assert_state("ˇhello", Mode::Normal);
487    }
488
489    #[gpui::test]
490    async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
491        let mut cx = VimTestContext::new(cx, true).await;
492
493        cx.set_state("ˇhello world", Mode::Normal);
494        cx.simulate_keystrokes("c x t r w c x i w");
495        cx.assert_state("hello ˇworld", Mode::Normal);
496    }
497
498    #[gpui::test]
499    async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) {
500        let mut cx = VimTestContext::new(cx, true).await;
501
502        cx.set_state("ˇirrelevant", Mode::Normal);
503        cx.simulate_keystrokes("c x c");
504
505        assert_eq!(cx.active_operator(), None);
506    }
507
508    #[gpui::test]
509    async fn test_clear_exchange(cx: &mut gpui::TestAppContext) {
510        let mut cx = VimTestContext::new(cx, true).await;
511
512        cx.set_state("ˇhello world", Mode::Normal);
513        cx.simulate_keystrokes("c x i w c x c");
514
515        cx.update_editor(|editor, window, cx| {
516            let highlights = editor.all_text_background_highlights(window, cx);
517            assert_eq!(0, highlights.len());
518        });
519    }
520}