1use std::ops::Range;
2
3use editor::{DisplayPoint, display_map::DisplaySnapshot};
4use gpui::Context;
5use text::Bias;
6use ui::Window;
7
8use crate::Vim;
9
10impl Vim {
11 /// Creates a duplicate of every selection below it in the first place that has both its start
12 /// and end
13 pub(super) fn helix_duplicate_selections_below(
14 &mut self,
15 times: Option<usize>,
16 window: &mut Window,
17 cx: &mut Context<Self>,
18 ) {
19 self.duplicate_selections(
20 times,
21 window,
22 cx,
23 |prev_point| *prev_point.row_mut() += 1,
24 |prev_range, map| prev_range.end.row() >= map.max_point().row(),
25 false,
26 );
27 }
28
29 /// Creates a duplicate of every selection above it in the first place that has both its start
30 /// and end
31 pub(super) fn helix_duplicate_selections_above(
32 &mut self,
33 times: Option<usize>,
34 window: &mut Window,
35 cx: &mut Context<Self>,
36 ) {
37 self.duplicate_selections(
38 times,
39 window,
40 cx,
41 |prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1),
42 |prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(),
43 true,
44 );
45 }
46
47 fn duplicate_selections(
48 &mut self,
49 times: Option<usize>,
50 window: &mut Window,
51 cx: &mut Context<Self>,
52 advance_search: impl Fn(&mut DisplayPoint),
53 end_search: impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
54 above: bool,
55 ) {
56 let times = times.unwrap_or(1);
57 self.update_editor(cx, |_, editor, cx| {
58 let mut selections = Vec::new();
59 let map = editor.display_snapshot(cx);
60 let mut original_selections = editor.selections.all_display(&map);
61 // The order matters, because it is recorded when the selections are added.
62 if above {
63 original_selections.reverse();
64 }
65
66 for origin in original_selections {
67 let origin = origin.tail()..origin.head();
68 selections.push(display_point_range_to_offset_range(&origin, &map));
69 let mut last_origin = origin;
70 for _ in 1..=times {
71 if let Some(duplicate) = find_next_valid_duplicate_space(
72 last_origin.clone(),
73 &map,
74 &advance_search,
75 &end_search,
76 ) {
77 selections.push(display_point_range_to_offset_range(&duplicate, &map));
78 last_origin = duplicate;
79 } else {
80 break;
81 }
82 }
83 }
84
85 editor.change_selections(Default::default(), window, cx, |s| {
86 s.select_ranges(selections);
87 });
88 });
89 }
90}
91
92fn find_next_valid_duplicate_space(
93 mut origin: Range<DisplayPoint>,
94 map: &DisplaySnapshot,
95 advance_search: &impl Fn(&mut DisplayPoint),
96 end_search: &impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
97) -> Option<Range<DisplayPoint>> {
98 while !end_search(&origin, map) {
99 advance_search(&mut origin.start);
100 advance_search(&mut origin.end);
101
102 if map.clip_point(origin.start, Bias::Left) == origin.start
103 && map.clip_point(origin.end, Bias::Right) == origin.end
104 {
105 return Some(origin);
106 }
107 }
108 None
109}
110
111fn display_point_range_to_offset_range(
112 range: &Range<DisplayPoint>,
113 map: &DisplaySnapshot,
114) -> Range<usize> {
115 range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
116}
117
118#[cfg(test)]
119mod tests {
120 use db::indoc;
121
122 use crate::{state::Mode, test::VimTestContext};
123
124 #[gpui::test]
125 async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
126 let mut cx = VimTestContext::new(cx, true).await;
127 cx.enable_helix();
128
129 cx.set_state(
130 indoc! {"
131 The quick brown
132 fox «jumpsˇ»
133 over the
134 lazy dog."},
135 Mode::HelixNormal,
136 );
137
138 cx.simulate_keystrokes("C");
139
140 cx.assert_state(
141 indoc! {"
142 The quick brown
143 fox «jumpsˇ»
144 over the
145 lazy« dog.ˇ»"},
146 Mode::HelixNormal,
147 );
148
149 cx.simulate_keystrokes("C");
150
151 cx.assert_state(
152 indoc! {"
153 The quick brown
154 fox «jumpsˇ»
155 over the
156 lazy« dog.ˇ»"},
157 Mode::HelixNormal,
158 );
159
160 cx.simulate_keystrokes("alt-C");
161
162 cx.assert_state(
163 indoc! {"
164 The «quickˇ» brown
165 fox «jumpsˇ»
166 over the
167 lazy« dog.ˇ»"},
168 Mode::HelixNormal,
169 );
170
171 cx.simulate_keystrokes(",");
172
173 cx.assert_state(
174 indoc! {"
175 The «quickˇ» brown
176 fox jumps
177 over the
178 lazy dog."},
179 Mode::HelixNormal,
180 );
181 }
182
183 #[gpui::test]
184 async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
185 let mut cx = VimTestContext::new(cx, true).await;
186 cx.enable_helix();
187
188 cx.set_state(
189 indoc! {"
190 The quick brown
191 «ˇfox» jumps
192 over the
193 lazy dog."},
194 Mode::HelixNormal,
195 );
196
197 cx.simulate_keystrokes("C C alt-C");
198
199 cx.assert_state(
200 indoc! {"
201 «ˇThe» quick brown
202 «ˇfox» jumps
203 «ˇove»r the
204 «ˇlaz»y dog."},
205 Mode::HelixNormal,
206 );
207 }
208
209 #[gpui::test]
210 async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
211 let mut cx = VimTestContext::new(cx, true).await;
212 cx.enable_helix();
213
214 cx.set_state(
215 indoc! {"
216 The «qˇ»uick brown
217 fox jumps
218 over the
219 lazy dog."},
220 Mode::HelixNormal,
221 );
222
223 cx.simulate_keystrokes("9 C");
224
225 cx.assert_state(
226 indoc! {"
227 The «qˇ»uick brown
228 fox «jˇ»umps
229 over« ˇ»the
230 lazy« ˇ»dog."},
231 Mode::HelixNormal,
232 );
233 }
234}