1use editor::movement::{self, TextLayoutDetails};
2use gpui::{actions, AppContext, WindowContext};
3use language::Point;
4use workspace::Workspace;
5
6use crate::{
7 motion::{right, Motion},
8 utils::copy_selections_content,
9 Mode, Vim,
10};
11
12actions!(vim, [Substitute, SubstituteLine]);
13
14pub(crate) fn init(cx: &mut AppContext) {
15 cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
16 Vim::update(cx, |vim, cx| {
17 vim.start_recording(cx);
18 let count = vim.take_count(cx);
19 substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
20 })
21 });
22
23 cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
24 Vim::update(cx, |vim, cx| {
25 vim.start_recording(cx);
26 if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
27 vim.switch_mode(Mode::VisualLine, false, cx)
28 }
29 let count = vim.take_count(cx);
30 substitute(vim, count, true, cx)
31 })
32 });
33}
34
35pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
36 vim.update_active_editor(cx, |editor, cx| {
37 editor.set_clip_at_line_ends(false, cx);
38 editor.transact(cx, |editor, cx| {
39 let text_layout_details = TextLayoutDetails::new(editor, cx);
40 editor.change_selections(None, cx, |s| {
41 s.move_with(|map, selection| {
42 if selection.start == selection.end {
43 Motion::Right.expand_selection(
44 map,
45 selection,
46 count,
47 true,
48 &text_layout_details,
49 );
50 }
51 if line_mode {
52 // in Visual mode when the selection contains the newline at the end
53 // of the line, we should exclude it.
54 if !selection.is_empty() && selection.end.column() == 0 {
55 selection.end = movement::left(map, selection.end);
56 }
57 Motion::CurrentLine.expand_selection(
58 map,
59 selection,
60 None,
61 false,
62 &text_layout_details,
63 );
64 if let Some((point, _)) = (Motion::FirstNonWhitespace {
65 display_lines: false,
66 })
67 .move_point(
68 map,
69 selection.start,
70 selection.goal,
71 None,
72 &text_layout_details,
73 ) {
74 selection.start = point;
75 }
76 }
77 })
78 });
79 copy_selections_content(editor, line_mode, cx);
80 let selections = editor.selections.all::<Point>(cx).into_iter();
81 let edits = selections.map(|selection| (selection.start..selection.end, ""));
82 editor.edit(edits, cx);
83 });
84 });
85 vim.switch_mode(Mode::Insert, true, cx);
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.assert_shared_state("The quick ˇ").await;
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.assert_shared_state(indoc! {"
165 The ˇver
166 the lazy dog"})
167 .await;
168
169 let cases = cx.each_marked_position(indoc! {"
170 The ˇquick brown
171 fox jumps ˇover
172 the ˇlazy dog"});
173 for initial_state in cases {
174 cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
175 .await;
176 cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
177 .await;
178 }
179 }
180
181 #[gpui::test]
182 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
183 let mut cx = NeovimBackedTestContext::new(cx)
184 .await
185 .binding(["shift-v", "c"]);
186 cx.assert(indoc! {"
187 The quˇick brown
188 fox jumps over
189 the lazy dog"})
190 .await;
191 // Test pasting code copied on change
192 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
193 cx.assert_state_matches().await;
194
195 cx.assert_all(indoc! {"
196 The quick brown
197 fox juˇmps over
198 the laˇzy dog"})
199 .await;
200 let mut cx = cx.binding(["shift-v", "j", "c"]);
201 cx.assert(indoc! {"
202 The quˇick brown
203 fox jumps over
204 the lazy dog"})
205 .await;
206 // Test pasting code copied on delete
207 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
208 cx.assert_state_matches().await;
209
210 cx.assert_all(indoc! {"
211 The quick brown
212 fox juˇmps over
213 the laˇzy dog"})
214 .await;
215 }
216
217 #[gpui::test]
218 async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
219 let mut cx = NeovimBackedTestContext::new(cx).await;
220
221 let initial_state = indoc! {"
222 The quick brown
223 fox juˇmps over
224 the lazy dog
225 "};
226
227 // normal mode
228 cx.set_shared_state(initial_state).await;
229 cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
230 cx.assert_shared_state(indoc! {"
231 The quick brown
232 oˇ
233 the lazy dog
234 "})
235 .await;
236
237 // visual mode
238 cx.set_shared_state(initial_state).await;
239 cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
240 .await;
241 cx.assert_shared_state(indoc! {"
242 oˇ
243 the lazy dog
244 "})
245 .await;
246
247 // visual block mode
248 cx.set_shared_state(initial_state).await;
249 cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
250 .await;
251 cx.assert_shared_state(indoc! {"
252 The quick brown
253 oˇ
254 "})
255 .await;
256
257 // visual mode including newline
258 cx.set_shared_state(initial_state).await;
259 cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
260 .await;
261 cx.assert_shared_state(indoc! {"
262 The quick brown
263 oˇ
264 the lazy dog
265 "})
266 .await;
267
268 // indentation
269 cx.set_neovim_option("shiftwidth=4").await;
270 cx.set_shared_state(initial_state).await;
271 cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
272 .await;
273 cx.assert_shared_state(indoc! {"
274 The quick brown
275 oˇ
276 the lazy dog
277 "})
278 .await;
279 }
280}