diff --git a/Cask b/Cask index 09184ea..c9db627 100644 --- a/Cask +++ b/Cask @@ -4,7 +4,9 @@ (package-file "evil-collection.el") (development + (depends-on "evil") (depends-on "f") (depends-on "ert-runner") (depends-on "package-lint") - (depends-on "annalist")) + (depends-on "annalist") + (depends-on "magit")) diff --git a/Makefile b/Makefile index d436604..381885c 100755 --- a/Makefile +++ b/Makefile @@ -1,14 +1,21 @@ EMACS ?= emacs +CASK ?= cask + +LOADPATH = -L . +TESTPATH = -L ./test + +ELPA_DIR = \ + .cask/$(shell $(EMACS) -Q --batch --eval '(princ emacs-version)')/elpa compile: - cask exec $(EMACS) -Q -batch \ + $(CASK) exec $(EMACS) -Q -batch \ -L . \ --eval '(setq evil-want-integration nil)' \ --eval '(setq byte-compile-error-on-warn t)' \ -f batch-byte-compile *.el modes/*/*.el lint: - cask exec $(EMACS) -Q -batch \ + $(CASK) exec $(EMACS) -Q -batch \ --eval "(require 'package)" \ --eval "(push '(\"melpa\" . \"http://melpa.org/packages/\") package-archives)" \ --eval "(package-initialize)" \ @@ -18,7 +25,18 @@ lint: --eval "(advice-add 'package-lint--check-version-regexp-list :around 'ignore)" \ -f package-lint-batch-and-exit *.el modes/*/*.el -test: - cask exec ert-runner +test: elpa + $(CASK) exec $(EMACS) -Q -batch $(LOADPATH) $(TESTPATH) \ +-l evil-collection-test.el -l evil-collection-magit-tests.el -f ert-run-tests-batch-and-exit -.PHONY: compile lint test +magit-test: elpa + $(CASK) exec $(EMACS) -Q -batch $(LOADPATH) $(TESTPATH) \ +-l evil-collection-magit-tests.el -f ert-run-tests-batch-and-exit + +elpa: $(ELPA_DIR) +$(ELPA_DIR): Cask + $(CASK) install + mkdir -p $(ELPA_DIR) + touch $@ + +.PHONY: compile lint test elpa diff --git a/modes/magit/README.org b/modes/magit/README.org new file mode 100644 index 0000000..6664989 --- /dev/null +++ b/modes/magit/README.org @@ -0,0 +1,211 @@ +* Black magic + + This library configures Magit and Evil to play well with each other. For some + background see [[https://github.com/justbur/evil-magit/issues/1][Issue #1]]. + + *Note*: I intend to track the latest commits to the master branch of the [[https://github.com/magit/magit][magit + repo]], meaning the keybindings here are potentially ahead of the last stable + release of magit. Once the code in evil-collection-magit stabilizes, I may switch to + primarily tracking the stable release of magit and secondarily track the latest + commits to master. Any help is welcomed. + +* Recent Changes (most recent first) + + 1. [2019-09-04] Don't use =evil-next-visual-line= and + =evil=previous-visual-line=. See [[https://github.com/emacs-evil/evil-magit/issues/70][Issue #70]]. + 1. [2019-06-16] Added =evil-collection-magit-stage-untracked-file-with-intent= at + =I=. See [[https://github.com/emacs-evil/evil-magit/issues/67][Issue #67]]. + 2. [2019-02-15] Added =forge-dispatch= at =@=. + 3. [2018-05-22] Added =evil-collection-magit-use-z-for-folds=. See docstring for more + information. + 4. [2018-03-13] Added basic evil support for =magit-list-repositories=. + 5. [2016-07-27] Moved submodules popup to ' and added the new subtree popup at + ". This is not mnemonic in any way but easy to reach and keeps the two keys + together. + 6. [2016-03-24] Moved =magit-diff-less-context= to = to fix conflict with + moved revert. + 7. [2016-03-21] Moved revert commands from =o= and =O= to =-= and + =_=. Rationale is that you are subtracting a commit. This makes room for + =o= and =O= to be reset and the new reset popup command. Think of resetting + to an "old" state. =¯\_(ツ)_/¯= + 8. Added =evil-collection-magit-want-horizontal-movement=. Use =h= and =l= for movement + like vim, moving =h= to =H=, =l= to =L=, and =L= to =C-l=. + 9. Added =evil-collection-magit-toggle-text-mode= on =C-t=. This is a quick way to enter + text mode in a magit buffer, which allows arbitrary movement, copying, etc. + Use =C-t= to return to the previous magit mode. + 10. When =evil-collection-magit-use-y-for-yank= is non nil, =C-w= will prefix the evil + window switching functions from magit buffers. + 11. =evil-collection-magit-use-y-for-yank= is now the default. It has worked well for me so + far, and I've had good feedback, but please let me know if you see issues. + You can use the original behavior with =(setq evil-collection-magit-use-y-for-yank + nil)=. See the table below for a summary of differences. + + +* Installation and Use + + Evil and Magit are both required. After requiring those packages, the following + will setup the new key bindings for you. + + #+BEGIN_SRC emacs-lisp + ;; optional: this is the evil state that evil-collection-magit will use + ;; (setq evil-collection-magit-state 'normal) + ;; optional: disable additional bindings for yanking text + ;; (setq evil-collection-magit-use-y-for-yank nil) + (evil-collection-init) + #+END_SRC emacs-lisp + +** Note on Evil usage + + This package assumes that you either use the global variant of evil mode (e.g., + through =(evil-mode 1)=), or at least have =evil-local-mode= (the local variant) + enabled in the magit buffers you want these bindings to take effect in. When + evil is disabled in a magit buffer, this package will not affect the default key + bindings (with one minor exception). + +** Note on =evil-collection-magit-use-y-for-yank= + + =evil-collection-magit-use-y-for-yank= enables evil's visual state for linewise selection, + and as a consequnce =y= will yank text from the buffer. + + With this enabled which it is by default evil-collection-magit uses =v= and =V= to select + by line. Selection in magit occurs linewise, so this choice is to avoid + confusion that might arise if someone thought they could stage part of a line + with =v= for example. + +** Text mode + + Text mode can be toggled with =evil-collection-magit-toggle-text-mode= (triggered with + =C-t= or =\=). This takes nearly any magit buffer out of the related magit mode + and puts it into =text-mode=. This allows free movement in the buffer using the + standard evil movement and selection commands, making it easy to for example + copy arbitrary text in the buffer. It also effectively prevents magit keys from + shadowing evil ones, so =f= runs =evil-find-char= instead of + =magit-fetch-popup=, allowing all vim related movement commands to be used in + magit buffers. You can think of this if you like as another state for evil-collection-magit + to be in. + + Several requests have been made to allow selecting and copying arbitrary text in + the magit buffers, but there are many conflicts between evil bindings and magit + bindings and there is no elegant solution to this problem in my opinion. Text + mode is the best that I have come up with. + +* Key Bindings + + The basic key binding scheme for evil-collection-magit (EM) is described in the following + tables. Blank columns indicate that the key is carried over from the left. + + | Category | Default | EM w/o yank opt | w/ yank opt (default) | w/ horiz move | w/ folds | + |------------------------+---------+------------------------+-----------------------+---------------+----------| + | cherry pick | =a/A= | | | | | + | branch | =b= | | | | | + | bisect | =B= | | | | | + | commit | =c= | | | | | + | diff | =d/D= | | | | | + | ediff | =e/E= | | | | | + | fetch | =f= | | | | | + | pull | =F= | | | | | + | refresh | =g= | =gr/gR= (=g= in popup) | | | | + | help | =h/?= | | | =H/?= | | + | ignore | =i/I= | | | | | + | intent to stage | =I= | | | | | + | jump | =j= | =g= | | | | + | delete | =k= | =x= | | | | + | untrack | =K= | =X= | | | | + | log | =l/L= | | | =L/C-l= | | + | merge | =m= | | | | | + | remote | =M= | | | | | + | next section | =n= | =C-j= | | | | + | next section sibling | =M-n= | =gj= or =]= | | | | + | submodule | =o= | ' | | | | + | subtree | =O= | " | | | | + | prev section | =p= | =C-k= | | | | + | prev section sibling | =M-p= | =gk= or =[= | | | | + | push | =P= | =P= or =p= | | | | + | quit | =q= | =q= or =ESC= | | | | + | rebase | =r= | | | | | + | rename | =R= | | | | | + | stage | =s/S= | | | | | + | tag | =t= | | | | | + | notes | =T= | | | | | + | unstage | =u/U= | | | | | + | revert | =v/V= | =-/_= | | | | + | am | =w= | | | | | + | patch | =W= | | | | | + | reset | =x/X= | =o/O= | | | | + | show-refs | =y= | | =yr= (=y= in popup) | | | + | cherry | =Y= | | | | | + | stash | =z/Z= | | | | =Z= | + | git-cmd | =:= | =¦= | | | | + | run | =!= | | | | | + | forge | =@= | | | | | + | diff less/more context | =-/+= | = / + | | | | + | copy section info | =C-w= | | =ys= | | | + | copy buffer info | =M-w= | | =yb= | | | + +** New Commands + + | Command | EM w/o yank opt | EM w/ yank opt (default) | w/ horiz move | + |-----------------------------+--------------------------+--------------------------+---------------| + | evil-goto-line | =G= | | | + | evil-next-visual-line | =j= | | | + | evil-previous-visual-line | =k= | | | + | evil-backward-char | under =M-x= | | =h= | + | evil-forward-char | under =M-x= | | =l= | + | evil-search-next | =n= | | | + | evil-search-previous | =N= | | | + | set-mark-command | =v= or =V= | =C-SPC= | | + | evil-visual-line | under =M-x= | =v= or =V= | | + | evil-ex | =:= | | | + | evil-search-forward | =/= | | | + | evil-scroll-page-up | =C-b= | | | + | evil-scroll-down | =C-d= | | | + | evil-scroll-page-down | =C-f= | | | + | evil-scroll-up | =C-u= (if =C-u= scrolls) | | | + | evil-emacs-state | =C-z= | | | + | evil-yank-line | under =M-x= | =yy= | | + | evil-window-map | under =M-x= | =C-w= | | + | evil-collection-magit-toggle-text-mode | =C-t/\= | | | + + + Any other bindings are meant to be consistent with these. + + Use =evil-collection-magit-revert= to revert changes made by evil-collection-magit to the default + evil+magit behavior. + +** To add other common evil commands + + Some may want =?= to search backward instead of launching the popup which is + also bound to =h=. To get this behavior, add the following line after + =(evil-collection-init)= in your configuration. + + #+BEGIN_SRC emacs-lisp +(evil-define-key evil-collection-magit-state magit-mode-map "?" 'evil-search-backward) + #+END_SRC + + Most (but not all) magit bindings are in =magit-mode-map=, so other commands can + be bound in this way too. + +** To remove commands + + Typically, to prevent evil-collection-magit from overriding the default behavior with evil + and magit loaded, you should bind the respective key to =nil= after loading + evil-collection-magit. For example, to make =escape= behave as default: + + #+BEGIN_SRC emacs-lisp +(evil-define-key* evil-collection-magit-state magit-mode-map [escape] nil) + #+END_SRC + +* Known Conflicts + + These are the third-party packages that conflict with these bindings and will + probably need to be disabled in magit buffers for evil-collection-magit to work properly. + + 1. [[https://github.com/hlissner/evil-snipe][evil-snipe]] + 2. [[https://github.com/syl20bnr/evil-escape][evil-escape]] with [[https://github.com/justbur/evil-magit/issues/4][certain escape sequences]] + +* Disclaimer + + Given the complexity of magit key bindings combined with the complexity of git + itself, it is possible that there are some rough edges where the current binding + is not the expected one in a buffer. It will be very helpful for you to report + any such instances. diff --git a/modes/magit/evil-collection-magit.el b/modes/magit/evil-collection-magit.el index 8fbed2d..2a9c6fc 100644 --- a/modes/magit/evil-collection-magit.el +++ b/modes/magit/evil-collection-magit.el @@ -1,36 +1,39 @@ -;;; evil-collection-magit.el --- Bindings for `magit' -*- lexical-binding: t -*- +;;; evil-collection-magit.el --- Evil-based key bindings for magit -;; Copyright (C) 2018 James Nguyen +;; Copyright (C) 2015-2016 Justin Burkett -;; Author: James Nguyen -;; Maintainer: James Nguyen +;; Author: Justin Burkett +;; Maintainer: Justin Burkett +;; James Nguyen ;; Pierre Neidhardt -;; URL: https://github.com/emacs-evil/evil-collection -;; Version: 0.0.1 -;; Package-Requires: ((emacs "25.1")) -;; Keywords: evil, emacs, tools +;; Package-Requires: ((emacs "25.1") (evil "1.2.3") (magit "2.6.0")) +;; Homepage: https://github.com/emacs-evil/evil-collection +;; Version: 0.4.1 -;; 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, +;; This file 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, or (at your +;; option) any later version. +;; +;; This file 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 . +;; +;; For a full copy of the GNU General Public License +;; see . ;;; Commentary: -;;; Bindings for `magit' -;;; This file is to work around an issue described in -;;; https://github.com/emacs-evil/evil-collection/issues/108 -;;; Ideally this file is only temporary and should be removed once -;;; #108 is resolved. + +;; This library configures Magit and Evil to play well with each +;; other. For some background see https://github.com/magit/evil-magit/issues/1. + +;; See the README at +;; https://github.com/emacs-evil/evil-collection/tree/master/modes/magit +;; for a table describing the key binding changes. ;;; Code: + (require 'evil-collection) (require 'magit nil t) @@ -39,13 +42,616 @@ (defconst evil-collection-magit-maps '(magit-blame-mode-map magit-blame-read-only-mode-map)) +(defcustom evil-collection-magit-use-y-for-yank t + "When non nil, replace \"y\" for `magit-show-refs-popup' with +\"yy\" for `evil-yank-line', `ys' `magit-copy-section-value', +\"yb\" for `magit-copy-buffer-revision' and \"yr\" for +`magit-show-refs-popup'. This keeps \"y\" for +`magit-show-refs-popup' in the help +popup (`magit-dispatch-popup'). Default is t." + :group 'magit + :type 'boolean) + +(defcustom evil-collection-magit-want-horizontal-movement nil + "When non nil, use \"h\" and \"l\" for horizontal movement in +most magit buffers. The old \"h\" for help is moved to \"H\" and +similarly for the old \"l\" for the log popup. The old \"L\" is +then put on \"C-l\"." + :group 'magit + :type 'boolean) + +(defcustom evil-collection-magit-use-z-for-folds nil + "When non nil, use \"z\" as a prefix for common vim fold commands, such as + - z1 Reset visibility to level 1 for all sections + - z2 Reset visibility to level 2 for all sections + - z3 Reset visibility to level 3 for all sections + - z4 Reset visibility to level 4 for all sections + - za Toggle a section + - zo Show section + - zO Show sections recursively + - zc Hide section + - zC Hide sections recursively + - zr Same as z4. + +When this option is enabled, the stash popup is available on \"Z\"." + :group 'magit + :type 'boolean) + +(defcustom evil-collection-magit-state (if evil-collection-magit-use-y-for-yank 'normal 'motion) + "State to use for most magit buffers." + :group 'magit + :type 'symbol) + +;; without this set-mark-command activates visual-state which is just annoying +;; and introduces possible bugs +(defun evil-collection-magit-remove-visual-activate-hook () + (when (derived-mode-p 'magit-mode) + (remove-hook 'activate-mark-hook 'evil-visual-activate-hook t))) +(add-hook 'evil-local-mode-hook 'evil-collection-magit-remove-visual-activate-hook) + +(defun evil-collection-magit-maybe-deactivate-mark () + "Deactivate mark if region is active. Used for ESC binding." + (interactive) + (when (region-active-p) (deactivate-mark))) + +(defvar evil-collection-magit-emacs-to-default-state-modes + '(git-commit-mode) + "Modes that should be in the default evil state") + +(defvar evil-collection-magit-emacs-to-evil-collection-magit-state-modes + '(git-rebase-mode + magit-mode + magit-cherry-mode + magit-diff-mode + magit-log-mode + magit-log-select-mode + magit-process-mode + magit-reflog-mode + magit-refs-mode + magit-revision-mode + magit-stash-mode + magit-stashes-mode + magit-status-mode) + "Modes that switch from emacs state to `evil-collection-magit-state'") + +(defvar evil-collection-magit-default-to-evil-collection-magit-state-modes + '(magit-blob-mode + magit-gitflow-mode) + "Modes that switch from default state to `evil-collection-magit-state'") + +(defvar evil-collection-magit-untouched-modes + ;; TODO do something here + '(git-popup-mode + magit-blame-mode + magit-blame-read-only-mode + magit-file-mode) + "Modes whose evil states are unchanged") + +(defvar evil-collection-magit-ignored-modes + '(git-commit-major-mode + magit-auto-revert-mode + magit-blame-put-keymap-before-view-mode + magit-diff-mode + magit-merge-preview-mode + transient-resume-mode + magit-rebase-mode + magit-file-mode-major-mode + magit-wip-after-save-mode + magit-wip-after-save-local-mode-major-mode + magit-wip-after-save-local-mode + magit-wip-after-apply-mode + magit-wip-before-change-mode + magit-wip-initial-backup-mode + magit-wip-mode + ;; gh + magit-gh-pulls-mode + ;; git-gutter + git-gutter-mode + git-gutter-mode-major-mode + git-gutter+-commit-mode + git-gutter+-mode + git-gutter+-enable-fringe-display-mode + git-gutter+-enable-default-display-mode) + "Currently ignored modes. They are collected here for testing +purposes.") + +(defun evil-collection-magit-set-initial-states () + "Set the initial state for relevant modes." + (dolist (mode (append evil-collection-magit-emacs-to-evil-collection-magit-state-modes + evil-collection-magit-default-to-evil-collection-magit-state-modes)) + (evil-set-initial-state mode evil-collection-magit-state)) + (dolist (mode evil-collection-magit-emacs-to-default-state-modes) + (evil-set-initial-state mode evil-default-state))) + +(defun evil-collection-magit-revert-initial-states () + "Revert the initial state for modes to their values before +evil-collection-magit was loaded." + (dolist (mode (append evil-collection-magit-emacs-to-evil-collection-magit-state-modes + evil-collection-magit-emacs-to-default-state-modes)) + (evil-set-initial-state mode 'emacs)) + (dolist (mode evil-collection-magit-default-to-evil-collection-magit-state-modes) + (evil-set-initial-state mode evil-default-state))) + +(defvar evil-collection-magit-section-maps + '(magit-branch-section-map + magit-commit-section-map + magit-commit-message-section-map + magit-error-section-map + magit-file-section-map + magit-hunk-section-map + magit-module-commit-section-map + magit-remote-section-map + magit-staged-section-map + magit-stash-section-map + magit-stashes-section-map + magit-tag-section-map + magit-unpulled-section-map + magit-unpushed-section-map + magit-unstaged-section-map + magit-untracked-section-map + + ;; new ones that I haven't looked at yet + magit-button-section-map + magit-commitbuf-section-map + magit-diffbuf-section-map + magit-diffstat-section-map + magit-headers-section-map + magit-message-section-map + ;; FIXME: deal with new bindings in this one + magit-module-section-map + magit-modules-section-map + magit-processbuf-section-map + magit-process-section-map + magit-pulls-section-map + magit-unmerged-section-map + magit-status-section-map + magit-worktree-section-map) + "All magit section maps. For testing purposes only at the +moment.") + +;; Old way of excluding newlines +;; (when evil-collection-magit-use-y-for-yank +;; (dolist (map evil-collection-magit-section-maps) +;; (when (and map (keymapp (symbol-value map))) +;; (map-keymap +;; (lambda (_ def) +;; (when (commandp def) +;; (evil-set-command-property def :exclude-newline t))) +;; (symbol-value map))))) + +(defvar evil-collection-magit-in-visual-pre-command) + +(defun evil-collection-magit--around-visual-pre-command (orig-func &rest args) + (let ((evil-collection-magit-in-visual-pre-command t)) + (apply orig-func args))) + +(defun evil-collection-magit--filter-args-visual-expand-region (arglist) + ;; pretend that the command has the :exclude-newline property by rewriting the + ;; EXCLUDE-NEWLINE arg to this function + (cons (and (bound-and-true-p evil-collection-magit-in-visual-pre-command) + (null (car arglist)) + (eq (evil-visual-type) 'line) + (derived-mode-p 'magit-mode)) + ;; shouldn't be necessary, but this will prevent it from failing if an + ;; arg is added. + (cdr arglist))) + +(when (and (fboundp 'advice-add) evil-collection-magit-use-y-for-yank) + (advice-add 'evil-visual-pre-command + :around #'evil-collection-magit--around-visual-pre-command) + (advice-add 'evil-visual-expand-region + :filter-args #'evil-collection-magit--filter-args-visual-expand-region)) + +(defvar evil-collection-magit-mode-map-bindings + (let ((states (if evil-collection-magit-use-y-for-yank + `(,evil-collection-magit-state visual) + `(,evil-collection-magit-state)))) + (append + `((,states magit-mode-map "g") + (,states magit-mode-map "C-j" magit-section-forward "n") + (,states magit-mode-map "gj" magit-section-forward-sibling "M-n") + (,states magit-mode-map "]" magit-section-forward-sibling "M-n") + (,states magit-mode-map "C-k" magit-section-backward "p") + (,states magit-mode-map "gk" magit-section-backward-sibling "M-p") + (,states magit-mode-map "[" magit-section-backward-sibling "M-p") + (,states magit-mode-map "gr" magit-refresh "g") + (,states magit-mode-map "gR" magit-refresh-all "G") + (,states magit-mode-map "x" magit-delete-thing "k") + (,states magit-mode-map "X" magit-file-untrack "K") + (,states magit-mode-map "-" magit-revert-no-commit "v") + (,states magit-mode-map "_" magit-revert "V") + (,states magit-mode-map "p" magit-push "P") + (,states magit-mode-map "o" magit-reset-quickly "x") + (,states magit-mode-map "O" magit-reset "X") + (,states magit-mode-map "|" magit-git-command ":") + (,states magit-mode-map "'" magit-submodule "o") + (,states magit-mode-map "\"" magit-subtree "O") + (,states magit-mode-map "=" magit-diff-less-context "-") + (,states magit-mode-map "@" forge-dispatch) + (,states magit-mode-map "j" evil-next-line) + (,states magit-mode-map "k" evil-previous-line) + (,states magit-mode-map "gg" evil-goto-first-line) + (,states magit-mode-map "G" evil-goto-line) + (,states magit-mode-map "C-d" evil-scroll-down) + (,states magit-mode-map "C-f" evil-scroll-page-down) + (,states magit-mode-map "C-b" evil-scroll-page-up) + (,states magit-mode-map ":" evil-ex) + + ;; these are to fix the priority of the log mode map and the magit mode map + ;; FIXME: Conflict between this and revert. Revert seems more important here + ;; (,states magit-log-mode-map "-" magit-log-half-commit-limit "-") + (,states magit-log-mode-map "=" magit-log-toggle-commit-limit "=") + + (,states magit-mode-map "S-SPC" magit-diff-show-or-scroll-up "SPC") + (,states magit-mode-map "S-DEL" magit-diff-show-or-scroll-down "DEL") + + ((,evil-collection-magit-state) magit-mode-map "C-z" evil-emacs-state) + ((,evil-collection-magit-state) magit-mode-map "" magit-mode-bury-buffer)) + + (if (eq evil-search-module 'evil-search) + `((,states magit-mode-map "/" evil-ex-search-forward) + (,states magit-mode-map "n" evil-ex-search-next) + (,states magit-mode-map "N" evil-ex-search-previous)) + `((,states magit-mode-map "/" evil-search-forward) + (,states magit-mode-map "n" evil-search-next) + (,states magit-mode-map "N" evil-search-previous))) + + `((,states magit-status-mode-map "gz" magit-jump-to-stashes "jz") + (,states magit-status-mode-map "gt" magit-jump-to-tracked "jt") + (,states magit-status-mode-map "gn" magit-jump-to-untracked "jn") + (,states magit-status-mode-map "gu" magit-jump-to-unstaged "ju") + (,states magit-status-mode-map "gs" magit-jump-to-staged "js") + (,states magit-status-mode-map "gfu" magit-jump-to-unpulled-from-upstream "jfu") + (,states magit-status-mode-map "gfp" magit-jump-to-unpulled-from-pushremote "jfp") + (,states magit-status-mode-map "gpu" magit-jump-to-unpushed-to-upstream "jpu") + (,states magit-status-mode-map "gpp" magit-jump-to-unpushed-to-pushremote "jpp") + (,states magit-status-mode-map "gh" magit-section-up "^") + (,states magit-diff-mode-map "gj" magit-section-forward) + (,states magit-diff-mode-map "gd" magit-jump-to-diffstat-or-diff "j") + ;; NOTE This is now transient-map and the binding is C-g. + ;; ((emacs) magit-popup-mode-map "" "q") + ) + + (when evil-collection-magit-want-horizontal-movement + `((,states magit-mode-map "H" magit-dispatch "h") + (,states magit-mode-map "L" magit-log "l") + (,states magit-mode-map "C-l" magit-log-refresh "L") + (,states magit-mode-map "h" evil-backward-char) + (,states magit-mode-map "l" evil-forward-char))) + + (when evil-want-C-u-scroll + `((,states magit-mode-map "C-u" evil-scroll-up))) + + (if evil-collection-magit-use-y-for-yank + `((,states magit-mode-map "v" evil-visual-line) + (,states magit-mode-map "V" evil-visual-line) + (,states magit-mode-map "C-w" evil-window-map) + (,states magit-mode-map "y") + (,states magit-mode-map "yy" evil-yank-line) + (,states magit-mode-map "yr" magit-show-refs "y") + (,states magit-mode-map "ys" magit-copy-section-value "C-w") + (,states magit-mode-map "yb" magit-copy-buffer-revision "M-w") + ((visual) magit-mode-map "y" evil-yank)) + `((,states magit-mode-map "v" set-mark-command) + (,states magit-mode-map "V" set-mark-command) + (,states magit-mode-map "" evil-collection-magit-maybe-deactivate-mark))) + + (when evil-collection-magit-use-z-for-folds + `((,states magit-mode-map "z") + (,states magit-mode-map "z1" magit-section-show-level-1-all) + (,states magit-mode-map "z2" magit-section-show-level-2-all) + (,states magit-mode-map "z3" magit-section-show-level-3-all) + (,states magit-mode-map "z4" magit-section-show-level-4-all) + (,states magit-mode-map "za" magit-section-toggle) + (,states magit-mode-map "zc" magit-section-hide) + (,states magit-mode-map "zC" magit-section-hide-children) + (,states magit-mode-map "zo" magit-section-show) + (,states magit-mode-map "zO" magit-section-show-children) + (,states magit-mode-map "zr" magit-section-show-level-4-all))))) + "evil-collection-magit bindings for major modes. Each element of this list +takes the form + +\(EVIL-STATE MAGIT-MAP NEW-KEY DEF ORIG-KEY\). + +ORIG-KEY is only used for testing purposes, and +denotes the original magit key for this command.") + +(dolist (binding evil-collection-magit-mode-map-bindings) + (when binding + (dolist (state (nth 0 binding)) + (evil-collection-define-key + state (nth 1 binding) (nth 2 binding) (nth 3 binding))))) + +(defvar evil-collection-magit-minor-mode-map-bindings + `(((,evil-collection-magit-state visual) magit-blob-mode-map "gj" magit-blob-next "n") + ((,evil-collection-magit-state visual) magit-blob-mode-map "gk" magit-blob-previous "p") + ((,evil-collection-magit-state visual) git-commit-mode-map "gk" git-commit-prev-message "M-p") + ((,evil-collection-magit-state visual) git-commit-mode-map "gj" git-commit-next-message "M-n") + ((normal) magit-blame-read-only-mode-map "j" evil-next-line) + ((normal) magit-blame-read-only-mode-map "C-j" magit-blame-next-chunk "n") + ((normal) magit-blame-read-only-mode-map "gj" magit-blame-next-chunk "n") + ((normal) magit-blame-read-only-mode-map "gJ" magit-blame-next-chunk-same-commit "N") + ((normal) magit-blame-read-only-mode-map "k" evil-previous-line) + ((normal) magit-blame-read-only-mode-map "C-k" magit-blame-previous-chunk "p") + ((normal) magit-blame-read-only-mode-map "gk" magit-blame-previous-chunk "p") + ((normal) magit-blame-read-only-mode-map "gK" magit-blame-previous-chunk-same-commit "P")) + "evil-collection-magit bindings for minor modes. Each element of +this list takes the form + +\(EVIL-STATE MAGIT-MAP NEW-KEY DEF ORIG-KEY)\. + +ORIG-KEY is only used for testing purposes, and +denotes the original magit key for this command.") + +(dolist (binding evil-collection-magit-minor-mode-map-bindings) + (when binding + (dolist (state (nth 0 binding)) + ;; TODO: Maybe switch to `evil-define-minor-mode-key' + (evil-collection-define-key + state (nth 1 binding) (nth 2 binding) (nth 3 binding))))) + +;; Make relevant maps into overriding maps so that they shadow the global evil +;; maps by default +(dolist (map (list magit-mode-map + magit-cherry-mode-map + magit-mode-map + magit-blob-mode-map + magit-diff-mode-map + magit-log-mode-map + magit-log-select-mode-map + magit-reflog-mode-map + magit-status-mode-map + magit-file-mode-map + magit-log-read-revs-map + magit-process-mode-map + magit-refs-mode-map)) + (evil-make-overriding-map map (if evil-collection-magit-use-y-for-yank + 'all + evil-collection-magit-state))) + +(evil-make-overriding-map magit-blame-read-only-mode-map 'normal) + +(eval-after-load 'magit-gh-pulls + `(evil-make-overriding-map magit-gh-pulls-mode-map ',evil-collection-magit-state)) + +;; Need to refresh evil keymaps when blame mode is entered. +(add-hook 'magit-blame-mode-hook 'evil-normalize-keymaps) + +(evil-set-initial-state 'magit-repolist-mode 'motion) +(evil-define-key 'motion magit-repolist-mode-map + (kbd "RET") 'magit-repolist-status + (kbd "gr") 'magit-list-repositories) +(add-hook 'magit-repolist-mode-hook 'evil-normalize-keymaps) + +(evil-set-initial-state 'magit-submodule-list-mode 'motion) +(evil-define-key 'motion magit-submodule-list-mode-map + (kbd "RET") 'magit-repolist-status + (kbd "gr") 'magit-list-submodules) +(add-hook 'magit-submodule-list-mode-hook 'evil-normalize-keymaps) + + +(eval-after-load 'git-rebase + `(progn + ;; for the compiler + (defvar git-rebase-mode-map) + (defvar evil-collection-magit-rebase-commands-w-descriptions + ;; nil in the first element means don't bind here + '(("p" git-rebase-pick "pick = use commit") + ("r" git-rebase-reword "reword = use commit, but edit the commit message") + ("e" git-rebase-edit "edit = use commit, but stop for amending") + ("s" git-rebase-squash "squash = use commit, but meld into previous commit") + ("f" git-rebase-fixup "fixup = like \"squash\", but discard this commit's log message") + ("x" git-rebase-exec "exec = run command (the rest of the line) using shell") + ("d" git-rebase-kill-line "drop = remove commit" "k") + ("u" git-rebase-undo "undo last change") + (nil with-editor-finish "tell Git to make it happen") + (nil with-editor-cancel "tell Git that you changed your mind, i.e. abort") + ("k" evil-previous-line "move point to previous line" "p") + ("j" evil-next-line "move point to next line" "n") + ("M-k" git-rebase-move-line-up "move the commit at point up" "\M-p") + ("M-j" git-rebase-move-line-down "move the commit at point down" "\M-n") + (nil git-rebase-show-commit "show the commit at point in another buffer"))) + + (dolist (cmd evil-collection-magit-rebase-commands-w-descriptions) + (when (car cmd) + (evil-collection-define-key evil-collection-magit-state 'git-rebase-mode-map + (car cmd) (nth 1 cmd)))) + + (evil-make-overriding-map git-rebase-mode-map evil-collection-magit-state) + + (defun evil-collection-magit-add-rebase-messages () + "Remove evil-state annotations and reformat git-rebase buffer." + (goto-char (point-min)) + (let ((inhibit-read-only t) + (state-regexp (format "<%s-state> " evil-collection-magit-state)) + (aux-map (evil-get-auxiliary-keymap git-rebase-mode-map evil-collection-magit-state))) + (save-excursion + (save-match-data + (flush-lines "^#.+ = ") + (goto-char (point-min)) + (when (and (boundp 'git-rebase-show-instructions) + git-rebase-show-instructions + (re-search-forward "^# Commands:\n" nil t)) + (dolist (cmd evil-collection-magit-rebase-commands-w-descriptions) + (insert + (format "# %-8s %s\n" + (if (and (car cmd) + (eq (nth 1 cmd) + (lookup-key aux-map (kbd (car cmd))))) + (car cmd) + (replace-regexp-in-string + state-regexp "" + (substitute-command-keys + (format "\\[%s]" (nth 1 cmd))))) + (nth 2 cmd))))))))) + (remove-hook 'git-rebase-mode-hook 'git-rebase-mode-show-keybindings) + (add-hook 'git-rebase-mode-hook 'evil-collection-magit-add-rebase-messages t))) + +;; section maps: evil's auxiliary maps don't work here, because these maps are +;; text overlays + +(defun evil-collection-magit-stage-untracked-file-with-intent () + "Call `magit-stage-untracked' with optional arg." + (interactive) + (when (and (derived-mode-p 'magit-mode) + (magit-apply--get-selection) + (eq (magit-diff-type) 'untracked)) + (magit-stage-untracked t))) + +(defvar evil-collection-magit-original-section-bindings + `((,(copy-keymap magit-file-section-map) "\C-j" magit-diff-visit-file-worktree) + (,(copy-keymap magit-hunk-section-map) "\C-j" magit-diff-visit-file-worktree)) + "For testing purposes only. The original magit keybindings that +evil-collection-magit affects.") + +(defun evil-collection-magit-adjust-section-bindings () + "Revert changed bindings in section maps generated by evil-collection-magit" + (define-key magit-file-section-map "I" + 'evil-collection-magit-stage-untracked-file-with-intent) + (define-key magit-file-section-map "\C-j" nil) ; breaking change + (define-key magit-hunk-section-map "\C-j" nil)) ; breaking change + +(defun evil-collection-magit-revert-section-bindings () + "Revert changed bindings in section maps generated by evil-collection-magit" + (define-key magit-file-section-map "I" nil) + (define-key magit-file-section-map "\C-j" 'magit-diff-visit-file-worktree) + (define-key magit-hunk-section-map "\C-j" 'magit-diff-visit-file-worktree)) + +;; Popups + +(defvar evil-collection-magit-dispatch-popup-backup + (copy-tree (get 'magit-dispatch 'transient--layout) t)) +(defvar evil-collection-magit-popup-keys-changed nil) + +(defvar evil-collection-magit-popup-changes + (append + (when evil-collection-magit-use-z-for-folds + '((magit-dispatch "z" "Z" magit-stash))) + (when evil-collection-magit-want-horizontal-movement + '((magit-dispatch "L" "\C-l" magit-log-refresh) + (magit-dispatch "l" "L" magit-log))) + '((magit-branch "x" "X" magit-branch-reset) + (magit-branch "k" "x" magit-branch-delete) + (magit-dispatch "o" "'" magit-submodule) + (magit-dispatch "O" "\"" magit-subtree) + (magit-dispatch "V" "_" magit-revert) + (magit-dispatch "X" "O" magit-reset) + (magit-dispatch "v" "-" magit-reverse) + (magit-dispatch "k" "x" magit-discard) + (magit-remote "k" "x" magit-remote-remove) + (magit-revert "v" "o" magit-revert-no-commit) + ;; FIXME: how to properly handle a popup with a key that appears twice (in + ;; `define-transient-command' definition)? Currently we rely on: + ;; 1. first call to `evil-collection-magit-change-popup-key' changes the first "V" + ;; entry of `magit-revert' (the first entry in `define-transient-command' + ;; definition of `magit-revert'), second call changes the second "V". + ;; 2. the remapping here are in the same order as in `magit-revert' + ;; definition + (magit-revert "V" "O" magit-revert-and-commit) + (magit-revert "V" "O" magit-sequencer-continue) + (magit-tag "k" "x" magit-tag-delete))) + "Changes to popup keys") + +(defun evil-collection-magit-change-popup-key (popup from to &rest _args) + "Wrap `magit-change-popup-key'." + (transient-suffix-put popup from :key to)) + +(defun evil-collection-magit-adjust-popups () + "Adjust popup keys to match evil-collection-magit." + (unless evil-collection-magit-popup-keys-changed + (dolist (change evil-collection-magit-popup-changes) + (apply #'evil-collection-magit-change-popup-key change)) + (with-eval-after-load 'forge + (transient-remove-suffix 'magit-dispatch 'forge-dispatch) + (transient-append-suffix 'magit-dispatch "!" + '("@" "Forge" forge-dispatch))) + (setq evil-collection-magit-popup-keys-changed t))) + +(defun evil-collection-magit-revert-popups () + "Revert popup keys changed by evil-collection-magit." + (put 'magit-dispatch 'transient--layout evil-collection-magit-dispatch-popup-backup) + (when evil-collection-magit-popup-keys-changed + (dolist (change evil-collection-magit-popup-changes) + (evil-collection-magit-change-popup-key + (nth 0 change) (nth 2 change) (nth 1 change))) + (with-eval-after-load 'forge + (transient-suffix-put 'magit-dispatch "@" :key "'")) + (setq evil-collection-magit-popup-keys-changed nil))) + +;;;###autoload +(defun evil-collection-magit-init () + "This function completes the setup of evil-collection-magit. It is called +automatically when evil-collection-magit-setup is called.. The only reason to use +this function is if you've called `evil-collection-magit-revert' and wish to +go back to evil-collection-magit behavior." + (interactive) + (evil-collection-magit-adjust-section-bindings) + (evil-collection-magit-adjust-popups) + (evil-collection-magit-set-initial-states)) + +;;;###autoload +(defun evil-collection-magit-revert () + "Revert changes by evil-collection-magit that affect default evil+magit behavior." + (interactive) + (evil-collection-magit-revert-section-bindings) + (evil-collection-magit-revert-popups) + (evil-collection-magit-revert-initial-states) + (message "evil-collection-magit reverted")) + +(define-minor-mode evil-collection-magit-toggle-text-minor-mode + "Minor mode used to enabled toggle key in `text-mode' after +using `evil-collection-magit-toggle-text-mode'" + :keymap (make-sparse-keymap)) + +(evil-define-key 'normal evil-collection-magit-toggle-text-minor-mode-map + "\C-t" 'evil-collection-magit-toggle-text-mode + "\\" 'evil-collection-magit-toggle-text-mode) +(evil-define-key evil-collection-magit-state magit-mode-map + "\C-t" 'evil-collection-magit-toggle-text-mode + "\\" 'evil-collection-magit-toggle-text-mode) + +(defvar evil-collection-magit-last-mode nil + "Used to store last magit mode before entering text mode using +`evil-collection-magit-toggle-text-mode'.") + +(defun evil-collection-magit-toggle-text-mode () + "Switch to `text-mode' and back from magit buffers." + (interactive) + (cond ((derived-mode-p 'magit-mode) + (setq evil-collection-magit-last-mode major-mode) + (message "Switching to text-mode") + (text-mode) + (evil-collection-magit-toggle-text-minor-mode 1) + (evil-normalize-keymaps)) + ((and (eq major-mode 'text-mode) + (functionp evil-collection-magit-last-mode)) + (message "Switching to %s" evil-collection-magit-last-mode) + (evil-collection-magit-toggle-text-minor-mode -1) + (evil-normalize-keymaps) + (funcall evil-collection-magit-last-mode) + (magit-refresh) + (evil-change-state evil-collection-magit-state)) + (t + (user-error "evil-collection-magit-toggle-text-mode unexpected state")))) + ;;;###autoload (defun evil-collection-magit-setup () "Set up `evil' bindings for `magit'." + + ;; This is to work around an issue described in + ;; https://github.com/emacs-evil/evil-collection/issues/108 + ;; Ideally this file is only temporary and should be removed once + ;; #108 is resolved. (evil-collection-define-key 'normal 'magit-blame-mode-map "q" 'magit-blame-quit) (evil-collection-define-key 'normal 'magit-blame-read-only-mode-map - "q" 'magit-blame-quit)) + "q" 'magit-blame-quit) + (evil-collection-magit-init)) + +;;; evil-collection-magit.el ends soon (provide 'evil-collection-magit) +;; Local Variables: +;; indent-tabs-mode: nil +;; End: ;;; evil-collection-magit.el ends here diff --git a/test/evil-collection-magit-tests.el b/test/evil-collection-magit-tests.el new file mode 100644 index 0000000..9b4c892 --- /dev/null +++ b/test/evil-collection-magit-tests.el @@ -0,0 +1,112 @@ +;;; evil-collection-magit-tests.el --- evil-based key bindings for magit + +;; Copyright (C) 2015-2016 Justin Burkett + +;; Author: Justin Burkett +;; Homepage: https://github.com/emacs-evil/evil-collection + +;; This file 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, or (at your +;; option) any later version. +;; +;; This file 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. +;; +;; For a full copy of the GNU General Public License +;; see . +(require 'evil-collection) +(evil-collection-require 'magit) + +(ert-deftest evil-collection-magit-mode-map-tests () + "Test that original bindings in `evil-collection-magit-mode-map-bindings' +are correct." + (dolist (binding evil-collection-magit-mode-map-bindings) + (when (nth 4 binding) + (should (eq (lookup-key (symbol-value (nth 1 binding)) (kbd (nth 4 binding))) + (nth 3 binding))))) + (dolist (binding evil-collection-magit-minor-mode-map-bindings) + (when (nth 4 binding) + (should (eq (lookup-key (symbol-value (nth 1 binding)) (kbd (nth 4 binding))) + (nth 3 binding)))))) + +(ert-deftest evil-collection-magit-section-map-tests () + "Test that original bindings in +`evil-collection-magit-original-section-bindings' are correct." + (dolist (binding evil-collection-magit-original-section-bindings) + (should (eq (lookup-key (nth 0 binding) (nth 1 binding)) + (nth 2 binding))))) + +;; (ert-deftest evil-collection-magit-popup-action-tests () +;; "Test that bindings are as expected in popups." +;; (when evil-collection-magit-popup-keys-changed +;; (dolist (change evil-collection-magit-popup-changes) +;; (let ((alist (plist-get (symbol-value (nth 0 change)) (nth 1 change)))) +;; (should +;; (eq (nth 2 (assoc (string-to-char (nth 3 change)) alist)) +;; (nth 4 change))))))) + +(defun evil-collection-magit-collect-magit-section-maps () + (let (res) + (mapatoms + (lambda (sym) + (when (string-match-p "^magit-.*-section-map$" (symbol-name sym)) + (push sym res)))) + res)) + +(setq evil-collection-magit-section-maps-test (evil-collection-magit-collect-magit-section-maps)) +;; (setq evil-collection-magit-commands-in-section-maps +;; (let (res) +;; (dolist (map evil-collection-magit-section-maps-test) +;; (when (and (boundp map) (keymapp (symbol-value map))) +;; (map-keymap +;; (lambda (_ def) +;; (when (commandp def) +;; (if res +;; (add-to-list 'res def) +;; (setq res (list def))))) +;; (symbol-value map)))) +;; res)) + +(ert-deftest evil-collection-magit-section-maps-accounted-for () + "Check that `evil-collection-magit-section-maps' includes all section-maps +we can find." + (dolist (map evil-collection-magit-section-maps-test) + (when (and (boundp map) (keymapp (symbol-value map))) + (should (memq map evil-collection-magit-section-maps))))) + +(defun evil-collection-magit-collect-git-magit-modes () + (let (res) + (mapatoms + (lambda (sym) + (when (and (or (boundp sym) + (fboundp sym)) + (string-match-p "^\\(git\\|magit\\)-.*-mode$" (symbol-name sym))) + (push sym res)))) + res)) + +(ert-deftest evil-collection-magit-all-modes-accounted-for () + "Check that mode lists include all modes we can find." + (let ((modes (evil-collection-magit-collect-git-magit-modes)) + res) + (dolist (mode modes) + (when (boundp mode) + (should (memq mode + (append + evil-collection-magit-emacs-to-default-state-modes + evil-collection-magit-emacs-to-evil-collection-magit-state-modes + evil-collection-magit-default-to-evil-collection-magit-state-modes + evil-collection-magit-untouched-modes + evil-collection-magit-ignored-modes))))))) + +(ert-deftest evil-collection-magit-expand-region-arg-number () + "Check that the number of args accepted by +`evil-visual-expand-region' does not change." + (should-not (evil-visual-expand-region)) + (should-not (evil-visual-expand-region t)) + (should-error (evil-visual-expand-region t t) :type + 'wrong-number-of-arguments)) + +;;; evil-collection-magit-tests.el ends here