1use crate::{
  2    Operator, 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::{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
 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, 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    /// 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}