Edit: this is now a package: diff-at-point
This solution parses the diff hunks to lookup a buffer location in the diff.
By default, if the current location isn't part of the diff, the closest line in the diff is used (unless the option argument strict
isn't nil
).
(defun diff-at-point-file-line-to-point
(current-filename-relative current-line current-column &optional strict)
"This can be used to navigate to a point in a diff buffer.
Given the file, line and column in the original (non-diff) file,
this returns the point in the diff buffer or nil if it can't be found.
This can be used to implement a utility to open a diff buffer,
then navigate to the point the user was viewing.
However this function doesn't make any change the the buffer state,
that's up to the caller to implement.
CURRENT-FILENAME-RELATIVE the filename to look-up in the diff.
Typically this is the filename of the current buffer,
relative to the repository root.
Arguments CURRENT-LINE & CURRENT-COLUMN define the location in the file.
Typically this is taken from the current `point'.
When STRICT is enabled, only return a result if the line exists in the diff,
otherwise return a point in the closest hunk."
(save-excursion
(if
(not
(re-search-forward
(concat
;; Prefix '+++ '.
"^"
"\\-\\-\\-[[:blank:]]+.*\n"
"\\+\\+\\+[[:blank:]]+"
;; Optional 'b/'.
"\\(\\|b/\\)"
(regexp-quote current-filename-relative)
;; Optional ' (some text)'
;; Subversion quirk.
"\\(\\|[[:blank:]]+.*\\)\n"
"@@[[:blank:]]+.*[[:blank:]]@@"
;; may have trailing text, ignore this.
) nil t 1))
(error "Unable to find filename in diff: %S" current-filename-relative)
(beginning-of-line)
(let
(
;; Next file or end of document.
(point-found nil)
;; Fallback point closest to the hunk,
;; used if 'current-line' isn't inside the hunk in the diff.
;;
;; The fallback uses either the beginning or end of the hunk.
(fallback-point-begin nil)
(fallback-point-end nil)
(fallback-point-is-begin nil)
;; The distance of the fallback point to the line we're looking for.
(fallback-delta-lines nil)
;; Find the next hunk or file max, to restrict the search.
(current-filename-diff-point-max
(save-excursion
(if
(re-search-forward
(concat
"\\(^\\)"
;; Optional, don't capture, ignore.
;; Git uses: "diff ..." & "index ..."
;; Subversion uses: "Index ..." & "===...".
;;
;; So use any non-blank line start except for '-' & '+'.
"\\(?:[^\\-\\+[:blank:]]+.*\n\\)+?"
;; Prefix '+++ '.
"\\-\\-\\-[[:blank:]]+.*\n"
"\\+\\+\\+[[:blank:]]+.*\n"
;; May have trailing text which can be safely ignored.
"@@[[:blank:]]+.*[[:blank:]]@@")
nil t 1)
(match-beginning 0)
(point-max)))))
;; Now search for the current hunk.
(save-excursion
(while
(and
(eq point-found nil)
(re-search-forward
(concat
"^\\(@@\\)[[:blank:]]+"
;; Previous (ignore).
"\\-"
"\\([[:digit:]]+\\)\\,\\([[:digit:]]+\\)"
"[[:blank:]]+"
;; Current (use).
"\\+"
"\\([[:digit:]]+\\)\\,\\([[:digit:]]+\\)"
"[[:blank:]]+@@")
current-filename-diff-point-max t 1))
(let*
(
(diff-hunk-point (match-beginning 1))
(diff-hunk-begin
(string-to-number
(buffer-substring-no-properties
(match-beginning 4)
(match-end 4))))
(diff-hunk-lines
(string-to-number
(buffer-substring-no-properties
(match-beginning 5)
(match-end 5))))
(diff-hunk-end (+ diff-hunk-begin diff-hunk-lines)))
;; We have something like this:
;; @@ -1,4 +1,5 @@
;; string-to-number
;; (message "%S %S" diff-hunk-begin diff-hunk-end)
;; If the last hunk was set as the fallback, use this chink as the
;; end of that fallback.
(when (and
(eq fallback-point-end nil)
(not (eq fallback-point-begin nil)))
(setq fallback-point-end diff-hunk-point))
;; Scan down the the line...
(cond
((< current-line diff-hunk-begin)
(let ((delta (- diff-hunk-begin current-line)))
(when (or
(eq fallback-delta-lines nil)
(> fallback-delta-lines delta))
(setq fallback-point-begin diff-hunk-point)
(setq fallback-point-is-begin t)
(setq fallback-delta-lines delta)
;; Set next iteration.
(setq fallback-point-end nil))))
((>= current-line diff-hunk-end)
(let ((delta (- current-line diff-hunk-end)))
(when (or
(eq fallback-delta-lines nil)
(> fallback-delta-lines delta))
(setq fallback-point-begin diff-hunk-point)
(setq fallback-point-is-begin nil)
(setq fallback-delta-lines delta)
;; Set next iteration.
(setq fallback-point-end nil))))
(t
(let ((diff-line-current diff-hunk-begin))
(forward-line)
;; Avoid eternal loop (for mal-formed diffs).
(while (eq point-found nil)
(let ((c (char-after (point))))
(cond
((memq c '(?\s ?+))
(when (eq diff-line-current current-line)
(setq point-found (+ 1 (point) current-column)))
(setq diff-line-current (+ 1 diff-line-current)))
((eq c ?-)
nil)
(t
(error "Malformed diff, unexpected character %S" c))))
(forward-line))))))))
;; May be nil, return either way.
(if strict
point-found
(or
point-found
;; Use the beginning or end of the hunk.
(save-excursion
(if fallback-point-is-begin
(progn
(goto-char fallback-point-begin)
(forward-line 1)
(forward-char)
)
(progn
(goto-char (or fallback-point-end current-filename-diff-point-max))
(forward-line -1)
(forward-char)))
;; fallback-point-end
(point))))))))
This is a utility function which uses the function above to open diff and jump to the point (if possible).
(defun vc-root-diff-fullscreen-and-jump-to-point (&optional branch) "
Open a diff of the repository in the current frame.
Jumping to the file, then line if possible.
"
(interactive)
(let
(
(pop-up-windows nil)
(current-filename (buffer-file-name))
(current-line (line-number-at-pos))
(current-column (- (point) (line-beginning-position))))
(when
(if branch
(vc-root-version-diff nil branch nil)
(vc-root-diff nil))
(when current-filename
(let*
(
(current-filename-relative (file-relative-name current-filename default-directory))
(point-found (diff-at-point-file-line-to-point current-filename-relative current-line current-column)))
;; Go to the file in the diff which we were previously viewing.
(when point-found
(goto-char point-found)))))))
M-x report-emacs-bug
. IIRC Dan Nicolaescu asked for this some years ago and had some prototype code working, so maybe he now has something working reliably.