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