Skip to content

Instantly share code, notes, and snippets.

@codecoll
Last active March 22, 2022 07:12
Show Gist options
  • Save codecoll/84245ad37efd947a4d8e3fd494a00091 to your computer and use it in GitHub Desktop.
Save codecoll/84245ad37efd947a4d8e3fd494a00091 to your computer and use it in GitHub Desktop.
;;; Iniline refined git diff with live update
;; Copyright (C) 2022
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; TODO:
;;
;; - more testing
;; - overlays should have a unique identifiying property, so we don't bother other overlays
;; - delete temporary files when the feature is turned off?
;;
;;; Code:
(setq my-inline-diff-revert-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<f1>") 'my-inline-diff-revert)
map))
(defun my-inline-diff ()
(interactive)
(cond ((and (boundp 'my-inline-diff-status)
(eq my-inline-diff-status 'condensed))
(remove-overlays)
(remove-hook 'after-change-functions 'my-inline-diff-show-diff-after-change t)
(setq my-inline-diff-status 'off))
((and (boundp 'my-inline-diff-status)
(eq my-inline-diff-status 'full))
(save-excursion
(goto-char (point-min))
(let ((last 1)
(context 3)
line)
(while (not (eobp))
(goto-char (or (next-overlay-change (point))
(point-max)))
(setq line (line-number-at-pos))
(if (> (- line last) (* 2 context))
(overlay-put (make-overlay
(save-excursion
(goto-line last)
(forward-line context)
(point))
(save-excursion
(forward-line (- context))
(line-end-position)))
'display (propertize
"...\n"
'face 'diff-hunk-header)))
(setq last line))))
(setq my-inline-diff-status 'condensed))
(t
(my-inline-diff-show-diff)
(if (and (boundp 'my-inline-diff-changes)
my-inline-diff-changes)
(add-hook 'after-change-functions 'my-inline-diff-show-diff-after-change t t)
(message "No differences.")))))
(defun my-inline-diff-show-diff-after-change (start end length)
(if (sit-for 0.3)
(my-inline-diff-show-diff)))
(defun my-inline-diff-show-diff ()
(unless (boundp 'my-inline-diff-old-file)
(make-local-variable 'my-inline-diff-old-file)
(setq my-inline-diff-old-file (make-temp-file "oldfile")))
(unless (boundp 'my-inline-diff-new-file)
(make-local-variable 'my-inline-diff-new-file)
(setq my-inline-diff-new-file (make-temp-file "newfile")))
(let* ((oldfile my-inline-diff-old-file)
(newfile my-inline-diff-new-file)
(file (current-buffer))
inputchanged)
(unless (and (boundp 'my-inline-diff-new-file-tick)
(equal my-inline-diff-new-file-tick
(buffer-modified-tick file)))
(with-temp-file newfile
(insert-buffer file))
(make-local-variable 'my-inline-diff-new-file-tick)
(setq my-inline-diff-new-file-tick (buffer-modified-tick file))
(setq inputchanged t)
(let ((time (file-attribute-modification-time
(file-attributes file))))
;; simple heuristics for refetching HEAD version, if the file
;; was not modified on disk then we don't fetch it again
(unless (and time
(boundp 'my-inline-diff-old-file-modification-time)
(equal my-inline-diff-old-file-modification-time time))
(if (/= 0 (with-temp-file oldfile
(let ((default-directory (expand-file-name
(locate-dominating-file
(buffer-file-name file)
".git"))))
(call-process "git" nil (current-buffer) nil
"show" (concat "HEAD:"
(substring
(buffer-file-name file)
(length default-directory)))))))
(error (buffer-string)))
(make-local-variable 'my-inline-diff-old-file-modification-time)
(setq my-inline-diff-old-file-modification-time time)
(setq inputchanged t))))
(save-excursion
(with-current-buffer (get-buffer-create "*diffbuff*")
(when inputchanged
(erase-buffer)
(if (= 2 (call-process "git" nil (current-buffer) nil
"diff"
"-U100000"
"--word-diff=porcelain"
"--word-diff-regex=."
oldfile
newfile))
(error (buffer-string)))
(goto-char (point-min))
(re-search-forward "^@" nil t)
(forward-line 1)
(let ((line 1)
(pos-in-line 0)
changes
previous)
(while (not (eobp))
(cond
;; newline -------------------------------------------------------------
((eq (char-after) ?~)
(if (eq previous ?-)
(let ((change (car changes)))
(setcar changes
(plist-put change 'text (concat (plist-get change 'text)
"\n"))))
(if (eq previous ?+)
(let ((change (car changes)))
(setcar changes
(plist-put change 'length (1+ (plist-get change 'length))))))
(incf line))
(setq pos-in-line 0))
;; deletion -------------------------------------------------------------
((eq (char-after) ?-)
(let ((text (buffer-substring-no-properties
(1+ (point))
(line-end-position))))
(if (eq previous ?-)
(let ((change (car changes)))
(setcar changes
(plist-put change 'text (concat (plist-get change 'text)
text))))
(push (list 'line line
'pos pos-in-line
'action 'remove
'text text)
changes))))
;; addition -------------------------------------------------------------
((eq (char-after) ?+)
(push (list 'line line
'pos pos-in-line
'action 'add
'length (- (line-end-position) (1+ (point))))
changes)
(incf pos-in-line (plist-get (car changes) 'length)))
((eq (char-after) ? )
(incf pos-in-line (- (line-end-position) (1+ (point))))))
(unless (eq (char-after) ?~)
(setq previous (char-after)))
(forward-line 1))
(with-current-buffer file
(make-local-variable 'my-inline-diff-changes)
(setq my-inline-diff-changes (nreverse changes)))))
(with-current-buffer file
(goto-char (point-min))
(remove-overlays)
(let ((line 1))
(dolist (change (and (boundp 'my-inline-diff-changes)
my-inline-diff-changes))
(forward-line (- (plist-get change 'line) line))
(setq line (plist-get change 'line))
(if (eq (plist-get change 'action) 'add)
(let ((o (make-overlay
(+ (line-beginning-position)
(plist-get change 'pos))
(+ (line-beginning-position)
(plist-get change 'pos)
(plist-get change 'length)))))
(overlay-put o 'face 'diff-added)
(overlay-put o 'keymap my-inline-diff-revert-map))
(let* ((pos (+ (line-beginning-position)
(plist-get change 'pos)))
(overlay (make-overlay pos pos)))
(overlay-put overlay
'before-string
(propertize (plist-get change 'text)
'face 'diff-removed))
(let ((o (make-overlay
pos
(1+ pos))))
(overlay-put o
'keymap my-inline-diff-revert-map)
(overlay-put o
'my-inline-diff-overlay overlay))))))
(when (and (boundp 'my-inline-diff-changes)
my-inline-diff-changes)
(make-local-variable 'my-inline-diff-status)
(setq my-inline-diff-status 'full)))))))
(defun my-inline-diff-revert ()
(interactive)
;; fixme: there can be other overlays too at point
(let ((overlay (car (overlays-at (point)))))
(when (yes-or-no-p "Revert this change?")
(if (eq (overlay-get overlay 'face) 'diff-added)
(delete-region (overlay-start overlay) (overlay-end overlay))
(let* ((o (overlay-get overlay 'my-inline-diff-overlay))
(str (overlay-get o 'before-string)))
(delete-overlay o)
(insert (propertize str 'face nil))))
(delete-overlay overlay))))
(provide 'inline-diff)
;; Demo steps:
;;
;; - this file is under version control in Git, let's make some changes
;; compared to the latest committed version (these changes are taken
;; from the buffer, so the file does not have to be saved to show
;; them)
;;
;; - inline diff can be toggled with a command which I bound to a key,
;; so let's activate it
;;
;; - as you can see changes made since the latest version are shown
;; with diff colors (green: added, reddish: removed)
;;
;; - calling the command again shows a compressed diff with the
;; unchanged parts hidden
;;
;; - calling the command again hides the inline diff, so it works in a
;; cycle
;;
;; - the inline diff updates as you type with some delay, so the
;; highlight does not affect typing
;;
;; - you can also revert changes in the file by putting cursor on the
;; added part, or after it, in case of removal and press a hotkey
;; which I set to F1, because it's easy to reach
;;
;; - The end.
;;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment