1use std::sync::Arc;
2
3use crate::{
4 Mode, Vim,
5 motion::{Motion, MotionKind},
6};
7use editor::{Editor, SelectionEffects, movement};
8use gpui::{Context, Window, actions};
9use language::Point;
10
11actions!(
12 vim,
13 [
14 /// Substitutes characters in the current selection.
15 Substitute,
16 /// Substitutes the entire line.
17 SubstituteLine
18 ]
19);
20
21pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
22 Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
23 vim.start_recording(cx);
24 let count = Vim::take_count(cx);
25 Vim::take_forced_motion(cx);
26 vim.substitute(count, vim.mode == Mode::VisualLine, window, cx);
27 });
28
29 Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| {
30 vim.start_recording(cx);
31 if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
32 vim.switch_mode(Mode::VisualLine, false, window, cx)
33 }
34 let count = Vim::take_count(cx);
35 Vim::take_forced_motion(cx);
36 vim.substitute(count, true, window, cx)
37 });
38}
39
40impl Vim {
41 pub fn substitute(
42 &mut self,
43 count: Option<usize>,
44 line_mode: bool,
45 window: &mut Window,
46 cx: &mut Context<Self>,
47 ) {
48 self.store_visual_marks(window, cx);
49 self.update_editor(cx, |vim, editor, cx| {
50 editor.set_clip_at_line_ends(false, cx);
51 editor.transact(window, cx, |editor, window, cx| {
52 let text_layout_details = editor.text_layout_details(window, cx);
53 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
54 s.move_with(&mut |map, selection| {
55 if selection.start == selection.end {
56 Motion::Right.expand_selection(
57 map,
58 selection,
59 count,
60 &text_layout_details,
61 false,
62 );
63 }
64 if line_mode {
65 // in Visual mode when the selection contains the newline at the end
66 // of the line, we should exclude it.
67 if !selection.is_empty() && selection.end.column() == 0 {
68 selection.end = movement::left(map, selection.end);
69 }
70 Motion::CurrentLine.expand_selection(
71 map,
72 selection,
73 None,
74 &text_layout_details,
75 false,
76 );
77 if let Some((point, _)) = (Motion::FirstNonWhitespace {
78 display_lines: false,
79 })
80 .move_point(
81 map,
82 selection.start,
83 selection.goal,
84 None,
85 &text_layout_details,
86 ) {
87 selection.start = point;
88 }
89 }
90 })
91 });
92 let kind = if line_mode {
93 MotionKind::Linewise
94 } else {
95 MotionKind::Exclusive
96 };
97 vim.copy_selections_content(editor, kind, window, cx);
98 let linked_edits = editor.linked_edits_for_selections(Arc::from(""), cx);
99 let selections = editor
100 .selections
101 .all::<Point>(&editor.display_snapshot(cx))
102 .into_iter();
103 let edits = selections.map(|selection| (selection.start..selection.end, ""));
104 editor.edit(edits, cx);
105 linked_edits.apply(cx);
106 });
107 });
108 self.switch_mode(Mode::Insert, true, window, cx);
109 }
110}
111
112#[cfg(test)]
113mod test {
114 use crate::{
115 state::Mode,
116 test::{NeovimBackedTestContext, VimTestContext},
117 };
118 use indoc::indoc;
119
120 #[gpui::test]
121 async fn test_substitute(cx: &mut gpui::TestAppContext) {
122 let mut cx = VimTestContext::new(cx, true).await;
123
124 // supports a single cursor
125 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
126 cx.simulate_keystrokes("s x");
127 cx.assert_editor_state("xˇbc\n");
128
129 // supports a selection
130 cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
131 cx.assert_editor_state("a«bcˇ»\n");
132 cx.simulate_keystrokes("s x");
133 cx.assert_editor_state("axˇ\n");
134
135 // supports counts
136 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
137 cx.simulate_keystrokes("2 s x");
138 cx.assert_editor_state("xˇc\n");
139
140 // supports multiple cursors
141 cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
142 cx.simulate_keystrokes("2 s x");
143 cx.assert_editor_state("axˇdexˇg\n");
144
145 // does not read beyond end of line
146 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
147 cx.simulate_keystrokes("5 s x");
148 cx.assert_editor_state("xˇ\n");
149
150 // it handles multibyte characters
151 cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
152 cx.simulate_keystrokes("4 s");
153 cx.assert_editor_state("ˇ\n");
154
155 // should transactionally undo selection changes
156 cx.simulate_keystrokes("escape u");
157 cx.assert_editor_state("ˇcàfé\n");
158
159 // it handles visual line mode
160 cx.set_state(
161 indoc! {"
162 alpha
163 beˇta
164 gamma"},
165 Mode::Normal,
166 );
167 cx.simulate_keystrokes("shift-v s");
168 cx.assert_editor_state(indoc! {"
169 alpha
170 ˇ
171 gamma"});
172 }
173
174 #[gpui::test]
175 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
176 let mut cx = NeovimBackedTestContext::new(cx).await;
177
178 cx.set_shared_state("The quick ˇbrown").await;
179 cx.simulate_shared_keystrokes("v w c").await;
180 cx.shared_state().await.assert_eq("The quick ˇ");
181
182 cx.set_shared_state(indoc! {"
183 The ˇquick brown
184 fox jumps over
185 the lazy dog"})
186 .await;
187 cx.simulate_shared_keystrokes("v w j c").await;
188 cx.shared_state().await.assert_eq(indoc! {"
189 The ˇver
190 the lazy dog"});
191
192 cx.simulate_at_each_offset(
193 "v w j c",
194 indoc! {"
195 The ˇquick brown
196 fox jumps ˇover
197 the ˇlazy dog"},
198 )
199 .await
200 .assert_matches();
201 cx.simulate_at_each_offset(
202 "v w k c",
203 indoc! {"
204 The ˇquick brown
205 fox jumps ˇover
206 the ˇlazy dog"},
207 )
208 .await
209 .assert_matches();
210 }
211
212 #[gpui::test]
213 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
214 let mut cx = NeovimBackedTestContext::new(cx).await;
215 cx.simulate(
216 "shift-v c",
217 indoc! {"
218 The quˇick brown
219 fox jumps over
220 the lazy dog"},
221 )
222 .await
223 .assert_matches();
224 // Test pasting code copied on change
225 cx.simulate_shared_keystrokes("escape j p").await;
226 cx.shared_state().await.assert_matches();
227
228 cx.simulate_at_each_offset(
229 "shift-v c",
230 indoc! {"
231 The quick brown
232 fox juˇmps over
233 the laˇzy dog"},
234 )
235 .await
236 .assert_matches();
237 cx.simulate(
238 "shift-v j c",
239 indoc! {"
240 The quˇick brown
241 fox jumps over
242 the lazy dog"},
243 )
244 .await
245 .assert_matches();
246 // Test pasting code copied on delete
247 cx.simulate_shared_keystrokes("escape j p").await;
248 cx.shared_state().await.assert_matches();
249
250 cx.simulate_at_each_offset(
251 "shift-v j c",
252 indoc! {"
253 The quick brown
254 fox juˇmps over
255 the laˇzy dog"},
256 )
257 .await
258 .assert_matches();
259 }
260
261 #[gpui::test]
262 async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
263 let mut cx = NeovimBackedTestContext::new(cx).await;
264
265 let initial_state = indoc! {"
266 The quick brown
267 fox juˇmps over
268 the lazy dog
269 "};
270
271 // normal mode
272 cx.set_shared_state(initial_state).await;
273 cx.simulate_shared_keystrokes("shift-s o").await;
274 cx.shared_state().await.assert_eq(indoc! {"
275 The quick brown
276 oˇ
277 the lazy dog
278 "});
279
280 // visual mode
281 cx.set_shared_state(initial_state).await;
282 cx.simulate_shared_keystrokes("v k shift-s o").await;
283 cx.shared_state().await.assert_eq(indoc! {"
284 oˇ
285 the lazy dog
286 "});
287
288 // visual block mode
289 cx.set_shared_state(initial_state).await;
290 cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
291 cx.shared_state().await.assert_eq(indoc! {"
292 The quick brown
293 oˇ
294 "});
295
296 // visual mode including newline
297 cx.set_shared_state(initial_state).await;
298 cx.simulate_shared_keystrokes("v $ shift-s o").await;
299 cx.shared_state().await.assert_eq(indoc! {"
300 The quick brown
301 oˇ
302 the lazy dog
303 "});
304
305 // indentation
306 cx.set_neovim_option("shiftwidth=4").await;
307 cx.set_shared_state(initial_state).await;
308 cx.simulate_shared_keystrokes("> > shift-s o").await;
309 cx.shared_state().await.assert_eq(indoc! {"
310 The quick brown
311 oˇ
312 the lazy dog
313 "});
314 }
315}