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