duplicate.rs

  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}