feat(vc-gutter): add +diff-hl backend

This adds an alternative backend to the :ui vc-gutter module, enabled
with the +diff-hl flag. In the future, I intend for diff-hl to replace
git-gutter, as it is slightly faster and depends on more native
functionality (vc.el), but it's still a little buggy. It will remain
opt-in until those issues are sorted out.
This commit is contained in:
Henrik Lissner 2022-08-08 17:46:31 +02:00
parent cd9bc5a1fd
commit 27a448b04b
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
10 changed files with 137 additions and 89 deletions

View file

@ -1,20 +1,12 @@
;;; ui/vc-gutter/config.el -*- lexical-binding: t; -*-
;; TODO Implement me
(defvar +vc-gutter-in-margin nil
"If non-nil, use the margin for diffs instead of the fringe.")
(defvar +vc-gutter-in-remote-files nil
"If non-nil, enable the vc gutter in remote files (e.g. open through TRAMP).")
(defvar +vc-gutter-diff-unsaved-buffer nil
"If non-nil, `diff-hl-flydiff-mode' will be activated. This allows on-the-fly
diffing, even for unsaved buffers.")
(defvar +vc-gutter-default-style t
"If non-nil, enable the default look of the vc gutter.
This means subtle thin bitmaps on the left, an arrow bitmap for flycheck, and
flycheck indicators moved to the right fringe.")
;;
;;; Default styles
@ -31,13 +23,29 @@ flycheck indicators moved to the right fringe.")
;; to shrink the fringe and sacrifice precious space for other fringe
;; indicators (like flycheck or flyspell).
;; TODO Extract these into a package with faces that themes can target.
(after! git-gutter-fringe
(define-fringe-bitmap 'git-gutter-fr:added [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:modified [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240]
nil nil 'bottom))
(if (not (featurep! +diff-hl))
(after! git-gutter-fringe
(define-fringe-bitmap 'git-gutter-fr:added [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:modified [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240]
nil nil 'bottom))
(defadvice! +vc-gutter-define-thin-bitmaps-a (&rest args)
:override #'diff-hl-define-bitmaps
(set-face-background 'diff-hl-insert nil)
(set-face-background 'diff-hl-delete nil)
(set-face-background 'diff-hl-change nil)
(define-fringe-bitmap 'diff-hl-bmp-middle [224] nil nil '(center repeated))
(define-fringe-bitmap 'diff-hl-bmp-delete [240 224 192 128] nil nil 'top))
(defun +vc-gutter-type-face-fn (type _pos)
(intern (format "diff-hl-%s" type)))
(defun +vc-gutter-type-at-pos-fn (type _pos)
(if (eq type 'delete)
'diff-hl-bmp-delete
'diff-hl-bmp-middle))
(setq diff-hl-fringe-bmp-function #'+vc-gutter-type-at-pos-fn
diff-hl-draw-borders nil))
;; FIX: To minimize overlap between flycheck indicators and git-gutter/diff-hl
;; indicators in the left fringe.
@ -50,9 +58,10 @@ flycheck indicators moved to the right fringe.")
;;
;;; Packages
;;; git-gutter
(use-package! git-gutter
:unless (featurep! +diff-hl)
:commands git-gutter:revert-hunk git-gutter:stage-hunk
:init
(add-hook! 'find-file-hook
@ -129,26 +138,69 @@ is deferred until the file is saved. Respects `git-gutter:disabled-modes'."
:from-end is-reverse)))
;; subtle diff indicators in the fringe
(after! git-gutter-fringe
(when +vc-gutter-default-style
;; standardize default fringe width
(if (fboundp 'fringe-mode) (fringe-mode '4))
;;
;;; diff-hl
;; places the git gutter outside the margins.
(setq-default fringes-outside-margins t)
;; thin fringe bitmaps
(define-fringe-bitmap 'git-gutter-fr:added [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:modified [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240]
nil nil 'bottom)))
(use-package! diff-hl
:when (featurep! +diff-hl)
:hook (doom-first-file . global-diff-hl-mode)
:hook (diff-hl-mode . diff-hl-flydiff-mode)
:config
(set-popup-rule! "^\\*diff-hl" :select nil :size '+popup-shrink-to-fit)
(after! flycheck
(when +vc-gutter-default-style
;; let diff have left fringe, flycheck can have right fringe
(setq flycheck-indication-mode 'right-fringe)
;; A non-descript, left-pointing arrow
(define-fringe-bitmap 'flycheck-fringe-bitmap-double-arrow
[16 48 112 240 112 48 16] nil nil 'center)))
;; PERF: reduce load on remote
(defvaralias 'diff-hl-disable-on-remote '+vc-gutter-in-remote-files)
;; PERF: A slightly faster algorithm for diffing.
(setq vc-git-diff-switches '("--histogram"))
;; PERF: Slightly more conservative delay before updating the diff
(setq diff-hl-flydiff-delay 0.5) ; default: 0.3
;; UX: get realtime feedback in diffs after staging/unstaging hunks.
(setq diff-hl-show-staged-changes nil)
;; UX: Update diffs when it makes sense too, without being too slow
(when (featurep! :editor evil)
(map! :after diff-hl-show-hunk
:map diff-hl-show-hunk-map
:n "p" #'diff-hl-show-hunk-previous
:n "n" #'diff-hl-show-hunk-next
:n "c" #'diff-hl-show-hunk-copy-original-text
:n "r" #'diff-hl-show-hunk-revert-hunk
:n "[" #'diff-hl-show-hunk-previous
:n "]" #'diff-hl-show-hunk-next
:n "{" #'diff-hl-show-hunk-previous
:n "}" #'diff-hl-show-hunk-next
:n "S" #'diff-hl-show-hunk-stage-hunk))
;; UX: Refresh git-gutter on ESC or refocusing the Emacs frame.
(add-hook! '(doom-escape-hook doom-switch-window-hook) :append
(defun +vc-gutter-update-h (&rest _)
"Return nil to prevent shadowing other `doom-escape-hook' hooks."
(ignore (or inhibit-redisplay
(and (or (bound-and-true-p diff-hl-mode)
(bound-and-true-p diff-hl-dir-mode))
(diff-hl-update-once))))))
;; UX: Update diff-hl when magit alters git state.
(when (featurep! :tools magit)
(add-hook 'magit-pre-refresh-hook #'diff-hl-magit-pre-refresh)
(add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh)
(add-hook 'magit-post-stage-hook #'+vc-gutter-update-h)
(add-hook 'magit-post-unstage-hook #'+vc-gutter-update-h))
;; UX: Don't delete the current hunk's indicators while we're editing
(add-hook! 'diff-hl-flydiff-mode-hook
(defun +vc-gutter-init-flydiff-mode-h ()
(if diff-hl-flydiff-mode
(progn
(advice-remove #'diff-hl-overlay-modified #'ignore)
(when (featurep! :editor evil)
(add-hook 'evil-insert-state-exit-hook #'diff-hl-flydiff-update)))
(remove-hook 'evil-insert-state-exit-hook #'diff-hl-flydiff-update))))
;; FIX: Reverting a hunk causes the cursor to be moved to an unexpected place,
;; often far from the target hunk.
(defadvice! +vc-gutter--save-excursion-a (fn &rest args)
"Suppresses unexpected cursor movement by `diff-hl-revert-hunk'."
:around #'diff-hl-revert-hunk
(let ((pt (point)))
(prog1 (apply fn args)
(goto-char pt)))))