@@ -498,8 +498,9 @@
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
- "shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
- "alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }]
+ "shift-c": "vim::HelixDuplicateBelow",
+ "alt-shift-c": "vim::HelixDuplicateAbove",
+ ",": "vim::HelixKeepNewestSelection"
}
},
{
@@ -1,4 +1,5 @@
mod boundary;
+mod duplicate;
mod object;
mod paste;
mod select;
@@ -40,6 +41,13 @@ actions!(
HelixSelectLine,
/// Select all matches of a given pattern within the current selection.
HelixSelectRegex,
+ /// Removes all but the one selection that was created last.
+ /// `Newest` can eventually be `Primary`.
+ HelixKeepNewestSelection,
+ /// Copies all selections below.
+ HelixDuplicateBelow,
+ /// Copies all selections above.
+ HelixDuplicateAbove,
]
);
@@ -51,6 +59,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_goto_last_modification);
Vim::action(editor, cx, Vim::helix_paste);
Vim::action(editor, cx, Vim::helix_select_regex);
+ Vim::action(editor, cx, Vim::helix_keep_newest_selection);
+ Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
+ let times = Vim::take_count(cx);
+ vim.helix_duplicate_selections_below(times, window, cx);
+ });
+ Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
+ let times = Vim::take_count(cx);
+ vim.helix_duplicate_selections_above(times, window, cx);
+ });
}
impl Vim {
@@ -575,6 +592,18 @@ impl Vim {
});
});
}
+
+ fn helix_keep_newest_selection(
+ &mut self,
+ _: &HelixKeepNewestSelection,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.update_editor(cx, |_, editor, cx| {
+ let newest = editor.selections.newest::<usize>(cx);
+ editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
+ });
+ }
}
#[cfg(test)]
@@ -0,0 +1,233 @@
+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<usize>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<usize>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<usize>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ advance_search: impl Fn(&mut DisplayPoint),
+ end_search: impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
+ above: bool,
+ ) {
+ let times = times.unwrap_or(1);
+ self.update_editor(cx, |_, editor, cx| {
+ let mut selections = Vec::new();
+ let (map, mut original_selections) = editor.selections.all_display(cx);
+ // 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<DisplayPoint>,
+ map: &DisplaySnapshot,
+ advance_search: &impl Fn(&mut DisplayPoint),
+ end_search: &impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
+) -> Option<Range<DisplayPoint>> {
+ 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<DisplayPoint>,
+ map: &DisplaySnapshot,
+) -> Range<usize> {
+ 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,
+ );
+ }
+}