replace.rs

  1use crate::{
  2    motion::{self},
  3    state::Mode,
  4    Vim,
  5};
  6use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint};
  7use gpui::{actions, ViewContext};
  8use language::Point;
  9use std::ops::Range;
 10use std::sync::Arc;
 11
 12actions!(vim, [ToggleReplace, UndoReplace]);
 13
 14pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 15    Vim::action(editor, cx, |vim, _: &ToggleReplace, cx| {
 16        vim.replacements = vec![];
 17        vim.start_recording(cx);
 18        vim.switch_mode(Mode::Replace, false, cx);
 19    });
 20
 21    Vim::action(editor, cx, |vim, _: &UndoReplace, cx| {
 22        if vim.mode != Mode::Replace {
 23            return;
 24        }
 25        let count = Vim::take_count(cx);
 26        vim.undo_replace(count, cx)
 27    });
 28}
 29
 30impl Vim {
 31    pub(crate) fn multi_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
 32        self.update_editor(cx, |vim, editor, cx| {
 33            editor.transact(cx, |editor, cx| {
 34                editor.set_clip_at_line_ends(false, cx);
 35                let map = editor.snapshot(cx);
 36                let display_selections = editor.selections.all::<Point>(cx);
 37
 38                // Handles all string that require manipulation, including inserts and replaces
 39                let edits = display_selections
 40                    .into_iter()
 41                    .map(|selection| {
 42                        let is_new_line = text.as_ref() == "\n";
 43                        let mut range = selection.range();
 44                        // "\n" need to be handled separately, because when a "\n" is typing,
 45                        // we don't do a replace, we need insert a "\n"
 46                        if !is_new_line {
 47                            range.end.column += 1;
 48                            range.end = map.buffer_snapshot.clip_point(range.end, Bias::Right);
 49                        }
 50                        let replace_range = map.buffer_snapshot.anchor_before(range.start)
 51                            ..map.buffer_snapshot.anchor_after(range.end);
 52                        let current_text = map
 53                            .buffer_snapshot
 54                            .text_for_range(replace_range.clone())
 55                            .collect();
 56                        vim.replacements.push((replace_range.clone(), current_text));
 57                        (replace_range, text.clone())
 58                    })
 59                    .collect::<Vec<_>>();
 60
 61                editor.edit_with_block_indent(edits.clone(), Vec::new(), cx);
 62
 63                editor.change_selections(None, cx, |s| {
 64                    s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
 65                });
 66                editor.set_clip_at_line_ends(true, cx);
 67            });
 68        });
 69    }
 70
 71    fn undo_replace(&mut self, maybe_times: Option<usize>, cx: &mut ViewContext<Self>) {
 72        self.update_editor(cx, |vim, editor, cx| {
 73            editor.transact(cx, |editor, cx| {
 74                editor.set_clip_at_line_ends(false, cx);
 75                let map = editor.snapshot(cx);
 76                let selections = editor.selections.all::<Point>(cx);
 77                let mut new_selections = vec![];
 78                let edits: Vec<(Range<Point>, String)> = selections
 79                    .into_iter()
 80                    .filter_map(|selection| {
 81                        let end = selection.head();
 82                        let start = motion::backspace(
 83                            &map,
 84                            end.to_display_point(&map),
 85                            maybe_times.unwrap_or(1),
 86                        )
 87                        .to_point(&map);
 88                        new_selections.push(
 89                            map.buffer_snapshot.anchor_before(start)
 90                                ..map.buffer_snapshot.anchor_before(start),
 91                        );
 92
 93                        let mut undo = None;
 94                        let edit_range = start..end;
 95                        for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
 96                            if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
 97                                && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
 98                            {
 99                                undo = Some(inverse.clone());
100                                vim.replacements.remove(vim.replacements.len() - i - 1);
101                                break;
102                            }
103                        }
104                        Some((edit_range, undo?))
105                    })
106                    .collect::<Vec<_>>();
107
108                editor.edit(edits, cx);
109
110                editor.change_selections(None, cx, |s| {
111                    s.select_ranges(new_selections);
112                });
113                editor.set_clip_at_line_ends(true, cx);
114            });
115        });
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use indoc::indoc;
122
123    use crate::{
124        state::Mode,
125        test::{NeovimBackedTestContext, VimTestContext},
126    };
127
128    #[gpui::test]
129    async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
130        let mut cx = VimTestContext::new(cx, true).await;
131        cx.simulate_keystrokes("shift-r");
132        assert_eq!(cx.mode(), Mode::Replace);
133        cx.simulate_keystrokes("escape");
134        assert_eq!(cx.mode(), Mode::Normal);
135    }
136
137    #[gpui::test]
138    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
139    async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
140        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
141
142        // test normal replace
143        cx.set_shared_state(indoc! {"
144            ˇThe quick brown
145            fox jumps over
146            the lazy dog."})
147            .await;
148        cx.simulate_shared_keystrokes("shift-r O n e").await;
149        cx.shared_state().await.assert_eq(indoc! {"
150            Oneˇ quick brown
151            fox jumps over
152            the lazy dog."});
153
154        // test replace with line ending
155        cx.set_shared_state(indoc! {"
156            The quick browˇn
157            fox jumps over
158            the lazy dog."})
159            .await;
160        cx.simulate_shared_keystrokes("shift-r O n e").await;
161        cx.shared_state().await.assert_eq(indoc! {"
162            The quick browOneˇ
163            fox jumps over
164            the lazy dog."});
165
166        // test replace with blank line
167        cx.set_shared_state(indoc! {"
168        The quick brown
169        ˇ
170        fox jumps over
171        the lazy dog."})
172            .await;
173        cx.simulate_shared_keystrokes("shift-r O n e").await;
174        cx.shared_state().await.assert_eq(indoc! {"
175            The quick brown
176            Oneˇ
177            fox jumps over
178            the lazy dog."});
179
180        // test replace with newline
181        cx.set_shared_state(indoc! {"
182            The quˇick brown
183            fox jumps over
184            the lazy dog."})
185            .await;
186        cx.simulate_shared_keystrokes("shift-r enter O n e").await;
187        cx.shared_state().await.assert_eq(indoc! {"
188            The qu
189            Oneˇ brown
190            fox jumps over
191            the lazy dog."});
192
193        // test replace with multi cursor and newline
194        cx.set_state(
195            indoc! {"
196            ˇThe quick brown
197            fox jumps over
198            the lazy ˇdog."},
199            Mode::Normal,
200        );
201        cx.simulate_keystrokes("shift-r O n e");
202        cx.assert_state(
203            indoc! {"
204            Oneˇ quick brown
205            fox jumps over
206            the lazy Oneˇ."},
207            Mode::Replace,
208        );
209        cx.simulate_keystrokes("enter T w o");
210        cx.assert_state(
211            indoc! {"
212            One
213            Twoˇck brown
214            fox jumps over
215            the lazy One
216            Twoˇ"},
217            Mode::Replace,
218        );
219    }
220
221    #[gpui::test]
222    async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
223        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
224
225        cx.set_shared_state("ˇhello\n").await;
226        cx.simulate_shared_keystrokes("3 shift-r - escape").await;
227        cx.shared_state().await.assert_eq("--ˇ-lo\n");
228
229        cx.set_shared_state("ˇhello\n").await;
230        cx.simulate_shared_keystrokes("3 shift-r a b c escape")
231            .await;
232        cx.shared_state().await.assert_eq("abcabcabˇc\n");
233    }
234
235    #[gpui::test]
236    async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
237        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
238
239        cx.set_shared_state("ˇhello world\n").await;
240        cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
241            .await;
242        cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
243    }
244
245    #[gpui::test]
246    async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
247        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
248
249        const UNDO_REPLACE_EXAMPLES: &[&str] = &[
250            // replace undo with single line
251            "ˇThe quick brown fox jumps over the lazy dog.",
252            // replace undo with ending line
253            indoc! {"
254                The quick browˇn
255                fox jumps over
256                the lazy dog."
257            },
258            // replace undo with empty line
259            indoc! {"
260                The quick brown
261                ˇ
262                fox jumps over
263                the lazy dog."
264            },
265        ];
266
267        for example in UNDO_REPLACE_EXAMPLES {
268            // normal undo
269            cx.simulate("shift-r O n e backspace backspace backspace", example)
270                .await
271                .assert_matches();
272            // undo with new line
273            cx.simulate("shift-r O enter e backspace backspace backspace", example)
274                .await
275                .assert_matches();
276            cx.simulate(
277                "shift-r O enter n enter e backspace backspace backspace backspace backspace",
278                example,
279            )
280            .await
281            .assert_matches();
282        }
283    }
284
285    #[gpui::test]
286    async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
287        let mut cx = VimTestContext::new(cx, true).await;
288        cx.set_state("ˇabcˇabcabc", Mode::Normal);
289        cx.simulate_keystrokes("shift-r 1 2 3 4");
290        cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
291        assert_eq!(cx.mode(), Mode::Replace);
292        cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
293        cx.assert_state("ˇabˇcabcabc", Mode::Replace);
294    }
295
296    #[gpui::test]
297    async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
298        let mut cx = VimTestContext::new(cx, true).await;
299
300        cx.set_state("ˇaaaa", Mode::Normal);
301        cx.simulate_keystrokes("0 shift-r b b b escape u");
302        cx.assert_state("ˇaaaa", Mode::Normal);
303    }
304}