helpful

a fork of Wilfred's helpful
git clone https://git.trogloxene.org/helpful.git
Log | Files | Refs | README

commit 001707510a9b3394f977e345a210a67233537750
parent 75f9b5fafd7b63bd701d1d9c6cb4c92b5fc8aeac
Author: Wilfred Hughes <me@wilfred.me.uk>
Date:   Mon,  5 Feb 2018 23:18:36 +0000

Display keymap references in docstrings

Diffstat:
MCHANGELOG.md | 1+
Mhelpful.el | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/unit-test.el | 41++++++++++++++++++++++++++++++++++++++++-
3 files changed, 144 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -18,6 +18,7 @@ Better handling of docstrings: `` `C-M-\' `` was not highlighted because `C-M-\` doesn't look like a symbol. * Handle nested key sequences correctly, such as `` `\\[foo]' ``. +* Handle keymap sequences, such as `\\{foo-mode-map}`. # v0.6 diff --git a/helpful.el b/helpful.el @@ -619,9 +619,99 @@ blank line afterwards." docstring t t)) -;; TODO: handle keymaps of the form \\{foo}. +(defun helpful--char-table-keys (char-table) + "Convert CHAR-TABLE to list of pairs (KEYCODES COMMAND)." + ;; Kludge: use `describe-vector' to convert a char-table to a sparse + ;; keymap. + (let (result) + (map-char-table + (lambda (key value) + (push (list (vector key) value) result)) + char-table) + result)) + +(defun helpful--keymap-keys (keymap) + "Return all the keys and commands in KEYMAP. +Flattens nested keymaps and follows remapped commands. + +Returns a list of pairs (KEYCODES COMMAND), where KEYCODES is a +vector suitable for `key-description', and COMMAND is a smbol." + (let (result) + (if (symbolp keymap) + (push (list (vector) keymap) result) + (dolist (item (cdr keymap)) + (cond + ((and (consp item) + (eq (car item) 'menu-bar)) + ;; Skip menu bar items. + nil) + ;; Sparse keymaps are lists. + ((consp item) + (-let [(keycode . value) item] + (-each (helpful--keymap-keys value) + (-lambda ((keycodes command)) + (push (list (vconcat (vector keycode) keycodes) command) + result))))) + ;; Dense keymaps are char-tables. + ((char-table-p item) + (map-char-table + (lambda (keycode value) + (-each (helpful--keymap-keys value) + (-lambda ((keycodes command)) + (push (list (vconcat (vector keycode) keycodes) command) + result)))) + item))))) + ;; For every command `foo' mapped to a command `bar', show `foo' with + ;; the key sequence for `bar'. + (setq result + (-map-when + (-lambda ((keycodes _)) + (and (> (length keycodes) 1) + (eq (elt keycodes 0) 'remap))) + (-lambda ((keycodes command)) + (list + (where-is-internal (elt keycodes 1) global-map t) + command)) + result)) + ;; Preserve the original order of the keymap. + (nreverse result))) + +;; TODO: unlike `substitute-command-keys', this shows keybindings +;; which are currently shadowed (e.g. a global minor mode map). +(defun helpful--format-keymap (keymap) + "Format KEYMAP." + (let* ((keys-and-commands (helpful--keymap-keys keymap)) + ;; Convert keycodes [27 i] to "C-M-i". + (keys (-map #'-first-item keys-and-commands)) + ;; Add padding so all our strings are the same length. + (formatted-keys (-map #'key-description keys)) + (max-formatted-length (-max (cons 0 (-map #'length formatted-keys)))) + (aligned-keys (--map (s-pad-right (1+ max-formatted-length) + " " it) + formatted-keys)) + ;; Format commands as buttons. + (commands (-map (-lambda ((_ command)) command) + keys-and-commands)) + (formatted-commands + (--map + (helpful--button + (symbol-name it) + 'helpful-describe-button + 'symbol it) + commands)) + ;; Build lines for display. + (lines + (-map (-lambda ((key . command)) (format "%s %s" key command)) + (-zip-pair aligned-keys formatted-commands)))) + ;; The flattened keymap will have normal bindings first, and + ;; inherited bindings last. Sort so that we group by prefix. + (s-join "\n" (-sort #'string< lines)))) + +;; TODO: \\<foo> (defun helpful--format-command-keys (docstring) - "Convert command key references in docstrings to buttons. + "Convert command key references and keymap references +in DOCSTRING to buttons. + Emacs uses \\= to escape \\[ references, so replace that unescaping too." ;; Based on `substitute-command-keys', but converts command @@ -650,6 +740,17 @@ unescaping too." ;; Set the new keymap. (setq keymap (symbol-value (intern symbol-name))))) ((looking-at + ;; Text of the form \\{foo-mode-map} + (rx "\\{" (group (+ (not (in "}")))) "}")) + (let* ((symbol-with-parens (match-string 0)) + (symbol-name (match-string 1)) + (keymap (symbol-value (intern symbol-name)))) + ;; Remove the original string. + (delete-region (point) + (+ (point) (length symbol-with-parens))) + (insert + (helpful--format-keymap keymap)))) + ((looking-at ;; Text of the form \\[foo-command] (rx "\\[" (group (+ (not (in "]")))) "]")) (let* ((symbol-with-parens (match-string 0)) diff --git a/test/unit-test.el b/test/unit-test.el @@ -188,7 +188,16 @@ (should (eq (get-text-property 0 'face formatted) - 'button)))) + 'button))) + ;; Propertize mode maps. + (-let [formatted (helpful--format-docstring "`\\{python-mode-map}'")] + (should + (string-equal formatted "C-SPC")) + (should + (eq + (get-text-property 0 'face formatted) + 'button))) + ) (setq helpful-var-without-defvar 'foo) @@ -302,3 +311,33 @@ file." load-history))) ;; This should not error. (helpful-function 'helpful-fn-in-elc))) + +(ert-deftest helpful--keymap-keys--sparse () + (let* ((parent-keymap (make-sparse-keymap)) + (keymap (make-sparse-keymap))) + (set-keymap-parent keymap parent-keymap) + (define-key parent-keymap (kbd "a") #'forward-char) + (define-key keymap (kbd "C-c C-M-a") #'backward-char) + (define-key keymap [remap quoted-insert] #'forward-line) + (should + (equal + (helpful--keymap-keys keymap) + '(([17] forward-line) + ([3 27 1] backward-char) + ([97] forward-char)))))) + +(ert-deftest helpful--keymap-keys () + (let* ((parent-keymap (make-keymap)) + (keymap (make-keymap))) + (set-keymap-parent keymap parent-keymap) + (define-key parent-keymap (kbd "a") #'forward-char) + (define-key keymap (kbd "C-c C-M-a") #'backward-char) + (define-key keymap [remap quoted-insert] #'forward-line) + (should + (equal + ;; This order differs from a sparse keymap. We should fix that + ;; if it makes any difference. + (helpful--keymap-keys keymap) + '(([3 27 1] backward-char) + ([17] forward-line) + ([97] forward-char))))))