;;;; 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: ;; ;; ;; ;; ;; ;; ;; ;; ;; 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 \" \") 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 "") (evil-define-state emacs "Emacs state." :tag "") (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)