FG42/conf/emacs.d/git-gutter.el

352 lines
11 KiB
EmacsLisp

;;; git-gutter.el --- Port of Sublime Text 2 plugin GitGutter
;; Copyright (C) 2013 by Syohei YOSHIDA
;; Author: Syohei YOSHIDA <syohex@gmail.com>
;; URL: https://github.com/syohex/emacs-git-gutter
;; Version: 0.19
;; 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 <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Port of GitGutter which is a plugin of Sublime Text2
;;; Code:
(eval-when-compile
(require 'cl))
(defgroup git-gutter nil
"Port GitGutter"
:prefix "git-gutter:"
:group 'vc)
(defcustom git-gutter:window-width nil
"Character width of gutter window. Emacs mistakes width of some characters.
It is better to explicitly assign width to this variable, if you use full-width
character for signs of changes"
:type 'integer
:group 'git-gutter)
(defcustom git-gutter:diff-option ""
"Option of 'git diff'"
:type 'string
:group 'git-gutter)
(defcustom git-gutter:modified-sign "="
"Modified sign"
:type 'string
:group 'git-gutter)
(defcustom git-gutter:added-sign "+"
"Added sign"
:type 'string
:group 'git-gutter)
(defcustom git-gutter:deleted-sign "-"
"Deleted sign"
:type 'string
:group 'git-gutter)
(defcustom git-gutter:unchanged-sign nil
"Deleted sign"
:type 'string
:group 'git-gutter)
(defcustom git-gutter:always-show-gutter nil
"Always show gutter"
:type 'boolean
:group 'git-gutter)
(defcustom git-gutter:lighter " GitGutter"
"Minor mode lighter in mode-line"
:type 'string
:group 'git-gutter)
(defface git-gutter:modified
'((t (:foreground "magenta" :weight bold)))
"Face of modified"
:group 'git-gutter)
(defface git-gutter:added
'((t (:foreground "green" :weight bold)))
"Face of added"
:group 'git-gutter)
(defface git-gutter:deleted
'((t (:foreground "red" :weight bold)))
"Face of deleted"
:group 'git-gutter)
(defface git-gutter:unchanged
'((t (:background "yellow")))
"Face of unchanged"
:group 'git-gutter)
(defvar git-gutter:view-diff-function #'git-gutter:view-diff-infos
"Function of viewing changes")
(defvar git-gutter:clear-function #'git-gutter:clear-overlays
"Function of clear changes")
(defvar git-gutter:enabled nil)
(defvar git-gutter:overlays nil)
(defvar git-gutter:diffinfos nil)
(defun git-gutter:in-git-repository-p ()
(with-temp-buffer
(let ((cmd "git rev-parse --is-inside-work-tree"))
(when (zerop (call-process-shell-command cmd nil t))
(goto-char (point-min))
(string= "true" (buffer-substring-no-properties
(point) (line-end-position)))))))
(defun git-gutter:root-directory ()
(with-temp-buffer
(let* ((cmd "git rev-parse --show-toplevel")
(ret (call-process-shell-command cmd nil t)))
(unless (zerop ret)
(error "Here is not git repository!!"))
(goto-char (point-min))
(let ((root (buffer-substring-no-properties (point) (line-end-position))))
(when (string= root "")
(error "You maybe be in '.git/' directory"))
(file-name-as-directory root)))))
(defun git-gutter:changes-to-number (str)
(if (string= str "")
1
(string-to-number str)))
(defun git-gutter:make-diffinfo (type start &optional end)
(list :type type :start-line start :end-line end))
(defun git-gutter:diff (curfile)
(let ((cmd (format "git diff --no-ext-diff -U0 %s %s" git-gutter:diff-option curfile))
(regexp "^@@ -\\([0-9]+\\),?\\([0-9]*\\) \\+\\([0-9]+\\),?\\([0-9]*\\) @@"))
(with-temp-buffer
(let ((ret (call-process-shell-command cmd nil t)))
(unless (or (zerop ret))
(error (format "Failed '%s'" cmd))))
(goto-char (point-min))
(loop while (re-search-forward regexp nil t)
for orig-line = (string-to-number (match-string 1))
for new-line = (string-to-number (match-string 3))
for orig-changes = (git-gutter:changes-to-number (match-string 2))
for new-changes = (git-gutter:changes-to-number (match-string 4))
for end-line = (1- (+ new-line new-changes))
collect
(cond ((zerop orig-changes)
(git-gutter:make-diffinfo 'added new-line end-line))
((zerop new-changes)
(git-gutter:make-diffinfo 'deleted (1- orig-line)))
(t
(git-gutter:make-diffinfo 'modified new-line end-line)))))))
(defun git-gutter:line-to-pos (line)
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(point)))
(defmacro git-gutter:before-string (sign)
`(propertize " " 'display `((margin left-margin) ,sign)))
(defun git-gutter:select-face (type)
(case type
(added 'git-gutter:added)
(modified 'git-gutter:modified)
(deleted 'git-gutter:deleted)))
(defun git-gutter:select-sign (type)
(case type
(added git-gutter:added-sign)
(modified git-gutter:modified-sign)
(deleted git-gutter:deleted-sign)))
(defun git-gutter:propertized-sign (type)
(let ((sign (git-gutter:select-sign type))
(face (git-gutter:select-face type)))
(propertize sign 'face face)))
(defun git-gutter:view-region (sign start-line end-line)
(let ((beg (git-gutter:line-to-pos start-line)))
(goto-char beg)
(while (and (<= (line-number-at-pos) end-line) (not (eobp)))
(git-gutter:view-at-pos sign (point))
(forward-line 1))))
(defun git-gutter:view-at-pos (sign pos)
(let ((ov (make-overlay pos pos)))
(overlay-put ov 'before-string (git-gutter:before-string sign))
(push ov git-gutter:overlays)))
(defun git-gutter:view-diff-info (diffinfo)
(let* ((start-line (plist-get diffinfo :start-line))
(end-line (plist-get diffinfo :end-line))
(type (plist-get diffinfo :type))
(sign (git-gutter:propertized-sign type)))
(case type
((modified added) (git-gutter:view-region sign start-line end-line))
(deleted (git-gutter:view-at-pos
sign (git-gutter:line-to-pos start-line))))))
(defun git-gutter:sign-width (sign)
(loop for s across sign
sum (char-width s)))
(defun git-gutter:longest-sign-width ()
(let ((signs (list git-gutter:modified-sign
git-gutter:added-sign
git-gutter:deleted-sign)))
(when git-gutter:unchanged-sign
(add-to-list 'signs git-gutter:unchanged-sign))
(apply #'max (mapcar #'git-gutter:sign-width signs))))
(defun git-gutter:view-for-unchanged ()
(save-excursion
(let ((sign (propertize git-gutter:unchanged-sign
'face 'git-gutter:unchanged)))
(goto-char (point-min))
(while (not (eobp))
(git-gutter:view-at-pos sign (point))
(forward-line 1)))))
(defun git-gutter:view-diff-infos (diffinfos)
(let ((curwin (get-buffer-window))
(win-width (or git-gutter:window-width
(git-gutter:longest-sign-width))))
(when git-gutter:unchanged-sign
(git-gutter:view-for-unchanged))
(when diffinfos
(save-excursion
(mapc #'git-gutter:view-diff-info diffinfos)))
(when (or git-gutter:always-show-gutter diffinfos git-gutter:unchanged-sign)
(set-window-margins curwin win-width (cdr (window-margins curwin))))))
(defun git-gutter:delete-overlay ()
(mapc #'delete-overlay git-gutter:overlays)
(setq git-gutter:overlays nil)
(let ((curwin (get-buffer-window)))
(set-window-margins curwin 0 (cdr (window-margins curwin)))))
(defun git-gutter:process-diff (curfile)
(let ((diffinfos (git-gutter:diff curfile)))
(setq git-gutter:diffinfos diffinfos)
(funcall git-gutter:view-diff-function diffinfos)))
(defun git-gutter:clear-overlays ()
(git-gutter:delete-overlay))
(defun git-gutter:search-near-diff-index (diffinfos is-reverse)
(loop with current-line = (line-number-at-pos)
with cmp-fn = (if is-reverse '> '<)
for diffinfo in (if is-reverse (reverse diffinfos) diffinfos)
for index = 0 then (1+ index)
for start-line = (plist-get diffinfo :start-line)
when (funcall cmp-fn current-line start-line)
return (if is-reverse
(1- (- (length diffinfos) index))
index)))
;;;###autoload
(defun git-gutter:next-diff (arg)
(interactive "p")
(when git-gutter:diffinfos
(let* ((is-reverse (< arg 0))
(diffinfos git-gutter:diffinfos)
(len (length diffinfos))
(index (git-gutter:search-near-diff-index diffinfos is-reverse))
(real-index (if index
(let ((next (if is-reverse (1+ index) (1- index))))
(mod (+ arg next) len))
(if is-reverse (1- (length diffinfos)) 0)))
(diffinfo (nth real-index diffinfos)))
(goto-char (point-min))
(forward-line (1- (plist-get diffinfo :start-line))))))
;;;###autoload
(defun git-gutter:previous-diff (arg)
(interactive "p")
(git-gutter:next-diff (- arg)))
;;;###autoload
(defun git-gutter ()
(interactive)
(git-gutter:delete-overlay)
(let ((file (buffer-file-name)))
(when (and file (file-exists-p file))
(let* ((gitroot (git-gutter:root-directory))
(default-directory gitroot)
(current-file (file-relative-name file gitroot)))
(git-gutter:process-diff current-file)
(setq git-gutter:enabled t)))))
;;;###autoload
(defun git-gutter:clear ()
(interactive)
(funcall git-gutter:clear-function)
(setq git-gutter:enabled nil))
;;;###autoload
(defun git-gutter:toggle ()
(interactive)
(if git-gutter:enabled
(git-gutter:clear)
(git-gutter)))
(defun git-gutter:check-file-and-directory ()
(and (buffer-file-name)
default-directory (file-directory-p default-directory)))
;;;###autoload
(define-minor-mode git-gutter-mode ()
"Git-Gutter mode"
:group 'git-gutter
:init-value nil
:global nil
:lighter git-gutter:lighter
(if git-gutter-mode
(if (and (git-gutter:check-file-and-directory)
(git-gutter:in-git-repository-p))
(progn
(make-local-variable 'git-gutter:overlays)
(make-local-variable 'git-gutter:enabled)
(make-local-variable 'git-gutter:diffinfos)
(add-hook 'after-save-hook 'git-gutter nil t)
(add-hook 'after-revert-hook 'git-gutter nil t)
(add-hook 'change-major-mode-hook 'git-gutter nil t)
(add-hook 'window-configuration-change-hook 'git-gutter nil t)
(run-with-idle-timer 0 nil 'git-gutter))
(message "Here is not Git work tree")
(git-gutter-mode -1))
(remove-hook 'after-save-hook 'git-gutter t)
(remove-hook 'after-revert-hook 'git-gutter t)
(remove-hook 'change-major-mode-hook 'git-gutter t)
(remove-hook 'window-configuration-change-hook 'git-gutter t)
(git-gutter:clear)))
;;;###autoload
(define-global-minor-mode global-git-gutter-mode
git-gutter-mode
(lambda ()
(when (and (not (minibufferp)) (buffer-file-name))
(git-gutter-mode 1)))
:group 'git-gutter)
(provide 'git-gutter)
;;; git-gutter.el ends here