dotemacs

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

elisp-refs.el (33224B)


      1 ;;; elisp-refs.el --- find callers of elisp functions or macros -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2016-2020  Wilfred Hughes <me@wilfred.me.uk>
      4 
      5 ;; Author: Wilfred Hughes <me@wilfred.me.uk>
      6 ;; Version: 1.5
      7 ;; Package-Version: 20220220.2305
      8 ;; Package-Commit: 8f84280997d8b233d66fb9958a34b46078c58b03
      9 ;; Keywords: lisp
     10 ;; Package-Requires: ((dash "2.12.0") (s "1.11.0"))
     11 
     12 ;; This program is free software; you can redistribute it and/or modify
     13 ;; it under the terms of the GNU General Public License as published by
     14 ;; the Free Software Foundation, either version 3 of the License, or
     15 ;; (at your option) any later version.
     16 
     17 ;; This program is distributed in the hope that it will be useful,
     18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     20 ;; GNU General Public License for more details.
     21 
     22 ;; You should have received a copy of the GNU General Public License
     23 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     24 
     25 ;;; Commentary:
     26 
     27 ;; elisp-refs.el is an Emacs package for finding references to
     28 ;; functions, macros or variables. Unlike a dumb text search,
     29 ;; elisp-refs.el actually parses the code, so it's never confused by
     30 ;; comments or `foo-bar' matching `foo'.
     31 ;;
     32 ;; See https://github.com/Wilfred/refs.el/blob/master/README.md for
     33 ;; more information.
     34 
     35 ;;; Code:
     36 
     37 (require 'dash)
     38 (require 's)
     39 (require 'format)
     40 (eval-when-compile (require 'cl-lib))
     41 
     42 ;;; Internal
     43 
     44 (defvar elisp-refs-verbose t)
     45 
     46 (defun elisp-refs--format-int (integer)
     47   "Format INTEGER as a string, with , separating thousands."
     48   (let ((number (abs integer))
     49         (parts nil))
     50     (while (> number 999)
     51       (push (format "%03d" (mod number 1000))
     52             parts)
     53       (setq number (/ number 1000)))
     54     (push (format "%d" number) parts)
     55     (concat
     56      (if (< integer 0) "-" "")
     57      (s-join "," parts))))
     58 
     59 (defsubst elisp-refs--start-pos (end-pos)
     60   "Find the start position of form ending at END-POS
     61 in the current buffer."
     62   (let ((parse-sexp-ignore-comments t))
     63     (scan-sexps end-pos -1)))
     64 
     65 (defun elisp-refs--sexp-positions (buffer start-pos end-pos)
     66   "Return a list of start and end positions of all the sexps
     67 between START-POS and END-POS (inclusive) in BUFFER.
     68 
     69 Positions exclude quote characters, so given 'foo or `foo, we
     70 report the position of the f.
     71 
     72 Not recursive, so we don't consider subelements of nested sexps."
     73   (let ((positions nil))
     74     (with-current-buffer buffer
     75       (condition-case _err
     76           (catch 'done
     77             (while t
     78               (let* ((sexp-end-pos (let ((parse-sexp-ignore-comments t))
     79                                      (scan-sexps start-pos 1))))
     80                 ;; If we've reached a sexp beyond the range requested,
     81                 ;; or if there are no sexps left, we're done.
     82                 (when (or (null sexp-end-pos) (> sexp-end-pos end-pos))
     83                   (throw 'done nil))
     84                 ;; Otherwise, this sexp is in the range requested.
     85                 (push (list (elisp-refs--start-pos sexp-end-pos) sexp-end-pos)
     86                       positions)
     87                 (setq start-pos sexp-end-pos))))
     88         ;; Terminate when we see "Containing expression ends prematurely"
     89         (scan-error nil)))
     90     (nreverse positions)))
     91 
     92 (defun elisp-refs--read-buffer-form ()
     93   "Read a form from the current buffer, starting at point.
     94 Returns a list:
     95 \(form form-start-pos form-end-pos symbol-positions read-start-pos)
     96 
     97 SYMBOL-POSITIONS are 0-indexed, relative to READ-START-POS."
     98   (let* ((read-with-symbol-positions t)
     99          (read-start-pos (point))
    100          (form (read (current-buffer)))
    101          (symbols (if (boundp 'read-symbol-positions-list)
    102                       read-symbol-positions-list
    103                     (read-positioning-symbols (current-buffer))))
    104          (end-pos (point))
    105          (start-pos (elisp-refs--start-pos end-pos)))
    106     (list form start-pos end-pos symbols read-start-pos)))
    107 
    108 (defvar elisp-refs--path nil
    109   "A buffer-local variable used by `elisp-refs--contents-buffer'.
    110 Internal implementation detail.")
    111 
    112 (defun elisp-refs--read-all-buffer-forms (buffer)
    113   "Read all the forms in BUFFER, along with their positions."
    114   (with-current-buffer buffer
    115     (goto-char (point-min))
    116     (let ((forms nil))
    117       (condition-case err
    118           (while t
    119             (push (elisp-refs--read-buffer-form) forms))
    120         (error
    121          (if (or (equal (car err) 'end-of-file)
    122                  ;; TODO: this shouldn't occur in valid elisp files,
    123                  ;; but it's happening in helm-utils.el.
    124                  (equal (car err) 'scan-error))
    125              ;; Reached end of file, we're done.
    126              (nreverse forms)
    127            ;; Some unexpected error, propagate.
    128            (error "Unexpected error whilst reading %s position %s: %s"
    129                   (abbreviate-file-name elisp-refs--path) (point) err)))))))
    130 
    131 (defun elisp-refs--proper-list-p (val)
    132   "Is VAL a proper list?"
    133   (if (fboundp 'format-proper-list-p)
    134       ;; Emacs stable.
    135       (with-no-warnings (format-proper-list-p val))
    136     ;; Function was renamed in Emacs master:
    137     ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=2fde6275b69fd113e78243790bf112bbdd2fe2bf
    138     (with-no-warnings (proper-list-p val))))
    139 
    140 (defun elisp-refs--walk (buffer form start-pos end-pos symbol match-p &optional path)
    141   "Walk FORM, a nested list, and return a list of sublists (with
    142 their positions) where MATCH-P returns t. FORM is traversed
    143 depth-first (pre-order traversal, left-to-right).
    144 
    145 MATCH-P is called with three arguments:
    146 \(SYMBOL CURRENT-FORM PATH).
    147 
    148 PATH is the first element of all the enclosing forms of
    149 CURRENT-FORM, innermost first, along with the index of the
    150 current form.
    151 
    152 For example if we are looking at h in (e f (g h)), PATH takes the
    153 value ((g . 1) (e . 2)).
    154 
    155 START-POS and END-POS should be the position of FORM within BUFFER."
    156   (cond
    157    ((funcall match-p symbol form path)
    158     ;; If this form matches, just return it, along with the position.
    159     (list (list form start-pos end-pos)))
    160    ;; Otherwise, recurse on the subforms.
    161    ((consp form)
    162     (let ((matches nil)
    163           ;; Find the positions of the subforms.
    164           (subforms-positions
    165            (if (eq (car-safe form) '\`)
    166                ;; Kludge: `elisp-refs--sexp-positions' excludes the ` when
    167                ;; calculating positions. So, to find the inner
    168                ;; positions when walking from `(...) to (...), we
    169                ;; don't need to increment the start position.
    170                (cons nil (elisp-refs--sexp-positions buffer start-pos end-pos))
    171              ;; Calculate the positions after the opening paren.
    172              (elisp-refs--sexp-positions buffer (1+ start-pos) end-pos))))
    173       ;; For each subform, recurse if it's a list, or a matching symbol.
    174       (--each (-zip form subforms-positions)
    175         (-let [(subform subform-start subform-end) it]
    176           (when (or
    177                  (and (consp subform) (elisp-refs--proper-list-p subform))
    178                  (and (symbolp subform) (eq subform symbol)))
    179             (-when-let (subform-matches
    180                         (elisp-refs--walk
    181                          buffer subform
    182                          subform-start subform-end
    183                          symbol match-p
    184                          (cons (cons (car-safe form) it-index) path)))
    185               (push subform-matches matches)))))
    186 
    187       ;; Concat the results from all the subforms.
    188       (apply #'append (nreverse matches))))))
    189 
    190 ;; TODO: condition-case (condition-case ... (error ...)) is not a call
    191 ;; TODO: (cl-destructuring-bind (foo &rest bar) ...) is not a call
    192 ;; TODO: letf, cl-letf, -let, -let*
    193 (defun elisp-refs--function-p (symbol form path)
    194   "Return t if FORM looks like a function call to SYMBOL."
    195   (cond
    196    ((not (consp form))
    197     nil)
    198    ;; Ignore (defun _ (SYMBOL ...) ...)
    199    ((or (equal (car path) '(defsubst . 2))
    200         (equal (car path) '(defun . 2))
    201         (equal (car path) '(defmacro . 2))
    202         (equal (car path) '(cl-defun . 2)))
    203     nil)
    204    ;; Ignore (lambda (SYMBOL ...) ...)
    205    ((equal (car path) '(lambda . 1))
    206     nil)
    207    ;; Ignore (let (SYMBOL ...) ...)
    208    ;; and (let* (SYMBOL ...) ...)
    209    ((or
    210      (equal (car path) '(let . 1))
    211      (equal (car path) '(let* . 1)))
    212     nil)
    213    ;; Ignore (let ((SYMBOL ...)) ...)
    214    ((or
    215      (equal (cl-second path) '(let . 1))
    216      (equal (cl-second path) '(let* . 1)))
    217     nil)
    218    ;; Ignore (declare-function NAME  (ARGS...))
    219    ((equal (car path) '(declare-function . 3))
    220     nil)
    221    ;; (SYMBOL ...)
    222    ((eq (car form) symbol)
    223     t)
    224    ;; (foo ... #'SYMBOL ...)
    225    ((--any-p (equal it (list 'function symbol)) form)
    226     t)
    227    ;; (funcall 'SYMBOL ...)
    228    ((and (eq (car form) 'funcall)
    229          (equal `',symbol (cl-second form)))
    230     t)
    231    ;; (apply 'SYMBOL ...)
    232    ((and (eq (car form) 'apply)
    233          (equal `',symbol (cl-second form)))
    234     t)))
    235 
    236 (defun elisp-refs--macro-p (symbol form path)
    237   "Return t if FORM looks like a macro call to SYMBOL."
    238   (cond
    239    ((not (consp form))
    240     nil)
    241    ;; Ignore (defun _ (SYMBOL ...) ...)
    242    ((or (equal (car path) '(defsubst . 2))
    243         (equal (car path) '(defun . 2))
    244         (equal (car path) '(defmacro . 2)))
    245     nil)
    246    ;; Ignore (lambda (SYMBOL ...) ...)
    247    ((equal (car path) '(lambda . 1))
    248     nil)
    249    ;; Ignore (let (SYMBOL ...) ...)
    250    ;; and (let* (SYMBOL ...) ...)
    251    ((or
    252      (equal (car path) '(let . 1))
    253      (equal (car path) '(let* . 1)))
    254     nil)
    255    ;; Ignore (let ((SYMBOL ...)) ...)
    256    ((or
    257      (equal (cl-second path) '(let . 1))
    258      (equal (cl-second path) '(let* . 1)))
    259     nil)
    260    ;; (SYMBOL ...)
    261    ((eq (car form) symbol)
    262     t)))
    263 
    264 ;; Looking for a special form is exactly the same as looking for a
    265 ;; macro.
    266 (defalias 'elisp-refs--special-p 'elisp-refs--macro-p)
    267 
    268 (defun elisp-refs--variable-p (symbol form path)
    269   "Return t if this looks like a variable reference to SYMBOL.
    270 We consider parameters to be variables too."
    271   (cond
    272    ((consp form)
    273     nil)
    274    ;; Ignore (defun _ (SYMBOL ...) ...)
    275    ((or (equal (car path) '(defsubst . 1))
    276         (equal (car path) '(defun . 1))
    277         (equal (car path) '(defmacro . 1))
    278         (equal (car path) '(cl-defun . 1)))
    279     nil)
    280    ;; (let (SYMBOL ...) ...) is a variable, not a function call.
    281    ((or
    282      (equal (cl-second path) '(let . 1))
    283      (equal (cl-second path) '(let* . 1)))
    284     t)
    285    ;; (lambda (SYMBOL ...) ...) is a variable
    286    ((equal (cl-second path) '(lambda . 1))
    287     t)
    288    ;; (let ((SYMBOL ...)) ...) is also a variable.
    289    ((or
    290      (equal (cl-third path) '(let . 1))
    291      (equal (cl-third path) '(let* . 1)))
    292     t)
    293    ;; Ignore (SYMBOL ...) otherwise, we assume it's a function/macro
    294    ;; call.
    295    ((equal (car path) (cons symbol 0))
    296     nil)
    297    ((eq form symbol)
    298     t)))
    299 
    300 ;; TODO: benchmark building a list with `push' rather than using
    301 ;; mapcat.
    302 (defun elisp-refs--read-and-find (buffer symbol match-p)
    303   "Read all the forms in BUFFER, and return a list of all forms that
    304 contain SYMBOL where MATCH-P returns t.
    305 
    306 For every matching form found, we return the form itself along
    307 with its start and end position."
    308   (-non-nil
    309    (--mapcat
    310     (-let [(form start-pos end-pos symbol-positions _read-start-pos) it]
    311       ;; Optimisation: don't bother walking a form if contains no
    312       ;; references to the symbol we're looking for.
    313       (when (assq symbol symbol-positions)
    314         (elisp-refs--walk buffer form start-pos end-pos symbol match-p)))
    315     (elisp-refs--read-all-buffer-forms buffer))))
    316 
    317 (defun elisp-refs--read-and-find-symbol (buffer symbol)
    318   "Read all the forms in BUFFER, and return a list of all
    319 positions of SYMBOL."
    320   (-non-nil
    321    (--mapcat
    322     (-let [(_ _ _ symbol-positions read-start-pos) it]
    323       (--map
    324        (-let [(sym . offset) it]
    325          (when (eq sym symbol)
    326            (-let* ((start-pos (+ read-start-pos offset))
    327                    (end-pos (+ start-pos (length (symbol-name sym)))))
    328              (list sym start-pos end-pos))))
    329        symbol-positions))
    330 
    331     (elisp-refs--read-all-buffer-forms buffer))))
    332 
    333 (defun elisp-refs--filter-obarray (pred)
    334   "Return a list of all the items in `obarray' where PRED returns t."
    335   (let (symbols)
    336     (mapatoms (lambda (symbol)
    337                 (when (and (funcall pred symbol)
    338                            (not (equal (symbol-name symbol) "")))
    339                   (push symbol symbols))))
    340     symbols))
    341 
    342 (defun elisp-refs--loaded-paths ()
    343   "Return a list of all files that have been loaded in Emacs.
    344 Where the file was a .elc, return the path to the .el file instead."
    345   (let ((elc-paths (-non-nil (mapcar #'-first-item load-history))))
    346     (-non-nil
    347      (--map
    348       (let ((el-name (format "%s.el" (file-name-sans-extension it)))
    349             (el-gz-name (format "%s.el.gz" (file-name-sans-extension it))))
    350         (cond ((file-exists-p el-name) el-name)
    351               ((file-exists-p el-gz-name) el-gz-name)
    352               ;; Ignore files where we can't find a .el file.
    353               (t nil)))
    354       elc-paths))))
    355 
    356 (defun elisp-refs--contents-buffer (path)
    357   "Read PATH into a disposable buffer, and return it.
    358 Works around the fact that Emacs won't allow multiple buffers
    359 visiting the same file."
    360   (let ((fresh-buffer (generate-new-buffer (format " *refs-%s*" path)))
    361         ;; Be defensive against users overriding encoding
    362         ;; configurations (Helpful bugs #75 and #147).
    363         (coding-system-for-read nil)
    364         (file-name-handler-alist
    365          '(("\\(?:\\.dz\\|\\.txz\\|\\.xz\\|\\.lzma\\|\\.lz\\|\\.g?z\\|\\.\\(?:tgz\\|svgz\\|sifz\\)\\|\\.tbz2?\\|\\.bz2\\|\\.Z\\)\\(?:~\\|\\.~[-[:alnum:]:#@^._]+\\(?:~[[:digit:]]+\\)?~\\)?\\'" .
    366             jka-compr-handler)
    367            ("\\`/:" . file-name-non-special))))
    368     (with-current-buffer fresh-buffer
    369       (setq-local elisp-refs--path path)
    370       (insert-file-contents path)
    371       ;; We don't enable emacs-lisp-mode because it slows down this
    372       ;; function significantly. We just need the syntax table for
    373       ;; scan-sexps to do the right thing with comments.
    374       (set-syntax-table emacs-lisp-mode-syntax-table))
    375     fresh-buffer))
    376 
    377 (defvar elisp-refs--highlighting-buffer
    378   nil
    379   "A temporary buffer used for highlighting.
    380 Since `elisp-refs--syntax-highlight' is a hot function, we
    381 don't want to create lots of temporary buffers.")
    382 
    383 (defun elisp-refs--syntax-highlight (str)
    384   "Apply font-lock properties to a string STR of Emacs lisp code."
    385   ;; Ensure we have a highlighting buffer to work with.
    386   (unless (and elisp-refs--highlighting-buffer
    387                (buffer-live-p elisp-refs--highlighting-buffer))
    388     (setq elisp-refs--highlighting-buffer
    389           (generate-new-buffer " *refs-highlighting*"))
    390     (with-current-buffer elisp-refs--highlighting-buffer
    391       (delay-mode-hooks (emacs-lisp-mode))))
    392 
    393   (with-current-buffer elisp-refs--highlighting-buffer
    394     (erase-buffer)
    395     (insert str)
    396     (if (fboundp 'font-lock-ensure)
    397         (font-lock-ensure)
    398       (with-no-warnings
    399         (font-lock-fontify-buffer)))
    400     (buffer-string)))
    401 
    402 (defun elisp-refs--replace-tabs (string)
    403   "Replace tabs in STRING with spaces."
    404   ;; This is important for unindenting, as we may unindent by less
    405   ;; than one whole tab.
    406   (s-replace "\t" (s-repeat tab-width " ") string))
    407 
    408 (defun elisp-refs--lines (string)
    409   "Return a list of all the lines in STRING.
    410 'a\nb' -> ('a\n' 'b')"
    411   (let ((lines nil))
    412     (while (> (length string) 0)
    413       (let ((index (s-index-of "\n" string)))
    414         (if index
    415             (progn
    416               (push (substring string 0 (1+ index)) lines)
    417               (setq string (substring string (1+ index))))
    418           (push string lines)
    419           (setq string ""))))
    420     (nreverse lines)))
    421 
    422 (defun elisp-refs--map-lines (string fn)
    423   "Execute FN for each line in string, and join the result together."
    424   (let ((result nil))
    425     (dolist (line (elisp-refs--lines string))
    426       (push (funcall fn line) result))
    427     (apply #'concat (nreverse result))))
    428 
    429 (defun elisp-refs--unindent-rigidly (string)
    430   "Given an indented STRING, unindent rigidly until
    431 at least one line has no indent.
    432 
    433 STRING should have a 'elisp-refs-start-pos property. The returned
    434 string will have this property updated to reflect the unindent."
    435   (let* ((lines (s-lines string))
    436          ;; Get the leading whitespace for each line.
    437          (indents (--map (car (s-match (rx bos (+ whitespace)) it))
    438                          lines))
    439          (min-indent (-min (--map (length it) indents))))
    440     (propertize
    441      (elisp-refs--map-lines
    442       string
    443       (lambda (line) (substring line min-indent)))
    444      'elisp-refs-unindented min-indent)))
    445 
    446 (defun elisp-refs--containing-lines (buffer start-pos end-pos)
    447   "Return a string, all the lines in BUFFER that are between
    448 START-POS and END-POS (inclusive).
    449 
    450 For the characters that are between START-POS and END-POS,
    451 propertize them."
    452   (let (expanded-start-pos expanded-end-pos)
    453     (with-current-buffer buffer
    454       ;; Expand START-POS and END-POS to line boundaries.
    455       (goto-char start-pos)
    456       (beginning-of-line)
    457       (setq expanded-start-pos (point))
    458       (goto-char end-pos)
    459       (end-of-line)
    460       (setq expanded-end-pos (point))
    461 
    462       ;; Extract the rest of the line before and after the section we're interested in.
    463       (let* ((before-match (buffer-substring expanded-start-pos start-pos))
    464              (after-match (buffer-substring end-pos expanded-end-pos))
    465              ;; Concat the extra text with the actual match, ensuring we
    466              ;; highlight the match as code, but highlight the rest as as
    467              ;; comments.
    468              (text (concat
    469                     (propertize before-match
    470                                 'face 'font-lock-comment-face)
    471                     (elisp-refs--syntax-highlight (buffer-substring start-pos end-pos))
    472                     (propertize after-match
    473                                 'face 'font-lock-comment-face))))
    474         (-> text
    475           (elisp-refs--replace-tabs)
    476           (elisp-refs--unindent-rigidly)
    477           (propertize 'elisp-refs-start-pos expanded-start-pos
    478                       'elisp-refs-path elisp-refs--path))))))
    479 
    480 (defun elisp-refs--find-file (button)
    481   "Open the file referenced by BUTTON."
    482   (find-file (button-get button 'path))
    483   (goto-char (point-min)))
    484 
    485 (define-button-type 'elisp-refs-path-button
    486   'action 'elisp-refs--find-file
    487   'follow-link t
    488   'help-echo "Open file")
    489 
    490 (defun elisp-refs--path-button (path)
    491   "Return a button that navigates to PATH."
    492   (with-temp-buffer
    493     (insert-text-button
    494      (abbreviate-file-name path)
    495      :type 'elisp-refs-path-button
    496      'path path)
    497     (buffer-string)))
    498 
    499 (defun elisp-refs--describe (button)
    500   "Show *Help* for the symbol referenced by BUTTON."
    501   (let ((symbol (button-get button 'symbol))
    502         (kind (button-get button 'kind)))
    503     (cond ((eq kind 'symbol)
    504            (describe-symbol symbol))
    505           ((eq kind 'variable)
    506            (describe-variable symbol))
    507           (t
    508            ;; Emacs uses `describe-function' for functions, macros and
    509            ;; special forms.
    510            (describe-function symbol)))))
    511 
    512 (define-button-type 'elisp-refs-describe-button
    513   'action 'elisp-refs--describe
    514   'follow-link t
    515   'help-echo "Describe")
    516 
    517 (defun elisp-refs--describe-button (symbol kind)
    518   "Return a button that shows *Help* for SYMBOL.
    519 KIND should be 'function, 'macro, 'variable, 'special or 'symbol."
    520   (with-temp-buffer
    521     (insert (symbol-name kind) " ")
    522     (insert-text-button
    523      (symbol-name symbol)
    524      :type 'elisp-refs-describe-button
    525      'symbol symbol
    526      'kind kind)
    527     (buffer-string)))
    528 
    529 (defun elisp-refs--pluralize (number thing)
    530   "Human-friendly description of NUMBER occurrences of THING."
    531   (format "%s %s%s"
    532           (elisp-refs--format-int number)
    533           thing
    534           (if (equal number 1) "" "s")))
    535 
    536 (defun elisp-refs--format-count (symbol ref-count file-count
    537                                         searched-file-count prefix)
    538   (let* ((file-str (if (zerop file-count)
    539                        ""
    540                      (format " in %s" (elisp-refs--pluralize file-count "file"))))
    541          (found-str (format "Found %s to %s%s."
    542                             (elisp-refs--pluralize ref-count "reference")
    543                             symbol
    544                             file-str))
    545          (searched-str (if prefix
    546                            (format "Searched %s in %s."
    547                                    (elisp-refs--pluralize searched-file-count "loaded file")
    548                                    (elisp-refs--path-button (file-name-as-directory prefix)))
    549                          (format "Searched all %s loaded in Emacs."
    550                                  (elisp-refs--pluralize searched-file-count "file")))))
    551     (s-word-wrap 70 (format "%s %s" found-str searched-str))))
    552 
    553 ;; TODO: if we have multiple matches on one line, we repeatedly show
    554 ;; that line. That's slightly confusing.
    555 (defun elisp-refs--show-results (symbol description results
    556                                         searched-file-count prefix)
    557   "Given a RESULTS list where each element takes the form \(forms . buffer\),
    558 render a friendly results buffer."
    559   (let ((buf (get-buffer-create (format "*refs: %s*" symbol))))
    560     (switch-to-buffer buf)
    561     (let ((inhibit-read-only t))
    562       (erase-buffer)
    563       (save-excursion
    564         ;; Insert the header.
    565         (insert
    566          (elisp-refs--format-count
    567           description
    568           (-sum (--map (length (car it)) results))
    569           (length results)
    570           searched-file-count
    571           prefix)
    572          "\n\n")
    573         ;; Insert the results.
    574         (--each results
    575           (-let* (((forms . buf) it)
    576                   (path (with-current-buffer buf elisp-refs--path)))
    577             (insert
    578              (propertize "File: " 'face 'bold)
    579              (elisp-refs--path-button path) "\n")
    580             (--each forms
    581               (-let [(_ start-pos end-pos) it]
    582                 (insert (elisp-refs--containing-lines buf start-pos end-pos)
    583                         "\n")))
    584             (insert "\n")))
    585         ;; Prepare the buffer for the user.
    586         (elisp-refs-mode)))
    587     ;; Cleanup buffers created when highlighting results.
    588     (when elisp-refs--highlighting-buffer
    589       (kill-buffer elisp-refs--highlighting-buffer))))
    590 
    591 (defun elisp-refs--loaded-bufs ()
    592   "Return a list of open buffers, one for each path in `load-path'."
    593   (mapcar #'elisp-refs--contents-buffer (elisp-refs--loaded-paths)))
    594 
    595 (defun elisp-refs--search-1 (bufs match-fn)
    596   "Call MATCH-FN on each buffer in BUFS, reporting progress
    597 and accumulating results.
    598 
    599 BUFS should be disposable: we make no effort to preserve their
    600 state during searching.
    601 
    602 MATCH-FN should return a list where each element takes the form:
    603 \(form start-pos end-pos)."
    604   (let* (;; Our benchmark suggests we spend a lot of time in GC, and
    605          ;; performance improves if we GC less frequently.
    606          (gc-cons-percentage 0.8)
    607          (total-bufs (length bufs)))
    608     (let ((searched 0)
    609           (forms-and-bufs nil))
    610       (dolist (buf bufs)
    611         (let* ((matching-forms (funcall match-fn buf)))
    612           ;; If there were any matches in this buffer, push the
    613           ;; matches along with the buffer into our results
    614           ;; list.
    615           (when matching-forms
    616             (push (cons matching-forms buf) forms-and-bufs))
    617           ;; Give feedback to the user on our progress, because
    618           ;; searching takes several seconds.
    619           (when (and (zerop (mod searched 10))
    620                      elisp-refs-verbose)
    621             (message "Searched %s/%s files" searched total-bufs))
    622           (cl-incf searched)))
    623       (when elisp-refs-verbose
    624         (message "Searched %s/%s files" total-bufs total-bufs))
    625       forms-and-bufs)))
    626 
    627 (defun elisp-refs--search (symbol description match-fn &optional path-prefix)
    628   "Find references to SYMBOL in all loaded files; call MATCH-FN on each buffer.
    629 When PATH-PREFIX, limit to loaded files whose path starts with that prefix.
    630 
    631 Display the results in a hyperlinked buffer.
    632 
    633 MATCH-FN should return a list where each element takes the form:
    634 \(form start-pos end-pos)."
    635   (let* ((loaded-paths (elisp-refs--loaded-paths))
    636          (matching-paths (if path-prefix
    637                              (--filter (s-starts-with? path-prefix it) loaded-paths)
    638                            loaded-paths))
    639          (loaded-src-bufs (mapcar #'elisp-refs--contents-buffer matching-paths)))
    640     ;; Use unwind-protect to ensure we always cleanup temporary
    641     ;; buffers, even if the user hits C-g.
    642     (unwind-protect
    643         (progn
    644           (let ((forms-and-bufs
    645                  (elisp-refs--search-1 loaded-src-bufs match-fn)))
    646             (elisp-refs--show-results symbol description forms-and-bufs
    647                                       (length loaded-src-bufs) path-prefix)))
    648       ;; Clean up temporary buffers.
    649       (--each loaded-src-bufs (kill-buffer it)))))
    650 
    651 (defun elisp-refs--completing-read-symbol (prompt &optional filter)
    652   "Read an interned symbol from the minibuffer,
    653 defaulting to the symbol at point. PROMPT is the string to prompt
    654 with.
    655 
    656 If FILTER is given, only offer symbols where (FILTER sym) returns
    657 t."
    658   (let ((filter (or filter (lambda (_) t))))
    659     (read
    660      (completing-read prompt
    661                       (elisp-refs--filter-obarray filter)
    662                       nil nil nil nil
    663                       (-if-let (sym (thing-at-point 'symbol))
    664                           (when (funcall filter (read sym))
    665                             sym))))))
    666 
    667 ;;; Commands
    668 
    669 ;;;###autoload
    670 (defun elisp-refs-function (symbol &optional path-prefix)
    671   "Display all the references to function SYMBOL, in all loaded
    672 elisp files.
    673 
    674 If called with a prefix, prompt for a directory to limit the search.
    675 
    676 This searches for functions, not macros. For that, see
    677 `elisp-refs-macro'."
    678   (interactive
    679    (list (elisp-refs--completing-read-symbol "Function: " #'functionp)
    680          (when current-prefix-arg
    681            (read-directory-name "Limit search to loaded files in: "))))
    682   (when (not (functionp symbol))
    683     (if (macrop symbol)
    684         (user-error "%s is a macro. Did you mean elisp-refs-macro?"
    685                     symbol)
    686       (user-error "%s is not a function. Did you mean elisp-refs-symbol?"
    687                   symbol)))
    688   (elisp-refs--search symbol
    689                       (elisp-refs--describe-button symbol 'function)
    690                       (lambda (buf)
    691                         (elisp-refs--read-and-find buf symbol #'elisp-refs--function-p))
    692                       path-prefix))
    693 
    694 ;;;###autoload
    695 (defun elisp-refs-macro (symbol &optional path-prefix)
    696   "Display all the references to macro SYMBOL, in all loaded
    697 elisp files.
    698 
    699 If called with a prefix, prompt for a directory to limit the search.
    700 
    701 This searches for macros, not functions. For that, see
    702 `elisp-refs-function'."
    703   (interactive
    704    (list (elisp-refs--completing-read-symbol "Macro: " #'macrop)
    705          (when current-prefix-arg
    706            (read-directory-name "Limit search to loaded files in: "))))
    707   (when (not (macrop symbol))
    708     (if (functionp symbol)
    709         (user-error "%s is a function. Did you mean elisp-refs-function?"
    710                     symbol)
    711       (user-error "%s is not a function. Did you mean elisp-refs-symbol?"
    712                   symbol)))
    713   (elisp-refs--search symbol
    714                       (elisp-refs--describe-button symbol 'macro)
    715                       (lambda (buf)
    716                         (elisp-refs--read-and-find buf symbol #'elisp-refs--macro-p))
    717                       path-prefix))
    718 
    719 ;;;###autoload
    720 (defun elisp-refs-special (symbol &optional path-prefix)
    721   "Display all the references to special form SYMBOL, in all loaded
    722 elisp files.
    723 
    724 If called with a prefix, prompt for a directory to limit the search."
    725   (interactive
    726    (list (elisp-refs--completing-read-symbol "Special form: " #'special-form-p)
    727          (when current-prefix-arg
    728            (read-directory-name "Limit search to loaded files in: "))))
    729   (elisp-refs--search symbol
    730                       (elisp-refs--describe-button symbol 'special-form)
    731                       (lambda (buf)
    732                         (elisp-refs--read-and-find buf symbol #'elisp-refs--special-p))
    733                       path-prefix))
    734 
    735 ;;;###autoload
    736 (defun elisp-refs-variable (symbol &optional path-prefix)
    737   "Display all the references to variable SYMBOL, in all loaded
    738 elisp files.
    739 
    740 If called with a prefix, prompt for a directory to limit the search."
    741   (interactive
    742    ;; This is awkward. We don't want to just offer defvar variables,
    743    ;; because then we can't search for code which uses `let' to bind
    744    ;; symbols. There doesn't seem to be a good way to only offer
    745    ;; variables that have been bound at some point.
    746    (list (elisp-refs--completing-read-symbol "Variable: " )
    747          (when current-prefix-arg
    748            (read-directory-name "Limit search to loaded files in: "))))
    749   (elisp-refs--search symbol
    750                       (elisp-refs--describe-button symbol 'variable)
    751                       (lambda (buf)
    752                         (elisp-refs--read-and-find buf symbol #'elisp-refs--variable-p))
    753                       path-prefix))
    754 
    755 ;;;###autoload
    756 (defun elisp-refs-symbol (symbol &optional path-prefix)
    757   "Display all the references to SYMBOL in all loaded elisp files.
    758 
    759 If called with a prefix, prompt for a directory to limit the
    760 search."
    761   (interactive
    762    (list (elisp-refs--completing-read-symbol "Symbol: " )
    763          (when current-prefix-arg
    764            (read-directory-name "Limit search to loaded files in: "))))
    765   (elisp-refs--search symbol
    766                       (elisp-refs--describe-button symbol 'symbol)
    767                       (lambda (buf)
    768                         (elisp-refs--read-and-find-symbol buf symbol))
    769                       path-prefix))
    770 
    771 ;;; Mode
    772 
    773 (defvar elisp-refs-mode-map
    774   (let ((map (make-sparse-keymap)))
    775     ;; TODO: it would be nice for TAB to navigate to file buttons too,
    776     ;; like *Help* does.
    777     (set-keymap-parent map special-mode-map)
    778     (define-key map (kbd "<tab>") #'elisp-refs-next-match)
    779     (define-key map (kbd "<backtab>") #'elisp-refs-prev-match)
    780     (define-key map (kbd "n") #'elisp-refs-next-match)
    781     (define-key map (kbd "p") #'elisp-refs-prev-match)
    782     (define-key map (kbd "q") #'kill-this-buffer)
    783     (define-key map (kbd "RET") #'elisp-refs-visit-match)
    784     map)
    785   "Keymap for `elisp-refs-mode'.")
    786 
    787 (define-derived-mode elisp-refs-mode special-mode "Refs"
    788   "Major mode for refs results buffers.")
    789 
    790 (defun elisp-refs-visit-match ()
    791   "Go to the search result at point."
    792   (interactive)
    793   (let* ((path (get-text-property (point) 'elisp-refs-path))
    794          (pos (get-text-property (point) 'elisp-refs-start-pos))
    795          (unindent (get-text-property (point) 'elisp-refs-unindented))
    796          (column-offset (current-column))
    797          (line-offset -1))
    798     (when (null path)
    799       (user-error "No match here"))
    800 
    801     ;; If point is not on the first line of the match, work out how
    802     ;; far away the first line is.
    803     (save-excursion
    804       (while (equal pos (get-text-property (point) 'elisp-refs-start-pos))
    805         (forward-line -1)
    806         (cl-incf line-offset)))
    807 
    808     (find-file path)
    809     (goto-char pos)
    810     ;; Move point so we're on the same char in the buffer that we were
    811     ;; on in the results buffer.
    812     (forward-line line-offset)
    813     (beginning-of-line)
    814     (let ((target-offset (+ column-offset unindent))
    815           (i 0))
    816       (while (< i target-offset)
    817         (if (looking-at "\t")
    818             (cl-incf i tab-width)
    819           (cl-incf i))
    820         (forward-char 1)))))
    821 
    822 (defun elisp-refs--move-to-match (direction)
    823   "Move point one match forwards.
    824 If DIRECTION is -1, moves backwards instead."
    825   (let* ((start-pos (point))
    826          (match-pos (get-text-property start-pos 'elisp-refs-start-pos))
    827          current-match-pos)
    828     (condition-case _err
    829         (progn
    830           ;; Move forward/backwards until we're on the next/previous match.
    831           (catch 'done
    832             (while t
    833               (setq current-match-pos
    834                     (get-text-property (point) 'elisp-refs-start-pos))
    835               (when (and current-match-pos
    836                          (not (equal match-pos current-match-pos)))
    837                 (throw 'done nil))
    838               (forward-char direction)))
    839           ;; Move to the beginning of that match.
    840           (while (equal (get-text-property (point) 'elisp-refs-start-pos)
    841                         (get-text-property (1- (point)) 'elisp-refs-start-pos))
    842             (forward-char -1))
    843           ;; Move forward until we're on the first char of match within that
    844           ;; line.
    845           (while (or
    846                   (looking-at " ")
    847                   (eq (get-text-property (point) 'face)
    848                       'font-lock-comment-face))
    849             (forward-char 1)))
    850       ;; If we're at the last result, don't move point.
    851       (end-of-buffer
    852        (progn
    853          (goto-char start-pos)
    854          (signal 'end-of-buffer nil))))))
    855 
    856 (defun elisp-refs-prev-match ()
    857   "Move to the previous search result in the Refs buffer."
    858   (interactive)
    859   (elisp-refs--move-to-match -1))
    860 
    861 (defun elisp-refs-next-match ()
    862   "Move to the next search result in the Refs buffer."
    863   (interactive)
    864   (elisp-refs--move-to-match 1))
    865 
    866 (provide 'elisp-refs)
    867 ;;; elisp-refs.el ends here