1use gpui::WindowContext;
2use language::Point;
3
4use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
5
6pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
7 let line_mode = vim.state().mode == Mode::VisualLine;
8 vim.update_active_editor(cx, |editor, cx| {
9 editor.set_clip_at_line_ends(false, cx);
10 editor.transact(cx, |editor, cx| {
11 editor.change_selections(None, cx, |s| {
12 s.move_with(|map, selection| {
13 if selection.start == selection.end {
14 Motion::Right.expand_selection(map, selection, count, true);
15 }
16 if line_mode {
17 Motion::CurrentLine.expand_selection(map, selection, None, false);
18 if let Some((point, _)) = (Motion::FirstNonWhitespace {
19 display_lines: false,
20 })
21 .move_point(
22 map,
23 selection.start,
24 selection.goal,
25 None,
26 ) {
27 selection.start = point;
28 }
29 }
30 })
31 });
32 copy_selections_content(editor, line_mode, cx);
33 let selections = editor.selections.all::<Point>(cx).into_iter();
34 let edits = selections.map(|selection| (selection.start..selection.end, ""));
35 editor.edit(edits, cx);
36 });
37 });
38 vim.switch_mode(Mode::Insert, true, cx);
39}
40
41#[cfg(test)]
42mod test {
43 use crate::{
44 state::Mode,
45 test::{NeovimBackedTestContext, VimTestContext},
46 };
47 use indoc::indoc;
48
49 #[gpui::test]
50 async fn test_substitute(cx: &mut gpui::TestAppContext) {
51 let mut cx = VimTestContext::new(cx, true).await;
52
53 // supports a single cursor
54 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
55 cx.simulate_keystrokes(["s", "x"]);
56 cx.assert_editor_state("xˇbc\n");
57
58 // supports a selection
59 cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
60 cx.assert_editor_state("a«bcˇ»\n");
61 cx.simulate_keystrokes(["s", "x"]);
62 cx.assert_editor_state("axˇ\n");
63
64 // supports counts
65 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
66 cx.simulate_keystrokes(["2", "s", "x"]);
67 cx.assert_editor_state("xˇc\n");
68
69 // supports multiple cursors
70 cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
71 cx.simulate_keystrokes(["2", "s", "x"]);
72 cx.assert_editor_state("axˇdexˇg\n");
73
74 // does not read beyond end of line
75 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
76 cx.simulate_keystrokes(["5", "s", "x"]);
77 cx.assert_editor_state("xˇ\n");
78
79 // it handles multibyte characters
80 cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
81 cx.simulate_keystrokes(["4", "s"]);
82 cx.assert_editor_state("ˇ\n");
83
84 // should transactionally undo selection changes
85 cx.simulate_keystrokes(["escape", "u"]);
86 cx.assert_editor_state("ˇcàfé\n");
87
88 // it handles visual line mode
89 cx.set_state(
90 indoc! {"
91 alpha
92 beˇta
93 gamma"},
94 Mode::Normal,
95 );
96 cx.simulate_keystrokes(["shift-v", "s"]);
97 cx.assert_editor_state(indoc! {"
98 alpha
99 ˇ
100 gamma"});
101 }
102
103 #[gpui::test]
104 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
105 let mut cx = NeovimBackedTestContext::new(cx).await;
106
107 cx.set_shared_state("The quick ˇbrown").await;
108 cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
109 cx.assert_shared_state("The quick ˇ").await;
110
111 cx.set_shared_state(indoc! {"
112 The ˇquick brown
113 fox jumps over
114 the lazy dog"})
115 .await;
116 cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
117 cx.assert_shared_state(indoc! {"
118 The ˇver
119 the lazy dog"})
120 .await;
121
122 let cases = cx.each_marked_position(indoc! {"
123 The ˇquick brown
124 fox jumps ˇover
125 the ˇlazy dog"});
126 for initial_state in cases {
127 cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
128 .await;
129 cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
130 .await;
131 }
132 }
133
134 #[gpui::test]
135 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
136 let mut cx = NeovimBackedTestContext::new(cx)
137 .await
138 .binding(["shift-v", "c"]);
139 cx.assert(indoc! {"
140 The quˇick brown
141 fox jumps over
142 the lazy dog"})
143 .await;
144 // Test pasting code copied on change
145 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
146 cx.assert_state_matches().await;
147
148 cx.assert_all(indoc! {"
149 The quick brown
150 fox juˇmps over
151 the laˇzy dog"})
152 .await;
153 let mut cx = cx.binding(["shift-v", "j", "c"]);
154 cx.assert(indoc! {"
155 The quˇick brown
156 fox jumps over
157 the lazy dog"})
158 .await;
159 // Test pasting code copied on delete
160 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
161 cx.assert_state_matches().await;
162
163 cx.assert_all(indoc! {"
164 The quick brown
165 fox juˇmps over
166 the laˇzy dog"})
167 .await;
168 }
169}