;;; evil-collection.el --- A set of keybindings for Evil mode -*- lexical-binding: t -*-

;; Copyright (C) 2017, 2023 James Nguyen

;; Author: James Nguyen <james@jojojames.com>
;; Pierre Neidhardt <mail@ambrevar.xyz>
;; Maintainer: James Nguyen <james@jojojames.com>
;; Pierre Neidhardt <mail@ambrevar.xyz>
;; URL: https://github.com/emacs-evil/evil-collection
;; Package-Version: 3.0.2
;; Package-Revision: 3.0.2-0-gbce35101edcf
;; Package-Requires: ((emacs "29.1") (evil "1.2.13"))
;; Keywords: evil, tools

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:
;; A set of keybindings for Evil mode.
;;
;; If you want to use Evil in the minibuffer, you'll have to enable it by
;; setting `evil-collection-setup-minibuffer' to t before loading this package.
;; This is so because many users find it confusing.
;; Some minibuffer-related packages such as Helm rely on this option.

;;; Code:

;; `evil' requires `seq-into'?
;; This require on `seq' before loading `evil 'prevents `evil' from erroring
;; out with the below message on Emacs 29.
;; Symbol's function definition is void: seq-into
;; Looks like this error can be traced through evil ->
;; Look at the commit that moved this line above `evil' to see the error message.
;; evil -> evil-vars -> read-kbd-macro -> seq-into -> error.
;; https://github.com/emacs-evil/evil/issues/1627
(require 'seq)
(require 'cl-lib)
(require 'evil)
;; `annalist' is optional; only used to record and describe bindings via
;; `evil-collection-describe-bindings'. Install it to enable that feature.
(require 'annalist nil t)

(declare-function annalist-record "annalist")
(declare-function annalist-describe "annalist")
(declare-function annalist-define-view "annalist")
(declare-function annalist-string-< "annalist")

(defvar evil-collection-base-dir (file-name-directory load-file-name)
  "Store the directory evil-collection.el was loaded from.")

(defvar evil-want-integration)
(defvar evil-want-keybinding)

(defvar evil-collection--startup-warning-flag nil)

(defun evil-collection-maybe-display-startup-warning ()
  "Warn if `evil-want-keybinding' was not set before loading Evil.
Runs at most once."
  (unless evil-collection--startup-warning-flag
    (setq evil-collection--startup-warning-flag t)
    (when (featurep 'evil-keybindings)
      (if evil-want-keybinding
          (display-warning
           '(evil-collection)
           "Make sure to set `evil-want-keybinding' to nil before loading evil \
or evil-collection.\
\n
See https://github.com/emacs-evil/evil-collection/issues/60 for more details.")
        (display-warning
         '(evil-collection)
         "`evil-want-keybinding' was set to nil but not before loading evil.\
\n
Make sure to set `evil-want-keybinding' to nil before loading evil \
or evil-collection.\
\n
See https://github.com/emacs-evil/evil-collection/issues/60 for more details.")))))

(unless (featurep 'evil-integration)
  (message "Requiring evil-integration. Set evil-want-integration to t to\
 remove this message.\
\n
See https://github.com/emacs-evil/evil-collection/issues/60 for more details.")
  (require 'evil-integration))

(defgroup evil-collection nil
  "A set of keybindings for Evil mode."
  :group 'evil)

(defcustom evil-collection-setup-minibuffer nil
  "Whether to setup Evil bindings in the minibuffer."
  :type 'boolean)

(defcustom evil-collection-calendar-want-org-bindings nil
  "Whether to bind Org functions in calendar keymap."
  :type 'boolean)

(defcustom evil-collection-setup-debugger-keys t
  "Whether to bind debugger keys when debugger is active.

Debugger in this case is dependent on mode.

This is only relevant for debug modes that are part of another mode,

e.g. `indium'. Modes like `edebug' or `realgud' needs to be explicitly disabled

through removing their entry from `evil-collection-mode-list'.

This variable is obsolete; new customization should use
`evil-collection-binding-overrides' instead:

  (setq evil-collection-binding-overrides
        \\='((debug-breakpoint :enabled nil)))"
  :type 'boolean)

(make-obsolete-variable 'evil-collection-setup-debugger-keys
                        "use `evil-collection-binding-overrides': `debug-continue', `debug-step-over', `debug-step-into', `debug-step-out', `debug-breakpoint'."
                        "evil-collection 2.0.0")

(defcustom evil-collection-want-unimpaired-p t
  "Whether to enable unimpaired style bindings globally."
  :type 'boolean)

(defcustom evil-collection-want-find-usages-bindings t
  "Whether to bind `xref-find-references'-like bindings.

This will bind additional find-* type commands, e.g. usages, assignments, etc..

This variable is obsolete; New customization
should use `evil-collection-binding-overrides' instead:

  (setq evil-collection-binding-overrides
        \\='((find-usages :enabled nil)))"
  :type 'boolean)

(make-obsolete-variable 'evil-collection-want-find-usages-bindings
                        "use `evil-collection-binding-overrides': `find-usages'."
                        "evil-collection 2.0.0")

(defcustom evil-collection-want-g-bindings t
  "Whether to bind g* bindings."
  :type 'boolean)

(defvar evil-collection--modes-with-delayed-setup
  `(emms
    eshell)
  "List of modes whose keybinds aren't completely set up after the mode is
loaded. This can be a problem for cases where we're doing key translations
using `evil-collection-setup-hook' which would result in an empty keymap.

Normally we run `evil-collection-setup-hook' right away after the mode
is loaded in `with-eval-after-load' (see `evil-collection-init') but for these
modes, we skip running that hook and let the corresponding `evil-collection'
package handle running `evil-collection-setup-hook'.

Elements in this list either match a target mode symbol or the car of a list in
`evil-collection--supported-modes'.

If `evil-collection-always-run-setup-hook-after-load' is t, this list isn't
read and `evil-collection-setup-hook' will be ran in the
`with-eval-after-load' block in `evil-collection-init'.")

(defcustom evil-collection-always-run-setup-hook-after-load nil
  "Whether to always run `evil-collection-setup-hook' after mode is loaded.

See `evil-collection-init' and `evil-collection--modes-with-delayed-setup'."
  :type 'boolean)

(defcustom evil-collection-defer-delay 3
  "Default idle delay in seconds used when deferring mode initialization.

Used as a fallback when no explicit delay is specified for a mode in
`evil-collection-defer'."
  :type 'number)

(defvar evil-collection--supported-modes
  `(2048-game
    ag
    agent-shell
    agent-shell-manager
    alchemist
    anaconda-mode
    apropos
    arc-mode
    atomic-chrome
    auto-package-update
    beginend
    bluetooth
    bm
    bookmark
    (buff-menu "buff-menu")
    bufler
    calc
    calendar
    cider
    citre
    cmake-mode
    color-rg
    comint
    company
    compile
    consult
    corfu
    crdt
    (csv "csv-mode")
    (custom cus-edit)
    cus-theme
    dape
    dashboard
    daemons
    deadgrep
    debbugs
    debug
    devdocs
    dictionary
    diff-hl
    diff-mode
    difftastic
    dired
    dired-sidebar
    disk-usage
    distel
    doc-view
    docker
    eat
    ebib
    ebuku
    eca
    edbi
    edebug
    ediff
    eglot
    elpaca
    ement
    explain-pause-mode
    eldoc
    elfeed
    elisp-mode
    elisp-refs
    elisp-slime-nav
    embark
    emcp
    emms
    ,@(when (>= emacs-major-version 29) '(emoji))
    epa
    ert
    eshell
    eval-sexp-fu
    evil-ghostel
    evil-mc
    eww
    fanyi
    finder
    flycheck
    flymake
    fj
    forge
    free-keys
    fzfa
    geiser
    ggtags
    git-timemachine
    gited
    gnus
    go-mode
    gptel
    grep
    guix
    hackernews
    helm
    help
    helpful
    hg-histedit
    hideshow
    hungry-delete
    hyrolo
    ibuffer
    ielm
    (image image-mode)
    image-dired
    image+
    imenu
    imenu-list
    (indent "indent")
    indium
    info
    ivy
    js2-mode
    ,@(when (>= emacs-major-version 30) '(kmacro))
    leetcode
    lispy
    lms
    log-edit
    log-view
    lsp-ui-imenu
    lua-mode
    kotlin-mode
    macrostep
    man
    (magit magit-submodule) ;; See https://github.com/emacs-evil/evil-collection/issues/637
    magit-repos
    magit-section
    magit-todos
    markdown-mode
    message
    minesweeper
    ,@(when evil-collection-setup-minibuffer '(minibuffer))
    monky
    mpc
    mpdel
    mpdired
    mu4e
    mu4e-conversation
    neotree
    newsticker
    notmuch
    nov
    omnisharp
    org
    org-agenda
    org-present
    org-roam
    osx-dictionary
    outline
    ovpn-mode
    p4
    (package-menu package)
    pass
    (pdf pdf-view)
    popup
    proced
    (process-menu simple)
    prodigy
    profiler
    p-search
    python
    quickrun
    racer
    racket-describe
    reader
    realgud
    reftex
    replace ;; For `occur'.
    restclient
    rg
    ripgrep
    rjsx-mode
    robe
    rtags
    ruby-mode
    scheme
    scroll-lock
    selectrum
    sh-script
    shell-maker
    ,@(when (>= emacs-major-version 28) '(shortdoc))
    simple
    simple-mpc
    slime
    sly
    smerge-mode
    snake
    so-long
    speedbar
    ,@(when (>= emacs-major-version 27) '(tab-bar))
    tablist
    tabulated-list
    tar-mode
    telega
    (term term ansi-term multi-term)
    tetris
    ,@(when (>= emacs-major-version 27) '(thread))
    tide
    timer-list
    transmission
    trashed
    tuareg
    typescript-mode
    ultra-scroll
    vc-annotate
    vc-dir
    vc-git
    vdiff
    vertico
    view
    vlf
    vterm
    vundo
    w3m
    wdired
    wgrep
    which-key
    with-editor
    woman
    xref
    xwidget
    yaml-mode
    youtube-dl
    zmusic
    (ztree ztree-diff ztree-dir))
  "List of modes supported by evil-collection. Elements are
either target mode symbols or lists which `car' is the mode
symbol and `cdr' the packages to register.")

(dolist (mode evil-collection--supported-modes)
  (let ((ec-mode-name (if (listp mode) (car mode) mode)))
    (autoload
      (intern (format "evil-collection-%s-setup" ec-mode-name))
      (expand-file-name
       (format "modes/%s/evil-collection-%s" ec-mode-name ec-mode-name)
       evil-collection-base-dir))))

(defcustom evil-collection-mode-list evil-collection--supported-modes
  "The list of modes which will be evilified by `evil-collection-init'.
Elements are either target mode symbols or lists which `car' is the
mode symbol and `cdr' the packages to register.

By default, `minibuffer' is not included because many users find
this confusing. It will be included if
`evil-collection-setup-minibuffer' is set to t."
  :type '(repeat (choice symbol sexp)))

(defcustom evil-collection-config
  '((buff-menu :defer t)
    (calc :defer t)
    (comint :defer t)
    (debug :defer t)
    (diff-mode :defer t)
    (dired :hook dired-mode)
    (edebug :defer t)
    (eldoc :defer t)
    (help :defer t)
    (image :defer t)
    (indent :defer t)
    (info :defer t)
    (replace :defer t)
    (outline :defer t)
    (package-menu :defer t)
    (process-menu :defer t)
    (simple :defer t)
    (tab-bar :defer t)
    (tabulated-list :defer t)
    (xref :defer t))
  "The list of modes with special configuration.

These modes should match entries within `evil-collection-mode-list'.

This variable is consumed only by `evil-collection-setup'.


NOTE: The API of this variable may change drastically.

Currently supported keys:

:defer t or TIME in seconds to defer loading mode.

:hook HOOK-NAME to initialize the mode when a hook fires.  The hook
name does not need the \"-hook\" suffix; it will be appended
automatically.  When :hook is present, :defer is ignored."
  :type '(repeat (choice symbol sexp)))

(defcustom evil-collection-key-whitelist '()
  "List of keys that may be used by Evil Collection.
This is a list of strings that are suitable for input to
`kbd'.  If there are no keys in the list, the whitelist will be ignored."
  :type '(repeat string))

(defcustom evil-collection-key-blacklist '()
  "List of keys that may not be used by Evil Collection.
This is a list of strings that are suitable for input to `kbd'."
  :type '(repeat string))

(defcustom evil-collection-state-passlist '()
  "List of evil states that may be used by Evil Collection.
This is a list of symbols that are suitable for input to
 `evil-define-key'. Ignore when there are no states in the list."
  :type '(repeat symbol))

(defcustom evil-collection-state-denylist
  (if (bound-and-true-p evil-disable-insert-state-bindings)
      '(insert)
    '())
  "List of evil states that may not be used by Evil Collection.
This is a list of symbols that are suitable for input to
 `evil-define-key'."
  :type '(repeat symbol))

(defcustom evil-collection-repl-submit-state 'normal
  "Evil state in which RET submits the prompt in REPL-like buffers.

The other state gets RET bound to `newline'.  S-RET always inserts a
newline regardless of state.

New customization should use
`evil-collection-binding-overrides' instead:

  `repl-submit', `repl-newline', `repl-force-newline'

  (setq evil-collection-binding-overrides
        \\='((repl-submit  :state insert)
          (repl-newline :state normal)))"
  :type '(choice (const :tag "Submit in normal state" normal)
                 (const :tag "Submit in insert state" insert)))

(make-obsolete-variable 'evil-collection-repl-submit-state
                        "use `evil-collection-binding-overrides': `repl-submit', `repl-newline'."
                        "evil-collection 2.0.0")

(defvar evil-collection-binding-defaults
  `((term-toggle-escape :enabled t
                        :state (normal insert)
                        :key "C-c C-z")
    (repl-submit :enabled t
                 :state ,(lambda (_map-sym _id _keys _command)
                           evil-collection-repl-submit-state)
                 :key ("RET" "<return>" "C-m"))
    (repl-newline :enabled t
                  :state ,(lambda (_map-sym _id _keys _command)
                            (if (eq evil-collection-repl-submit-state 'normal)
                                'insert
                              'normal))
                  :key ("RET" "<return>" "C-m"))
    (repl-force-newline :enabled t
                        :state (normal insert)
                        :key ("S-<return>" "S-RET"))
    (find-usages :enabled ,(lambda (_map-sym _id _keys _command)
                             evil-collection-want-find-usages-bindings)
                 :state normal
                 :key "gr")
    (find-definition :enabled t :state normal :key "gd")
    (pop-definition :enabled t :state normal :key "C-t")
    (lookup-doc :enabled t :state normal :key "K")
    (goto-repl :enabled t :state normal :key "gz")
    (find-file :enabled t :state normal :key "gf")
    (quit :enabled t :state normal :key "q")
    (quit-save :enabled t :state normal :key "ZZ")
    (quit-cancel :enabled t :state normal :key "ZQ")
    (rename :enabled t :state normal :key ("r" "R"))
    (edit :enabled t :state normal :key ("e" "E"))
    (browse-url :enabled t :state normal :key "gx")
    (describe-mode :enabled t :state normal :key "g?")
    (refresh :enabled t :state normal :key "gr")
    (refresh-all :enabled t :state normal :key "gR")
    (action :enabled t :state normal :key ("RET" "<return>"))
    (action-other :enabled t :state normal :key ("S-<return>" "S-RET" "go"))
    (action-stay :enabled t :state normal :key ("M-<return>" "M-RET" "gO"))
    (debug-continue    :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ("c" "<f5>"))
    (debug-step-over   :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ("n" "<f10>"))
    (debug-step-into   :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ("i" "s" "<f11>"))
    (debug-step-out    :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ("o" "<S-f11>"))
    (debug-breakpoint  :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ("b" "<f9>"))
    (debug-eval        :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key "e")
    (debug-locals      :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key "L")
    (debug-restart     :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key "R")
    (debug-frame-up    :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key "<")
    (debug-frame-down  :enabled ,(lambda (_map-sym _id _keys _command)
                                    evil-collection-setup-debugger-keys)
                       :state normal
                       :key ">")
    (next-button :enabled t :state normal :key ("<tab>" "TAB"))
    (previous-button :enabled t :state normal :key ("<backtab>" "<S-tab>"))
    (cycle-next :enabled t :state normal :key ("<tab>" "TAB"))
    (cycle-previous :enabled t :state normal :key ("<backtab>" "<S-tab>"))
    (section-toggle :enabled t :state normal :key ("<tab>" "TAB"))
    (section-toggle-all :enabled t :state normal :key ("<backtab>" "<S-tab>"))
    (next-item :enabled t :state normal :key "gj")
    (prev-item :enabled t :state normal :key "gk")
    (next-section :enabled t :state normal :key ("]]"))
    (prev-section :enabled t :state normal :key ("[["))
    (next-section-2 :enabled t :state normal :key "C-j")
    (prev-section-2 :enabled t :state normal :key "C-k")
    (scroll-down :enabled t :state normal :key "SPC")
    (scroll-up :enabled t :state normal :key ("S-SPC" "S-<space>"))
    (history-previous :enabled t :state normal :key "C-p")
    (history-next :enabled t :state normal :key "C-n")
    (completion-previous :enabled t :state insert :key "C-p")
    (completion-next :enabled t :state insert :key "C-n")
    (zoom-in :enabled t :state normal :key ("+" "="))
    (zoom-out :enabled t :state normal :key "-")
    (zoom-reset :enabled t :state normal :key "0")
    (jump :enabled t :state normal :key "J")
    (mark :enabled t :state normal :key "m")
    (mark-all :enabled t :state normal :key "M")
    (toggle :enabled t :state normal :key "t")
    (toggle-all :enabled t :state normal :key "~")
    (unmark :enabled t :state normal :key "u")
    (unmark-all :enabled t :state normal :key "U")
    (mark-delete :enabled t :state normal :key "d")
    (execute-marks :enabled t :state normal :key "x")
    (delete :enabled t :state normal :key "d")
    (delete-2 :enabled t :state normal :key "x")
    (search-or-filter :enabled t :state normal :key "s"))
  "Built-in entries for the evil-collection theme system.

Each entry has the form (ID . PLIST) and may use:

  :enabled  nil, t, or a function with the signature
              (lambda (map-sym id keys command) ...)
            called at lookup time.  Missing entries are treated as t.
            Any of the four arguments may be nil when
            `evil-collection-binding-enabled-p' is called outside the
            bind helpers (e.g. as a pure feature toggle from a
            consumer-side setup function).  Override examples:

              ;; Skip in one specific keymap.
              (find-usages :enabled
                ,(lambda (map-sym _id _keys _cmd)
                   (not (eq map-sym \\='dired-sidebar-mode-map))))

              ;; Skip when bound to a particular key.
              (find-usages :enabled
                ,(lambda (_map-sym _id keys _cmd)
                   (not (member \"gr\" keys))))

  :state    Evil state symbol, list of state symbols, or a function
            with the same signature as :enabled.
  :key      Key string (suitable for `kbd'), list of key strings, or
            with the same signature as :enabled.

:state and :key are optional.  An entry with only :enabled acts as a
pure feature toggle: the consumer-side code does the binding itself
and asks the resolver via `evil-collection-binding-enabled-p' whether
the feature is on.

Function values for :state and :key are funcalled at lookup time with
args mirroring `:enabled'.  Plain symbols are never funcalled even
when they happen to have a function binding (e.g. the state symbol
`insert' is also the `insert' function); that is why
anonymous lambdas are required when a property's value should be
computed dynamically.

Users customize via `evil-collection-binding-overrides'; do not
modify this variable directly.")

(defcustom evil-collection-binding-overrides nil
  "User overrides merged onto `evil-collection-binding-defaults'.

Same shape as `evil-collection-binding-defaults'.  Properties present
here win per-property; properties omitted here fall through to the
defaults.

  (setq evil-collection-binding-overrides
        \\='((term-toggle-escape :state normal :key \"C-c j\")))

Use `:enabled nil' to disable a feature.  An explicit nil counts as
\"set\", not \"absent\"."
  :type '(alist :key-type symbol :value-type plist))

(defun evil-collection-binding--get (id prop)
  "Resolve PROP for theme entry ID.

Overrides win over defaults per-property.  Returns nil if neither
side sets PROP."
  (let ((over (cdr (assq id evil-collection-binding-overrides)))
        (def  (cdr (assq id evil-collection-binding-defaults))))
    (cond ((plist-member over prop) (plist-get over prop))
          ((plist-member def prop)  (plist-get def prop)))))

(defun evil-collection-binding--resolve (v &optional map-sym id keys command)
  "Funcall V with (MAP-SYM ID KEYS COMMAND) if it is an anonymous
function; otherwise return V.
Plain symbols are returned as-is even when they have a function
binding — they are data (state names, command names) in this
context, never callables."
  (if (and (functionp v) (not (symbolp v)))
      (funcall v map-sym id keys command)
    v))

(defun evil-collection-binding--listify (v)
  "Wrap V in a list unless it already is one.  Nil stays nil."
  (cond ((null v) nil)
        ((listp v) v)
        (t (list v))))

(defun evil-collection-binding-enabled-p (id &optional map-sym keys command)
  "Return non-nil if theme entry ID is enabled.

:enabled may be nil, t, or a function with the signature
\(lambda (MAP-SYM ID KEYS COMMAND) ...).  Missing -> t.

The optional MAP-SYM, KEYS, and COMMAND are forwarded to the function
when present at the call site, and otherwise nil — this lets callers
that resolve `:enabled' as a pure feature toggle (no concrete keymap
in scope) keep their existing call shape."
  (let* ((over (cdr (assq id evil-collection-binding-overrides)))
         (def  (cdr (assq id evil-collection-binding-defaults)))
         (v (cond ((plist-member over :enabled) (plist-get over :enabled))
                  ((plist-member def :enabled)  (plist-get def :enabled))
                  (t t))))
    (if (and (functionp v) (not (symbolp v)))
        (funcall v map-sym id keys command)
      v)))

(defun evil-collection-binding-states (id &optional map-sym keys command)
  "Return list of evil states configured for theme entry ID.

The optional MAP-SYM, KEYS, and COMMAND are forwarded to a
function-valued `:state' — same shape as `:enabled'."
  (evil-collection-binding--listify
   (evil-collection-binding--resolve
    (evil-collection-binding--get id :state) map-sym id keys command)))

(defun evil-collection-binding-keys (id &optional map-sym command)
  "Return list of key strings configured for theme entry ID."
  (let* ((over (cdr (assq id evil-collection-binding-overrides)))
         (def  (cdr (assq id evil-collection-binding-defaults))))
    (evil-collection-binding--listify
     (if (plist-member over :key)
         (let ((default-keys
                (evil-collection-binding--listify
                 (evil-collection-binding--resolve
                  (plist-get def :key) map-sym id nil command))))
           (evil-collection-binding--resolve
            (plist-get over :key) map-sym id default-keys command))
       (evil-collection-binding--resolve
        (plist-get def :key) map-sym id nil command)))))

(defun evil-collection-bind (map-sym &rest id-command-pairs)
  "Bind one or more theme entries in MAP-SYM.

ID-COMMAND-PAIRS is an even-length list alternating theme IDs and
commands:

  (evil-collection-bind \\='foo-mode-map
    \\='action \\='foo-do
    \\='quit   \\='foo-quit)

Each ID is resolved through `evil-collection-binding-keys'; entries with
`:enabled' nil are skipped.  Pairs that share the same `:state'
value are batched into a single `evil-collection-define-key' call
so a deferred map installs one `after-load-functions' hook per
state-group instead of one per pair."
  (let ((groups nil))
    (while id-command-pairs
      (let* ((id (pop id-command-pairs))
             (cmd (pop id-command-pairs))
             (keys (evil-collection-binding-keys id map-sym cmd)))
        (when (evil-collection-binding-enabled-p id map-sym keys cmd)
          (let ((states (evil-collection-binding-states id map-sym keys cmd)))
            (when keys
              (let ((pairs (cl-mapcan (lambda (k) (list (kbd k) cmd))
                                      keys))
                    (group (assoc states groups)))
                (if group
                    (setcdr group (nconc (cdr group) pairs))
                  (push (cons states pairs) groups))))))))
    (dolist (group (nreverse groups))
      (apply #'evil-collection-define-key
             (car group) map-sym (cdr group)))))

(defun evil-collection-bind-minor-mode (id mode command)
  "Bind theme entry ID to COMMAND in minor-mode MODE.

Like `evil-collection-bind' but routes through
`evil-collection-define-minor-mode-key'."
  (let ((keys (evil-collection-binding-keys id mode command)))
    (when (evil-collection-binding-enabled-p id mode keys command)
      (let ((states (evil-collection-binding-states id mode keys command)))
        (when keys
          (apply #'evil-collection-define-minor-mode-key states mode
                 (cl-mapcan (lambda (key) (list (kbd key) command))
                            keys)))))))

(defmacro evil-collection-define-push-action (name fallback)
  "Define NAME as an interactive command that calls `push-button' when point
is on a button (per `button-at'), otherwise calls FALLBACK interactively.

Evil's state aux keymaps shadow text-property keymaps (e.g. `button-map' on
buttons), so binding a primary action like a `*-visit-result' directly to
RET prevents header/embedded buttons from receiving the keypress.  Wrapping
the fallback restores Emacs's text-property-aware dispatch."
  (declare (indent defun))
  `(defun ,name ()
     ,(format "Push button at point, else call `%s'.
Generated by `evil-collection-define-push-action' to restore button-keymap
text-property dispatch under Evil normal state." fallback)
     (interactive)
     (if (button-at (point))
         (call-interactively #'push-button)
       (call-interactively ',fallback))))

(defun evil-collection--keys-conflict (key-a key-b)
  "Return t if KEY-A and KEY-B conflict.
Keys conflict if they're equal or if one is a prefix of the other."
  (setq key-a (vconcat key-a)
        key-b (vconcat key-b))
  (let ((len (min (length key-a) (length key-b))))
    (equal (substring key-a 0 len) (substring key-b 0 len))))

(defsubst evil-collection--can-bind-key (key whitelist blacklist)
  "Return t if KEY can be bound.
Return nil if KEY conflicts with a key in BLACKLIST and is not
a member of WHITELIST.

Both WHITELIST and BLACKLIST must be lists of keys in Emacs'
internal key representation (i.e., after calling `kbd' on the key
description."
  (or (member key whitelist)
      (not (cl-member key blacklist
                      :test #'evil-collection--keys-conflict))))

(defun evil-collection-bind-local (id command)
  "Bind theme entry ID to COMMAND in the current buffer.

Like `evil-collection-bind' but uses `evil-local-set-key'."
  (let ((keys (evil-collection-binding-keys id nil command)))
    (when (evil-collection-binding-enabled-p id nil keys command)
      (let ((whitelist (mapcar #'kbd evil-collection-key-whitelist))
            (blacklist (mapcar #'kbd evil-collection-key-blacklist))
            (states (evil-collection--filter-states
                     (evil-collection-binding-states id nil keys command))))
        (when states
          (dolist (key keys)
            (let ((kbd-key (kbd key)))
              (when (evil-collection--can-bind-key kbd-key whitelist blacklist)
                (when (featurep 'annalist)
                  (annalist-record 'evil-collection 'keybindings
                                   (list 'local (car states) kbd-key command)
                                   :local t))
                (dolist (state states)
                  (evil-local-set-key state kbd-key command))))))))))

(defvar evil-collection-setup-hook nil
  "Hook run by `evil-collection-init' for each mode that is evilified.
This hook runs after all setup (including keybindings) for a mode has already
taken place. The arguments passed to functions for this hook are the name of the
mode and a list of keymap names (i.e. symbols, not actual keymaps) customized by
Evil Collection for that mode. More arguments may be added in the future, so
functions added to this hook should include a \"&rest _rest\" for forward
compatibility.")

(defun evil-collection-define-operator-key (operator map-sym &rest bindings)
  "Define a key on a specific OPERATOR e.g. yank or delete.

This function is useful for adding specific binds to operator maps
\(e.g. `evil-yank' or `evil-delete') without erasing the original bind.

For example, say one wants to bind \"yf\" to something but also wants to keep
\"yy\".

This function takes care of checking the whitelist/blacklist against the full
binding.

For example:
\(evil-collection-define-operator-key \='yank
  \='pass-mode-map \"f\" \='pass-copy-field)

This will check \"yf\" against a user's white/blacklist and also record the
binding in `annalist' as so."
  (declare (indent defun))
  (let* ((prefix (if (eq operator 'yank) "y" "d"))
         (operators (if (eq operator 'yank)
                        'evil-collection-yank-operators
                      'evil-collection-delete-operators))
         (remap (if (eq operator 'yank) [remap evil-yank] [remap evil-delete]))
         (whitelist (mapcar #'kbd evil-collection-key-whitelist))
         (blacklist (mapcar #'kbd evil-collection-key-blacklist))
         filtered-bindings)
    (while bindings
      (let* ((key (pop bindings))
             (key-with-prefix (concat prefix key))
             (def (pop bindings))
             (def-with-menu-item
              `(menu-item
                ""
                nil
                :filter
                (lambda (&optional _)
                  (when (or
                         (eq evil-this-operator (key-binding ,remap))
                         (memq evil-this-operator ,operators))
                    (setq evil-inhibit-operator t)
                    ',def)))))
        (when (or (and whitelist (member key-with-prefix whitelist))
                  (not (member key-with-prefix blacklist)))
          (when (featurep 'annalist)
            (annalist-record 'evil-collection 'keybindings
                             ;; Record the binding as if it was in 'normal mode
                             ;; instead of 'operator mode as the user would be
                             ;; in normal mode when triggering the operator.
                             (list map-sym 'normal key-with-prefix def)
                             :local (or (eq map-sym 'local)
                                        (local-variable-p map-sym))))
          ;; Use the original key declared when actually setting the binding.
          (push key filtered-bindings)
          ;; Use the definition attached to the menu-item when setting the
          ;; binding.
          (push def-with-menu-item filtered-bindings))))
    (setq filtered-bindings (nreverse filtered-bindings))
    (evil-collection--define-key 'operator map-sym filtered-bindings)))

(defun evil-collection--filter-states (state)
  "Return a list states after filtering STATE (a single symbol or list of symbols).
The return value adheres to `evil-collection-state-passlist' and
`evil-collection-state-denylist'. When the STATE is nil, which
means all states for `evil-define-key', return nil."
  (let ((states (if (listp state) state (list state))))
    (seq-difference
     (if evil-collection-state-passlist
         (seq-intersection states evil-collection-state-passlist)
       states)
     evil-collection-state-denylist)))

(defun evil-collection-define-key (state map-sym &rest bindings)
  "Wrapper for `evil-define-key*' with additional features.
Unlike `evil-define-key*' MAP-SYM should be a quoted keymap other than the
unquoted keymap required for `evil-define-key*'. This function adds the ability
to filter keys on the basis of `evil-collection-key-whitelist' and
`evil-collection-key-blacklist'. It also records bindings with annalist.el."
  (declare (indent defun))
  (let* ((whitelist (mapcar #'kbd evil-collection-key-whitelist))
         (blacklist (mapcar #'kbd evil-collection-key-blacklist))
         (states-to-bind (evil-collection--filter-states state))
         filtered-bindings)
    (when (or states-to-bind (null state))
      (while bindings
        (let ((key (pop bindings))
              (def (pop bindings)))
          (when (evil-collection--can-bind-key key whitelist blacklist)
            (when (featurep 'annalist)
              (annalist-record 'evil-collection 'keybindings
                               (list map-sym state key def)
                               :local (or (eq map-sym 'local)
                                          (local-variable-p map-sym))))
            (push key filtered-bindings)
            (push def filtered-bindings))))
      (setq filtered-bindings (nreverse filtered-bindings))
      (evil-collection--define-key states-to-bind map-sym filtered-bindings))))

(defun evil-collection-define-minor-mode-key (state mode &rest bindings)
  "Wrapper for `evil-define-minor-mode-key' with additional features.
Like `evil-collection-define-key' but binds to MODE (a minor-mode
symbol) rather than a keymap.  Bindings auto-activate when MODE is
enabled without relying on `evil-normalize-keymaps'.

Filters keys via `evil-collection-key-whitelist' /
`evil-collection-key-blacklist' and records bindings with annalist."
  (declare (indent defun))
  (let* ((whitelist (mapcar #'kbd evil-collection-key-whitelist))
         (blacklist (mapcar #'kbd evil-collection-key-blacklist))
         (states-to-bind (evil-collection--filter-states state))
         filtered-bindings)
    (when (or states-to-bind (null state))
      (while bindings
        (let ((key (pop bindings))
              (def (pop bindings)))
          (when (evil-collection--can-bind-key key whitelist blacklist)
            (when (featurep 'annalist)
              (annalist-record 'evil-collection 'keybindings
                               (list mode state key def)))
            (push key filtered-bindings)
            (push def filtered-bindings))))
      (setq filtered-bindings (nreverse filtered-bindings))
      (when filtered-bindings
        (apply #'evil-define-minor-mode-key
               states-to-bind mode filtered-bindings)))))

(defun evil-collection-can-bind-key (key)
  "Return whether or not we should bind KEY."
  (let* ((whitelist (mapcar #'kbd evil-collection-key-whitelist))
         (blacklist (mapcar #'kbd evil-collection-key-blacklist)))
    (evil-collection--can-bind-key key whitelist blacklist)))

(defun evil-collection--define-key (state map-sym bindings)
  "Workhorse function for `evil-collection-define-key'.

See `evil-collection-define-key' docstring for more details."
  (cond ((null bindings))
        ((and (boundp map-sym) (keymapp (symbol-value map-sym)))
         (condition-case-unless-debug err
             (apply #'evil-define-key*
                    state (symbol-value map-sym) bindings)
           (error
            (message "evil-collection: error setting key in %s %S"
                     map-sym err))))
        ((boundp map-sym)
         (user-error "evil-collection: %s is not a keymap" map-sym))
        (t
         (let* ((fname (format "evil-collection-define-key-in-%s" map-sym))
                (fun (make-symbol fname)))
           (fset fun `(lambda (&rest args)
                        (when (and (boundp ',map-sym) (keymapp ,map-sym))
                          (remove-hook 'after-load-functions #',fun)
                          (condition-case-unless-debug err
                              (apply #'evil-define-key*
                                     ',state ,map-sym ',bindings)
                            (error
                             (message
                              ,(format
                                "evil-collection: error setting key in %s %%S"
                                map-sym)
                              err))))))
           (add-hook 'after-load-functions fun t)))))

(defun evil-collection-inhibit-insert-state (map-sym)
  "Unmap insertion keys from normal state.
This is particularly useful for read-only modes."
  (evil-collection-define-key 'normal map-sym
    [remap evil-append] #'ignore
    [remap evil-append-line] #'ignore
    [remap evil-insert] #'ignore
    [remap evil-insert-line] #'ignore
    [remap evil-change] #'ignore
    [remap evil-change-line] #'ignore
    [remap evil-substitute] #'ignore
    [remap evil-change-whole-line] #'ignore
    [remap evil-delete] #'ignore
    [remap evil-delete-line] #'ignore
    [remap evil-delete-char] #'ignore
    [remap evil-delete-backward-char] #'ignore
    [remap evil-replace] #'ignore
    [remap evil-replace-state] #'ignore
    [remap evil-open-below] #'ignore
    [remap evil-open-above] #'ignore
    [remap evil-paste-after] #'ignore
    [remap evil-paste-before] #'ignore
    [remap evil-join] #'ignore
    [remap evil-indent] #'ignore
    [remap evil-shift-left] #'ignore
    [remap evil-shift-right] #'ignore
    [remap evil-invert-char] #'ignore))

(defun evil-collection-set-readonly-bindings (map-sym)
  "Unmap insertion keys from normal state. Additionally q can `quit-window'.
This is particularly useful for read-only modes. Make sure it's
called before setting up other evil bindings so that it can be
overriden."
  (evil-collection-inhibit-insert-state map-sym)
  (evil-collection-define-key 'normal map-sym
    "q"  #'quit-window
    "ZZ" #'quit-window
    "ZQ" #'evil-quit))

(defun evil-collection--binding-lessp (a b)
  "Comparison function used to sort bindings of the form (state key def)."
  (let ((a-state (symbol-name (nth 0 a)))
        (b-state (symbol-name (nth 0 b)))
        (a-key (nth 1 a))
        (b-key (nth 1 b)))
    (if (not (string= a-state b-state))
        (string-lessp a-state b-state)
      (string-lessp a-key b-key))))

(with-eval-after-load 'annalist
  (annalist-define-view 'keybindings 'evil-collection-valid
    (list (list 'keymap :sort #'annalist-string-<)
          (list 'state :sort #'annalist-string-<))
    :inherit 'valid)

  (annalist-define-view 'keybindings 'evil-collection-active
    (list (list 'keymap :sort #'annalist-string-<)
          (list 'state :sort #'annalist-string-<))
    :inherit 'active))

(defun evil-collection-describe-bindings (&optional arg)
  "Print bindings made by Evil Collection to separate buffer.

With non-nil ARG, restrict to bindings corresponding to active
modes in the current buffer.

Requires the optional `annalist' package."
  (interactive "P")
  (unless (require 'annalist nil t)
    (user-error
     "`evil-collection-describe-bindings' requires the `annalist' package"))
  (annalist-describe 'evil-collection 'keybindings
                     (if arg
                         'evil-collection-active
                       'evil-collection-valid)))

(defun evil-collection--mode-file (mode file)
  "Return path to FILE for MODE. Return nil if it doesn't exist."
  (let ((path (expand-file-name
               (format "modes/%s/%s" mode file) evil-collection-base-dir)))
    (when (file-exists-p path)
      path)))

(defun evil-collection-open-config-file (mode)
  "Open configuration file corresponding to MODE."
  (interactive
   (list
    (completing-read
     "Mode: "
     (cl-remove-if-not
      (lambda (mode)
        (evil-collection--mode-file mode (format "evil-collection-%s.el" mode)))
      (directory-files
       (expand-file-name "modes" evil-collection-base-dir)
       nil "^[^.]")))))
  (find-file (evil-collection--mode-file mode (format "evil-collection-%s.el" mode))))

(defun evil-collection-open-readme (mode)
  "Open README.org corresponding to MODE."
  (interactive
   (list
    (completing-read
     "Mode: "
     (cl-remove-if-not
      (lambda (mode)
        (evil-collection--mode-file mode "README.org"))
      (directory-files
       (expand-file-name "modes" evil-collection-base-dir)
       nil "^[^.]")))))
  (find-file (evil-collection--mode-file mode "README.org")))

(defun evil-collection--delay (condition form hook &optional append local name)
  "Execute FORM when CONDITION becomes true, checking with HOOK.
NAME specifies the name of the entry added to HOOK.  If APPEND is
non-nil, the entry is appended to the hook.  If LOCAL is non-nil,
the buffer-local value of HOOK is modified.

This is a backport of `evil-delay' without the deprecation notice to deal with
CI until migration can be done.
Ref: https://github.com/emacs-evil/evil-collection/issues/750"
  (eval `(evil-with-delay ,condition (,hook ,append ,local ,name) ,form) t))

;;;###autoload
(cl-defun evil-collection-translate-minor-mode-key (states modes
                                                           &rest translations
                                                           &key destructive
                                                           &allow-other-keys)
  "Translate keys in the keymap(s) corresponding to STATES and MODES.

Similar to `evil-collection-translate-key' but for minor modes.
STATES should be the name of an evil state, a list of states, or nil. MODES
should be a symbol corresponding to minor-mode to make the translations in or a
list of minor-mode symbols. TRANSLATIONS corresponds to a list of
key replacement pairs. For example, specifying \"a\" \"b\" will bind \"a\" to
\"b\"'s definition in the keymap. Specifying nil as a replacement will unbind a
key. If DESTRUCTIVE is nil, a backup of the keymap will be stored on the initial
invocation, and future invocations will always look up keys in the backup
keymap. When no TRANSLATIONS are given, this function will only create the
backup keymap without making any translations. On the other hand, if DESTRUCTIVE
is non-nil, the keymap will be destructively altered without creating a backup.
For example, calling this function multiple times with \"a\" \"b\" \"b\" \"a\"
would continue to swap and unswap the definitions of these keys. This means that
when DESTRUCTIVE is non-nil, all related swaps/cycles should be done in the same
invocation."
  (declare (indent defun))
  (unless (listp modes)
    (setq modes (list modes)))
  (unless (and (listp states)
               (not (null states)))
    (setq states (list states)))
  (dolist (mode-symbol modes)
    (let ((keymap-symbol (intern (format "%S-map" mode-symbol))))
      (dolist (state states)
        (let ((hook-name
               (symbol-name
                (gensym
                 (format "evil-collection-translate-key-in-%s" keymap-symbol)))))
          (evil-collection--delay `(and (boundp ',keymap-symbol)
                                        (keymapp ,keymap-symbol))
                                  `(evil-collection--translate-minor-mode-key
                                    ',state
                                    ',mode-symbol
                                    ',translations
                                    ,destructive)
                                  'after-load-functions
                                  t
                                  nil
                                  hook-name))))))


(defun evil-collection--translate-minor-mode-key (state
                                                  mode-symbol
                                                  translations
                                                  destructive)
  "Helper function for `evil-collection-translate-minor-mode-key'.
In the minor mode keymap corresponding to STATE and MODE-SYMBOL, make the key
TRANSLATIONS. When DESTRUCTIVE is non-nil, make the TRANSLATIONS destructively
without creating/referencing a backup keymap."
  (let* ((keymap-symbol (intern (format "%S-map" mode-symbol)))
         (backup-keymap-symbol (intern (format "evil-collection-%s%s-backup-map"
                                               mode-symbol
                                               (if state
                                                   (format "-%s-state" state)
                                                 ""))))
         (keymap (symbol-value keymap-symbol))
         (lookup-keymap (if (and (not destructive)
                                 (boundp backup-keymap-symbol))
                            (symbol-value backup-keymap-symbol)
                          (copy-keymap  ;FIXME: Why copy?
                           (if state
                               (evil-get-minor-mode-keymap state mode-symbol)
                             keymap))))
         (maps (cl-loop for (key replacement) on translations by 'cddr
                        ;; :destructive can be in TRANSLATIONS
                        unless (keywordp key)
                        collect key
                        and collect (when replacement
                                      (evil-lookup-key lookup-keymap replacement)))))
    (unless (or destructive
                (boundp backup-keymap-symbol))
      (set backup-keymap-symbol lookup-keymap))
    (apply #'evil-define-minor-mode-key state mode-symbol maps)))

(defun evil-collection--translate-key (state keymap-symbol
                                             translations
                                             destructive)
  "Helper function for `evil-collection-translate-key'.
In the keymap corresponding to STATE and KEYMAP-SYMBOL, make the key
TRANSLATIONS. When DESTRUCTIVE is non-nil, make the TRANSLATIONS destructively
without creating/referencing a backup keymap."
  (let* ((backup-keymap-symbol (intern (format "evil-collection-%s%s-backup-map"
                                               keymap-symbol
                                               (if state
                                                   (format "-%s-state" state)
                                                 ""))))
         (keymap (symbol-value keymap-symbol))
         (lookup-keymap (if (and (not destructive)
                                 (boundp backup-keymap-symbol))
                            (symbol-value backup-keymap-symbol)
                          (copy-keymap
                           (if state
                               (evil-get-auxiliary-keymap keymap state t t)
                             keymap))))
         (maps (cl-loop for (key replacement) on translations by 'cddr
                        ;; :destructive can be in TRANSLATIONS
                        unless (keywordp key)
                        collect key
                        and collect (when replacement
                                      (evil-lookup-key lookup-keymap replacement)))))
    (unless (or destructive
                (boundp backup-keymap-symbol))
      (set backup-keymap-symbol lookup-keymap))
    (apply #'evil-define-key* state keymap maps)))

;;;###autoload
(cl-defun evil-collection-translate-key (states keymaps
                                                &rest translations
                                                &key destructive
                                                &allow-other-keys)
  "Translate keys in the keymap(s) corresponding to STATES and KEYMAPS.
STATES should be the name of an evil state, a list of states, or nil. KEYMAPS
should be a symbol corresponding to the keymap to make the translations in or a
list of keymap symbols. Like `evil-define-key', when a keymap does not exist,
the keybindings will be deferred until the keymap is defined, so
`with-eval-after-load' is not necessary. TRANSLATIONS corresponds to a list of
key replacement pairs. For example, specifying \"a\" \"b\" will bind \"a\" to
\"b\"'s definition in the keymap. Specifying nil as a replacement will unbind a
key. If DESTRUCTIVE is nil, a backup of the keymap will be stored on the initial
invocation, and future invocations will always look up keys in the backup
keymap. When no TRANSLATIONS are given, this function will only create the
backup keymap without making any translations. On the other hand, if DESTRUCTIVE
is non-nil, the keymap will be destructively altered without creating a backup.
For example, calling this function multiple times with \"a\" \"b\" \"b\" \"a\"
would continue to swap and unswap the definitions of these keys. This means that
when DESTRUCTIVE is non-nil, all related swaps/cycles should be done in the same
invocation."
  (declare (indent defun))
  (unless (listp keymaps)
    (setq keymaps (list keymaps)))
  (unless (and (listp states)
               (not (null states)))
    (setq states (list states)))
  (dolist (keymap-symbol keymaps)
    (dolist (state states)
      (let ((hook-name
             (symbol-name
              (gensym
               (format "evil-collection-translate-key-in-%s" keymap-symbol)))))
        (evil-collection--delay `(and (boundp ',keymap-symbol)
                                      (keymapp ,keymap-symbol))
                                `(evil-collection--translate-key
                                  ',state
                                  ',keymap-symbol
                                  ',translations
                                  ,destructive)
                                'after-load-functions
                                t
                                nil
                                hook-name)))))


;;;###autoload
(defmacro evil-collection-swap-key (states keymaps &rest args)
  "Wrapper around `evil-collection-translate-key' for swapping keys.
STATES, KEYMAPS, and ARGS are passed to `evil-collection-translate-key'. ARGS
should consist of key swaps (e.g. \"a\" \"b\" is equivalent to \"a\" \"b\" \"b\"
\"a\" with `evil-collection-translate-key') and optionally keyword arguments for
`evil-collection-translate-key'."
  (declare (indent defun))
  (setq args (cl-loop for (key replacement) on args by 'cddr
                      collect key and collect replacement
                      and unless (keywordp key)
                      collect replacement and collect key))
  `(evil-collection-translate-key ,states ,keymaps ,@args))

;;;###autoload
(defmacro evil-collection-swap-minor-mode-key (states modes &rest args)
  "Wrapper around `evil-collection-translate-minor-mode-key' for swapping keys.
STATES, MODES, and ARGS are passed to
`evil-collection-translate-minor-mode-key'. ARGS should consist of key swaps
\(e.g. \"a\" \"b\" is equivalent to \"a\" \"b\" \"b\" \"a\"
with `evil-collection-translate-minor-mode-key') and optionally keyword
arguments for `evil-collection-translate-minor-mode-key'."
  (declare (indent defun))
  (setq args (cl-loop for (key replacement) on args by 'cddr
                      collect key and collect replacement
                      and unless (keywordp key)
                      collect replacement and collect key))
  `(evil-collection-translate-minor-mode-key ,states ,modes ,@args))

;;;###autoload
(defun evil-collection-require (mode &optional noerror)
  "Require the evil-collection-MODE file, but do not activate it.

MODE should be a symbol. This requires the evil-collection-MODE
feature without needing to manipulate `load-path'. NOERROR is
forwarded to `require'."
  (let* ((mode-name (symbol-name mode))
         (feature (intern (format "evil-collection-%s" mode-name)))
         (file (expand-file-name
                (format "modes/%s/evil-collection-%s" mode-name mode-name)
                evil-collection-base-dir)))
    (require feature file noerror)))

(declare-function evil-collection-unimpaired-setup "evil-collection-unimpaired")

;;;###autoload
(defun evil-collection-init (&optional modes)
  "Register the Evil bindings for all modes in `evil-collection-mode-list'.

Alternatively, you may register select bindings manually, for
instance:

  (with-eval-after-load \='calendar
    (evil-collection-calendar-setup))

If MODES is specified (as either one mode or a list of modes), use those modes
instead of the modes in `evil-collection-mode-list'."
  (interactive)
  (evil-collection-maybe-display-startup-warning)
  (if modes
      (or (listp modes) (setq modes (list modes)))
    (setq modes evil-collection-mode-list))
  (dolist (mode modes)
    (let ((m mode)
          (reqs (list mode)))
      (when (listp mode)
        (setq m (car mode)
              reqs (cdr mode)))
      (dolist (req reqs)
        (with-eval-after-load req
          ;; (message (format "Loaded %S..." req))
          (evil-collection-require m)
          (funcall (intern (concat "evil-collection-" (symbol-name m)
                                   "-setup")))
          (let ((mode-keymaps
                 (ignore-errors
                   (symbol-value
                    (intern (format "evil-collection-%s-maps" m))))))
            (when (or evil-collection-always-run-setup-hook-after-load
                      (not (memq m evil-collection--modes-with-delayed-setup)))
              (run-hook-with-args 'evil-collection-setup-hook
                                  m mode-keymaps)))))))
  (when evil-collection-want-unimpaired-p
    (evil-collection-require 'unimpaired)
    (evil-collection-unimpaired-setup)))

(defun evil-collection-setup (&optional modes)
  "Register the Evil bindings for all modes in `evil-collection-mode-list'.

----------------------EXPERIMENTAL------------------------------------------

This is a special wrapper over `evil-collection-init' that respects
configuration from `evil-collection-config'. This function is experimental,
so don't use if you don't want breakages or API changes.

If MODES is specified (as either one mode or a list of modes), use those modes
instead of the modes in `evil-collection-mode-list'.

----------------------EXPERIMENTAL------------------------------------------"
  (if modes
      (or (listp modes) (setq modes (list modes)))
    (setq modes evil-collection-mode-list))
  (let ((configs evil-collection-config)
        (deferred)  ; alist of (mode . delay)
        (hooked))   ; alist of (mode . hook)
    (dolist (config configs)
      (if-let* ((hook (plist-get (cdr config) :hook)))
          (push (cons (car config) hook) hooked)
        (when-let* ((defer (plist-get (cdr config) :defer)))
          (push (cons (car config) defer) deferred))))
    (let ((filtered-modes
           (cl-remove-if
            (lambda (mode)
              ;; `evil-collection-config' format is slightly different
              ;; than `evil-collection-mode-list', so use the mode
              ;; entry from the mode list instead.
              (cl-some
               (lambda (m)
                 (or (when-let* ((entry (assq m deferred)))
                       (setf (car entry) mode) t)
                     (when-let* ((entry (assq m hooked)))
                       (setf (car entry) mode) t)))
               (if (consp mode) mode (list mode))))
            modes)))
      (evil-collection-init filtered-modes))
    (message (format "Deferring: %S" (mapcar #'car deferred)))
    (dolist (entry deferred)
      (let ((mode (car entry))
            (delay (cdr entry)))
        (run-with-idle-timer
         (if (numberp delay) delay evil-collection-defer-delay) nil
         (apply-partially 'evil-collection-init (list mode)))))
    (dolist (entry hooked)
      (let* ((mode (car entry))
             (hook-sym (cdr entry))
             (hook-name (let ((name (symbol-name hook-sym)))
                          (if (string-suffix-p "-hook" name)
                              hook-sym
                            (intern (concat name "-hook")))))
             (mode-sym (if (consp mode) (car mode) mode))
             (fn-sym (intern (format "evil-collection--init-%s" mode-sym))))
        (defalias fn-sym
          (lambda ()
            (remove-hook hook-name fn-sym)
            (evil-collection-init (list mode))))
        (add-hook hook-name fn-sym)))))

(defvar evil-collection-delete-operators '(evil-delete
                                           evil-cp-delete
                                           evil-sp-delete
                                           lispyville-delete)
  "List of delete operators.")

(defvar evil-collection-yank-operators '(evil-yank
                                         evil-cp-yank
                                         evil-sp-yank
                                         lispyville-yank)
  "List of yank operators.")

;;* Search

(defun evil-collection-evil-search-enabled ()
  (eq evil-search-module 'evil-search))

(defvar evil-collection-evil-search-forward
  `(menu-item "" nil :filter ,(lambda (&optional _)
                               (if (eq evil-search-module 'evil-search)
                                   #'evil-ex-search-forward
                                 #'evil-search-forward))))

(defvar evil-collection-evil-search-backward
  `(menu-item "" nil :filter ,(lambda (&optional _)
                               (if (eq evil-search-module 'evil-search)
                                   #'evil-ex-search-backward
                                 #'evil-search-backward))))

(defvar evil-collection-evil-search-next
  `(menu-item "" nil :filter ,(lambda (&optional _)
                               (if (eq evil-search-module 'evil-search)
                                   #'evil-ex-search-next
                                 #'evil-search-next))))

(defvar evil-collection-evil-search-previous
  `(menu-item "" nil :filter ,(lambda (&optional _)
                               (if (eq evil-search-module 'evil-search)
                                   #'evil-ex-search-previous
                                 #'evil-search-previous))))
;;* Unmap

(defvar evil-collection-unmap-skip-events
  '(menu-bar tool-bar tab-bar tab-line header-line mode-line)
  "Events under which `evil-collection-unmap' will not descend.
These are UI keymaps where removing entries is rarely the intent.")

;;;###autoload
(defun evil-collection-unmap (map-sym &rest things)
  "Completely unmap THINGS from MAP-SYM.

MAP-SYM is a symbol bound to a keymap. If MAP-SYM is not yet bound
to a keymap, the operation is deferred via `after-load-functions',
matching the deferral behaviour of `evil-collection-define-key'.

Each thing in THINGS is one of:

  - A command symbol — every binding pointing to that command
    anywhere in the keymap is removed (TAB, RET, C-c TAB,
    evil-state bindings, [remap ...] entries, etc.).

  - A key description string accepted by `kbd' (e.g. \"RET\",
    \"a\", \"<return>\", \"TAB\", \"C-c C-c\") — the binding at
    that exact key sequence is removed at the top level and
    inside every evil-state auxiliary keymap.

  - A key vector — same as a key description, already parsed.

Removals use `define-key' with REMOVE set, so bindings are
deleted entirely, not just shadowed with nil.

Parent keymaps installed via `set-keymap-parent' are not modified,
and sub-keymaps stored under events in
`evil-collection-unmap-skip-events' (menu-bar, tool-bar, etc.) are
not descended into."
  (declare (indent defun))
  (cond
   ((and (boundp map-sym) (keymapp (symbol-value map-sym)))
    (evil-collection--unmap (symbol-value map-sym) things))
   ((boundp map-sym)
    (user-error "evil-collection-unmap: %s is not a keymap" map-sym))
   (t
    (let* ((fname (format "evil-collection-unmap-in-%s" map-sym))
           (fun (make-symbol fname)))
      (fset fun
            `(lambda (&rest _)
               (when (and (boundp ',map-sym) (keymapp ,map-sym))
                 (remove-hook 'after-load-functions #',fun)
                 (condition-case-unless-debug err
                     (evil-collection--unmap
                      (symbol-value ',map-sym) ',things)
                   (error
                    (message
                     ,(format
                       "evil-collection-unmap: error unmapping in %s %%S"
                       map-sym)
                     err))))))
      (add-hook 'after-load-functions fun t)))))

(defun evil-collection--unmap (keymap things)
  "Workhorse for `evil-collection-unmap'."
  (let (commands keys)
    (dolist (thing things)
      (cond
       ((null thing))
       ((symbolp thing) (push thing commands))
       ((vectorp thing) (push thing keys))
       ((stringp thing) (push (kbd thing) keys))
       (t (user-error "evil-collection-unmap: cannot unmap %S" thing))))
    ;; --- Remove by command (deep walk) -----------------------------------
    (when commands
      (let (paths)
        (evil-collection--walk-keymap-shallow
         keymap []
         (lambda (key-vec binding)
           (let ((real (if (and (consp binding)
                                (eq (car-safe binding) 'menu-item))
                           (nth 2 binding)
                         binding)))
             (when (memq real commands)
               (push (copy-sequence key-vec) paths)))))
        ;; Remove deeper paths first so we don't yank a prefix from under
        ;; entries we still want to remove.
        (setq paths (sort paths (lambda (a b) (> (length a) (length b)))))
        (dolist (path paths)
          (ignore-errors (define-key keymap path nil t)))))
    ;; --- Remove by key (top-level + evil state aux maps) -----------------
    (dolist (key keys)
      (ignore-errors (define-key keymap key nil t))
      (dolist (entry (cdr keymap))
        (when (and (consp entry)
                   (symbolp (car entry))
                   (keymapp (cdr entry))
                   (string-suffix-p "-state" (symbol-name (car entry))))
          (ignore-errors (define-key (cdr entry) key nil t)))))))

(defun evil-collection--walk-keymap-shallow (keymap prefix fn)
  "Walk KEYMAP under PREFIX, calling FN with (KEY-VEC BINDING).

Stops at the parent-keymap boundary (does NOT follow inherited
keymaps). Skips sub-keymaps whose event is in
`evil-collection-unmap-skip-events'."
  (let ((tail (cdr keymap)))
    (while (and tail (not (eq (car-safe tail) 'keymap)))
      (let ((entry (car tail)))
        (cond
         ((stringp entry))                  ; prompt — skip
         ((char-table-p entry)
          (map-char-table
           (lambda (ev binding)
             (when binding
               (cond
                ((characterp ev)
                 (evil-collection--walk-keymap-entry ev binding prefix fn))
                ((and (consp ev)
                      (characterp (car ev))
                      (characterp (cdr ev)))
                 (let ((c (car ev)) (end (cdr ev)))
                   (while (<= c end)
                     (evil-collection--walk-keymap-entry c binding prefix fn)
                     (setq c (1+ c))))))))
           entry))
         ((vectorp entry)
          (let ((i 0) (len (length entry)))
            (while (< i len)
              (let ((b (aref entry i)))
                (when b
                  (evil-collection--walk-keymap-entry i b prefix fn)))
              (setq i (1+ i)))))
         ((consp entry)
          (let* ((ev (car entry))
                 (rest (cdr entry))
                 ;; Strip optional item-string in (EVENT ITEM-STRING . BINDING)
                 (binding (if (and (consp rest) (stringp (car rest)))
                              (cdr rest)
                            rest)))
            (evil-collection--walk-keymap-entry ev binding prefix fn)))))
      (setq tail (cdr tail)))))

(defun evil-collection--walk-keymap-entry (event binding prefix fn)
  "Dispatch on BINDING type for `evil-collection--walk-keymap-shallow'."
  (let ((key (vconcat prefix (vector event))))
    (cond
     ((memq event evil-collection-unmap-skip-events))
     ((keymapp binding)
      (evil-collection--walk-keymap-shallow binding key fn))
     ((and (consp binding)
           (eq (car-safe binding) 'menu-item)
           (keymapp (nth 2 binding)))
      (evil-collection--walk-keymap-shallow (nth 2 binding) key fn))
     (binding
      (funcall fn key binding)))))

(provide 'evil-collection)
;;; evil-collection.el ends here
