From 48b6aa9a699df0680a6d31e9611ebd1ca9909de4 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 8 Mar 2023 02:49:14 +0100 Subject: [PATCH] Add command for resetting diff hunks (#5736) --- book/src/generated/typable-cmd.md | 1 + helix-term/src/commands.rs | 22 +++++------ helix-term/src/commands/typed.rs | 65 +++++++++++++++++++++++++++++++ helix-vcs/src/diff.rs | 58 +++++++++++++++++++-------- helix-vcs/src/diff/line_cache.rs | 8 ++++ helix-vcs/src/diff/worker.rs | 15 ++++--- helix-vcs/src/diff/worker/test.rs | 6 +-- helix-view/src/gutter.rs | 2 +- 8 files changed, 140 insertions(+), 37 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index badadc43d..8b367aad8 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -76,3 +76,4 @@ | `:pipe` | Pipe each selection to the shell command. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:run-shell-command`, `:sh` | Run a shell command | +| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c76e9f2bd..ebdfdfde2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3011,13 +3011,13 @@ fn goto_first_change_impl(cx: &mut Context, reverse: bool) { let (view, doc) = current!(editor); if let Some(handle) = doc.diff_handle() { let hunk = { - let hunks = handle.hunks(); + let diff = handle.load(); let idx = if reverse { - hunks.len().saturating_sub(1) + diff.len().saturating_sub(1) } else { 0 }; - hunks.nth_hunk(idx) + diff.nth_hunk(idx) }; if hunk != Hunk::NONE { let range = hunk_range(hunk, doc.text().slice(..)); @@ -3049,19 +3049,19 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { let selection = doc.selection(view.id).clone().transform(|range| { let cursor_line = range.cursor_line(doc_text) as u32; - let hunks = diff_handle.hunks(); + let diff = diff_handle.load(); let hunk_idx = match direction { - Direction::Forward => hunks + Direction::Forward => diff .next_hunk(cursor_line) - .map(|idx| (idx + count).min(hunks.len() - 1)), - Direction::Backward => hunks + .map(|idx| (idx + count).min(diff.len() - 1)), + Direction::Backward => diff .prev_hunk(cursor_line) .map(|idx| idx.saturating_sub(count)), }; let Some(hunk_idx) = hunk_idx else { return range; }; - let hunk = hunks.nth_hunk(hunk_idx); + let hunk = diff.nth_hunk(hunk_idx); let new_range = hunk_range(hunk, doc_text); if editor.mode == Mode::Select { let head = if new_range.head < range.anchor { @@ -4721,14 +4721,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let textobject_change = |range: Range| -> Range { let diff_handle = doc.diff_handle().unwrap(); - let hunks = diff_handle.hunks(); + let diff = diff_handle.load(); let line = range.cursor_line(text); - let hunk_idx = if let Some(hunk_idx) = hunks.hunk_at(line as u32, false) { + let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { hunk_idx } else { return range; }; - let hunk = hunks.nth_hunk(hunk_idx).after; + let hunk = diff.nth_hunk(hunk_idx).after; let start = text.line_to_char(hunk.start as usize); let end = text.line_to_char(hunk.end as usize); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0ddca6df7..77c143212 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2033,6 +2033,64 @@ fn run_shell_command( Ok(()) } +fn reset_diff_change( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(args.is_empty(), ":reset-diff-change takes no arguments"); + + let editor = &mut cx.editor; + let scrolloff = editor.config().scrolloff; + + let (view, doc) = current!(editor); + // TODO refactor to use let..else once MSRV is raised to 1.65 + let handle = match doc.diff_handle() { + Some(handle) => handle, + None => bail!("Diff is not available in the current buffer"), + }; + + let diff = handle.load(); + let doc_text = doc.text().slice(..); + let line = doc.selection(view.id).primary().cursor_line(doc_text); + + // TODO refactor to use let..else once MSRV is raised to 1.65 + let hunk_idx = match diff.hunk_at(line as u32, true) { + Some(hunk_idx) => hunk_idx, + None => 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 transaction = Transaction::change( + doc.text(), + [( + anchor, + doc_text.line_to_char(hunk.after.end as usize), + (!text.is_empty()).then_some(text), + )] + .into_iter(), + ); + 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); + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2569,6 +2627,13 @@ fn run_shell_command( fun: run_shell_command, completer: Some(completers::filename), }, + TypableCommand { + name: "reset-diff-change", + aliases: &["diffget", "diffg"], + doc: "Reset the diff change at the cursor position.", + fun: reset_diff_change, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index 9c6a362f7..ca33dda44 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -28,11 +28,18 @@ struct Event { render_lock: Option, } +#[derive(Clone, Debug, Default)] +struct DiffInner { + diff_base: Rope, + doc: Rope, + hunks: Vec, +} + #[derive(Clone, Debug)] pub struct DiffHandle { channel: UnboundedSender, render_lock: Arc>, - hunks: Arc>>, + diff: Arc>, inverted: bool, } @@ -47,10 +54,10 @@ fn new_with_handle( redraw_handle: RedrawHandle, ) -> (DiffHandle, JoinHandle<()>) { let (sender, receiver) = unbounded_channel(); - let hunks: Arc>> = Arc::default(); + let diff: Arc> = Arc::default(); let worker = DiffWorker { channel: receiver, - hunks: hunks.clone(), + diff: diff.clone(), new_hunks: Vec::default(), redraw_notify: redraw_handle.0, diff_finished_notify: Arc::default(), @@ -58,7 +65,7 @@ fn new_with_handle( let handle = tokio::spawn(worker.run(diff_base, doc)); let differ = DiffHandle { channel: sender, - hunks, + diff, inverted: false, render_lock: redraw_handle.1, }; @@ -69,9 +76,9 @@ pub fn invert(&mut self) { self.inverted = !self.inverted; } - pub fn hunks(&self) -> FileHunks { - FileHunks { - hunks: self.hunks.lock(), + pub fn load(&self) -> Diff { + Diff { + diff: self.diff.lock(), inverted: self.inverted, } } @@ -168,12 +175,28 @@ pub fn is_pure_removal(&self) -> bool { /// A list of changes in a file sorted in ascending /// non-overlapping order #[derive(Debug)] -pub struct FileHunks<'a> { - hunks: MutexGuard<'a, Vec>, +pub struct Diff<'a> { + diff: MutexGuard<'a, DiffInner>, inverted: bool, } -impl FileHunks<'_> { +impl Diff<'_> { + pub fn diff_base(&self) -> &Rope { + if self.inverted { + &self.diff.doc + } else { + &self.diff.diff_base + } + } + + pub fn doc(&self) -> &Rope { + if self.inverted { + &self.diff.diff_base + } else { + &self.diff.doc + } + } + pub fn is_inverted(&self) -> bool { self.inverted } @@ -181,7 +204,7 @@ pub fn is_inverted(&self) -> bool { /// Returns the `Hunk` for the `n`th change in this file. /// if there is no `n`th change `Hunk::NONE` is returned instead. pub fn nth_hunk(&self, n: u32) -> Hunk { - match self.hunks.get(n as usize) { + match self.diff.hunks.get(n as usize) { Some(hunk) if self.inverted => hunk.invert(), Some(hunk) => hunk.clone(), None => Hunk::NONE, @@ -189,7 +212,7 @@ pub fn nth_hunk(&self, n: u32) -> Hunk { } pub fn len(&self) -> u32 { - self.hunks.len() as u32 + self.diff.hunks.len() as u32 } pub fn is_empty(&self) -> bool { @@ -204,19 +227,20 @@ pub fn next_hunk(&self, line: u32) -> Option { }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); match res { // Search found a hunk that starts exactly at this line, return the next hunk if it exists. - Ok(pos) if pos + 1 == self.hunks.len() => None, + Ok(pos) if pos + 1 == self.diff.hunks.len() => None, Ok(pos) => Some(pos as u32 + 1), // No hunk starts exactly at this line, so the search returns // the position where a hunk starting at this line should be inserted. // That position is exactly the position of the next hunk or the end // of the list if no such hunk exists - Err(pos) if pos == self.hunks.len() => None, + Err(pos) if pos == self.diff.hunks.len() => None, Err(pos) => Some(pos as u32), } } @@ -228,6 +252,7 @@ pub fn prev_hunk(&self, line: u32) -> Option { |hunk: &Hunk| hunk.after.clone() }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).end); @@ -237,7 +262,7 @@ pub fn prev_hunk(&self, line: u32) -> Option { // which represents a pure removal. // Removals are technically empty but are still shown as single line hunks // and as such we must jump to the previous hunk (if it exists) if we are already inside the removal - Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32), + Ok(pos) if !hunk_range(&self.diff.hunks[pos]).is_empty() => Some(pos as u32), // No hunk ends exactly at this line, so the search returns // the position where a hunk ending at this line should be inserted. @@ -255,6 +280,7 @@ pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); @@ -267,7 +293,7 @@ pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { // The previous hunk contains this hunk if it exists and doesn't end before this line Err(0) => None, Err(pos) => { - let hunk = hunk_range(&self.hunks[pos - 1]); + let hunk = hunk_range(&self.diff.hunks[pos - 1]); if hunk.end > line || include_removal && hunk.start == line && hunk.is_empty() { Some(pos as u32 - 1) } else { diff --git a/helix-vcs/src/diff/line_cache.rs b/helix-vcs/src/diff/line_cache.rs index c3ee5daa3..8e48250f1 100644 --- a/helix-vcs/src/diff/line_cache.rs +++ b/helix-vcs/src/diff/line_cache.rs @@ -43,6 +43,14 @@ pub fn new(diff_base: Rope, doc: Rope) -> InternedRopeLines { res } + pub fn doc(&self) -> Rope { + self.doc.clone() + } + + pub fn diff_base(&self) -> Rope { + self.diff_base.clone() + } + /// Updates the `diff_base` and optionally the document if `doc` is not None pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option) { self.interned.clear(); diff --git a/helix-vcs/src/diff/worker.rs b/helix-vcs/src/diff/worker.rs index f4bb4dbfb..5406446fd 100644 --- a/helix-vcs/src/diff/worker.rs +++ b/helix-vcs/src/diff/worker.rs @@ -10,7 +10,7 @@ use tokio::time::{timeout, timeout_at, Duration}; use crate::diff::{ - Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC, + DiffInner, Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC, }; use super::line_cache::InternedRopeLines; @@ -21,7 +21,7 @@ pub(super) struct DiffWorker { pub channel: UnboundedReceiver, - pub hunks: Arc>>, + pub diff: Arc>, pub new_hunks: Vec, pub redraw_notify: Arc, pub diff_finished_notify: Arc, @@ -46,7 +46,7 @@ pub async fn run(mut self, diff_base: Rope, doc: Rope) { if let Some(lines) = interner.interned_lines() { self.perform_diff(lines); } - self.apply_hunks(); + self.apply_hunks(interner.diff_base(), interner.doc()); while let Some(event) = self.channel.recv().await { let (doc, diff_base) = self.accumulate_events(event).await; @@ -70,15 +70,18 @@ pub async fn run(mut self, diff_base: Rope, doc: Rope) { #[cfg(not(test))] tokio::task::block_in_place(process_accumulated_events); - self.apply_hunks(); + self.apply_hunks(interner.diff_base(), interner.doc()); } } /// update the hunks (used by the gutter) by replacing it with `self.new_hunks`. /// `self.new_hunks` is always empty after this function runs. /// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs` - fn apply_hunks(&mut self) { - swap(&mut *self.hunks.lock(), &mut self.new_hunks); + fn apply_hunks(&mut self, diff_base: Rope, doc: Rope) { + let mut diff = self.diff.lock(); + diff.diff_base = diff_base; + diff.doc = doc; + swap(&mut diff.hunks, &mut self.new_hunks); self.diff_finished_notify.notify_waiters(); self.new_hunks.clear(); } diff --git a/helix-vcs/src/diff/worker/test.rs b/helix-vcs/src/diff/worker/test.rs index 144424265..6a68d987c 100644 --- a/helix-vcs/src/diff/worker/test.rs +++ b/helix-vcs/src/diff/worker/test.rs @@ -12,12 +12,12 @@ fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) { ) } async fn into_diff(self, handle: JoinHandle<()>) -> Vec { - let hunks = self.hunks; + let diff = self.diff; // dropping the channel terminates the task drop(self.channel); handle.await.unwrap(); - let hunks = hunks.lock(); - Vec::clone(&*hunks) + let diff = diff.lock(); + Vec::clone(&diff.hunks) } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index cb9e43336..36e8e16a4 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -100,7 +100,7 @@ pub fn diff<'doc>( let deleted = theme.get("diff.minus"); let modified = theme.get("diff.delta"); if let Some(diff_handle) = doc.diff_handle() { - let hunks = diff_handle.hunks(); + let hunks = diff_handle.load(); let mut hunk_i = 0; let mut hunk = hunks.nth_hunk(hunk_i); Box::new(