;;; -*- lexical-binding: t -*- ;;; core-micro-state.el --- Spacemacs Core File ;; ;; Copyright (c) 2012-2014 Sylvain Benner ;; Copyright (c) 2014-2015 Sylvain Benner & Contributors ;; ;; Author: Sylvain Benner ;; URL: https://github.com/syl20bnr/spacemacs ;; ;; This file is not part of GNU Emacs. ;; ;;; License: GPLv3 (require 'corelv) (defun spacemacs/defface-micro-state-faces () "Define faces for micro-states." (let* ((hname 'spacemacs-micro-state-header-face) (bname 'spacemacs-micro-state-binding-face) (box '(:line-width -1 :color (plist-get (face-attribute 'mode-line :box) :color))) (err (face-attribute 'error :foreground))) (eval `(defface ,hname '((t ())) "Face for micro-state header in echo area. The header is the name of the micro-state." :group 'spacemacs)) (set-face-attribute hname nil :background "DarkGoldenrod2" :foreground "black" :bold t :box box) (eval `(defface ,bname '((t ())) "Face for micro-state key binding in echo area. Characters enclosed in `[]' will have this face applied to them." :group 'spacemacs)) (set-face-attribute bname nil :foreground err :bold t))) (spacemacs/defface-micro-state-faces) (defun spacemacs//micro-state-set-minibuffer-height (str) "Set the max mini windows size given a string STR." (let ((line-count (1+ (how-many-str "\n" str)))) (when (and (> line-count max-mini-window-height) (> line-count 10)) (setq max-mini-window-height line-count)))) (defmacro spacemacs|define-micro-state (name &rest props) "Define a micro-state called NAME. NAME is a symbol. Available PROPS: `:on-enter SEXP' Evaluate SEXP when the micro-state is switched on. `:on-exit SEXP' Evaluate SEXP when leaving the micro-state. `:doc STRING or SEXP' A STRING or a SEXP that evaluates to a string. `:use-minibuffer BOOLEAN' If non nil then the minibuffer is used to display the documenation strings. Default is nil. `:disable-evil-leader BOOLEAN' If non nil then the evil leader has no effect when the micro state is active. Default to nil. `:persistent BOOLEAN' If BOOLEAN is non nil then the micro-state never exits. A binding with an explicitly set `exit t' property is required. Default is nil. `:execute-binding-on-enter BOOLEAN' If BOOLEAN is non nil then execute the micro-state command bound to to the pressed key that started the micro-state. `:bindings EXPRESSIONS' One or several EXPRESSIONS with the form (STRING1 SYMBOL1 :doc STRING :pre SEXP :post SEXP :exit SYMBOL) where: - STRING1 is a key to be bound to the function or key map SYMBOL1. - :doc STRING or SEXP is a STRING or an SEXP that evalutes to a string - :pre is an SEXP evaluated before the bound action - :post is an SEXP evaluated after the bound action - :exit SYMBOL or SEXP, if non nil then pressing this key will leave the micro-state (default is nil). Important note: due to inner working of transient-maps in Emacs the `:exit' keyword is evaluate *before* the actual execution of the bound command. All properties supported by `spacemacs//create-key-binding-form' can be used." (declare (indent 1)) (let* ((func (spacemacs//micro-state-func-name name)) (doc (spacemacs/mplist-get props :doc)) (persistent (plist-get props :persistent)) (disable-leader (plist-get props :disable-evil-leader)) (msg-func (if (plist-get props :use-minibuffer) 'message 'corelv-message)) (exec-binding (plist-get props :execute-binding-on-enter)) (on-enter (spacemacs/mplist-get props :on-enter)) (on-exit (spacemacs/mplist-get props :on-exit)) (bindings (spacemacs/mplist-get props :bindings)) (wrappers (spacemacs//micro-state-create-wrappers name doc msg-func disable-leader bindings)) (keymap-body (spacemacs//micro-state-fill-map-sexps wrappers)) (bindkeys (spacemacs//create-key-binding-form props func))) `(progn (defun ,func () ,(format "%S micro-state." name) (interactive) ,@on-enter (let ((doc ,@doc)) (when doc (spacemacs//micro-state-set-minibuffer-height doc) (apply ',msg-func (list (spacemacs//micro-state-propertize-doc (format "%S: %s" ',name doc)))))) ,(when exec-binding (spacemacs//micro-state-auto-execute bindings)) (,(if (version< emacs-version "24.4") 'set-temporary-overlay-map 'set-transient-map) (let ((map (make-sparse-keymap))) ,@keymap-body map) ',(spacemacs//micro-state-create-exit-func name wrappers persistent on-exit))) ,@bindkeys))) (defun spacemacs//micro-state-func-name (name) "Return the name of the micro-state function." (intern (format "spacemacs/%S-micro-state" name))) (defun spacemacs//micro-state-auto-execute (bindings) "Auto execute the binding corresponding to `this-command-keys'." `(let* ((key (substring (this-command-keys) (1- (length (this-command-keys))))) (binding (assoc key ',bindings))) (when binding (call-interactively (cadr binding))))) (defun spacemacs//micro-state-create-wrappers (name doc msg-func disable-leader bindings) "Return an alist (key wrapper) for each binding in BINDINGS." (mapcar (lambda (x) (spacemacs//micro-state-create-wrapper name doc msg-func x)) (append bindings ;; force SPC to quit the micro-state to avoid a edge case ;; with evil-leader (list `(,dotspacemacs-leader-key ,(unless disable-leader 'evil-leader--default-map) :exit t))))) (defun spacemacs//micro-state-create-wrapper (name default-doc msg-func binding) "Create a wrapper of FUNC and return a tuple (key wrapper BINDING)." (let* ((key (car binding)) (wrapped (cadr binding)) (binding-doc (spacemacs/mplist-get binding :doc)) (binding-pre (spacemacs/mplist-get binding :pre)) (binding-post (spacemacs/mplist-get binding :post)) (wrapper-name (intern (format "spacemacs//%S-%S-%s" name wrapped key))) (doc-body `((let ((bdoc ,@binding-doc) (defdoc ,@default-doc)) (if bdoc (apply ',msg-func (list (spacemacs//micro-state-propertize-doc (format "%S: %s" ',name bdoc)))) (when (and defdoc ',wrapped (not (plist-get ',binding :exit))) (spacemacs//micro-state-set-minibuffer-height defdoc) (apply ',msg-func (list (spacemacs//micro-state-propertize-doc (format "%S: %s" ',name defdoc))))))))) (wrapper-func (if (and (boundp wrapped) (eval `(keymapp ,wrapped))) wrapped `(defun ,wrapper-name () "Auto-generated function" (interactive) ,@binding-pre (let ((throwp t)) (catch 'exit (when (fboundp ',wrapped) (setq this-command ',wrapped) (call-interactively ',wrapped) (setq last-command ',wrapped)) (setq throwp nil)) ,@binding-post (when throwp (throw 'exit nil))) (when ,@doc-body (spacemacs//micro-state-set-minibuffer-height ,@doc-body) ,@doc-body))))) (append (list (car binding) (eval wrapper-func)) binding))) (defun spacemacs//micro-state-fill-map-sexps (wrappers) "Return a list of `define-key' sexp to fill the micro-state temporary map." (mapcar (lambda (x) `(define-key map ,(kbd (car x)) ',(cadr x))) wrappers)) (defun spacemacs//micro-state-create-exit-func (name wrappers persistent on-exit) "Return a function to execute when leaving the micro-state. The returned function returns nil if the executed command exits the micro-state." (let ((func (intern (format "spacemacs//%s-on-exit" name)))) (eval `(defun ,func () "Function executed after each micro-state command." (let* ((cur-wrapper (spacemacs//get-current-wrapper ',name ',wrappers)) (exitp (if cur-wrapper (plist-get cur-wrapper :exit) ,(not persistent)))) (when (listp exitp) (setq exitp (eval exitp))) (when exitp ,@on-exit (spacemacs//micro-state-close-window)) (not exitp)))))) (defun spacemacs//get-current-wrapper (name wrappers) "Return the wrapper being executed. Return nil if no wrapper is being executed (i.e. an unbound key has been pressed)." (let ((micro-state-fun (spacemacs//micro-state-func-name name))) (catch 'found (dolist (wrapper wrappers) (let ((key (car wrapper)) (func (cadr wrapper))) (if (and (or (eq this-command micro-state-fun) (eq this-command func)) (equal (this-command-keys) (kbd key))) (throw 'found wrapper)))) nil))) (defun spacemacs//micro-state-propertize-doc (doc) "Return a propertized doc string from DOC." (when (string-match "^\\(.+?\\):\\([[:ascii:]]*\\)$" doc) (let* ((header (match-string 1 doc)) (pheader (when header (propertize (concat " " header " ") 'face 'spacemacs-micro-state-header-face))) (tail (spacemacs//micro-state-propertize-doc-rec (match-string 2 doc)))) (concat pheader tail)))) (defun spacemacs//micro-state-propertize-doc-rec (doc) "Recursively propertize keys" (if (string-match "^\\([[:ascii:]]*?\\)\\(\\[.+?\\]\\)\\([[:ascii:]]*\\)$" doc) (let* ((head (match-string 1 doc)) (key (match-string 2 doc)) (pkey (when key (propertize key 'face 'spacemacs-micro-state-binding-face))) (tail (spacemacs//micro-state-propertize-doc-rec (match-string 3 doc)))) (concat head pkey tail)) doc)) (defun spacemacs//micro-state-close-window () "Close micro-state help window." (when (window-live-p corelv-wnd) (let ((buf (window-buffer corelv-wnd))) (delete-window corelv-wnd) (kill-buffer buf)))) (provide 'core-micro-state)