From e2cebacf331e4f6c68518ae6e9eaab1b78cdf3a0 Mon Sep 17 00:00:00 2001
From: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
Date: Tue, 18 Mar 2025 15:39:47 +0000
Subject: [PATCH] feat: basic implementation of blocking Blame handler

---
 helix-term/src/handlers.rs       |  4 +++
 helix-term/src/handlers/blame.rs | 61 ++++++++++++++++++++++++++++++++
 helix-view/src/editor.rs         | 16 +++++++++
 helix-view/src/handlers.rs       |  6 ++++
 4 files changed, 87 insertions(+)
 create mode 100644 helix-term/src/handlers/blame.rs

diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
index b580e678..6f18d43b 100644
--- a/helix-term/src/handlers.rs
+++ b/helix-term/src/handlers.rs
@@ -11,6 +11,7 @@ use crate::handlers::signature_help::SignatureHelpHandler;
 pub use helix_view::handlers::Handlers;
 
 mod auto_save;
+mod blame;
 pub mod completion;
 mod diagnostics;
 mod signature_help;
@@ -22,11 +23,13 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
     let event_tx = completion::CompletionHandler::new(config).spawn();
     let signature_hints = SignatureHelpHandler::new().spawn();
     let auto_save = AutoSaveHandler::new().spawn();
+    let blame = blame::BlameHandler.spawn();
 
     let handlers = Handlers {
         completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
         signature_hints,
         auto_save,
+        blame,
     };
 
     completion::register_hooks(&handlers);
@@ -34,5 +37,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
     auto_save::register_hooks(&handlers);
     diagnostics::register_hooks(&handlers);
     snippet::register_hooks(&handlers);
+    blame::register_hooks(&handlers);
     handlers
 }
diff --git a/helix-term/src/handlers/blame.rs b/helix-term/src/handlers/blame.rs
new file mode 100644
index 00000000..50f1e842
--- /dev/null
+++ b/helix-term/src/handlers/blame.rs
@@ -0,0 +1,61 @@
+#![allow(dead_code, unused_variables)]
+use helix_event::{register_hook, send_blocking};
+use helix_view::{
+    handlers::{BlameEvent, Handlers},
+    Editor,
+};
+
+use crate::{events::PostCommand, job};
+
+pub struct BlameHandler;
+
+impl helix_event::AsyncHook for BlameHandler {
+    type Event = BlameEvent;
+
+    fn handle_event(
+        &mut self,
+        event: Self::Event,
+        timeout: Option<tokio::time::Instant>,
+    ) -> Option<tokio::time::Instant> {
+        self.finish_debounce();
+        None
+    }
+
+    fn finish_debounce(&mut self) {
+        job::dispatch_blocking(move |editor, _| {
+            request_git_blame(editor);
+        })
+    }
+}
+
+pub(super) fn register_hooks(handlers: &Handlers) {
+    let tx = handlers.blame.clone();
+    register_hook!(move |event: &mut PostCommand<'_, '_>| {
+        if event.cx.editor.config().vcs.blame {
+            send_blocking(&tx, BlameEvent::PostCommand);
+        }
+
+        Ok(())
+    });
+}
+
+fn request_git_blame(editor: &mut Editor) {
+    let (view, doc) = current_ref!(editor);
+    let text = doc.text();
+    let selection = doc.selection(view.id);
+    let Some(file) = doc.path() else {
+        return;
+    };
+    let Ok(cursor_line) = TryInto::<u32>::try_into(
+        text.char_to_line(selection.primary().cursor(doc.text().slice(..))),
+    ) else {
+        return;
+    };
+
+    let output = editor.diff_providers.blame_line(file, cursor_line);
+
+    match output {
+        Ok(blame) => editor.set_status(blame.to_string()),
+        Err(err) => editor.set_error(err.to_string()),
+    }
+}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index cdc48a54..7008be51 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -171,6 +171,19 @@ impl Default for GutterLineNumbersConfig {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+pub struct VersionControlConfig {
+    /// Whether to enable git blame
+    pub blame: bool,
+}
+
+impl Default for VersionControlConfig {
+    fn default() -> Self {
+        Self { blame: true }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
 pub struct FilePickerConfig {
@@ -366,6 +379,8 @@ pub struct Config {
     pub end_of_line_diagnostics: DiagnosticFilter,
     // Set to override the default clipboard provider
     pub clipboard_provider: ClipboardProvider,
+    /// Version control
+    pub vcs: VersionControlConfig,
 }
 
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@@ -1009,6 +1024,7 @@ impl Default for Config {
             inline_diagnostics: InlineDiagnosticsConfig::default(),
             end_of_line_diagnostics: DiagnosticFilter::Disable,
             clipboard_provider: ClipboardProvider::default(),
+            vcs: VersionControlConfig::default(),
         }
     }
 }
diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs
index a26c4ddb..0bc310b6 100644
--- a/helix-view/src/handlers.rs
+++ b/helix-view/src/handlers.rs
@@ -16,11 +16,17 @@ pub enum AutoSaveEvent {
     LeftInsertMode,
 }
 
+#[derive(Debug)]
+pub enum BlameEvent {
+    PostCommand,
+}
+
 pub struct Handlers {
     // only public because most of the actual implementation is in helix-term right now :/
     pub completions: CompletionHandler,
     pub signature_hints: Sender<lsp::SignatureHelpEvent>,
     pub auto_save: Sender<AutoSaveEvent>,
+    pub blame: Sender<BlameEvent>,
 }
 
 impl Handlers {