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