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