dgutov / diff-hl

Emacs package for highlighting uncommitted changes
GNU General Public License v3.0
913 stars 44 forks source link

Highlight diffs on-the-fly #33

Closed PythonNut closed 9 years ago

PythonNut commented 9 years ago

The README indicates that showing accurate diffs even if the buffer has been modified, but not saved is a possible feature. This is an issue to track the discussion and development of this feature, if this is deemed worthwhile to implement.

dgutov commented 9 years ago

Not a priority, as far as I'm concerned. Even if someone implements it, it'll likely be too slow on large files, and clash with the "staged hunks" display, if that's ever implemented.

I'd look at a patch, though.

PythonNut commented 9 years ago

I'm currently experimenting. I think I might arrive at a patch not too far in the future (as in weeks, not years). Here's something that mostly works. (occasionally it gets hunks confused). It's just a snippet (uncommited) from my init.el. The code is truly terrible. I redefine other people's functions and the code is a DRY as an ocean.

Rather surprisingly, the performance already seems acceptable. (But then, this does come from a person who uses diff-hl and git-gutter+-mode at the same time).

(defun diff-buffer-with-file-unified ()
  "View the differences between BUFFER and its associated file.
This requires the external program `diff' to be in your `exec-path'."
  (interactive)
  (with-current-buffer (get-buffer (current-buffer))
    (diff-custom buffer-file-name (current-buffer) "-U 0" 'noasync)))

;; diff, but do not open a window for the buffer
(defun diff-custom (old new &optional switches no-async)
  "Find and display the differences between OLD and NEW files.
When called interactively, read NEW, then OLD, using the
minibuffer.  The default for NEW is the current buffer's file
name, and the default for OLD is a backup file for NEW, if one
exists.  If NO-ASYNC is non-nil, call diff synchronously.

When called interactively with a prefix argument, prompt
interactively for diff switches.  Otherwise, the switches
specified in the variable `diff-switches' are passed to the diff command."
  (interactive
    (let* ((newf (if (and buffer-file-name (file-exists-p buffer-file-name))
                   (read-file-name
                     (concat "Diff new file (default "
                       (file-name-nondirectory buffer-file-name) "): ")
                     nil buffer-file-name t)
                   (read-file-name "Diff new file: " nil nil t)))
            (oldf (file-newest-backup newf)))
      (setq oldf (if (and oldf (file-exists-p oldf))
                   (read-file-name
                     (concat "Diff original file (default "
                       (file-name-nondirectory oldf) "): ")
                     (file-name-directory oldf) oldf t)
                   (read-file-name "Diff original file: "
                     (file-name-directory newf) nil t)))
      (list oldf newf (diff-switches))))
  (diff-no-select old new switches no-async))

(with-eval-after-load 'diff-hl
  (defun diff-hl-changes ()
    (let* ((file buffer-file-name)
            (backend (vc-backend file)))
      (when backend
        (let ((state (vc-state file backend)))
          (cond
            ((or (eq state 'edited)
               (and (eq state 'up-to-date)
                 ;; VC state is stale in after-revert-hook.
                 (or revert-buffer-in-progress-p
                   ;; Diffing against an older revision.
                   diff-hl-reference-revision)))
              (let* ((buf-name " *diff-hl* ")
                      diff-auto-refine-mode
                      res)
                (diff-hl-with-diff-switches
                  (vc-call-backend backend 'diff (list file)
                    diff-hl-reference-revision nil
                    buf-name))
                (with-current-buffer buf-name
                  (goto-char (point-min))
                  (unless (eobp)
                    (ignore-errors
                      (diff-beginning-of-hunk t))
                    (while (looking-at diff-hunk-header-re-unified)
                      (let ((line (string-to-number (match-string 3)))
                             (len (let ((m (match-string 4)))
                                    (if m (string-to-number m) 1)))
                             (beg (point)))
                        (diff-end-of-hunk)
                        (let* ((inserts (diff-count-matches "^\\+" beg (point)))
                                (deletes (diff-count-matches "^-" beg (point)))
                                (type (cond ((zerop deletes) 'insert)
                                        ((zerop inserts) 'delete)
                                        (t 'change))))
                          (when (eq type 'delete)
                            (setq len 1)
                            (cl-incf line))
                          (push (list line len type) res))))))
                (diff-buffer-with-file-unified)
                (with-current-buffer "*Diff*"
                  (goto-char (point-min))
                  (unless (eobp)
                    (ignore-errors
                      (diff-beginning-of-hunk t))
                    (while (looking-at diff-hunk-header-re-unified)
                      (let ((line (string-to-number (match-string 3)))
                             (len (let ((m (match-string 4)))
                                    (if m (string-to-number m) 1)))
                             (beg (point)))
                        (diff-end-of-hunk)
                        (let* ((inserts (diff-count-matches "^\\+" beg (point)))
                                (deletes (diff-count-matches "^-" beg (point)))
                                (type (cond ((zerop deletes) 'insert)
                                        ((zerop inserts) 'delete)
                                        (t 'change))))
                          (when (eq type 'delete)
                            (setq len 1)
                            (cl-incf line))
                          (push (list line len type) res)
                          (setq res (mapcar
                                      (lambda (item)
                                        (if (> (first item) line)
                                          (list (+ (first item) (- inserts deletes)) (second item) (third item))
                                          item)) res)))))))
                (nreverse res)))
            ((eq state 'added)
              `((1 ,(line-number-at-pos (point-max)) insert)))
            ((eq state 'removed)
              `((1 ,(line-number-at-pos (point-max)) delete))))))))
  ;; (add-hook 'post-command-hook 'diff-hl-update)
  (run-with-idle-timer 1 t 'diff-hl-update))
dgutov commented 9 years ago

You can avoid using a temporary file, see the second option in this answer: http://stackoverflow.com/a/18174051/615245

While making a patch, keep in mind that this project also requires copyright assignment to FSF. I could make the relevant behavior more customizable, though.

PythonNut commented 9 years ago

Well, there are larger problems. This fails when an unsaved diff modified the result of an unstaged diff. The current program just starts marking huge sections as modified.

To fix this, we need line-precision diffing.

dgutov commented 9 years ago

I don't think you need two diffs at the same time. Just diff the buffer contents against the current repository version.

PythonNut commented 9 years ago

I tried that, although not very hard in theory, vc doesn't support it, and I don't know enough to make a version that works in all systems that vc does.

dgutov commented 9 years ago

You should look at the source of vc-revision-other-window. Apparently, all we need is vc-working-revision and vc-find-revision (or just the backend command, if we'd like to choose the output buffer).

PythonNut commented 9 years ago

The other question is whether or not we want to highlight unsaved changes with different markers as well.

Obviously, that would be nice, but it would cost us one diff over the straight revision ==> buffer method.

dgutov commented 9 years ago

Err, probably not. That's a lot of highlighting already.

But note that "changed lines" can be an entirely different highlighter. It doesn't need to call the external process at all, and it can work either through post-command-hook (and recording where changes happen), or by parsing buffer-undo-list. See highlight-changes-mode.

PythonNut commented 9 years ago

I never figured out how to get highlight-changes-mode to work by lines. Also, it doesn't have a very good way to determine what has been deleted.

Perhaps we can use both, highlight-changes-mode to tell us which hunks are not synchronized, and the buffer ==> revision diff to actually get the hunks.

How would process substitution work in Emacs' case? I don't know of any command that causes Emacs to print a buffer to a pipe/stream.

PythonNut commented 9 years ago

Actually, I don't think that would be too hard, we can just have emacs "save" to a FIFO, and pass it to diff.

git diff :0:<buffer-relative-file-name> /tmp/diff-hl-fifo

However this doesn't fix the case for other vcs systems. Or did you want to pay the one-time overhead of vc-find-revision for the sake of generality? After which we do

diff <(buffer-file-name)>.~<(vc-working-revision buffer-file-name)>~ /tmp/diff-hl-fifo

Does this program work on Windows? Is windows support a concern?

PythonNut commented 9 years ago

Here's a working version that uses vc-find-revision. Current problems:

(defun vc-show-working-revision ()
  (interactive)
  (vc-ensure-vc-buffer)
  (let* ((file buffer-file-name)
          (revision (vc-working-revision file)))
    ;; hide the buffer and pass it on
    (with-current-buffer (vc-find-revision file revision)
      (rename-buffer (concat " " (buffer-name)))
      (current-buffer))))

(defun diff-buffer-with-head-unified ()
  "View the differences between BUFFER and its associated file.
This requires the external program `diff' to be in your `exec-path'."
  (interactive)
  (with-current-buffer (get-buffer (current-buffer))
    (with-current-buffer
      (diff-no-select
        (vc-show-working-revision)
        (current-buffer)
        "-U 0" 'noasync)
      (concat " " (buffer-name))
      (current-buffer))))

(with-eval-after-load 'diff-hl
  (setq diff-hl-draw-borders nil)
  (defun diff-hl-changes ()
    (let* ((file buffer-file-name)
            (backend (vc-backend file)))
      (when backend
        (let ((state (vc-state file backend)))
          (cond
            ((or (eq state 'edited)
               (and (eq state 'up-to-date)
                 ;; VC state is stale in after-revert-hook.
                 (or revert-buffer-in-progress-p
                   ;; Diffing against an older revision.
                   diff-hl-reference-revision)))
              (let* ((buf-name " *diff-hl* ")
                      diff-auto-refine-mode
                      res)
                (diff-buffer-with-head-unified)
                (with-current-buffer "*Diff*"
                  (goto-char (point-min))
                  (unless (eobp)
                    (ignore-errors
                      (diff-beginning-of-hunk t))
                    (while (looking-at diff-hunk-header-re-unified)
                      (let ((line (string-to-number (match-string 3)))
                             (len (let ((m (match-string 4)))
                                    (if m (string-to-number m) 1)))
                             (beg (point)))
                        (diff-end-of-hunk)
                        (let* ((inserts (diff-count-matches "^\\+" beg (point)))
                                (deletes (diff-count-matches "^-" beg (point)))
                                (type (cond ((zerop deletes) 'insert)
                                        ((zerop inserts) 'delete)
                                        (t 'change))))
                          (when (eq type 'delete)
                            (setq len 1)
                            (cl-incf line))
                          (push (list line len type) res))))))
                (nreverse res)))
            ((eq state 'added)
              `((1 ,(line-number-at-pos (point-max)) insert)))
            ((eq state 'removed)
              `((1 ,(line-number-at-pos (point-max)) delete))))))))
  ;; (add-hook 'post-command-hook 'diff-hl-update)
  (run-with-idle-timer 1 t 'diff-hl-update))
dgutov commented 9 years ago

We need give the Diff buffer a different name so it doesn't clobber diffs the user is using, and we also want it to be hidden.

Any variation on *diff* would solve both those problems.

We need to cleanup the working revision files (messy and bad), or make them in a location that requires no cleanup (clean, good).

Somewhere in /tmp, I guess. We should also not retrieve them every time, but rather only do that when the file is opened, or the user does a commit. Maybe also monitor the current repository version in a timer, to catch commits made by external tools or unanticipated packages.

Also note that the name vc-show-working-revision is no good: this code isn't part of the VC package.

PythonNut commented 9 years ago

Any variation on diff would solve both those problems.

The problem is that the buffer name is hardcoded.

Somewhere in /tmp, I guess.

Or /dev/shm on Linux, which is guaranteed to be a ramdisk.

We should also not retrieve them every time, but rather only do that when the file is opened, or the user does a commit.

vc already tracks when the file already exists and returns it instead of creating a new one. I don't know if it also checks if said file is out of date.

Also note that the name vc-show-working-revision is no good: this code isn't part of the VC package.

Noted. It's a bad habit of mine to experiment by copying definitions and editing them.

dgutov commented 9 years ago

vc already tracks when the file already exists and returns it instead of creating a new one. I don't know if it also checks if said file is out of date.

Ah, so it does (and it ensures the match by having the revision a part of the filename). However, that logic is tied to having the checked out file in the same directory as the current one. So you'll probably need to write a different version of vc-find-revision, using a temporary directory.

PythonNut commented 9 years ago

Any variation on diff would solve both those problems.

The problem is that the buffer name is hardcoded.

Wait no, apparently I can't read. It's not hardcoded at all.

dgutov commented 9 years ago

How would process substitution work in Emacs' case? I don't know of any command that causes Emacs to print a buffer to a pipe/stream.

shell-command-on-region?

PythonNut commented 9 years ago

I suppose I learn new things every day. We've stopped needing to do that, right? (as diff does it automatically).

dgutov commented 9 years ago

diff-no-select writes a copy of the buffer to disk, so maybe we could do better. Or not; someone would have to measure the performance impact.

PythonNut commented 9 years ago

We could potentially have it write to a ramdisk, which would be very fast. I know it writes to /tmp by default, which is often a randisk.

dgutov commented 9 years ago

Sure. But we'll still need to measure it. :)

PythonNut commented 9 years ago

Okay, so I have almost working code. The last problem is that vc-find-revision does not check if the revision has changed. If I'm on a branch dev and I push commits to dev emacs doesn't refresh the revision file.

dgutov commented 9 years ago

Why would it check for a change in revision? It takes revision as an argument.

PythonNut commented 9 years ago

Suppose I'm on branch dev with file test.el. vc creates the working revision file as test.el.~dev~. If I add more commits to dev it has no way of knowing that test.el.~dev~ is out of date.

dgutov commented 9 years ago

Right, that would be a problem. Without us having to re-implement each working-revision function to return non-symbolic refs, maybe add a function to vc-checkin-hook that would delete all the working revision files for the repo. But that won't account for commits or other history manipulations made from the command line.

Or/and put it at the end of find-file-hook (after vc-find-file-hook). That was, it'll run when the buffer is reverted.

dgutov commented 9 years ago

Or maybe we could ask the core to provide a variable, setting which to t would force working-revision implementations to return non-symbolic refs.

After all, your example shows that vc-revision-other-window can return a stale result. And it could be fixed likewise by using that variable.

PythonNut commented 9 years ago

Hmm... would this be considered a bug in vc itself? I'd consider it a bug in git if git diff @ dev showed stale diffs for dev for example.

dgutov commented 9 years ago

Indeed.

PythonNut commented 9 years ago

How do we intend to fix it? I've never encountered this before. Obviously, we do want it to work in current versions. Do we advise it conditionally on the version?

dgutov commented 9 years ago

Obviously, we do want it to work in current versions

Only if it's not too much trouble. Emacs releases come and go. Not everyone has to have access to the latest goodies. :)

Do we advise it conditionally on the version?

Probably. First we'll have to fix it in the core, and the advice could be based on that fix.

PythonNut commented 9 years ago

For git:

(defadvice vc-git-working-revision
  (around use-hashes-only (file) activate preactivate compile)
  "Git-specific version of `vc-working-revision'."
  (vc-git--rev-parse "HEAD"))

I haven't used the other vc systems to the point where I can intelligently comprehend their code, and checking them all seems to be a daunting task.

dgutov commented 9 years ago

Yes, the above should work. But doing just this will make VC show Git-ffafafafafaf in the mode-line where it now shows Git-master, which would be a step back in functionality.

Maybe the working-revision VC command needs a new optional "literal" argument.

PythonNut commented 9 years ago

Hm... I suppose the question is what we mean by revision. In my mind, a revision must be immutable. master is not a revision, but ffafafafafaf is. master is a branch. That might just be me, but I suspect I'm not the only one...

It's probably a little late to fix this without breaking tons of stuff.

Maybe the working-revision VC command needs a new optional "literal" argument.

That would require us to update all of the backends, right? That would require quite a few people (unless we know someone who knows a ton of vc systems. monotone, anyone?).

dgutov commented 9 years ago

I suppose the question is what we mean by revision... It's probably a little late to fix this without breaking tons of stuff.

Yes, now what VC means by "revision" is defined by the places it's used in.

That would require us to update all of the backends, right? That would require quite a few people (unless we know someone who knows a ton of vc systems).

Not if the argument is optional, then the backends can be updated gradually. And the backends that alreay return a "concrete" revision would be unaffected.

PythonNut commented 9 years ago

Handling this is a half step more delicate than I planned because vc also caches the revisions as lisp attributes.

Also, it struck me that this will cost one more disk access per diff (for the index).

dgutov commented 9 years ago

vc also caches the revisions as lisp attributes

Where? I can't see that in vc-git-find-revision or vc-find-revision.

Also, it struck me that this will cost one more disk access per diff (for the index).

At what point?

PythonNut commented 9 years ago
(defun vc-working-revision (file &optional backend)
  "Return the repository version from which FILE was checked out.
If FILE is not registered, this function always returns nil."
  (or (vc-file-getprop file 'vc-working-revision)
      (progn
        (setq backend (or backend (vc-responsible-backend file)))
        (when backend
          (vc-file-setprop file 'vc-working-revision
                   (vc-call-backend backend 'working-revision file))))))

Also, I just noticed that the indentation in vc is a horrible mess of both tabs and spaces (with no rhyme or reason).

dgutov commented 9 years ago

Oh, right. I was looking at wrong functions.

Every time we diff, we need to check if the current cached file from HEAD is out-of-date.

When does that happen? Emacs does not cache the git diff output, so maybe passing HEAD verbatim into various commands should work just as well.

By the way, you should feel free to bring up various problems either at emacs-devel, or via M-x report-emacs-bug.

I just noticed that the indentation in vc is a horrible mess of both tabs and spaces

Let's not go there. :)

PythonNut commented 9 years ago

When does that happen? Emacs does not cache the git diff output, so maybe passing HEAD verbatim into various commands should work just as well.

Here's how my code works (currently)

  1. Get a concrete revision name (currently, this costs us one disk read to get the hash of head)
  2. Produce an unmodified copy of the file in /dev/shm (or /tmp if no /dev/shm is found). This is very cheap as we prefer a ramdisk.
  3. Diff the current buffer with the unmodified copy (producing another temp file in /dev/shm). This is cheap too.

On further diffs

  1. Get a concrete revision name
  2. Reuse the unmodified copy. (cached)
  3. Diff the current buffer with the unmodified copy

The rev-parse, which is one of the more expensive operations in this system, costs 0.002s on my system, which I doubt will be a problem.

maybe passing HEAD verbatim into various commands should work just as well.

I would like to. Ultimately, I wish there were some sort of (vc-diff (vc-working-revision buffer-file-name) (current-buffer)) magic that would simplify this, but I haven't found it.The difficulty arises from diffing with the current buffer (i.e. not a file). We need to manually write it out somewhere. When we do, we can no longer rely on vc for diffing, because our temp file is not under version control. Without vc, HEAD loses its meaning.

By the way, you should feel free to bring up various problems either at emacs-devel, or via M-x report-emacs-bug.

Yes. I'm looking into just becoming an emacs developer outright and contributing. First, I'm cooking up a ton of advice to prove that everything works (before I start breaking things).

All of my ingredients are together now. My code works, as far as I can tell. Now I need to make it pretty and fast.

dgutov commented 9 years ago

The rev-parse, which is one of the more expensive operations in this system, costs 0.002s on my system, which I doubt will be a problem.

I'm pretty sure the diff operation will become the most expensive, at least on some files.

Ultimately, I wish there were some sort of ... magic that would simplify this

git diff can use stdin: http://stackoverflow.com/a/18174051/615245. Until VC supports this kind of operation, this probably won't help us, though, but diff can do that as well.

Yes. I'm looking into just becoming an emacs developer outright and contributing.

That's great, but filing some bugs and starting a discussion or two would be good starting steps in this direction.

Now I need to make it pretty and fast.

And then, I guess, we'll have to make diff-hl somehow flexible in when/how to diff. That's not trivial, too.

PythonNut commented 9 years ago

I'm pretty sure the diff operation will become the most expensive, at least on some files.

Probably. I just checked with some medium source files (~4000 lines) and the diff took about 0.002s, so there is room for it to outstrip the rev-parse, but not by terribly much.

git diff can use stdin: http://stackoverflow.com/a/18174051/615245. Until VC supports this kind of operation, this probably won't help us, though, but diff can do that as well.

That's interesting. We'd need to forgo diff-no-select and write our own diffing procedure.

That's great, but filing some bugs and starting a discussion or two would be good starting steps in this direction.

I have in the past. One bug was fixed quickly, and the other has not yet been touched by anyone. (And I can't fix it myself). I don't really know what to discuss, so I haven't done any of that.

And then, I guess, we'll have to make diff-hl somehow flexible in when/how to diff. That's not trivial, too.

Right now, I've advised dif-hl-changes, and I run diff-hl-update with an idle-timer (it was actually working fine in the post-command-hook, but I chickened out of that). It's working pretty well so far, but if you have better ideas, I'm all ears.

dgutov commented 9 years ago

We'd need to forgo diff-no-select and write our own diffing procedure.

Or update diff-no-select to use stdin when it can. You should measure the performance first, though: the temp file solution is probably fast enough anyway, especially if the user has an SSD.

and the other has not yet been touched by anyone

What's the number? I can look at it, if it's within my competence (and interesting enough :)).

I don't really know what to discuss, so I haven't done any of that.

If you intend to become an Emacs developer, surely you have some improvement to Emacs in mind? Or will you stop at this package?

I've advised dif-hl-changes

That could be generalized with a diff-hl-changes-function variable...

I run diff-hl-update with an idle-timer (it was actually working fine in the post-command-hook, but I chickened out of that)

Yeah, an idle timer is safer anyway.

It's working pretty well so far, but if you have better ideas, I'm all ears.

Sounds like it might work fine as a new minor mode. Not sure yet if I want this to be the default behavior. But if that's all you needed to change, overall the necessary changes look pretty simple, which is good.

PythonNut commented 9 years ago

@dgutov Sorry, I meant to add the bug #, but I never got around to it. It's #19455, and it deals with some special zsh syntax that breaks sh-mode highlighting.

If you intend to become an Emacs developer, surely you have some improvement to Emacs in mind? Or will you stop at this package?

I usually do, but I generally just go and write code and, when I get stuck, ask specific questions on emacs.stackexchange to help me along.

Sounds like it might work fine as a new minor mode. Not sure yet if I want this to be the default behavior. But if that's all you needed to change, overall the necessary changes look pretty simple, which is good.

I have no problem with it being a custom variable or a minor mode.

That could be generalized with a diff-hl-changes-function variable...

Yes please.

The one other thing that I've found is that diff-hl removes highlighting during editing only to have it restored later when my idle-timer fires. This makes for some blinking that is a little distracting. Obviously this makes sense without flydiffs but with them, it just gets in the way. How can I disable this? (Or am I misunderstanding something?)

dgutov commented 9 years ago

19455

I see, yeah it looks pretty niche to me. But if you'd like a pointer, sh-syntax-propertize-function should be the function to improve.

How can I disable this?

With (remove-hook 'after-change-functions 'diff-hl-edit t)?

PythonNut commented 9 years ago

Okay, so summer is flying by. Here's my code, taken direct from my config. It's a bit of a mess, and I will clean it up, but I also don't want to let my absent-mindedness produce the possibility that I will forget this entirely.

(defun diff-hl-make-temp-file-name (file rev &optional manual)
  "Return a backup file name for REV or the current version of FILE.
If MANUAL is non-nil it means that a name for backups created by
the user should be returned."
  (let* ((auto-save-file-name-transforms
           `((".*" ,temporary-file-directory t))))
    (expand-file-name
      (concat (make-auto-save-file-name)
        ".~" (subst-char-in-string
               ?/ ?_ rev)
        (unless manual ".") "~")
      temporary-file-directory)))

(defun diff-hl-create-revision (file revision)
  "Read REVISION of FILE into a buffer and return the buffer."
  (let ((automatic-backup (diff-hl-make-temp-file-name file revision))
         (filebuf (get-file-buffer file))
         (filename (diff-hl-make-temp-file-name file revision 'manual)))
    (unless (file-exists-p filename)
      (if (file-exists-p automatic-backup)
        (rename-file automatic-backup filename nil)
        (with-current-buffer filebuf
          (let ((failed t)
                 (coding-system-for-read 'no-conversion)
                 (coding-system-for-write 'no-conversion))
            (unwind-protect
              (with-temp-file filename
                (let ((outbuf (current-buffer)))
                  ;; Change buffer to get local value of
                  ;; vc-checkout-switches.
                  (with-current-buffer filebuf
                    (vc-call find-revision file revision outbuf))))
              (setq failed nil)
              (when (and failed (file-exists-p filename))
                (delete-file filename)))))))
    filename))

(defun diff-hl-diff-buffer-with-head ()
  "View the differences between BUFFER and its associated file.
This requires the external program `diff' to be in your `exec-path'."
  (interactive)
  (vc-ensure-vc-buffer)
  (with-current-buffer (get-buffer (current-buffer))
    (let ((rev (diff-hl-create-revision
                 buffer-file-name
                 (vc-working-revision buffer-file-name
                   (vc-responsible-backend buffer-file-name)
                   t)))
           (temporary-file-directory
             (if (file-directory-p "/dev/shm/")
               "/dev/shm/"
               temporary-file-directory)))
      (diff-no-select rev (current-buffer) "-U 0" 'noasync
        (get-buffer-create " *diff-hl-diff*")))))

(with-eval-after-load 'vc
  (defadvice vc-working-revision
    (around concrete-revision (file &optional backend concrete) activate preactivate compile)
    (setq ad-return-value
      (if concrete
        (vc-call-backend backend 'working-revision file t)
        (or (vc-file-getprop file 'vc-working-revision)
          (progn
            (setq backend (or backend (vc-responsible-backend file)))
            (when backend
              (vc-file-setprop file 'vc-working-revision
                (vc-call-backend backend 'working-revision file)))))))))

(with-eval-after-load 'vc-git
  (defadvice vc-git-working-revision
    (around use-hashes-only (file &optional concrete) activate preactivate compile)
    "Git-specific version of `vc-working-revision'."
    (if concrete
      (setq ad-return-value (vc-git--rev-parse ad-do-it))
      ad-do-it)))

(with-eval-after-load 'diff-hl
  (defvar diff-hl-modified-tick 0)
  (make-variable-buffer-local 'diff-hl-modified-tick)

  (defadvice diff-hl-update
    (around flydiff (&optional auto) activate preactivate compile)
    (unless (and auto
              (or
                (= diff-hl-modified-tick (buffer-modified-tick))
                (file-remote-p default-directory)
                (not (buffer-modified-p))))
      ad-do-it))

  (defadvice diff-hl-changes
    (around flydiff activate preactivate compile)
    (setq ad-return-value
      (let* ((file buffer-file-name)
              (backend (vc-backend file)))
        (when backend
          (let ((state (vc-state file backend)))
            (cond
              ((or
                 (buffer-modified-p)
                 (eq state 'edited)
                 (and (eq state 'up-to-date)
                   ;; VC state is stale in after-revert-hook.
                   (or revert-buffer-in-progress-p
                     ;; Diffing against an older revision.
                     diff-hl-reference-revision)))
                (let (diff-auto-refine-mode res)
                  (with-current-buffer (diff-hl-diff-buffer-with-head)
                    (goto-char (point-min))
                    (unless (eobp)
                      (ignore-errors
                        (diff-beginning-of-hunk t))
                      (while (looking-at diff-hunk-header-re-unified)
                        (let ((line (string-to-number (match-string 3)))
                               (len (let ((m (match-string 4)))
                                      (if m (string-to-number m) 1)))
                               (beg (point)))
                          (diff-end-of-hunk)
                          (let* ((inserts (diff-count-matches "^\\+" beg (point)))
                                  (deletes (diff-count-matches "^-" beg (point)))
                                  (type (cond ((zerop deletes) 'insert)
                                          ((zerop inserts) 'delete)
                                          (t 'change))))
                            (when (eq type 'delete)
                              (setq len 1)
                              (cl-incf line))
                            (push (list line len type) res))))))
                  (setq diff-hl-modified-tick (buffer-modified-tick))
                  (nreverse res)))
              ((eq state 'added)
                `((1 ,(line-number-at-pos (point-max)) insert)))
              ((eq state 'removed)
                `((1 ,(line-number-at-pos (point-max)) delete)))))))))

  (defadvice diff-hl-overlay-modified
    (around preserve-overlays activate preactivate compile))

  (add-hook 'diff-hl-mode-hook
    (lambda ()
      (remove-hook 'after-change-functions #'diff-hl-edit t)))

  (run-with-idle-timer 0.3 t #'diff-hl-update t))
dgutov commented 9 years ago

Thanks for the update. It looks fine overall, but you've duplicated the temp-directory-finding logic once, and the (called-interactively-p 'any) bit is odd.

PythonNut commented 9 years ago

the (called-interactively-p 'any) bit is odd.

Ah yep, that's a side effect of some debugging I was doing a while ago. I've updated the code.

What should I do now?

dgutov commented 9 years ago

Should I send you the copyright assignment form now?

PythonNut commented 9 years ago

@dgutov sure, what do I need to do for that? Will it hold for the Emacs source tree too?

dgutov commented 9 years ago

Actually, forget about me sending, just follow these instructions:

http://git.savannah.gnu.org/cgit/gnulib.git/tree/doc/Copyright/request-assign.future

Put in "Emacs" as the name of the program. It covers the packages in GNU ELPA as well.