replace.rs

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