Subject: bug#61396: diff mode could distinguish changed from deleted lines

From: Stefan Monnier <[email-masked]>
Date: Tue, 12 Sep 2023 18:11:37 -0400

> - First, `diff -u` (contrary to `diff -c`) does not distinguish between
>   `removed/added` and `modified` lines.   And `diff-mode` currently inherits
>   this weakness.  I think there's a good case to be made for
>   highlighting the "truly added" and "truly removed" lines differently
>   from those that are modified.
>   I'd argue that a "logical" choice would be to highlight them the same
>   way as those parts highlighted by `diff-refine-hunk`
>   (i.e. `diff-refine-removed` and `diff-refine-added`) since that's how
>   refinement would highlight them if we were to ask it to.

The patch below does that for the case of unified diffs.
I kind of like the result.
It's not quite ready for prime time, but I'd be interested to hear what
other people think about it.

        Stefan

diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index e8f22622935..d503652a9cf 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -2314,6 +2308,17 @@ diff-refine-hunk
             (end (progn (diff-end-of-hunk) (point))))
         (diff--refine-hunk beg end)))))
 
+(defun diff--refine-propertize (beg end face)
+  (let ((ol (make-overlay beg end)))
+    (overlay-put ol 'diff-mode 'fine)
+    (overlay-put ol 'evaporate t)
+    (overlay-put ol 'face face)))
+
+(defcustom diff-refine-nonmodified t
+  "If non-nil also highlight the added/removed lines.
+This is currently only implemented for `unified' diffs."
+  :type 'boolean)
+
 (defun diff--refine-hunk (start end)
   (require 'smerge-mode)
   (goto-char start)
@@ -2328,18 +2333,26 @@ diff--refine-hunk
     (goto-char beg)
     (pcase style
       ('unified
-       (while (re-search-forward "^-" end t)
+       (while (re-search-forward "^[-+]" end t)
          (let ((beg-del (progn (beginning-of-line) (point)))
                beg-add end-add)
-           (when (and (diff--forward-while-leading-char ?- end)
-                      ;; Allow for "\ No newline at end of file".
-                      (progn (diff--forward-while-leading-char ?\\ end)
-                             (setq beg-add (point)))
-                      (diff--forward-while-leading-char ?+ end)
-                      (progn (diff--forward-while-leading-char ?\\ end)
-                             (setq end-add (point))))
+           (cond
+            ((eq (char-after) ?+)
+             (diff--forward-while-leading-char ?+ end)
+             (diff--refine-propertize beg-del (point) 'diff-refine-added))
+            ((and (diff--forward-while-leading-char ?- end)
+                  ;; Allow for "\ No newline at end of file".
+                  (progn (diff--forward-while-leading-char ?\\ end)
+                         (setq beg-add (point)))
+                  (diff--forward-while-leading-char ?+ end)
+                  (progn (diff--forward-while-leading-char ?\\ end)
+                         (setq end-add (point))))
              (smerge-refine-regions beg-del beg-add beg-add end-add
-                                    nil #'diff-refine-preproc props-r props-a)))))
+                                    nil #'diff-refine-preproc props-r props-a))
+            (t ;; If we're here, it's because
+             ;; (diff--forward-while-leading-char ?+ end) failed.
+             (diff--refine-propertize beg-del (point) 'diff-refine-removed)
+             )))))
       ('context
        (let* ((middle (save-excursion (re-search-forward "^---" end t)))
               (other middle))