summaryrefslogtreecommitdiff
path: root/lisp/diff-mode.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/diff-mode.el')
-rw-r--r--lisp/diff-mode.el383
1 files changed, 267 insertions, 116 deletions
diff --git a/lisp/diff-mode.el b/lisp/diff-mode.el
index a79fb577453..609c5ef6490 100644
--- a/lisp/diff-mode.el
+++ b/lisp/diff-mode.el
@@ -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.
@@ -72,7 +70,7 @@
:group 'diff-mode)
(defcustom diff-jump-to-old-file nil
- "*Non-nil means `diff-goto-source' jumps to the old file.
+ "Non-nil means `diff-goto-source' jumps to the old file.
Else, it jumps to the new file."
:type 'boolean
:group 'diff-mode)
@@ -157,7 +155,8 @@ when editing big diffs)."
("\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-hunk)
+ ("\C-c\C-w" . diff-refine-ignore-spaces-hunk)
+ ("\C-c\C-b" . diff-fine-highlight) ;No reason for `b' :-(
("\C-c\C-f" . next-error-follow-minor-mode))
"Keymap for `diff-mode'. See also `diff-mode-shared-map'.")
@@ -174,8 +173,9 @@ when editing big diffs)."
["Unified -> Context" diff-unified->context t]
;;["Fixup Headers" diff-fixup-modifs (not buffer-read-only)]
"-----"
- ["Split hunk" diff-split-hunk t]
- ["Refine hunk" diff-refine-hunk t]
+ ["Split hunk" diff-split-hunk (diff-splittable-p)]
+ ["Ignore whitespace changes" diff-refine-ignore-spaces-hunk t]
+ ["Highlight fine changes" diff-fine-highlight t]
["Kill current hunk" diff-hunk-kill t]
["Kill current file's hunks" diff-file-kill t]
"-----"
@@ -351,8 +351,11 @@ when editing big diffs)."
("^--- .+ ----$" . diff-hunk-header-face) ;context
("^[0-9,]+[acd][0-9,]+$" . diff-hunk-header-face) ;normal
("^---$" . diff-hunk-header-face) ;normal
- ("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) \\(\\S-+\\)\\(.*[^*-]\\)?\n"
- (0 diff-header-face) (2 diff-file-header-face prepend))
+ ;; For file headers, accept files with spaces, but be careful to rule
+ ;; out false-positives when matching hunk headers.
+ ("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) \\([^\t\n]+?\\)\\(?:\t.*\\| \\(\\*\\*\\*\\*\\|----\\)\\)?\n"
+ (0 diff-header-face)
+ (2 (if (not (match-end 3)) diff-file-header-face) prepend))
("^\\([-<]\\)\\(.*\n\\)"
(1 diff-indicator-removed-face) (2 diff-removed-face))
("^\\([+>]\\)\\(.*\n\\)"
@@ -385,12 +388,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 "^[^-+# \\]\\|"
@@ -418,13 +424,29 @@ but in the file header instead, in which case move forward to the first hunk."
(diff-beginning-of-file-and-junk)
(diff-hunk-next))))))
+(defun diff-unified-hunk-p ()
+ (save-excursion
+ (ignore-errors
+ (diff-beginning-of-hunk)
+ (looking-at "^@@"))))
+
(defun diff-beginning-of-file ()
(beginning-of-line)
(unless (looking-at diff-file-header-re)
- (forward-line 2)
- (condition-case ()
- (re-search-backward diff-file-header-re)
- (error (error "Can't find the beginning of the file")))))
+ (let ((start (point))
+ res)
+ ;; diff-file-header-re may need to match up to 4 lines, so in case
+ ;; we're inside the header, we need to move up to 3 lines forward.
+ (forward-line 3)
+ (if (and (setq res (re-search-backward diff-file-header-re nil t))
+ ;; Maybe the 3 lines forward were too much and we matched
+ ;; a file header after our starting point :-(
+ (or (<= (point) start)
+ (setq res (re-search-backward diff-file-header-re nil t))))
+ res
+ (goto-char start)
+ (error "Can't find the beginning of the file")))))
+
(defun diff-end-of-file ()
(re-search-forward "^[-+#!<>0-9@* \\]" nil t)
@@ -477,26 +499,34 @@ If the prefix ARG is given, restrict the view to the current file instead."
"Go to the beginning of file-related diff-info.
This is like `diff-beginning-of-file' except it tries to skip back over leading
data such as \"Index: ...\" and such."
- (let ((start (point))
- (file (condition-case err (progn (diff-beginning-of-file) (point))
- (error err)))
- ;; prevhunk is one of the limits.
- (prevhunk (save-excursion (ignore-errors (diff-hunk-prev) (point))))
- err)
- (when (consp file)
- ;; Presumably, we started before the file header, in the leading junk.
- (setq err file)
- (diff-file-next)
- (setq file (point)))
- (let ((index (save-excursion
- (re-search-backward "^Index: " prevhunk t))))
- (when index (setq file index))
- (if (<= file start)
- (goto-char file)
- ;; File starts *after* the starting point: we really weren't in
- ;; a file diff but elsewhere.
- (goto-char start)
- (signal (car err) (cdr err))))))
+ (let* ((start (point))
+ (prevfile (condition-case err
+ (save-excursion (diff-beginning-of-file) (point))
+ (error err)))
+ (err (if (consp prevfile) prevfile))
+ (nextfile (ignore-errors
+ (save-excursion
+ (goto-char start) (diff-file-next) (point))))
+ ;; prevhunk is one of the limits.
+ (prevhunk (save-excursion
+ (ignore-errors
+ (if (numberp prevfile) (goto-char prevfile))
+ (diff-hunk-prev) (point))))
+ (previndex (save-excursion
+ (re-search-backward "^Index: " prevhunk t))))
+ ;; If we're in the junk, we should use nextfile instead of prevfile.
+ (if (and (numberp nextfile)
+ (or (not (numberp prevfile))
+ (and previndex (> previndex prevfile))))
+ (setq prevfile nextfile))
+ (if (and previndex (numberp prevfile) (< previndex prevfile))
+ (setq prevfile previndex))
+ (if (and (numberp prevfile) (<= prevfile start))
+ (goto-char prevfile)
+ ;; File starts *after* the starting point: we really weren't in
+ ;; a file diff but elsewhere.
+ (goto-char start)
+ (signal (car err) (cdr err)))))
(defun diff-file-kill ()
"Kill current file's hunks."
@@ -530,6 +560,13 @@ data such as \"Index: ...\" and such."
(while (re-search-forward re end t) (incf n))
n)))
+(defun diff-splittable-p ()
+ (save-excursion
+ (beginning-of-line)
+ (and (looking-at "^[-+ ]")
+ (progn (forward-line -1) (looking-at "^[-+ ]"))
+ (diff-unified-hunk-p))))
+
(defun diff-split-hunk ()
"Split the current (unified diff) hunk at point into two hunks."
(interactive)
@@ -624,9 +661,11 @@ If the OLD prefix arg is passed, tell the file NAME of the old file."
(list (if old (match-string 2) (match-string 4))
(if old (match-string 4) (match-string 2)))))))))
-(defun diff-find-file-name (&optional old prefix)
+(defun diff-find-file-name (&optional old batch prefix)
"Return the file corresponding to the current patch.
Non-nil OLD means that we want the old file.
+Non-nil BATCH means to prefer returning an incorrect answer than to prompt
+the user.
PREFIX is only used internally: don't use it."
(save-excursion
(unless (looking-at diff-file-header-re)
@@ -663,7 +702,10 @@ PREFIX is only used internally: don't use it."
(boundp 'cvs-pcl-cvs-dirchange-re)
(save-excursion
(re-search-backward cvs-pcl-cvs-dirchange-re nil t))
- (diff-find-file-name old (match-string 1)))
+ (diff-find-file-name old batch (match-string 1)))
+ ;; Invent something, if necessary.
+ (when batch
+ (or (car fs) default-directory))
;; if all else fails, ask the user
(let ((file (read-file-name (format "Use file %s: " (or (first fs) ""))
nil (first fs) t (first fs))))
@@ -711,7 +753,12 @@ else cover the whole buffer."
(let ((line1 (match-string 4))
(lines1 (match-string 5))
(line2 (match-string 6))
- (lines2 (match-string 7)))
+ (lines2 (match-string 7))
+ ;; Variables to use the special undo function.
+ (old-undo buffer-undo-list)
+ (old-end (marker-position end))
+ (start (match-beginning 0))
+ (reversible t))
(replace-match
(concat "***************\n*** " line1 ","
(number-to-string (+ (string-to-number line1)
@@ -753,6 +800,14 @@ else cover the whole buffer."
(if (not (save-excursion (re-search-forward "^+" nil t)))
(delete-region (point) (point-max))
(let ((modif nil) (delete nil))
+ (if (save-excursion (re-search-forward "^\\+.*\n-" nil t))
+ ;; Normally, lines in a substitution come with
+ ;; first the removals and then the additions, and
+ ;; the context->unified function follows this
+ ;; convention, of course. Yet, other alternatives
+ ;; are valid as well, but they preclude the use of
+ ;; context->unified as an undo command.
+ (setq reversible nil))
(while (not (eobp))
(case (char-after)
(?\s (insert " ") (setq modif nil) (backward-char 1))
@@ -771,7 +826,15 @@ else cover the whole buffer."
(forward-line 1)
(when delete
(delete-region last-pt (point))
- (setq delete nil)))))))))))))))
+ (setq delete nil)))))))
+ (unless (or (not reversible) (eq buffer-undo-list t))
+ ;; Drop the many undo entries and replace them with
+ ;; a single entry that uses diff-context->unified to do
+ ;; the work.
+ (setq buffer-undo-list
+ (cons (list 'apply (- old-end end) start (point-max)
+ 'diff-context->unified start (point-max))
+ old-undo)))))))))))
(defun diff-context->unified (start end &optional to-context)
"Convert context diffs to unified diffs.
@@ -785,68 +848,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.
@@ -1159,26 +1243,30 @@ 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)
@@ -1326,7 +1414,7 @@ Whitespace differences are ignored."
(if (> (- (car forw) orig) (- orig (car back))) back forw)
(or back forw))))
-(defsubst diff-xor (a b) (if a (not b) b))
+(defsubst diff-xor (a b) (if a (if (not b) a) b))
(defun diff-find-source-location (&optional other-file reverse)
"Find out (BUF LINE-OFFSET POS SRC DST SWITCHED).
@@ -1409,8 +1497,15 @@ the value of this variable when given an appropriate prefix argument).
With a prefix argument, REVERSE the hunk."
(interactive "P")
(destructuring-bind (buf line-offset pos old new &optional switched)
- ;; If REVERSE go to the new file, otherwise go to the old.
- (diff-find-source-location (not reverse) reverse)
+ ;; Sometimes we'd like to have the following behavior: if REVERSE go
+ ;; to the new file, otherwise go to the old. But that means that by
+ ;; default we use the old file, which is the opposite of the default
+ ;; for diff-goto-source, and is thus confusing. Also when you don't
+ ;; know about it it's pretty surprising.
+ ;; TODO: make it possible to ask explicitly for this behavior.
+ ;;
+ ;; This is duplicated in diff-test-hunk.
+ (diff-find-source-location nil reverse)
(cond
((null line-offset)
(error "Can't find the text to patch"))
@@ -1454,8 +1549,7 @@ With a prefix argument, REVERSE the hunk."
With a prefix argument, try to REVERSE the hunk."
(interactive "P")
(destructuring-bind (buf line-offset pos src dst &optional switched)
- ;; If REVERSE go to the new file, otherwise go to the old.
- (diff-find-source-location (not reverse) reverse)
+ (diff-find-source-location nil reverse)
(set-window-point (display-buffer buf) (+ (car pos) (cdr src)))
(diff-hunk-status-msg line-offset (diff-xor reverse switched) t)))
@@ -1506,7 +1600,7 @@ For use in `add-log-current-defun-function'."
(goto-char (+ (car pos) (cdr src)))
(add-log-current-defun))))))
-(defun diff-refine-hunk ()
+(defun diff-refine-ignore-spaces-hunk ()
"Refine the current hunk by ignoring space differences."
(interactive)
(let* ((char-offset (- (point) (progn (diff-beginning-of-hunk 'try-harder)
@@ -1551,6 +1645,63 @@ For use in `add-log-current-defun-function'."
(delete-file file1)
(delete-file file2))))
+;;; Fine change highlighting.
+
+(defface diff-fine-change
+ '((t :background "yellow"))
+ "Face used for char-based changes shown by `diff-fine-highlight'.")
+
+(defun diff-fine-highlight-preproc ()
+ (while (re-search-forward "^." nil t)
+ ;; Replace the hunk's leading prefix (+, -, !, <, or >) on each line
+ ;; with something constant, otherwise it'll be flagged as changes
+ ;; (since it's typically "-" on one side and "+" on the other).
+ ;; Note that we keep the same number of chars: we treat the prefix
+ ;; as part of the texts-to-diff, so that finding the right char
+ ;; afterwards will be easier. This only makes sense because we make
+ ;; diffs at char-granularity.
+ (replace-match " ")))
+
+(defun diff-fine-highlight ()
+ "Highlight changes of hunk at point at a finer granularity."
+ (interactive)
+ (require 'smerge-mode)
+ (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-fine-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-fine-highlight-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-fine-highlight-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-fine-highlight-preproc)))))))
+
+
;; provide the package
(provide 'diff-mode)