replace.rs

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