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