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