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