From f9b739fdd13284e8ebae173b1f91d7e2b5ab1880 Mon Sep 17 00:00:00 2001 From: Paul Nelson Date: Sun, 21 Dec 2025 22:55:18 +0100 Subject: [PATCH] Add Ediff support Add the command 'diff-hl-ediff-current-hunk', bound to 'C-x v e' in 'diff-hl-mode-map' and 'e' in the show-hunk UI, that uses Ediff to compare against the reference revision, jumping to the hunk nearest point. * diff-hl.el (diff-hl--use-git-index-base-p): New helper function. (diff-hl-diff-against-reference, diff-hl-diff-buffer-with-reference): Use it. (diff-hl--ediff-reference-buffer): New helper function. (diff-hl-ediff-current-hunk): New command. (diff-hl-command-map): Bind 'e' to 'diff-hl-ediff-current-hunk'. (diff-hl-repeat-exceptions): Add 'diff-hl-ediff-current-hunk'. * diff-hl-show-hunk.el (diff-hl-show-hunk-ediff): New command. (diff-hl-show-hunk-map): Bind it to 'e'. * diff-hl-show-hunk-inline.el (diff-hl-show-hunk-inline): Update footer text to include Ediff keybinding. * diff-hl-show-hunk-posframe.el (diff-hl-show-hunk-posframe--header-line): Add Ediff button to the posframe header line. --- diff-hl-show-hunk-inline.el | 2 +- diff-hl-show-hunk-posframe.el | 5 +++ diff-hl-show-hunk.el | 7 ++++ diff-hl.el | 72 +++++++++++++++++++++++++++++++---- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/diff-hl-show-hunk-inline.el b/diff-hl-show-hunk-inline.el index 3b5214bb..bea0a47b 100644 --- a/diff-hl-show-hunk-inline.el +++ b/diff-hl-show-hunk-inline.el @@ -361,7 +361,7 @@ BUFFER is a buffer with the hunk." (when smart-lines (when (not (eq 0 original-lines-number)) original-lines-number))) - (footer "(q)Quit (p)Previous (n)Next (r)Revert (c)Copy original")) + (footer "(q)Quit (p)Previous (n)Next (e)Ediff (r)Revert (c)Copy original")) (unless diff-hl-show-staged-changes (setq footer (concat footer " (S)Stage"))) (diff-hl-show-hunk-inline-show diff --git a/diff-hl-show-hunk-posframe.el b/diff-hl-show-hunk-posframe.el index 48ea54b4..6be657bc 100644 --- a/diff-hl-show-hunk-posframe.el +++ b/diff-hl-show-hunk-posframe.el @@ -145,6 +145,11 @@ The button calls an ACTION." "Copy original (\\[diff-hl-show-hunk-copy-original-text])" #'diff-hl-show-hunk-copy-original-text) + (diff-hl-show-hunk--posframe-button + "⇄ Ediff" + "Ediff (\\[diff-hl-show-hunk-ediff])" + #'diff-hl-show-hunk-ediff) + (diff-hl-show-hunk--posframe-button "♻ Revert hunk" "Revert hunk (\\[diff-hl-show-hunk-revert-hunk])" diff --git a/diff-hl-show-hunk.el b/diff-hl-show-hunk.el index 641683d7..e1f23978 100644 --- a/diff-hl-show-hunk.el +++ b/diff-hl-show-hunk.el @@ -219,6 +219,7 @@ Returns a list with the buffer and the line number of the clicked line." (define-key map (kbd "p") #'diff-hl-show-hunk-previous) (define-key map (kbd "n") #'diff-hl-show-hunk-next) (define-key map (kbd "c") #'diff-hl-show-hunk-copy-original-text) + (define-key map (kbd "e") #'diff-hl-show-hunk-ediff) (define-key map (kbd "r") #'diff-hl-show-hunk-revert-hunk) (define-key map (kbd "[") #'diff-hl-show-hunk-previous) (define-key map (kbd "]") #'diff-hl-show-hunk-next) @@ -246,6 +247,12 @@ Returns a list with the buffer and the line number of the clicked line." (diff-hl-show-hunk-hide) (diff-hl-stage-current-hunk)) +(defun diff-hl-show-hunk-ediff () + "Dismiss the popup and run Ediff for the current hunk." + (interactive) + (diff-hl-show-hunk-hide) + (diff-hl-ediff-current-hunk)) + ;;;###autoload (defun diff-hl-show-hunk-previous () "Go to previous hunk/change and show it." diff --git a/diff-hl.el b/diff-hl.el index 6f3f1377..f7fe1cf4 100644 --- a/diff-hl.el +++ b/diff-hl.el @@ -36,6 +36,7 @@ ;; `diff-hl-previous-hunk' C-x v [ ;; `diff-hl-next-hunk' C-x v ] ;; `diff-hl-show-hunk' C-x v * +;; `diff-hl-ediff-current-hunk' C-x v e ;; `diff-hl-stage-current-hunk' C-x v S ;; `diff-hl-set-reference-rev' ;; `diff-hl-reset-reference-rev' @@ -432,6 +433,17 @@ It can be a relative expression as well, such as \"HEAD^\" with Git, or (declare-function vc-git--rev-parse "vc-git") (declare-function vc-hg-command "vc-hg") (declare-function vc-bzr-command "vc-bzr") +(declare-function vc-find-revision-no-save "vc") +(declare-function ediff-buffers "ediff") +(declare-function ediff-diff-at-point "ediff-util") +(declare-function ediff-jump-to-difference "ediff-util") +(defvar ediff-number-of-differences) + +(defun diff-hl--use-git-index-base-p (backend) + "Whether diff-hl should use the Git index as the reference base." + (and (eq backend 'Git) + (not diff-hl-reference-revision) + (not diff-hl-show-staged-changes))) (defun diff-hl-changes-buffer (file backend &optional new-rev bufname) (diff-hl-with-diff-switches @@ -440,9 +452,7 @@ It can be a relative expression as well, such as \"HEAD^\" with Git, or (defun diff-hl-diff-against-reference (file backend buffer &optional new-rev) (cond ((and (not new-rev) - (not diff-hl-reference-revision) - (not diff-hl-show-staged-changes) - (eq backend 'Git)) + (diff-hl--use-git-index-base-p backend)) (apply #'vc-git-command buffer (if (diff-hl--use-async-p) 'async 1) (list file) @@ -902,6 +912,54 @@ buffer will show the position corresponding to its current line." (diff-hl-diff-skip-to line relname) (setq vc-sentinel-movepoint (point)))))))) +(defun diff-hl--ediff-reference-buffer (file) + "Return the reference buffer for FILE used in Ediff." + (unless file + (user-error "No current file")) + (let ((backend (vc-backend file))) + (unless backend + (user-error "The buffer is not under version control")) + (let* ((reference diff-hl-reference-revision) + ;; Use the index snapshot only when diff-hl hides staged changes. + (use-index (and (diff-hl--use-git-index-base-p backend) + (not diff-hl-highlight-reference-function))) + (buf + (if use-index + (let ((obj (diff-hl-git-index-object-name file))) + (unless obj + (user-error "No index entry for %s" file)) + (let ((filename (diff-hl-git-index-revision file obj))) + (find-file-noselect filename))) + (let ((rev (or reference + (assoc-default backend diff-hl-head-revision-alist) + (diff-hl-working-revision file backend)))) + (unless rev + (user-error "No reference revision specified")) + (setq rev (diff-hl-resolved-revision backend rev)) + (vc-find-revision-no-save file rev backend))))) + (with-current-buffer buf + (set-buffer-modified-p nil) + (read-only-mode 1)) + buf))) + +;;;###autoload +(defun diff-hl-ediff-current-hunk () + "Run Ediff against current comparison base. Jump to hunk at point." + (interactive) + (require 'ediff) + (let* ((pos (point)) + (file (or buffer-file-name + (and-let* ((base (buffer-base-buffer))) + (buffer-file-name base)))) + (refbuf (diff-hl--ediff-reference-buffer file)) + (startup + (list + (lambda () + (unless (zerop ediff-number-of-differences) + (ediff-jump-to-difference + (max 1 (ediff-diff-at-point 'B pos)))))))) + (ediff-buffers refbuf (current-buffer) startup 'diff-hl-ediff))) + (defun diff-hl-diff-read-revisions (rev1-default) (let* ((file buffer-file-name) (files (list file)) @@ -1303,6 +1361,7 @@ Pops up a diff buffer that can be edited to choose the changes to stage." (define-key map "[" 'diff-hl-previous-hunk) (define-key map "]" 'diff-hl-next-hunk) (define-key map "*" 'diff-hl-show-hunk) + (define-key map "e" 'diff-hl-ediff-current-hunk) (define-key map "{" 'diff-hl-show-hunk-previous) (define-key map "}" 'diff-hl-show-hunk-next) (define-key map "S" 'diff-hl-stage-dwim) @@ -1375,7 +1434,8 @@ The value of this variable is a mode line template as in (defvar diff-hl-repeat-exceptions '(diff-hl-show-hunk diff-hl-show-hunk-previous - diff-hl-show-hunk-next)) + diff-hl-show-hunk-next + diff-hl-ediff-current-hunk)) (when (require 'smartrep nil t) (declare-function smartrep-define-key 'smartrep) @@ -1515,9 +1575,7 @@ CONTEXT-LINES is the size of the unified diff context, defaults to 0." (backend (or backend (vc-backend file))) (temporary-file-directory diff-hl-temporary-directory) (rev - (if (and (eq backend 'Git) - (not diff-hl-reference-revision) - (not diff-hl-show-staged-changes)) + (if (diff-hl--use-git-index-base-p backend) (diff-hl-git-index-revision file (diff-hl-git-index-object-name file))