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}