dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

helpful.el (107069B)


      1 ;;; helpful.el --- A better *help* buffer            -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2017-2022  Wilfred Hughes
      4 
      5 ;; Author: Wilfred Hughes <me@wilfred.me.uk>
      6 ;; URL: https://github.com/Wilfred/helpful
      7 ;; Keywords: help, lisp
      8 ;; Version: 0.21
      9 ;; Package-Requires: ((emacs "25") (dash "2.18.0") (s "1.11.0") (f "0.20.0") (elisp-refs "1.2"))
     10 
     11 ;; This program is free software; you can redistribute it and/or modify
     12 ;; it under the terms of the GNU General Public License as published by
     13 ;; the Free Software Foundation, either version 3 of the License, or
     14 ;; (at your option) any later version.
     15 
     16 ;; This program is distributed in the hope that it will be useful,
     17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19 ;; GNU General Public License for more details.
     20 
     21 ;; You should have received a copy of the GNU General Public License
     22 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     23 
     24 ;;; Commentary:
     25 
     26 ;; Helpful is a replacement for *help* buffers that provides much more
     27 ;; contextual information.  To get started, try:
     28 ;; `M-x helpful-function RET helpful-function
     29 ;;
     30 ;; The full set of commands you can try is:
     31 ;;
     32 ;; * helpful-function
     33 ;; * helpful-command
     34 ;; * helpful-key
     35 ;; * helpful-macro
     36 ;; * helpful-callable
     37 ;; * helpful-variable
     38 ;; * helpful-at-point
     39 ;;
     40 ;; For more information and screenshots, see
     41 ;; https://github.com/wilfred/helpful
     42 
     43 ;;; Code:
     44 
     45 (require 'elisp-refs)
     46 (require 'help)
     47 (require 'help-fns)
     48 (require 'dash)
     49 (require 's)
     50 (require 'f)
     51 (require 'find-func)
     52 (require 'nadvice)
     53 (require 'info-look)
     54 (require 'edebug)
     55 (require 'trace)
     56 (require 'imenu)
     57 (require 'cc-langs)
     58 
     59 (declare-function org-link-types "ol" ())
     60 (declare-function org-link-store-props "ol" (&rest plist))
     61 (declare-function org-link-get-parameter "ol" (type key))
     62 
     63 (defvar-local helpful--sym nil)
     64 (defvar-local helpful--callable-p nil)
     65 (defvar-local helpful--associated-buffer nil
     66   "The buffer being used when showing inspecting
     67 buffer-local variables.")
     68 (defvar-local helpful--start-buffer nil
     69   "The buffer we were originally called from.")
     70 (defvar-local helpful--view-literal nil
     71   "Whether to show a value as a literal, or a pretty interactive
     72 view.")
     73 (defvar-local helpful--first-display t
     74   "Whether this is the first time this results buffer has been
     75 displayed.
     76 
     77 Nil means that we're refreshing, so we don't want to clobber any
     78 settings changed by the user.")
     79 
     80 (defgroup helpful nil
     81   "A rich help system with contextual information."
     82   :link '(url-link "https://github.com/Wilfred/helpful")
     83   :group 'help)
     84 
     85 (defcustom helpful-max-buffers
     86   5
     87   "Helpful will kill the least recently used Helpful buffer
     88 if there are more than this many.
     89 
     90 To disable cleanup entirely, set this variable to nil. See also
     91 `helpful-kill-buffers' for a one-off cleanup."
     92   :type '(choice (const nil) number)
     93   :group 'helpful)
     94 
     95 (defcustom helpful-switch-buffer-function
     96   #'pop-to-buffer
     97   "Function called to display the *Helpful* buffer."
     98   :type 'function
     99   :group 'helpful)
    100 
    101 ;; TODO: explore whether more basic highlighting is fast enough to
    102 ;; handle larger functions. See `c-font-lock-init' and its use of
    103 ;; font-lock-keywords-1.
    104 (defconst helpful-max-highlight 5000
    105   "Don't highlight code with more than this many characters.
    106 
    107 This is currently only used for C code, as lisp highlighting
    108 seems to be more efficient. This may change again in future.
    109 
    110 See `this-command' as an example of a large piece of C code that
    111 can make Helpful very slow.")
    112 
    113 (defun helpful--kind-name (symbol callable-p)
    114   "Describe what kind of symbol this is."
    115   (cond
    116    ((not callable-p) "variable")
    117    ((commandp symbol) "command")
    118    ((macrop symbol) "macro")
    119    ((functionp symbol) "function")
    120    ((special-form-p symbol) "special form")))
    121 
    122 (defun helpful--buffer (symbol callable-p)
    123   "Return a buffer to show help for SYMBOL in."
    124   (let* ((current-buffer (current-buffer))
    125          (buf-name
    126           (format "*helpful %s*"
    127                   (if (symbolp symbol)
    128                       (format "%s: %s"
    129                               (helpful--kind-name symbol callable-p)
    130                               symbol)
    131                     "lambda")))
    132          (buf (get-buffer buf-name)))
    133     (unless buf
    134       ;; If we need to create the buffer, ensure we don't exceed
    135       ;; `helpful-max-buffers' by killing the least recently used.
    136       (when (numberp helpful-max-buffers)
    137         (let* ((buffers (buffer-list))
    138                (helpful-bufs (--filter (with-current-buffer it
    139                                          (eq major-mode 'helpful-mode))
    140                                        buffers))
    141                ;; `buffer-list' seems to be ordered by most recently
    142                ;; visited first, so keep those.
    143                (excess-buffers (-drop (1- helpful-max-buffers) helpful-bufs)))
    144           ;; Kill buffers so we have one buffer less than the maximum
    145           ;; before we create a new one.
    146           (-each excess-buffers #'kill-buffer)))
    147 
    148       (setq buf (get-buffer-create buf-name)))
    149 
    150     ;; Initialise the buffer with the symbol and associated data.
    151     (with-current-buffer buf
    152       (helpful-mode)
    153       (setq helpful--sym symbol)
    154       (setq helpful--callable-p callable-p)
    155       (setq helpful--start-buffer current-buffer)
    156       (setq helpful--associated-buffer current-buffer)
    157       (setq list-buffers-directory
    158         (if (symbolp symbol) (format "%s: %s" (helpful--kind-name symbol callable-p) symbol) "lambda"))
    159       (if (helpful--primitive-p symbol callable-p)
    160           (setq-local comment-start "//")
    161         (setq-local comment-start ";")))
    162     buf))
    163 
    164 (defface helpful-heading
    165   '((t (:weight bold)))
    166   "Face used for headings in Helpful buffers.")
    167 
    168 (defun helpful--heading (text)
    169   "Propertize TEXT as a heading."
    170   (propertize (concat text "\n") 'face 'helpful-heading))
    171 
    172 (defun helpful--format-closure (sym form)
    173   "Given a closure, return an equivalent defun form."
    174   (-let (((_keyword _env args . body) form)
    175          (docstring nil))
    176     (when (stringp (car body))
    177       (setq docstring (car body))
    178       (setq body (cdr body))
    179       ;; Ensure that the docstring doesn't have lines starting with (,
    180       ;; or it breaks indentation.
    181       (setq docstring
    182             (s-replace "\n(" "\n\\(" docstring)))
    183     (if docstring
    184         `(defun ,sym ,args ,docstring ,@body)
    185       `(defun ,sym ,args ,@body))))
    186 
    187 (defun helpful--pretty-print (value)
    188   "Pretty-print VALUE.
    189 
    190 If VALUE is very big, the user may press \\[keyboard-quit] to
    191 gracefully stop the printing. If VALUE is self-referential, the
    192 error will be caught and displayed."
    193   ;; Inspired by `ielm-eval-input'.
    194   (condition-case err
    195       (s-trim-right (pp-to-string value))
    196     (error
    197      (propertize (format "(Display error: %s)" (cadr err))
    198                  'face 'font-lock-comment-face))
    199     (quit
    200      (propertize "(User quit during pretty-printing.)"
    201                  'face 'font-lock-comment-face))))
    202 
    203 (defun helpful--sort-symbols (sym-list)
    204   "Sort symbols in SYM-LIST alphabetically."
    205   (--sort
    206    (string< (symbol-name it) (symbol-name other))
    207    sym-list))
    208 
    209 (defun helpful--button (text type &rest properties)
    210   ;; `make-text-button' mutates our string to add properties. Copy
    211   ;; TEXT to prevent mutating our arguments, and to support 'pure'
    212   ;; strings, which are read-only.
    213   (setq text (substring-no-properties text))
    214   (apply #'make-text-button
    215          text nil
    216          :type type
    217          properties))
    218 
    219 (defun helpful--canonical-symbol (sym callable-p)
    220   "If SYM is an alias, return the underlying symbol.
    221 Return SYM otherwise."
    222   (let ((depth 0))
    223     (if (and (symbolp sym) callable-p)
    224         (progn
    225           ;; Follow the chain of symbols until we find a symbol that
    226           ;; isn't pointing to a symbol.
    227           (while (and (symbolp (symbol-function sym))
    228                       (< depth 10))
    229             (setq sym (symbol-function sym))
    230             (setq depth (1+ depth)))
    231           ;; If this is an alias to a primitive, return the
    232           ;; primitive's symbol.
    233           (when (subrp (symbol-function sym))
    234             (setq sym (intern (subr-name (symbol-function sym))))))
    235       (setq sym (indirect-variable sym))))
    236   sym)
    237 
    238 (defun helpful--aliases (sym callable-p)
    239   "Return all the aliases for SYM."
    240   (let ((canonical (helpful--canonical-symbol sym callable-p))
    241         aliases)
    242     (mapatoms
    243      (lambda (s)
    244        (when (and
    245               ;; Skip variables that aren't bound, so we're faster.
    246               (if callable-p (fboundp s) (boundp s))
    247 
    248               ;; If this symbol is a new alias for our target sym,
    249               ;; add it.
    250               (eq canonical (helpful--canonical-symbol s callable-p))
    251 
    252               ;; Don't include SYM.
    253               (not (eq sym s)))
    254          (push s aliases))))
    255     (helpful--sort-symbols aliases)))
    256 
    257 (defun helpful--obsolete-info (sym callable-p)
    258   (when (symbolp sym)
    259     (get sym (if callable-p 'byte-obsolete-info 'byte-obsolete-variable))))
    260 
    261 (defun helpful--format-alias (sym callable-p)
    262   (let ((obsolete-info (helpful--obsolete-info sym callable-p))
    263         (sym-button (helpful--button
    264                      (symbol-name sym)
    265                      'helpful-describe-exactly-button
    266                      'symbol sym
    267                      'callable-p callable-p)))
    268     (cond
    269      (obsolete-info
    270       (-if-let (version (-last-item obsolete-info))
    271           (format "%s (obsolete since %s)" sym-button version)
    272         (format "%s (obsolete)" sym-button)))
    273      (t
    274       sym-button))))
    275 
    276 (defun helpful--indent-rigidly (s amount)
    277   "Indent string S by adding AMOUNT spaces to each line."
    278   (with-temp-buffer
    279     (insert s)
    280     (indent-rigidly (point-min) (point-max) amount)
    281     (buffer-string)))
    282 
    283 (defun helpful--format-properties (symbol)
    284   "Return a string describing all the properties of SYMBOL."
    285   (let* ((syms-and-vals
    286           (-partition 2 (and (symbolp symbol) (symbol-plist symbol))))
    287          (syms-and-vals
    288           (-sort (-lambda ((sym1 _) (sym2 _))
    289                    (string-lessp (symbol-name sym1) (symbol-name sym2)))
    290                  syms-and-vals))
    291          (lines
    292           (--map
    293            (-let* (((sym val) it)
    294                    (pretty-val
    295                     (helpful--pretty-print val)))
    296              (format "%s\n%s%s"
    297                      (propertize (symbol-name sym)
    298                                  'face 'font-lock-constant-face)
    299                      (helpful--indent-rigidly pretty-val 2)
    300                      (cond
    301                       ;; Also offer to disassemble byte-code
    302                       ;; properties.
    303                       ((byte-code-function-p val)
    304                        (format "\n  %s"
    305                                (helpful--make-disassemble-button val)))
    306                       ((eq sym 'ert--test)
    307                        (format "\n  %s"
    308                                (helpful--make-run-test-button symbol)))
    309                       (t
    310                        ""))))
    311            syms-and-vals)))
    312     (when lines
    313       (s-join "\n" lines))))
    314 
    315 (define-button-type 'helpful-forget-button
    316   'action #'helpful--forget
    317   'symbol nil
    318   'callable-p nil
    319   'follow-link t
    320   'help-echo "Unbind this function")
    321 
    322 ;; TODO: it would be nice to optionally delete the source code too.
    323 (defun helpful--forget (button)
    324   "Unbind the current symbol."
    325   (let* ((sym (button-get button 'symbol))
    326          (callable-p (button-get button 'callable-p))
    327          (kind (helpful--kind-name sym callable-p)))
    328     (when (yes-or-no-p (format "Forget %s %s?" kind sym))
    329       (if callable-p
    330           (fmakunbound sym)
    331         (makunbound sym))
    332       (message "Forgot %s %s." kind sym)
    333       (kill-buffer (current-buffer)))))
    334 
    335 (define-button-type 'helpful-c-source-directory
    336   'action #'helpful--c-source-directory
    337   'follow-link t
    338   'help-echo "Set directory to Emacs C source code")
    339 
    340 (defun helpful--c-source-directory (_button)
    341   "Set `find-function-C-source-directory' so we can show the
    342 source code to primitives."
    343   (let ((emacs-src-dir (read-directory-name "Path to Emacs source code: ")))
    344     ;; Let the user specify the source path with or without src/,
    345     ;; which is a subdirectory in the Emacs tree.
    346     (unless (equal (f-filename emacs-src-dir) "src")
    347       (setq emacs-src-dir (f-join emacs-src-dir "src")))
    348     (setq find-function-C-source-directory emacs-src-dir))
    349   (helpful-update))
    350 
    351 (define-button-type 'helpful-disassemble-button
    352   'action #'helpful--disassemble
    353   'follow-link t
    354   'object nil
    355   'help-echo "Show disassembled bytecode")
    356 
    357 (defun helpful--disassemble (button)
    358   "Disassemble the current symbol."
    359   ;; `disassemble' can handle both symbols (e.g. 'when) and raw
    360   ;; byte-code objects.
    361   (disassemble (button-get button 'object)))
    362 
    363 (define-button-type 'helpful-run-test-button
    364   'action #'helpful--run-test
    365   'follow-link t
    366   'symbol nil
    367   'help-echo "Run ERT test")
    368 
    369 (defun helpful--run-test (button)
    370   "Disassemble the current symbol."
    371   (ert (button-get button 'symbol)))
    372 
    373 (define-button-type 'helpful-edebug-button
    374   'action #'helpful--edebug
    375   'follow-link t
    376   'symbol nil
    377   'help-echo "Toggle edebug (re-evaluates definition)")
    378 
    379 (defun helpful--kbd-macro-p (sym)
    380   "Is SYM a keyboard macro?"
    381   (and (symbolp sym)
    382        (let ((func (symbol-function sym)))
    383          (or (stringp func)
    384              (vectorp func)))))
    385 
    386 (defun helpful--edebug-p (sym)
    387   "Does function SYM have its definition patched by edebug?"
    388   (let ((fn-def (indirect-function sym)))
    389     ;; Edebug replaces function source code with a sexp that has
    390     ;; `edebug-enter', `edebug-after' etc interleaved. This means the
    391     ;; function is interpreted, so `indirect-function' returns a list.
    392     (when (and (consp fn-def) (consp (cdr fn-def)))
    393       (-let [fn-end (-last-item fn-def)]
    394         (and (consp fn-end)
    395              (eq (car fn-end) 'edebug-enter))))))
    396 
    397 (defun helpful--can-edebug-p (sym callable-p buf pos)
    398   "Can we use edebug with SYM?"
    399   (and
    400    ;; SYM must be a function.
    401    callable-p
    402    ;; The function cannot be a primitive, it must be defined in elisp.
    403    (not (helpful--primitive-p sym callable-p))
    404    ;; We need to be able to find its definition, or we can't step
    405    ;; through the source.
    406    buf pos))
    407 
    408 (defun helpful--toggle-edebug (sym)
    409   "Enable edebug when function SYM is called,
    410 or disable if already enabled."
    411   (-let ((should-edebug (not (helpful--edebug-p sym)))
    412          ((buf pos created) (helpful--definition sym t)))
    413     (if (and buf pos)
    414         (progn
    415           (with-current-buffer buf
    416             (save-excursion
    417               (save-restriction
    418                 (widen)
    419                 (goto-char pos)
    420 
    421                 (let* ((edebug-all-forms should-edebug)
    422                        (edebug-all-defs should-edebug)
    423                        (form (edebug-read-top-level-form)))
    424                   ;; Based on `edebug-eval-defun'.
    425                   (eval (eval-sexp-add-defvars form) lexical-binding)))))
    426           ;; If we're enabling edebug, we need the source buffer to
    427           ;; exist. Otherwise, we can clean it up.
    428           (when (and created (not should-edebug))
    429             (kill-buffer buf)))
    430 
    431       (user-error "Could not find source for edebug"))))
    432 
    433 (defun helpful--edebug (button)
    434   "Toggle edebug for the current symbol."
    435   (helpful--toggle-edebug (button-get button 'symbol))
    436   (helpful-update))
    437 
    438 (define-button-type 'helpful-trace-button
    439   'action #'helpful--trace
    440   'follow-link t
    441   'symbol nil
    442   'help-echo "Toggle function tracing")
    443 
    444 (defun helpful--trace (button)
    445   "Toggle tracing for the current symbol."
    446   (let ((sym (button-get button 'symbol)))
    447     (if (trace-is-traced sym)
    448         (untrace-function sym)
    449       (trace-function sym)))
    450   (helpful-update))
    451 
    452 (define-button-type 'helpful-navigate-button
    453   'action #'helpful--navigate
    454   'path nil
    455   'position nil
    456   'follow-link t
    457   'help-echo "Navigate to definition")
    458 
    459 (defun helpful--goto-char-widen (pos)
    460   "Move point to POS in the current buffer.
    461 If narrowing is in effect, widen if POS isn't in the narrowed area."
    462   (when (or (< pos (point-min))
    463             (> pos (point-max)))
    464     (widen))
    465   (goto-char pos))
    466 
    467 (defun helpful--navigate (button)
    468   "Navigate to the path this BUTTON represents."
    469   (find-file (substring-no-properties (button-get button 'path)))
    470   ;; We use `get-text-property' to work around an Emacs 25 bug:
    471   ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=f7c4bad17d83297ee9a1b57552b1944020f23aea
    472   (-when-let (pos (get-text-property button 'position
    473                                      (marker-buffer button)))
    474     (helpful--goto-char-widen pos)))
    475 
    476 (defun helpful--navigate-button (text path &optional pos)
    477   "Return a button that opens PATH and puts point at POS."
    478   (helpful--button
    479    text
    480    'helpful-navigate-button
    481    'path path
    482    'position pos))
    483 
    484 (define-button-type 'helpful-buffer-button
    485   'action #'helpful--switch-to-buffer
    486   'buffer nil
    487   'position nil
    488   'follow-link t
    489   'help-echo "Switch to this buffer")
    490 
    491 (defun helpful--switch-to-buffer (button)
    492   "Navigate to the buffer this BUTTON represents."
    493   (let ((buf (button-get button 'buffer))
    494         (pos (button-get button 'position)))
    495     (switch-to-buffer buf)
    496     (when pos
    497       (helpful--goto-char-widen pos))))
    498 
    499 (defun helpful--buffer-button (buffer &optional pos)
    500   "Return a button that switches to BUFFER and puts point at POS."
    501   (helpful--button
    502    (buffer-name buffer)
    503    'helpful-buffer-button
    504    'buffer buffer
    505    'position pos))
    506 
    507 (define-button-type 'helpful-customize-button
    508   'action #'helpful--customize
    509   'symbol nil
    510   'follow-link t
    511   'help-echo "Open Customize for this symbol")
    512 
    513 (defun helpful--customize (button)
    514   "Open Customize for this symbol."
    515   (customize-variable (button-get button 'symbol)))
    516 
    517 (define-button-type 'helpful-associated-buffer-button
    518   'action #'helpful--associated-buffer
    519   'symbol nil
    520   'prompt-p nil
    521   'follow-link t
    522   'help-echo "Change associated buffer")
    523 
    524 (defun helpful--read-live-buffer (prompt predicate)
    525   "Read a live buffer name, and return the buffer object.
    526 
    527 This is largely equivalent to `read-buffer', but counsel.el
    528 overrides that to include previously opened buffers."
    529   (let* ((names (-map #'buffer-name (buffer-list)))
    530          (default
    531            (cond
    532             ;; If we're already looking at a buffer-local value, start
    533             ;; the prompt from the relevant buffer.
    534             ((and helpful--associated-buffer
    535                   (buffer-live-p helpful--associated-buffer))
    536              (buffer-name helpful--associated-buffer))
    537             ;; If we're looking at the global value, offer the initial
    538             ;; buffer.
    539             ((and helpful--start-buffer
    540                   (buffer-live-p helpful--start-buffer))
    541              (buffer-name helpful--start-buffer))
    542             ;; If we're looking at the global value and have no initial
    543             ;; buffer, choose the first normal buffer.
    544             (t
    545              (--first (and (not (s-starts-with-p " " it))
    546                            (not (s-starts-with-p "*" it)))
    547                       names))
    548             )))
    549     (get-buffer
    550      (completing-read
    551       prompt
    552       names
    553       predicate
    554       t
    555       nil
    556       nil
    557       default))))
    558 
    559 (defun helpful--associated-buffer (button)
    560   "Change the associated buffer, so we can see buffer-local values."
    561   (let ((sym (button-get button 'symbol))
    562         (prompt-p (button-get button 'prompt-p)))
    563     (if prompt-p
    564         (setq helpful--associated-buffer
    565               (helpful--read-live-buffer
    566                "View variable in: "
    567                (lambda (buf-name)
    568                  (local-variable-p sym (get-buffer buf-name)))))
    569       (setq helpful--associated-buffer nil)))
    570   (helpful-update))
    571 
    572 (define-button-type 'helpful-toggle-button
    573   'action #'helpful--toggle
    574   'symbol nil
    575   'buffer nil
    576   'follow-link t
    577   'help-echo "Toggle this symbol between t and nil")
    578 
    579 (defun helpful--toggle (button)
    580   "Toggle the symbol between nil and t."
    581   (let ((sym (button-get button 'symbol))
    582         (buf (button-get button 'buffer)))
    583     (save-current-buffer
    584       ;; If this is a buffer-local variable, ensure we're in the right
    585       ;; buffer.
    586       (when buf
    587         (set-buffer buf))
    588       (set sym (not (symbol-value sym))))
    589     (helpful-update)))
    590 
    591 (define-button-type 'helpful-set-button
    592   'action #'helpful--set
    593   'symbol nil
    594   'buffer nil
    595   'follow-link t
    596   'help-echo "Set the value of this symbol")
    597 
    598 (defun helpful--set (button)
    599   "Set the value of this symbol."
    600   (let* ((sym (button-get button 'symbol))
    601          (buf (button-get button 'buffer))
    602          (sym-value (helpful--sym-value sym buf))
    603          ;; Inspired by `counsel-read-setq-expression'.
    604          (expr
    605           (minibuffer-with-setup-hook
    606               (lambda ()
    607                 (add-function :before-until (local 'eldoc-documentation-function)
    608                               #'elisp-eldoc-documentation-function)
    609                 (run-hooks 'eval-expression-minibuffer-setup-hook)
    610                 (goto-char (minibuffer-prompt-end))
    611                 (forward-char (length (format "(setq %S " sym))))
    612             (read-from-minibuffer
    613              "Eval: "
    614              (format
    615               (if (or (consp sym-value)
    616                       (and (symbolp sym-value)
    617                            (not (null sym-value))
    618                            (not (keywordp sym-value))))
    619                   "(setq %s '%S)"
    620                 "(setq %s %S)")
    621               sym sym-value)
    622              read-expression-map t
    623              'read-expression-history))))
    624     (save-current-buffer
    625       ;; If this is a buffer-local variable, ensure we're in the right
    626       ;; buffer.
    627       (when buf
    628         (set-buffer buf))
    629       (eval-expression expr))
    630     (helpful-update)))
    631 
    632 (define-button-type 'helpful-view-literal-button
    633   'action #'helpful--view-literal
    634   'help-echo "Toggle viewing as a literal")
    635 
    636 (defun helpful--view-literal (_button)
    637   "Set the value of this symbol."
    638   (setq helpful--view-literal
    639         (not helpful--view-literal))
    640   (helpful-update))
    641 
    642 (define-button-type 'helpful-all-references-button
    643   'action #'helpful--all-references
    644   'symbol nil
    645   'callable-p nil
    646   'follow-link t
    647   'help-echo "Find all references to this symbol")
    648 
    649 (defun helpful--all-references (button)
    650   "Find all the references to the symbol that this BUTTON represents."
    651   (let ((sym (button-get button 'symbol))
    652         (callable-p (button-get button 'callable-p)))
    653     (cond
    654      ((not callable-p)
    655       (elisp-refs-variable sym))
    656      ((functionp sym)
    657       (elisp-refs-function sym))
    658      ((macrop sym)
    659       (elisp-refs-macro sym)))))
    660 
    661 (define-button-type 'helpful-callees-button
    662   'action #'helpful--show-callees
    663   'symbol nil
    664   'source nil
    665   'follow-link t
    666   'help-echo "Find the functions called by this function/macro")
    667 
    668 (defun helpful--display-callee-group (callees)
    669   "Insert every entry in CALLEES."
    670   (dolist (sym (helpful--sort-symbols callees))
    671     (insert "  "
    672             (helpful--button
    673              (symbol-name sym)
    674              'helpful-describe-exactly-button
    675              'symbol sym
    676              'callable-p t)
    677             "\n")))
    678 
    679 (defun helpful--show-callees (button)
    680   "Find all the references to the symbol that this BUTTON represents."
    681   (let* ((buf (get-buffer-create "*helpful callees*"))
    682          (sym (button-get button 'symbol))
    683          (raw-source (button-get button 'source))
    684          (source
    685           (if (stringp raw-source)
    686               (read raw-source)
    687             raw-source))
    688          (syms (helpful--callees source))
    689          (primitives (-filter (lambda (sym) (helpful--primitive-p sym t)) syms))
    690          (compounds (-remove (lambda (sym) (helpful--primitive-p sym t)) syms)))
    691 
    692     (pop-to-buffer buf)
    693     (let ((inhibit-read-only t))
    694       (erase-buffer)
    695 
    696       ;; TODO: Macros used, special forms used, global vars used.
    697       (insert (format "Functions called by %s:\n\n" sym))
    698       (helpful--display-callee-group compounds)
    699 
    700       (when primitives
    701         (insert "\n")
    702         (insert (format "Primitives called by %s:\n\n" sym))
    703         (helpful--display-callee-group primitives))
    704 
    705       (goto-char (point-min))
    706 
    707       (helpful-mode))))
    708 
    709 (define-button-type 'helpful-manual-button
    710   'action #'helpful--manual
    711   'symbol nil
    712   'follow-link t
    713   'help-echo "View this symbol in the Emacs manual")
    714 
    715 (defun helpful--manual (button)
    716   "Open the manual for the system that this BUTTON represents."
    717   (let ((sym (button-get button 'symbol)))
    718     (info-lookup 'symbol sym #'emacs-lisp-mode)))
    719 
    720 (define-button-type 'helpful-describe-button
    721   'action #'helpful--describe
    722   'symbol nil
    723   'follow-link t
    724   'help-echo "Describe this symbol")
    725 
    726 (defun helpful--describe (button)
    727   "Describe the symbol that this BUTTON represents."
    728   (let ((sym (button-get button 'symbol)))
    729     (helpful-symbol sym)))
    730 
    731 (define-button-type 'helpful-describe-exactly-button
    732   'action #'helpful--describe-exactly
    733   'symbol nil
    734   'callable-p nil
    735   'follow-link t
    736   'help-echo "Describe this symbol")
    737 
    738 (defun helpful--describe-exactly (button)
    739   "Describe the symbol that this BUTTON represents.
    740 This differs from `helpful--describe' because here we know
    741 whether the symbol represents a variable or a callable."
    742   (let ((sym (button-get button 'symbol))
    743         (callable-p (button-get button 'callable-p)))
    744     (if callable-p
    745         (helpful-callable sym)
    746       (helpful-variable sym))))
    747 
    748 (define-button-type 'helpful-info-button
    749   'action #'helpful--info
    750   'info-node nil
    751   'follow-link t
    752   'help-echo "View this Info node")
    753 
    754 (defun helpful--info (button)
    755   "Describe the symbol that this BUTTON represents."
    756   (info (button-get button 'info-node)))
    757 
    758 (define-button-type 'helpful-shortdoc-button
    759   'action #'helpful--shortdoc
    760   'info-node nil
    761   'follow-link t
    762   'help-echo "View this Shortdoc group")
    763 
    764 (defun helpful--shortdoc (button)
    765   "Describe the symbol that this BUTTON represents."
    766   (shortdoc-display-group (button-get button 'shortdoc-group)
    767                           (button-get button 'symbol)))
    768 
    769 (defun helpful--split-first-line (docstring)
    770   "If the first line is a standalone sentence, ensure we have a
    771 blank line afterwards."
    772   (let* ((lines (s-lines docstring))
    773          (first-line (-first-item lines))
    774          (second-line (when (> (length lines) 1) (nth 1 lines))))
    775     (if (and (s-ends-with-p "." first-line)
    776              (stringp second-line)
    777              (not (equal second-line "")))
    778         (s-join "\n"
    779                 (-cons* first-line "" (cdr lines)))
    780       docstring)))
    781 
    782 (defun helpful--propertize-sym-ref (sym-name before-txt after-txt)
    783   "Given a symbol name from a docstring, convert to a button (if
    784 bound) or else highlight."
    785   (let* ((sym (intern sym-name)))
    786     (cond
    787      ;; Highlight keywords.
    788      ((s-matches-p
    789        (rx ":"
    790            symbol-start
    791            (+? (or (syntax word) (syntax symbol)))
    792            symbol-end)
    793        sym-name)
    794       (propertize sym-name
    795                   'face 'font-lock-builtin-face))
    796      ((and (boundp sym) (s-ends-with-p "variable " before-txt))
    797       (helpful--button
    798        sym-name
    799        'helpful-describe-exactly-button
    800        'symbol sym
    801        'callable-p nil))
    802      ((and (fboundp sym) (or
    803                           (s-starts-with-p " command" after-txt)
    804                           (s-ends-with-p "command " before-txt)
    805                           (s-ends-with-p "function " before-txt)))
    806       (helpful--button
    807        sym-name
    808        'helpful-describe-exactly-button
    809        'symbol sym
    810        'callable-p t))
    811      ;; Only create a link if this is a symbol that is bound as a
    812      ;; variable or callable.
    813      ((or (boundp sym) (fboundp sym))
    814       (helpful--button
    815        sym-name
    816        'helpful-describe-button
    817        'symbol sym))
    818      ;; If this is already a button, don't modify it.
    819      ((get-text-property 0 'button sym-name)
    820       sym-name)
    821      ;; Highlight the quoted string.
    822      (t
    823       (propertize sym-name
    824                   'face 'font-lock-constant-face)))))
    825 
    826 (defun helpful--propertize-info (docstring)
    827   "Convert info references in DOCSTRING to buttons."
    828   (replace-regexp-in-string
    829    ;; Replace all text that looks like a link to an Info page.
    830    (rx (seq (group
    831              bow
    832              (any "Ii")
    833              "nfo"
    834              (one-or-more whitespace))
    835             (group
    836              (or "node" "anchor")
    837              (one-or-more whitespace))
    838             (any "'`‘")
    839             (group
    840              (one-or-more
    841               (not (any "'’"))))
    842             (any "'’")))
    843    (lambda (it)
    844      ;; info-name matches "[Ii]nfo ".
    845      ;; space matches "node " or "anchor ".
    846      ;; info-node has the form "(cl)Loop Facility".
    847      (let ((info-name (match-string 1 it))
    848            (space (match-string 2 it))
    849            (info-node (match-string 3 it)))
    850        ;; If the docstring doesn't specify a manual, assume the Emacs manual.
    851        (save-match-data
    852          (unless (string-match "^([^)]+)" info-node)
    853            (setq info-node (concat "(emacs)" info-node))))
    854        (concat
    855         info-name
    856         space
    857         (helpful--button
    858          info-node
    859          'helpful-info-button
    860          'info-node info-node))))
    861    docstring
    862    t t))
    863 
    864 (defun helpful--keymap-keys (keymap)
    865   "Return all the keys and commands in KEYMAP.
    866 Flattens nested keymaps and follows remapped commands.
    867 
    868 Returns a list of pairs (KEYCODES COMMAND), where KEYCODES is a
    869 vector suitable for `key-description', and COMMAND is a smbol."
    870   (cond
    871    ;; Prefix keys.
    872    ((and
    873      (symbolp keymap)
    874      (fboundp keymap)
    875      ;; Prefix keys use a keymap in the function slot of a symbol.
    876      (keymapp (symbol-function keymap)))
    877     (helpful--keymap-keys (symbol-function keymap)))
    878    ;; Other symbols or compiled functions mean we've reached a leaf,
    879    ;; so this is a command we can call.
    880    ((or
    881      (symbolp keymap)
    882      (functionp keymap)
    883      ;; Strings or vectors mean a keyboard macro.
    884      (stringp keymap)
    885      (vectorp keymap))
    886     `(([] ,keymap)))
    887    ((stringp (car keymap))
    888     (helpful--keymap-keys (cdr keymap)))
    889    ;; Otherwise, recurse on the keys at this level of the keymap.
    890    (t
    891     (let (result)
    892       (dolist (item (cdr keymap))
    893         (cond
    894          ((and (consp item)
    895                (eq (car item) 'menu-bar))
    896           ;; Skip menu bar items.
    897           nil)
    898          ;; Sparse keymaps are lists.
    899          ((consp item)
    900           (-let [(keycode . value) item]
    901             (-each (helpful--keymap-keys value)
    902               (-lambda ((keycodes command))
    903                 (push (list (vconcat (vector keycode) keycodes) command)
    904                       result)))))
    905          ;; Dense keymaps are char-tables.
    906          ((char-table-p item)
    907           (map-char-table
    908            (lambda (keycode value)
    909              (-each (helpful--keymap-keys value)
    910                (-lambda ((keycodes command))
    911                  (push (list (vconcat (vector keycode) keycodes) command)
    912                        result))))
    913            item))))
    914       ;; For every command `new-func' mapped to a command `orig-func', show `new-func' with
    915       ;; the key sequence for `orig-func'.
    916       (setq result
    917             (-map-when
    918              (-lambda ((keycodes _))
    919                (and (> (length keycodes) 1)
    920                     (eq (elt keycodes 0) 'remap)))
    921              (-lambda ((keycodes command))
    922                (list
    923                 (where-is-internal (elt keycodes 1) global-map t)
    924                 command))
    925              result))
    926       ;; Preserve the original order of the keymap.
    927       (nreverse result)))))
    928 
    929 (defun helpful--format-hook (hook-val)
    930   "Given a list value assigned to a hook, format it with links to functions."
    931   (let ((lines
    932          (--map
    933           (if (and (symbolp it) (fboundp it))
    934               (helpful--button
    935                (symbol-name it)
    936                'helpful-describe-exactly-button
    937                'symbol it
    938                'callable-p t)
    939             (helpful--syntax-highlight (helpful--pretty-print it)))
    940           hook-val)))
    941     (format "(%s)"
    942             (s-join "\n " lines))))
    943 
    944 ;; TODO: unlike `substitute-command-keys', this shows keybindings
    945 ;; which are currently shadowed (e.g. a global minor mode map).
    946 (defun helpful--format-keymap (keymap)
    947   "Format KEYMAP."
    948   (let* ((keys-and-commands (helpful--keymap-keys keymap))
    949          ;; Convert keycodes [27 i] to "C-M-i".
    950          (keys (-map #'-first-item keys-and-commands))
    951          ;; Add padding so all our strings are the same length.
    952          (formatted-keys (-map #'key-description keys))
    953          (max-formatted-length (-max (cons 0 (-map #'length formatted-keys))))
    954          (aligned-keys (--map (s-pad-right (1+ max-formatted-length)
    955                                            " " it)
    956                               formatted-keys))
    957          ;; Format commands as buttons.
    958          (commands (-map (-lambda ((_ command)) command)
    959                          keys-and-commands))
    960          (formatted-commands
    961           (--map
    962            (cond
    963             ((symbolp it)
    964              (helpful--button
    965               (symbol-name it)
    966               'helpful-describe-button
    967               'symbol it))
    968             ((or (stringp it) (vectorp it))
    969              "Keyboard Macro")
    970             (t
    971              "#<anonymous-function>"))
    972            commands))
    973          ;; Build lines for display.
    974          (lines
    975           (-map (-lambda ((key . command)) (format "%s %s" key command))
    976                 (-zip-pair aligned-keys formatted-commands))))
    977     ;; The flattened keymap will have normal bindings first, and
    978     ;; inherited bindings last. Sort so that we group by prefix.
    979     (s-join "\n" (-sort #'string< lines))))
    980 
    981 (defun helpful--format-commands (str keymap)
    982   "Replace all the \\[ references in STR with buttons."
    983   (replace-regexp-in-string
    984    ;; Text of the form \\[foo-command]
    985    (rx "\\[" (group (+ (not (in "]")))) "]")
    986    (lambda (it)
    987      (let* ((button-face (if (>= emacs-major-version 28) 'help-key-binding 'button))
    988             (symbol-name (match-string 1 it))
    989             (symbol (intern symbol-name))
    990             (key (where-is-internal symbol keymap t))
    991             (key-description
    992              (if key
    993                  (key-description key)
    994                (format "M-x %s" symbol-name))))
    995        (helpful--button
    996         key-description
    997         'helpful-describe-exactly-button
    998         'symbol symbol
    999         'callable-p t
   1000         'face button-face)))
   1001    str
   1002    t
   1003    t))
   1004 
   1005 (defun helpful--chars-before (pos n)
   1006   "Return up to N chars before POS in the current buffer.
   1007 The string may be shorter than N or empty if out-of-range."
   1008   (buffer-substring
   1009    (max (point-min) (- pos n))
   1010    pos))
   1011 
   1012 (defun helpful--chars-after (pos n)
   1013   "Return up to N chars after POS in the current buffer.
   1014 The string may be shorter than N or empty if out-of-range."
   1015   (buffer-substring
   1016    pos
   1017    (min (point-max) (+ pos n))))
   1018 
   1019 (defun helpful--format-command-keys (docstring)
   1020   "Convert command key references and keymap references
   1021 in DOCSTRING to buttons.
   1022 
   1023 Emacs uses \\= to escape \\[ references, so replace that
   1024 unescaping too."
   1025   ;; Loosely based on `substitute-command-keys', but converts
   1026   ;; references to buttons.
   1027   (let ((keymap nil))
   1028     (with-temp-buffer
   1029       (insert docstring)
   1030       (goto-char (point-min))
   1031       (while (not (eobp))
   1032         (cond
   1033          ((looking-at
   1034            ;; Text of the form "foo"
   1035            (rx "\""))
   1036           ;; For literal strings, escape backslashes so our output
   1037           ;; shows copy-pasteable literals.
   1038           (let* ((start-pos (point))
   1039                  (end-pos (progn (forward-char) (search-forward "\"" nil t)))
   1040                  contents)
   1041             (if end-pos
   1042                 (progn
   1043                   (setq contents (buffer-substring start-pos end-pos))
   1044                   (delete-region start-pos end-pos)
   1045                   (insert (s-replace "\\" "\\\\" contents)))
   1046               (forward-char 1))))
   1047          ((looking-at
   1048            ;; Text of the form \=X
   1049            (rx "\\="))
   1050           ;; Remove the escaping, then step over the escaped char.
   1051           ;; Step over the escaped character.
   1052           (delete-region (point) (+ (point) 2))
   1053           (forward-char 1))
   1054          ((looking-at
   1055            ;; Text of the form `foo'
   1056            (rx "`"))
   1057           (let* ((start-pos (point))
   1058                  (end-pos (search-forward "'" nil t))
   1059                  (contents
   1060                   (when end-pos
   1061                     (buffer-substring (1+ start-pos) (1- end-pos)))))
   1062             (cond
   1063              ((null contents)
   1064               ;; If there's no closing ' to match the opening `, just
   1065               ;; leave it.
   1066               (goto-char (1+ start-pos)))
   1067              ((s-contains-p "`" contents)
   1068               ;; If we have repeated backticks `foo `bar', leave the
   1069               ;; first one.
   1070               (goto-char (1+ start-pos)))
   1071              ((s-contains-p "\\[" contents)
   1072               (delete-region start-pos end-pos)
   1073               (insert (helpful--format-commands contents keymap)))
   1074              ;; Highlight a normal `foo', extracting the surrounding
   1075              ;; text so we can detect e.g. "function `foo'".
   1076              (t
   1077               (let ((before (helpful--chars-before start-pos 10))
   1078                     (after (helpful--chars-after end-pos 10)))
   1079                 (delete-region start-pos end-pos)
   1080                 (insert (helpful--propertize-sym-ref contents before after)))))))
   1081          ((looking-at
   1082            ;; Text of the form \\<foo-keymap>
   1083            (rx "\\<" (group (+ (not (in ">")))) ">"
   1084                (? "\n")))
   1085           (let* ((symbol-with-parens (match-string 0))
   1086                  (symbol-name (match-string 1)))
   1087             ;; Remove the original string.
   1088             (delete-region (point)
   1089                            (+ (point) (length symbol-with-parens)))
   1090             ;; Set the new keymap.
   1091             (setq keymap (symbol-value (intern symbol-name)))))
   1092          ((looking-at
   1093            ;; Text of the form \\{foo-mode-map}
   1094            (rx "\\{" (group (+ (not (in "}")))) "}"))
   1095           (let* ((symbol-with-parens (match-string 0))
   1096                  (symbol-name (match-string 1))
   1097                  (keymap
   1098                   ;; Gracefully handle variables not being defined.
   1099                   (ignore-errors
   1100                     (symbol-value (intern symbol-name)))))
   1101             ;; Remove the original string.
   1102             (delete-region (point)
   1103                            (+ (point) (length symbol-with-parens)))
   1104             (if keymap
   1105                 (insert (helpful--format-keymap keymap))
   1106               (insert (format "Keymap %s is not currently defined."
   1107                               symbol-name)))))
   1108          ((looking-at
   1109            ;; Text of the form \\[foo-command]
   1110            (rx "\\[" (group (+ (not (in "]")))) "]"))
   1111           (let* ((symbol-with-parens (match-string 0)))
   1112             ;; Remove the original string.
   1113             (delete-region (point)
   1114                            (+ (point) (length symbol-with-parens)))
   1115             ;; Add a button.
   1116             (insert (helpful--format-commands symbol-with-parens keymap))))
   1117          ;; Don't modify other characters.
   1118          (t
   1119           (forward-char 1))))
   1120       (buffer-string))))
   1121 
   1122 ;; TODO: fix upstream Emacs bug that means `-map' is not highlighted
   1123 ;; in the docstring for `--map'.
   1124 (defun helpful--format-docstring (docstring)
   1125   "Replace cross-references with links in DOCSTRING."
   1126   (-> docstring
   1127       (helpful--split-first-line)
   1128       (helpful--propertize-info)
   1129       (helpful--propertize-links)
   1130       (helpful--propertize-bare-links)
   1131       (helpful--format-command-keys)
   1132       (s-trim)))
   1133 
   1134 (define-button-type 'helpful-link-button
   1135   'action #'helpful--follow-link
   1136   'follow-link t
   1137   'help-echo "Follow this link")
   1138 
   1139 (defun helpful--propertize-links (docstring)
   1140   "Convert URL links in docstrings to buttons."
   1141   (replace-regexp-in-string
   1142    (rx "URL `" (group (*? any)) "'")
   1143    (lambda (match)
   1144      (let ((url (match-string 1 match)))
   1145        (concat "URL "
   1146                (helpful--button
   1147                 url
   1148                 'helpful-link-button
   1149                 'url url))))
   1150    docstring))
   1151 
   1152 (defun helpful--propertize-bare-links (docstring)
   1153   "Convert URL links in docstrings to buttons."
   1154   (replace-regexp-in-string
   1155    (rx (group (or string-start space "<"))
   1156        (group "http" (? "s") "://" (+? (not (any space))))
   1157        (group (? (any "." ">" ")"))
   1158               (or space string-end ">")))
   1159    (lambda (match)
   1160      (let ((space-before (match-string 1 match))
   1161            (url (match-string 2 match))
   1162            (after (match-string 3 match)))
   1163        (concat
   1164         space-before
   1165         (helpful--button
   1166          url
   1167          'helpful-link-button
   1168          'url url)
   1169         after)))
   1170    docstring))
   1171 
   1172 (defun helpful--follow-link (button)
   1173   "Follow the URL specified by BUTTON."
   1174   (browse-url (button-get button 'url)))
   1175 
   1176 (defconst helpful--highlighting-funcs
   1177   '(ert--activate-font-lock-keywords
   1178     highlight-quoted-mode
   1179     rainbow-delimiters-mode)
   1180   "Highlighting functions that are safe to run in a temporary buffer.
   1181 This is used in `helpful--syntax-highlight' to support extra
   1182 highlighting that the user may have configured in their mode
   1183 hooks.")
   1184 
   1185 ;; TODO: crashes on `backtrace-frame' on a recent checkout.
   1186 
   1187 (defun helpful--syntax-highlight (source &optional mode)
   1188   "Return a propertized version of SOURCE in MODE."
   1189   (unless mode
   1190     (setq mode #'emacs-lisp-mode))
   1191   (if (or
   1192        (< (length source) helpful-max-highlight)
   1193        (eq mode 'emacs-lisp-mode))
   1194       (with-temp-buffer
   1195         (insert source)
   1196 
   1197         ;; Switch to major-mode MODE, but don't run any hooks.
   1198         (delay-mode-hooks (funcall mode))
   1199 
   1200         ;; `delayed-mode-hooks' contains mode hooks like
   1201         ;; `emacs-lisp-mode-hook'. Build a list of functions that are run
   1202         ;; when the mode hooks run.
   1203         (let (hook-funcs)
   1204           (dolist (hook delayed-mode-hooks)
   1205             (let ((funcs (symbol-value hook)))
   1206               (setq hook-funcs (append hook-funcs funcs))))
   1207 
   1208           ;; Filter hooks to those that relate to highlighting, and run them.
   1209           (setq hook-funcs (-intersection hook-funcs helpful--highlighting-funcs))
   1210           (-map #'funcall hook-funcs))
   1211 
   1212         (if (fboundp 'font-lock-ensure)
   1213             (font-lock-ensure)
   1214           (with-no-warnings
   1215             (font-lock-fontify-buffer)))
   1216         (buffer-string))
   1217     ;; SOURCE was too long to highlight in a reasonable amount of
   1218     ;; time.
   1219     (concat
   1220      (propertize
   1221       "// Skipping highlighting due to "
   1222       'face 'font-lock-comment-face)
   1223      (helpful--button
   1224       "helpful-max-highlight"
   1225       'helpful-describe-exactly-button
   1226       'symbol 'helpful-max-highlight
   1227       'callable-p nil)
   1228      (propertize
   1229       ".\n"
   1230       'face 'font-lock-comment-face)
   1231      source)))
   1232 
   1233 (defun helpful--source (sym callable-p buf pos)
   1234   "Return the source code of SYM.
   1235 If the source code cannot be found, return the sexp used."
   1236   (catch 'source
   1237     (unless (symbolp sym)
   1238       (throw 'source sym))
   1239 
   1240     (let ((source nil))
   1241       (when (and buf pos)
   1242         (with-current-buffer buf
   1243           (save-excursion
   1244             (save-restriction
   1245               (goto-char pos)
   1246 
   1247               (if (and (helpful--primitive-p sym callable-p)
   1248                        (not callable-p))
   1249                   ;; For variables defined in .c files, only show the
   1250                   ;; DEFVAR expression rather than the huge containing
   1251                   ;; function.
   1252                   (progn
   1253                     (setq pos (line-beginning-position))
   1254                     (forward-list)
   1255                     (forward-char)
   1256                     (narrow-to-region pos (point)))
   1257                 ;; Narrow to the top-level definition.
   1258                 (let ((parse-sexp-ignore-comments t))
   1259                   (narrow-to-defun t)))
   1260 
   1261               ;; If there was a preceding comment, POS will be
   1262               ;; after that comment. Move the position to include that comment.
   1263               (setq pos (point-min))
   1264 
   1265               (setq source (buffer-substring-no-properties (point-min) (point-max))))))
   1266         (setq source (s-trim-right source))
   1267         (when (and source (buffer-file-name buf))
   1268           (setq source (propertize source
   1269                                    'helpful-path (buffer-file-name buf)
   1270                                    'helpful-pos pos
   1271                                    'helpful-pos-is-start t)))
   1272         (throw 'source source)))
   1273 
   1274     (when callable-p
   1275       ;; Could not find source -- probably defined interactively, or via
   1276       ;; a macro, or file has changed.
   1277       ;; TODO: verify that the source hasn't changed before showing.
   1278       ;; TODO: offer to download C sources for current version.
   1279       (throw 'source (indirect-function sym)))))
   1280 
   1281 (defun helpful--has-shortdoc-p (sym)
   1282   "Return non-nil if shortdoc.el is available and SYM is in a shortdoc group."
   1283   (and (featurep 'shortdoc)
   1284        (shortdoc-function-groups sym)))
   1285 
   1286 (defun helpful--in-manual-p (sym)
   1287   "Return non-nil if SYM is in an Info manual."
   1288   (let ((completions
   1289          (cl-letf (((symbol-function #'message)
   1290                     (lambda (_format-string &rest _args))))
   1291            (info-lookup->completions 'symbol 'emacs-lisp-mode))))
   1292     (-when-let (buf (get-buffer " temp-info-look"))
   1293       (kill-buffer buf))
   1294     (or (assoc sym completions)
   1295         (assoc-string sym completions))))
   1296 
   1297 (defun helpful--version-info (sym)
   1298   "If SYM has version information, format and return it.
   1299 Return nil otherwise."
   1300   (when (symbolp sym)
   1301     (let ((package-version
   1302            (get sym 'custom-package-version))
   1303           (emacs-version
   1304            (get sym 'custom-version)))
   1305       (cond
   1306        (package-version
   1307         (format
   1308          "This variable was added, or its default value changed, in %s version %s."
   1309          (car package-version)
   1310          (cdr package-version)))
   1311        (emacs-version
   1312         (format
   1313          "This variable was added, or its default value changed, in Emacs %s."
   1314          emacs-version))))))
   1315 
   1316 (defun helpful--library-path (library-name)
   1317   "Find the absolute path for the source of LIBRARY-NAME.
   1318 
   1319 LIBRARY-NAME takes the form \"foo.el\" , \"foo.el\" or
   1320 \"src/foo.c\".
   1321 
   1322 If .elc files exist without the corresponding .el, return nil."
   1323   (when (member (f-ext library-name) '("c" "rs"))
   1324     (setq library-name
   1325           (f-expand library-name
   1326                     (f-parent find-function-C-source-directory))))
   1327   (condition-case nil
   1328       (find-library-name library-name)
   1329     (error nil)))
   1330 
   1331 (defun helpful--macroexpand-try (form)
   1332   "Try to fully macroexpand FORM.
   1333 If it fails, attempt to partially macroexpand FORM."
   1334   (catch 'result
   1335     (ignore-errors
   1336       ;; Happy path: we can fully expand the form.
   1337       (throw 'result (macroexpand-all form)))
   1338     (ignore-errors
   1339       ;; Attempt one level of macroexpansion.
   1340       (throw 'result (macroexpand-1 form)))
   1341     ;; Fallback: just return the original form.
   1342     form))
   1343 
   1344 (defun helpful--tree-any-p (pred tree)
   1345   "Walk TREE, applying PRED to every subtree.
   1346 Return t if PRED ever returns t."
   1347   (catch 'found
   1348     (let ((stack (list tree)))
   1349       (while stack
   1350         (let ((next (pop stack)))
   1351           (cond
   1352            ((funcall pred next)
   1353             (throw 'found t))
   1354            ((consp next)
   1355             (push (car next) stack)
   1356             (push (cdr next) stack))))))
   1357     nil))
   1358 
   1359 (defun helpful--find-by-macroexpanding (buf sym callable-p)
   1360   "Search BUF for the definition of SYM by macroexpanding
   1361 interesting forms in BUF."
   1362   (catch 'found
   1363     (with-current-buffer buf
   1364       (save-excursion
   1365         (goto-char (point-min))
   1366         (condition-case nil
   1367             (while t
   1368               (let ((form (read (current-buffer)))
   1369                     (var-def-p
   1370                      (lambda (sexp)
   1371                        (and (eq (car-safe sexp) 'defvar)
   1372                             (eq (car-safe (cdr sexp)) sym))))
   1373                     (fn-def-p
   1374                      (lambda (sexp)
   1375                        ;; `defun' ultimately expands to `defalias'.
   1376                        (and (eq (car-safe sexp) 'defalias)
   1377                             (equal (car-safe (cdr sexp)) `(quote ,sym))))))
   1378                 (setq form (helpful--macroexpand-try form))
   1379 
   1380                 (when (helpful--tree-any-p
   1381                        (if callable-p fn-def-p var-def-p)
   1382                        form)
   1383                   ;; `read' puts point at the end of the form, so go
   1384                   ;; back to the start.
   1385                   (throw 'found (scan-sexps (point) -1)))))
   1386           (end-of-file nil))))))
   1387 
   1388 (defun helpful--open-if-needed (path)
   1389   "Return a list (BUF OPENED) where BUF is a buffer visiting PATH.
   1390 If a buffer already exists, return that. If not, open PATH with
   1391 the `emacs-lisp-mode' syntax table active but skip any hooks."
   1392   (let ((initial-buffers (buffer-list))
   1393         (buf nil)
   1394         (opened nil)
   1395         ;; Skip running hooks that may prompt the user.
   1396         (find-file-hook nil)
   1397         ;; If we end up opening a buffer, don't bother with file
   1398         ;; variables. It prompts the user, and we discard the buffer
   1399         ;; afterwards anyway.
   1400         (enable-local-variables nil))
   1401     ;; Opening large .c files can be slow (e.g. when looking at
   1402     ;; `defalias'), especially if the user has configured mode hooks.
   1403     ;;
   1404     ;; Bind `auto-mode-alist' to nil, so we open the buffer in
   1405     ;; `fundamental-mode' if it isn't already open.
   1406     (let ((auto-mode-alist nil))
   1407       (setq buf (find-file-noselect path)))
   1408 
   1409     (unless (-contains-p initial-buffers buf)
   1410       (setq opened t)
   1411 
   1412       (let ((syntax-table emacs-lisp-mode-syntax-table))
   1413         (when (s-ends-with-p ".c" path)
   1414           (setq syntax-table (make-syntax-table))
   1415           (c-populate-syntax-table syntax-table))
   1416 
   1417         ;; If it's a freshly opened buffer, we need to set the syntax
   1418         ;; table so we can search correctly.
   1419         (with-current-buffer buf
   1420           (set-syntax-table syntax-table))))
   1421 
   1422     (list buf opened)))
   1423 
   1424 (defun helpful--definition (sym callable-p)
   1425   "Return a list (BUF POS OPENED) where SYM is defined.
   1426 
   1427 BUF is the buffer containing the definition. If the user wasn't
   1428 already visiting this buffer, OPENED is t and callers should kill
   1429 the buffer when done.
   1430 
   1431 POS is the position of the start of the definition within the
   1432 buffer."
   1433   (let ((primitive-p (helpful--primitive-p sym callable-p))
   1434         (library-name nil)
   1435         (src-path nil)
   1436         (buf nil)
   1437         (pos nil)
   1438         (opened nil))
   1439     ;; We shouldn't be called on primitive functions if we don't have
   1440     ;; a directory of Emacs C sourcecode.
   1441     (cl-assert
   1442      (or find-function-C-source-directory
   1443          (not primitive-p)))
   1444 
   1445     (when (symbolp sym)
   1446       (if callable-p
   1447           (setq library-name (cdr (find-function-library sym)))
   1448         ;; Based on `find-variable-noselect'.
   1449         (setq library-name
   1450               (or
   1451                (symbol-file sym 'defvar)
   1452                (help-C-file-name sym 'var)))))
   1453 
   1454     (when library-name
   1455       (setq src-path (helpful--library-path library-name)))
   1456 
   1457     (cond
   1458      ((and (not (symbolp sym)) (functionp sym))
   1459       (list nil nil nil))
   1460      ((and callable-p library-name)
   1461       (when src-path
   1462         (-let [(src-buf src-opened) (helpful--open-if-needed src-path)]
   1463           (setq buf src-buf)
   1464           (setq opened src-opened))
   1465 
   1466         ;; Based on `find-function-noselect'.
   1467         (with-current-buffer buf
   1468           ;; `find-function-search-for-symbol' moves point. Prevent
   1469           ;; that.
   1470           (save-excursion
   1471             ;; Narrowing has been fixed upstream:
   1472             ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=abd18254aec76b26e86ae27e91d2c916ec20cc46
   1473             (save-restriction
   1474               (widen)
   1475               (setq pos
   1476                     (cdr (find-function-search-for-symbol sym nil library-name))))))
   1477         ;; If we found the containing buffer, but not the symbol, attempt
   1478         ;; to find it by macroexpanding interesting forms.
   1479         (when (and buf (not pos))
   1480           (setq pos (helpful--find-by-macroexpanding buf sym t)))))
   1481      ;; A function, but no file found.
   1482      (callable-p
   1483       ;; Functions defined interactively may have an edebug property
   1484       ;; that contains the location of the definition.
   1485       (-when-let (edebug-info (get sym 'edebug))
   1486         (-let [marker (if (consp edebug-info)
   1487                           (car edebug-info)
   1488                         edebug-info)]
   1489           (setq buf (marker-buffer marker))
   1490           (setq pos (marker-position marker)))))
   1491      ((and (not callable-p) src-path)
   1492       (-let [(src-buf src-opened) (helpful--open-if-needed src-path)]
   1493         (setq buf src-buf)
   1494         (setq opened src-opened)
   1495 
   1496         (with-current-buffer buf
   1497           ;; `find-function-search-for-symbol' moves point. Prevent
   1498           ;; that.
   1499           (save-excursion
   1500             (condition-case _err
   1501                 (setq pos (cdr (find-variable-noselect sym 'defvar)))
   1502               (search-failed nil)
   1503               ;; If your current Emacs instance doesn't match the source
   1504               ;; code configured in find-function-C-source-directory, we can
   1505               ;; get an error about not finding source. Try
   1506               ;; `default-tab-width' against Emacs trunk.
   1507               (error nil)))))))
   1508 
   1509     (list buf pos opened)))
   1510 
   1511 (defun helpful--reference-positions (sym callable-p buf)
   1512   "Return all the buffer positions of references to SYM in BUF."
   1513   (-let* ((forms-and-bufs
   1514            (elisp-refs--search-1
   1515             (list buf)
   1516             (lambda (buf)
   1517               (elisp-refs--read-and-find
   1518                buf sym
   1519                (if callable-p
   1520                    #'elisp-refs--function-p
   1521                  #'elisp-refs--variable-p)))))
   1522           ;; Since we only searched one buffer, we know that
   1523           ;; forms-and-bufs has only one item.
   1524           (forms-and-buf (-first-item forms-and-bufs))
   1525           ((forms . _buf) forms-and-buf))
   1526     (-map
   1527      (-lambda ((_code start-pos _end-pos)) start-pos)
   1528      forms)))
   1529 
   1530 (defun helpful--all-keymap-syms ()
   1531   "Return all keymaps defined in this Emacs instance."
   1532   (let (keymaps)
   1533     (mapatoms
   1534      (lambda (sym)
   1535        (when (and (boundp sym) (keymapp (symbol-value sym)))
   1536          (push sym keymaps))))
   1537     keymaps))
   1538 
   1539 (defun helpful--key-sequences (command-sym keymap global-keycodes)
   1540   "Return all the key sequences of COMMAND-SYM in KEYMAP."
   1541   (let* ((keycodes
   1542           ;; Look up this command in the keymap, its parent and the
   1543           ;; global map. We need to include the global map to find
   1544           ;; remapped commands.
   1545           (where-is-internal command-sym keymap nil t))
   1546          ;; Look up this command in the parent keymap.
   1547          (parent-keymap (keymap-parent keymap))
   1548          (parent-keycodes
   1549           (when parent-keymap
   1550             (where-is-internal
   1551              command-sym (list parent-keymap) nil t)))
   1552          ;; Look up this command in the global map.
   1553          (global-keycodes
   1554           (unless (eq keymap global-map)
   1555             global-keycodes)))
   1556     (->> keycodes
   1557          ;; Ignore keybindings from the parent or global map.
   1558          (--remove (or (-contains-p global-keycodes it)
   1559                        (-contains-p parent-keycodes it)))
   1560          ;; Convert raw keycode vectors into human-readable strings.
   1561          (-map #'key-description))))
   1562 
   1563 (defun helpful--keymaps-containing (command-sym)
   1564   "Return a list of pairs listing keymap names that contain COMMAND-SYM,
   1565 along with the keybindings in each keymap.
   1566 
   1567 Keymap names are typically variable names, but may also be
   1568 descriptions of values in `minor-mode-map-alist'.
   1569 
   1570 We ignore keybindings that are menu items, and ignore keybindings
   1571 from parent keymaps.
   1572 
   1573 `widget-global-map' is also ignored as it generally contains the
   1574 same bindings as `global-map'."
   1575   (let* ((keymap-syms (helpful--all-keymap-syms))
   1576          (keymap-sym-vals (-map #'symbol-value keymap-syms))
   1577          (global-keycodes (where-is-internal
   1578                            command-sym (list global-map) nil t))
   1579          matching-keymaps)
   1580     ;; Look for this command in all keymaps bound to variables.
   1581     (-map
   1582      (-lambda ((keymap-sym . keymap))
   1583        (let ((key-sequences (helpful--key-sequences command-sym keymap global-keycodes)))
   1584          (when (and key-sequences (not (eq keymap-sym 'widget-global-map)))
   1585            (push (cons (symbol-name keymap-sym) key-sequences)
   1586                  matching-keymaps))))
   1587      (-zip-pair keymap-syms keymap-sym-vals))
   1588 
   1589     ;; Look for this command in keymaps used by minor modes that
   1590     ;; aren't bound to variables.
   1591     (-map
   1592      (-lambda ((minor-mode . keymap))
   1593        ;; Only consider this keymap if we didn't find it bound to a variable.
   1594        (when (and (keymapp keymap)
   1595                   (not (memq keymap keymap-sym-vals)))
   1596          (let ((key-sequences (helpful--key-sequences command-sym keymap global-keycodes)))
   1597            (when key-sequences
   1598              (push (cons (format "minor-mode-map-alist (%s)" minor-mode)
   1599                          key-sequences)
   1600                    matching-keymaps)))))
   1601      ;; TODO: examine `minor-mode-overriding-map-alist' too.
   1602      minor-mode-map-alist)
   1603 
   1604     matching-keymaps))
   1605 
   1606 (defun helpful--merge-alists (l1 l2)
   1607   "Given two alists mapping symbols to lists, return a single
   1608 alist with the lists concatenated."
   1609   (let* ((l1-keys (-map #'-first-item l1))
   1610          (l2-keys (-map #'-first-item l2))
   1611          (l2-extra-keys (-difference l2-keys l1-keys))
   1612          (l2-extra-values
   1613           (--map (assoc it l2) l2-extra-keys))
   1614          (l1-with-values
   1615           (-map (-lambda ((key . values))
   1616                   (cons key (append values
   1617                                     (cdr (assoc key l2)))))
   1618                 l1)))
   1619     (append l1-with-values l2-extra-values)))
   1620 
   1621 (defun helpful--keymaps-containing-aliases (command-sym aliases)
   1622   "Return a list of pairs mapping keymap symbols to the
   1623 keybindings for COMMAND-SYM in each keymap.
   1624 
   1625 Includes keybindings for aliases, unlike
   1626 `helpful--keymaps-containing'."
   1627   (let* ((syms (cons command-sym aliases))
   1628          (syms-keymaps (-map #'helpful--keymaps-containing syms)))
   1629     (-reduce #'helpful--merge-alists syms-keymaps)))
   1630 
   1631 (defun helpful--format-keys (command-sym aliases)
   1632   "Describe all the keys that call COMMAND-SYM."
   1633   (let (mode-lines
   1634         global-lines)
   1635     (--each (helpful--keymaps-containing-aliases command-sym aliases)
   1636       (-let [(map . keys) it]
   1637         (dolist (key keys)
   1638           (push
   1639            (format "%s %s"
   1640                    (propertize map 'face 'font-lock-variable-name-face)
   1641                    (if (>= emacs-major-version 28)
   1642                        (propertize key 'face 'help-key-binding)
   1643                      key))
   1644            (if (eq map 'global-map) global-lines mode-lines)))))
   1645     (setq global-lines (-sort #'string< global-lines))
   1646     (setq mode-lines (-sort #'string< mode-lines))
   1647     (-let [lines (-concat global-lines mode-lines)]
   1648       (if lines
   1649           (s-join "\n" lines)
   1650         "This command is not in any keymaps."))))
   1651 
   1652 (defun helpful--outer-sexp (buf pos)
   1653   "Find position POS in BUF, and return the name of the outer sexp,
   1654 along with its position.
   1655 
   1656 Moves point in BUF."
   1657   (with-current-buffer buf
   1658     (goto-char pos)
   1659     (let* ((ppss (syntax-ppss))
   1660            (outer-sexp-posns (nth 9 ppss)))
   1661       (when outer-sexp-posns
   1662         (goto-char (car outer-sexp-posns))))
   1663     (list (point) (-take 2 (read buf)))))
   1664 
   1665 (defun helpful--count-values (items)
   1666   "Return an alist of the count of each value in ITEMS.
   1667 E.g. (x x y z y) -> ((x . 2) (y . 2) (z . 1))"
   1668   (let (counts)
   1669     (dolist (item items (nreverse counts))
   1670       (-if-let (item-and-count (assoc item counts))
   1671           (setcdr item-and-count (1+ (cdr item-and-count)))
   1672         (push (cons item 1) counts)))))
   1673 
   1674 (defun helpful--without-advice (sym)
   1675   "Given advised function SYM, return the function object
   1676 without the advice. Assumes function has been loaded."
   1677   (advice--cd*r
   1678    (advice--symbol-function sym)))
   1679 
   1680 (defun helpful--advised-p (sym)
   1681   "Does SYM have advice associated with it?"
   1682   (and (symbolp sym)
   1683        (advice--p (advice--symbol-function sym))))
   1684 
   1685 (defun helpful--format-head (head)
   1686   "Given a 'head' (the first two symbols of a sexp) format and
   1687 syntax highlight it."
   1688   (-let* (((def name) head)
   1689           (formatted-name
   1690            (if (and (consp name) (eq (car name) 'quote))
   1691                (format "'%S" (cadr name))
   1692              (format "%S" name)))
   1693           (formatted-def
   1694            (format "(%s %s ...)" def formatted-name))
   1695           )
   1696     (helpful--syntax-highlight formatted-def)))
   1697 
   1698 (defun helpful--format-reference (head longest-head ref-count position path)
   1699   "Return a syntax-highlighted version of HEAD, with a link
   1700 to its source location."
   1701   (let ((formatted-count
   1702          (format "%d reference%s"
   1703                  ref-count (if (> ref-count 1) "s" ""))))
   1704     (propertize
   1705      (format
   1706       "%s %s"
   1707       (s-pad-right longest-head " " (helpful--format-head head))
   1708       (propertize formatted-count 'face 'font-lock-comment-face))
   1709      'helpful-path path
   1710      'helpful-pos position)))
   1711 
   1712 (defun helpful--format-position-heads (position-heads path)
   1713   "Given a list of outer sexps, format them for display.
   1714 POSITION-HEADS takes the form ((123 (defun foo)) (456 (defun bar)))."
   1715   (let ((longest-head
   1716          (->> position-heads
   1717               (-map (-lambda ((_pos head)) (helpful--format-head head)))
   1718               (-map #'length)
   1719               (-max))))
   1720     (->> (helpful--count-values position-heads)
   1721          (-map (-lambda (((pos head) . count))
   1722                  (helpful--format-reference head longest-head count pos path)))
   1723          (s-join "\n"))))
   1724 
   1725 (defun helpful--primitive-p (sym callable-p)
   1726   "Return t if SYM is defined in C."
   1727   (let ((subrp (if (fboundp 'subr-primitive-p)
   1728                    #'subr-primitive-p
   1729                  #'subrp)))
   1730     (cond
   1731      ((and callable-p (helpful--advised-p sym))
   1732       (funcall subrp (helpful--without-advice sym)))
   1733      (callable-p
   1734       (funcall subrp (indirect-function sym)))
   1735      (t
   1736       (let ((filename (find-lisp-object-file-name sym 'defvar)))
   1737         (or (eq filename 'C-source)
   1738             (and (stringp filename)
   1739                  (let ((ext (file-name-extension filename)))
   1740                    (or (equal ext "c")
   1741                        (equal ext "rs"))))))))))
   1742 
   1743 (defun helpful--sym-value (sym buf)
   1744   "Return the value of SYM in BUF."
   1745   (cond
   1746    ;; If we're given a buffer, look up the variable in that buffer.
   1747    (buf
   1748     (with-current-buffer buf
   1749       (symbol-value sym)))
   1750    ;; If we don't have a buffer, and this is a buffer-local variable,
   1751    ;; ensure we return the default value.
   1752    ((local-variable-if-set-p sym)
   1753     (default-value sym))
   1754    ;; Otherwise, just return the value in the current buffer, which is
   1755    ;; the global value.
   1756    (t
   1757     (symbol-value sym))))
   1758 
   1759 (defun helpful--insert-section-break ()
   1760   "Insert section break into helpful buffer."
   1761   (insert "\n\n"))
   1762 
   1763 (defun helpful--insert-implementations ()
   1764   "When `helpful--sym' is a generic method, insert its implementations."
   1765   (let ((func helpful--sym)
   1766         (content))
   1767     (when (fboundp #'cl--generic-describe)
   1768       (with-temp-buffer
   1769         (declare-function cl--generic-describe "cl-generic" (function))
   1770         (cl--generic-describe func)
   1771         (goto-char (point-min))
   1772         (when (re-search-forward "^Implementations:$" nil t)
   1773           (setq content (buffer-substring (point) (point-max)))))
   1774       (when content
   1775         (helpful--insert-section-break)
   1776         (insert (helpful--heading "Implementations") (s-trim content))))))
   1777 
   1778 (defun helpful--calculate-references (sym callable-p source-path)
   1779   "Calculate references for SYM in SOURCE-PATH."
   1780   (when source-path
   1781     (let* ((primitive-p (helpful--primitive-p sym callable-p))
   1782            (buf (elisp-refs--contents-buffer source-path))
   1783            (positions
   1784             (if primitive-p
   1785                 nil
   1786               (helpful--reference-positions
   1787                helpful--sym helpful--callable-p buf)))
   1788            (return-value (--map (helpful--outer-sexp buf it) positions)))
   1789       (kill-buffer buf)
   1790       return-value)))
   1791 
   1792 (defun helpful--make-shortdoc-sentence (sym)
   1793   "Make a line for shortdoc groups of SYM."
   1794   (when (featurep 'shortdoc)
   1795     (-when-let (groups (--map (helpful--button
   1796                                (symbol-name it)
   1797                                'helpful-shortdoc-button
   1798                                'shortdoc-group it)
   1799                               (shortdoc-function-groups sym)))
   1800       (if (= 1 (length groups))
   1801           (format "Other relevant functions are documented in the %s group."
   1802                   (car groups))
   1803         (format "Other relevant functions are documented in the %s groups."
   1804                 (concat (s-join ", " (butlast groups))
   1805                         " and " (car (last groups))))))))
   1806 
   1807 (defun helpful--make-manual-button (sym)
   1808   "Make manual button for SYM."
   1809   (helpful--button
   1810    "View in manual"
   1811    'helpful-manual-button
   1812    'symbol sym))
   1813 
   1814 (defun helpful--make-toggle-button (sym buffer)
   1815   "Make toggle button for SYM in BUFFER."
   1816   (helpful--button
   1817    "Toggle"
   1818    'helpful-toggle-button
   1819    'symbol sym
   1820    'buffer buffer))
   1821 
   1822 (defun helpful--make-set-button (sym buffer)
   1823   "Make set button for SYM in BUFFER."
   1824   (helpful--button
   1825    "Set"
   1826    'helpful-set-button
   1827    'symbol sym
   1828    'buffer buffer))
   1829 
   1830 (defun helpful--make-toggle-literal-button ()
   1831   "Make set button for SYM in BUFFER."
   1832   (helpful--button
   1833    (if helpful--view-literal
   1834        ;; TODO: only offer for strings that have newlines, tabs or
   1835        ;; properties.
   1836        "Pretty view"
   1837      "View as literal")
   1838    'helpful-view-literal-button))
   1839 
   1840 (defun helpful--make-customize-button (sym)
   1841   "Make customize button for SYM."
   1842   (helpful--button
   1843    "Customize"
   1844    'helpful-customize-button
   1845    'symbol sym))
   1846 
   1847 (defun helpful--make-references-button (sym callable-p)
   1848   "Make references button for SYM."
   1849   (helpful--button
   1850    "Find all references"
   1851    'helpful-all-references-button
   1852    'symbol sym
   1853    'callable-p callable-p))
   1854 
   1855 (defun helpful--make-edebug-button (sym)
   1856   "Make edebug button for SYM."
   1857   (helpful--button
   1858    (format "%s edebug"
   1859            (if (helpful--edebug-p sym)
   1860                "Disable" "Enable"))
   1861    'helpful-edebug-button
   1862    'symbol sym))
   1863 
   1864 (defun helpful--make-tracing-button (sym)
   1865   "Make tracing button for SYM."
   1866   (helpful--button
   1867    (format "%s tracing"
   1868            (if (trace-is-traced sym)
   1869                "Disable" "Enable"))
   1870    'helpful-trace-button
   1871    'symbol sym))
   1872 
   1873 (defun helpful--make-disassemble-button (obj)
   1874   "Make disassemble button for OBJ.
   1875 OBJ may be a symbol or a compiled function object."
   1876   (helpful--button
   1877    "Disassemble"
   1878    'helpful-disassemble-button
   1879    'object obj))
   1880 
   1881 (defun helpful--make-run-test-button (sym)
   1882   "Make an ERT test button for SYM."
   1883   (helpful--button
   1884    "Run test"
   1885    'helpful-run-test-button
   1886    'symbol sym))
   1887 
   1888 (defun helpful--make-forget-button (sym callable-p)
   1889   "Make forget button for SYM."
   1890   (helpful--button
   1891    "Forget"
   1892    'helpful-forget-button
   1893    'symbol sym
   1894    'callable-p callable-p))
   1895 
   1896 (defun helpful--make-callees-button (sym source)
   1897   (helpful--button
   1898    (format "Functions used by %s" sym)
   1899    'helpful-callees-button
   1900    'symbol sym
   1901    'source source))
   1902 
   1903 ;; TODO: this only reports if a function is autoloaded because we
   1904 ;; autoloaded it. This ignores newly defined functions that are
   1905 ;; autoloaded. Built-in help has this limitation too, but if we can
   1906 ;; find the source, we should instead see if there's an autoload
   1907 ;; cookie.
   1908 (defun helpful--autoloaded-p (sym buf)
   1909   "Return non-nil if function SYM is autoloaded."
   1910   (-when-let (file-name (buffer-file-name buf))
   1911     (setq file-name (s-chop-suffix ".gz" file-name))
   1912     (condition-case nil
   1913         (help-fns--autoloaded-p sym file-name)
   1914       ; new in Emacs 29.0.50
   1915       ; see https://github.com/Wilfred/helpful/pull/283
   1916       (error (help-fns--autoloaded-p sym)))))
   1917 
   1918 (defun helpful--compiled-p (sym)
   1919   "Return non-nil if function SYM is byte-compiled"
   1920   (and (symbolp sym)
   1921        (byte-code-function-p (symbol-function sym))))
   1922 
   1923 (defun helpful--native-compiled-p (sym)
   1924   "Return non-nil if function SYM is native-compiled"
   1925   (and (symbolp sym)
   1926        (fboundp 'subr-native-elisp-p)
   1927        (subr-native-elisp-p (symbol-function sym))))
   1928 
   1929 (defun helpful--join-and (items)
   1930   "Join a list of strings with commas and \"and\"."
   1931   (cond
   1932    ((= (length items) 0)
   1933     "")
   1934    ((= (length items) 1)
   1935     (car items))
   1936    (t
   1937     (format "%s and %s"
   1938             (s-join ", " (-drop-last 1 items))
   1939             (-last-item items)))))
   1940 
   1941 (defun helpful--summary (sym callable-p buf pos)
   1942   "Return a one sentence summary for SYM."
   1943   (-let* ((primitive-p (helpful--primitive-p sym callable-p))
   1944           (canonical-sym (helpful--canonical-symbol sym callable-p))
   1945           (alias-p (not (eq canonical-sym sym)))
   1946           (alias-button
   1947            (if callable-p
   1948                ;; Show a link to 'defalias' in the manual.
   1949                (helpful--button
   1950                 "function alias"
   1951                 'helpful-manual-button
   1952                 'symbol 'defalias)
   1953              ;; Show a link to the variable aliases section in the
   1954              ;; manual.
   1955              (helpful--button
   1956               "alias"
   1957               'helpful-info-button
   1958               'info-node "(elisp)Variable Aliases")))
   1959           (special-form-button
   1960            (helpful--button
   1961             "special form"
   1962             'helpful-info-button
   1963             'info-node "(elisp)Special Forms"))
   1964           (keyboard-macro-button
   1965            (helpful--button
   1966             "keyboard macro"
   1967             'helpful-info-button
   1968             'info-node "(elisp)Keyboard Macros"))
   1969           (interactive-button
   1970            (helpful--button
   1971             "interactive"
   1972             'helpful-info-button
   1973             'info-node "(elisp)Using Interactive"))
   1974           (autoload-button
   1975            (helpful--button
   1976             "autoloaded"
   1977             'helpful-info-button
   1978             'info-node "(elisp)Autoload"))
   1979           (compiled-button
   1980            (helpful--button
   1981             "byte-compiled"
   1982             'helpful-info-button
   1983             'info-node "(elisp)Byte Compilation"))
   1984           (native-compiled-button
   1985            (helpful--button
   1986             "natively compiled"
   1987             'helpful-describe-button
   1988             'symbol 'native-compile))
   1989           (buffer-local-button
   1990            (helpful--button
   1991             "buffer-local"
   1992             'helpful-info-button
   1993             'info-node "(elisp)Buffer-Local Variables"))
   1994           (autoloaded-p
   1995            (and callable-p buf (helpful--autoloaded-p sym buf)))
   1996           (compiled-p
   1997            (and callable-p (helpful--compiled-p sym)))
   1998           (native-compiled-p
   1999            (and callable-p (helpful--native-compiled-p sym)))
   2000           (buttons
   2001            (list
   2002             (if alias-p alias-button)
   2003             (if (and callable-p autoloaded-p) autoload-button)
   2004             (if (and callable-p (commandp sym)) interactive-button)
   2005             (if compiled-p compiled-button)
   2006             (if native-compiled-p native-compiled-button)
   2007             (if (and (not callable-p) (local-variable-if-set-p sym))
   2008                 buffer-local-button)))
   2009           (description
   2010            (helpful--join-and (-non-nil buttons)))
   2011           (kind
   2012            (cond
   2013             ((special-form-p sym)
   2014              special-form-button)
   2015             (alias-p
   2016              (format "for %s,"
   2017                      (helpful--button
   2018                       (symbol-name canonical-sym)
   2019                       'helpful-describe-exactly-button
   2020                       'symbol canonical-sym
   2021                       'callable-p callable-p)))
   2022             ((not callable-p) "variable")
   2023             ((macrop sym) "macro")
   2024             ((helpful--kbd-macro-p sym) keyboard-macro-button)
   2025             (t "function")))
   2026           (defined
   2027             (cond
   2028              (buf
   2029               (let ((path (buffer-file-name buf)))
   2030                 (if path
   2031                     (format
   2032                      "defined in %s"
   2033                      (helpful--navigate-button
   2034                       (file-name-nondirectory path) path pos))
   2035                   (format "defined in buffer %s"
   2036                           (helpful--buffer-button buf pos)))))
   2037              (primitive-p
   2038               "defined in C source code")
   2039              ((helpful--kbd-macro-p sym) nil)
   2040              (t
   2041               "without a source file"))))
   2042 
   2043     (s-word-wrap
   2044      70
   2045      (format "%s is %s %s %s%s."
   2046              (if (symbolp sym)
   2047                  (helpful--format-symbol sym)
   2048                "This lambda")
   2049              (if (string-match-p
   2050                   (rx bos (or "a" "e" "i" "o" "u"))
   2051                   description)
   2052                  "an"
   2053                "a")
   2054              description
   2055              kind
   2056              (if defined (concat " " defined) "")))))
   2057 
   2058 (defun helpful--callees (form)
   2059   "Given source code FORM, return a list of all the functions called."
   2060   (let* ((expanded-form (macroexpand-all form))
   2061          ;; Find all the functions called after macro expansion.
   2062          (all-fns (helpful--callees-1 expanded-form))
   2063          ;; Only consider the functions that were in the original code
   2064          ;; before macro expansion.
   2065          (form-syms (-filter #'symbolp (-flatten form)))
   2066          (form-fns (--filter (memq it form-syms) all-fns)))
   2067     (-distinct form-fns)))
   2068 
   2069 (defun helpful--callees-1 (form)
   2070   "Return a list of all the functions called in FORM.
   2071 Assumes FORM has been macro expanded. The returned list
   2072 may contain duplicates."
   2073   (cond
   2074    ((not (consp form))
   2075     nil)
   2076    ;; See `(elisp)Special Forms'. For these special forms, we recurse
   2077    ;; just like functions but ignore the car.
   2078    ((memq (car form) '(and catch defconst defvar if interactive
   2079                            or prog1 prog2 progn save-current-buffer
   2080                            save-restriction setq setq-default
   2081                            track-mouse unwind-protect while))
   2082     (-flatten
   2083      (-map #'helpful--callees-1 (cdr form))))
   2084 
   2085    ((eq (car form) 'cond)
   2086     (let* ((clauses (cdr form))
   2087            (clause-fns
   2088             ;; Each clause is a list of forms.
   2089             (--map
   2090              (-map #'helpful--callees-1 it) clauses)))
   2091       (-flatten clause-fns)))
   2092 
   2093    ((eq (car form) 'condition-case)
   2094     (let* ((protected-form (nth 2 form))
   2095            (protected-form-fns (helpful--callees-1 protected-form))
   2096            (handlers (-drop 3 form))
   2097            (handler-bodies (-map #'cdr handlers))
   2098            (handler-fns
   2099             (--map
   2100              (-map #'helpful--callees-1 it) handler-bodies)))
   2101       (append
   2102        protected-form-fns
   2103        (-flatten handler-fns))))
   2104 
   2105    ;; Calling a function with a well known higher order function, for
   2106    ;; example (funcall 'foo 1 2).
   2107    ((and
   2108      (memq (car form) '(funcall apply call-interactively
   2109                                 mapcar mapc mapconcat -map))
   2110      (eq (car-safe (nth 1 form)) 'quote))
   2111     (cons
   2112      (cadr (nth 1 form))
   2113      (-flatten
   2114       (-map #'helpful--callees-1 (cdr form)))))
   2115 
   2116    ((eq (car form) 'function)
   2117     (let ((arg (nth 1 form)))
   2118       (if (symbolp arg)
   2119           ;; #'foo, which is the same as (function foo), is a function
   2120           ;; reference.
   2121           (list arg)
   2122         ;; Handle (function (lambda ...)).
   2123         (helpful--callees-1 arg))))
   2124 
   2125    ((eq (car form) 'lambda)
   2126     ;; Only consider the body, not the param list.
   2127     (-flatten (-map #'helpful--callees-1 (-drop 2 form))))
   2128 
   2129    ((eq (car form) 'closure)
   2130     ;; Same as lambda, but has an additional argument of the
   2131     ;; closed-over variables.
   2132     (-flatten (-map #'helpful--callees-1 (-drop 3 form))))
   2133 
   2134    ((memq (car form) '(let let*))
   2135     ;; Extract function calls used to set the let-bound variables.
   2136     (let* ((var-vals (-second-item form))
   2137            (var-val-callees
   2138             (--map
   2139              (if (consp it)
   2140                  (-map #'helpful--callees-1 it)
   2141                nil)
   2142              var-vals)))
   2143       (append
   2144        (-flatten var-val-callees)
   2145        ;; Function calls in the let body.
   2146        (-map #'helpful--callees-1 (-drop 2 form)))))
   2147 
   2148    ((eq (car form) 'quote)
   2149     nil)
   2150    (t
   2151     (cons
   2152      (car form)
   2153      (-flatten
   2154       (-map #'helpful--callees-1 (cdr form)))))))
   2155 
   2156 (defun helpful--ensure-loaded ()
   2157   "Ensure the symbol associated with the current buffer has been loaded."
   2158   (when (and helpful--callable-p
   2159              (symbolp helpful--sym))
   2160     (let ((fn-obj (symbol-function helpful--sym)))
   2161       (when (autoloadp fn-obj)
   2162         (autoload-do-load fn-obj)))))
   2163 
   2164 (defun helpful--hook-p (symbol value)
   2165   "Does SYMBOL look like a hook?"
   2166   (and
   2167    (or
   2168     (s-ends-with-p "-hook" (symbol-name symbol))
   2169     ;; E.g. `after-change-functions', which can be used with
   2170     ;; `add-hook'.
   2171     (s-ends-with-p "-functions" (symbol-name symbol)))
   2172    (consp value)))
   2173 
   2174 (defun helpful--format-value (sym value)
   2175   "Format VALUE as a string."
   2176   (cond
   2177    (helpful--view-literal
   2178     (helpful--syntax-highlight (helpful--pretty-print value)))
   2179    ;; Allow strings to be viewed with properties rendered in
   2180    ;; Emacs, rather than as a literal.
   2181    ((stringp value)
   2182     value)
   2183    ;; Allow keymaps to be viewed with keybindings shown and
   2184    ;; links to the commands bound.
   2185    ((keymapp value)
   2186     (helpful--format-keymap value))
   2187    ((helpful--hook-p sym value)
   2188     (helpful--format-hook value))
   2189    (t
   2190     (helpful--pretty-print value))))
   2191 
   2192 (defun helpful--original-value (sym)
   2193   "Return the original value for SYM, if any.
   2194 
   2195 If SYM has an original value, return it in a list. Return nil
   2196 otherwise."
   2197   (let* ((orig-val-expr (get sym 'standard-value)))
   2198     (when (consp orig-val-expr)
   2199       (ignore-errors
   2200         (list
   2201          (eval (car orig-val-expr)))))))
   2202 
   2203 (defun helpful--original-value-differs-p (sym)
   2204   "Return t if SYM has an original value, and its current
   2205 value is different."
   2206   (let ((orig-val-list (helpful--original-value sym)))
   2207     (and (consp orig-val-list)
   2208          (not (eq (car orig-val-list)
   2209                   (symbol-value sym))))))
   2210 
   2211 (defun helpful-update ()
   2212   "Update the current *Helpful* buffer to the latest
   2213 state of the current symbol."
   2214   (interactive)
   2215   (cl-assert (not (null helpful--sym)))
   2216   (unless (buffer-live-p helpful--associated-buffer)
   2217     (setq helpful--associated-buffer nil))
   2218   (helpful--ensure-loaded)
   2219   (-let* ((val
   2220            ;; Look at the value before setting `inhibit-read-only', so
   2221            ;; users can see the correct value of that variable.
   2222            (unless helpful--callable-p
   2223              (helpful--sym-value helpful--sym helpful--associated-buffer)))
   2224           (inhibit-read-only t)
   2225           (start-line (line-number-at-pos))
   2226           (start-column (current-column))
   2227           (primitive-p (helpful--primitive-p helpful--sym helpful--callable-p))
   2228           (canonical-sym (helpful--canonical-symbol helpful--sym helpful--callable-p))
   2229           (look-for-src (or (not primitive-p)
   2230                             find-function-C-source-directory))
   2231           ((buf pos opened)
   2232            (if look-for-src
   2233                (helpful--definition helpful--sym helpful--callable-p)
   2234              '(nil nil nil)))
   2235           (source (when look-for-src
   2236                     (helpful--source helpful--sym helpful--callable-p buf pos)))
   2237           (source-path (when buf
   2238                          (buffer-file-name buf)))
   2239           (references (helpful--calculate-references
   2240                        helpful--sym helpful--callable-p
   2241                        source-path))
   2242           (aliases (helpful--aliases helpful--sym helpful--callable-p)))
   2243 
   2244     (erase-buffer)
   2245 
   2246     (insert (helpful--summary helpful--sym helpful--callable-p buf pos))
   2247 
   2248     (when (helpful--obsolete-info helpful--sym helpful--callable-p)
   2249       (insert
   2250        "\n\n"
   2251        (helpful--format-obsolete-info helpful--sym helpful--callable-p)))
   2252 
   2253     (when (and helpful--callable-p
   2254                (not (helpful--kbd-macro-p helpful--sym)))
   2255       (helpful--insert-section-break)
   2256       (insert
   2257        (helpful--heading "Signature")
   2258        (helpful--syntax-highlight (helpful--signature helpful--sym))))
   2259 
   2260     (when (not helpful--callable-p)
   2261       (helpful--insert-section-break)
   2262       (let* ((sym helpful--sym)
   2263              (multiple-views-p
   2264               (or (stringp val)
   2265                   (keymapp val)
   2266                   (helpful--hook-p sym val))))
   2267         (when helpful--first-display
   2268           (if (stringp val)
   2269               ;; For strings, it's more intuitive to display them as
   2270               ;; literals, so "1" and 1 are distinct.
   2271               (setq helpful--view-literal t)
   2272             ;; For everything else, prefer the pretty view if available.
   2273             (setq helpful--view-literal nil)))
   2274         (insert
   2275          (helpful--heading
   2276           (cond
   2277            ;; Buffer-local variable and we're looking at the value in
   2278            ;; a specific buffer.
   2279            ((and
   2280              helpful--associated-buffer
   2281              (local-variable-p sym helpful--associated-buffer))
   2282             (format "Value in %s"
   2283                     (helpful--button
   2284                      (format "#<buffer %s>" (buffer-name helpful--associated-buffer))
   2285                      'helpful-buffer-button
   2286                      'buffer helpful--associated-buffer
   2287                      'position pos)))
   2288            ;; Buffer-local variable but default/global value.
   2289            ((local-variable-if-set-p sym)
   2290             "Global Value")
   2291            ;; This variable is not buffer-local.
   2292            (t "Value")))
   2293          (helpful--format-value sym val)
   2294          "\n\n")
   2295         (when (helpful--original-value-differs-p sym)
   2296           (insert
   2297            (helpful--heading "Original Value")
   2298            (helpful--format-value
   2299             sym
   2300             (car (helpful--original-value sym)))
   2301            "\n\n"))
   2302         (when multiple-views-p
   2303           (insert (helpful--make-toggle-literal-button) " "))
   2304 
   2305         (when (local-variable-if-set-p sym)
   2306           (insert
   2307            (helpful--button
   2308             "Buffer values"
   2309             'helpful-associated-buffer-button
   2310             'symbol sym
   2311             'prompt-p t)
   2312            " "
   2313            (helpful--button
   2314             "Global value"
   2315             'helpful-associated-buffer-button
   2316             'symbol sym
   2317             'prompt-p nil)
   2318            " "))
   2319         (when (memq (helpful--sym-value helpful--sym helpful--associated-buffer) '(nil t))
   2320           (insert (helpful--make-toggle-button helpful--sym helpful--associated-buffer) " "))
   2321         (insert (helpful--make-set-button helpful--sym helpful--associated-buffer))
   2322         (when (custom-variable-p helpful--sym)
   2323           (insert " " (helpful--make-customize-button helpful--sym)))))
   2324 
   2325     (let ((docstring (helpful--docstring helpful--sym helpful--callable-p))
   2326           (version-info (unless helpful--callable-p
   2327                           (helpful--version-info helpful--sym))))
   2328       (when (or docstring version-info)
   2329         (helpful--insert-section-break)
   2330         (insert
   2331          (helpful--heading "Documentation"))
   2332         (when docstring
   2333           (insert (helpful--format-docstring docstring)))
   2334         (when version-info
   2335           (insert "\n\n" (s-word-wrap 70 version-info)))
   2336         (when (and (symbolp helpful--sym)
   2337                    helpful--callable-p
   2338                    (helpful--has-shortdoc-p helpful--sym))
   2339           (insert "\n\n")
   2340           (insert (helpful--make-shortdoc-sentence helpful--sym)))
   2341         (when (and (symbolp helpful--sym) (helpful--in-manual-p helpful--sym))
   2342           (insert "\n\n")
   2343           (insert (helpful--make-manual-button helpful--sym)))))
   2344 
   2345     ;; Show keybindings.
   2346     ;; TODO: allow users to conveniently add and remove keybindings.
   2347     (when (commandp helpful--sym)
   2348       (helpful--insert-section-break)
   2349       (insert
   2350        (helpful--heading "Key Bindings")
   2351        (helpful--format-keys helpful--sym aliases)))
   2352 
   2353     (helpful--insert-section-break)
   2354 
   2355     (insert
   2356      (helpful--heading "References")
   2357      (let ((src-button
   2358             (when source-path
   2359               (helpful--navigate-button
   2360                (file-name-nondirectory source-path)
   2361                source-path
   2362                (or pos
   2363                    0)))))
   2364        (cond
   2365         ((and source-path references)
   2366          (format "References in %s:\n%s"
   2367                  src-button
   2368                  (helpful--format-position-heads references source-path)))
   2369         ((and source-path primitive-p)
   2370          (format
   2371           "Finding references in a .%s file is not supported."
   2372           (f-ext source-path)))
   2373         (source-path
   2374          (format "%s is unused in %s."
   2375                  helpful--sym
   2376                  src-button))
   2377         ((and primitive-p (null find-function-C-source-directory))
   2378          "C code is not yet loaded.")
   2379         (t
   2380          "Could not find source file.")))
   2381      "\n\n"
   2382      (helpful--make-references-button helpful--sym helpful--callable-p))
   2383 
   2384     (when (and
   2385            helpful--callable-p
   2386            (symbolp helpful--sym)
   2387            source
   2388            (not primitive-p))
   2389       (insert
   2390        " "
   2391        (helpful--make-callees-button helpful--sym source)))
   2392 
   2393     (when (helpful--advised-p helpful--sym)
   2394       (helpful--insert-section-break)
   2395       (insert
   2396        (helpful--heading "Advice")
   2397        (format "This %s is advised."
   2398                (if (macrop helpful--sym) "macro" "function"))))
   2399 
   2400     (let ((can-edebug
   2401            (helpful--can-edebug-p helpful--sym helpful--callable-p buf pos))
   2402           (can-trace
   2403            (and (symbolp helpful--sym)
   2404                 helpful--callable-p
   2405                 ;; Tracing uses advice, and you can't apply advice to
   2406                 ;; primitive functions that are replaced with special
   2407                 ;; opcodes. For example, `narrow-to-region'.
   2408                 (not (plist-get (symbol-plist helpful--sym) 'byte-opcode))))
   2409           (can-disassemble
   2410            (and helpful--callable-p (not primitive-p)))
   2411           (can-forget
   2412            (and (not (special-form-p helpful--sym))
   2413                 (not primitive-p))))
   2414       (when (or can-edebug can-trace can-disassemble can-forget)
   2415         (helpful--insert-section-break)
   2416         (insert (helpful--heading "Debugging")))
   2417       (when can-edebug
   2418         (insert
   2419          (helpful--make-edebug-button helpful--sym)))
   2420       (when can-trace
   2421         (when can-edebug
   2422           (insert " "))
   2423         (insert
   2424          (helpful--make-tracing-button helpful--sym)))
   2425 
   2426       (when (and
   2427              (or can-edebug can-trace)
   2428              (or can-disassemble can-forget))
   2429         (insert "\n"))
   2430 
   2431       (when can-disassemble
   2432         (insert (helpful--make-disassemble-button helpful--sym)))
   2433 
   2434       (when can-forget
   2435         (when can-disassemble
   2436           (insert " "))
   2437         (insert (helpful--make-forget-button helpful--sym helpful--callable-p))))
   2438 
   2439     (when aliases
   2440       (helpful--insert-section-break)
   2441       (insert
   2442        (helpful--heading "Aliases")
   2443        (s-join "\n" (--map (helpful--format-alias it helpful--callable-p)
   2444                            aliases))))
   2445 
   2446     (when helpful--callable-p
   2447       (helpful--insert-implementations))
   2448 
   2449     (helpful--insert-section-break)
   2450 
   2451     (when (or source-path primitive-p)
   2452       (insert
   2453        (helpful--heading
   2454         (if (eq helpful--sym canonical-sym)
   2455             "Source Code"
   2456           "Alias Source Code"))
   2457        (cond
   2458         (source-path
   2459          (concat
   2460           (propertize (format "%s Defined in " (if primitive-p "//" ";;"))
   2461                       'face 'font-lock-comment-face)
   2462           (helpful--navigate-button
   2463            (f-abbrev source-path)
   2464            source-path
   2465            pos)
   2466           "\n"))
   2467         (primitive-p
   2468          (concat
   2469           (propertize
   2470            "C code is not yet loaded."
   2471            'face 'font-lock-comment-face)
   2472           "\n\n"
   2473           (helpful--button
   2474            "Set C source directory"
   2475            'helpful-c-source-directory))))))
   2476     (when source
   2477       (insert
   2478        (cond
   2479         ((stringp source)
   2480          (let ((mode (when primitive-p
   2481                        (pcase (file-name-extension source-path)
   2482                          ("c" 'c-mode)
   2483                          ("rs" (when (fboundp 'rust-mode) 'rust-mode))))))
   2484            (helpful--syntax-highlight source mode)))
   2485         ((and (consp source) (eq (car source) 'closure))
   2486          (helpful--syntax-highlight
   2487           (concat ";; Closure converted to defun by helpful.\n"
   2488                   (helpful--pretty-print
   2489                    (helpful--format-closure helpful--sym source)))))
   2490         (t
   2491          (helpful--syntax-highlight
   2492           (concat
   2493            (if (eq helpful--sym canonical-sym)
   2494                ";; Could not find source code, showing raw function object.\n"
   2495              ";; Could not find alias source code, showing raw function object.\n")
   2496            (helpful--pretty-print source)))))))
   2497 
   2498     (helpful--insert-section-break)
   2499 
   2500     (-when-let (formatted-props (helpful--format-properties helpful--sym))
   2501       (insert
   2502        (helpful--heading "Symbol Properties")
   2503        formatted-props))
   2504 
   2505     (goto-char (point-min))
   2506     (forward-line (1- start-line))
   2507     (forward-char start-column)
   2508     (setq helpful--first-display nil)
   2509 
   2510     (when opened
   2511       (kill-buffer buf))))
   2512 
   2513 ;; TODO: this isn't sufficient for `edebug-eval-defun'.
   2514 (defun helpful--skip-advice (docstring)
   2515   "Remove mentions of advice from DOCSTRING."
   2516   (let* ((lines (s-lines docstring))
   2517          (relevant-lines
   2518           (--drop-while
   2519            (or (s-starts-with-p ":around advice:" it)
   2520                (s-starts-with-p "This function has :around advice:" it))
   2521            lines)))
   2522     (s-trim (s-join "\n" relevant-lines))))
   2523 
   2524 (defun helpful--format-argument (arg)
   2525   "Format ARG (a symbol) according to Emacs help conventions."
   2526   (let ((arg-str (symbol-name arg)))
   2527     (if (s-starts-with-p "&" arg-str)
   2528         arg-str
   2529       (s-upcase arg-str))))
   2530 
   2531 (defun helpful--format-symbol (sym)
   2532   "Format symbol as a string, escaping as necessary."
   2533   ;; Arguably this is an Emacs bug. We should be able to use
   2534   ;; (format "%S" sym)
   2535   ;; but that converts foo? to "foo\\?". You can see this in other
   2536   ;; parts of the Emacs UI, such as ERT.
   2537   (s-replace " " "\\ " (format "%s" sym)))
   2538 
   2539 ;; TODO: this is broken for -any?.
   2540 (defun helpful--signature (sym)
   2541   "Get the signature for function SYM, as a string.
   2542 For example, \"(some-func FOO &optional BAR)\"."
   2543   (let (docstring-sig
   2544         source-sig
   2545         (advertised-args
   2546          (when (symbolp sym)
   2547            (gethash (symbol-function sym) advertised-signature-table))))
   2548     ;; Get the usage from the function definition.
   2549     (let* ((function-args
   2550             (cond
   2551              ((symbolp sym)
   2552               (help-function-arglist sym))
   2553              ((byte-code-function-p sym)
   2554               ;; argdesc can be a list of arguments or an integer
   2555               ;; encoding the min/max number of arguments. See
   2556               ;; Byte-Code Function Objects in the elisp manual.
   2557               (let ((argdesc (aref sym 0)))
   2558                 (if (consp argdesc)
   2559                     argdesc
   2560                   ;; TODO: properly handle argdesc values.
   2561                   nil)))
   2562              (t
   2563               ;; Interpreted function (lambda ...)
   2564               (cadr sym))))
   2565            (formatted-args
   2566             (cond
   2567              (advertised-args
   2568               (-map #'helpful--format-argument advertised-args))
   2569              ((listp function-args)
   2570               (-map #'helpful--format-argument function-args))
   2571              (t
   2572               (list function-args)))))
   2573       (setq source-sig
   2574             (cond
   2575              ;; If it's a function object, just show the arguments.
   2576              ((not (symbolp sym))
   2577               (format "(%s)"
   2578                       (s-join " " formatted-args)))
   2579              ;; If it has multiple arguments, join them with spaces.
   2580              (formatted-args
   2581               (format "(%s %s)"
   2582                       (helpful--format-symbol sym)
   2583                       (s-join " " formatted-args)))
   2584              ;; Otherwise, this function takes no arguments when called.
   2585              (t
   2586               (format "(%s)" (helpful--format-symbol sym))))))
   2587 
   2588     ;; If the docstring ends with (fn FOO BAR), extract that.
   2589     (-when-let (docstring (documentation sym))
   2590       (-when-let (docstring-with-usage (help-split-fundoc docstring sym))
   2591         (setq docstring-sig (car docstring-with-usage))))
   2592 
   2593     (cond
   2594      ;; Advertised signature always wins.
   2595      (advertised-args
   2596       source-sig)
   2597      ;; If that's not set, use the usage specification in the
   2598      ;; docstring, if present.
   2599      (docstring-sig
   2600       (replace-regexp-in-string "\\\\=\\(['\\`‘’]\\)" "\\1" docstring-sig t))
   2601      (t
   2602       ;; Otherwise, just use the signature from the source code.
   2603       source-sig))))
   2604 
   2605 (defun helpful--format-obsolete-info (sym callable-p)
   2606   (-let [(use _ date) (helpful--obsolete-info sym callable-p)]
   2607     (helpful--format-docstring
   2608      (s-word-wrap
   2609       70
   2610       (format "This %s is obsolete%s%s"
   2611               (helpful--kind-name sym callable-p)
   2612               (if date (format " since %s" date)
   2613                 "")
   2614               (cond ((stringp use) (concat "; " use))
   2615                     (use (format "; use `%s' instead." use))
   2616                     (t ".")))))))
   2617 
   2618 (defun helpful--docstring (sym callable-p)
   2619   "Get the docstring for SYM.
   2620 Note that this returns the raw docstring, including \\=\\=
   2621 escapes that are used by `substitute-command-keys'."
   2622   (let ((text-quoting-style 'grave)
   2623         docstring)
   2624     (if callable-p
   2625         (progn
   2626           (setq docstring (documentation sym t))
   2627           (-when-let (docstring-with-usage (help-split-fundoc docstring sym))
   2628             (setq docstring (cdr docstring-with-usage))
   2629             (when docstring
   2630               ;; Advice mutates the docstring, see
   2631               ;; `advice--make-docstring'. Undo that.
   2632               ;; TODO: Only do this if the function is advised.
   2633               (setq docstring (helpful--skip-advice docstring)))))
   2634       (setq docstring
   2635             (documentation-property sym 'variable-documentation t)))
   2636     docstring))
   2637 
   2638 (defun helpful--read-symbol (prompt default-val predicate)
   2639   "Read a symbol from the minibuffer, with completion.
   2640 Returns the symbol."
   2641   (when (and default-val
   2642              (not (funcall predicate default-val)))
   2643     (setq default-val nil))
   2644   (when default-val
   2645     ;; `completing-read' expects a string.
   2646     (setq default-val (symbol-name default-val))
   2647 
   2648     ;; TODO: Only modify the prompt when we don't have ido/ivy/helm,
   2649     ;; because the default is obvious for them.
   2650     (setq prompt
   2651           (replace-regexp-in-string
   2652            (rx ": " eos)
   2653            (format " (default: %s): " default-val)
   2654            prompt)))
   2655   (intern (completing-read prompt obarray
   2656                            predicate t nil nil
   2657                            default-val)))
   2658 
   2659 (defun helpful--update-and-switch-buffer (symbol callable-p)
   2660   "Update and switch to help buffer for SYMBOL."
   2661   (let ((buf (helpful--buffer symbol callable-p)))
   2662     (with-current-buffer buf
   2663       (helpful-update))
   2664     (funcall helpful-switch-buffer-function buf)))
   2665 
   2666 ;;;###autoload
   2667 (defun helpful-function (symbol)
   2668   "Show help for function named SYMBOL.
   2669 
   2670 See also `helpful-macro', `helpful-command' and `helpful-callable'."
   2671   (interactive
   2672    (list (helpful--read-symbol
   2673           "Function: "
   2674           (helpful--callable-at-point)
   2675           #'functionp)))
   2676   (helpful--update-and-switch-buffer symbol t))
   2677 
   2678 ;;;###autoload
   2679 (defun helpful-command (symbol)
   2680   "Show help for interactive function named SYMBOL.
   2681 
   2682 See also `helpful-function'."
   2683   (interactive
   2684    (list (helpful--read-symbol
   2685           "Command: "
   2686           (helpful--callable-at-point)
   2687           #'commandp)))
   2688   (helpful--update-and-switch-buffer symbol t))
   2689 
   2690 ;;;###autoload
   2691 (defun helpful-key (key-sequence)
   2692   "Show help for interactive command bound to KEY-SEQUENCE."
   2693   (interactive
   2694    (list (read-key-sequence "Press key: ")))
   2695   (let ((sym (key-binding key-sequence)))
   2696     (cond
   2697      ((null sym)
   2698       (user-error "No command is bound to %s"
   2699                   (key-description key-sequence)))
   2700      ((commandp sym)
   2701       (helpful--update-and-switch-buffer sym t))
   2702      (t
   2703       (user-error "%s is bound to %s which is not a command"
   2704                   (key-description key-sequence)
   2705                   sym)))))
   2706 
   2707 ;;;###autoload
   2708 (defun helpful-macro (symbol)
   2709   "Show help for macro named SYMBOL."
   2710   (interactive
   2711    (list (helpful--read-symbol
   2712           "Macro: "
   2713           (helpful--callable-at-point)
   2714           #'macrop)))
   2715   (helpful--update-and-switch-buffer symbol t))
   2716 
   2717 ;;;###autoload
   2718 (defun helpful-callable (symbol)
   2719   "Show help for function, macro or special form named SYMBOL.
   2720 
   2721 See also `helpful-macro', `helpful-function' and `helpful-command'."
   2722   (interactive
   2723    (list (helpful--read-symbol
   2724           "Callable: "
   2725           (helpful--callable-at-point)
   2726           #'fboundp)))
   2727   (helpful--update-and-switch-buffer symbol t))
   2728 
   2729 (defun helpful--variable-p (symbol)
   2730   "Return non-nil if SYMBOL is a variable."
   2731   (or (get symbol 'variable-documentation)
   2732       (and (boundp symbol)
   2733            (not (keywordp symbol))
   2734            (not (eq symbol nil))
   2735            (not (eq symbol t)))))
   2736 
   2737 (defun helpful--bound-p (symbol)
   2738   "Return non-nil if SYMBOL is a variable or callable.
   2739 
   2740 This differs from `boundp' because we do not consider nil, t
   2741 or :foo."
   2742   (or (fboundp symbol)
   2743       (helpful--variable-p symbol)))
   2744 
   2745 (defun helpful--bookmark-jump (bookmark)
   2746   "Create and switch to helpful bookmark BOOKMARK."
   2747   (let ((callable-p (bookmark-prop-get bookmark 'callable-p))
   2748         (sym (bookmark-prop-get bookmark 'sym))
   2749         (position (bookmark-prop-get bookmark 'position)))
   2750     (if callable-p
   2751         (helpful-callable sym)
   2752       (helpful-variable sym))
   2753     (goto-char position)))
   2754 
   2755 (defun helpful--bookmark-make-record ()
   2756   "Create a bookmark record for helpful buffers.
   2757 
   2758 See docs of `bookmark-make-record-function'."
   2759   `((sym . ,helpful--sym)
   2760     (callable-p . ,helpful--callable-p)
   2761     (position    . ,(point))
   2762     (handler     . helpful--bookmark-jump)))
   2763 
   2764 (defun helpful--convert-c-name (symbol var)
   2765   "Convert SYMBOL from a C name to an Elisp name.
   2766 E.g. convert `Fmake_string' to `make-string' or
   2767 `Vgc_cons_percentage' to `gc-cons-percentage'. Interpret
   2768 SYMBOL as variable name if VAR, else a function name. Return
   2769 nil if SYMBOL doesn't begin with \"F\" or \"V\"."
   2770   (let ((string (symbol-name symbol))
   2771         (prefix (if var "V" "F")))
   2772     (when (s-starts-with-p prefix string)
   2773       (intern
   2774        (s-chop-prefix
   2775         prefix
   2776         (s-replace "_" "-" string))))))
   2777 
   2778 ;;;###autoload
   2779 (defun helpful-symbol (symbol)
   2780   "Show help for SYMBOL, a variable, function or macro.
   2781 
   2782 See also `helpful-callable' and `helpful-variable'."
   2783   (interactive
   2784    (list (helpful--read-symbol
   2785           "Symbol: "
   2786           (helpful--symbol-at-point)
   2787           #'helpful--bound-p)))
   2788   (let ((c-var-sym (helpful--convert-c-name symbol t))
   2789         (c-fn-sym (helpful--convert-c-name symbol nil)))
   2790     (cond
   2791      ((and (boundp symbol) (fboundp symbol))
   2792       (if (y-or-n-p
   2793            (format "%s is a both a variable and a callable, show variable?"
   2794                    symbol))
   2795           (helpful-variable symbol)
   2796         (helpful-callable symbol)))
   2797      ((fboundp symbol)
   2798       (helpful-callable symbol))
   2799      ((boundp symbol)
   2800       (helpful-variable symbol))
   2801      ((and c-fn-sym (fboundp c-fn-sym))
   2802       (helpful-callable c-fn-sym))
   2803      ((and c-var-sym (boundp c-var-sym))
   2804       (helpful-variable c-var-sym))
   2805      (t
   2806       (user-error "Not bound: %S" symbol)))))
   2807 
   2808 ;;;###autoload
   2809 (defun helpful-variable (symbol)
   2810   "Show help for variable named SYMBOL."
   2811   (interactive
   2812    (list (helpful--read-symbol
   2813           "Variable: "
   2814           (helpful--variable-at-point)
   2815           #'helpful--variable-p)))
   2816   (helpful--update-and-switch-buffer symbol nil))
   2817 
   2818 (defun helpful--variable-at-point-exactly ()
   2819   "Return the symbol at point, if it's a bound variable."
   2820   (let ((var (variable-at-point)))
   2821     ;; `variable-at-point' uses 0 rather than nil to signify no symbol
   2822     ;; at point (presumably because 'nil is a symbol).
   2823     (unless (symbolp var)
   2824       (setq var nil))
   2825     (when (helpful--variable-p var)
   2826       var)))
   2827 
   2828 (defun helpful--variable-defined-at-point ()
   2829   "Return the variable defined in the form enclosing point."
   2830   ;; TODO: do the same thing if point is just before a top-level form.
   2831   (save-excursion
   2832     (save-restriction
   2833       (widen)
   2834       (let* ((ppss (syntax-ppss))
   2835              (sexp-start (nth 1 ppss))
   2836              sexp)
   2837         (when sexp-start
   2838           (goto-char sexp-start)
   2839           (setq sexp (condition-case nil
   2840                          (read (current-buffer))
   2841                        (error nil)))
   2842           (when (memq (car-safe sexp)
   2843                       (list 'defvar 'defvar-local 'defcustom 'defconst))
   2844             (nth 1 sexp)))))))
   2845 
   2846 (defun helpful--variable-at-point ()
   2847   "Return the variable exactly under point, or defined at point."
   2848   (let ((var (helpful--variable-at-point-exactly)))
   2849     (if var
   2850         var
   2851       (let ((var (helpful--variable-defined-at-point)))
   2852         (when (helpful--variable-p var)
   2853           var)))))
   2854 
   2855 (defun helpful--callable-at-point ()
   2856   (let ((sym (symbol-at-point))
   2857         (enclosing-sym (function-called-at-point)))
   2858     (if (fboundp sym)
   2859         sym
   2860       enclosing-sym)))
   2861 
   2862 (defun helpful--symbol-at-point-exactly ()
   2863   "Return the symbol at point, if it's bound."
   2864   (let ((sym (symbol-at-point)))
   2865     (when (helpful--bound-p sym)
   2866       sym)))
   2867 
   2868 (defun helpful--symbol-at-point ()
   2869   "Find the most relevant symbol at or around point.
   2870 Returns nil if nothing found."
   2871   (or
   2872    (helpful--symbol-at-point-exactly)
   2873    (helpful--callable-at-point)
   2874    (helpful--variable-at-point)))
   2875 
   2876 ;;;###autoload
   2877 (defun helpful-at-point ()
   2878   "Show help for the symbol at point."
   2879   (interactive)
   2880   (-if-let (symbol (helpful--symbol-at-point))
   2881       (helpful-symbol symbol)
   2882     (user-error "There is no symbol at point.")))
   2883 
   2884 (defun helpful--imenu-index ()
   2885   "Return a list of headings in the current buffer, suitable for
   2886 imenu."
   2887   (let (headings)
   2888     (goto-char (point-min))
   2889     (while (not (eobp))
   2890       (when (eq (get-text-property (point) 'face)
   2891                 'helpful-heading)
   2892         (push
   2893          (cons
   2894           (buffer-substring-no-properties
   2895            (line-beginning-position) (line-end-position))
   2896           (line-beginning-position))
   2897          headings))
   2898       (forward-line))
   2899     (nreverse headings)))
   2900 
   2901 (defun helpful--flash-region (start end)
   2902   "Temporarily highlight region from START to END."
   2903   (let ((overlay (make-overlay start end)))
   2904     (overlay-put overlay 'face 'highlight)
   2905     (run-with-timer 1.5 nil 'delete-overlay overlay)))
   2906 
   2907 (defun helpful-visit-reference ()
   2908   "Go to the reference at point."
   2909   (interactive)
   2910   (let* ((sym helpful--sym)
   2911          (path (get-text-property (point) 'helpful-path))
   2912          (pos (get-text-property (point) 'helpful-pos))
   2913          (pos-is-start (get-text-property (point) 'helpful-pos-is-start)))
   2914     (when (and path pos)
   2915       ;; If we're looking at a source excerpt, calculate the offset of
   2916       ;; point, so we don't just go the start of the excerpt.
   2917       (when pos-is-start
   2918         (save-excursion
   2919           (let ((offset 0))
   2920             (while (and
   2921                     (get-text-property (point) 'helpful-pos)
   2922                     (not (eobp)))
   2923               (backward-char 1)
   2924               (setq offset (1+ offset)))
   2925             ;; On the last iteration we moved outside the source
   2926             ;; excerpt, so we overcounted by one character.
   2927             (setq offset (1- offset))
   2928 
   2929             ;; Set POS so we go to exactly the place in the source
   2930             ;; code where point was in the helpful excerpt.
   2931             (setq pos (+ pos offset)))))
   2932 
   2933       (find-file path)
   2934       (helpful--goto-char-widen pos)
   2935       (recenter 0)
   2936       (save-excursion
   2937         (let ((defun-end (scan-sexps (point) 1)))
   2938           (while (re-search-forward
   2939                   (rx-to-string `(seq symbol-start ,(symbol-name sym) symbol-end))
   2940                   defun-end t)
   2941             (helpful--flash-region (match-beginning 0) (match-end 0))))))))
   2942 
   2943 (defun helpful-kill-buffers ()
   2944   "Kill all `helpful-mode' buffers.
   2945 
   2946 See also `helpful-max-buffers'."
   2947   (interactive)
   2948   (dolist (buffer (buffer-list))
   2949     (when (eq (buffer-local-value 'major-mode buffer) 'helpful-mode)
   2950       (kill-buffer buffer))))
   2951 
   2952 (defvar helpful-mode-map
   2953   (let* ((map (make-sparse-keymap)))
   2954     (define-key map (kbd "g") #'helpful-update)
   2955     (define-key map [remap revert-buffer] #'helpful-update)
   2956     (when (fboundp 'revert-buffer-quick)
   2957       (define-key map [remap revert-buffer-quick] #'helpful-update))
   2958 
   2959     (define-key map (kbd "RET") #'helpful-visit-reference)
   2960 
   2961     (define-key map (kbd "TAB") #'forward-button)
   2962     (define-key map (kbd "<backtab>") #'backward-button)
   2963 
   2964     (define-key map (kbd "n") #'forward-button)
   2965     (define-key map (kbd "p") #'backward-button)
   2966     map)
   2967   "Keymap for `helpful-mode'.")
   2968 
   2969 (declare-function bookmark-prop-get "bookmark" (bookmark prop))
   2970 (declare-function bookmark-make-record-default "bookmark"
   2971                   (&optional no-file no-context posn))
   2972 ;; Ensure this variable is defined even if bookmark.el isn't loaded
   2973 ;; yet. This follows the pattern in help-mode.el.gz.
   2974 ;; TODO: find a cleaner solution.
   2975 (defvar bookmark-make-record-function)
   2976 
   2977 (defun helpful--add-support-for-org-links ()
   2978   "Improve support for org \"help\" links through helpful."
   2979   (helpful--support-storing-org-links)
   2980   (helpful--prefer-helpful-when-following-org-link))
   2981 
   2982 (defun helpful--support-storing-org-links ()
   2983   "Make `org-store-link' in a helpful buffer return a \"help\" link."
   2984   (when (and (fboundp 'org-link-set-parameters)
   2985              (not (-contains-p (org-link-types) "helpful")))
   2986     (org-link-set-parameters "helpful"
   2987                              :store #'helpful--org-link-store)))
   2988 
   2989 (defun helpful--org-link-store ()
   2990   "Store \"help\" type link when in a helpful buffer."
   2991   (when (derived-mode-p 'helpful-mode)
   2992     ;; Create a "help" link instead of a dedicated "helpful" link: the
   2993     ;; author of the Org document uses helful, but this is not
   2994     ;; necessarily the case of the reader of the document.
   2995     (org-link-store-props :type "help"
   2996                           :link (format "help:%s" helpful--sym)
   2997                           :description nil)))
   2998 
   2999 (defun helpful--prefer-helpful-when-following-org-link ()
   3000   "Prefer helpful when using `org-open-at-point' on a \"help\" link."
   3001   (when (fboundp 'org-link-set-parameters)
   3002     (let ((follow-function (org-link-get-parameter "help" :follow)))
   3003       (when (not (equal follow-function #'helpful--org-link-follow))
   3004         (org-link-set-parameters "help"
   3005                                  :follow #'helpful--org-link-follow)))))
   3006 
   3007 (defun helpful--org-link-follow (link _)
   3008   (helpful-symbol (intern link)))
   3009 
   3010 (define-derived-mode helpful-mode special-mode "Helpful"
   3011   "Major mode for *Helpful* buffers."
   3012   (add-hook 'xref-backend-functions #'elisp--xref-backend nil t)
   3013 
   3014   (setq imenu-create-index-function #'helpful--imenu-index)
   3015   ;; Prevent imenu converting "Source Code" to "Source.Code".
   3016   (setq-local imenu-space-replacement " ")
   3017 
   3018   ;; Enable users to bookmark helpful buffers.
   3019   (set (make-local-variable 'bookmark-make-record-function)
   3020        #'helpful--bookmark-make-record)
   3021 
   3022   ;; This function should normally only be called once after Org and
   3023   ;; helpful are loaded. To avoid using `eval-after-load' (which is
   3024   ;; only recommended in user init files), the function is called each
   3025   ;; time the major mode is used.
   3026   (helpful--add-support-for-org-links))
   3027 
   3028 (provide 'helpful)
   3029 ;;; helpful.el ends here