diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 48eaf289c6f9..8995da8f9db4 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -13,7 +13,7 @@ use crate::{ }; use helix_stdx::rope::{self, RopeSliceExt}; use smallvec::{smallvec, SmallVec}; -use std::borrow::Cow; +use std::{borrow::Cow, iter, slice}; use tree_sitter::Node; /// A single selection range. @@ -503,6 +503,16 @@ impl Selection { &self.ranges } + /// Returns an iterator over the line ranges of each range in the selection. + /// + /// Adjacent and overlapping line ranges of the [Range]s in the selection are merged. + pub fn line_ranges<'a>(&'a self, text: RopeSlice<'a>) -> LineRangeIter<'a> { + LineRangeIter { + ranges: self.ranges.iter().peekable(), + text, + } + } + pub fn primary_index(&self) -> usize { self.primary_index } @@ -727,6 +737,33 @@ impl From for Selection { } } +pub struct LineRangeIter<'a> { + ranges: iter::Peekable>, + text: RopeSlice<'a>, +} + +impl<'a> Iterator for LineRangeIter<'a> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + let (start, mut end) = self.ranges.next()?.line_range(self.text); + while let Some((next_start, next_end)) = + self.ranges.peek().map(|range| range.line_range(self.text)) + { + // Merge overlapping and adjacent ranges. + // This subtraction cannot underflow because the ranges are sorted. + if next_start - end <= 1 { + end = next_end; + self.ranges.next(); + } else { + break; + } + } + + Some((start, end)) + } +} + // TODO: checkSelection -> check if valid for doc length && sorted pub fn keep_or_remove_matches( @@ -1165,6 +1202,32 @@ mod test { assert_eq!(Range::new(12, 0).line_range(s), (0, 2)); } + #[test] + fn selection_line_ranges() { + let (text, selection) = crate::test::print( + r#" L0 + #[|these]# line #(|ranges)# are #(|merged)# L1 + L2 + single one-line #(|range)# L3 + L4 + single #(|multiline L5 + range)# L6 + L7 + these #(|multiline L8 + ranges)# are #(|also L9 + merged)# L10 + L11 + adjacent #(|ranges)# L12 + are merged #(|the same way)# L13 + "#, + ); + let rope = Rope::from_str(&text); + assert_eq!( + vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)], + selection.line_ranges(rope.slice(..)).collect::>(), + ); + } + #[test] fn test_cursor() { let r = Rope::from_str("\r\nHi\r\nthere!"); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f38ae6bba4d4..b6182f8aab17 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2305,37 +2305,36 @@ fn reset_diff_change( let diff = handle.load(); let doc_text = doc.text().slice(..); - let line = doc.selection(view.id).primary().cursor_line(doc_text); - - let Some(hunk_idx) = diff.hunk_at(line as u32, true) else { - bail!("There is no change at the cursor") - }; - let hunk = diff.nth_hunk(hunk_idx); let diff_base = diff.diff_base(); - let before_start = diff_base.line_to_char(hunk.before.start as usize); - let before_end = diff_base.line_to_char(hunk.before.end as usize); - let text: Tendril = diff - .diff_base() - .slice(before_start..before_end) - .chunks() - .collect(); - let anchor = doc_text.line_to_char(hunk.after.start as usize); + let mut changes = 0; + let transaction = Transaction::change( doc.text(), - [( - anchor, - doc_text.line_to_char(hunk.after.end as usize), - (!text.is_empty()).then_some(text), - )] - .into_iter(), + diff.hunks_intersecting_line_ranges(doc.selection(view.id).line_ranges(doc_text)) + .map(|hunk| { + changes += 1; + let start = diff_base.line_to_char(hunk.before.start as usize); + let end = diff_base.line_to_char(hunk.before.end as usize); + let text: Tendril = diff_base.slice(start..end).chunks().collect(); + ( + doc_text.line_to_char(hunk.after.start as usize), + doc_text.line_to_char(hunk.after.end as usize), + (!text.is_empty()).then_some(text), + ) + }), ); + if changes == 0 { + bail!("There are no changes under any selection"); + } + drop(diff); // make borrow check happy doc.apply(&transaction, view.id); - // select inserted text - let text_len = before_end - before_start; - doc.set_selection(view.id, Selection::single(anchor, anchor + text_len)); doc.append_changes_to_history(view); view.ensure_cursor_in_view(doc, scrolloff); + cx.editor.set_status(format!( + "Reset {changes} change{}", + if changes == 1 { "" } else { "s" } + )); Ok(()) } diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index c72deb7ea7f3..634b179b4ff5 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -1,3 +1,4 @@ +use std::iter::Peekable; use std::ops::Range; use std::sync::Arc; @@ -259,6 +260,22 @@ impl Diff<'_> { } } + /// Iterates over all hunks that intersect with the given line ranges. + /// + /// Hunks are returned at most once even when intersecting with multiple of the line + /// ranges. + pub fn hunks_intersecting_line_ranges(&self, line_ranges: I) -> impl Iterator + where + I: Iterator, + { + HunksInLineRangesIter { + hunks: &self.diff.hunks, + line_ranges: line_ranges.peekable(), + inverted: self.inverted, + cursor: 0, + } + } + pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { let hunk_range = if self.inverted { |hunk: &Hunk| hunk.before.clone() @@ -290,3 +307,42 @@ impl Diff<'_> { } } } + +pub struct HunksInLineRangesIter<'a, I: Iterator> { + hunks: &'a [Hunk], + line_ranges: Peekable, + inverted: bool, + cursor: usize, +} + +impl<'a, I: Iterator> Iterator for HunksInLineRangesIter<'a, I> { + type Item = &'a Hunk; + + fn next(&mut self) -> Option { + let hunk_range = if self.inverted { + |hunk: &Hunk| hunk.before.clone() + } else { + |hunk: &Hunk| hunk.after.clone() + }; + + loop { + let (start_line, end_line) = self.line_ranges.peek()?; + let hunk = self.hunks.get(self.cursor)?; + + if (hunk_range(hunk).end as usize) < *start_line { + // If the hunk under the cursor comes before this range, jump the cursor + // ahead to the next hunk that overlaps with the line range. + self.cursor += self.hunks[self.cursor..] + .partition_point(|hunk| (hunk_range(hunk).end as usize) < *start_line); + } else if (hunk_range(hunk).start as usize) <= *end_line { + // If the hunk under the cursor overlaps with this line range, emit it + // and move the cursor up so that the hunk cannot be emitted twice. + self.cursor += 1; + return Some(hunk); + } else { + // Otherwise, go to the next line range. + self.line_ranges.next(); + } + } + } +}