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