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