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::{AutoindentMode, 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.buffer().update(cx, |buffer, cx| {
 62                    buffer.edit(
 63                        edits.clone(),
 64                        Some(AutoindentMode::Block {
 65                            original_indent_columns: Vec::new(),
 66                        }),
 67                        cx,
 68                    );
 69                });
 70
 71                editor.change_selections(None, cx, |s| {
 72                    s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
 73                });
 74                editor.set_clip_at_line_ends(true, cx);
 75            });
 76        });
 77    }
 78
 79    fn undo_replace(&mut self, maybe_times: Option<usize>, cx: &mut ViewContext<Self>) {
 80        self.update_editor(cx, |vim, editor, cx| {
 81            editor.transact(cx, |editor, cx| {
 82                editor.set_clip_at_line_ends(false, cx);
 83                let map = editor.snapshot(cx);
 84                let selections = editor.selections.all::<Point>(cx);
 85                let mut new_selections = vec![];
 86                let edits: Vec<(Range<Point>, String)> = selections
 87                    .into_iter()
 88                    .filter_map(|selection| {
 89                        let end = selection.head();
 90                        let start = motion::backspace(
 91                            &map,
 92                            end.to_display_point(&map),
 93                            maybe_times.unwrap_or(1),
 94                        )
 95                        .to_point(&map);
 96                        new_selections.push(
 97                            map.buffer_snapshot.anchor_before(start)
 98                                ..map.buffer_snapshot.anchor_before(start),
 99                        );
100
101                        let mut undo = None;
102                        let edit_range = start..end;
103                        for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
104                            if range.start.to_point(&map.buffer_snapshot) <= edit_range.start
105                                && range.end.to_point(&map.buffer_snapshot) >= edit_range.end
106                            {
107                                undo = Some(inverse.clone());
108                                vim.replacements.remove(vim.replacements.len() - i - 1);
109                                break;
110                            }
111                        }
112                        Some((edit_range, undo?))
113                    })
114                    .collect::<Vec<_>>();
115
116                editor.buffer().update(cx, |buffer, cx| {
117                    buffer.edit(edits, None, cx);
118                });
119
120                editor.change_selections(None, cx, |s| {
121                    s.select_ranges(new_selections);
122                });
123                editor.set_clip_at_line_ends(true, cx);
124            });
125        });
126    }
127}
128
129#[cfg(test)]
130mod test {
131    use indoc::indoc;
132
133    use crate::{
134        state::Mode,
135        test::{NeovimBackedTestContext, VimTestContext},
136    };
137
138    #[gpui::test]
139    async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
140        let mut cx = VimTestContext::new(cx, true).await;
141        cx.simulate_keystrokes("shift-r");
142        assert_eq!(cx.mode(), Mode::Replace);
143        cx.simulate_keystrokes("escape");
144        assert_eq!(cx.mode(), Mode::Normal);
145    }
146
147    #[gpui::test]
148    async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
149        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
150
151        // test normal replace
152        cx.set_shared_state(indoc! {"
153            ˇThe quick brown
154            fox jumps over
155            the lazy dog."})
156            .await;
157        cx.simulate_shared_keystrokes("shift-r O n e").await;
158        cx.shared_state().await.assert_eq(indoc! {"
159            Oneˇ quick brown
160            fox jumps over
161            the lazy dog."});
162
163        // test replace with line ending
164        cx.set_shared_state(indoc! {"
165            The quick browˇn
166            fox jumps over
167            the lazy dog."})
168            .await;
169        cx.simulate_shared_keystrokes("shift-r O n e").await;
170        cx.shared_state().await.assert_eq(indoc! {"
171            The quick browOneˇ
172            fox jumps over
173            the lazy dog."});
174
175        // test replace with blank line
176        cx.set_shared_state(indoc! {"
177        The quick brown
178        ˇ
179        fox jumps over
180        the lazy dog."})
181            .await;
182        cx.simulate_shared_keystrokes("shift-r O n e").await;
183        cx.shared_state().await.assert_eq(indoc! {"
184            The quick brown
185            Oneˇ
186            fox jumps over
187            the lazy dog."});
188
189        // test replace with newline
190        cx.set_shared_state(indoc! {"
191            The quˇick brown
192            fox jumps over
193            the lazy dog."})
194            .await;
195        cx.simulate_shared_keystrokes("shift-r enter O n e").await;
196        cx.shared_state().await.assert_eq(indoc! {"
197            The qu
198            Oneˇ brown
199            fox jumps over
200            the lazy dog."});
201
202        // test replace with multi cursor and newline
203        cx.set_state(
204            indoc! {"
205            ˇThe quick brown
206            fox jumps over
207            the lazy ˇdog."},
208            Mode::Normal,
209        );
210        cx.simulate_keystrokes("shift-r O n e");
211        cx.assert_state(
212            indoc! {"
213            Oneˇ quick brown
214            fox jumps over
215            the lazy Oneˇ."},
216            Mode::Replace,
217        );
218        cx.simulate_keystrokes("enter T w o");
219        cx.assert_state(
220            indoc! {"
221            One
222            Twoˇck brown
223            fox jumps over
224            the lazy One
225            Twoˇ"},
226            Mode::Replace,
227        );
228    }
229
230    #[gpui::test]
231    async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
232        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
233
234        cx.set_shared_state("ˇhello\n").await;
235        cx.simulate_shared_keystrokes("3 shift-r - escape").await;
236        cx.shared_state().await.assert_eq("--ˇ-lo\n");
237
238        cx.set_shared_state("ˇhello\n").await;
239        cx.simulate_shared_keystrokes("3 shift-r a b c escape")
240            .await;
241        cx.shared_state().await.assert_eq("abcabcabˇc\n");
242    }
243
244    #[gpui::test]
245    async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
246        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
247
248        cx.set_shared_state("ˇhello world\n").await;
249        cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
250            .await;
251        cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
252    }
253
254    #[gpui::test]
255    async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
256        let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
257
258        const UNDO_REPLACE_EXAMPLES: &[&'static str] = &[
259            // replace undo with single line
260            "ˇThe quick brown fox jumps over the lazy dog.",
261            // replace undo with ending line
262            indoc! {"
263                The quick browˇn
264                fox jumps over
265                the lazy dog."
266            },
267            // replace undo with empty line
268            indoc! {"
269                The quick brown
270                ˇ
271                fox jumps over
272                the lazy dog."
273            },
274        ];
275
276        for example in UNDO_REPLACE_EXAMPLES {
277            // normal undo
278            cx.simulate("shift-r O n e backspace backspace backspace", example)
279                .await
280                .assert_matches();
281            // undo with new line
282            cx.simulate("shift-r O enter e backspace backspace backspace", example)
283                .await
284                .assert_matches();
285            cx.simulate(
286                "shift-r O enter n enter e backspace backspace backspace backspace backspace",
287                example,
288            )
289            .await
290            .assert_matches();
291        }
292    }
293
294    #[gpui::test]
295    async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
296        let mut cx = VimTestContext::new(cx, true).await;
297        cx.set_state("ˇabcˇabcabc", Mode::Normal);
298        cx.simulate_keystrokes("shift-r 1 2 3 4");
299        cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
300        assert_eq!(cx.mode(), Mode::Replace);
301        cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
302        cx.assert_state("ˇabˇcabcabc", Mode::Replace);
303    }
304
305    #[gpui::test]
306    async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
307        let mut cx = VimTestContext::new(cx, true).await;
308
309        cx.set_state("ˇaaaa", Mode::Normal);
310        cx.simulate_keystrokes("0 shift-r b b b escape u");
311        cx.assert_state("ˇaaaa", Mode::Normal);
312    }
313}