From c8eb9f289977837827303b2d5ec4afccc3a495a2 Mon Sep 17 00:00:00 2001 From: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Wed, 19 Mar 2025 23:44:36 +0000 Subject: [PATCH] feat: add `line_blame` static command _ --- book/src/generated/static-cmd.md | 1 + helix-term/src/commands.rs | 37 ++++++++++++++++++++++++++++++++ helix-term/src/handlers/blame.rs | 30 +++++++------------------- helix-term/src/keymap/default.rs | 1 + helix-vcs/src/diff.rs | 14 ++++++++++++ helix-vcs/src/git/blame.rs | 4 +++- helix-vcs/src/lib.rs | 8 +++---- helix-view/src/document.rs | 7 ++++++ helix-view/src/handlers.rs | 4 ++-- 9 files changed, 77 insertions(+), 29 deletions(-) diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index af7515b8..f1cba01d 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -173,6 +173,7 @@ | `redo` | Redo change | normal: `` U ``, select: `` U `` | | `earlier` | Move backward in history | normal: `` <A-u> ``, select: `` <A-u> `` | | `later` | Move forward in history | normal: `` <A-U> ``, select: `` <A-U> `` | +| `line_blame` | Blame for the current line | normal: `` <space>B ``, select: `` <space>B `` | | `commit_undo_checkpoint` | Commit changes to new checkpoint | insert: `` <C-s> `` | | `yank` | Yank selection | normal: `` y ``, select: `` y `` | | `yank_to_clipboard` | Yank selections to clipboard | normal: `` <space>y ``, select: `` <space>y `` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a197792e..09c66961 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -472,6 +472,7 @@ impl MappableCommand { redo, "Redo change", earlier, "Move backward in history", later, "Move forward in history", + line_blame, "Blame for the current line", commit_undo_checkpoint, "Commit changes to new checkpoint", yank, "Yank selection", yank_to_clipboard, "Yank selections to clipboard", @@ -6559,6 +6560,42 @@ fn goto_word(cx: &mut Context) { jump_to_word(cx, Movement::Move) } +fn line_blame(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + const BLAME_ERROR: &str = "No blame available for the current file"; + let Some(diff) = doc.diff_handle() else { + cx.editor.set_error(BLAME_ERROR); + return; + }; + + let Some(path) = doc.path() else { + cx.editor.set_error(BLAME_ERROR); + return; + }; + + let cursor_line = doc.cursor_line(view.id); + let (inserted_lines_count, deleted_lines_count) = + diff.load().inserted_and_deleted_before_line(cursor_line); + + let Ok(cursor_line) = u32::try_from(doc.cursor_line(view.id)) else { + cx.editor.set_error(BLAME_ERROR); + return; + }; + + let Ok(output) = cx.editor.diff_providers.blame_line( + path, + cursor_line, + inserted_lines_count, + deleted_lines_count, + ) else { + cx.editor.set_error(BLAME_ERROR); + return; + }; + + cx.editor + .set_status(output.parse_format(&cx.editor.config().version_control.inline_blame_format)); +} + fn extend_to_word(cx: &mut Context) { jump_to_word(cx, Movement::Extend) } diff --git a/helix-term/src/handlers/blame.rs b/helix-term/src/handlers/blame.rs index 0be7815b..923ae74c 100644 --- a/helix-term/src/handlers/blame.rs +++ b/helix-term/src/handlers/blame.rs @@ -32,19 +32,16 @@ impl helix_event::AsyncHook for BlameHandler { file, cursor_line, diff_providers, - removed_lines_count, - added_lines_count, + deleted_lines_count: removed_lines_count, + inserted_lines_count: added_lines_count, blame_format, } = event; self.cursor_line = cursor_line; - // convert 0-based line numbers into 1-based line numbers - let cursor_line = cursor_line + 1; - let worker = tokio::spawn(async move { diff_providers - .blame(&file, cursor_line, added_lines_count, removed_lines_count) + .blame_line(&file, cursor_line, added_lines_count, removed_lines_count) .map(|s| s.parse_format(&blame_format)) }); self.worker = Some(worker); @@ -87,38 +84,27 @@ pub(super) fn register_hooks(handlers: &Handlers) { } let (view, doc) = current!(event.cx.editor); - let text = doc.text(); - let selection = doc.selection(view.id); let Some(file) = doc.path() else { return Ok(()); }; let file = file.to_path_buf(); - let Ok(cursor_line) = - u32::try_from(text.char_to_line(selection.primary().cursor(doc.text().slice(..)))) - else { + let Ok(cursor_line) = u32::try_from(doc.cursor_line(view.id)) else { return Ok(()); }; let hunks = doc.diff_handle().unwrap().load(); - let mut removed_lines_count: u32 = 0; - let mut added_lines_count: u32 = 0; - for hunk in hunks.hunks_intersecting_line_ranges(std::iter::once((0, cursor_line as usize))) - { - let lines_inserted = hunk.after.end - hunk.after.start; - let lines_removed = hunk.before.end - hunk.before.start; - added_lines_count += lines_inserted; - removed_lines_count += lines_removed; - } + let (inserted_lines_count, deleted_lines_count) = + hunks.inserted_and_deleted_before_line(cursor_line as usize); send_blocking( &tx, BlameEvent::PostCommand { file, cursor_line, - removed_lines_count, - added_lines_count, + deleted_lines_count, + inserted_lines_count, // ok to clone because diff_providers is very small diff_providers: event.cx.editor.diff_providers.clone(), // ok to clone because blame_format is likely to be about 30 characters or less diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index e160b224..4e24624c 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -232,6 +232,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> { "D" => workspace_diagnostics_picker, "g" => changed_file_picker, "a" => code_action, + "B" => line_blame, "'" => last_picker, "G" => { "Debug (experimental)" sticky=true "l" => dap_launch, diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index e49e171d..80c861d0 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -177,6 +177,20 @@ impl Diff<'_> { } } + /// Get the amount of lines inserted and deleted before a given line + pub fn inserted_and_deleted_before_line(&self, cursor_line: usize) -> (u32, u32) { + let mut inserted_lines_count: u32 = 0; + let mut deleted_lines_count: u32 = 0; + for hunk in self.hunks_intersecting_line_ranges(std::iter::once((0, cursor_line))) { + let lines_inserted = hunk.after.end - hunk.after.start; + let lines_removed = hunk.before.end - hunk.before.start; + inserted_lines_count += lines_inserted; + deleted_lines_count += lines_removed; + } + + (inserted_lines_count, deleted_lines_count) + } + pub fn doc(&self) -> &Rope { if self.inverted { &self.diff.diff_base diff --git a/helix-vcs/src/git/blame.rs b/helix-vcs/src/git/blame.rs index 6e8ac480..a56b361c 100644 --- a/helix-vcs/src/git/blame.rs +++ b/helix-vcs/src/git/blame.rs @@ -134,7 +134,9 @@ pub fn blame_line( // So when our cursor is on the 10th added line or earlier, blame_line will be 0. This means // the blame will be incorrect. But that's fine, because when the cursor_line is on some hunk, // we can show to the user nothing at all. This is detected in the editor - let blame_line = line.saturating_sub(added_lines_count) + removed_lines_count; + // + // Add 1 to convert 0-based line numbers into 1-based + let blame_line = line.saturating_sub(added_lines_count) + removed_lines_count + 1; let repo_dir = get_repo_dir(file)?; let repo = open_repo(repo_dir) diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index c63558c0..6f8e3b0f 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -50,8 +50,8 @@ impl DiffProviderRegistry { }) } - /// Blame range of lines in a file. Lines are 1-indexed - pub fn blame( + /// Blame a line in a file + pub fn blame_line( &self, file: &Path, line: u32, @@ -60,7 +60,7 @@ impl DiffProviderRegistry { ) -> anyhow::Result<BlameInformation> { self.providers .iter() - .map(|provider| provider.blame(file, line, added_lines_count, removed_lines_count)) + .map(|provider| provider.blame_line(file, line, added_lines_count, removed_lines_count)) .next() .context("No provider found")? } @@ -125,7 +125,7 @@ impl DiffProvider { } } - fn blame( + fn blame_line( &self, file: &Path, line: u32, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 482cd1df..0ed23bad 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1262,6 +1262,13 @@ impl Document { Range::new(0, 1).grapheme_aligned(self.text().slice(..)) } + /// Get the line of cursor for the primary selection + pub fn cursor_line(&self, view_id: ViewId) -> usize { + let text = self.text(); + let selection = self.selection(view_id); + text.char_to_line(selection.primary().cursor(text.slice(..))) + } + /// Reset the view's selection on this document to the /// [origin](Document::origin) cursor. pub fn reset_selection(&mut self, view_id: ViewId) { diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs index 99789102..7926ec81 100644 --- a/helix-view/src/handlers.rs +++ b/helix-view/src/handlers.rs @@ -25,9 +25,9 @@ pub enum BlameEvent { file: PathBuf, cursor_line: u32, /// How many lines were removed before cursor_line - removed_lines_count: u32, + deleted_lines_count: u32, /// How many lines were added before cursor_line - added_lines_count: u32, + inserted_lines_count: u32, diff_providers: DiffProviderRegistry, /// Format of the blame blame_format: String,