+(defun change-log-search-file-name (where)
+ "Return the file-name for the change under point."
+ (save-excursion
+ (goto-char where)
+ (beginning-of-line 1)
+ (if (looking-at change-log-start-entry-re)
+ ;; We are at the start of an entry, search forward for a file
+ ;; name.
+ (progn
+ (re-search-forward change-log-file-names-re nil t)
+ (match-string-no-properties 2))
+ (if (looking-at change-log-file-names-re)
+ ;; We found a file name.
+ (match-string-no-properties 2)
+ ;; Look backwards for either a file name or the log entry start.
+ (if (re-search-backward
+ (concat "\\(" change-log-start-entry-re
+ "\\)\\|\\("
+ change-log-file-names-re "\\)") nil t)
+ (if (match-beginning 1)
+ ;; We got the start of the entry, look forward for a
+ ;; file name.
+ (progn
+ (re-search-forward change-log-file-names-re nil t)
+ (match-string-no-properties 2))
+ (match-string-no-properties 4))
+ ;; We must be before any file name, look forward.
+ (re-search-forward change-log-file-names-re nil t)
+ (match-string-no-properties 2))))))
+
+(defun change-log-find-file ()
+ "Visit the file for the change under point."
+ (interactive)
+ (let ((file (change-log-search-file-name (point))))
+ (if (and file (file-exists-p file))
+ (find-file file)
+ (message "No such file or directory: %s" file))))
+
+(defun change-log-search-tag-name-1 (&optional from)
+ "Search for a tag name within subexpression 1 of last match.
+Optional argument FROM specifies a buffer position where the tag
+name should be located. Return value is a cons whose car is the
+string representing the tag and whose cdr is the position where
+the tag was found."
+ (save-restriction
+ (narrow-to-region (match-beginning 1) (match-end 1))
+ (when from (goto-char from))
+ ;; The regexp below skips any symbol near `point' (FROM) followed by
+ ;; whitespace and another symbol. This should skip, for example,
+ ;; "struct" in a specification like "(struct buffer)" and move to
+ ;; "buffer". A leading paren is ignored.
+ (when (looking-at
+ "[(]?\\(?:\\(?:\\sw\\|\\s_\\)+\\(?:[ \t]+\\(\\sw\\|\\s_\\)+\\)\\)")
+ (goto-char (match-beginning 1)))
+ (cons (find-tag-default) (point))))
+
+(defconst change-log-tag-re
+ "(\\(\\(?:\\sw\\|\\s_\\)+\\(?:[, \t]+\\(?:\\sw\\|\\s_\\)+\\)*\\))"
+ "Regexp matching a tag name in change log entries.")
+
+(defun change-log-search-tag-name (&optional at)
+ "Search for a tag name near `point'.
+Optional argument AT non-nil means search near buffer position
+AT. Return value is a cons whose car is the string representing
+the tag and whose cdr is the position where the tag was found."
+ (save-excursion
+ (goto-char (setq at (or at (point))))
+ (save-restriction
+ (widen)
+ (or (condition-case nil
+ ;; Within parenthesized list?
+ (save-excursion
+ (backward-up-list)
+ (when (looking-at change-log-tag-re)
+ (change-log-search-tag-name-1 at)))
+ (error nil))
+ (condition-case nil
+ ;; Before parenthesized list on same line?
+ (save-excursion
+ (when (and (skip-chars-forward " \t")
+ (looking-at change-log-tag-re))
+ (change-log-search-tag-name-1)))
+ (error nil))
+ (condition-case nil
+ ;; Near file name?
+ (save-excursion
+ (when (and (progn
+ (beginning-of-line)
+ (looking-at change-log-file-names-re))
+ (goto-char (match-end 0))
+ (skip-syntax-forward " ")
+ (looking-at change-log-tag-re))
+ (change-log-search-tag-name-1)))
+ (error nil))
+ (condition-case nil
+ ;; Anywhere else within current entry?
+ (let ((from
+ (save-excursion
+ (end-of-line)
+ (if (re-search-backward change-log-start-entry-re nil t)
+ (match-beginning 0)
+ (point-min))))
+ (to
+ (save-excursion
+ (end-of-line)
+ (if (re-search-forward change-log-start-entry-re nil t)
+ (match-beginning 0)
+ (point-max)))))
+ (when (and (< from to) (<= from at) (<= at to))
+ (save-restriction
+ ;; Narrow to current change log entry.
+ (narrow-to-region from to)
+ (cond
+ ((re-search-backward change-log-tag-re nil t)
+ (narrow-to-region (match-beginning 1) (match-end 1))
+ (goto-char (point-max))
+ (cons (find-tag-default) (point-max)))
+ ((re-search-forward change-log-tag-re nil t)
+ (narrow-to-region (match-beginning 1) (match-end 1))
+ (goto-char (point-min))
+ (cons (find-tag-default) (point-min)))))))
+ (error nil))))))
+
+(defvar change-log-find-head nil)
+(defvar change-log-find-tail nil)
+(defvar change-log-find-window nil)
+
+(defun change-log-goto-source-1 (tag regexp file buffer
+ &optional window first last)
+ "Search for tag TAG in buffer BUFFER visiting file FILE.
+REGEXP is a regular expression for TAG. The remaining arguments
+are optional: WINDOW denotes the window to display the results of
+the search. FIRST is a position in BUFFER denoting the first
+match from previous searches for TAG. LAST is the position in
+BUFFER denoting the last match for TAG in the last search."
+ (with-current-buffer buffer
+ (save-excursion
+ (save-restriction
+ (widen)
+ (if last
+ (progn
+ ;; When LAST is set make sure we continue from the next
+ ;; line end to not find the same tag again.
+ (goto-char last)
+ (end-of-line)
+ (condition-case nil
+ ;; Try to go to the end of the current defun to avoid
+ ;; false positives within the current defun's body
+ ;; since these would match `add-log-current-defun'.
+ (end-of-defun)
+ ;; Don't fall behind when `end-of-defun' fails.
+ (error (progn (goto-char last) (end-of-line))))
+ (setq last nil))
+ ;; When LAST was not set start at beginning of BUFFER.
+ (goto-char (point-min)))
+ (let (current-defun)
+ (while (and (not last) (re-search-forward regexp nil t))
+ ;; Verify that `add-log-current-defun' invoked at the end
+ ;; of the match returns TAG. This heuristic works well
+ ;; whenever the name of the defun occurs within the first
+ ;; line of the defun.
+ (setq current-defun (add-log-current-defun))
+ (when (and current-defun (string-equal current-defun tag))
+ ;; Record this as last match.
+ (setq last (line-beginning-position))
+ ;; Record this as first match when there's none.
+ (unless first (setq first last)))))))
+ (if (or last first)
+ (with-selected-window
+ (setq change-log-find-window (or window (display-buffer buffer)))
+ (if last
+ (progn
+ (when (or (< last (point-min)) (> last (point-max)))
+ ;; Widen to show TAG.
+ (widen))
+ (push-mark)
+ (goto-char last))
+ ;; When there are no more matches go (back) to FIRST.
+ (message "No more matches for tag `%s' in file `%s'" tag file)
+ (setq last first)
+ (goto-char first))
+ ;; Return new "tail".
+ (list (selected-window) first last))
+ (message "Source location of tag `%s' not found in file `%s'" tag file)
+ nil)))
+
+(defun change-log-goto-source ()
+ "Go to source location of \"change log tag\" near `point'.
+A change log tag is a symbol within a parenthesized,
+comma-separated list. If no suitable tag can be found nearby,
+try to visit the file for the change under `point' instead."
+ (interactive)
+ (if (and (eq last-command 'change-log-goto-source)
+ change-log-find-tail)
+ (setq change-log-find-tail
+ (condition-case nil
+ (apply 'change-log-goto-source-1
+ (append change-log-find-head change-log-find-tail))
+ (error
+ (format "Cannot find more matches for tag `%s' in file `%s'"
+ (car change-log-find-head)
+ (nth 2 change-log-find-head)))))
+ (save-excursion
+ (let* ((at (point))
+ (tag-at (change-log-search-tag-name))
+ (tag (car tag-at))
+ (file (when tag-at (change-log-search-file-name (cdr tag-at))))
+ (file-at (when file (match-beginning 2)))
+ ;; `file-2' is the file `change-log-search-file-name' finds
+ ;; at `point'. We use `file-2' as a fallback when `tag' or
+ ;; `file' are not suitable for some reason.
+ (file-2 (change-log-search-file-name at))
+ (file-2-at (when file-2 (match-beginning 2))))
+ (cond
+ ((and (or (not tag) (not file) (not (file-exists-p file)))
+ (or (not file-2) (not (file-exists-p file-2))))
+ (error "Cannot find tag or file near `point'"))
+ ((and file-2 (file-exists-p file-2)
+ (or (not tag) (not file) (not (file-exists-p file))
+ (and (or (and (< file-at file-2-at) (<= file-2-at at))
+ (and (<= at file-2-at) (< file-2-at file-at))))))
+ ;; We either have not found a suitable file name or `file-2'
+ ;; provides a "better" file name wrt `point'. Go to the
+ ;; buffer of `file-2' instead.
+ (setq change-log-find-window
+ (display-buffer (find-file-noselect file-2))))
+ (t
+ (setq change-log-find-head
+ (list tag (concat "\\_<" (regexp-quote tag) "\\_>")
+ file (find-file-noselect file)))
+ (condition-case nil
+ (setq change-log-find-tail
+ (apply 'change-log-goto-source-1 change-log-find-head))
+ (error
+ (format "Cannot find matches for tag `%s' in file `%s'"
+ tag file)))))))))
+
+(defun change-log-next-error (&optional argp reset)
+ "Move to the Nth (default 1) next match in a ChangeLog buffer.
+Compatibility function for \\[next-error] invocations."
+ (interactive "p")
+ (let* ((argp (or argp 0))
+ (count (abs argp)) ; how many cycles
+ (down (< argp 0)) ; are we going down? (is argp negative?)
+ (up (not down))
+ (search-function (if up 're-search-forward 're-search-backward)))
+
+ ;; set the starting position
+ (goto-char (cond (reset (point-min))
+ (down (line-beginning-position))
+ (up (line-end-position))
+ ((point))))
+
+ (funcall search-function change-log-file-names-re nil t count))
+
+ (beginning-of-line)
+ ;; if we found a place to visit...
+ (when (looking-at change-log-file-names-re)
+ (let (change-log-find-window)
+ (change-log-goto-source)
+ (when change-log-find-window
+ ;; Select window displaying source file.
+ (select-window change-log-find-window)))))
+