use std::ops::Range; use editor::{DisplayPoint, display_map::DisplaySnapshot}; use gpui::Context; use text::Bias; use ui::Window; use crate::Vim; impl Vim { /// Creates a duplicate of every selection below it in the first place that has both its start /// and end pub(super) fn helix_duplicate_selections_below( &mut self, times: Option, window: &mut Window, cx: &mut Context, ) { self.duplicate_selections( times, window, cx, |prev_point| *prev_point.row_mut() += 1, |prev_range, map| prev_range.end.row() >= map.max_point().row(), false, ); } /// Creates a duplicate of every selection above it in the first place that has both its start /// and end pub(super) fn helix_duplicate_selections_above( &mut self, times: Option, window: &mut Window, cx: &mut Context, ) { self.duplicate_selections( times, window, cx, |prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1), |prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(), true, ); } fn duplicate_selections( &mut self, times: Option, window: &mut Window, cx: &mut Context, advance_search: impl Fn(&mut DisplayPoint), end_search: impl Fn(&Range, &DisplaySnapshot) -> bool, above: bool, ) { let times = times.unwrap_or(1); self.update_editor(cx, |_, editor, cx| { let mut selections = Vec::new(); let map = editor.display_snapshot(cx); let mut original_selections = editor.selections.all_display(&map); // The order matters, because it is recorded when the selections are added. if above { original_selections.reverse(); } for origin in original_selections { let origin = origin.tail()..origin.head(); selections.push(display_point_range_to_offset_range(&origin, &map)); let mut last_origin = origin; for _ in 1..=times { if let Some(duplicate) = find_next_valid_duplicate_space( last_origin.clone(), &map, &advance_search, &end_search, ) { selections.push(display_point_range_to_offset_range(&duplicate, &map)); last_origin = duplicate; } else { break; } } } editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(selections); }); }); } } fn find_next_valid_duplicate_space( mut origin: Range, map: &DisplaySnapshot, advance_search: &impl Fn(&mut DisplayPoint), end_search: &impl Fn(&Range, &DisplaySnapshot) -> bool, ) -> Option> { while !end_search(&origin, map) { advance_search(&mut origin.start); advance_search(&mut origin.end); if map.clip_point(origin.start, Bias::Left) == origin.start && map.clip_point(origin.end, Bias::Right) == origin.end { return Some(origin); } } None } fn display_point_range_to_offset_range( range: &Range, map: &DisplaySnapshot, ) -> Range { range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right) } #[cfg(test)] mod tests { use db::indoc; use crate::{state::Mode, test::VimTestContext}; #[gpui::test] async fn test_selection_duplication(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.enable_helix(); cx.set_state( indoc! {" The quick brown fox «jumpsˇ» over the lazy dog."}, Mode::HelixNormal, ); cx.simulate_keystrokes("C"); cx.assert_state( indoc! {" The quick brown fox «jumpsˇ» over the lazy« dog.ˇ»"}, Mode::HelixNormal, ); cx.simulate_keystrokes("C"); cx.assert_state( indoc! {" The quick brown fox «jumpsˇ» over the lazy« dog.ˇ»"}, Mode::HelixNormal, ); cx.simulate_keystrokes("alt-C"); cx.assert_state( indoc! {" The «quickˇ» brown fox «jumpsˇ» over the lazy« dog.ˇ»"}, Mode::HelixNormal, ); cx.simulate_keystrokes(","); cx.assert_state( indoc! {" The «quickˇ» brown fox jumps over the lazy dog."}, Mode::HelixNormal, ); } #[gpui::test] async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.enable_helix(); cx.set_state( indoc! {" The quick brown «ˇfox» jumps over the lazy dog."}, Mode::HelixNormal, ); cx.simulate_keystrokes("C C alt-C"); cx.assert_state( indoc! {" «ˇThe» quick brown «ˇfox» jumps «ˇove»r the «ˇlaz»y dog."}, Mode::HelixNormal, ); } #[gpui::test] async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.enable_helix(); cx.set_state( indoc! {" The «qˇ»uick brown fox jumps over the lazy dog."}, Mode::HelixNormal, ); cx.simulate_keystrokes("9 C"); cx.assert_state( indoc! {" The «qˇ»uick brown fox «jˇ»umps over« ˇ»the lazy« ˇ»dog."}, Mode::HelixNormal, ); } }