;;; helpful.el --- a better *help* buffer -*- lexical-binding: t; -*- ;; Copyright (C) 2017 Wilfred Hughes ;; Author: Wilfred Hughes ;; URL: https://github.com/Wilfred/helpful ;; Keywords: help, lisp ;; Version: 0.7 ;; Package-Requires: ((emacs "25.1") (dash "2.12.0") (dash-functional "1.2.0") (s "1.11.0") (elisp-refs "1.2") (shut-up "0.3")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Helpful is a replacement for *help* buffers that provides much more ;; contextual information. To get started, try: ;; `M-x helpful-function RET helpful-function ;; ;; The full set of commands you can try is: ;; ;; * helpful-function ;; * helpful-command ;; * helpful-key ;; * helpful-macro ;; * helpful-callable ;; * helpful-variable ;; * helpful-at-point ;; ;; For more information and screenshots, see ;; https://github.com/wilfred/helpful ;;; Code: (require 'elisp-refs) (require 'help) (require 'help-fns) (require 'dash) (require 'dash-functional) (require 's) (require 'shut-up) (require 'find-func) (require 'nadvice) (require 'info-look) (require 'edebug) (require 'trace) (defvar-local helpful--sym nil) (defvar-local helpful--callable-p nil) (defvar-local helpful--associated-buffer nil "We store a reference to the buffer we were called from, so we can show the value of buffer-local variables.") (defgroup helpful nil "A rich help system with contextual information." :group 'helpful) (defcustom helpful-max-buffers 5 "Helpful will kill the least recently used Helpful buffer if there are more than this many. To disable cleanup entirely, set this variable to nil. See also `helpful-kill-buffers' for a one-off cleanup." :group 'help :type '(choice (const nil) number) :group 'helpful) (defun helpful--kind-name (symbol callable-p) "Describe what kind of symbol this is." (cond ((not callable-p) "variable") ((commandp symbol) "command") ((macrop symbol) "macro") ((functionp symbol) "function") ((special-form-p symbol) "special form"))) (defun helpful--buffer (symbol callable-p) "Return a buffer to show help for SYMBOL in." (let* ((current-buffer (current-buffer)) (buf-name (format "*helpful %s: %s*" (helpful--kind-name symbol callable-p) symbol)) (buf (get-buffer buf-name))) (unless buf ;; If we need to create the buffer, ensure we don't exceed ;; `helpful-max-buffers' by killing the least recently used. (when (numberp helpful-max-buffers) (let* ((buffers (buffer-list)) (helpful-bufs (--filter (with-current-buffer it (eq major-mode 'helpful-mode)) buffers)) ;; `buffer-list' seems to be ordered by most recently ;; visited first, so keep those. (excess-buffers (-drop (1- helpful-max-buffers) helpful-bufs))) ;; Kill buffers so we have one buffer less than the maximum ;; before we create a new one. (-each excess-buffers #'kill-buffer))) (setq buf (get-buffer-create buf-name))) ;; Initialise the buffer with the symbol and associated data. (with-current-buffer buf (helpful-mode) (setq helpful--sym symbol) (setq helpful--callable-p callable-p) (setq helpful--associated-buffer current-buffer)) buf)) (defface helpful-heading '((t (:weight bold))) "Face used for headings in Helpful buffers.") (defun helpful--heading (text) "Propertize TEXT as a heading." (format "%s\n" (propertize text 'face 'helpful-heading))) (defun helpful--format-closure (sym form) "Given a closure, return an equivalent defun form." (-let (((_keyword _env args . body) form) (docstring nil)) (when (stringp (car body)) (setq docstring (car body)) (setq body (cdr body)) ;; Ensure that the docstring doesn't have lines starting with (, ;; or it breaks indentation. (setq docstring (s-replace "\n(" "\n\\(" docstring))) (if docstring `(defun ,sym ,args ,docstring ,@body) `(defun ,sym ,args ,@body)))) (defun helpful--pretty-print (value) "Pretty-print VALUE. Break lists over large lines, and ensure strings are surrounded with double-quotes." (with-temp-buffer (delay-mode-hooks (lisp-mode)) (cl-prettyprint value) (s-trim (buffer-string)))) (defun helpful--button (text type &rest properties) ;; `make-text-button' mutates our string to add properties. Copy ;; TEXT to prevent mutating our arguments, and to support 'pure' ;; strings, which are read-only. (setq text (substring-no-properties text)) (apply #'make-text-button text nil :type type properties)) (defun helpful--canonical-symbol (sym callable-p) "If SYM is an alias, return the underlying symbol. Return SYM otherwise." (let ((depth 0)) (if callable-p (while (and (symbolp (symbol-function sym)) (< depth 10)) (setq sym (symbol-function sym)) (setq depth (1+ depth))) (setq sym (indirect-variable sym)))) sym) (defun helpful--aliases (sym callable-p) "Return all the aliases for SYM." (let ((changed t) (canonical (helpful--canonical-symbol sym callable-p)) aliases) (while changed (setq changed nil) (mapatoms (lambda (s) (when (and ;; Skip variables that aren't bound, so we're faster. (if callable-p (fboundp s) (boundp s)) ;; If this symbol is a new alias for our target sym, ;; add it. (eq canonical (helpful--canonical-symbol s callable-p)) (not (-contains-p aliases s))) (push s aliases) (setq changed t))))) (--sort (string< (symbol-name it) (symbol-name other)) aliases))) (defun helpful--format-alias (sym callable-p) (let ((obsolete-info (if callable-p (get sym 'byte-obsolete-info) (get sym 'byte-obsolete-variable))) (sym-button (helpful--button (symbol-name sym) 'helpful-describe-exactly-button 'symbol sym 'callable-p callable-p))) (cond (obsolete-info (format "%s (obsolete since %s)" sym-button (-last-item obsolete-info))) (t sym-button)))) (defun helpful--indent-rigidly (s amount) "Indent string S by adding AMOUNT spaces to each line." (with-temp-buffer (insert s) (indent-rigidly (point-min) (point-max) amount) (buffer-string))) (defun helpful--format-properties (symbol) "Return a string describing all the properties of SYMBOL." (let* ((syms-and-vals (-partition 2 (symbol-plist symbol))) (syms-and-vals (-sort (-lambda ((sym1 _) (sym2 _)) (string-lessp (symbol-name sym1) (symbol-name sym2))) syms-and-vals)) (lines (--map (-let* (((sym val) it) (pretty-val (helpful--pretty-print val))) (format "%s\n%s%s" (propertize (symbol-name sym) 'face 'font-lock-constant-face) (helpful--indent-rigidly pretty-val 2) (cond ;; Also offer to disassemble byte-code ;; properties. ((byte-code-function-p val) (format "\n %s" (helpful--make-disassemble-button val))) ((eq sym 'ert--test) (format "\n %s" (helpful--make-run-test-button symbol))) (t "")))) syms-and-vals))) (when lines (s-join "\n" lines)))) (define-button-type 'helpful-forget-button 'action #'helpful--forget 'symbol nil 'callable-p nil 'follow-link t 'help-echo "Unbind this function") ;; TODO: it would be nice to optionally delete the source code too. (defun helpful--forget (button) "Unbind the current symbol." (let* ((sym (button-get button 'symbol)) (callable-p (button-get button 'callable-p)) (kind (helpful--kind-name sym callable-p))) (when (yes-or-no-p (format "Forget %s %s?" kind sym)) (if callable-p (fmakunbound sym) (makunbound sym)) (message "Forgot %s %s." kind sym) (kill-buffer (current-buffer))))) (define-button-type 'helpful-c-source-directory 'action #'helpful--c-source-directory 'follow-link t 'help-echo "Set directory to Emacs C source code") (defun helpful--c-source-directory (_button) "Set `find-function-C-source-directory' so we can show the source code to primitives." (let ((emacs-src-dir (read-directory-name "Path to Emacs source code: "))) ;; Let the user specify the source path with or without src/, ;; which is a subdirectory in the Emacs tree. (unless (equal (f-filename emacs-src-dir) "src") (setq emacs-src-dir (f-join emacs-src-dir "src"))) (setq find-function-C-source-directory emacs-src-dir)) (helpful-update)) (define-button-type 'helpful-disassemble-button 'action #'helpful--disassemble 'follow-link t 'object nil 'help-echo "Show disassembled bytecode") (defun helpful--disassemble (button) "Disassemble the current symbol." ;; `disassemble' can handle both symbols (e.g. 'when) and raw ;; byte-code objects. (disassemble (button-get button 'object))) (define-button-type 'helpful-run-test-button 'action #'helpful--run-test 'follow-link t 'symbol nil 'help-echo "Run ERT test") (defun helpful--run-test (button) "Disassemble the current symbol." (ert (button-get button 'symbol))) (define-button-type 'helpful-edebug-button 'action #'helpful--edebug 'follow-link t 'symbol nil 'help-echo "Toggle edebug (re-evaluates definition)") (defun helpful--edebug-p (sym) "Does function SYM have its definition patched by edebug?" (let ((fn-def (indirect-function sym))) ;; Edebug replaces function source code with a sexp that has ;; `edebug-enter', `edebug-after' etc interleaved. This means the ;; function is interpreted, so `indirect-function' returns a list. (when (and (consp fn-def) (consp (cdr fn-def))) (eq (car (-last-item fn-def)) 'edebug-enter)))) (defun helpful--can-edebug-p (sym callable-p) "Can we use edebug with SYM?" (and ;; SYM must be a function. callable-p ;; The function cannot be a primitive, it must be defined in elisp. (not (helpful--primitive-p sym callable-p)) ;; We need to be able to find its definition, or we can't step ;; through the source. (-let* (((buf pos opened) (helpful--definition sym t)) (have-definition (and buf pos))) (when opened (kill-buffer buf)) have-definition))) (defun helpful--toggle-edebug (sym) "Enable edebug when function SYM is called, or disable if already enabled." (-let ((should-edebug (not (helpful--edebug-p sym))) ((buf pos created) (helpful--definition sym t))) (if (and buf pos) (progn (with-current-buffer buf (save-excursion (save-restriction (widen) (goto-char pos) (let* ((edebug-all-forms should-edebug) (edebug-all-defs should-edebug) (form (edebug-read-top-level-form))) ;; Based on `edebug-eval-defun'. (eval (eval-sexp-add-defvars form) lexical-binding))))) ;; If we're enabling edebug, we need the source buffer to ;; exist. Otherwise, we can clean it up. (when (and created (not should-edebug)) (kill-buffer buf))) (user-error "Could not find source for edebug")))) (defun helpful--edebug (button) "Toggle edebug for the current symbol." (helpful--toggle-edebug (button-get button 'symbol)) (helpful-update)) (define-button-type 'helpful-trace-button 'action #'helpful--trace 'follow-link t 'symbol nil 'help-echo "Toggle function tracing") (defun helpful--trace (button) "Toggle tracing for the current symbol." (let ((sym (button-get button 'symbol))) (if (trace-is-traced sym) (untrace-function sym) (trace-function sym))) (helpful-update)) (define-button-type 'helpful-navigate-button 'action #'helpful--navigate 'path nil 'position nil 'follow-link t 'help-echo "Navigate to definition") (defun helpful--navigate (button) "Navigate to the path this BUTTON represents." (find-file (substring-no-properties (button-get button 'path))) ;; We use `get-text-property' to work around an Emacs 25 bug: ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=f7c4bad17d83297ee9a1b57552b1944020f23aea (-when-let (pos (get-text-property button 'position (marker-buffer button))) (goto-char pos))) (defun helpful--navigate-button (text path &optional pos) "Return a button that opens PATH and puts point at POS." (helpful--button text 'helpful-navigate-button 'path path 'position pos)) (define-button-type 'helpful-customize-button 'action #'helpful--customize 'symbol nil 'follow-link t 'help-echo "Open Customize for this symbol") (defun helpful--customize (button) "Open Customize for this symbol." (customize-variable (button-get button 'symbol))) (define-button-type 'helpful-toggle-button 'action #'helpful--toggle 'symbol nil 'buffer nil 'follow-link t 'help-echo "Toggle this symbol between t and nil") (defun helpful--toggle (button) "Toggle the symbol between nil and t." (let ((sym (button-get button 'symbol)) (buf (button-get button 'buffer))) (save-current-buffer ;; If this is a buffer-local variable, ensure we're in the right ;; buffer. (when buf (set-buffer buf)) (set sym (not (symbol-value sym)))) (helpful-update))) (define-button-type 'helpful-set-button 'action #'helpful--set 'symbol nil 'buffer nil 'follow-link t 'help-echo "Set the value of this symbol") (defun helpful--set (button) "Set the value of this symbol." (let* ((sym (button-get button 'symbol)) (buf (button-get button 'buffer)) (sym-value (helpful--sym-value sym buf)) ;; Inspired by `counsel-read-setq-expression'. (expr (minibuffer-with-setup-hook (lambda () (add-function :before-until (local 'eldoc-documentation-function) #'elisp-eldoc-documentation-function) (run-hooks 'eval-expression-minibuffer-setup-hook) (goto-char (minibuffer-prompt-end)) (forward-char (length (format "(setq %S " sym)))) (read-from-minibuffer "Eval: " (format (if (or (symbolp sym-value) (consp sym-value)) "(setq %s '%S)" "(setq %s %S)") sym sym-value) read-expression-map t 'read-expression-history)))) (save-current-buffer ;; If this is a buffer-local variable, ensure we're in the right ;; buffer. (when buf (set-buffer buf)) (eval-expression expr)) (helpful-update))) (define-button-type 'helpful-all-references-button 'action #'helpful--all-references 'symbol nil 'callable-p nil 'follow-link t 'help-echo "Find all references to this symbol") (defun helpful--all-references (button) "Find all the references to the symbol that this BUTTON represents." (let ((sym (button-get button 'symbol)) (callable-p (button-get button 'callable-p))) (cond ((not callable-p) (elisp-refs-variable sym)) ((functionp sym) (elisp-refs-function sym)) ((macrop sym) (elisp-refs-macro sym))))) (define-button-type 'helpful-manual-button 'action #'helpful--manual 'symbol nil 'follow-link t 'help-echo "View this symbol in the Emacs manual") (defun helpful--manual (button) "Open the manual for the system that this BUTTON represents." (let ((sym (button-get button 'symbol))) (info-lookup 'symbol sym #'emacs-lisp-mode))) (define-button-type 'helpful-describe-button 'action #'helpful--describe 'symbol nil 'follow-link t 'help-echo "Describe this symbol") (defun helpful--describe (button) "Describe the symbol that this BUTTON represents." (let ((sym (button-get button 'symbol))) (helpful-symbol sym))) (define-button-type 'helpful-describe-exactly-button 'action #'helpful--describe-exactly 'symbol nil 'callable-p nil 'follow-link t 'help-echo "Describe this symbol") (defun helpful--describe-exactly (button) "Describe the symbol that this BUTTON represents. This differs from `helpful--describe' because here we know whether the symbol represents a variable or a callable." (let ((sym (button-get button 'symbol)) (callable-p (button-get button 'callable-p))) (if callable-p (helpful-callable sym) (helpful-variable sym)))) (define-button-type 'helpful-info-button 'action #'helpful--info 'info-node nil 'follow-link t 'help-echo "View this Info node") (defun helpful--info (button) "Describe the symbol that this BUTTON represents." (info (button-get button 'info-node))) (defun helpful--split-first-line (docstring) "If the first line is a standalone sentence, ensure we have a blank line afterwards." (let* ((lines (s-lines docstring)) (first-line (-first-item lines)) (second-line (when (> (length lines) 1) (nth 1 lines)))) (if (and (s-ends-with-p "." first-line) (stringp second-line) (not (equal second-line ""))) (s-join "\n" (-cons* first-line "" (cdr lines))) docstring))) (defun helpful--propertize-keywords (docstring) "Propertize quoted keywords in docstrings." (replace-regexp-in-string ;; Replace all text of the form `foo'. (rx "`" (group ":" symbol-start (+? (or (syntax word) (syntax symbol))) symbol-end) "'") (lambda (it) (propertize (match-string 1 it) 'face 'font-lock-builtin-face)) docstring t t)) (defun helpful--propertize-quoted (docstring) "Convert `foo' in docstrings to buttons (if bound) or else highlight." (replace-regexp-in-string ;; Replace all text of the form `foo'. (rx "`" (+? (not (any "`" "'"))) "'") (lambda (it) (let* ((sym-name (s-chop-prefix "`" (s-chop-suffix "'" it))) (sym (intern sym-name))) (cond ;; Only create a link if this is a symbol that is bound as a ;; variable or callable. ((or (boundp sym) (fboundp sym)) (helpful--button sym-name 'helpful-describe-button 'symbol sym)) ;; If this is already a button, don't modify it. ((get-text-property 0 'button sym-name) sym-name) ;; Highlight the quoted string. (t (propertize sym-name 'face 'font-lock-constant-face))))) docstring t t)) (defun helpful--propertize-info (docstring) "Convert info references in docstrings to buttons." (replace-regexp-in-string ;; Replace all text of the form `foo'. (rx "Info node" (group (+ whitespace)) "`" (group (+ (not (in "'")))) "'") (lambda (it) ;; info-node has the form "(cl)Loop Facility". (let ((space (match-string 1 it)) (info-node (match-string 2 it))) (concat "Info node" space (helpful--button info-node 'helpful-info-button 'info-node info-node)))) docstring t t)) ;; TODO: handle keymaps of the form \\{foo}. (defun helpful--format-command-keys (docstring) "Convert command key references in docstrings to buttons. Emacs uses \\= to escape \\[ references, so replace that unescaping too." ;; Based on `substitute-command-keys', but converts command ;; references to buttons. (let ((keymap nil)) (with-temp-buffer (insert docstring) (goto-char (point-min)) (while (not (eobp)) (cond ((looking-at ;; Text of the form \=X (rx "\\=")) ;; Remove the escaping, then step over the escaped char. ;; Step over the escaped character. (delete-region (point) (+ (point) 2)) (forward-char 1)) ((looking-at ;; Text of the form \\ (rx "\\<" (group (+ (not (in ">")))) ">")) (let* ((symbol-with-parens (match-string 0)) (symbol-name (match-string 1))) ;; Remove the original string. (delete-region (point) (+ (point) (length symbol-with-parens))) ;; Set the new keymap. (setq keymap (symbol-value (intern symbol-name))))) ((looking-at ;; Text of the form \\[foo-command] (rx "\\[" (group (+ (not (in "]")))) "]")) (let* ((symbol-with-parens (match-string 0)) (symbol-name (match-string 1))) ;; Remove the original string. (delete-region (point) (+ (point) (length symbol-with-parens))) ;; Add a button. (let* ((symbol (intern symbol-name)) (key (where-is-internal symbol keymap t)) (key-description (if key (key-description key) (format "M-x %s" symbol-name)))) (insert (helpful--button key-description 'helpful-describe-exactly-button 'symbol symbol 'callable-p t))))) ;; Don't modify other characters. (t (forward-char 1)))) (buffer-string)))) ;; TODO: fix upstream Emacs bug that means `-map' is not highlighted ;; in the docstring for `--map'. (defun helpful--format-docstring (docstring) "Replace cross-references with links in DOCSTRING." (-> docstring (helpful--format-command-keys) (helpful--split-first-line) (helpful--propertize-info) (helpful--propertize-keywords) (helpful--propertize-quoted) (s-trim))) (defconst helpful--highlighting-funcs '(ert--activate-font-lock-keywords highlight-quoted-mode rainbow-delimiters-mode) "Highlighting functions that are safe to run in a temporary buffer. This is used in `helpful--syntax-highlight' to support extra highlighting that the user may have configured in their mode hooks.") (defun helpful--syntax-highlight (source &optional mode) "Return a propertized version of SOURCE in MODE." (unless mode (setq mode #'emacs-lisp-mode)) (with-temp-buffer (insert source) ;; Switch to major-mode MODE, but don't run any hooks. (delay-mode-hooks (funcall mode)) ;; `delayed-mode-hooks' contains mode hooks like ;; `emacs-lisp-mode-hook'. Build a list of functions that are run ;; when the mode hooks run. (let (hook-funcs) (dolist (hook delayed-mode-hooks) (let ((funcs (symbol-value hook))) (setq hook-funcs (append hook-funcs funcs)))) ;; Filter hooks to those that relate to highlighting, and run them. (setq hook-funcs (-intersection hook-funcs helpful--highlighting-funcs)) (-map #'funcall hook-funcs)) (if (fboundp 'font-lock-ensure) (font-lock-ensure) (with-no-warnings (font-lock-fontify-buffer))) (buffer-string))) (defun helpful--source (sym callable-p) "Return the source code of SYM. If the source code cannot be found, return the sexp used." (-let (((buf start-pos created) (helpful--definition sym callable-p)) (source nil)) (when (and buf start-pos) (with-current-buffer buf (save-excursion (save-restriction (goto-char start-pos) (narrow-to-defun) (setq source (buffer-substring-no-properties (point-min) (point-max)))))) (setq source (s-trim-right source)) (when (and source (buffer-file-name buf)) (setq source (propertize source 'helpful-path (buffer-file-name buf) 'helpful-pos start-pos 'helpful-pos-is-start t)))) (when (and buf created) (kill-buffer buf)) (if source source ;; Could not find source -- probably defined interactively, or via ;; a macro, or file has changed. ;; TODO: verify that the source hasn't changed before showing. ;; TODO: offer to download C sources for current version. (indirect-function sym)))) (defun helpful--in-manual-p (sym) "Return non-nil if SYM is in an Info manual." (let ((completions (shut-up (info-lookup->completions 'symbol 'emacs-lisp-mode)))) (-when-let (buf (get-buffer " temp-info-look")) (kill-buffer buf)) (or (assoc sym completions) (assoc-string sym completions)))) (defun helpful--find-library-name (path) "A wrapper around `find-library-name' that returns nil if PATH has no library with that name. This can happen when users have installed Emacs without its source code: they have e.g. org.elc but no org.el." (condition-case _err (find-library-name path) (error nil))) (defun helpful--definition (sym callable-p) "Return a list (BUF POS OPENED) where SYM is defined. BUF is the buffer containing the definition. If the user wasn't already visiting this buffer, OPENED is t and callers should kill the buffer when done. POS is the position of the start of the definition within the buffer." (-let ((initial-buffers (buffer-list)) (primitive-p (helpful--primitive-p sym callable-p)) (path nil) (buf nil) (pos nil) (opened nil)) (when callable-p (-let [(base-sym . src-path) (find-function-library sym)] ;; `base-sym' is the underlying symbol if `sym' is an alias. (setq sym base-sym) (setq path src-path))) (when (and primitive-p path find-function-C-source-directory) ;; Convert "src/foo.c" to "". (setq path (f-expand path (f-parent find-function-C-source-directory)))) (cond ((and callable-p path) ;; Convert foo.elc to foo.el. (-when-let (src-path (helpful--find-library-name path)) ;; Open `path' ourselves, so we can widen before searching. ;; ;; Opening large.c files can be slow (e.g. when looking at ;; `defalias'), especially if the user has configured mode hooks. ;; ;; Bind `auto-mode-alist' to nil, so we open the buffer in ;; `fundamental-mode' if it isn't already open. (let (auto-mode-alist ;; Don't both setting buffer-local variables, it's ;; annoying to prompt the user since we immediately ;; discard the buffer. enable-local-variables) (setq buf (find-file-noselect src-path))) (unless (-contains-p initial-buffers buf) (setq opened t)) ;; If it's a freshly opened buffer, we need to switch to the ;; correct mode so we can search correctly. Enable the mode, but ;; don't bother with mode hooks, because we just need the syntax ;; table for searching. (when opened (with-current-buffer buf (let (enable-local-variables) (delay-mode-hooks (normal-mode t))))) ;; Based on `find-function-noselect'. (with-current-buffer buf ;; `find-function-search-for-symbol' moves point. Prevent ;; that. (save-excursion ;; Narrowing has been fixed upstream: ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=abd18254aec76b26e86ae27e91d2c916ec20cc46 (save-restriction (widen) (setq pos (if primitive-p (cdr (find-function-C-source sym path nil)) (cdr (find-function-search-for-symbol sym nil path))))))))) (callable-p ;; Functions defined interactively may have an edebug property ;; that contains the location of the definition. (-when-let (edebug-info (get sym 'edebug)) (-let [marker (if (consp edebug-info) (car edebug-info) edebug-info)] (setq buf (marker-buffer marker)) (setq pos (marker-position marker))))) ((not callable-p) (condition-case _err (-let [(sym-buf . sym-pos) (find-definition-noselect sym 'defvar)] (setq buf sym-buf) (setq pos sym-pos)) (search-failed nil) ;; If your current Emacs instance doesn't match the source ;; code configured in find-function-C-source-directory, we can ;; get an error about not finding source. Try ;; `default-tab-width' against Emacs trunk. (error nil)))) (list buf pos opened))) (defun helpful--source-path (sym callable-p) "Return the path where SYM is defined." (-let* (((buf _ opened) (helpful--definition sym callable-p)) (path nil)) (when buf (setq path (buffer-file-name buf)) (when opened ;; If we've just created this buffer, close it. (kill-buffer buf))) path)) (defun helpful--source-pos (sym callable-p) "Return the file position where SYM is defined." (-let (((buf pos opened) (helpful--definition sym callable-p))) ;; If we've just created this buffer, close it. (when opened ;; If we've just created this buffer, close it. (kill-buffer buf)) pos)) (defun helpful--reference-positions (sym callable-p buf) "Return all the buffer positions of references to SYM in BUF." (-let* ((forms-and-bufs (elisp-refs--search-1 (list buf) (lambda (buf) (elisp-refs--read-and-find buf sym (if callable-p #'elisp-refs--function-p #'elisp-refs--variable-p))))) ;; Since we only searched one buffer, we know that ;; forms-and-bufs has only one item. (forms-and-buf (-first-item forms-and-bufs)) ((forms . _buf) forms-and-buf)) (-map (-lambda ((_code start-pos _end-pos)) start-pos) forms))) (defun helpful--all-keymap-syms () "Return all keymaps defined in this Emacs instance." (let (keymaps) (mapatoms (lambda (sym) (when (and (boundp sym) (keymapp (symbol-value sym))) (push sym keymaps)))) keymaps)) (defun helpful--keymaps-containing (command-sym) "Return a list of pairs listing keymap symbols that contain COMMAND-SYM, along with the keybindings in each keymap. We ignore keybindings that are menu items, and ignore keybindings from parent keymaps." (let (matching-keymaps) ;; Look for this command in all keymaps. (dolist (keymap-sym (helpful--all-keymap-syms)) (let* ((keymap (symbol-value keymap-sym)) (keycodes (where-is-internal command-sym (list keymap) nil t)) ;; Look up this command in the parent keymap. (parent-keymap (keymap-parent keymap)) (parent-keycodes (when parent-keymap (where-is-internal command-sym (list parent-keymap) nil t)))) (setq keycodes ;; Ignore keybindings that we've just inherited from the ;; parent. (-difference keycodes parent-keycodes)) (when keycodes (push (cons keymap-sym (-map #'key-description keycodes)) matching-keymaps)))) matching-keymaps)) (defun helpful--format-keys (command-sym) "Describe all the keys that call COMMAND-SYM." (let (mode-lines global-lines) (--each (helpful--keymaps-containing command-sym) (-let [(map . keys) it] (dolist (key keys) (push (format "%s %s" (propertize (symbol-name map) 'face 'font-lock-variable-name-face) key) (if (eq map 'global-map) global-lines mode-lines))))) (setq global-lines (-sort #'string< global-lines)) (setq mode-lines (-sort #'string< mode-lines)) (-let [lines (-concat global-lines mode-lines)] (if lines (s-join "\n" lines) "This command is not in any keymaps.")))) (defun helpful--outer-sexp (buf pos) "Find position POS in BUF, and return the name of the outer sexp, along with its position." (with-current-buffer buf (goto-char pos) (beginning-of-defun) (list (point) (-take 2 (read buf))))) (defun helpful--count-values (items) "Return an alist of the count of each value in ITEMS. E.g. (x x y z y) -> ((x . 2) (y . 2) (z . 1))" (let (counts) (dolist (item items (nreverse counts)) (-if-let (item-and-count (assoc item counts)) (setcdr item-and-count (1+ (cdr item-and-count))) (push (cons item 1) counts))))) (defun helpful--advised-p (sym) "A list of advice associated with SYM." (advice--p (advice--symbol-function sym))) (defun helpful--format-head (head) "Given a 'head' (the first two symbols of a sexp) format and syntax highlight it." (-let* (((def name) head) (formatted-name (if (and (consp name) (eq (car name) 'quote)) (format "'%S" (cadr name)) (format "%S" name))) (formatted-def (format "(%s %s ...)" def formatted-name)) ) (helpful--syntax-highlight formatted-def))) (defun helpful--format-reference (head longest-head ref-count position path) "Return a syntax-highlighted version of HEAD, with a link to its source location." (let ((formatted-count (format "%d reference%s" ref-count (if (> ref-count 1) "s" "")))) (propertize (format "%s %s" (s-pad-right longest-head " " (helpful--format-head head)) (propertize formatted-count 'face 'font-lock-comment-face)) 'helpful-path path 'helpful-pos position))) (defun helpful--format-position-heads (position-heads path) "Given a list of outer sexps, format them for display. POSITION-HEADS takes the form ((123 (defun foo)) (456 (defun bar)))." (let ((longest-head (->> position-heads (-map (-lambda ((_pos head)) (helpful--format-head head))) (-map #'length) (-max)))) (->> (helpful--count-values position-heads) (-map (-lambda (((pos head) . count)) (helpful--format-reference head longest-head count pos path))) (s-join "\n")))) (defun helpful--primitive-p (sym callable-p) "Return t if SYM is defined in C." (if callable-p (subrp (indirect-function sym)) (let ((filename (find-lisp-object-file-name sym 'defvar))) (or (eq filename 'C-source) (and (stringp filename) (equal (file-name-extension filename) "c")))))) (defun helpful--sym-value (sym buf) "Return the value of SYM in BUF." (with-current-buffer buf (symbol-value sym))) (defun helpful--insert-section-break () "Insert section break into helpful buffer." (insert "\n\n")) (defun helpful--calculate-references (sym callable-p source-path) "Calculate references for SYM in SOURCE-PATH." (when source-path (let* ((primitive-p (helpful--primitive-p sym callable-p)) (buf (elisp-refs--contents-buffer source-path)) (positions (if primitive-p nil (helpful--reference-positions helpful--sym helpful--callable-p buf))) (return-value (--map (helpful--outer-sexp buf it) positions))) (kill-buffer buf) return-value))) (defun helpful--make-manual-button (sym) "Make manual button for SYM." (helpful--button "View in manual" 'helpful-manual-button 'symbol sym)) (defun helpful--make-toggle-button (sym buffer) "Make toggle button for SYM in BUFFER." (helpful--button "Toggle" 'helpful-toggle-button 'symbol sym 'buffer buffer)) (defun helpful--make-set-button (sym buffer) "Make set button for SYM in BUFFER." (helpful--button "Set" 'helpful-set-button 'symbol sym 'buffer buffer)) (defun helpful--make-customize-button (sym) "Make customize button for SYM." (helpful--button "Customize" 'helpful-customize-button 'symbol sym)) (defun helpful--make-references-button (sym callable-p) "Make references button for SYM." (helpful--button "Find all references" 'helpful-all-references-button 'symbol sym 'callable-p callable-p)) (defun helpful--make-edebug-button (sym) "Make edebug button for SYM." (helpful--button (format "%s edebug" (if (helpful--edebug-p sym) "Disable" "Enable")) 'helpful-edebug-button 'symbol sym)) (defun helpful--make-tracing-button (sym) "Make tracing button for SYM." (helpful--button (format "%s tracing" (if (trace-is-traced sym) "Disable" "Enable")) 'helpful-trace-button 'symbol sym)) (defun helpful--make-disassemble-button (obj) "Make disassemble button for OBJ. OBJ may be a symbol or a compiled function object." (helpful--button "Disassemble" 'helpful-disassemble-button 'object obj)) (defun helpful--make-run-test-button (sym) "Make an ERT test button for SYM." (helpful--button "Run test" 'helpful-run-test-button 'symbol sym)) (defun helpful--make-forget-button (sym callable-p) "Make forget button for SYM." (helpful--button "Forget" 'helpful-forget-button 'symbol sym 'callable-p callable-p)) (defun helpful--summary (sym callable-p) "Return a one sentence summary for SYM." (-let* ((primitive-p (helpful--primitive-p sym callable-p)) ((buf pos opened) (if (or (not primitive-p) find-function-C-source-directory) (helpful--definition sym callable-p) '(nil nil nil))) (interactive-button (helpful--button "interactive" 'helpful-info-button 'info-node "(elisp)Using Interactive")) (autoload-button (helpful--button "autoloaded" 'helpful-info-button 'info-node "(elisp)Autoload")) ;; TODO: this only reports if a function is autoloaded ;; because we autoloaded it. This ignores newly defined ;; functions that are autoloaded. Built-in help has this ;; limitation too, but if we can find the source, we should ;; instead see if there's an autoload cookie. (autoloaded-p (if (and callable-p buf (buffer-file-name buf)) (help-fns--autoloaded-p sym (buffer-file-name buf)))) (description (cond ((and callable-p (commandp sym) autoloaded-p) (format "an %s, %s" interactive-button autoload-button)) ((and callable-p (commandp sym)) (format "an %s" interactive-button)) ((and callable-p autoloaded-p) (format "an %s" autoload-button)) (t "a"))) (kind (cond ((not callable-p) "variable") ((macrop sym) "macro") (t "function"))) (defined (cond (buf (let ((path (buffer-file-name buf))) (format "defined in %s" (helpful--navigate-button (file-name-nondirectory path) path pos)))) (primitive-p "defined in C source code") (t "without source code")))) (when opened (kill-buffer buf)) (format "%s is %s %s %s." sym description kind defined))) (defun helpful-update () "Update the current *Helpful* buffer to the latest state of the current symbol." (interactive) (cl-assert (not (null helpful--sym))) (unless (buffer-live-p helpful--associated-buffer) (setq helpful--associated-buffer nil)) (let* ((inhibit-read-only t) (start-line (line-number-at-pos)) (start-column (current-column)) (primitive-p (helpful--primitive-p helpful--sym helpful--callable-p)) (sym-type (cond ((not helpful--callable-p) "Variable") ((macrop helpful--sym) "Macro") (t "Function"))) (look-for-src (or (not primitive-p) find-function-C-source-directory)) (source (when look-for-src (helpful--source helpful--sym helpful--callable-p))) (source-path (when look-for-src (helpful--source-path helpful--sym helpful--callable-p))) (references (helpful--calculate-references helpful--sym helpful--callable-p source-path))) (erase-buffer) (insert (helpful--summary helpful--sym helpful--callable-p)) (when helpful--callable-p (helpful--insert-section-break) (insert (helpful--heading (format "%s Signature" sym-type)) (helpful--syntax-highlight (helpful--signature helpful--sym)))) (-when-let (docstring (helpful--docstring helpful--sym helpful--callable-p)) (helpful--insert-section-break) (insert (helpful--heading (format "%s Documentation" sym-type)) (helpful--format-docstring docstring)) (when (helpful--in-manual-p helpful--sym) (insert "\n\n") (insert (helpful--make-manual-button helpful--sym)))) (when (not helpful--callable-p) (helpful--insert-section-break) (let ((sym helpful--sym) (buf (or helpful--associated-buffer (current-buffer)))) (insert (helpful--heading "Value") (helpful--pretty-print (helpful--sym-value sym buf)) "\n\n") (when (memq (helpful--sym-value helpful--sym buf) '(nil t)) (insert (helpful--make-toggle-button helpful--sym buf) " ")) (insert (helpful--make-set-button helpful--sym buf)) (when (custom-variable-p helpful--sym) (insert " " (helpful--make-customize-button helpful--sym))))) ;; Show keybindings. ;; TODO: allow users to conveniently add and remove keybindings. (when (commandp helpful--sym) (helpful--insert-section-break) (insert (helpful--heading "Key Bindings") (helpful--format-keys helpful--sym))) (helpful--insert-section-break) (insert (helpful--heading "References") (let ((src-button (when source-path (helpful--navigate-button (file-name-nondirectory source-path) source-path (or (helpful--source-pos helpful--sym helpful--callable-p) 0))))) (cond ((and source-path references) (format "References in %s:\n%s" src-button (helpful--format-position-heads references source-path))) (source-path (format "%s is unused in %s." helpful--sym src-button)) ((and primitive-p (null find-function-C-source-directory)) "C code is not yet loaded.") (t "Could not find source file."))) "\n\n" (helpful--make-references-button helpful--sym helpful--callable-p)) (when (helpful--advised-p helpful--sym) (helpful--insert-section-break) (insert (helpful--heading "Advice") (format "This %s is advised." (downcase sym-type)))) (let ((can-edebug (helpful--can-edebug-p helpful--sym helpful--callable-p)) (can-trace (and helpful--callable-p ;; Tracing uses advice, and you can't apply advice to ;; primitive functions that are replaced with special ;; opcodes. For example, `narrow-to-region'. (not (plist-get (symbol-plist helpful--sym) 'byte-opcode)))) (can-disassemble (and helpful--callable-p (not primitive-p))) (can-forget (and (not (special-form-p helpful--sym)) (not primitive-p)))) (when (or can-edebug can-trace can-disassemble can-forget) (helpful--insert-section-break) (insert (helpful--heading "Debugging"))) (when can-edebug (insert (helpful--make-edebug-button helpful--sym))) (when can-trace (when can-edebug (insert " ")) (insert (helpful--make-tracing-button helpful--sym))) (when (and (or can-edebug can-trace) (or can-disassemble can-forget)) (insert "\n")) (when can-disassemble (insert (helpful--make-disassemble-button helpful--sym))) (when can-forget (when can-disassemble (insert " ")) (insert (helpful--make-forget-button helpful--sym helpful--callable-p)))) (let ((aliases (helpful--aliases helpful--sym helpful--callable-p))) (when (> (length aliases) 1) (helpful--insert-section-break) (insert (helpful--heading "Aliases") (s-join "\n" (--map (helpful--format-alias it helpful--callable-p) aliases))))) (helpful--insert-section-break) (insert (helpful--heading "Source Code") (cond (source-path (concat (propertize (format "%s Defined in " (if primitive-p "//" ";;")) 'face 'font-lock-comment-face) (helpful--navigate-button source-path source-path (helpful--source-pos helpful--sym helpful--callable-p)) "\n")) (primitive-p (concat (propertize "C code is not yet loaded." 'face 'font-lock-comment-face) "\n\n" (helpful--button "Set C source directory" 'helpful-c-source-directory))) (t (helpful--syntax-highlight (format ";; Source file is unknown\n"))))) (when source (insert (cond ((stringp source) (helpful--syntax-highlight source (if primitive-p 'c-mode))) ((and (consp source) (eq (car source) 'closure)) (helpful--syntax-highlight (concat ";; Closure converted to defun by helpful.\n" (helpful--pretty-print (helpful--format-closure helpful--sym source))))) (t (helpful--syntax-highlight (helpful--pretty-print source)))))) (helpful--insert-section-break) (-when-let (formatted-props (helpful--format-properties helpful--sym)) (insert (helpful--heading "Symbol Properties") formatted-props)) (goto-char (point-min)) (forward-line (1- start-line)) (forward-char start-column))) ;; TODO: this isn't sufficient for `edebug-eval-defun'. (defun helpful--skip-advice (docstring) "Remove mentions of advice from DOCSTRING." (let* ((lines (s-lines docstring)) (relevant-lines (--drop-while (s-starts-with-p ":around advice:" it) lines))) (s-trim (s-join "\n" relevant-lines)))) (defun helpful--format-argument (arg) "Format ARG (a symbol) according to Emacs help conventions." (let ((arg-str (symbol-name arg))) (if (s-starts-with-p "&" arg-str) arg-str (s-upcase arg-str)))) (defun helpful--signature (sym) "Get the signature for function SYM, as a string. For example, \"(some-func FOO &optional BAR)\"." (let (docstring-sig source-sig) ;; Get the usage from the function definition. (let* ((function-args (help-function-arglist sym)) (formatted-args (if (listp function-args) (-map #'helpful--format-argument function-args) (list function-args)))) (setq source-sig (if formatted-args (format "(%s %s)" sym (s-join " " formatted-args)) (format "(%s)" sym)))) ;; If the docstring ends with (fn FOO BAR), extract that. (-when-let (docstring (documentation sym)) (-when-let (docstring-with-usage (help-split-fundoc docstring sym)) (setq docstring-sig (car docstring-with-usage)))) (or docstring-sig source-sig))) (defun helpful--docstring (sym callable-p) "Get the docstring for SYM. Note that this returns the raw docstring, including \\=\\= escapes that are used by `substitute-command-keys'." (let ((text-quoting-style 'grave) docstring) (if callable-p (progn (setq docstring (documentation sym t)) (-when-let (docstring-with-usage (help-split-fundoc docstring sym)) (setq docstring (cdr docstring-with-usage)) (when docstring ;; Advice mutates the docstring, see ;; `advice--make-docstring'. Undo that. ;; TODO: Only do this if the function is advised. (setq docstring (helpful--skip-advice docstring))))) (setq docstring (documentation-property sym 'variable-documentation t))) docstring)) (defun helpful--read-symbol (prompt predicate) (let ((sym-here (symbol-at-point))) (read (completing-read prompt obarray predicate t nil nil (when (funcall predicate sym-here) (symbol-name sym-here)))))) ;;;###autoload (defun helpful-function (symbol) "Show help for function named SYMBOL." (interactive (list (helpful--read-symbol "Function: " #'functionp))) (pop-to-buffer (helpful--buffer symbol t)) (helpful-update)) ;;;###autoload (defun helpful-command (symbol) "Show help for interactive function named SYMBOL." (interactive (list (helpful--read-symbol "Command: " #'commandp))) (pop-to-buffer (helpful--buffer symbol t)) (helpful-update)) ;;;###autoload (defun helpful-key (key-sequence) "Show help for interactive command bound to KEY-SEQUENCE." (interactive (list (read-key-sequence "Press key: "))) (let ((sym (key-binding key-sequence))) (cond ((null sym) (user-error "No command is bound to %s" (key-description key-sequence))) ((commandp sym) (pop-to-buffer (helpful--buffer sym t)) (helpful-update)) (t (user-error "%s is bound to symbol %s which is not a command" (key-description key-sequence) sym))))) ;;;###autoload (defun helpful-macro (symbol) "Show help for macro named SYMBOL." (interactive (list (helpful--read-symbol "Macro: " #'macrop))) (pop-to-buffer (helpful--buffer symbol t)) (helpful-update)) ;;;###autoload (defun helpful-callable (symbol) "Show help for function, macro or special form named SYMBOL. See also `helpful-macro' and `helpful-function'." (interactive (list (helpful--read-symbol "Callable: " #'fboundp))) (pop-to-buffer (helpful--buffer symbol t)) (helpful-update)) (defun helpful--variable-p (symbol) "Return non-nil if SYMBOL is a variable." (or (get symbol 'variable-documentation) (and (boundp symbol) (not (keywordp symbol)) (not (eq symbol nil)) (not (eq symbol t))))) (defun helpful--bound-p (symbol) "Return non-nil if SYMBOL is a variable or callable. This differs from `boundp' because we do not consider nil, t or :foo." (or (fboundp symbol) (helpful--variable-p symbol))) ;;;###autoload (defun helpful-symbol (symbol) "Show help for SYMBOL, a variable, function or macro. See also `helpful-callable' and `helpful-variable'." (interactive (list (helpful--read-symbol "Symbol: " #'helpful--bound-p))) (cond ((and (boundp symbol) (fboundp symbol)) (if (y-or-n-p (format "%s is a both a variable and a callable, show variable?" symbol)) (helpful-variable symbol) (helpful-callable symbol))) ((fboundp symbol) (helpful-callable symbol)) ((boundp symbol) (helpful-variable symbol)) (t (user-error "Not bound: %S" symbol)))) ;;;###autoload (defun helpful-variable (symbol) "Show help for variable named SYMBOL." (interactive (list (helpful--read-symbol "Variable: " #'helpful--variable-p))) (pop-to-buffer (helpful--buffer symbol nil)) (helpful-update)) ;;;###autoload (defun helpful-at-point () "Show help for the symbol at point." (interactive) (-if-let (symbol (symbol-at-point)) (helpful-symbol symbol) (user-error "There is no symbol at point."))) (defun helpful--imenu-index () "Return a list of headings in the current buffer, suitable for imenu." (let (headings) (goto-char (point-min)) (while (not (eobp)) (when (eq (get-text-property (point) 'face) 'helpful-heading) (push (cons (buffer-substring-no-properties (line-beginning-position) (line-end-position)) (line-beginning-position)) headings)) (forward-line)) (nreverse headings))) (define-derived-mode helpful-mode special-mode "Helpful" "Major mode for *Helpful* buffers." (add-hook 'xref-backend-functions #'elisp--xref-backend nil t) (setq imenu-create-index-function #'helpful--imenu-index) ;; Prevent imenu converting "Source Code" to "Source.Code". (setq-local imenu-space-replacement " ")) (defun helpful-visit-reference () "Go to the reference at point." (interactive) (let* ((path (get-text-property (point) 'helpful-path)) (pos (get-text-property (point) 'helpful-pos)) (pos-is-start (get-text-property (point) 'helpful-pos-is-start))) (when (and path pos) ;; If we're looking at a source excerpt, calculate the offset of ;; point, so we don't just go the start of the excerpt. (when pos-is-start (save-excursion (let ((offset 0)) (while (and (get-text-property (point) 'helpful-pos) (not (eobp))) (backward-char 1) (setq offset (1+ offset))) ;; On the last iteration we moved outside the source ;; excerpt, so we overcounted by one character. (setq offset (1- offset)) ;; Set POS so we go to exactly the place in the source ;; code where point was in the helpful excerpt. (setq pos (+ pos offset))))) (find-file path) (when (or (< pos (point-min)) (> pos (point-max))) (widen)) (goto-char pos)))) (defun helpful-kill-buffers () "Kill all `helpful-mode' buffers. See also `helpful-max-buffers'." (interactive) (dolist (buffer (buffer-list)) (when (eq (buffer-local-value 'major-mode buffer) 'helpful-mode) (kill-buffer buffer)))) (define-key helpful-mode-map (kbd "g") #'helpful-update) (define-key helpful-mode-map (kbd "RET") #'helpful-visit-reference) (define-key helpful-mode-map (kbd "TAB") #'forward-button) (define-key helpful-mode-map (kbd "") #'backward-button) (provide 'helpful) ;;; helpful.el ends here