Last active
March 22, 2022 07:12
-
-
Save codecoll/84245ad37efd947a4d8e3fd494a00091 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;; 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