all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* bug#43709: minibuffer keybinding cheatsheet and launcher [CODE SUBMISSION]
@ 2020-09-29 16:59 Boruch Baum
  2020-09-29 17:41 ` Drew Adams
                   ` (2 more replies)
  0 siblings, 3 replies; 17+ messages in thread
From: Boruch Baum @ 2020-09-29 16:59 UTC (permalink / raw)
  To: 43709

[-- Attachment #1: Type: text/plain, Size: 691 bytes --]

If you've ever used packages such as `ivy' or `magit', you've probably
benefited from each's custom combination keybinding cheatsheet and
launcher: `hydra' in the case of `ivy', and `transient' for `magit'. The
attached package `key-assist' attempts to offer a generic and very
simple alternative requiring only the `completing-read' function
commonly used in core vanilla emacs. `key-assist' is trivial to
implement "on-the-fly" interactively for any buffer, and
programmatically much simpler to customize that either `hydra' or
`transient'. And did I mention that it only requires `completing-read'?

--
hkp://keys.gnupg.net
CA45 09B5 5351 7C11 A9D1  7286 0036 9E45 1595 8BC0

[-- Attachment #2: key-assist.el --]
[-- Type: text/plain, Size: 12593 bytes --]

;;; key-assist.el --- minibuffer keybinding cheatsheet and launcher -*- lexical-binding: t -*-

;; Copyright © 2020, Boruch Baum <boruch_baum@gmx.com>
;; Available for assignment to the Free Software Foundation, Inc.

;; Author: Boruch Baum <boruch_baum@gmx.com>
;; Maintainer: Boruch Baum <boruch_baum@gmx.com>
;; Keywords: minibuffer keybindings
;; Package: key-assist

;; This file is NOT part of GNU Emacs.

;; This 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 software 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 software.  If not, see <https://www.gnu.org/licenses/>.

;;
;;; Commentary:

;;   If you've ever used packages such as `ivy' or `magit', you've
;;   probably benefited from each's custom combination keybinding
;;   cheatsheet and launcher: `hydra' in the case of `ivy', and
;;   `transient' for `magit'. The current package `key-assist'
;;   attempts to offer a generic and very simple alternative requiring
;;   only the `completing-read' function commonly used in core vanilla
;;   emacs. `key-assist' is trivial to implement "on-the-fly"
;;   interactively for any buffer, and programmatically much simpler
;;   to customize that either `hydra' or `transient'. And did I
;;   mention that it only requires `completing-read'?

;;
;;; Dependencies:

;; `cl-seq' - For `cl-position', but `cl-lib' is long part of all
;;            modern emcasen.

;;
;;; Installation:

;; 1) Evaluate or load this file.

;;
;;; Interactive operation:

;;   Run M-x key-assist from the buffer of interest. Specify a
;;   selection (or don't), press <TAB> to view the presentation, and
;;   then either exit with your new-found knowledge of the command
;;   keybindings, or use standard emacs tab completion to select an
;;   option, and press <RETURN> to perform the action.
;;
;;   If you choose not to respond to the initial prompt, a list of
;;   keybindings and command descriptions will be generated based upon
;;   the first word of the buffer's major mode. For, example, in a
;;   `w3m' buffer, the list will be of all interactive functions
;;   beginning `w3m-'. This works out to be great as a default, but
;;   isn't always useful. For example, in an `emacs-lisp-mode' buffer,
;;   what would you expect it to usefully produce? At the other
;;   extreme might be a case of a buffer too many obscure keybindings
;;   of little use.

;;   You can also respond to the prompt with your own regexp of
;;   commands to show, or with the name of a keymap of your choice.
;;   For the purposes of `key-assist', a regexp can be just a
;;   substring, without REQUIRING any leading or trailing globs.

;;   In all cases, note that the package can only present keybindings
;;   currently active in the current buffer, so if a sub-package
;;   hasn't been loaded yet, that package's keybindings would not be
;;   presented. Also note that the commands are presented sorted by
;;   keybinding length, alphabetically.

;;
;;; Programmatic operation:

;;   See the docstrings for functions `key-assist' and
;;   `key-assist--get-cmds'.

;;
;;; Configuration:

;;   Two variables are available to exclude items from the
;;   presentation list: `key-assist-exclude-cmds' and
;;   `key-assist-exclude-regexps'. See there for further information.

;;
;;; Compatability

;; Tested with emacs 26.1 in debian.

;;  TODO:
;;
;;   * In a vanilla emacs environment, the sorting appears wrong
;;     seemingly partially because of how the completion buffer is
;;     constructed, bottom-up. This isn't an issue when using one of
;;     the supplemental minibuffer completion packages.

\f
;;
;;; Code

(require 'cl-seq) ;; cl-member, cl-position

;;
;;; Variables

(defvar key-assist-exclude-cmds
  '(ignore
    self-insert-command
    digit-argument
    negative-argument
    describe-mode)
  "List of commands to always exclude from `key-assist' output.")

(defvar key-assist-exclude-regexps '("-mouse-")
  "List of regexps of commands to exclude from `key-assist' output.")
;; TODO: Don't depend upon a mouse command having the word '-mouse-' in it.


;;
;;; Internal functions

(defun key-assist--get-keybinding (cmd &optional key-map)
  "Return a string with CMD's shortest keybinding."
  (let (shortest)
    (dolist (key (mapcar 'key-description
                         (where-is-internal
                           cmd key-map nil t)))
      (when (or (not shortest)
                (> (length shortest) (length key)))
        (setq shortest key)))
    shortest))

(defun key-assist--get-description (cmd)
  "Return a string with CMD's description.
CMD is a symbol of an interactive command."
;; TODO: Change hard-coded length to an ARG.
  (let ((doc (documentation cmd t)))
    (format "\t%s"
      (if (or (not doc)
              (not (string-match "\n" doc))
              (zerop (match-beginning 0)))
        (concat (symbol-name cmd) " (not documented)")
       (substring doc 0 (match-beginning 0))))))

(defun key-assist--vet-cmd (cmd result-list)
  "Check whether CMD should be on a `key-assist' list.

See `key-assist-exclude-cmds' and `key-assist-exclude-regexps'."
  (and
    (symbolp cmd)
    (commandp cmd)
    (not (cl-member cmd result-list
                    :test (lambda (cmd l) (equal cmd (nth 1 l))))) ; (assq cmd result-list))
    (not (memq cmd key-assist-exclude-cmds))
    (let ((not-found t))
      (mapc (lambda (x)
              (when (string-match x (format "%s" cmd))
                (setq not-found nil)))
            key-assist-exclude-regexps)
      not-found)))

(defun key-assist--parse-cmd (cmd result-list &optional key-map)
  "Extract commands and shortest keybindings from a keymap.

This is an internal function used by `key-assist'. Returns a list
whose elements are a keybinding string, a command symbol, and a
description string."
  (when (key-assist--vet-cmd cmd result-list)
    (let* ((key-map (when (keymapp key-map) key-map))
           (shortest (key-assist--get-keybinding cmd key-map)))
      (when shortest
        (list shortest cmd (concat shortest (key-assist--get-description cmd)))))))

(defun key-assist--get-cmds (&optional spec nosort nofinish)
  "Return a list of commands, keybindings, and descriptions.

Returns a list of CONS, whose CAR is the command, and whose CDR
is a string of the form \"shortest-keybinding tab-character
command-description\".

Optional arg SPEC may be a regexp string of desired commands. If
NIL, a regexp is generated based upon the first word of the
buffer's major mode. SPEC may also be a keymap of desired
commands. In both of these cases, the resulting list is sorted
alphabetically by keybinding length.

SPEC has additional options of being either a list of commands,
or a list of CONS whose CAR is a command, and whose CDR is either a
description-string or a function which returns a description
string. A final programmatic option is for SPEC to be any
combination of the above options. For that most complex case, the
first list element of SPEC must be the symbol 'collection. For
none of these additional options is sorting performed.

Optional arg NOSORT can be a function to replace the default sort
algorithm with the programmer's desired post-processing, or some
other non-nil value for no sorting at all. If a function, it
should accept a single list of elements (keybinding-string
commandp description-string) and should return a list of
elements (anything commandp description-string).

Optional arg NOFINSH return a list in `key-assist--parse-cmd'
format instead of the list of CONS described above. It is used
internally for processing 'collection lists."
  (setq spec
    (cond
     ((or (not spec)
          (and (stringp spec)
               (zerop (length spec))))
       (let ((str (symbol-name major-mode)))
         (substring str 0 (1+ (string-match "-" str)))))
     ((and (stringp spec)
           (boundp (intern spec))
           (keymapp (symbol-value (intern spec))))
       (symbol-value (intern spec)))
     (t spec)))
  (let (name result-elem (result-list '()))
    (cond
     ((keymapp spec)
       (let (cmd)
         (dolist (elem spec)
           (cond
            ((atom elem)) ;; eg. 'keymap
            ((listp (setq cmd (cdr elem)))) ;; TODO: possibly also embedded keymap?
            ((commandp cmd) ;; this excludes 'menubar
              (when (setq result-elem (key-assist--parse-cmd cmd result-list))
                (push result-elem result-list)))))))
     ((stringp spec)
       (mapatoms
         (lambda (x)
            (and (commandp x)
                 (string-match spec (setq name (symbol-name x)))
                 (when (setq result-elem
                         (key-assist--parse-cmd x result-list))
                   (push result-elem result-list))))))
     ((listp spec)
       (cond
        ((eq (car spec) 'collection)
          (dolist (collection-element (cdr spec))
            ;; Maybe it's more efficient to sort each collection element?
            (let ((temp-list (key-assist--get-cmds collection-element 'nosort 'nofinish)))
              (dolist (elem temp-list)
                (push elem result-list)))))
        ((commandp (car spec))
          (dolist (cmd spec)
            (when (setq result-elem (key-assist--parse-cmd cmd result-list))
              (push result-elem result-list))))
        (t ; spec is a list of CONS (cmd . (or string function))
          (dolist (elem spec)
            (when (key-assist--vet-cmd (car elem) result-list)
              (let ((shortest (key-assist--get-keybinding (car elem))))
                (when shortest
                  (push (list shortest
                              (car elem)
                              (if (stringp (cadr elem))
                                (cadr elem)
                               (funcall (cadr elem))))
                        result-list))))))))
     (t (error "Improper SPEC format.")))
    (when (not nosort)
      (setq result-list
        (if (functionp nosort)
          (funcall nosort result-list)
         (sort result-list
               (lambda (a b) (cond
                              ((= (length (car a)) (length (car b)))
                                 (string< (car a) (car b)))
                              ((< (length (car a)) (length (car b))))
                              (t nil)))))))
    (if nofinish
      result-list
     (mapcar (lambda (x) (cons (nth 1 x) (nth 2 x)))
             result-list))))

;;
;;; Interactive functions

(defun key-assist (&optional spec prompt nosort)
  "Prompt to eval a locally relevant function, with hints and keybindings.
Press TAB to see the hints.

Interactively, the optional arg SPEC is either a regexp string
for candidate commands to match, or a keymap from which to
prepare the hints. If NIL, a regexp is generated based upon the
first word of the buffer's major mode. Results are presented
sorted alphabetically by keybinding length.

Programmatically, optional arg PROMPT can be used to customize
the prompt. For the further programmatic options of SPEC and for
a description of arg NOSORT, see function `key-assist--get-cmds'.

See also variables `key-assist-exclude-regexps' and
`key-assist-exclude-cmds'."
  (interactive "MOptional: Enter command regexp or keymap name: ")
  (let* ((prompt   (or prompt "Select: "))
         (commands (key-assist--get-cmds spec nosort))
         (choices  (mapcar 'cdr commands))
         choice minibuffer-history)
    (if (not choices)
      (user-error "No choices found.")
     (while (not (setq choice
                   (cl-position
                     (substring-no-properties ; Is -no-properties necessary?
                       (completing-read prompt choices nil t))
                     choices :test 'equal))))
     (funcall (car (nth choice commands))))))

\f
;;
;;; Conclusion

(provide 'key-assist)

;;; key-assist.el ends here

^ permalink raw reply	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2021-07-22  6:49 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2020-09-29 16:59 bug#43709: minibuffer keybinding cheatsheet and launcher [CODE SUBMISSION] Boruch Baum
2020-09-29 17:41 ` Drew Adams
2020-09-29 18:22   ` Boruch Baum
2020-09-29 19:05     ` Drew Adams
2020-09-29 18:26   ` Boruch Baum
2020-10-01 13:51 ` Stefan Monnier
2020-10-01 15:31   ` Boruch Baum
2020-10-01 16:04     ` Stefan Monnier
2020-10-01 20:46       ` Boruch Baum
2020-10-01 18:25   ` Boruch Baum
2020-10-02  7:14 ` bug#43709: minibuffer keybinding cheatsheet and launcher [V2 CODE SUBMISSION] Boruch Baum
2020-10-02 18:24   ` Drew Adams
2020-10-15 18:53     ` bug#43709: minibuffer keybinding cheatsheet and launcher [V3 " Boruch Baum
2020-10-15 18:57       ` Drew Adams
2021-07-21 12:14       ` bug#43709: minibuffer keybinding cheatsheet and launcher [CODE SUBMISSION] Lars Ingebrigtsen
2021-07-21 13:48         ` Boruch Baum
2021-07-22  6:49           ` Phil Sainty

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.