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}