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>(&map.display_snapshot);
 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>(&map.display_snapshot);
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
154                .selections
155                .newest_display(&editor.display_snapshot(cx));
156            let snapshot = editor.snapshot(window, cx);
157            object.expand_selection(&snapshot, &mut selection, around, true, None);
158            let start = snapshot
159                .buffer_snapshot()
160                .anchor_before(selection.start.to_point(&snapshot));
161            let end = snapshot
162                .buffer_snapshot()
163                .anchor_before(selection.end.to_point(&snapshot));
164            let new_range = start..end;
165            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
166            editor.set_clip_at_line_ends(true, cx);
167        });
168    }
169
170    pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
171        self.stop_recording(cx);
172        self.update_editor(cx, |vim, editor, cx| {
173            let selection = editor.selections.newest_anchor();
174            let new_range = selection.start..selection.end;
175            let snapshot = editor.snapshot(window, cx);
176            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
177        });
178        self.switch_mode(Mode::Normal, false, window, cx);
179    }
180
181    pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
182        self.stop_recording(cx);
183        self.update_editor(cx, |_, editor, cx| {
184            editor.clear_background_highlights::<VimExchange>(cx);
185        });
186        self.clear_operator(window, cx);
187    }
188
189    pub fn exchange_motion(
190        &mut self,
191        motion: Motion,
192        times: Option<usize>,
193        forced_motion: bool,
194        window: &mut Window,
195        cx: &mut Context<Self>,
196    ) {
197        self.stop_recording(cx);
198        self.update_editor(cx, |vim, editor, cx| {
199            editor.set_clip_at_line_ends(false, cx);
200            let text_layout_details = editor.text_layout_details(window);
201            let mut selection = editor
202                .selections
203                .newest_display(&editor.display_snapshot(cx));
204            let snapshot = editor.snapshot(window, cx);
205            motion.expand_selection(
206                &snapshot,
207                &mut selection,
208                times,
209                &text_layout_details,
210                forced_motion,
211            );
212            let start = snapshot
213                .buffer_snapshot()
214                .anchor_before(selection.start.to_point(&snapshot));
215            let end = snapshot
216                .buffer_snapshot()
217                .anchor_before(selection.end.to_point(&snapshot));
218            let new_range = start..end;
219            vim.exchange_impl(new_range, editor, &snapshot, window, cx);
220            editor.set_clip_at_line_ends(true, cx);
221        });
222    }
223
224    pub fn exchange_impl(
225        &self,
226        new_range: Range<Anchor>,
227        editor: &mut Editor,
228        snapshot: &EditorSnapshot,
229        window: &mut Window,
230        cx: &mut Context<Editor>,
231    ) {
232        if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
233            let previous_range = ranges[0].clone();
234
235            let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot());
236            let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot());
237            let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot());
238            let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot());
239
240            let text_for = |range: Range<Anchor>| {
241                snapshot
242                    .buffer_snapshot()
243                    .text_for_range(range)
244                    .collect::<String>()
245            };
246
247            let mut final_cursor_position = None;
248
249            if previous_range_end < new_range_start || new_range_end < previous_range_start {
250                let previous_text = text_for(previous_range.clone());
251                let new_text = text_for(new_range.clone());
252                final_cursor_position = Some(new_range.start.to_display_point(snapshot));
253
254                editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
255            } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
256            {
257                final_cursor_position = Some(new_range.start.to_display_point(snapshot));
258                editor.edit([(new_range, text_for(previous_range))], cx);
259            } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
260            {
261                final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
262                editor.edit([(previous_range, text_for(new_range))], cx);
263            }
264
265            if let Some(position) = final_cursor_position {
266                editor.change_selections(Default::default(), window, cx, |s| {
267                    s.move_with(|_map, selection| {
268                        selection.collapse_to(position, SelectionGoal::None);
269                    });
270                })
271            }
272        } else {
273            let ranges = [new_range];
274            editor.highlight_background::<VimExchange>(
275                &ranges,
276                |theme| theme.colors().editor_document_highlight_read_background,
277                cx,
278            );
279        }
280    }
281}
282
283#[cfg(test)]
284mod test {
285    use indoc::indoc;
286
287    use crate::{
288        state::Mode,
289        test::{NeovimBackedTestContext, VimTestContext},
290    };
291
292    #[gpui::test]
293    async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
294        let mut cx = VimTestContext::new(cx, true).await;
295        cx.simulate_keystrokes("shift-r");
296        assert_eq!(cx.mode(), Mode::Replace);
297        cx.simulate_keystrokes("escape");
298        assert_eq!(cx.mode(), Mode::Normal);
299    }
300
301    #[gpui::test]
302    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
303    async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
304        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
305
306        // test normal replace
307        cx.set_shared_state(indoc! {"
308            ˇThe quick brown
309            fox jumps over
310            the lazy dog."})
311            .await;
312        cx.simulate_shared_keystrokes("shift-r O n e").await;
313        cx.shared_state().await.assert_eq(indoc! {"
314            Oneˇ quick brown
315            fox jumps over
316            the lazy dog."});
317
318        // test replace with line ending
319        cx.set_shared_state(indoc! {"
320            The quick browˇn
321            fox jumps over
322            the lazy dog."})
323            .await;
324        cx.simulate_shared_keystrokes("shift-r O n e").await;
325        cx.shared_state().await.assert_eq(indoc! {"
326            The quick browOneˇ
327            fox jumps over
328            the lazy dog."});
329
330        // test replace with blank line
331        cx.set_shared_state(indoc! {"
332        The quick brown
333        ˇ
334        fox jumps over
335        the lazy dog."})
336            .await;
337        cx.simulate_shared_keystrokes("shift-r O n e").await;
338        cx.shared_state().await.assert_eq(indoc! {"
339            The quick brown
340            Oneˇ
341            fox jumps over
342            the lazy dog."});
343
344        // test replace with newline
345        cx.set_shared_state(indoc! {"
346            The quˇick brown
347            fox jumps over
348            the lazy dog."})
349            .await;
350        cx.simulate_shared_keystrokes("shift-r enter O n e").await;
351        cx.shared_state().await.assert_eq(indoc! {"
352            The qu
353            Oneˇ brown
354            fox jumps over
355            the lazy dog."});
356
357        // test replace with multi cursor and newline
358        cx.set_state(
359            indoc! {"
360            ˇThe quick brown
361            fox jumps over
362            the lazy ˇdog."},
363            Mode::Normal,
364        );
365        cx.simulate_keystrokes("shift-r O n e");
366        cx.assert_state(
367            indoc! {"
368            Oneˇ quick brown
369            fox jumps over
370            the lazy Oneˇ."},
371            Mode::Replace,
372        );
373        cx.simulate_keystrokes("enter T w o");
374        cx.assert_state(
375            indoc! {"
376            One
377            Twoˇck brown
378            fox jumps over
379            the lazy One
380            Twoˇ"},
381            Mode::Replace,
382        );
383    }
384
385    #[gpui::test]
386    async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
387        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
388
389        cx.set_shared_state("ˇhello\n").await;
390        cx.simulate_shared_keystrokes("3 shift-r - escape").await;
391        cx.shared_state().await.assert_eq("--ˇ-lo\n");
392
393        cx.set_shared_state("ˇhello\n").await;
394        cx.simulate_shared_keystrokes("3 shift-r a b c escape")
395            .await;
396        cx.shared_state().await.assert_eq("abcabcabˇc\n");
397    }
398
399    #[gpui::test]
400    async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
401        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
402
403        cx.set_shared_state("ˇhello world\n").await;
404        cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
405            .await;
406        cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
407    }
408
409    #[gpui::test]
410    async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
411        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
412
413        const UNDO_REPLACE_EXAMPLES: &[&str] = &[
414            // replace undo with single line
415            "ˇThe quick brown fox jumps over the lazy dog.",
416            // replace undo with ending line
417            indoc! {"
418                The quick browˇn
419                fox jumps over
420                the lazy dog."
421            },
422            // replace undo with empty line
423            indoc! {"
424                The quick brown
425                ˇ
426                fox jumps over
427                the lazy dog."
428            },
429        ];
430
431        for example in UNDO_REPLACE_EXAMPLES {
432            // normal undo
433            cx.simulate("shift-r O n e backspace backspace backspace", example)
434                .await
435                .assert_matches();
436            // undo with new line
437            cx.simulate("shift-r O enter e backspace backspace backspace", example)
438                .await
439                .assert_matches();
440            cx.simulate(
441                "shift-r O enter n enter e backspace backspace backspace backspace backspace",
442                example,
443            )
444            .await
445            .assert_matches();
446        }
447    }
448
449    #[gpui::test]
450    async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
451        let mut cx = VimTestContext::new(cx, true).await;
452        cx.set_state("ˇabcˇabcabc", Mode::Normal);
453        cx.simulate_keystrokes("shift-r 1 2 3 4");
454        cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
455        assert_eq!(cx.mode(), Mode::Replace);
456        cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
457        cx.assert_state("ˇabˇcabcabc", Mode::Replace);
458    }
459
460    #[gpui::test]
461    async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
462        let mut cx = VimTestContext::new(cx, true).await;
463
464        cx.set_state("ˇaaaa", Mode::Normal);
465        cx.simulate_keystrokes("0 shift-r b b b escape u");
466        cx.assert_state("ˇaaaa", Mode::Normal);
467    }
468
469    #[gpui::test]
470    async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
471        let mut cx = VimTestContext::new(cx, true).await;
472
473        cx.set_state("ˇhello world", Mode::Normal);
474        cx.simulate_keystrokes("c x i w w c x i w");
475        cx.assert_state("world ˇhello", Mode::Normal);
476    }
477
478    #[gpui::test]
479    async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
480        let mut cx = VimTestContext::new(cx, true).await;
481
482        cx.set_state("ˇhello world", Mode::Normal);
483        cx.simulate_keystrokes("c x x w c x i w");
484        cx.assert_state("ˇworld", Mode::Normal);
485
486        // the focus should still be at the start of the word if we reverse the
487        // order of selections (smaller -> larger)
488        cx.set_state("ˇhello world", Mode::Normal);
489        cx.simulate_keystrokes("c x i w c x x");
490        cx.assert_state("ˇhello", Mode::Normal);
491    }
492
493    #[gpui::test]
494    async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
495        let mut cx = VimTestContext::new(cx, true).await;
496
497        cx.set_state("ˇhello world", Mode::Normal);
498        cx.simulate_keystrokes("c x t r w c x i w");
499        cx.assert_state("hello ˇworld", Mode::Normal);
500    }
501
502    #[gpui::test]
503    async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) {
504        let mut cx = VimTestContext::new(cx, true).await;
505
506        cx.set_state("ˇirrelevant", Mode::Normal);
507        cx.simulate_keystrokes("c x c");
508
509        assert_eq!(cx.active_operator(), None);
510    }
511
512    #[gpui::test]
513    async fn test_clear_exchange(cx: &mut gpui::TestAppContext) {
514        let mut cx = VimTestContext::new(cx, true).await;
515
516        cx.set_state("ˇhello world", Mode::Normal);
517        cx.simulate_keystrokes("c x i w c x c");
518
519        cx.update_editor(|editor, window, cx| {
520            let highlights = editor.all_text_background_highlights(window, cx);
521            assert_eq!(0, highlights.len());
522        });
523    }
524}