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