352 lines
11 KiB
EmacsLisp
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
|