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