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}