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}