1use editor::{Editor, SelectionEffects, movement};
2use gpui::{Context, Window, actions};
3use language::Point;
4
5use crate::{
6 Vim,
7 motion::{Motion, MotionKind},
8 state::Mode,
9};
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);
53 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
54 s.move_with(|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 selections = editor.selections.all::<Point>(cx).into_iter();
99 let edits = selections.map(|selection| (selection.start..selection.end, ""));
100 editor.edit(edits, cx);
101 });
102 });
103 self.switch_mode(Mode::Insert, true, window, cx);
104 }
105}
106
107#[cfg(test)]
108mod test {
109 use crate::{
110 state::Mode,
111 test::{NeovimBackedTestContext, VimTestContext},
112 };
113 use indoc::indoc;
114
115 #[gpui::test]
116 async fn test_substitute(cx: &mut gpui::TestAppContext) {
117 let mut cx = VimTestContext::new(cx, true).await;
118
119 // supports a single cursor
120 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
121 cx.simulate_keystrokes("s x");
122 cx.assert_editor_state("xˇbc\n");
123
124 // supports a selection
125 cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
126 cx.assert_editor_state("a«bcˇ»\n");
127 cx.simulate_keystrokes("s x");
128 cx.assert_editor_state("axˇ\n");
129
130 // supports counts
131 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
132 cx.simulate_keystrokes("2 s x");
133 cx.assert_editor_state("xˇc\n");
134
135 // supports multiple cursors
136 cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
137 cx.simulate_keystrokes("2 s x");
138 cx.assert_editor_state("axˇdexˇg\n");
139
140 // does not read beyond end of line
141 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
142 cx.simulate_keystrokes("5 s x");
143 cx.assert_editor_state("xˇ\n");
144
145 // it handles multibyte characters
146 cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
147 cx.simulate_keystrokes("4 s");
148 cx.assert_editor_state("ˇ\n");
149
150 // should transactionally undo selection changes
151 cx.simulate_keystrokes("escape u");
152 cx.assert_editor_state("ˇcàfé\n");
153
154 // it handles visual line mode
155 cx.set_state(
156 indoc! {"
157 alpha
158 beˇta
159 gamma"},
160 Mode::Normal,
161 );
162 cx.simulate_keystrokes("shift-v s");
163 cx.assert_editor_state(indoc! {"
164 alpha
165 ˇ
166 gamma"});
167 }
168
169 #[gpui::test]
170 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
171 let mut cx = NeovimBackedTestContext::new(cx).await;
172
173 cx.set_shared_state("The quick ˇbrown").await;
174 cx.simulate_shared_keystrokes("v w c").await;
175 cx.shared_state().await.assert_eq("The quick ˇ");
176
177 cx.set_shared_state(indoc! {"
178 The ˇquick brown
179 fox jumps over
180 the lazy dog"})
181 .await;
182 cx.simulate_shared_keystrokes("v w j c").await;
183 cx.shared_state().await.assert_eq(indoc! {"
184 The ˇver
185 the lazy dog"});
186
187 cx.simulate_at_each_offset(
188 "v w j c",
189 indoc! {"
190 The ˇquick brown
191 fox jumps ˇover
192 the ˇlazy dog"},
193 )
194 .await
195 .assert_matches();
196 cx.simulate_at_each_offset(
197 "v w k c",
198 indoc! {"
199 The ˇquick brown
200 fox jumps ˇover
201 the ˇlazy dog"},
202 )
203 .await
204 .assert_matches();
205 }
206
207 #[gpui::test]
208 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
209 let mut cx = NeovimBackedTestContext::new(cx).await;
210 cx.simulate(
211 "shift-v c",
212 indoc! {"
213 The quˇick brown
214 fox jumps over
215 the lazy dog"},
216 )
217 .await
218 .assert_matches();
219 // Test pasting code copied on change
220 cx.simulate_shared_keystrokes("escape j p").await;
221 cx.shared_state().await.assert_matches();
222
223 cx.simulate_at_each_offset(
224 "shift-v c",
225 indoc! {"
226 The quick brown
227 fox juˇmps over
228 the laˇzy dog"},
229 )
230 .await
231 .assert_matches();
232 cx.simulate(
233 "shift-v j c",
234 indoc! {"
235 The quˇick brown
236 fox jumps over
237 the lazy dog"},
238 )
239 .await
240 .assert_matches();
241 // Test pasting code copied on delete
242 cx.simulate_shared_keystrokes("escape j p").await;
243 cx.shared_state().await.assert_matches();
244
245 cx.simulate_at_each_offset(
246 "shift-v j c",
247 indoc! {"
248 The quick brown
249 fox juˇmps over
250 the laˇzy dog"},
251 )
252 .await
253 .assert_matches();
254 }
255
256 #[gpui::test]
257 async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
258 let mut cx = NeovimBackedTestContext::new(cx).await;
259
260 let initial_state = indoc! {"
261 The quick brown
262 fox juˇmps over
263 the lazy dog
264 "};
265
266 // normal mode
267 cx.set_shared_state(initial_state).await;
268 cx.simulate_shared_keystrokes("shift-s o").await;
269 cx.shared_state().await.assert_eq(indoc! {"
270 The quick brown
271 oˇ
272 the lazy dog
273 "});
274
275 // visual mode
276 cx.set_shared_state(initial_state).await;
277 cx.simulate_shared_keystrokes("v k shift-s o").await;
278 cx.shared_state().await.assert_eq(indoc! {"
279 oˇ
280 the lazy dog
281 "});
282
283 // visual block mode
284 cx.set_shared_state(initial_state).await;
285 cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
286 cx.shared_state().await.assert_eq(indoc! {"
287 The quick brown
288 oˇ
289 "});
290
291 // visual mode including newline
292 cx.set_shared_state(initial_state).await;
293 cx.simulate_shared_keystrokes("v $ shift-s o").await;
294 cx.shared_state().await.assert_eq(indoc! {"
295 The quick brown
296 oˇ
297 the lazy dog
298 "});
299
300 // indentation
301 cx.set_neovim_option("shiftwidth=4").await;
302 cx.set_shared_state(initial_state).await;
303 cx.simulate_shared_keystrokes("> > shift-s o").await;
304 cx.shared_state().await.assert_eq(indoc! {"
305 The quick brown
306 oˇ
307 the lazy dog
308 "});
309 }
310}