Commit 610a6418 authored by Stefan Monnier's avatar Stefan Monnier
Browse files

Initial revision, known outside of Emacs as version 1.8.

parent 73fb36f1
;;; diff-mode.el --- A mode for viewing/editing context diffs
;; Copyright (C) 1998-1999 Free Software Foundation, Inc.
;; Author: Stefan Monnier <monnier@cs.yale.edu>
;; Keywords: patch diff
;; Version: v1_8
;; Revision: diff-mode.el,v 1.11 1999/10/09 23:38:29 monnier Exp
;; This file is part of GNU Emacs.
;; GNU Emacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; Provides support for font-lock patterns, outline-regexps, navigation
;; commands, editing and various conversions as well as jumping
;; to the corresponding source file.
;; History:
;; inspired by Pavel Machek's patch-mode.el (<pavel@atrey.karlin.mff.cuni.cz>)
;; some efforts were spent to have it somewhat compatible with XEmacs'
;; diff-mode as well as with compilation-minor-mode
;; to use it, simply add to your .emacs the following lines:
;;
;; (autoload 'diff-mode "diff-mode" "Diff major mode" t)
;; (add-to-list 'auto-mode-alist '("\\.\\(diffs?\\|patch\\|rej\\)\\'" . diff-mode))
;; Bugs:
;; - reverse doesn't work with normal diffs.
;; - (nitpick) the mark is not always quite right in diff-goto-source.
;; Todo:
;; - improve narrowed-view support.
;; - improve diff-find-file-name.
;; - improve the `compile' support.
;;; Code:
(eval-when-compile (require 'cl))
(defgroup diff-mode ()
"Major-mode for viewing/editing diffs"
:group 'tools
:group 'diff)
(defcustom diff-jump-to-old-file-flag nil
"*Non-nil means `diff-goto-source' jumps to the old file.
Else, it jumps to the new file."
:group 'diff-mode
:type '(boolean))
(defcustom diff-update-on-the-fly-flag t
"*Non-nil means hunk headers are kept up-to-date on-the-fly.
When editing a diff file, the line numbers in the hunk headers
need to be kept consistent with the actual diff. This can
either be done on the fly (but this sometimes interacts poorly with the
undo mechanism) or whenever the file is written (can be slow
when editing big diffs)."
:group 'diff-mode
:type '(boolean))
(defvar diff-mode-hook nil
"Run after setting up the `diff-mode' major mode.")
(defvar diff-outline-regexp
"\\([*+][*+][*+] [^0-9]\\|@@ ...\\|\\*\\*\\* [0-9].\\|--- [0-9]..\\)")
;;;;
;;;; keymap, menu, ...
;;;;
(defmacro diff-defmap (var bindings doc)
`(defvar ,var
(let ((m (make-keymap)))
(dolist (b ,bindings)
(define-key m (car b) (cdr b)))
m)
,doc))
(diff-defmap diff-mode-shared-map
'(;; from Pavel Machek's patch-mode
("n" . diff-next-hunk)
("N" . diff-next-file)
("p" . diff-prev-hunk)
("P" . diff-prev-file)
("k" . diff-kill-hunk)
("K" . diff-kill-file)
;; from compilation-minor-mode
("}" . diff-next-file)
("{" . diff-prev-file)
("\C-m" . diff-goto-source)
;; from XEmacs' diff-mode
("W" . widen)
;;("." . diff-goto-source) ;display-buffer
;;("f" . diff-goto-source) ;find-file
("o" . diff-goto-source) ;other-window
;;("w" . diff-goto-source) ;other-frame
;;("N" . diff-narrow)
;;("h" . diff-show-header)
;;("j" . diff-show-difference) ;jump to Nth diff
;;("q" . diff-quit)
(" " . scroll-up)
("\177" . scroll-down)
;; our very own bindings
("A" . diff-ediff-patch)
("r" . diff-restrict-view)
("R" . diff-reverse-direction)
("U" . diff-context->unified)
("C" . diff-unified->context))
"Keymap for read-only `diff-mode'. Only active in read-only mode.")
(diff-defmap diff-mode-map
`(("\e" . ,diff-mode-shared-map)
;; from compilation-minor-mode
("\C-c\C-c" . diff-goto-source))
"Keymap for `diff-mode'. See also `diff-mode-shared-map'.")
(easy-menu-define diff-mode-menu diff-mode-map
"Menu for `diff-mode'."
'("Diff"
["Jump to Source" diff-goto-source t]
["Apply with Ediff" diff-ediff-patch t]
["-----" nil nil]
["Reverse direction" diff-reverse-direction t]
["Context -> Unified" diff-context->unified t]
["Unified -> Context" diff-unified->context t]
;;["Fixup Headers" diff-fixup-modifs (not buffer-read-only)]
))
;;;;
;;;; font-lock support
;;;;
(defface diff-file-header-face
'((((class color) (background light))
(:background "grey70" :bold t))
(t (:bold t)))
"diff-mode face used to highlight file header lines."
:group 'diff-mode)
(defvar diff-file-header-face 'diff-file-header-face)
(defface diff-index-face
'((((class color) (background light))
(:background "grey70" :bold t))
(t (:bold t)))
"diff-mode face used to highlight index header lines."
:group 'diff-mode)
(defvar diff-index-face 'diff-index-face)
(defface diff-hunk-header-face
'((((class color) (background light))
(:background "grey85"))
(t (:bold t)))
"diff-mode face used to highlight hunk header lines."
:group 'diff-mode)
(defvar diff-hunk-header-face 'diff-hunk-header-face)
(defface diff-removed-face
'((t ()))
"diff-mode face used to highlight removed lines."
:group 'diff-mode)
(defvar diff-removed-face 'diff-removed-face)
(defface diff-added-face
'((t ()))
"diff-mode face used to highlight added lines."
:group 'diff-mode)
(defvar diff-added-face 'diff-added-face)
(defface diff-changed-face
'((t ()))
"diff-mode face used to highlight changed lines."
:group 'diff-mode)
(defvar diff-changed-face 'diff-changed-face)
(defvar diff-font-lock-keywords
'(("^@@ .+ @@$" . diff-hunk-header-face) ;unified
("^--- .+ ----$" . diff-hunk-header-face) ;context
("^\\*\\*\\*.+\\*\\*\\*\n" . diff-hunk-header-face) ;context
("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) .*\n" . diff-file-header-face)
("^[0-9,]+[acd][0-9,]+$" . diff-hunk-header-face)
("^!.*\n" . diff-changed-face) ;context
("^[+>].*\n" . diff-added-face)
("^[-<].*\n" . diff-removed-face)
("^Index: .*\n" . diff-index-face)
("^[^-=+*!<>].*\n" . font-lock-comment-face)))
(defconst diff-font-lock-defaults
'(diff-font-lock-keywords t nil nil nil))
;;;;
;;;; Compile support
;;;;
(defvar diff-file-regexp-alist
'(("Index: \\(.+\\)" 1)))
(defvar diff-error-regexp-alist
'(("@@ -\\([0-9]+\\),[0-9]+ \\+\\([0-9]+\\),[0-9]+ @@" nil 2)
("--- \\([0-9]+\\),[0-9]+ ----" nil 1)
("\\([0-9]+\\)\\(,[0-9]+\\)?[adc]\\([0-9]+\\)" nil 3)))
;;;;
;;;; Movement
;;;;
(defconst diff-hunk-header-re "^\\(@@ .+ @@\\|\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\n\\*\\*\\* .+ \\*\\*\\*\\*\\|[0-9]+\\(,[0-9]+\\)?[acd][0-9]+\\(,[0-9]+\\)?\\)$")
(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)
(if (looking-at diff-hunk-header-re) (goto-char (match-end 0)))
(re-search-forward (case style
(unified "^[^-+ \\]")
(context "^\\([^-+! \\][ \t]\\|--- .+ ----\\)")
(normal "^\\([<>\\][ \t]\\|---\\)")
(t "^[^-+!<> \\]"))
nil 'move)
(beginning-of-line))
(defun diff-beginning-of-hunk ()
(beginning-of-line)
(unless (looking-at diff-hunk-header-re)
(forward-line 1)
(condition-case ()
(re-search-backward diff-hunk-header-re)
(error (error "Can't find the beginning of the hunk")))))
(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")))))
(defun diff-end-of-file ()
(re-search-forward "^[-+!<>0-9@* \\]" nil t)
(re-search-forward "^[^-+!<>0-9@* \\]" nil 'move)
(beginning-of-line))
(defun diff-next-hunk (&optional count)
"Move to next (COUNT'th) hunk."
(interactive "p")
(unless count (setq count 1))
(if (< count 0) (diff-prev-hunk (- count))
(when (looking-at diff-hunk-header-re) (incf count))
(condition-case ()
(re-search-forward diff-hunk-header-re nil nil count)
(error (error "Can't find next hunk")))
(goto-char (match-beginning 0))))
(defun diff-prev-hunk (&optional count)
"Move to previous (COUNT'th) hunk."
(interactive "p")
(unless count (setq count 1))
(if (< count 0) (diff-next-hunk (- count))
(condition-case ()
(re-search-backward diff-hunk-header-re nil nil count)
(error (error "Can't find previous hunk")))))
(defun diff-next-file (&optional count)
"Move to next (COUNT'th) file header."
(interactive "p")
(unless count (setq count 1))
(if (< count 0) (diff-prev-file (- count))
(when (looking-at diff-file-header-re) (incf count))
(condition-case ()
(re-search-forward diff-file-header-re nil nil count)
(error (error "Can't find next file")))
(goto-char (match-beginning 0))))
(defun diff-prev-file (&optional count)
"Move to (COUNT'th) previous file header."
(interactive "p")
(unless count (setq count 1))
(if (< count 0) (diff-next-file (- count))
(condition-case ()
(re-search-backward diff-file-header-re nil nil count)
(error (error "Can't find previous file")))))
(defun diff-restrict-view (&optional arg)
"Restrict the view to the current hunk.
If the prefix ARG is given, restrict the view to the current file instead."
(interactive "P")
(save-excursion
(if arg (diff-beginning-of-file) (diff-beginning-of-hunk))
(narrow-to-region (point)
(progn (if arg (diff-end-of-file) (diff-end-of-hunk))
(point)))
(set (make-local-variable 'diff-narrowed-to) (if arg 'file 'hunk))))
(defun diff-kill-hunk ()
"Kill current hunk."
(interactive)
(diff-beginning-of-hunk)
(let ((start (point))
(firsthunk (save-excursion
(ignore-errors
(diff-beginning-of-file) (diff-next-hunk) (point))))
(nexthunk (save-excursion
(ignore-errors
(diff-next-hunk) (point))))
(nextfile (save-excursion
(ignore-errors
(diff-next-file) (point)))))
(if (and firsthunk (= firsthunk start)
(or (null nexthunk)
(and nextfile (> nexthunk nextfile))))
;; we're the only hunk for this file, so kill the file
(diff-kill-file)
(diff-end-of-hunk)
(kill-region start (point)))))
(defun diff-kill-file ()
"Kill current file's hunks."
(interactive)
(diff-beginning-of-file)
(let* ((start (point))
(prevhunk (save-excursion
(ignore-errors
(diff-prev-hunk) (point))))
(index (save-excursion
(re-search-backward "^Index: " prevhunk t))))
(when index (setq start index))
(diff-end-of-file)
(kill-region start (point))))
;;;;
;;;; jump to other buffers
;;;;
(defun diff-filename-drop-dir (file)
(when (string-match "/" file) (substring file (match-end 0))))
(defun diff-find-file-name (&optional old)
"Return the file corresponding to the current patch.
Non-nil OLD means that we want the old file."
(save-excursion
(unless (looking-at diff-file-header-re)
(or (ignore-errors (diff-beginning-of-file))
(re-search-forward diff-file-header-re nil t)))
(let* ((limit (save-excursion
(condition-case ()
(progn (diff-prev-hunk) (point))
(error (point-min)))))
(fs (append
(when (looking-at "[-*][-*][-*] \\(\\S-+\\)\\s-.*\n[-+][-+][-+] \\(\\S-+\\)\\s-.*$")
(list (if old (match-string 1) (match-string 2))
(if old (match-string 2) (match-string 1))))
(progn (forward-line 1) nil)
(when (save-excursion
(re-search-backward "^Index: \\(.+\\)" limit t))
(list (match-string 1)))
(when (re-search-backward "^diff \\(-\\S-+ +\\)*\\(\\S-+\\)\\( +\\(\\S-+\\)\\)?" nil t)
(list (if old (match-string 2) (match-string 4))
(if old (match-string 4) (match-string 2))))))
(fs (delq nil fs))
(file
;; look for each file in turn. If none found, try again but
;; ignoring the first level of directory, ...
(do* ((files fs (delq nil (mapcar 'diff-filename-drop-dir files)))
(file nil nil))
((or (null files)
(setq file (do* ((files files (cdr files))
(file (car files) (car files)))
((or (null file) (file-exists-p file))
file))))
file))))
(or
file
(and (string-match "\\.rej\\'" (or buffer-file-name ""))
(let ((file (substring buffer-file-name 0 (match-beginning 0))))
(when (file-exists-p file) file)))
;; FIXME: use a more informative prompt
(let ((file (read-file-name "File: " nil (first fs) nil (first fs))))
;; FIXME: remember for the next invocation
file)))))
(defun diff-goto-source (&optional other-file)
"Jump to the corresponding source line.
`diff-jump-to-old-file-flag' (or its opposite if the OTHER-FILE prefix arg
is give) determines whether to jump to the old or the new file.
If the prefix arg is bigger than 8 (for example with \\[universal-argument] \\[universal-argument])
then `diff-jump-to-old-file-flag' is also set, for the next invokations."
(interactive "P")
(save-excursion
(let ((old (if (not other-file) diff-jump-to-old-file-flag
(not diff-jump-to-old-file-flag))))
(when (> (prefix-numeric-value other-file) 8)
(setq diff-jump-to-old-file-flag old))
(diff-beginning-of-hunk)
(let* ((loc (if (not (looking-at "[-@*\n ]*\\([0-9,]+\\)\\([ acd+]+\\([0-9,]+\\)\\)?"))
(error "Can't find the hunk header")
(if old (match-string 1)
(if (match-end 3) (match-string 3)
(unless (re-search-forward "^--- \\([0-9,]+\\)" nil t)
(error "Can't find the hunk separator"))
(match-string 1)))))
(lines (if (string-match "^\\([0-9]*\\),\\([0-9]*\\)" loc)
(cons (string-to-number (match-string 1 loc))
(string-to-number (match-string 2 loc)))
(cons (string-to-number loc) nil)))
(file (diff-find-file-name old)))
(unless file (error "Can't find the file"))
(pop-to-buffer (find-file-noselect file))
(let* ((line (car lines))
(span (if (or (null (cdr lines)) (< (cdr lines) 0)) 0
(if (< (cdr lines) line) (cdr lines)
(- (cdr lines) line)))))
(ignore-errors
(goto-line line)
(forward-line span)
(push-mark (point) t t)
(goto-line line)))))))
(defun diff-ediff-patch ()
"Call `ediff-patch-file' on the current buffer."
(interactive)
(condition-case err
(ediff-patch-file (current-buffer))
(wrong-number-of-arguments (ediff-patch-file))))
;;;;
;;;; Conversion functions
;;;;
;;(defvar diff-inhibit-after-change nil
;; "Non-nil means inhibit `diff-mode's after-change functions.")
(defun diff-unified->context (start end)
"Convert unified diffs to context diffs.
START and END are either taken from the region (if a prefix arg is given) or
else cover the whole bufer."
(interactive (if current-prefix-arg
(list (mark) (point))
(list (point-min) (point-max))))
(unless (markerp end) (setq end (copy-marker end)))
(let (;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion
(goto-char start)
(while (and (re-search-forward "^\\(\\(---\\) .+\n\\(\\+\\+\\+\\) .+\\|@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([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 ((line1 (match-string 4))
(lines1 (match-string 5))
(line2 (match-string 6))
(lines2 (match-string 7)))
(replace-match
(concat "***************\n*** " line1 ","
(number-to-string (+ (string-to-number line1)
(string-to-number lines1)
-1)) " ****"))
(forward-line 1)
(save-restriction
(narrow-to-region (point)
(progn (diff-end-of-hunk 'unified) (point)))
(let ((hunk (buffer-string)))
(goto-char (point-min))
(if (not (save-excursion (re-search-forward "^-" nil t)))
(delete-region (point) (point-max))
(goto-char (point-max))
(let ((modif nil) last-pt)
(while (progn (setq last-pt (point))
(= (forward-line -1) 0))
(case (char-after)
(? (insert " ") (setq modif nil) (backward-char 1))
(?+ (delete-region (point) last-pt) (setq modif t))
(?- (if (not modif)
(progn (forward-char 1)
(insert " "))
(delete-char 1)
(insert "! "))
(backward-char 2))
(?\\ (when (save-excursion (forward-line -1)
(= (char-after) ?+))
(delete-region (point) last-pt) (setq modif t)))
(t (setq modif nil))))))
(goto-char (point-max))
(save-excursion
(insert "--- " line2 ","
(number-to-string (+ (string-to-number line2)
(string-to-number lines2)
-1)) " ----\n" hunk))
;;(goto-char (point-min))
(forward-line 1)
(if (not (save-excursion (re-search-forward "^+" nil t)))
(delete-region (point) (point-max))
(let ((modif nil) (delete nil))
(while (not (eobp))
(case (char-after)
(? (insert " ") (setq modif nil) (backward-char 1))
(?- (setq delete t) (setq modif t))
(?+ (if (not modif)
(progn (forward-char 1)
(insert " "))
(delete-char 1)
(insert "! "))
(backward-char 2))
(?\\ (when (save-excursion (forward-line 1)
(not (eobp)))
(setq delete t) (setq modif t)))
(t (setq modif nil)))
(let ((last-pt (point)))
(forward-line 1)
(when delete
(delete-region last-pt (point))
(setq delete nil)))))))))))))))
(defun diff-context->unified (start end)
"Convert context diffs to unified diffs.
START and END are either taken from the region (if a prefix arg is given) or
else cover the whole bufer."
(interactive (if current-prefix-arg
(list (mark) (point))
(list (point-min) (point-max))))
(unless (markerp end) (setq end (copy-marker end)))
(let (;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion
(goto-char start)
(while (and (re-search-forward "^\\(\\(\\*\\*\\*\\) .+\n\\(---\\) .+\\|\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\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))
(?\ ;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))))
(?\ ;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)) " @@"))))))))))
(defun diff-reverse-direction (start end)
"Reverse the direction of the diffs.
START and END are either taken from the region (if a prefix arg is given) or
else cover the whole bufer."
(interactive (if current-prefix-arg
(list (mark) (point))
(list (point-min) (point-max))))
(unless (markerp end) (setq end (copy-marker end)))
(let (;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion