on master help sign in
Add command to merge consecutive ranges in selection (#5047)
1107296DylanBulfin
book/src/keymap.md+1
@@ -111,6 +111,7 @@
111| `s` | Select all regex matches inside selections | `select_regex` |
111| `s` | Select all regex matches inside selections | `select_regex` |
112| `S` | Split selection into subselections on regex matches | `split_selection` |
112| `S` | Split selection into subselections on regex matches | `split_selection` |
113| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
113| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
114+| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` |
114| `&` | Align selection in columns | `align_selections` |
115| `&` | Align selection in columns | `align_selections` |
115| `_` | Trim whitespace from the selection | `trim_selections` |
116| `_` | Trim whitespace from the selection | `trim_selections` |
116| `;` | Collapse selection onto a single cursor | `collapse_selection` |
117| `;` | Collapse selection onto a single cursor | `collapse_selection` |
helix-core/src/selection.rs+83−12
@@ -495,28 +495,53 @@ impl Selection {
495
495
496 /// Normalizes a `Selection`.
496 /// Normalizes a `Selection`.
497 fn normalize(mut self) -> Self {
497 fn normalize(mut self) -> Self {
498− let primary = self.ranges[self.primary_index];
498+ let mut primary = self.ranges[self.primary_index];
499 self.ranges.sort_unstable_by_key(Range::from);
499 self.ranges.sort_unstable_by_key(Range::from);
500+
501+ self.ranges.dedup_by(|curr_range, prev_range| {
502+ if prev_range.overlaps(curr_range) {
503+ let new_range = curr_range.merge(*prev_range);
504+ if prev_range == &primary || curr_range == &primary {
505+ primary = new_range;
506+ }
507+ *prev_range = new_range;
508+ true
509+ } else {
510+ false
511+ }
512+ });
513+
500 self.primary_index = self
514 self.primary_index = self
501 .ranges
515 .ranges
502 .iter()
516 .iter()
503 .position(|&range| range == primary)
517 .position(|&range| range == primary)
504 .unwrap();
518 .unwrap();
505
519
506− let mut prev_i = 0;
520+ self
507− for i in 1..self.ranges.len() {
521+ }
508− if self.ranges[prev_i].overlaps(&self.ranges[i]) {
522+
509− self.ranges[prev_i] = self.ranges[prev_i].merge(self.ranges[i]);
523+ // Merges all ranges that are consecutive
524+ pub fn merge_consecutive_ranges(mut self) -> Self {
525+ let mut primary = self.ranges[self.primary_index];
526+
527+ self.ranges.dedup_by(|curr_range, prev_range| {
528+ if prev_range.to() == curr_range.from() {
529+ let new_range = curr_range.merge(*prev_range);
530+ if prev_range == &primary || curr_range == &primary {
531+ primary = new_range;
532+ }
533+ *prev_range = new_range;
534+ true
510 } else {
535 } else {
511− prev_i += 1;
536+ false
512− self.ranges[prev_i] = self.ranges[i];
513 }
537 }
514− if i == self.primary_index {
515− self.primary_index = prev_i;
516− }
517− }
538+ });
518
539
519− self.ranges.truncate(prev_i + 1);
540+ self.primary_index = self
541+ .ranges
542+ .iter()
543+ .position(|&range| range == primary)
544+ .unwrap();
520
545
521 self
546 self
522 }
547 }
@@ -1132,6 +1157,52 @@ mod test {
1132 &["", "abcd", "efg", "rs", "xyz"]
1157 &["", "abcd", "efg", "rs", "xyz"]
1133 );
1158 );
1134 }
1159 }
1160+
1161+ #[test]
1162+ fn test_merge_consecutive_ranges() {
1163+ let selection = Selection::new(
1164+ smallvec![
1165+ Range::new(0, 1),
1166+ Range::new(1, 10),
1167+ Range::new(15, 20),
1168+ Range::new(25, 26),
1169+ Range::new(26, 30)
1170+ ],
1171+ 4,
1172+ );
1173+
1174+ let result = selection.merge_consecutive_ranges();
1175+
1176+ assert_eq!(
1177+ result.ranges(),
1178+ &[Range::new(0, 10), Range::new(15, 20), Range::new(25, 30)]
1179+ );
1180+ assert_eq!(result.primary_index, 2);
1181+
1182+ let selection = Selection::new(smallvec![Range::new(0, 1)], 0);
1183+ let result = selection.merge_consecutive_ranges();
1184+
1185+ assert_eq!(result.ranges(), &[Range::new(0, 1)]);
1186+ assert_eq!(result.primary_index, 0);
1187+
1188+ let selection = Selection::new(
1189+ smallvec![
1190+ Range::new(0, 1),
1191+ Range::new(1, 5),
1192+ Range::new(5, 8),
1193+ Range::new(8, 10),
1194+ Range::new(10, 15),
1195+ Range::new(18, 25)
1196+ ],
1197+ 3,
1198+ );
1199+
1200+ let result = selection.merge_consecutive_ranges();
1201+
1202+ assert_eq!(result.ranges(), &[Range::new(0, 15), Range::new(18, 25)]);
1203+ assert_eq!(result.primary_index, 0);
1204+ }
1205+
1135 #[test]
1206 #[test]
1136 fn test_selection_contains() {
1207 fn test_selection_contains() {
1137 fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
1208 fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
helix-term/src/commands.rs+7
@@ -244,6 +244,7 @@ impl MappableCommand {
244 select_regex, "Select all regex matches inside selections",
244 select_regex, "Select all regex matches inside selections",
245 split_selection, "Split selections on regex matches",
245 split_selection, "Split selections on regex matches",
246 split_selection_on_newline, "Split selection on newlines",
246 split_selection_on_newline, "Split selection on newlines",
247+ merge_consecutive_selections, "Merge consecutive selections",
247 search, "Search for regex pattern",
248 search, "Search for regex pattern",
248 rsearch, "Reverse search for regex pattern",
249 rsearch, "Reverse search for regex pattern",
249 search_next, "Select next search match",
250 search_next, "Select next search match",
@@ -1589,6 +1590,12 @@ fn split_selection_on_newline(cx: &mut Context) {
1589 doc.set_selection(view.id, selection);
1590 doc.set_selection(view.id, selection);
1590}
1591}
1591
1592
1593+fn merge_consecutive_selections(cx: &mut Context) {
1594+ let (view, doc) = current!(cx.editor);
1595+ let selection = doc.selection(view.id).clone().merge_consecutive_ranges();
1596+ doc.set_selection(view.id, selection);
1597+}
1598+
1592#[allow(clippy::too_many_arguments)]
1599#[allow(clippy::too_many_arguments)]
1593fn search_impl(
1600fn search_impl(
1594 editor: &mut Editor,
1601 editor: &mut Editor,
helix-term/src/keymap/default.rs+1
@@ -76,6 +76,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
76
76
77 "s" => select_regex,
77 "s" => select_regex,
78 "A-s" => split_selection_on_newline,
78 "A-s" => split_selection_on_newline,
79+ "A-_" => merge_consecutive_selections,
79 "S" => split_selection,
80 "S" => split_selection,
80 ";" => collapse_selection,
81 ";" => collapse_selection,
81 "A-;" => flip_selections,
82 "A-;" => flip_selections,