replace.rs

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