2

Emacs has a useful feature to jump to the line (via diff-goto-source-and-close).

Is there a way to do the reverse with vc-root-diff or something similar?

That is to open a diff, jumping to the current line of code (if it's been modified or added?).

2

3 Answers 3

3

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)))))))
0
2

Edit, I've since written made a version of this which parses the diff hunks, here.


Posting own answer, it works but it has some limitations.

This is a wrapper for vc-root-diff which opens a fillscreen buffer, and jumps to the current file then the line/column (using search).

Ideally it would position the cursor using the diff line ranges, as long as you're on a unique within the file.

;; Override pop-up-windows,
(defun vc-root-diff-fullscreen-and-jump-to-point () "
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))
      ;; TODO, parse the diff chunks instead of searching for plain text.
      ;; not urgent.
      (current-line
        (buffer-substring-no-properties
          (line-beginning-position)
          (line-end-position)))

      ;; Chars to move backwards (so we keep the same relative point).
      (point-offset (- (save-excursion (end-of-line) (point)) (point)))

      )
    (when (vc-root-diff nil)
      ;; It's possible we don't have a file.
      (when current-filename
        ;; Go to the file in the diff which we were previously viewing.
        (let ((current-filename-rel (file-relative-name current-filename default-directory)))
          (when
            (search-forward-regexp
              (concat
                "^"
                ;; Prefix '--- '.
                "\\-\\-\\-[[:blank:]]+.*\n"
                ;; Prefix '+++ '.
                "\\+\\+\\+[[:blank:]]+"
                ;; Optional 'b/'.
                "\\(\\|b/\\)"
                (regexp-quote current-filename-rel)
                ;; Optional ' (some text)'
                ;; Subversion quirk.
                "\\(\\|[[:blank:]]+.*\\)\n"
                "@@[[:blank:]]+.*@@"
                ;; may have trailing text, ignore this.
                )
              nil t 1
              )

            ;; Now search for the current line.
            (when
              (search-forward-regexp
                (concat
                  ;; Space or '+' line prefix.
                  "^\\( \\|\\+\\)"
                  (regexp-quote current-line)
                  "$"
                  )
                nil t 1
                )
              (backward-char point-offset)
              )
            )
          )
        )
      )
    )
  )

11
  • 1
    Your "Subversion quirk" is presumably wrong. That's optionally matching one-or-more space chars followed by zero-or-more chars which are neither a backslash nor the letter n.
    – phils
    Commented Jul 2, 2019 at 22:18
  • 1
    I would imagine you intended to use [^\n]* there, to say "zero or more non-newlines" (in a double-quoted string \n is a newline), but you can write that as .*
    – phils
    Commented Jul 2, 2019 at 22:32
  • Thanks, corrected.
    – ideasman42
    Commented Jul 2, 2019 at 22:46
  • 1
    Oh, and you probably wanted [:blank:] where you've used [:space:], as the latter will match newline chars which I don't think you intended? blank matches "horizontal whitespace".
    – phils
    Commented Jul 2, 2019 at 22:49
  • 1
    @phils unfortunately, the eval won't work with lexical binding. You should do (rx-to-string `(sequence ... ,current-line ...) t) instead. Or if you have Emacs 27 (rx ... (literal current-line) ...).
    – npostavs
    Commented Jul 3, 2019 at 0:48
1

This isn't exactly the same as you've asked for, but you may like vc-region-history (bound to C-xvh by default) or, if you're using Magit, the equivalent command magit-log-buffer-file (l in the magit-file-popup menu; hence commonly it would be C-cM-gl).

With these commands you firstly mark a region you are interested in, and they then give you (in a single buffer) a history of that region: the sequence of commits which affected the region, and the diff for each of those changes.

Give this a try and see if it suits your requirements.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.