;;; core-spacebind.el --- Spacemacs Core File -*- lexical-binding: t -*- ;; ;; Copyright (c) 2012-2022 Sylvain Benner & Contributors ;; ;; Author: Eugene "JAremko" Yaremenko ;; URL: https://github.com/syl20bnr/spacemacs ;; ;; This file is not part of GNU Emacs. ;; ;; 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 . (require 'core-keybindings) (defvar spacebind--eager-bind t "If true bind keys right after `spacmeacs|spacebind' macro-expanse. Otherwise binding happens at the next event loop.") ;;;; Binding stacks (defvar spacebind--bs-minor-mode-replacements '() "Binding stack for `spacemacs/add-key-based-replacements-for-minor-mode'.") (defvar spacebind--bs-major-mode-replacements '() "Binding stack for `which-key-add-major-mode-key-based-replacements'.") (defvar spacebind--bs-declare-prefix '() "Binding stack for `spacemacs/declare-prefix'.") (defvar spacebind--bs-declare-prefix-for-mode '() "Binding stack for `spacemacs/declare-prefix-for-mode'.") (defvar spacebind--bs-leader-keys '() "Binding stack for `spacemacs/set-leader-keys'.") (defvar spacebind--bs-leader-keys-for-major-mode '() "Binding stack for `spacemacs/set-leader-keys-for-major-mode'.") (defvar spacebind--bs-leader-keys-for-minor-mode '() "Binding stack for `spacemacs/set-leader-keys-for-minor-mode'.") (defvar spacebind--bs-global-replacements '() "Binding stack for `which-key-add-key-based-replacements'.") (defvar spacebind--bs-fn-key-seq-override '() "Binding stack for `spacemacs/add-which-key-fn-key-seq-override'.") (defvar spacebind--timer [t] "`run-with-idle-timer' return value for `spacebind//process-bind-stack'.") (defun spacebind//process-bind-stack () "Binds keys and prefixes popping the binding stacks." (unwind-protect (progn ;; `spacemacs/add-key-based-replacements-for-minor-mode' (dolist (args spacebind--bs-minor-mode-replacements) (let ((mode (car args)) (keys (string-join (append `(,(spacemacs/leader-key)) (cadr args)) " ")) (label (caddr args))) (spacemacs/add-key-based-replacements-for-minor-mode mode keys label))) ;; `which-key-add-major-mode-key-based-replacements' (dolist (args spacebind--bs-major-mode-replacements) (let ((mode (car args)) (keys (string-join (append `(,(spacemacs/leader-key)) `(,(spacemacs/major-mode-prefix)) (cadr args)) " ")) (label (caddr args))) (which-key-add-major-mode-key-based-replacements mode keys label))) ;; `spacemacs/declare-prefix' (dolist (args spacebind--bs-declare-prefix) (let ((prefix (string-join (car args) " ")) (label (cadr args))) (spacemacs/declare-prefix prefix label))) ;; `spacemacs/declare-prefix-for-mode' (dolist (args spacebind--bs-declare-prefix-for-mode) (let ((mode (car args)) (prefix (string-join (cadr args) " ")) (label (caddr args))) (spacemacs/declare-prefix-for-mode mode prefix label))) ;; `spacemacs/set-leader-keys' (dolist (args spacebind--bs-leader-keys) (let ((keys (string-join (car args) " ")) (fn-sym (cadr args))) (spacemacs/set-leader-keys keys fn-sym))) ;; `spacemacs/set-leader-keys-for-major-mode' (dolist (args spacebind--bs-leader-keys-for-major-mode) (let ((mode (car args)) (keys (string-join (cadr args) " ")) (fn-sym (caddr args))) (spacemacs/set-leader-keys-for-major-mode mode keys fn-sym))) ;; `spacemacs/set-leader-keys-for-minor-mode' (dolist (args spacebind--bs-leader-keys-for-minor-mode) (let ((mode (car args)) (keys (string-join (cadr args) " ")) (fn-sym (caddr args))) (spacemacs/set-leader-keys-for-minor-mode mode keys fn-sym))) ;; `which-key-add-key-based-replacements' (dolist (args spacebind--bs-global-replacements) (let ((keys (string-join (append `(,(spacemacs/leader-key)) (car args)) " ")) (label (cadr args))) (which-key-add-key-based-replacements keys label))) ;; `spacemacs/add-which-key-fn-key-seq-override' (dolist (args spacebind--bs-fn-key-seq-override) (let ((sym (car args)) (rep (cadr args)) (label (caddr args))) (spacemacs/add-which-key-fn-key-seq-override sym rep label)))) ;; Reset stacks (setq spacebind--bs-global-replacements nil spacebind--bs-leader-keys-for-minor-mode nil spacebind--bs-leader-keys-for-major-mode nil spacebind--bs-leader-keys nil spacebind--bs-declare-prefix-for-mode nil spacebind--bs-declare-prefix nil spacebind--bs-major-mode-replacements nil spacebind--bs-minor-mode-replacements nil spacebind--bs-fn-key-seq-override nil ;; Reset timer var spacebind--timer [t]))) ;; TODO: Make this configurable. (defun spacemacs/major-mode-prefix () "Get current prefix for major modes. NOTE: `dotspacemacs-major-mode-leader-key' isn't the same." "m") (defun spacemacs/leader-key () "Returns `dotspacemacs-leader-key'" dotspacemacs-leader-key) (defun spacebind//strip-docstring (mode-binding-form) "Remove second element of MODE-BINDING-FORM if it is a string." (if (stringp (cadr mode-binding-form)) (cons (car mode-binding-form) (cddr mode-binding-form)) mode-binding-form)) (defun spacemacs/add-key-based-replacements-for-minor-mode (mode key-sequence replacement &rest more) "Proxy for `which-key-add-key-based-replacements' MODE currently ignored. FIXME: We should disable replacements when the mode is disabled. The function `which-key-add-major-mode-key-based-replacements' might give a clue how to do this. Also there is discussion about the feature: https://github.com/justbur/emacs-which-key/issues/212" (apply #'which-key-add-key-based-replacements key-sequence replacement more)) (defun spacemacs/add-which-key-fn-key-seq-override (fn-name key-rep label) "Replace FN-NAME function's key sequence with KEY-REP and LABEL." (push `((nil . ,fn-name) . (,key-rep . ,label)) which-key-replacement-alist)) (cl-defstruct spacemacs--spacebind-state "State object for `spacebind' macro implementation. CTYPE - current binding type. BSTACK - plist of generated key-binding stacks of the shape: (:minor-mode-replacements :major-mode-replacements :declare-prefix :declare-prefix-for-mode :set-leader-keys :set-leader-keys-for-major-mode :set-leader-keys-for-minor-mode :global-replacements :fn-key-seq-override )" ctype bstack) (cl-defgeneric spacemacs//spacebind-dispatch (state binding) (:documentation "Based on BINDING type modify STATE using BINDING value.")) (cl-defmethod spacemacs//spacebind-dispatch ((state spacemacs--spacebind-state) (keyword-or-symbol symbol)) "If KEYWORD is a keyword, set STATE slot ctype(current type) to its value. If KEYWORD is a non-keyword symbol (without \":\" prefix in the name), wrap its value into a list and re-apply the function to it." (if (not (keywordp keyword-or-symbol)) (spacemacs//spacebind-dispatch state (list keyword-or-symbol)) (setf (spacemacs--spacebind-state-ctype state) keyword-or-symbol) state)) (cl-defmethod spacemacs//spacebind-dispatch ((state spacemacs--spacebind-state) (sexp list)) "Apply STATE method from ctype slot to SEXP." (funcall (spacemacs--spacebind-state-ctype state) state sexp) state) (cl-defmethod spacemacs//spacebind-dispatch ((state spacemacs--spacebind-state) (_ string)) "Ignore strings - used to implement doc-strings." state) (defun spacemacs//spacebind-form-visitor (form path k-fn p-fn) "Applies K-FN to FORM if it is a key binding form. Otherwise applies P-FN. PATH passed to the applied function. NOTE: This function strips all newline characters, replaces successive spaces with a singular in string elements of FORM and trims tails of function labels delimited by \"|\" character." (list (cl-labels ((apply-str-fmt (el) (thread-last el ;; Convert new lines and multiply spaces into singular. ;; This is done to enable better code formatting. (replace-regexp-in-string "[\n[:space:]]+" " ") ;; Discard everything after | symbol in labels. ;; This way we can add extra text into the README.org ;; files while omitting it in labels. (replace-regexp-in-string "[[:punct:][:space:]]*|.+" ""))) (str-fmt-rec (depth el) (cond ((stringp el) (apply-str-fmt el)) ((and (= depth 0) (listp el)) ;; We don't want to go deeper than a single level. (mapcar (apply-partially #'str-fmt-rec (1+ depth)) el)) (t el))) (str-fmt (el) (str-fmt-rec 0 el))) (cl-destructuring-bind (key-or-prefix-form leader-label-or-fn-symbol leader-label-or-next-form) (mapcar #'str-fmt (seq-take form 3)) (let ((full-key-or-prefix (append path ;; ("key" :label "label") or "key". `(,(or (car-safe key-or-prefix-form) key-or-prefix-form)))) (key-or-prefix-label (thread-first key-or-prefix-form (cdr-safe) (plist-get :label)))) (if (symbolp leader-label-or-fn-symbol) (funcall k-fn full-key-or-prefix key-or-prefix-label leader-label-or-fn-symbol ;; Either "label" or ("doc label" :label "label"). (or (thread-first leader-label-or-next-form (cdr-safe) (plist-get :label)) leader-label-or-next-form)) (funcall p-fn full-key-or-prefix leader-label-or-fn-symbol))))))) (defun spacemacs//spacebind-form-walker-rec (path k-fn p-fn form) "Recursive body of `spacemacs//spacebind-form-walker'." (let* ((fn-sym-or-label (car-safe (cdr-safe form))) (prefix-form? (stringp fn-sym-or-label)) (binding-form? (and fn-sym-or-label (symbolp fn-sym-or-label))) (list-of-forms? (and form (cl-every #'consp form))) (binding-or-prefix-form? (or binding-form? prefix-form?)) (head (car form)) (cur-path (if prefix-form? (append path `(,(or (car-safe head) head))) path)) ;; Strip key and label from prefix forms. (bindings (if prefix-form? (cddr form) form))) (append (when binding-or-prefix-form? (spacemacs//spacebind-form-visitor form path k-fn p-fn)) (when (or prefix-form? list-of-forms?) (seq-mapcat (apply-partially 'spacemacs//spacebind-form-walker-rec cur-path k-fn p-fn) bindings))))) (defun spacemacs//spacebind-form-walker (b-forms k-fn p-fn) "Part of `spacemacs--spacebind-state' interpreters implementation. B-FORMS is a root node of a binding tree without mode (car of the root form). K-FN called for each key binding node with 3 arguments: full_key_sequence, function_symbol and label_for_leader_menu. P-FN called for each prefix binding node with 2 arguments: full_key_prefix_sequence and label_for_leader_menu. Both K-FN and P-FN should return binding evaluation forms. The forms will be concatenated and substituted by `spacebind' macro." (spacemacs//spacebind-form-walker-rec nil k-fn p-fn b-forms)) (defun spacemacs//spacebind-stack-push (state stack-k sexp) "Push SEXP onto stack specified by STACK-K in the STATE bstack plist slot." (cl-callf (lambda (bs) (plist-put bs stack-k (cons sexp (plist-get bs stack-k)))) (spacemacs--spacebind-state-bstack state)) state) ;;;; Key bindings - keywords handlers (cl-defmethod :global ((state spacemacs--spacebind-state) form) "Interpreter for global binding forms." (spacemacs//spacebind-form-walker ;; Strip optional doc-string. (if (char-or-string-p (car form)) (cdr form) form) (lambda (key-seq key-label fn-symbol label) (spacemacs//spacebind-stack-push state :global-replacements `(,key-seq ,label)) (spacemacs//spacebind-stack-push state :set-leader-keys `(,key-seq ,fn-symbol)) (when key-label (spacemacs//spacebind-stack-push state :fn-key-seq-override `(,(symbol-name fn-symbol) ,key-label ,label)))) (lambda (key-prefix label) (spacemacs//spacebind-stack-push state :declare-prefix `(,key-prefix ,label))))) (cl-defmethod :major ((state spacemacs--spacebind-state) form) "Interpreter for major mode binding forms." (let* ((form (spacebind//strip-docstring form)) (mode (pop form))) (spacemacs//spacebind-form-walker form (lambda (key-seq key-label fn-symbol label) (spacemacs//spacebind-stack-push state :major-mode-replacements `(,mode ,key-seq ,label)) (spacemacs//spacebind-stack-push state :set-leader-keys-for-major-mode `(,mode ,key-seq ,fn-symbol)) (when key-label (spacemacs//spacebind-stack-push state :fn-key-seq-override `(,(symbol-name fn-symbol) ,key-label ,label)))) (lambda (key-prefix label) (spacemacs//spacebind-stack-push state :declare-prefix-for-mode `(,mode ,key-prefix ,label)))))) (cl-defmethod :minor ((state spacemacs--spacebind-state) form) "Interpreter for minor mode binding forms." (let* ((form (spacebind//strip-docstring form)) (mode (pop form))) (spacemacs//spacebind-form-walker form (lambda (key-seq key-label fn-symbol label) (spacemacs//spacebind-stack-push state :minor-mode-replacements `(,mode ,key-seq ,label)) (spacemacs//spacebind-stack-push state :set-leader-keys-for-minor-mode `(,mode ,key-seq ,fn-symbol)) (when key-label (spacemacs//spacebind-stack-push state :fn-key-seq-override `(,(symbol-name fn-symbol) ,key-label ,label)))) (lambda (key-prefix label) (spacemacs//spacebind-stack-push state :declare-prefix-for-mode `(,mode ,key-prefix ,label)))))) (defmacro spacemacs|spacebind (&rest bindings) "Bind keys and their prefixes declared via BINDINGS tree like structure. BINDINGS format: ... ... ... DELIMITER_KEYWORD - specifies a type of following (or forms). Currently supported types: :major, :minor and :global. :major and :minor bindings have this shape: ( ...) :global forms have a similar shape: ( ...) is a string that will be used to generate a key-bindings section in the corresponding README.org files. is a recursive form that can be: A prefix form: ( ) Or a key-binding form: ( ) is what will be displayed in the menu and used for the documentation generation. and are singular keys represented as strings. is the function that will be bound to the . See core-spacebind-utest.el for examples. NOTE: This macro also has `use-package' integration via `:spacebind' key NOTE: strings support formatting: - \n and multiply spaces are converted into single spaces in the . - Everything after and including | symbol is ignored and punctuation before the character trimmed. This is done so you can provide additional information for the binding documentation while keeping labels brief. NOTE: You can override key labels and displayed key sequences with :label Example: ((\"k\" :label \"press k\") foo-fn (\"for docs\" :label \"displayed\")) \(fn < ...>...)" (cl-list* 'progn (let ((state (make-spacemacs--spacebind-state))) (dolist (sexp bindings) ;; Fill stacks (spacemacs//spacebind-dispatch state sexp)) (cl-flet ((get-stack (key) (thread-first state (spacemacs--spacebind-state-bstack) (plist-get key)))) (seq-reduce (lambda (acc pair) (if-let ((stack (get-stack (car pair))) (stack-var (cadr pair))) ;; We do it this way because `nconc' should have ;; the shortest list as the first argument. (append acc `(,stack-var (nconc ',stack ,stack-var))) acc)) `((:minor-mode-replacements spacebind--bs-minor-mode-replacements) (:major-mode-replacements spacebind--bs-major-mode-replacements) (:declare-prefix spacebind--bs-declare-prefix) (:declare-prefix-for-mode spacebind--bs-declare-prefix-for-mode) (:set-leader-keys spacebind--bs-leader-keys) (:set-leader-keys-for-major-mode spacebind--bs-leader-keys-for-major-mode) (:set-leader-keys-for-minor-mode spacebind--bs-leader-keys-for-minor-mode) (:global-replacements spacebind--bs-global-replacements) (:fn-key-seq-override spacebind--bs-fn-key-seq-override)) '(setq)))) ;; Schedule stacks processing with `spacebind//process-bind-stack' function. `((when (aref spacebind--timer 0) (if (not spacebind--eager-bind) (setq spacebind--timer (run-with-idle-timer 0 nil #'spacebind//process-bind-stack)) (setq spacebind--timer [t]) (spacebind//process-bind-stack)))))) (provide 'core-spacebind)