summaryrefslogtreecommitdiff
path: root/evil-states.el
diff options
context:
space:
mode:
Diffstat (limited to 'evil-states.el')
-rw-r--r--evil-states.el365
1 files changed, 365 insertions, 0 deletions
diff --git a/evil-states.el b/evil-states.el
new file mode 100644
index 0000000..dfa12ea
--- /dev/null
+++ b/evil-states.el
@@ -0,0 +1,365 @@
+;;;; State system
+
+;; What is "modes" in Vim is "states" in Evil. States are defined
+;; with the macro `evil-define-state'.
+;;
+;; A state consists of a universal keymap (like
+;; `evil-vi-state-map' for vi state) and a buffer-local keymap for
+;; overriding the former (like `evil-vi-state-local-map').
+;; Sandwiched between these keymaps may be so-called auxiliary
+;; keymaps, which contain state bindings assigned to an Emacs mode
+;; (minor or major): more on that below.
+;;
+;; A state may "inherit" keymaps from another state. For example,
+;; Visual state will enable vi state's keymaps in addition to its own.
+;; The keymap order then becomes:
+;;
+;; <visual-local-map>
+;; <visual auxiliary maps>
+;; <visual-universal-map>
+;; <vi-local-map>
+;; <vi auxiliary maps>
+;; <vi-universal-map>
+;;
+;; Since the activation of auxiliary maps depends on the current
+;; buffer and its modes, states are necessarily buffer-local.
+;; Different buffers can have different states, and different buffers
+;; enable states differently. (Thus, what keymaps to enable cannot be
+;; determined at compile time.) For example, the user may define some
+;; Visual state bindings for foo-mode, and if he enters foo-mode and
+;; Visual state in the current buffer, then the auxiliary keymap
+;; containing those bindings will be active. In a buffer where
+;; foo-mode is not enabled, it will not be.
+;;
+;; Hence, state bindings may be grouped into Emacs modes. This is
+;; useful for writing extensions.
+;;
+;; All state keymaps are listed in `evil-mode-map-alist', which is
+;; then listed in `emulation-mode-map-alist'. This gives state keymaps
+;; precedence over other keymaps. Note that `evil-mode-map-alist'
+;; has both a default (global) value and a buffer-local value. The
+;; default value is constructed when Evil is loaded and its states
+;; are defined. Afterwards, when entering a buffer, the default value
+;; is copied into the buffer-local value, and that value is reordered
+;; according to the current state (pushing Visual keymaps to the top
+;; when the user enters Visual state, etc.).
+
+(require 'evil-common)
+
+(defun evil-enable ()
+ "Enable Evil in the current buffer, if appropriate.
+To enable Evil globally, do (evil-mode 1)."
+ ;; TODO: option for enabling vi keys in the minibuffer
+ (unless (minibufferp)
+ (evil-local-mode 1)))
+
+(define-minor-mode evil-local-mode
+ "Minor mode for setting up Evil in a single buffer."
+ :init-value nil
+ (cond
+ (evil-local-mode
+ (setq emulation-mode-map-alists
+ (evil-concat-lists '(evil-mode-map-alist)
+ emulation-mode-map-alists))
+ (evil-refresh-local-maps)
+ (unless (memq 'evil-modeline-tag global-mode-string)
+ (setq global-mode-string
+ (append '(" " evil-modeline-tag " ")
+ global-mode-string)))
+ (evil-vi-state))
+ (t
+ (when evil-state
+ (funcall (evil-state-func) -1)))))
+
+(define-globalized-minor-mode evil-mode
+ evil-local-mode evil-enable)
+
+(put 'evil-mode 'function-documentation
+ "Toggle Evil in all buffers.
+Enable with positive ARG and disable with negative ARG.
+See `evil-local-mode' to toggle Evil in the
+current buffer only.")
+
+(defun evil-state-property (state prop)
+ "Return property PROP for STATE."
+ (evil-get-property evil-states-alist state prop))
+
+(defun evil-state-p (sym)
+ "Whether SYM is the name of a state."
+ (assq sym evil-states-alist))
+
+(defun evil-state-func (&optional state)
+ "Return the toggle function for STATE."
+ (setq state (or state evil-state))
+ (evil-state-property state :mode))
+
+(defun evil-state-keymaps (state &rest excluded)
+ "Return an ordered list of keymaps activated by STATE."
+ (let* ((state (or state evil-state))
+ (map (symbol-value (evil-state-property state :keymap)))
+ (local-map (symbol-value (evil-state-property
+ state :local-keymap)))
+ (aux-maps (evil-state-auxiliary-keymaps state))
+ (enable (evil-state-property state :enable))
+ (excluded (add-to-list 'excluded state))
+ ;; the keymaps for STATE
+ (result (append (list local-map) aux-maps (list map))))
+ ;; the keymaps for other states and modes enabled by STATE
+ (dolist (entry enable result)
+ (cond
+ ((evil-state-p entry)
+ (unless (memq entry excluded)
+ (dolist (mode (evil-state-modes entry excluded))
+ (add-to-list 'result mode t))))
+ (t
+ (add-to-list 'result entry t))))))
+
+(defun evil-state-auxiliary-keymaps (state)
+ "Return an ordered list of auxiliary keymaps for STATE."
+ (let* ((state (or state evil-state))
+ (alist (symbol-value (evil-state-property state :aux)))
+ result)
+ (dolist (map (current-active-maps) result)
+ (when (keymapp (setq map (cdr (assq map alist))))
+ (add-to-list 'result map t)))))
+
+(defun evil-normalize-keymaps (&optional state)
+ "Create a buffer-local value for `evil-mode-map-alist'.
+Its order reflects the state in the current buffer."
+ (let ((state (or state evil-state)) alist mode)
+ ;; initialize a buffer-local value
+ (setq evil-mode-map-alist
+ (copy-sequence (default-value 'evil-mode-map-alist)))
+ ;; update references to buffer-local keymaps
+ (evil-refresh-local-maps)
+ ;; disable all modes
+ (dolist (entry evil-mode-map-alist)
+ (set (car entry) nil))
+ ;; enable modes for current state
+ (unless (null state)
+ (dolist (map (evil-state-keymaps state))
+ (setq mode (or (car (rassq map evil-mode-map-alist))
+ (car (rassq map minor-mode-map-alist))))
+ (when mode
+ (set mode t)
+ (add-to-list 'alist (cons mode map) t)))
+ ;; move the enabled modes to the front of the list
+ (setq evil-mode-map-alist
+ (evil-concat-lists
+ alist evil-mode-map-alist)))))
+
+;; Local keymaps are implemented using buffer-local variables.
+;; However, unless a buffer-local value already exists,
+;; `define-key' acts on the variable's default (global) value.
+;; So we need to initialize the variable whenever we enter a
+;; new buffer or when the buffer-local values are reset.
+(defun evil-refresh-local-maps ()
+ "Initialize a buffer-local value for all local keymaps."
+ (let ((modes (evil-state-property nil :local-mode))
+ (maps (evil-state-property nil :local-keymap))
+ map mode state)
+ (dolist (entry maps)
+ (setq state (car entry)
+ map (cdr entry)
+ mode (cdr (assq state modes)))
+ ;; initalize the variable
+ (unless (symbol-value map)
+ (set map (make-sparse-keymap)))
+ ;; refresh the keymap's entry in `evil-mode-map-alist'
+ (setq evil-mode-map-alist
+ (copy-sequence evil-mode-map-alist))
+ (evil-add-to-alist 'evil-mode-map-alist mode
+ (symbol-value map)))))
+
+(defun evil-set-cursor (specs)
+ "Change the cursor's apperance according to SPECS.
+SPECS may be a cursor type as per `cursor-type', a color
+string as passed to `set-cursor-color', a zero-argument
+function for changing the cursor, or a list of the above.
+If SPECS is nil, make the cursor a black box."
+ (set-cursor-color "black")
+ (setq cursor-type 'box)
+ (unless (and (listp specs) (not (consp specs)))
+ (setq specs (list specs)))
+ (dolist (spec specs)
+ (cond
+ ((functionp spec)
+ (funcall spec))
+ ((stringp spec)
+ (set-cursor-color spec))
+ (t
+ (setq cursor-type spec))))
+ (redisplay))
+
+(defun evil-change-state (state)
+ "Change state to STATE.
+Disable all states if nil."
+ (let ((func (evil-state-property
+ (or state evil-state 'emacs) :mode)))
+ (funcall func (if state 1 -1))))
+
+(defmacro evil-define-state (state doc &rest body)
+ "Define a Evil state STATE.
+DOC is a general description and shows up in all docstrings.
+Then follows one or more optional keywords:
+
+:tag STRING Mode line indicator.
+:message STRING Echo area message when changing to STATE.
+:cursor SPEC Cursor to use in STATE.
+:entry-hook LIST Hooks run when changing to STATE.
+:exit-hook LIST Hooks run when changing from STATE.
+:enable LIST List of other states and modes enabled by STATE.
+
+Following the keywords is optional code to be executed each time
+the state is enabled or disabled.
+
+For example:
+
+ (evil-define-state test
+ \"A simple test state.\"
+ :tag \"<T> \")
+
+The basic keymap of this state will then be
+`evil-test-state-map', and so on."
+ (declare (debug (&define name
+ [&optional stringp]
+ [&rest [keywordp sexp]]
+ def-body))
+ (indent defun))
+ (let ((mode (intern (format "evil-%s-state" state)))
+ (keymap (intern (format "evil-%s-state-map" state)))
+ (local-mode (intern (format "evil-%s-state-local" state)))
+ (local-keymap (intern (format "evil-%s-state-local-map" state)))
+ (aux (intern (format "evil-%s-state-auxiliary-maps" state)))
+ (predicate (intern (format "evil-%s-state-p" state)))
+ (tag (intern (format "evil-%s-state-tag" state)))
+ (message (intern (format "evil-%s-state-message" state)))
+ (cursor (intern (format "evil-%s-state-cursor" state)))
+ (entry-hook (intern (format "evil-%s-state-entry-hook" state)))
+ (exit-hook (intern (format "evil-%s-state-exit-hook" state)))
+ cursor-value enable entry-hook-value exit-hook-value keyword
+ message-value tag-value)
+ ;; collect keywords
+ (while (keywordp (car-safe body))
+ (setq keyword (pop body))
+ (cond
+ ((eq keyword :tag)
+ (setq tag-value (pop body)))
+ ((eq keyword :message)
+ (setq message-value (pop body)))
+ ((eq keyword :cursor)
+ (setq cursor-value (pop body)))
+ ((eq keyword :entry-hook)
+ (setq entry-hook-value (pop body)))
+ ((eq keyword :exit-hook)
+ (setq exit-hook-value (pop body)))
+ ((eq keyword :enable)
+ (setq enable (pop body)))
+ (t
+ (pop body))))
+
+ ;; macro expansion
+ `(let ((mode-map-alist (default-value 'evil-mode-map-alist)))
+
+ ;; Save the state's properties in `evil-states-alist' for
+ ;; runtime lookup. Among other things, this information is used
+ ;; to determine what keymaps should be activated by the state
+ ;; (and, when processing :enable, what keymaps are activated by
+ ;; other states). We cannot know this at compile time because
+ ;; it depends on the current buffer and its active keymaps
+ ;; (to which we may have assigned state bindings), as well as
+ ;; states whose definitions may not have been processed yet.
+ (evil-put-property
+ 'evil-states-alist ',state
+ :tag (defvar ,tag ,tag-value
+ ,(format "Modeline tag for %s state.\n\n%s" state doc))
+ :message (defvar ,message ,message-value
+ ,(format "Echo area indicator for %s state.\n\n%s"
+ state doc))
+ :cursor (defvar ,cursor ,cursor-value
+ ,(format "Cursor for %s state.
+May be a cursor type as per `cursor-type', a color string as passed
+to `set-cursor-color', a zero-argument function for changing the
+cursor, or a list of the above.\n\n%s" state doc))
+ :entry-hook (defvar ,entry-hook ,entry-hook-value
+ ,(format "Hooks to run when entering %s state.\n\n%s"
+ state doc))
+ :exit-hook (defvar ,exit-hook ,exit-hook-value
+ ,(format "Hooks to run when exiting %s state.\n\n%s"
+ state doc))
+ :mode (defvar ,mode nil
+ ,(format "Non-nil if %s state is enabled.
+Use the command `%s' to change this variable." state mode))
+ :keymap (defvar ,keymap (make-sparse-keymap)
+ ,(format "Keymap for %s state.\n\n%s" state doc))
+ :local-mode (defvar ,local-mode nil
+ ,(format "Non-nil if %s state is enabled.
+Use the command `%s' to change this variable." state mode))
+ :local-keymap (defvar ,local-keymap nil
+ ,(format "Buffer-local keymap for %s state.\n\n%s"
+ state doc))
+ :aux (defvar ,aux nil
+ ,(format "Association list of auxiliary keymaps for %s state.
+Elements have the form (KEYMAP . AUX-MAP), where AUX-MAP contains state
+bindings to be activated whenever KEYMAP and %s state are active."
+ state state))
+ :predicate (defun ,predicate ()
+ ,(format "Whether the current state is %s." state)
+ (eq evil-state ',state))
+ :enable ',enable)
+
+ (evil-add-to-alist 'mode-map-alist
+ ',local-mode ,local-keymap
+ ',mode ,keymap)
+
+ (setq-default evil-mode-map-alist mode-map-alist)
+
+ (make-variable-buffer-local ',mode)
+ (make-variable-buffer-local ',local-mode)
+ (make-variable-buffer-local ',local-keymap)
+
+ ;; define state function
+ (defun ,mode (&optional arg)
+ ,(format "Enable %s state. Disable with negative ARG.\n\n%s"
+ state doc)
+ (interactive)
+ (cond
+ ((and (numberp arg) (< arg 1))
+ (unwind-protect
+ (let (evil-state)
+ (evil-normalize-keymaps)
+ (run-hooks ',exit-hook)
+ ,@body)
+ (setq evil-state nil)))
+ (t
+ (unless evil-local-mode
+ (evil-enable))
+ (when evil-state
+ (funcall (evil-state-func) -1))
+ (unwind-protect
+ (let ((evil-state ',state))
+ (evil-normalize-keymaps)
+ (setq evil-modeline-tag ,tag)
+ (force-mode-line-update)
+ (evil-set-cursor ,cursor)
+ ,@body
+ (run-hooks ',entry-hook)
+ (when ,message (evil-unlogged-message ,message)))
+ (setq evil-state ',state)))))
+
+ ',state)))
+
+;; Define vi (command) state
+
+(evil-define-state vi
+ "Command state, AKA \"Normal\" state."
+ :tag "<V>")
+
+(evil-define-state emacs
+ "Emacs state."
+ :tag "<E>")
+
+(define-key evil-vi-state-map "\C-z" 'evil-emacs-state)
+(define-key evil-emacs-state-map "\C-z" 'evil-vi-state)
+
+(provide 'evil-states)