Skip to content

Commit

Permalink
Add command for resetting diff hunks (helix-editor#5736)
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalkuthe authored and Sagnik Bhattacharya committed Mar 21, 2023
1 parent 708dd17 commit fa70abb
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 37 deletions.
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
22 changes: 11 additions & 11 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(..));
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
65 changes: 65 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2033,6 +2033,64 @@ fn run_shell_command(
Ok(())
}

fn reset_diff_change(
cx: &mut compositor::Context,
args: &[Cow<str>],
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",
Expand Down Expand Up @@ -2569,6 +2627,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
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<HashMap<&'static str, &'static TypableCommand>> =
Expand Down
58 changes: 42 additions & 16 deletions helix-vcs/src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ struct Event {
render_lock: Option<RenderLock>,
}

#[derive(Clone, Debug, Default)]
struct DiffInner {
diff_base: Rope,
doc: Rope,
hunks: Vec<Hunk>,
}

#[derive(Clone, Debug)]
pub struct DiffHandle {
channel: UnboundedSender<Event>,
render_lock: Arc<RwLock<()>>,
hunks: Arc<Mutex<Vec<Hunk>>>,
diff: Arc<Mutex<DiffInner>>,
inverted: bool,
}

Expand All @@ -47,18 +54,18 @@ impl DiffHandle {
redraw_handle: RedrawHandle,
) -> (DiffHandle, JoinHandle<()>) {
let (sender, receiver) = unbounded_channel();
let hunks: Arc<Mutex<Vec<Hunk>>> = Arc::default();
let diff: Arc<Mutex<DiffInner>> = 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(),
};
let handle = tokio::spawn(worker.run(diff_base, doc));
let differ = DiffHandle {
channel: sender,
hunks,
diff,
inverted: false,
render_lock: redraw_handle.1,
};
Expand All @@ -69,9 +76,9 @@ impl DiffHandle {
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,
}
}
Expand Down Expand Up @@ -168,28 +175,44 @@ impl Hunk {
/// A list of changes in a file sorted in ascending
/// non-overlapping order
#[derive(Debug)]
pub struct FileHunks<'a> {
hunks: MutexGuard<'a, Vec<Hunk>>,
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
}

/// 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,
}
}

pub fn len(&self) -> u32 {
self.hunks.len() as u32
self.diff.hunks.len() as u32
}

pub fn is_empty(&self) -> bool {
Expand All @@ -204,19 +227,20 @@ impl FileHunks<'_> {
};

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),
}
}
Expand All @@ -228,6 +252,7 @@ impl FileHunks<'_> {
|hunk: &Hunk| hunk.after.clone()
};
let res = self
.diff
.hunks
.binary_search_by_key(&line, |hunk| hunk_range(hunk).end);

Expand All @@ -237,7 +262,7 @@ impl FileHunks<'_> {
// 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.
Expand All @@ -255,6 +280,7 @@ impl FileHunks<'_> {
};

let res = self
.diff
.hunks
.binary_search_by_key(&line, |hunk| hunk_range(hunk).start);

Expand All @@ -267,7 +293,7 @@ impl FileHunks<'_> {
// 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 {
Expand Down
8 changes: 8 additions & 0 deletions helix-vcs/src/diff/line_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ impl 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<Rope>) {
self.interned.clear();
Expand Down
15 changes: 9 additions & 6 deletions helix-vcs/src/diff/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tokio::sync::Notify;
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;
Expand All @@ -21,7 +21,7 @@ mod test;

pub(super) struct DiffWorker {
pub channel: UnboundedReceiver<Event>,
pub hunks: Arc<Mutex<Vec<Hunk>>>,
pub diff: Arc<Mutex<DiffInner>>,
pub new_hunks: Vec<Hunk>,
pub redraw_notify: Arc<Notify>,
pub diff_finished_notify: Arc<Notify>,
Expand All @@ -46,7 +46,7 @@ impl DiffWorker {
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;

Expand All @@ -70,15 +70,18 @@ impl DiffWorker {
#[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();
}
Expand Down
6 changes: 3 additions & 3 deletions helix-vcs/src/diff/worker/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ impl DiffHandle {
)
}
async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion helix-view/src/gutter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit fa70abb

Please sign in to comment.