/ diff-hl-dired.el
diff-hl-dired.el
  1  ;;; diff-hl-dired.el --- Highlight changed files in Dired -*- lexical-binding: t -*-
  2  
  3  ;; Copyright (C) 2012-2017, 2023  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  ;; To enable in all Dired buffers, add this to your init file:
 23  ;;
 24  ;; (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
 25  ;;
 26  ;; or
 27  ;;
 28  ;; (add-hook 'dired-mode-hook 'diff-hl-dired-mode-unless-remote)
 29  ;;
 30  ;; to do it only in local Dired buffers.
 31  
 32  ;;; Code:
 33  
 34  (require 'diff-hl)
 35  (require 'dired)
 36  (require 'vc-hooks)
 37  
 38  (defvar diff-hl-dired-process-buffer nil)
 39  
 40  (defgroup diff-hl-dired nil
 41    "VC diff highlighting on the side of a Dired window."
 42    :group 'diff-hl)
 43  
 44  (defface diff-hl-dired-insert
 45    '((default :inherit diff-hl-insert))
 46    "Face used to highlight added files.")
 47  
 48  (defface diff-hl-dired-delete
 49    '((default :inherit diff-hl-delete))
 50    "Face used to highlight directories with deleted files.")
 51  
 52  (defface diff-hl-dired-change
 53    '((default :inherit diff-hl-change))
 54    "Face used to highlight changed files.")
 55  
 56  (defface diff-hl-dired-unknown
 57    '((default :inherit dired-ignored))
 58    "Face used to highlight unregistered files.")
 59  
 60  (defface diff-hl-dired-ignored
 61    '((default :inherit dired-ignored))
 62    "Face used to highlight unregistered files.")
 63  
 64  (defcustom diff-hl-dired-extra-indicators t
 65    "Non-nil to indicate ignored files."
 66    :type 'boolean)
 67  
 68  (defcustom diff-hl-dired-ignored-backends '(RCS)
 69    "VC backends to ignore.
 70  The directories registered to one of these backends won't have
 71  status indicators."
 72    :type `(repeat (choice ,@(mapcar
 73                              (lambda (name)
 74                                `(const :tag ,(symbol-name name) ,name))
 75                              vc-handled-backends))))
 76  
 77  (defcustom diff-hl-dired-fringe-bmp-function 'diff-hl-fringe-bmp-from-type
 78    "Function to determine fringe bitmap from change type and position."
 79    :type 'function)
 80  
 81  ;;;###autoload
 82  (define-minor-mode diff-hl-dired-mode
 83    "Toggle VC diff highlighting on the side of a Dired window."
 84    :lighter ""
 85    (if diff-hl-dired-mode
 86        (progn
 87          (diff-hl-maybe-define-bitmaps)
 88          (set (make-local-variable 'diff-hl-dired-process-buffer) nil)
 89          (add-hook 'dired-after-readin-hook 'diff-hl-dired-update nil t))
 90      (remove-hook 'dired-after-readin-hook 'diff-hl-dired-update t)
 91      (diff-hl-dired-clear)))
 92  
 93  (defun diff-hl-dired-update ()
 94    "Highlight the Dired buffer."
 95    (let ((backend (ignore-errors (vc-responsible-backend default-directory)))
 96          (def-dir default-directory)
 97          (buffer (current-buffer))
 98          dirs-alist files-alist)
 99      (when (and backend (not (memq backend diff-hl-dired-ignored-backends)))
100        (diff-hl-dired-clear)
101        (if (buffer-live-p diff-hl-dired-process-buffer)
102            (let ((proc (get-buffer-process diff-hl-dired-process-buffer)))
103              (when proc (kill-process proc)))
104          (setq diff-hl-dired-process-buffer
105                (generate-new-buffer " *diff-hl-dired* tmp status")))
106        (with-current-buffer diff-hl-dired-process-buffer
107          (setq default-directory (expand-file-name def-dir))
108          (erase-buffer)
109          (diff-hl-dired-status-files
110           backend def-dir
111           (when diff-hl-dired-extra-indicators
112             (cl-loop for file in (directory-files def-dir)
113                      unless (member file '("." ".." ".hg"))
114                      collect file))
115           (lambda (entries &optional more-to-come)
116             (when (buffer-live-p buffer)
117               (with-current-buffer buffer
118                 (dolist (entry entries)
119                   (cl-destructuring-bind (file state &rest r) entry
120                     ;; Work around http://debbugs.gnu.org/18605
121                     (setq file (replace-regexp-in-string "\\` " "" file))
122                     (let ((type (plist-get
123                                  '( edited change added insert removed delete
124                                     unregistered unknown ignored ignored)
125                                  state)))
126                       (if (string-match "\\`\\([^/]+\\)/" file)
127                           (let* ((dir (match-string 1 file))
128                                  (value (cdr (assoc dir dirs-alist))))
129                             (unless (eq value type)
130                               (cond
131                                ((eq state 'up-to-date))
132                                ((null value)
133                                 (push (cons dir type) dirs-alist))
134                                ((not (eq type 'ignored))
135                                 (setcdr (assoc dir dirs-alist) 'change)))))
136                         (push (cons file type) files-alist)))))
137                 (unless more-to-come
138                   (diff-hl-dired-highlight-items
139                    (append dirs-alist files-alist))))
140               (unless more-to-come
141                 (kill-buffer diff-hl-dired-process-buffer))))
142           )))))
143  
144  (defun diff-hl-dired-status-files (backend dir files update-function)
145    "Using version control BACKEND, return list of (FILE STATE EXTRA) entries
146  for DIR containing FILES. Call UPDATE-FUNCTION as entries are added."
147    (vc-call-backend backend 'dir-status-files dir files update-function))
148  
149  (defun diff-hl-dired-highlight-items (alist)
150    "Highlight ALIST containing (FILE . TYPE) elements."
151    (dolist (pair alist)
152      (let ((file (car pair))
153            (type (cdr pair)))
154        (save-excursion
155          (goto-char (point-min))
156          (when (and type (dired-goto-file-1
157                           file (expand-file-name file) nil))
158            (let* ((diff-hl-fringe-bmp-function diff-hl-dired-fringe-bmp-function)
159                   (diff-hl-fringe-face-function 'diff-hl-dired-face-from-type)
160                   (o (diff-hl-add-highlighting type 'single)))
161              (overlay-put o 'modification-hooks '(diff-hl-overlay-modified))
162              (overlay-put o 'diff-hl-dired-type type)
163              ))))))
164  
165  (defun diff-hl-dired-face-from-type (type _pos)
166    (intern (format "diff-hl-dired-%s" type)))
167  
168  (defalias 'diff-hl-dired-clear 'diff-hl-remove-overlays)
169  
170  ;;;###autoload
171  (defun diff-hl-dired-mode-unless-remote ()
172    (unless (file-remote-p default-directory)
173      (diff-hl-dired-mode)))
174  
175  (provide 'diff-hl-dired)
176  
177  ;;; diff-hl-dired.el ends here