1use std::ops::Range;
2
3use editor::{DisplayPoint, MultiBufferOffset, display_map::DisplaySnapshot};
4use gpui::Context;
5use language::PointUtf16;
6use multi_buffer::MultiBufferRow;
7use text::Bias;
8use ui::Window;
9
10use crate::Vim;
11
12#[derive(Copy, Clone)]
13enum Direction {
14 Above,
15 Below,
16}
17
18impl Vim {
19 /// Creates a duplicate of every selection below it in the first place that has both its start
20 /// and end
21 pub(super) fn helix_duplicate_selections_below(
22 &mut self,
23 times: Option<usize>,
24 window: &mut Window,
25 cx: &mut Context<Self>,
26 ) {
27 self.duplicate_selections(times, window, cx, Direction::Below);
28 }
29
30 /// Creates a duplicate of every selection above it in the first place that has both its start
31 /// and end
32 pub(super) fn helix_duplicate_selections_above(
33 &mut self,
34 times: Option<usize>,
35 window: &mut Window,
36 cx: &mut Context<Self>,
37 ) {
38 self.duplicate_selections(times, window, cx, Direction::Above);
39 }
40
41 fn duplicate_selections(
42 &mut self,
43 times: Option<usize>,
44 window: &mut Window,
45 cx: &mut Context<Self>,
46 direction: Direction,
47 ) {
48 let times = times.unwrap_or(1);
49 self.update_editor(cx, |_, editor, cx| {
50 let mut selections = Vec::new();
51 let map = editor.display_snapshot(cx);
52 let mut original_selections = editor.selections.all_display(&map);
53 // The order matters, because it is recorded when the selections are added.
54 if matches!(direction, Direction::Above) {
55 original_selections.reverse();
56 }
57
58 for origin in original_selections {
59 let origin = origin.tail()..origin.head();
60 selections.push(display_point_range_to_offset_range(&origin, &map));
61 let mut last_origin = origin;
62 for _ in 1..=times {
63 if let Some(duplicate) =
64 find_next_valid_duplicate_space(last_origin.clone(), &map, direction)
65 {
66 selections.push(display_point_range_to_offset_range(&duplicate, &map));
67 last_origin = duplicate;
68 } else {
69 break;
70 }
71 }
72 }
73
74 editor.change_selections(Default::default(), window, cx, |s| {
75 s.select_ranges(selections);
76 });
77 });
78 }
79}
80
81fn find_next_valid_duplicate_space(
82 origin: Range<DisplayPoint>,
83 map: &DisplaySnapshot,
84 direction: Direction,
85) -> Option<Range<DisplayPoint>> {
86 let buffer = map.buffer_snapshot();
87 let start_col_utf16 = buffer
88 .point_to_point_utf16(origin.start.to_point(map))
89 .column;
90 let end_col_utf16 = buffer.point_to_point_utf16(origin.end.to_point(map)).column;
91
92 let mut candidate = origin;
93 loop {
94 match direction {
95 Direction::Below => {
96 if candidate.end.row() >= map.max_point().row() {
97 return None;
98 }
99 *candidate.start.row_mut() += 1;
100 *candidate.end.row_mut() += 1;
101 }
102 Direction::Above => {
103 if candidate.start.row() == DisplayPoint::zero().row() {
104 return None;
105 }
106 *candidate.start.row_mut() = candidate.start.row().0.saturating_sub(1);
107 *candidate.end.row_mut() = candidate.end.row().0.saturating_sub(1);
108 }
109 }
110
111 let start_row = DisplayPoint::new(candidate.start.row(), 0)
112 .to_point(map)
113 .row;
114 let end_row = DisplayPoint::new(candidate.end.row(), 0).to_point(map).row;
115
116 if start_col_utf16 > buffer.line_len_utf16(MultiBufferRow(start_row))
117 || end_col_utf16 > buffer.line_len_utf16(MultiBufferRow(end_row))
118 {
119 continue;
120 }
121
122 let start_col = buffer
123 .point_utf16_to_point(PointUtf16::new(start_row, start_col_utf16))
124 .column;
125 let end_col = buffer
126 .point_utf16_to_point(PointUtf16::new(end_row, end_col_utf16))
127 .column;
128
129 let candidate_start = DisplayPoint::new(candidate.start.row(), start_col);
130 let candidate_end = DisplayPoint::new(candidate.end.row(), end_col);
131
132 if map.clip_point(candidate_start, Bias::Left) == candidate_start
133 && map.clip_point(candidate_end, Bias::Right) == candidate_end
134 {
135 return Some(candidate_start..candidate_end);
136 }
137 }
138}
139
140fn display_point_range_to_offset_range(
141 range: &Range<DisplayPoint>,
142 map: &DisplaySnapshot,
143) -> Range<MultiBufferOffset> {
144 range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
145}
146
147#[cfg(test)]
148mod tests {
149 use db::indoc;
150
151 use crate::{state::Mode, test::VimTestContext};
152
153 #[gpui::test]
154 async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
155 let mut cx = VimTestContext::new(cx, true).await;
156 cx.enable_helix();
157
158 cx.set_state(
159 indoc! {"
160 The quick brown
161 fox «jumpsˇ»
162 over the
163 lazy dog."},
164 Mode::HelixNormal,
165 );
166
167 cx.simulate_keystrokes("C");
168
169 cx.assert_state(
170 indoc! {"
171 The quick brown
172 fox «jumpsˇ»
173 over the
174 lazy« dog.ˇ»"},
175 Mode::HelixNormal,
176 );
177
178 cx.simulate_keystrokes("C");
179
180 cx.assert_state(
181 indoc! {"
182 The quick brown
183 fox «jumpsˇ»
184 over the
185 lazy« dog.ˇ»"},
186 Mode::HelixNormal,
187 );
188
189 cx.simulate_keystrokes("alt-C");
190
191 cx.assert_state(
192 indoc! {"
193 The «quickˇ» brown
194 fox «jumpsˇ»
195 over the
196 lazy« dog.ˇ»"},
197 Mode::HelixNormal,
198 );
199
200 cx.simulate_keystrokes(",");
201
202 cx.assert_state(
203 indoc! {"
204 The «quickˇ» brown
205 fox jumps
206 over the
207 lazy dog."},
208 Mode::HelixNormal,
209 );
210 }
211
212 #[gpui::test]
213 async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
214 let mut cx = VimTestContext::new(cx, true).await;
215 cx.enable_helix();
216
217 cx.set_state(
218 indoc! {"
219 The quick brown
220 «ˇfox» jumps
221 over the
222 lazy dog."},
223 Mode::HelixNormal,
224 );
225
226 cx.simulate_keystrokes("C C alt-C");
227
228 cx.assert_state(
229 indoc! {"
230 «ˇThe» quick brown
231 «ˇfox» jumps
232 «ˇove»r the
233 «ˇlaz»y dog."},
234 Mode::HelixNormal,
235 );
236 }
237
238 #[gpui::test]
239 async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
240 let mut cx = VimTestContext::new(cx, true).await;
241 cx.enable_helix();
242
243 cx.set_state(
244 indoc! {"
245 The «qˇ»uick brown
246 fox jumps
247 over the
248 lazy dog."},
249 Mode::HelixNormal,
250 );
251
252 cx.simulate_keystrokes("9 C");
253
254 cx.assert_state(
255 indoc! {"
256 The «qˇ»uick brown
257 fox «jˇ»umps
258 over« ˇ»the
259 lazy« ˇ»dog."},
260 Mode::HelixNormal,
261 );
262 }
263
264 #[gpui::test]
265 async fn test_selection_duplication_multiline_multibyte(cx: &mut gpui::TestAppContext) {
266 let mut cx = VimTestContext::new(cx, true).await;
267 cx.enable_helix();
268
269 // Multiline selection on rows with multibyte chars should preserve
270 // the visual column on both start and end rows.
271 cx.set_state(
272 indoc! {"
273 «H䡻llo
274 Hëllo
275 Hallo"},
276 Mode::HelixNormal,
277 );
278
279 cx.simulate_keystrokes("C");
280
281 cx.assert_state(
282 indoc! {"
283 «H䡻llo
284 «H롻llo
285 Hallo"},
286 Mode::HelixNormal,
287 );
288 }
289
290 #[gpui::test]
291 async fn test_selection_duplication_multibyte(cx: &mut gpui::TestAppContext) {
292 let mut cx = VimTestContext::new(cx, true).await;
293 cx.enable_helix();
294
295 // Selection on a line with multibyte chars should duplicate to the
296 // same character column on the next line, not skip it.
297 cx.set_state(
298 indoc! {"
299 H«äˇ»llo
300 Hallo"},
301 Mode::HelixNormal,
302 );
303
304 cx.simulate_keystrokes("C");
305
306 cx.assert_state(
307 indoc! {"
308 H«äˇ»llo
309 H«aˇ»llo"},
310 Mode::HelixNormal,
311 );
312 }
313}