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