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, |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(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)]
85// mod 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.assert_shared_state("The quick ˇ").await;
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.assert_shared_state(indoc! {"
161// The ˇver
162// the lazy dog"})
163// .await;
164
165// let cases = cx.each_marked_position(indoc! {"
166// The ˇquick brown
167// fox jumps ˇover
168// the ˇlazy dog"});
169// for initial_state in cases {
170// cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
171// .await;
172// cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
173// .await;
174// }
175// }
176
177// #[gpui::test]
178// async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
179// let mut cx = NeovimBackedTestContext::new(cx)
180// .await
181// .binding(["shift-v", "c"]);
182// cx.assert(indoc! {"
183// The quˇick brown
184// fox jumps over
185// the lazy dog"})
186// .await;
187// // Test pasting code copied on change
188// cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
189// cx.assert_state_matches().await;
190
191// cx.assert_all(indoc! {"
192// The quick brown
193// fox juˇmps over
194// the laˇzy dog"})
195// .await;
196// let mut cx = cx.binding(["shift-v", "j", "c"]);
197// cx.assert(indoc! {"
198// The quˇick brown
199// fox jumps over
200// the lazy dog"})
201// .await;
202// // Test pasting code copied on delete
203// cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
204// cx.assert_state_matches().await;
205
206// cx.assert_all(indoc! {"
207// The quick brown
208// fox juˇmps over
209// the laˇzy dog"})
210// .await;
211// }
212
213// #[gpui::test]
214// async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
215// let mut cx = NeovimBackedTestContext::new(cx).await;
216
217// let initial_state = indoc! {"
218// The quick brown
219// fox juˇmps over
220// the lazy dog
221// "};
222
223// // normal mode
224// cx.set_shared_state(initial_state).await;
225// cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
226// cx.assert_shared_state(indoc! {"
227// The quick brown
228// oˇ
229// the lazy dog
230// "})
231// .await;
232
233// // visual mode
234// cx.set_shared_state(initial_state).await;
235// cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
236// .await;
237// cx.assert_shared_state(indoc! {"
238// oˇ
239// the lazy dog
240// "})
241// .await;
242
243// // visual block mode
244// cx.set_shared_state(initial_state).await;
245// cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
246// .await;
247// cx.assert_shared_state(indoc! {"
248// The quick brown
249// oˇ
250// "})
251// .await;
252
253// // visual mode including newline
254// cx.set_shared_state(initial_state).await;
255// cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
256// .await;
257// cx.assert_shared_state(indoc! {"
258// The quick brown
259// oˇ
260// the lazy dog
261// "})
262// .await;
263
264// // indentation
265// cx.set_neovim_option("shiftwidth=4").await;
266// cx.set_shared_state(initial_state).await;
267// cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
268// .await;
269// cx.assert_shared_state(indoc! {"
270// The quick brown
271// oˇ
272// the lazy dog
273// "})
274// .await;
275// }
276// }