Edit: this is now a package: diff-at-point
(defun diff-at-point-file-line-to-point (current-filename-relative current-line current-column &optional strict) " This "This can be used to navigate to a point in a diff buffer,. given 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 (in lines). (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. "\\(?:diff[[:blank:]]+.*\n\\)?" "\\(?:index[[:blank:]]+.*\n\\)?" ;; Prefix '+++ '. "^" "\\-\\-\\-[[:blank:]]+.*\n" "\\+\\+\\+[[:blank:]]+.*\n" "@@[[:blank:]]+.*[[:blank:]]@@" ;; 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) ) (if fallback-point-end (progn (goto-char (or fallback-point-end current-filename-diff-point-max)) (forward-line -1) (forward-char) ) ;; Last hunk, no end. (goto-char current-filename-diff-point-max) ) ) ;; fallback-point-end (point) ) ) ) ) ) ) )
(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) ) ) ) ) ) )