* term/x-win.el (x-gtk-stock-map, icon-map-list)
[bpt/emacs.git] / lisp / diff-mode.el
index be4c7e7..34b116d 100644 (file)
@@ -48,8 +48,6 @@
 ;;   Or maybe just make it into a ".rej to diff3-markers converter".
 ;;   Maybe just use `wiggle' (by Neil Brown) to do it for us.
 ;;
-;; - Refine hunk on a word-by-word basis.
-;; 
 ;; - in diff-apply-hunk, strip context in replace-match to better
 ;;   preserve markers and spacing.
 ;; - Handle `diff -b' output in context->unified.
@@ -92,6 +90,10 @@ when editing big diffs)."
   :type 'boolean
   :group 'diff-mode)
 
+(defcustom diff-auto-refine t
+  "Automatically highlight changes in detail as the user visits hunks."
+  :type 'boolean
+  :group 'diff-mode)
 
 (defcustom diff-mode-hook nil
   "Run after setting up the `diff-mode' major mode."
@@ -112,6 +114,8 @@ when editing big diffs)."
     ("N" . diff-file-next)
     ("p" . diff-hunk-prev)
     ("P" . diff-file-prev)
+    ("\t" . diff-hunk-next)
+    ([backtab] . diff-hunk-prev)
     ("k" . diff-hunk-kill)
     ("K" . diff-file-kill)
     ;; From compilation-minor-mode.
@@ -149,13 +153,14 @@ when editing big diffs)."
     ("\C-c\C-a" . diff-apply-hunk)
     ("\C-c\C-e" . diff-ediff-patch)
     ("\C-c\C-n" . diff-restrict-view)
-    ("\C-c\C-r" . diff-reverse-direction)
     ("\C-c\C-s" . diff-split-hunk)
     ("\C-c\C-t" . diff-test-hunk)
+    ("\C-c\C-r" . diff-reverse-direction)
     ("\C-c\C-u" . diff-context->unified)
     ;; `d' because it duplicates the context :-(  --Stef
     ("\C-c\C-d" . diff-unified->context)
-    ("\C-c\C-w" . diff-refine-ignore-spaces-hunk)
+    ("\C-c\C-w" . diff-ignore-whitespace-hunk)
+    ("\C-c\C-b" . diff-refine-hunk)  ;No reason for `b' :-(
     ("\C-c\C-f" . next-error-follow-minor-mode))
   "Keymap for `diff-mode'.  See also `diff-mode-shared-map'.")
 
@@ -173,7 +178,8 @@ when editing big diffs)."
     ;;["Fixup Headers"         diff-fixup-modifs       (not buffer-read-only)]
     "-----"
     ["Split hunk"              diff-split-hunk         (diff-splittable-p)]
-    ["Refine hunk"             diff-refine-ignore-spaces-hunk t]
+    ["Ignore whitespace changes" diff-ignore-whitespace-hunk t]
+    ["Highlight fine changes"  diff-refine-hunk        t]
     ["Kill current hunk"       diff-hunk-kill          t]
     ["Kill current file's hunks" diff-file-kill        t]
     "-----"
@@ -386,12 +392,15 @@ when editing big diffs)."
 (defconst diff-file-header-re (concat "^\\(--- .+\n\\+\\+\\+ \\|\\*\\*\\* .+\n--- \\|[^-+!<>0-9@* ]\\).+\n" (substring diff-hunk-header-re 1)))
 (defvar diff-narrowed-to nil)
 
-(defun diff-end-of-hunk (&optional style)
+(defun diff-hunk-style (&optional style)
   (when (looking-at diff-hunk-header-re)
-    (unless style
-      ;; Especially important for unified (because headers are ambiguous).
-      (setq style (cdr (assq (char-after) '((?@ . unified) (?* . context))))))
+    (setq style (cdr (assq (char-after) '((?@ . unified) (?* . context)))))
     (goto-char (match-end 0)))
+  style)
+
+(defun diff-end-of-hunk (&optional style)
+  ;; Especially important for unified (because headers are ambiguous).
+  (setq style (diff-hunk-style style))
   (let ((end (and (re-search-forward (case style
                                       ;; A `unified' header is ambiguous.
                                       (unified (concat "^[^-+# \\]\\|"
@@ -453,7 +462,10 @@ but in the file header instead, in which case move forward to the first hunk."
 
 ;; Define diff-{hunk,file}-{prev,next}
 (easy-mmode-define-navigation
- diff-hunk diff-hunk-header-re "hunk" diff-end-of-hunk diff-restrict-view)
+ diff-hunk diff-hunk-header-re "hunk" diff-end-of-hunk diff-restrict-view
+ (if diff-auto-refine
+     (condition-case-no-debug nil (diff-refine-hunk) (error nil))))
+
 (easy-mmode-define-navigation
  diff-file diff-file-header-re "file" diff-end-of-hunk)
 
@@ -682,7 +694,9 @@ PREFIX is only used internally: don't use it."
           ((or (null files)
                (setq file (do* ((files files (cdr files))
                                 (file (car files) (car files)))
-                              ((or (null file) (file-exists-p file))
+                              ;; Use file-regular-p to avoid
+                              ;; /dev/null, directories, etc.
+                              ((or (null file) (file-regular-p file))
                                file))))
            file))
        ;; <foo>.rej patches implicitly apply to <foo>
@@ -841,68 +855,89 @@ With a prefix argument, convert unified format to context format."
       (diff-unified->context start end)
     (unless (markerp end) (setq end (copy-marker end t)))
     (let ( ;;(diff-inhibit-after-change t)
-         (inhibit-read-only t))
+          (inhibit-read-only t))
       (save-excursion
-       (goto-char start)
-       (while (and (re-search-forward "^\\(\\(\\*\\*\\*\\) .+\n\\(---\\) .+\\|\\*\\{15\\}.*\n\\*\\*\\* \\([0-9]+\\),\\(-?[0-9]+\\) \\*\\*\\*\\*\\)$" nil t)
-                   (< (point) end))
-         (combine-after-change-calls
-           (if (match-beginning 2)
-               ;; we matched a file header
-               (progn
-                 ;; use reverse order to make sure the indices are kept valid
-                 (replace-match "+++" t t nil 3)
-                 (replace-match "---" t t nil 2))
-             ;; we matched a hunk header
-             (let ((line1s (match-string 4))
-                   (line1e (match-string 5))
-                   (pt1 (match-beginning 0)))
-               (replace-match "")
-               (unless (re-search-forward
-                        "^--- \\([0-9]+\\),\\(-?[0-9]+\\) ----$" nil t)
-                 (error "Can't find matching `--- n1,n2 ----' line"))
-               (let ((line2s (match-string 1))
-                     (line2e (match-string 2))
-                     (pt2 (progn
-                            (delete-region (progn (beginning-of-line) (point))
-                                           (progn (forward-line 1) (point)))
-                            (point-marker))))
-                 (goto-char pt1)
-                 (forward-line 1)
-                 (while (< (point) pt2)
-                   (case (char-after)
-                     ((?! ?-) (delete-char 2) (insert "-") (forward-line 1))
-                     (?\s     ;merge with the other half of the chunk
-                      (let* ((endline2
-                              (save-excursion
-                                (goto-char pt2) (forward-line 1) (point)))
-                             (c (char-after pt2)))
-                        (case c
-                          ((?! ?+)
-                           (insert "+"
-                                   (prog1 (buffer-substring (+ pt2 2) endline2)
-                                     (delete-region pt2 endline2))))
-                          (?\s         ;FIXME: check consistency
-                           (delete-region pt2 endline2)
-                           (delete-char 1)
-                           (forward-line 1))
-                          (?\\ (forward-line 1))
-                          (t (delete-char 1) (forward-line 1)))))
-                     (t (forward-line 1))))
-                 (while (looking-at "[+! ] ")
-                   (if (/= (char-after) ?!) (forward-char 1)
-                     (delete-char 1) (insert "+"))
-                   (delete-char 1) (forward-line 1))
-                 (save-excursion
-                   (goto-char pt1)
-                   (insert "@@ -" line1s ","
-                           (number-to-string (- (string-to-number line1e)
-                                                (string-to-number line1s)
-                                                -1))
-                           " +" line2s ","
-                           (number-to-string (- (string-to-number line2e)
-                                                (string-to-number line2s)
-                                                -1)) " @@")))))))))))
+        (goto-char start)
+        (while (and (re-search-forward "^\\(\\(\\*\\*\\*\\) .+\n\\(---\\) .+\\|\\*\\{15\\}.*\n\\*\\*\\* \\([0-9]+\\),\\(-?[0-9]+\\) \\*\\*\\*\\*\\)$" nil t)
+                    (< (point) end))
+          (combine-after-change-calls
+            (if (match-beginning 2)
+                ;; we matched a file header
+                (progn
+                  ;; use reverse order to make sure the indices are kept valid
+                  (replace-match "+++" t t nil 3)
+                  (replace-match "---" t t nil 2))
+              ;; we matched a hunk header
+              (let ((line1s (match-string 4))
+                    (line1e (match-string 5))
+                    (pt1 (match-beginning 0))
+                    ;; Variables to use the special undo function.
+                    (old-undo buffer-undo-list)
+                    (old-end (marker-position end))
+                    (reversible t))
+                (replace-match "")
+                (unless (re-search-forward
+                         "^--- \\([0-9]+\\),\\(-?[0-9]+\\) ----$" nil t)
+                  (error "Can't find matching `--- n1,n2 ----' line"))
+                (let ((line2s (match-string 1))
+                      (line2e (match-string 2))
+                      (pt2 (progn
+                             (delete-region (progn (beginning-of-line) (point))
+                                            (progn (forward-line 1) (point)))
+                             (point-marker))))
+                  (goto-char pt1)
+                  (forward-line 1)
+                  (while (< (point) pt2)
+                    (case (char-after)
+                      (?! (delete-char 2) (insert "-") (forward-line 1))
+                      (?- (forward-char 1) (delete-char 1) (forward-line 1))
+                      (?\s           ;merge with the other half of the chunk
+                       (let* ((endline2
+                               (save-excursion
+                                 (goto-char pt2) (forward-line 1) (point))))
+                         (case (char-after pt2)
+                           ((?! ?+)
+                            (insert "+"
+                                    (prog1 (buffer-substring (+ pt2 2) endline2)
+                                      (delete-region pt2 endline2))))
+                           (?\s
+                            (unless (= (- endline2 pt2)
+                                       (- (line-beginning-position 2) (point)))
+                              ;; If the two lines we're merging don't have the
+                              ;; same length (can happen with "diff -b"), then
+                              ;; diff-unified->context will not properly undo
+                              ;; this operation.
+                              (setq reversible nil))
+                            (delete-region pt2 endline2)
+                            (delete-char 1)
+                            (forward-line 1))
+                           (?\\ (forward-line 1))
+                           (t (setq reversible nil)
+                              (delete-char 1) (forward-line 1)))))
+                      (t (setq reversible nil) (forward-line 1))))
+                  (while (looking-at "[+! ] ")
+                    (if (/= (char-after) ?!) (forward-char 1)
+                      (delete-char 1) (insert "+"))
+                    (delete-char 1) (forward-line 1))
+                  (save-excursion
+                    (goto-char pt1)
+                    (insert "@@ -" line1s ","
+                            (number-to-string (- (string-to-number line1e)
+                                                 (string-to-number line1s)
+                                                 -1))
+                            " +" line2s ","
+                            (number-to-string (- (string-to-number line2e)
+                                                 (string-to-number line2s)
+                                                 -1)) " @@"))
+                  (set-marker pt2 nil)
+                  ;; The whole procedure succeeded, let's replace the myriad
+                  ;; of undo elements with just a single special one.
+                  (unless (or (not reversible) (eq buffer-undo-list t))
+                    (setq buffer-undo-list
+                          (cons (list 'apply (- old-end end) pt1 (point)
+                                      'diff-unified->context pt1 (point))
+                                old-undo)))
+                  )))))))))
 
 (defun diff-reverse-direction (start end)
   "Reverse the direction of the diffs.
@@ -1215,31 +1250,44 @@ Only works for unified diffs."
 
        ;; A context diff.
        ((eq (char-after) ?*)
-        (if (not (looking-at "\\*\\{15\\}\\(?: .*\\)?\n\\*\\*\\* \\([0-9]+\\),\\([0-9]+\\) \\*\\*\\*\\*"))
+        (if (not (looking-at "\\*\\{15\\}\\(?: .*\\)?\n\\*\\*\\* \\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? \\*\\*\\*\\*"))
             (error "Unrecognized context diff first hunk header format")
           (forward-line 2)
           (diff-sanity-check-context-hunk-half
-           (1+ (- (string-to-number (match-string 2))
-                  (string-to-number (match-string 1)))))
-          (if (not (looking-at "--- \\([0-9]+\\),\\([0-9]+\\) ----$"))
+          (if (match-string 2)
+              (1+ (- (string-to-number (match-string 2))
+                     (string-to-number (match-string 1))))
+            1))
+          (if (not (looking-at "--- \\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? ----$"))
               (error "Unrecognized context diff second hunk header format")
             (forward-line)
             (diff-sanity-check-context-hunk-half
-             (1+ (- (string-to-number (match-string 2))
-                    (string-to-number (match-string 1))))))))
+            (if (match-string 2)
+                (1+ (- (string-to-number (match-string 2))
+                       (string-to-number (match-string 1))))
+              1)))))
 
        ;; A unified diff.
        ((eq (char-after) ?@)
         (if (not (looking-at
-                  "@@ -[0-9]+,\\([0-9]+\\) \\+[0-9]+,\\([0-9]+\\) @@"))
+                  "@@ -[0-9]+\\(?:,\\([0-9]+\\)\\)? \\+[0-9]+\\(?:,\\([0-9]+\\)\\)? @@"))
             (error "Unrecognized unified diff hunk header format")
-          (let ((before (string-to-number (match-string 1)))
-                (after (string-to-number (match-string 2))))
+          (let ((before (if (match-string 1) (string-to-number (match-string 1)) 1))
+                (after (if (match-string 2) (string-to-number (match-string 2)) 1)))
             (forward-line)
             (while
                 (case (char-after)
                   (?\s (decf before) (decf after) t)
-                  (?- (decf before) t)
+                  (?-
+                   (if (and (looking-at diff-file-header-re)
+                            (zerop before) (zerop after))
+                       ;; No need to query: this is a case where two patches
+                       ;; are concatenated and only counting the lines will
+                       ;; give the right result.  Let's just add an empty
+                       ;; line so that our code which doesn't count lines
+                       ;; will not get confused.
+                       (progn (save-excursion (insert "\n")) nil)
+                     (decf before) t))
                   (?+ (decf after) t)
                   (t
                    (cond
@@ -1537,6 +1585,10 @@ then `diff-jump-to-old-file' is also set, for the next invocations."
 (defun diff-current-defun ()
   "Find the name of function at point.
 For use in `add-log-current-defun-function'."
+  ;; Kill change-log-default-name so it gets recomputed each time, since
+  ;; each hunk may belong to another file which may belong to another
+  ;; directory and hence have a different ChangeLog file.
+  (kill-local-variable 'change-log-default-name)
   (save-excursion
     (when (looking-at diff-hunk-header-re)
       (forward-line 1)
@@ -1559,8 +1611,8 @@ For use in `add-log-current-defun-function'."
            (goto-char (+ (car pos) (cdr src)))
            (add-log-current-defun))))))
 
-(defun diff-refine-ignore-spaces-hunk ()
-  "Refine the current hunk by ignoring space differences."
+(defun diff-ignore-whitespace-hunk ()
+  "Re-diff the current hunk, ignoring whitespace differences."
   (interactive)
   (let* ((char-offset (- (point) (progn (diff-beginning-of-hunk 'try-harder)
                                         (point))))
@@ -1604,6 +1656,75 @@ For use in `add-log-current-defun-function'."
       (delete-file file1)
       (delete-file file2))))
 
+;;; Fine change highlighting.
+
+(defface diff-refine-change
+  '((((class color) (min-colors 88) (background light))
+     :background "grey90")
+    (((class color) (min-colors 88) (background dark))
+     :background "grey40")
+    (((class color) (background light))
+     :background "yellow")
+    (((class color) (background dark))
+     :background "green")
+    (t :weight bold))
+  "Face used for char-based changes shown by `diff-refine-hunk'."
+  :group 'diff-mode)
+
+(defun diff-refine-preproc ()
+  (while (re-search-forward "^[+>]" nil t)
+    ;; Remove spurious changes due to the fact that one side of the hunk is
+    ;; marked with leading + or > and the other with leading - or <.
+    ;; We used to replace all the prefix chars with " " but this only worked
+    ;; when we did char-based refinement (or when using
+    ;; smerge-refine-weight-hack) since otherwise, the `forward' motion done
+    ;; in chopup do not necessarily do the same as the ones in highlight
+    ;; since the "_" is not treated the same as " ".
+    (replace-match (cdr (assq (char-before) '((?+ . "-") (?> . "<"))))))
+  )
+
+(defun diff-refine-hunk ()
+  "Highlight changes of hunk at point at a finer granularity."
+  (interactive)
+  (require 'smerge-mode)
+  (save-excursion
+    (diff-beginning-of-hunk 'try-harder)
+    (let* ((style (diff-hunk-style))    ;Skips the hunk header as well.
+           (beg (point))
+           (props '((diff-mode . fine) (face diff-refine-change)))
+           (end (progn (diff-end-of-hunk) (point))))
+
+      (remove-overlays beg end 'diff-mode 'fine)
+
+      (goto-char beg)
+      (case style
+        (unified
+         (while (re-search-forward "^\\(?:-.*\n\\)+\\(\\)\\(?:\\+.*\n\\)+"
+                                   end t)
+           (smerge-refine-subst (match-beginning 0) (match-end 1)
+                                (match-end 1) (match-end 0)
+                                props 'diff-refine-preproc)))
+        (context
+         (let* ((middle (save-excursion (re-search-forward "^---")))
+                (other middle))
+           (while (re-search-forward "^\\(?:!.*\n\\)+" middle t)
+             (smerge-refine-subst (match-beginning 0) (match-end 0)
+                                  (save-excursion
+                                    (goto-char other)
+                                    (re-search-forward "^\\(?:!.*\n\\)+" end)
+                                    (setq other (match-end 0))
+                                    (match-beginning 0))
+                                  other
+                                  props 'diff-refine-preproc))))
+        (t ;; Normal diffs.
+         (let ((beg1 (1+ (point))))
+           (when (re-search-forward "^---.*\n" end t)
+             ;; It's a combined add&remove, so there's something to do.
+             (smerge-refine-subst beg1 (match-beginning 0)
+                                  (match-end 0) end
+                                  props 'diff-refine-preproc))))))))
+
+
 ;; provide the package
 (provide 'diff-mode)