replace.rs

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