From 27a448b04b8173038eb2c2592298fb37451eb227 Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Mon, 8 Aug 2022 17:46:31 +0200 Subject: [PATCH] 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. --- modules/config/default/+emacs-bindings.el | 8 +- modules/config/default/+evil-bindings.el | 8 +- modules/editor/evil/config.el | 4 +- modules/emacs/dired/config.el | 11 +- modules/ui/vc-gutter/README.org | 7 +- modules/ui/vc-gutter/autoload.el | 34 ------ modules/ui/vc-gutter/autoload/diff-hl.el | 11 ++ modules/ui/vc-gutter/autoload/git-gutter.el | 11 ++ modules/ui/vc-gutter/config.el | 128 ++++++++++++++------ modules/ui/vc-gutter/packages.el | 4 +- 10 files changed, 137 insertions(+), 89 deletions(-) delete mode 100644 modules/ui/vc-gutter/autoload.el create mode 100644 modules/ui/vc-gutter/autoload/diff-hl.el create mode 100644 modules/ui/vc-gutter/autoload/git-gutter.el diff --git a/modules/config/default/+emacs-bindings.el b/modules/config/default/+emacs-bindings.el index 6bac7a5bc..a7f6e0968 100644 --- a/modules/config/default/+emacs-bindings.el +++ b/modules/config/default/+emacs-bindings.el @@ -349,11 +349,11 @@ :desc "Kill link to remote" "y" #'+vc/browse-at-remote-kill :desc "Kill link to homepage" "Y" #'+vc/browse-at-remote-kill-homepage (:when (featurep! :ui vc-gutter) - :desc "Git revert hunk" "r" #'git-gutter:revert-hunk - :desc "Git stage hunk" "s" #'git-gutter:stage-hunk + :desc "Git revert hunk" "r" #'+vc-gutter/revert-hunk + :desc "Git stage hunk" "s" #'+vc-gutter/stage-hunk :desc "Git time machine" "t" #'git-timemachine-toggle - :desc "Jump to next hunk" "n" #'git-gutter:next-hunk - :desc "Jump to previous hunk" "p" #'git-gutter:previous-hunk) + :desc "Jump to next hunk" "n" #'+vc-gutter/next-hunk + :desc "Jump to previous hunk" "p" #'+vc-gutter/previous-hunk) (:when (featurep! :tools magit) :desc "Magit dispatch" "/" #'magit-dispatch :desc "Magit file dispatch" "." #'magit-file-dispatch diff --git a/modules/config/default/+evil-bindings.el b/modules/config/default/+evil-bindings.el index d28239877..7cc3f7fb3 100644 --- a/modules/config/default/+evil-bindings.el +++ b/modules/config/default/+evil-bindings.el @@ -452,11 +452,11 @@ (:when (featurep! :ui vc-gutter) (:when (featurep! :ui hydra) :desc "VCGutter" "." #'+vc/gutter-hydra/body) - :desc "Revert hunk" "r" #'git-gutter:revert-hunk - :desc "Git stage hunk" "s" #'git-gutter:stage-hunk + :desc "Revert hunk at point" "r" #'+vc-gutter/revert-hunk + :desc "stage hunk at point" "s" #'+vc-gutter/stage-hunk :desc "Git time machine" "t" #'git-timemachine-toggle - :desc "Jump to next hunk" "]" #'git-gutter:next-hunk - :desc "Jump to previous hunk" "[" #'git-gutter:previous-hunk) + :desc "Jump to next hunk" "]" #'+vc-gutter/next-hunk + :desc "Jump to previous hunk" "[" #'+vc-gutter/previous-hunk) (:when (featurep! :tools magit) :desc "Magit dispatch" "/" #'magit-dispatch :desc "Magit file dispatch" "." #'magit-file-dispatch diff --git a/modules/editor/evil/config.el b/modules/editor/evil/config.el index 12823896a..a7e603cf6 100644 --- a/modules/editor/evil/config.el +++ b/modules/editor/evil/config.el @@ -445,8 +445,8 @@ directives. By default, this only recognizes C directives.") :m "]x" #'+web:encode-html-entities :m "[x" #'+web:decode-html-entities) (:when (featurep! :ui vc-gutter) - :m "]d" #'git-gutter:next-hunk - :m "[d" #'git-gutter:previous-hunk) + :m "]d" #'+vc-gutter/next-hunk + :m "[d" #'+vc-gutter/previous-hunk) (:when (featurep! :ui hl-todo) :m "]t" #'hl-todo-next :m "[t" #'hl-todo-previous) diff --git a/modules/emacs/dired/config.el b/modules/emacs/dired/config.el index 2d6c18272..66d8aac73 100644 --- a/modules/emacs/dired/config.el +++ b/modules/emacs/dired/config.el @@ -72,11 +72,12 @@ Fixes #3939: unsortable dired entries on Windows." (use-package! diff-hl - :hook (dired-mode . diff-hl-dired-mode-unless-remote) - :hook (magit-post-refresh . diff-hl-magit-post-refresh) - :config - ;; use margin instead of fringe - (diff-hl-margin-mode)) + :when (featurep! :ui vc-gutter) + :hook (dired-mode-hook . diff-hl-margin-local-mode) + :init + (unless (featurep! :ui vc-gutter +diff-hl) + (add-hook 'dired-mode-hook #'diff-hl-dired-mode-unless-remote) + (add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh))) (use-package! ranger diff --git a/modules/ui/vc-gutter/README.org b/modules/ui/vc-gutter/README.org index a199596ef..dbfd0018d 100644 --- a/modules/ui/vc-gutter/README.org +++ b/modules/ui/vc-gutter/README.org @@ -13,6 +13,10 @@ Supports Git, Svn, Hg, and Bzr. [[doom-contrib-maintainer:][Become a maintainer?]] ** Module flags +- +diff-hl :: + Use [[doom-package:][diff-hl]] instead of git-gutter to power the VC gutter. It is a little + faster, but is slightly more prone to visual glitching. [[doom-package:][diff-hl]] is intended to + replace git-gutter at some point in the future. - +pretty :: Apply some stylistic defaults to the fringe, enabling thin bars in the fringe. This look takes after the modern look of git-gutter in VSCode and Sublime @@ -22,7 +26,8 @@ Supports Git, Svn, Hg, and Bzr. diff-hl's faces (like modus-themes does). ** Packages -- [[doom-package:][git-gutter-fringe]] +- [[doom-package:][git-gutter-fringe]] unless [[doom-module:][+diff-hl]] +- [[doom-package:][diff-hl]] if [[doom-module:][+diff-hl]] ** TODO Hacks #+begin_quote diff --git a/modules/ui/vc-gutter/autoload.el b/modules/ui/vc-gutter/autoload.el deleted file mode 100644 index 28d9de652..000000000 --- a/modules/ui/vc-gutter/autoload.el +++ /dev/null @@ -1,34 +0,0 @@ -;;; ui/vc-gutter/autoload.el -*- lexical-binding: t; -*- -;;;###if (featurep! :ui hydra) - -;;;###autoload (autoload '+vc/gutter-hydra/body "ui/vc-gutter/autoload" nil t) -(defhydra +vc/gutter-hydra - (:body-pre (git-gutter-mode 1) :hint nil) - " - [git gutter] - Movement Hunk Actions Misc. +%-4s(car (git-gutter:statistic))/ -%-4s(cdr (git-gutter:statistic)) - ╭──────────────────────────────────┴────────────────╯ - ^_g_^ [_s_] stage [_R_] set start Rev - ^_k_^ [_r_] revert - ^↑ ^ [_m_] mark - ^↓ ^ [_p_] popup ╭───────────────────── - ^_j_^ │[_q_] quit - ^_G_^ │[_Q_] Quit and disable" - ("j" (progn (git-gutter:next-hunk 1) (recenter))) - ("k" (progn (git-gutter:previous-hunk 1) (recenter))) - ("g" (progn (goto-char (point-min)) (git-gutter:next-hunk 1))) - ("G" (progn (goto-char (point-min)) (git-gutter:previous-hunk 1))) - ("s" git-gutter:stage-hunk) - ("r" git-gutter:revert-hunk) - ("m" git-gutter:mark-hunk) - ("p" git-gutter:popup-hunk) - ("R" git-gutter:set-start-revision) - ("q" - (when (get-buffer git-gutter:popup-buffer) - (kill-buffer (get-buffer git-gutter:popup-buffer))) - :color blue) - ("Q" - (progn (git-gutter-mode -1) - (when (get-buffer git-gutter:popup-buffer) - (kill-buffer (get-buffer git-gutter:popup-buffer)))) - :color blue)) diff --git a/modules/ui/vc-gutter/autoload/diff-hl.el b/modules/ui/vc-gutter/autoload/diff-hl.el new file mode 100644 index 000000000..f22f8aab5 --- /dev/null +++ b/modules/ui/vc-gutter/autoload/diff-hl.el @@ -0,0 +1,11 @@ +;;; ui/vc-gutter/autoload/diff-hl.el -*- lexical-binding: t; -*- +;;;###if (featurep! +diff-hl) + +;;;###autoload +(defalias '+vc-gutter/stage-hunk #'diff-hl-stage-current-hunk) +;;;###autoload +(defalias '+vc-gutter/revert-hunk #'diff-hl-revert-hunk) +;;;###autoload +(defalias '+vc-gutter/next-hunk #'diff-hl-next-hunk) +;;;###autoload +(defalias '+vc-gutter/previous-hunk #'diff-hl-previous-hunk) diff --git a/modules/ui/vc-gutter/autoload/git-gutter.el b/modules/ui/vc-gutter/autoload/git-gutter.el new file mode 100644 index 000000000..64fbedfad --- /dev/null +++ b/modules/ui/vc-gutter/autoload/git-gutter.el @@ -0,0 +1,11 @@ +;;; ui/vc-gutter/autoload/vc-gutter.el -*- lexical-binding: t; -*- +;;;###if (not (featurep! +diff-hl)) + +;;;###autoload +(defalias '+vc-gutter/stage-hunk #'git-gutter:stage-hunk) +;;;###autoload +(defalias '+vc-gutter/revert-hunk #'git-gutter:revert-hunk) +;;;###autoload +(defalias '+vc-gutter/next-hunk #'git-gutter:next-hunk) +;;;###autoload +(defalias '+vc-gutter/previous-hunk #'git-gutter:previous-hunk) diff --git a/modules/ui/vc-gutter/config.el b/modules/ui/vc-gutter/config.el index fb51b2190..475fcb443 100644 --- a/modules/ui/vc-gutter/config.el +++ b/modules/ui/vc-gutter/config.el @@ -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))))) diff --git a/modules/ui/vc-gutter/packages.el b/modules/ui/vc-gutter/packages.el index 9cb2cae70..a07b00947 100644 --- a/modules/ui/vc-gutter/packages.el +++ b/modules/ui/vc-gutter/packages.el @@ -1,4 +1,6 @@ ;; -*- no-byte-compile: t; -*- ;;; ui/vc-gutter/packages.el -(package! git-gutter-fringe :pin "648cb5b57faec55711803cdc9434e55a733c3eba") +(if (featurep! +diff-hl) + (package! diff-hl :pin "dabb7be6283488abd8d232ea8ce590d502713ed8") + (package! git-gutter-fringe :pin "648cb5b57faec55711803cdc9434e55a733c3eba"))