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