/ diff-hl-margin.el
diff-hl-margin.el
  1  ;;; diff-hl-margin.el --- Highlight buffer changes on margins -*- lexical-binding: t -*-
  2  
  3  ;; Copyright (C) 2012-2025  Free Software Foundation, Inc.
  4  
  5  ;; This file is part of GNU Emacs.
  6  
  7  ;; GNU Emacs is free software: you can redistribute it and/or modify
  8  ;; it under the terms of the GNU General Public License as published by
  9  ;; the Free Software Foundation, either version 3 of the License, or
 10  ;; (at your option) any later version.
 11  
 12  ;; GNU Emacs is distributed in the hope that it will be useful,
 13  ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  ;; GNU General Public License for more details.
 16  
 17  ;; You should have received a copy of the GNU General Public License
 18  ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
 19  
 20  ;;; Commentary:
 21  
 22  ;; This is a global mode, it modifies `diff-hl-mode' to use the margin
 23  ;; instead of the fringe. To toggle, type `M-x diff-hl-margin-mode'.
 24  ;;
 25  ;; Compared to the default behavior, this makes `diff-hl-mode'
 26  ;; indicators show up even when Emacs is running in a terminal.
 27  ;;
 28  ;; On the flip side, the indicators look simpler, and they are
 29  ;; incompatible with `linum-mode' or any other mode that uses the
 30  ;; margin.
 31  ;;
 32  ;; You might want to enable it conditionally in your init file
 33  ;; depending on whether Emacs is running in graphical mode:
 34  ;;
 35  ;; (unless (window-system) (diff-hl-margin-mode))
 36  
 37  (require 'cl-lib)
 38  (require 'diff-hl)
 39  (require 'diff-hl-dired)
 40  
 41  (defvar diff-hl-margin-old-highlight-function nil)
 42  
 43  (defvar diff-hl-margin-old-highlight-ref-function nil)
 44  
 45  (defvar diff-hl-margin-old-width nil)
 46  
 47  (defgroup diff-hl-margin nil
 48    "Highlight buffer changes on margin"
 49    :group 'diff-hl)
 50  
 51  (defface diff-hl-margin-insert
 52    '((default :inherit diff-hl-insert))
 53    "Face used to highlight inserted lines on the margin.")
 54  
 55  (defface diff-hl-margin-delete
 56    '((default :inherit diff-hl-delete))
 57    "Face used to highlight deleted lines on the margin.")
 58  
 59  (defface diff-hl-margin-change
 60    '((default :inherit diff-hl-change))
 61    "Face used to highlight changed lines on the margin.")
 62  
 63  (defface diff-hl-margin-ignored
 64    '((default :inherit dired-ignored))
 65    "Face used to highlight changed lines on the margin.")
 66  
 67  (defface diff-hl-margin-unknown
 68    '((default :inherit dired-ignored))
 69    "Face used to highlight changed lines on the margin.")
 70  
 71  (defface diff-hl-margin-reference-insert
 72    '((default :inherit diff-hl-reference-insert))
 73    "Face used to highlight lines inserted since reference rev on the margin.")
 74  
 75  (defface diff-hl-margin-reference-delete
 76    '((default :inherit diff-hl-reference-delete))
 77    "Face used to highlight lines deleted since reference rev on the margin.")
 78  
 79  (defface diff-hl-margin-reference-change
 80    '((default :inherit diff-hl-reference-change))
 81    "Face used to highlight changed since reference rev on the margin.")
 82  
 83  (defcustom diff-hl-margin-symbols-alist
 84    '((insert . "+") (delete . "-") (change . "!")
 85      (unknown . "?") (ignored . "i") (reference . " "))
 86    "Associative list from symbols to strings."
 87    :type '(alist :key-type symbol
 88                  :value-type string
 89                  :options (insert delete change unknown ignored reference))
 90    :set (lambda (symbol value)
 91           (defvar diff-hl-margin-spec-cache)
 92           (set-default symbol value)
 93           (setq diff-hl-margin-spec-cache nil)))
 94  
 95  ;;;###autoload
 96  (define-minor-mode diff-hl-margin-mode
 97    "Toggle displaying `diff-hl-mode' highlights on the margin."
 98    :lighter "" :global t
 99    (if diff-hl-margin-mode
100        (progn
101          (add-hook 'diff-hl-mode-on-hook 'diff-hl-margin-local-mode)
102          (add-hook 'diff-hl-mode-off-hook 'diff-hl-margin-local-mode-off)
103          (add-hook 'diff-hl-dired-mode-on-hook 'diff-hl-margin-local-mode)
104          (add-hook 'diff-hl-dired-mode-off-hook 'diff-hl-margin-local-mode-off))
105      (remove-hook 'diff-hl-mode-on-hook 'diff-hl-margin-local-mode)
106      (remove-hook 'diff-hl-mode-off-hook 'diff-hl-margin-local-mode-off)
107      (remove-hook 'diff-hl-dired-mode-on-hook 'diff-hl-margin-local-mode)
108      (remove-hook 'diff-hl-dired-mode-off-hook 'diff-hl-margin-local-mode-off))
109    (dolist (buf (buffer-list))
110      (with-current-buffer buf
111        (cond
112         (diff-hl-mode
113          (diff-hl-margin-local-mode (if diff-hl-margin-mode 1 -1))
114          (diff-hl-update))
115         (diff-hl-dired-mode
116          (diff-hl-margin-local-mode (if diff-hl-margin-mode 1 -1))
117          (diff-hl-dired-update))))))
118  
119  ;;;###autoload
120  (define-minor-mode diff-hl-margin-local-mode
121    "Toggle displaying `diff-hl-mode' highlights on the margin locally.
122  You probably shouldn't use this function directly."
123    :lighter ""
124    (let ((width-var (intern (format "%s-margin-width" diff-hl-side))))
125      (if diff-hl-margin-local-mode
126          (progn
127            (setq-local diff-hl-margin-old-highlight-function
128                        diff-hl-highlight-function)
129            (setq-local diff-hl-margin-old-highlight-ref-function
130                        diff-hl-highlight-reference-function)
131            (setq-local diff-hl-highlight-function
132                        #'diff-hl-highlight-on-margin)
133            (setq-local diff-hl-highlight-reference-function
134                        #'diff-hl-highlight-on-margin-flat)
135            (setq-local diff-hl-margin-old-width (symbol-value width-var))
136            (set width-var 1))
137        (when diff-hl-margin-old-highlight-function
138          (setq diff-hl-highlight-function diff-hl-margin-old-highlight-function
139                diff-hl-highlight-reference-function diff-hl-margin-old-highlight-ref-function
140                diff-hl-margin-old-highlight-function nil))
141        (set width-var diff-hl-margin-old-width)
142        (kill-local-variable 'diff-hl-margin-old-width)))
143    (dolist (win (get-buffer-window-list))
144      (set-window-buffer win (current-buffer))))
145  
146  (defun diff-hl-margin-local-mode-off ()
147    (diff-hl-margin-local-mode -1))
148  
149  (defvar diff-hl-margin-spec-cache nil)
150  
151  (defun diff-hl-margin-spec-cache ()
152    (or diff-hl-margin-spec-cache
153        (setq diff-hl-margin-spec-cache
154              (diff-hl-margin-build-spec-cache))))
155  
156  (defun diff-hl-margin-build-spec-cache ()
157    (nconc
158     (cl-loop for (type . char) in diff-hl-margin-symbols-alist
159              unless (eq type 'reference)
160              nconc
161              (cl-loop for side in '(left right)
162                       collect
163                       (cons
164                        (cons type side)
165                        (propertize
166                         " " 'display
167                         `((margin ,(intern (format "%s-margin" side)))
168                           ,(propertize char 'face
169                                        (intern (format "diff-hl-margin-%s" type))))))))
170     (cl-loop for char = (or (assoc-default 'reference diff-hl-margin-symbols-alist)
171                             " ")
172              for type in '(insert delete change)
173              nconc
174              (cl-loop for side in '(left right)
175                       collect
176                       (cons
177                        (list type side 'reference)
178                        (propertize
179                         " " 'display
180                         `((margin ,(intern (format "%s-margin" side)))
181                           ,(propertize char 'face
182                                        (intern (format "diff-hl-margin-reference-%s" type))))))))))
183  
184  (defun diff-hl-margin-ensure-visible ()
185    (let ((width-var (intern (format "%s-margin-width" diff-hl-side))))
186      (when (zerop (symbol-value width-var))
187        (set width-var 1)
188        (dolist (win (get-buffer-window-list))
189          (set-window-buffer win (current-buffer))))))
190  
191  (defun diff-hl-highlight-on-margin (ovl type _shape)
192    (diff-hl-margin-ensure-visible)
193    (let ((spec (cdr (assoc (cons type diff-hl-side)
194                            (diff-hl-margin-spec-cache)))))
195      (overlay-put ovl 'before-string spec)))
196  
197  (defun diff-hl-highlight-on-margin-flat (ovl type _shape)
198    (let ((spec (cdr (assoc (list type diff-hl-side 'reference)
199                            (diff-hl-margin-spec-cache)))))
200      (overlay-put ovl 'before-string spec)))
201  
202  (provide 'diff-hl-margin)
203  
204  ;;; diff-hl-margin.el ends here