Unnamed repository; edit this file 'description' to name the repository.
Reset all changes overlapped by selections in ':reset-diff-change' (#10178)
This is useful for resetting multiple changes at once. For example you might use 'maf' or even '%' to select a larger region and reset all changes within. The original behavior of resetting the change on the current line is retained when the primary selection is 1-width since we look for chunks in the line range of each selection.
Michael Davis 2024-05-20
parent 2301430 · commit ff6aca1
-rw-r--r--helix-core/src/selection.rs65
-rw-r--r--helix-term/src/commands/typed.rs45
-rw-r--r--helix-vcs/src/diff.rs56
3 files changed, 142 insertions, 24 deletions
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 48eaf289..8995da8f 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<Range> for Selection {
}
}
+pub struct LineRangeIter<'a> {
+ ranges: iter::Peekable<slice::Iter<'a, Range>>,
+ text: RopeSlice<'a>,
+}
+
+impl<'a> Iterator for LineRangeIter<'a> {
+ type Item = (usize, usize);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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(
@@ -1166,6 +1203,32 @@ mod test {
}
#[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::<Vec<_>>(),
+ );
+ }
+
+ #[test]
fn test_cursor() {
let r = Rope::from_str("\r\nHi\r\nthere!");
let s = r.slice(..);
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index f38ae6bb..b6182f8a 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 c72deb7e..634b179b 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<I>(&self, line_ranges: I) -> impl Iterator<Item = &Hunk>
+ where
+ I: Iterator<Item = (usize, usize)>,
+ {
+ 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<u32> {
let hunk_range = if self.inverted {
|hunk: &Hunk| hunk.before.clone()
@@ -290,3 +307,42 @@ impl Diff<'_> {
}
}
}
+
+pub struct HunksInLineRangesIter<'a, I: Iterator<Item = (usize, usize)>> {
+ hunks: &'a [Hunk],
+ line_ranges: Peekable<I>,
+ inverted: bool,
+ cursor: usize,
+}
+
+impl<'a, I: Iterator<Item = (usize, usize)>> Iterator for HunksInLineRangesIter<'a, I> {
+ type Item = &'a Hunk;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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();
+ }
+ }
+ }
+}