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.
This commit is contained in:
parent
2301430e37
commit
ff6aca12b7
3 changed files with 142 additions and 24 deletions
|
@ -13,7 +13,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use helix_stdx::rope::{self, RopeSliceExt};
|
use helix_stdx::rope::{self, RopeSliceExt};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, iter, slice};
|
||||||
use tree_sitter::Node;
|
use tree_sitter::Node;
|
||||||
|
|
||||||
/// A single selection range.
|
/// A single selection range.
|
||||||
|
@ -503,6 +503,16 @@ impl Selection {
|
||||||
&self.ranges
|
&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 {
|
pub fn primary_index(&self) -> usize {
|
||||||
self.primary_index
|
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
|
// TODO: checkSelection -> check if valid for doc length && sorted
|
||||||
|
|
||||||
pub fn keep_or_remove_matches(
|
pub fn keep_or_remove_matches(
|
||||||
|
@ -1165,6 +1202,32 @@ mod test {
|
||||||
assert_eq!(Range::new(12, 0).line_range(s), (0, 2));
|
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::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cursor() {
|
fn test_cursor() {
|
||||||
let r = Rope::from_str("\r\nHi\r\nthere!");
|
let r = Rope::from_str("\r\nHi\r\nthere!");
|
||||||
|
|
|
@ -2305,37 +2305,36 @@ fn reset_diff_change(
|
||||||
|
|
||||||
let diff = handle.load();
|
let diff = handle.load();
|
||||||
let doc_text = doc.text().slice(..);
|
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 diff_base = diff.diff_base();
|
||||||
let before_start = diff_base.line_to_char(hunk.before.start as usize);
|
let mut changes = 0;
|
||||||
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 transaction = Transaction::change(
|
let transaction = Transaction::change(
|
||||||
doc.text(),
|
doc.text(),
|
||||||
[(
|
diff.hunks_intersecting_line_ranges(doc.selection(view.id).line_ranges(doc_text))
|
||||||
anchor,
|
.map(|hunk| {
|
||||||
doc_text.line_to_char(hunk.after.end as usize),
|
changes += 1;
|
||||||
(!text.is_empty()).then_some(text),
|
let start = diff_base.line_to_char(hunk.before.start as usize);
|
||||||
)]
|
let end = diff_base.line_to_char(hunk.before.end as usize);
|
||||||
.into_iter(),
|
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
|
drop(diff); // make borrow check happy
|
||||||
doc.apply(&transaction, view.id);
|
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);
|
doc.append_changes_to_history(view);
|
||||||
view.ensure_cursor_in_view(doc, scrolloff);
|
view.ensure_cursor_in_view(doc, scrolloff);
|
||||||
|
cx.editor.set_status(format!(
|
||||||
|
"Reset {changes} change{}",
|
||||||
|
if changes == 1 { "" } else { "s" }
|
||||||
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::iter::Peekable;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
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> {
|
pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option<u32> {
|
||||||
let hunk_range = if self.inverted {
|
let hunk_range = if self.inverted {
|
||||||
|hunk: &Hunk| hunk.before.clone()
|
|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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue