replace.rs

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