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