├── .ert-runner ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cask ├── Makefile ├── README.md ├── bench.sh ├── elisp-refs-bench.el ├── elisp-refs.el ├── refs_filtered.png ├── refs_screenshot.png └── test ├── elisp-refs-unit-test.el └── test-helper.el /.ert-runner: -------------------------------------------------------------------------------- 1 | --reporter ert 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run test suite 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | emacs_version: 11 | - '25.1' 12 | - '26.1' 13 | - '27.1' 14 | - '28.1' 15 | - snapshot 16 | 17 | steps: 18 | - uses: purcell/setup-emacs@master 19 | with: 20 | version: ${{ matrix.emacs_version }} 21 | - uses: conao3/setup-cask@master 22 | 23 | - uses: actions/checkout@v2 24 | 25 | - name: Install dependencies with Cask 26 | run: cask install 27 | 28 | - name: Run tests 29 | env: 30 | COVERALLS_FLAG_NAME: Emacs ${{ matrix.emacs_version }} 31 | COVERALLS_PARALLEL: 1 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | # Run tests under both interpreted and compiled elisp. 34 | run: cask exec ert-runner 35 | - name: Run tests (compiled elisp) 36 | env: 37 | COVERALLS_FLAG_NAME: Emacs ${{ matrix.emacs_version }} compiled 38 | COVERALLS_PARALLEL: 1 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | # Run tests under both interpreted and compiled elisp. 41 | run: | 42 | cask build 43 | cask exec ert-runner 44 | 45 | finalize: 46 | runs-on: ubuntu-latest 47 | if: always() 48 | needs: test 49 | steps: 50 | - run: curl "https://coveralls.io/webhook?repo_name=$GITHUB_REPOSITORY&repo_token=${{ secrets.GITHUB_TOKEN }}" -d "payload[build_num]=$GITHUB_RUN_NUMBER&payload[status]=done" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | *.elc 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.6 2 | 3 | ## v1.5 (tagged 9 March 2023) 4 | 5 | Fixed Emacs 29, which would previously crash or not find symbols. 6 | 7 | ## v1.4 (tagged 28 January 2022) 8 | 9 | Fixed an issue where elisp-refs would kill its own results buffer if 10 | no results were found. 11 | 12 | Fixed an issue when running elisp-refs on Emacs master, which renamed 13 | `format-proper-list-p` to `proper-list-p` (and moved it to subr.el). 14 | 15 | Fixed an issue where the results buffer showed the wrong lines if 16 | comments contained parentheses. 17 | 18 | Fixed an issue where arguments in `declare-function` were confused 19 | with a function call. 20 | 21 | Fixed an error when pressing RET on a line that didn't have a search 22 | result. 23 | 24 | ## v1.3 25 | 26 | * Refs buffers now have names of the form `*refs: foo*`. 27 | * Fixed an issue with a dependency on a loop.el version that doesn't 28 | exist. 29 | 30 | ## v1.2 31 | 32 | * You can now filter search results to a directory. This is useful 33 | when working on large elisp codebases, and it's faster too. 34 | * Results buffers now include a link to describe the thing being 35 | searched for. 36 | 37 | ## v1.1 38 | 39 | * Rebranded to elisp-refs. 40 | * Commands are now autoloaded. 41 | * Added examples to the readme of cases that we can't detect. 42 | * Sharp-quoted function references are now highlighted with context. 43 | * Give more feedback on first run, when we're decompressing .el.gz 44 | files. 45 | * Searches now default to the symbol at point. 46 | 47 | ## v1.0 48 | 49 | Initial release. 50 | 51 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source melpa) 2 | 3 | (package-file "elisp-refs.el") 4 | 5 | (development 6 | (depends-on "shut-up") 7 | (depends-on "ert-runner") 8 | (depends-on "undercover") 9 | (depends-on "f")) 10 | 11 | 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CASK ?= cask 2 | EMACS ?= emacs 3 | 4 | all: test 5 | 6 | test: unit 7 | 8 | unit: 9 | ${CASK} exec ert-runner 10 | 11 | install: 12 | ${CASK} install 13 | 14 | bench: 15 | ./bench.sh 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elisp-refs 2 | [![Coverage Status](https://coveralls.io/repos/github/Wilfred/elisp-refs/badge.svg?branch=master)](https://coveralls.io/github/Wilfred/elisp-refs?branch=master) 3 | [![MELPA](http://melpa.org/packages/elisp-refs-badge.svg)](http://melpa.org/#/elisp-refs) 4 | 5 | elisp-refs is an intelligent code search for Emacs lisp. 6 | 7 | It can find references to functions, macros or variables. Unlike a 8 | dumb text search, elisp-refs actually parses the code, so it's never 9 | confused by comments or variables with the same name as functions. 10 | 11 | ![screenshot](refs_screenshot.png) 12 | 13 | This is particularly useful for finding all the places a function is 14 | used, or finding examples of usage. 15 | 16 | Interested readers may enjoy my blog post: 17 | [Searching A Million Lines Of Lisp](http://www.wilfred.me.uk/blog/2016/09/30/searching-a-million-lines-of-lisp/). 18 | 19 | ## Installation 20 | 21 | Install from MELPA (recommended) or just add elisp-refs to your `load-path`. 22 | 23 | ## Commands available 24 | 25 | * `elisp-refs-function` (find function calls) 26 | * `elisp-refs-macro` (find macro calls) 27 | * `elisp-refs-variable` (find variable references) 28 | * `elisp-refs-special` (find special form calls) 29 | * `elisp-refs-symbol` (find all references to a symbol) 30 | 31 | These command search all the files currently loaded in your Emacs 32 | instance. 33 | 34 | If called with a prefix, you can limit search results to specific 35 | directories. For example: 36 | 37 | `C-u M-x elisp-refs-macro RET pcase RET ~/.emacs.d/elpa/magit-20160927.510 RET` 38 | 39 | will search for uses of `pcase` in magit: 40 | 41 | ![filtering screenshot](refs_filtered.png) 42 | 43 | ## Semantic analysis 44 | 45 | elisp-refs has *street smarts*: given `(defun foo (bar) (baz))`, it 46 | understands that `bar` is a variable and `baz` is a function. 47 | 48 | elisp-refs understands the following forms: 49 | 50 | * `defun` `defsubst` `defmacro` `cl-defun` 51 | * `lambda` 52 | * `let` `let*` 53 | * `funcall` `apply` 54 | * sharp quoted expressions (e.g. `#'some-func`) 55 | 56 | ## Limitations 57 | 58 | elisp-refs understands elisp special forms, and a few common 59 | macros. However, it **cannot understand arbitrary macros**. 60 | 61 | Therefore elisp-refs will assume that `(other-macro (foo bar))` is a 62 | function call to `foo`. If this is incorrect, you may wish to use the 63 | command `elisp-refs-symbol` to find all references to the `foo` symbol. 64 | 65 | If `other-macro` is a common macro, please consider submitting a patch 66 | to `elisp-refs--function-p` to make elisp-refs smarter. 67 | 68 | elisp-refs also does not support **indirect calls**. 69 | 70 | ``` emacs-lisp 71 | ;; Since we do a simple syntax tree walk, this isn't treated as a 72 | ;; call to foo. 73 | (let ((x (symbol-function 'foo))) 74 | (funcall x)) 75 | 76 | ;; Similarly, indirect function calls are not treated as 77 | ;; function calls. 78 | (defun call-func (x) 79 | (funcall x)) 80 | (call-func 'foo) 81 | 82 | ;; However, if you use sharp quoting, elisp-refs knows it's a function 83 | reference! 84 | (call-func #'foo) 85 | ``` 86 | 87 | ## Running tests 88 | 89 | You can run the tests with: 90 | 91 | ``` 92 | $ cask install 93 | $ cask exec ert-runner 94 | ``` 95 | 96 | ## Performance 97 | 98 | elisp-refs is CPU-intensive elisp and has been carefully optimised. You 99 | can run the benchmark script with: 100 | 101 | ``` 102 | $ cask install 103 | $ ./bench.sh 104 | ``` 105 | 106 | New features are carefully measured to ensure performance does not get 107 | worse. 108 | 109 | See elisp-refs-bench.el for more details. 110 | 111 | ## Alternative Projects 112 | 113 | **xref-find-references**: This command is included in Emacs 25.1, but 114 | it's based on a text search. It is confused by comments and strings, 115 | and cannot distinguish between functions and variables. 116 | 117 | xrefs-find-references is also line oriented, so it does not show the 118 | whole sexp that matched your search. Since it requires text files, 119 | it doesn't search built-in .el.gz files. 120 | 121 | **TAGS**: It is possible to record function references in TAGS 122 | files. Whilst [universal-ctags](https://github.com/universal-ctags/ctags) (formerly 123 | known as exuberant-ctags) does provide the ability to find references, 124 | it is not supported in its lisp parser. 125 | 126 | etags, the TAGS implementation shipped with Emacs, cannot find 127 | references (to my knowledge). 128 | 129 | **[el-search](https://elpa.gnu.org/packages/el-search.html)** allows 130 | you to search for arbitrary forms in elisp files. It's slower, but a 131 | much more general tool. Its design greatly influenced elisp-refs. 132 | 133 | **[elisp-slime-nav](https://github.com/purcell/elisp-slime-nav)** 134 | finds definitions, not references. It's a great complementary tool. 135 | -------------------------------------------------------------------------------- /bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | cask eval "(progn (require 'elisp-refs-bench) (elisp-refs-bench))" 6 | -------------------------------------------------------------------------------- /elisp-refs-bench.el: -------------------------------------------------------------------------------- 1 | ;;; elisp-refs-bench.el --- measure elisp-refs.el performance 2 | 3 | ;;; Code: 4 | 5 | (require 'elisp-refs) 6 | (require 'dash) 7 | 8 | (defmacro elisp-refs--print-time (form) 9 | "Evaluate FORM, and print the time taken." 10 | `(progn 11 | (message "Timing %s" ',form) 12 | (-let [(total-time gc-runs gc-time) 13 | (cl-letf (((symbol-function #'message) 14 | (lambda (_format-string &rest _args)))) 15 | (benchmark-run 1 ,form))] 16 | (message "Elapsed time: %fs (%fs in %d GCs)\n" 17 | total-time 18 | gc-time 19 | gc-runs)))) 20 | 21 | ;; TODO: benchmark elisp-refs-variable (and add a smoke test) 22 | ;; TODO: make this more representative by loading more elisp files 23 | ;; before searching. Running this in a GUI is also conspicuously 24 | ;; slower, which bench.sh doesn't reflect. 25 | (defun elisp-refs-bench () 26 | "Measure runtime of searching." 27 | (interactive) 28 | (elisp-refs--report-loc) 29 | ;; Measure a fairly uncommon function. 30 | (elisp-refs--print-time (elisp-refs-function 'mod)) 31 | ;; Measure a common macro 32 | (elisp-refs--print-time (elisp-refs-macro 'when)) 33 | ;; Compare with searching for the same symbol without walking 34 | (elisp-refs--print-time (elisp-refs-symbol 'when)) 35 | ;; Synthetic test of a large number of results. 36 | (message "Formatting 10,000 results") 37 | (let ((forms (-repeat 10000 (list '(ignored) 1 64))) 38 | (buf (generate-new-buffer " *dummy-elisp-refs-buf*"))) 39 | (with-current-buffer buf 40 | (insert "(defun foo (bar) (if bar nil (with-current-buffer bar))) ;; blah") 41 | (setq-local elisp-refs--path "/tmp/foo.el")) 42 | (elisp-refs--print-time 43 | (elisp-refs--show-results 'foo "foo bar" (list (cons forms buf)) 44 | 20 nil)) 45 | (kill-buffer buf))) 46 | 47 | (defun elisp-refs--report-loc () 48 | "Report the total number of lines of code searched." 49 | (interactive) 50 | (let* ((loaded-paths (elisp-refs--loaded-paths)) 51 | (loaded-src-bufs (cl-letf (((symbol-function #'message) 52 | (lambda (_format-string &rest _args)))) 53 | (-map #'elisp-refs--contents-buffer loaded-paths))) 54 | (total-lines (-sum (--map (with-current-buffer it 55 | (line-number-at-pos (point-max))) 56 | loaded-src-bufs)))) 57 | ;; Clean up temporary buffers. 58 | (--each loaded-src-bufs (kill-buffer it)) 59 | (message "Total LOC: %s" (elisp-refs--format-int total-lines)))) 60 | 61 | (provide 'elisp-refs-bench) 62 | ;;; elisp-refs-bench.el ends here 63 | -------------------------------------------------------------------------------- /elisp-refs.el: -------------------------------------------------------------------------------- 1 | ;;; elisp-refs.el --- find callers of elisp functions or macros -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2020 Wilfred Hughes 4 | 5 | ;; Author: Wilfred Hughes 6 | ;; Version: 1.6 7 | ;; Keywords: lisp 8 | ;; Package-Requires: ((dash "2.12.0") (s "1.11.0")) 9 | 10 | ;; This program is free software; you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation, either version 3 of the License, or 13 | ;; (at your option) any later version. 14 | 15 | ;; This program is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with this program. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; elisp-refs.el is an Emacs package for finding references to 26 | ;; functions, macros or variables. Unlike a dumb text search, 27 | ;; elisp-refs.el actually parses the code, so it's never confused by 28 | ;; comments or `foo-bar' matching `foo'. 29 | ;; 30 | ;; See https://github.com/Wilfred/refs.el/blob/master/README.md for 31 | ;; more information. 32 | 33 | ;;; Code: 34 | 35 | (require 'dash) 36 | (require 's) 37 | (require 'format) 38 | (eval-when-compile (require 'cl-lib)) 39 | 40 | (defvar symbols-with-pos-enabled) 41 | (declare-function symbol-with-pos-p nil (object)) 42 | (declare-function symbol-with-pos-pos nil (ls)) 43 | 44 | ;;; Internal 45 | 46 | (defvar elisp-refs-verbose t) 47 | 48 | (defun elisp-refs--format-int (integer) 49 | "Format INTEGER as a string, with , separating thousands." 50 | (let ((number (abs integer)) 51 | (parts nil)) 52 | (while (> number 999) 53 | (push (format "%03d" (mod number 1000)) 54 | parts) 55 | (setq number (/ number 1000))) 56 | (push (format "%d" number) parts) 57 | (concat 58 | (if (< integer 0) "-" "") 59 | (s-join "," parts)))) 60 | 61 | (defsubst elisp-refs--start-pos (end-pos) 62 | "Find the start position of form ending at END-POS 63 | in the current buffer." 64 | (let ((parse-sexp-ignore-comments t)) 65 | (scan-sexps end-pos -1))) 66 | 67 | (defun elisp-refs--sexp-positions (buffer start-pos end-pos) 68 | "Return a list of start and end positions of all the sexps 69 | between START-POS and END-POS (inclusive) in BUFFER. 70 | 71 | Positions exclude quote characters, so given 'foo or `foo, we 72 | report the position of the symbol foo. 73 | 74 | Not recursive, so we don't consider subelements of nested sexps." 75 | (let ((positions nil)) 76 | (with-current-buffer buffer 77 | (condition-case _err 78 | (catch 'done 79 | (while t 80 | (let* ((sexp-end-pos (let ((parse-sexp-ignore-comments t)) 81 | (scan-sexps start-pos 1)))) 82 | ;; If we've reached a sexp beyond the range requested, 83 | ;; or if there are no sexps left, we're done. 84 | (when (or (null sexp-end-pos) (> sexp-end-pos end-pos)) 85 | (throw 'done nil)) 86 | ;; Otherwise, this sexp is in the range requested. 87 | (push (list (elisp-refs--start-pos sexp-end-pos) sexp-end-pos) 88 | positions) 89 | (setq start-pos sexp-end-pos)))) 90 | ;; Terminate when we see "Containing expression ends prematurely" 91 | (scan-error nil))) 92 | (nreverse positions))) 93 | 94 | (defun elisp-refs--read-buffer-form (symbols-with-pos) 95 | "Read a form from the current buffer, starting at point. 96 | Returns a list: 97 | \(form form-start-pos form-end-pos symbol-positions read-start-pos) 98 | 99 | In Emacs 28 and earlier, SYMBOL-POSITIONS is a list of 0-indexed 100 | symbol positions relative to READ-START-POS, according to 101 | `read-symbol-positions-list'. 102 | 103 | In Emacs 29+, SYMBOL-POSITIONS is nil. If SYMBOLS-WITH-POS is 104 | non-nil, forms are read with `read-positioning-symbols'." 105 | (let* ((read-with-symbol-positions t) 106 | (read-start-pos (point)) 107 | (form (if (and symbols-with-pos (fboundp 'read-positioning-symbols)) 108 | (read-positioning-symbols (current-buffer)) 109 | (read (current-buffer)))) 110 | (symbols (if (boundp 'read-symbol-positions-list) 111 | read-symbol-positions-list 112 | nil)) 113 | (end-pos (point)) 114 | (start-pos (elisp-refs--start-pos end-pos))) 115 | (list form start-pos end-pos symbols read-start-pos))) 116 | 117 | (defvar elisp-refs--path nil 118 | "A buffer-local variable used by `elisp-refs--contents-buffer'. 119 | Internal implementation detail.") 120 | 121 | (defun elisp-refs--read-all-buffer-forms (buffer symbols-with-pos) 122 | "Read all the forms in BUFFER, along with their positions." 123 | (with-current-buffer buffer 124 | (goto-char (point-min)) 125 | (let ((forms nil)) 126 | (condition-case err 127 | (while t 128 | (push (elisp-refs--read-buffer-form symbols-with-pos) forms)) 129 | (error 130 | (if (or (equal (car err) 'end-of-file) 131 | ;; TODO: this shouldn't occur in valid elisp files, 132 | ;; but it's happening in helm-utils.el. 133 | (equal (car err) 'scan-error)) 134 | ;; Reached end of file, we're done. 135 | (nreverse forms) 136 | ;; Some unexpected error, propagate. 137 | (error "Unexpected error whilst reading %s position %s: %s" 138 | (abbreviate-file-name elisp-refs--path) (point) err))))))) 139 | 140 | (defun elisp-refs--proper-list-p (val) 141 | "Is VAL a proper list?" 142 | (if (fboundp 'proper-list-p) 143 | ;; `proper-list-p' was added in Emacs 27.1. 144 | ;; http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=2fde6275b69fd113e78243790bf112bbdd2fe2bf 145 | (with-no-warnings (proper-list-p val)) 146 | ;; Earlier Emacs versions only had format-proper-list-p. 147 | (with-no-warnings (format-proper-list-p val)))) 148 | 149 | (defun elisp-refs--walk (buffer form start-pos end-pos symbol match-p &optional path) 150 | "Walk FORM, a nested list, and return a list of sublists (with 151 | their positions) where MATCH-P returns t. FORM is traversed 152 | depth-first (pre-order traversal, left-to-right). 153 | 154 | MATCH-P is called with three arguments: 155 | \(SYMBOL CURRENT-FORM PATH). 156 | 157 | PATH is the first element of all the enclosing forms of 158 | CURRENT-FORM, innermost first, along with the index of the 159 | current form. 160 | 161 | For example if we are looking at h in (e f (g h)), PATH takes the 162 | value ((g . 1) (e . 2)). 163 | 164 | START-POS and END-POS should be the position of FORM within BUFFER." 165 | (cond 166 | ((funcall match-p symbol form path) 167 | ;; If this form matches, just return it, along with the position. 168 | (list (list form start-pos end-pos))) 169 | ;; Otherwise, recurse on the subforms. 170 | ((consp form) 171 | (let ((matches nil) 172 | ;; Find the positions of the subforms. 173 | (subforms-positions 174 | (if (eq (car-safe form) '\`) 175 | ;; Kludge: `elisp-refs--sexp-positions' excludes the ` when 176 | ;; calculating positions. So, to find the inner 177 | ;; positions when walking from `(...) to (...), we 178 | ;; don't need to increment the start position. 179 | (cons nil (elisp-refs--sexp-positions buffer start-pos end-pos)) 180 | ;; Calculate the positions after the opening paren. 181 | (elisp-refs--sexp-positions buffer (1+ start-pos) end-pos)))) 182 | ;; For each subform, recurse if it's a list, or a matching symbol. 183 | (--each (-zip-pair form subforms-positions) 184 | (-let [(subform subform-start subform-end) it] 185 | (when (or 186 | (and (consp subform) (elisp-refs--proper-list-p subform)) 187 | (and (symbolp subform) (eq subform symbol))) 188 | (-when-let (subform-matches 189 | (elisp-refs--walk 190 | buffer subform 191 | subform-start subform-end 192 | symbol match-p 193 | (cons (cons (car-safe form) it-index) path))) 194 | (push subform-matches matches))))) 195 | 196 | ;; Concat the results from all the subforms. 197 | (apply #'append (nreverse matches)))))) 198 | 199 | ;; TODO: condition-case (condition-case ... (error ...)) is not a call 200 | ;; TODO: (cl-destructuring-bind (foo &rest bar) ...) is not a call 201 | ;; TODO: letf, cl-letf, -let, -let* 202 | (defun elisp-refs--function-p (symbol form path) 203 | "Return t if FORM looks like a function call to SYMBOL." 204 | (cond 205 | ((not (consp form)) 206 | nil) 207 | ;; Ignore (defun _ (SYMBOL ...) ...) 208 | ((or (equal (car path) '(defsubst . 2)) 209 | (equal (car path) '(defun . 2)) 210 | (equal (car path) '(defmacro . 2)) 211 | (equal (car path) '(cl-defun . 2))) 212 | nil) 213 | ;; Ignore (lambda (SYMBOL ...) ...) 214 | ((equal (car path) '(lambda . 1)) 215 | nil) 216 | ;; Ignore (let (SYMBOL ...) ...) 217 | ;; and (let* (SYMBOL ...) ...) 218 | ((or 219 | (equal (car path) '(let . 1)) 220 | (equal (car path) '(let* . 1))) 221 | nil) 222 | ;; Ignore (let ((SYMBOL ...)) ...) 223 | ((or 224 | (equal (cl-second path) '(let . 1)) 225 | (equal (cl-second path) '(let* . 1))) 226 | nil) 227 | ;; Ignore (declare-function NAME (ARGS...)) 228 | ((equal (car path) '(declare-function . 3)) 229 | nil) 230 | ;; (SYMBOL ...) 231 | ((eq (car form) symbol) 232 | t) 233 | ;; (foo ... #'SYMBOL ...) 234 | ((--any-p (equal it (list 'function symbol)) form) 235 | t) 236 | ;; (funcall 'SYMBOL ...) 237 | ((and (eq (car form) 'funcall) 238 | (equal `',symbol (cl-second form))) 239 | t) 240 | ;; (apply 'SYMBOL ...) 241 | ((and (eq (car form) 'apply) 242 | (equal `',symbol (cl-second form))) 243 | t))) 244 | 245 | (defun elisp-refs--macro-p (symbol form path) 246 | "Return t if FORM looks like a macro call to SYMBOL." 247 | (cond 248 | ((not (consp form)) 249 | nil) 250 | ;; Ignore (defun _ (SYMBOL ...) ...) 251 | ((or (equal (car path) '(defsubst . 2)) 252 | (equal (car path) '(defun . 2)) 253 | (equal (car path) '(defmacro . 2))) 254 | nil) 255 | ;; Ignore (lambda (SYMBOL ...) ...) 256 | ((equal (car path) '(lambda . 1)) 257 | nil) 258 | ;; Ignore (let (SYMBOL ...) ...) 259 | ;; and (let* (SYMBOL ...) ...) 260 | ((or 261 | (equal (car path) '(let . 1)) 262 | (equal (car path) '(let* . 1))) 263 | nil) 264 | ;; Ignore (let ((SYMBOL ...)) ...) 265 | ((or 266 | (equal (cl-second path) '(let . 1)) 267 | (equal (cl-second path) '(let* . 1))) 268 | nil) 269 | ;; (SYMBOL ...) 270 | ((eq (car form) symbol) 271 | t))) 272 | 273 | ;; Looking for a special form is exactly the same as looking for a 274 | ;; macro. 275 | (defalias 'elisp-refs--special-p 'elisp-refs--macro-p) 276 | 277 | (defun elisp-refs--variable-p (symbol form path) 278 | "Return t if this looks like a variable reference to SYMBOL. 279 | We consider parameters to be variables too." 280 | (cond 281 | ((consp form) 282 | nil) 283 | ;; Ignore (defun _ (SYMBOL ...) ...) 284 | ((or (equal (car path) '(defsubst . 1)) 285 | (equal (car path) '(defun . 1)) 286 | (equal (car path) '(defmacro . 1)) 287 | (equal (car path) '(cl-defun . 1))) 288 | nil) 289 | ;; (let (SYMBOL ...) ...) is a variable, not a function call. 290 | ((or 291 | (equal (cl-second path) '(let . 1)) 292 | (equal (cl-second path) '(let* . 1))) 293 | t) 294 | ;; (lambda (SYMBOL ...) ...) is a variable 295 | ((equal (cl-second path) '(lambda . 1)) 296 | t) 297 | ;; (let ((SYMBOL ...)) ...) is also a variable. 298 | ((or 299 | (equal (cl-third path) '(let . 1)) 300 | (equal (cl-third path) '(let* . 1))) 301 | t) 302 | ;; Ignore (SYMBOL ...) otherwise, we assume it's a function/macro 303 | ;; call. 304 | ((equal (car path) (cons symbol 0)) 305 | nil) 306 | ((eq form symbol) 307 | t))) 308 | 309 | ;; TODO: benchmark building a list with `push' rather than using 310 | ;; mapcat. 311 | (defun elisp-refs--read-and-find (buffer symbol match-p) 312 | "Read all the forms in BUFFER, and return a list of all forms that 313 | contain SYMBOL where MATCH-P returns t. 314 | 315 | For every matching form found, we return the form itself along 316 | with its start and end position." 317 | (-non-nil 318 | (--mapcat 319 | (-let [(form start-pos end-pos symbol-positions _read-start-pos) it] 320 | ;; Optimisation: if we have a list of positions for the current 321 | ;; form (Emacs 28 and earlier), and it doesn't contain the 322 | ;; symbol we're looking for, don't bother walking the form. 323 | (when (or (null symbol-positions) (assq symbol symbol-positions)) 324 | (elisp-refs--walk buffer form start-pos end-pos symbol match-p))) 325 | (elisp-refs--read-all-buffer-forms buffer nil)))) 326 | 327 | (defun elisp-refs--walk-positioned-symbols (forms symbol) 328 | "Given a nested list of FORMS, return a list of all positions of SYMBOL. 329 | Assumes `symbol-with-pos-pos' is defined (Emacs 29+)." 330 | (cond 331 | ((symbol-with-pos-p forms) 332 | (let ((symbols-with-pos-enabled t)) 333 | (if (eq forms symbol) 334 | (list (list symbol 335 | (symbol-with-pos-pos forms) 336 | (+ (symbol-with-pos-pos forms) (length (symbol-name symbol)))))))) 337 | ((elisp-refs--proper-list-p forms) 338 | ;; Proper list, use `--mapcat` to reduce how much we recurse. 339 | (--mapcat (elisp-refs--walk-positioned-symbols it symbol) forms)) 340 | ((consp forms) 341 | ;; Improper list, we have to recurse on head and tail. 342 | (append (elisp-refs--walk-positioned-symbols (car forms) symbol) 343 | (elisp-refs--walk-positioned-symbols (cdr forms) symbol))) 344 | ((vectorp forms) 345 | (--mapcat (elisp-refs--walk-positioned-symbols it symbol) forms)))) 346 | 347 | (defun elisp-refs--read-and-find-symbol (buffer symbol) 348 | "Read all the forms in BUFFER, and return a list of all 349 | positions of SYMBOL." 350 | (let* ((symbols-with-pos (fboundp 'symbol-with-pos-pos)) 351 | (forms (elisp-refs--read-all-buffer-forms buffer symbols-with-pos))) 352 | 353 | (if symbols-with-pos 354 | (elisp-refs--walk-positioned-symbols forms symbol) 355 | (-non-nil 356 | (--mapcat 357 | (-let [(_ _ _ symbol-positions read-start-pos) it] 358 | (--map 359 | (-let [(sym . offset) it] 360 | (when (eq sym symbol) 361 | (-let* ((start-pos (+ read-start-pos offset)) 362 | (end-pos (+ start-pos (length (symbol-name sym))))) 363 | (list sym start-pos end-pos)))) 364 | symbol-positions)) 365 | forms))))) 366 | 367 | (defun elisp-refs--filter-obarray (pred) 368 | "Return a list of all the items in `obarray' where PRED returns t." 369 | (let (symbols) 370 | (mapatoms (lambda (symbol) 371 | (when (and (funcall pred symbol) 372 | (not (equal (symbol-name symbol) ""))) 373 | (push symbol symbols)))) 374 | symbols)) 375 | 376 | (defun elisp-refs--loaded-paths () 377 | "Return a list of all files that have been loaded in Emacs. 378 | Where the file was a .elc, return the path to the .el file instead." 379 | (let ((elc-paths (-non-nil (mapcar #'-first-item load-history)))) 380 | (-non-nil 381 | (--map 382 | (let ((el-name (format "%s.el" (file-name-sans-extension it))) 383 | (el-gz-name (format "%s.el.gz" (file-name-sans-extension it)))) 384 | (cond ((file-exists-p el-name) el-name) 385 | ((file-exists-p el-gz-name) el-gz-name) 386 | ;; Ignore files where we can't find a .el file. 387 | (t nil))) 388 | elc-paths)))) 389 | 390 | (defun elisp-refs--contents-buffer (path) 391 | "Read PATH into a disposable buffer, and return it. 392 | Works around the fact that Emacs won't allow multiple buffers 393 | visiting the same file." 394 | (let ((fresh-buffer (generate-new-buffer (format " *refs-%s*" path))) 395 | ;; Be defensive against users overriding encoding 396 | ;; configurations (Helpful bugs #75 and #147). 397 | (coding-system-for-read nil) 398 | (file-name-handler-alist 399 | '(("\\(?:\\.dz\\|\\.txz\\|\\.xz\\|\\.lzma\\|\\.lz\\|\\.g?z\\|\\.\\(?:tgz\\|svgz\\|sifz\\)\\|\\.tbz2?\\|\\.bz2\\|\\.Z\\)\\(?:~\\|\\.~[-[:alnum:]:#@^._]+\\(?:~[[:digit:]]+\\)?~\\)?\\'" . 400 | jka-compr-handler) 401 | ("\\(?:^/\\)\\(\\(?:\\(?:\\(-\\|[[:alnum:]]\\{2,\\}\\)\\(?::\\)\\(?:\\([^/:|[:blank:]]+\\)\\(?:@\\)\\)?\\(\\(?:[%._[:alnum:]-]+\\|\\(?:\\[\\)\\(?:\\(?:[[:alnum:]]*:\\)+[.[:alnum:]]*\\)?\\(?:]\\)\\)\\(?:\\(?:#\\)\\(?:[[:digit:]]+\\)\\)?\\)?\\)\\(?:|\\)\\)+\\)?\\(?:\\(-\\|[[:alnum:]]\\{2,\\}\\)\\(?::\\)\\(?:\\([^/:|[:blank:]]+\\)\\(?:@\\)\\)?\\(\\(?:[%._[:alnum:]-]+\\|\\(?:\\[\\)\\(?:\\(?:[[:alnum:]]*:\\)+[.[:alnum:]]*\\)?\\(?:]\\)\\)\\(?:\\(?:#\\)\\(?:[[:digit:]]+\\)\\)?\\)?\\)\\(?::\\)\\([^\n ]*\\'\\)" . tramp-file-name-handler) 402 | ("\\`/:" . file-name-non-special)))) 403 | (with-current-buffer fresh-buffer 404 | (setq-local elisp-refs--path path) 405 | (insert-file-contents path) 406 | ;; We don't enable emacs-lisp-mode because it slows down this 407 | ;; function significantly. We just need the syntax table for 408 | ;; scan-sexps to do the right thing with comments. 409 | (set-syntax-table emacs-lisp-mode-syntax-table)) 410 | fresh-buffer)) 411 | 412 | (defvar elisp-refs--highlighting-buffer 413 | nil 414 | "A temporary buffer used for highlighting. 415 | Since `elisp-refs--syntax-highlight' is a hot function, we 416 | don't want to create lots of temporary buffers.") 417 | 418 | (defun elisp-refs--syntax-highlight (str) 419 | "Apply font-lock properties to a string STR of Emacs lisp code." 420 | ;; Ensure we have a highlighting buffer to work with. 421 | (unless (and elisp-refs--highlighting-buffer 422 | (buffer-live-p elisp-refs--highlighting-buffer)) 423 | (setq elisp-refs--highlighting-buffer 424 | (generate-new-buffer " *refs-highlighting*")) 425 | (with-current-buffer elisp-refs--highlighting-buffer 426 | (delay-mode-hooks (emacs-lisp-mode)))) 427 | 428 | (with-current-buffer elisp-refs--highlighting-buffer 429 | (erase-buffer) 430 | (insert str) 431 | (if (fboundp 'font-lock-ensure) 432 | (font-lock-ensure) 433 | (with-no-warnings 434 | (font-lock-fontify-buffer))) 435 | (buffer-string))) 436 | 437 | (defun elisp-refs--replace-tabs (string) 438 | "Replace tabs in STRING with spaces." 439 | ;; This is important for unindenting, as we may unindent by less 440 | ;; than one whole tab. 441 | (s-replace "\t" (s-repeat tab-width " ") string)) 442 | 443 | (defun elisp-refs--lines (string) 444 | "Return a list of all the lines in STRING. 445 | 'a\nb' -> ('a\n' 'b')" 446 | (let ((lines nil)) 447 | (while (> (length string) 0) 448 | (let ((index (s-index-of "\n" string))) 449 | (if index 450 | (progn 451 | (push (substring string 0 (1+ index)) lines) 452 | (setq string (substring string (1+ index)))) 453 | (push string lines) 454 | (setq string "")))) 455 | (nreverse lines))) 456 | 457 | (defun elisp-refs--map-lines (string fn) 458 | "Execute FN for each line in string, and join the result together." 459 | (let ((result nil)) 460 | (dolist (line (elisp-refs--lines string)) 461 | (push (funcall fn line) result)) 462 | (apply #'concat (nreverse result)))) 463 | 464 | (defun elisp-refs--unindent-rigidly (string) 465 | "Given an indented STRING, unindent rigidly until 466 | at least one line has no indent. 467 | 468 | STRING should have a 'elisp-refs-start-pos property. The returned 469 | string will have this property updated to reflect the unindent." 470 | (let* ((lines (s-lines string)) 471 | ;; Get the leading whitespace for each line. 472 | (indents (--map (car (s-match (rx bos (+ whitespace)) it)) 473 | lines)) 474 | (min-indent (-min (--map (length it) indents)))) 475 | (propertize 476 | (elisp-refs--map-lines 477 | string 478 | (lambda (line) (substring line min-indent))) 479 | 'elisp-refs-unindented min-indent))) 480 | 481 | (defun elisp-refs--containing-lines (buffer start-pos end-pos) 482 | "Return a string, all the lines in BUFFER that are between 483 | START-POS and END-POS (inclusive). 484 | 485 | For the characters that are between START-POS and END-POS, 486 | propertize them." 487 | (let (expanded-start-pos expanded-end-pos) 488 | (with-current-buffer buffer 489 | ;; Expand START-POS and END-POS to line boundaries. 490 | (goto-char start-pos) 491 | (beginning-of-line) 492 | (setq expanded-start-pos (point)) 493 | (goto-char end-pos) 494 | (end-of-line) 495 | (setq expanded-end-pos (point)) 496 | 497 | ;; Extract the rest of the line before and after the section we're interested in. 498 | (let* ((before-match (buffer-substring expanded-start-pos start-pos)) 499 | (after-match (buffer-substring end-pos expanded-end-pos)) 500 | ;; Concat the extra text with the actual match, ensuring we 501 | ;; highlight the match as code, but highlight the rest as as 502 | ;; comments. 503 | (text (concat 504 | (propertize before-match 505 | 'face 'font-lock-comment-face) 506 | (elisp-refs--syntax-highlight (buffer-substring start-pos end-pos)) 507 | (propertize after-match 508 | 'face 'font-lock-comment-face)))) 509 | (-> text 510 | (elisp-refs--replace-tabs) 511 | (elisp-refs--unindent-rigidly) 512 | (propertize 'elisp-refs-start-pos expanded-start-pos 513 | 'elisp-refs-path elisp-refs--path)))))) 514 | 515 | (defun elisp-refs--find-file (button) 516 | "Open the file referenced by BUTTON." 517 | (find-file (button-get button 'path)) 518 | (goto-char (point-min))) 519 | 520 | (define-button-type 'elisp-refs-path-button 521 | 'action 'elisp-refs--find-file 522 | 'follow-link t 523 | 'help-echo "Open file") 524 | 525 | (defun elisp-refs--path-button (path) 526 | "Return a button that navigates to PATH." 527 | (with-temp-buffer 528 | (insert-text-button 529 | (abbreviate-file-name path) 530 | :type 'elisp-refs-path-button 531 | 'path path) 532 | (buffer-string))) 533 | 534 | (defun elisp-refs--describe (button) 535 | "Show *Help* for the symbol referenced by BUTTON." 536 | (let ((symbol (button-get button 'symbol)) 537 | (kind (button-get button 'kind))) 538 | (cond ((eq kind 'symbol) 539 | (describe-symbol symbol)) 540 | ((eq kind 'variable) 541 | (describe-variable symbol)) 542 | (t 543 | ;; Emacs uses `describe-function' for functions, macros and 544 | ;; special forms. 545 | (describe-function symbol))))) 546 | 547 | (define-button-type 'elisp-refs-describe-button 548 | 'action 'elisp-refs--describe 549 | 'follow-link t 550 | 'help-echo "Describe") 551 | 552 | (defun elisp-refs--describe-button (symbol kind) 553 | "Return a button that shows *Help* for SYMBOL. 554 | KIND should be 'function, 'macro, 'variable, 'special or 'symbol." 555 | (with-temp-buffer 556 | (insert (symbol-name kind) " ") 557 | (insert-text-button 558 | (symbol-name symbol) 559 | :type 'elisp-refs-describe-button 560 | 'symbol symbol 561 | 'kind kind) 562 | (buffer-string))) 563 | 564 | (defun elisp-refs--pluralize (number thing) 565 | "Human-friendly description of NUMBER occurrences of THING." 566 | (format "%s %s%s" 567 | (elisp-refs--format-int number) 568 | thing 569 | (if (equal number 1) "" "s"))) 570 | 571 | (defun elisp-refs--format-count (symbol ref-count file-count 572 | searched-file-count prefix) 573 | (let* ((file-str (if (zerop file-count) 574 | "" 575 | (format " in %s" (elisp-refs--pluralize file-count "file")))) 576 | (found-str (format "Found %s to %s%s." 577 | (elisp-refs--pluralize ref-count "reference") 578 | symbol 579 | file-str)) 580 | (searched-str (if prefix 581 | (format "Searched %s in %s." 582 | (elisp-refs--pluralize searched-file-count "loaded file") 583 | (elisp-refs--path-button (file-name-as-directory prefix))) 584 | (format "Searched all %s loaded in Emacs." 585 | (elisp-refs--pluralize searched-file-count "file"))))) 586 | (s-word-wrap 70 (format "%s %s" found-str searched-str)))) 587 | 588 | ;; TODO: if we have multiple matches on one line, we repeatedly show 589 | ;; that line. That's slightly confusing. 590 | (defun elisp-refs--show-results (symbol description results 591 | searched-file-count prefix) 592 | "Given a RESULTS list where each element takes the form \(forms . buffer\), 593 | render a friendly results buffer." 594 | (let ((buf (get-buffer-create (format "*refs: %s*" symbol)))) 595 | (switch-to-buffer buf) 596 | (let ((inhibit-read-only t)) 597 | (erase-buffer) 598 | (save-excursion 599 | ;; Insert the header. 600 | (insert 601 | (elisp-refs--format-count 602 | description 603 | (-sum (--map (length (car it)) results)) 604 | (length results) 605 | searched-file-count 606 | prefix) 607 | "\n\n") 608 | ;; Insert the results. 609 | (--each results 610 | (-let* (((forms . buf) it) 611 | (path (with-current-buffer buf elisp-refs--path))) 612 | (insert 613 | (propertize "File: " 'face 'bold) 614 | (elisp-refs--path-button path) "\n") 615 | (--each forms 616 | (-let [(_ start-pos end-pos) it] 617 | (insert (elisp-refs--containing-lines buf start-pos end-pos) 618 | "\n"))) 619 | (insert "\n"))) 620 | ;; Prepare the buffer for the user. 621 | (elisp-refs-mode))) 622 | ;; Cleanup buffers created when highlighting results. 623 | (when elisp-refs--highlighting-buffer 624 | (kill-buffer elisp-refs--highlighting-buffer)))) 625 | 626 | (defun elisp-refs--loaded-bufs () 627 | "Return a list of open buffers, one for each path in `load-path'." 628 | (mapcar #'elisp-refs--contents-buffer (elisp-refs--loaded-paths))) 629 | 630 | (defun elisp-refs--search-1 (bufs match-fn) 631 | "Call MATCH-FN on each buffer in BUFS, reporting progress 632 | and accumulating results. 633 | 634 | BUFS should be disposable: we make no effort to preserve their 635 | state during searching. 636 | 637 | MATCH-FN should return a list where each element takes the form: 638 | \(form start-pos end-pos)." 639 | (let* (;; Our benchmark suggests we spend a lot of time in GC, and 640 | ;; performance improves if we GC less frequently. 641 | (gc-cons-percentage 0.8) 642 | (total-bufs (length bufs))) 643 | (let ((searched 0) 644 | (forms-and-bufs nil)) 645 | (dolist (buf bufs) 646 | (let* ((matching-forms (funcall match-fn buf))) 647 | ;; If there were any matches in this buffer, push the 648 | ;; matches along with the buffer into our results 649 | ;; list. 650 | (when matching-forms 651 | (push (cons matching-forms buf) forms-and-bufs)) 652 | ;; Give feedback to the user on our progress, because 653 | ;; searching takes several seconds. 654 | (when (and (zerop (mod searched 10)) 655 | elisp-refs-verbose) 656 | (message "Searched %s/%s files" searched total-bufs)) 657 | (cl-incf searched))) 658 | (when elisp-refs-verbose 659 | (message "Searched %s/%s files" total-bufs total-bufs)) 660 | forms-and-bufs))) 661 | 662 | (defun elisp-refs--search (symbol description match-fn &optional path-prefix) 663 | "Find references to SYMBOL in all loaded files; call MATCH-FN on each buffer. 664 | When PATH-PREFIX, limit to loaded files whose path starts with that prefix. 665 | 666 | Display the results in a hyperlinked buffer. 667 | 668 | MATCH-FN should return a list where each element takes the form: 669 | \(form start-pos end-pos)." 670 | (let* ((loaded-paths (elisp-refs--loaded-paths)) 671 | (matching-paths (if path-prefix 672 | (--filter (s-starts-with? path-prefix it) loaded-paths) 673 | loaded-paths)) 674 | (loaded-src-bufs (mapcar #'elisp-refs--contents-buffer matching-paths))) 675 | ;; Use unwind-protect to ensure we always cleanup temporary 676 | ;; buffers, even if the user hits C-g. 677 | (unwind-protect 678 | (progn 679 | (let ((forms-and-bufs 680 | (elisp-refs--search-1 loaded-src-bufs match-fn))) 681 | (elisp-refs--show-results symbol description forms-and-bufs 682 | (length loaded-src-bufs) path-prefix))) 683 | ;; Clean up temporary buffers. 684 | (--each loaded-src-bufs (kill-buffer it))))) 685 | 686 | (defun elisp-refs--completing-read-symbol (prompt &optional filter) 687 | "Read an interned symbol from the minibuffer, 688 | defaulting to the symbol at point. PROMPT is the string to prompt 689 | with. 690 | 691 | If FILTER is given, only offer symbols where (FILTER sym) returns 692 | t." 693 | (let ((filter (or filter (lambda (_) t)))) 694 | (read 695 | (completing-read prompt 696 | (elisp-refs--filter-obarray filter) 697 | nil nil nil nil 698 | (-if-let (sym (thing-at-point 'symbol)) 699 | (when (funcall filter (read sym)) 700 | sym)))))) 701 | 702 | ;;; Commands 703 | 704 | ;;;###autoload 705 | (defun elisp-refs-function (symbol &optional path-prefix) 706 | "Display all the references to function SYMBOL, in all loaded 707 | elisp files. 708 | 709 | If called with a prefix, prompt for a directory to limit the search. 710 | 711 | This searches for functions, not macros. For that, see 712 | `elisp-refs-macro'." 713 | (interactive 714 | (list (elisp-refs--completing-read-symbol "Function: " #'functionp) 715 | (when current-prefix-arg 716 | (read-directory-name "Limit search to loaded files in: ")))) 717 | (when (not (functionp symbol)) 718 | (if (macrop symbol) 719 | (user-error "%s is a macro. Did you mean elisp-refs-macro?" 720 | symbol) 721 | (user-error "%s is not a function. Did you mean elisp-refs-symbol?" 722 | symbol))) 723 | (elisp-refs--search symbol 724 | (elisp-refs--describe-button symbol 'function) 725 | (lambda (buf) 726 | (elisp-refs--read-and-find buf symbol #'elisp-refs--function-p)) 727 | path-prefix)) 728 | 729 | ;;;###autoload 730 | (defun elisp-refs-macro (symbol &optional path-prefix) 731 | "Display all the references to macro SYMBOL, in all loaded 732 | elisp files. 733 | 734 | If called with a prefix, prompt for a directory to limit the search. 735 | 736 | This searches for macros, not functions. For that, see 737 | `elisp-refs-function'." 738 | (interactive 739 | (list (elisp-refs--completing-read-symbol "Macro: " #'macrop) 740 | (when current-prefix-arg 741 | (read-directory-name "Limit search to loaded files in: ")))) 742 | (when (not (macrop symbol)) 743 | (if (functionp symbol) 744 | (user-error "%s is a function. Did you mean elisp-refs-function?" 745 | symbol) 746 | (user-error "%s is not a function. Did you mean elisp-refs-symbol?" 747 | symbol))) 748 | (elisp-refs--search symbol 749 | (elisp-refs--describe-button symbol 'macro) 750 | (lambda (buf) 751 | (elisp-refs--read-and-find buf symbol #'elisp-refs--macro-p)) 752 | path-prefix)) 753 | 754 | ;;;###autoload 755 | (defun elisp-refs-special (symbol &optional path-prefix) 756 | "Display all the references to special form SYMBOL, in all loaded 757 | elisp files. 758 | 759 | If called with a prefix, prompt for a directory to limit the search." 760 | (interactive 761 | (list (elisp-refs--completing-read-symbol "Special form: " #'special-form-p) 762 | (when current-prefix-arg 763 | (read-directory-name "Limit search to loaded files in: ")))) 764 | (elisp-refs--search symbol 765 | (elisp-refs--describe-button symbol 'special-form) 766 | (lambda (buf) 767 | (elisp-refs--read-and-find buf symbol #'elisp-refs--special-p)) 768 | path-prefix)) 769 | 770 | ;;;###autoload 771 | (defun elisp-refs-variable (symbol &optional path-prefix) 772 | "Display all the references to variable SYMBOL, in all loaded 773 | elisp files. 774 | 775 | If called with a prefix, prompt for a directory to limit the search." 776 | (interactive 777 | ;; This is awkward. We don't want to just offer defvar variables, 778 | ;; because then we can't search for code which uses `let' to bind 779 | ;; symbols. There doesn't seem to be a good way to only offer 780 | ;; variables that have been bound at some point. 781 | (list (elisp-refs--completing-read-symbol "Variable: " ) 782 | (when current-prefix-arg 783 | (read-directory-name "Limit search to loaded files in: ")))) 784 | (elisp-refs--search symbol 785 | (elisp-refs--describe-button symbol 'variable) 786 | (lambda (buf) 787 | (elisp-refs--read-and-find buf symbol #'elisp-refs--variable-p)) 788 | path-prefix)) 789 | 790 | ;;;###autoload 791 | (defun elisp-refs-symbol (symbol &optional path-prefix) 792 | "Display all the references to SYMBOL in all loaded elisp files. 793 | 794 | If called with a prefix, prompt for a directory to limit the 795 | search." 796 | (interactive 797 | (list (elisp-refs--completing-read-symbol "Symbol: " ) 798 | (when current-prefix-arg 799 | (read-directory-name "Limit search to loaded files in: ")))) 800 | (elisp-refs--search symbol 801 | (elisp-refs--describe-button symbol 'symbol) 802 | (lambda (buf) 803 | (elisp-refs--read-and-find-symbol buf symbol)) 804 | path-prefix)) 805 | 806 | ;;; Mode 807 | 808 | (defvar elisp-refs-mode-map 809 | (let ((map (make-sparse-keymap))) 810 | ;; TODO: it would be nice for TAB to navigate to file buttons too, 811 | ;; like *Help* does. 812 | (set-keymap-parent map special-mode-map) 813 | (define-key map (kbd "") #'elisp-refs-next-match) 814 | (define-key map (kbd "") #'elisp-refs-prev-match) 815 | (define-key map (kbd "n") #'elisp-refs-next-match) 816 | (define-key map (kbd "p") #'elisp-refs-prev-match) 817 | (define-key map (kbd "RET") #'elisp-refs-visit-match) 818 | map) 819 | "Keymap for `elisp-refs-mode'.") 820 | 821 | (define-derived-mode elisp-refs-mode special-mode "Refs" 822 | "Major mode for refs results buffers.") 823 | 824 | (defun elisp--refs-visit-match (open-fn) 825 | "Go to the search result at point. 826 | Open file with function OPEN_FN. `find-file` or `find-file-other-window`" 827 | (interactive) 828 | (let* ((path (get-text-property (point) 'elisp-refs-path)) 829 | (pos (get-text-property (point) 'elisp-refs-start-pos)) 830 | (unindent (get-text-property (point) 'elisp-refs-unindented)) 831 | (column-offset (current-column)) 832 | (line-offset -1)) 833 | (when (null path) 834 | (user-error "No match here")) 835 | 836 | ;; If point is not on the first line of the match, work out how 837 | ;; far away the first line is. 838 | (save-excursion 839 | (while (equal pos (get-text-property (point) 'elisp-refs-start-pos)) 840 | (forward-line -1) 841 | (cl-incf line-offset))) 842 | 843 | (funcall open-fn path) 844 | (goto-char pos) 845 | ;; Move point so we're on the same char in the buffer that we were 846 | ;; on in the results buffer. 847 | (forward-line line-offset) 848 | (beginning-of-line) 849 | (let ((target-offset (+ column-offset unindent)) 850 | (i 0)) 851 | (while (< i target-offset) 852 | (if (looking-at "\t") 853 | (cl-incf i tab-width) 854 | (cl-incf i)) 855 | (forward-char 1))))) 856 | 857 | (defun elisp-refs-visit-match () 858 | "Goto the search result at point." 859 | (interactive) 860 | (elisp--refs-visit-match #'find-file)) 861 | 862 | (defun elisp-refs-visit-match-other-window () 863 | "Goto the search result at point, opening in another window." 864 | (interactive) 865 | (elisp--refs-visit-match #'find-file-other-window)) 866 | 867 | 868 | (defun elisp-refs--move-to-match (direction) 869 | "Move point one match forwards. 870 | If DIRECTION is -1, moves backwards instead." 871 | (let* ((start-pos (point)) 872 | (match-pos (get-text-property start-pos 'elisp-refs-start-pos)) 873 | current-match-pos) 874 | (condition-case _err 875 | (progn 876 | ;; Move forward/backwards until we're on the next/previous match. 877 | (catch 'done 878 | (while t 879 | (setq current-match-pos 880 | (get-text-property (point) 'elisp-refs-start-pos)) 881 | (when (and current-match-pos 882 | (not (equal match-pos current-match-pos))) 883 | (throw 'done nil)) 884 | (forward-char direction))) 885 | ;; Move to the beginning of that match. 886 | (while (equal (get-text-property (point) 'elisp-refs-start-pos) 887 | (get-text-property (1- (point)) 'elisp-refs-start-pos)) 888 | (forward-char -1)) 889 | ;; Move forward until we're on the first char of match within that 890 | ;; line. 891 | (while (or 892 | (looking-at " ") 893 | (eq (get-text-property (point) 'face) 894 | 'font-lock-comment-face)) 895 | (forward-char 1))) 896 | ;; If we're at the last result, don't move point. 897 | (end-of-buffer 898 | (progn 899 | (goto-char start-pos) 900 | (signal 'end-of-buffer nil)))))) 901 | 902 | (defun elisp-refs-prev-match () 903 | "Move to the previous search result in the Refs buffer." 904 | (interactive) 905 | (elisp-refs--move-to-match -1)) 906 | 907 | (defun elisp-refs-next-match () 908 | "Move to the next search result in the Refs buffer." 909 | (interactive) 910 | (elisp-refs--move-to-match 1)) 911 | 912 | (provide 'elisp-refs) 913 | ;;; elisp-refs.el ends here 914 | -------------------------------------------------------------------------------- /refs_filtered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wilfred/elisp-refs/541a064c3ce27867872cf708354a65d83baf2a6d/refs_filtered.png -------------------------------------------------------------------------------- /refs_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wilfred/elisp-refs/541a064c3ce27867872cf708354a65d83baf2a6d/refs_screenshot.png -------------------------------------------------------------------------------- /test/elisp-refs-unit-test.el: -------------------------------------------------------------------------------- 1 | (require 'ert) 2 | (require 'elisp-refs) 3 | 4 | ;; Tests in GitHub actions are recursing more deeply, meaning we hit recursion 5 | ;; limits. I suspect this is due to undercover collecting coverage 6 | ;; metrics. 7 | (when (getenv "CI") 8 | (message "Updating recursion limits from: max-specpdl-size: %s max-lisp-eval-depth: %s " 9 | max-specpdl-size 10 | max-lisp-eval-depth) 11 | (setq max-specpdl-size 4000) 12 | (setq max-lisp-eval-depth 2000)) 13 | 14 | (defmacro with-temp-backed-buffer (contents &rest body) 15 | "Create a temporary file with CONTENTS, and evaluate BODY 16 | whilst visiting that file." 17 | (let ((filename-sym (make-symbol "filename")) 18 | (buf-sym (make-symbol "buf"))) 19 | `(let* ((,filename-sym (make-temp-file "with-temp-buffer-and-file")) 20 | (,buf-sym (find-file-noselect ,filename-sym))) 21 | (unwind-protect 22 | (with-current-buffer ,buf-sym 23 | (insert ,contents) 24 | (set-syntax-table emacs-lisp-mode-syntax-table) 25 | (cl-letf (((symbol-function #'message) 26 | (lambda (_format-string) &rest _args))) 27 | (save-buffer)) 28 | ,@body ) 29 | (kill-buffer ,buf-sym) 30 | (delete-file ,filename-sym))))) 31 | 32 | (ert-deftest elisp-refs--format-int () 33 | "Ensure we format thousands correctly in numbers." 34 | (should (equal (elisp-refs--format-int 123) "123")) 35 | (should (equal (elisp-refs--format-int -123) "-123")) 36 | (should (equal (elisp-refs--format-int 1234) "1,234")) 37 | (should (equal (elisp-refs--format-int -1234) "-1,234")) 38 | (should (equal (elisp-refs--format-int 1234567) "1,234,567"))) 39 | 40 | (ert-deftest elisp-refs--pluralize () 41 | (should (equal (elisp-refs--pluralize 0 "thing") "0 things")) 42 | (should (equal (elisp-refs--pluralize 1 "thing") "1 thing")) 43 | (should (equal (elisp-refs--pluralize 2 "thing") "2 things")) 44 | (should (equal (elisp-refs--pluralize 1001 "thing") "1,001 things"))) 45 | 46 | (ert-deftest elisp-refs--unindent-split-properties () 47 | "Ensure we can still unindent when properties are split 48 | into separate region. Regression test for a very subtle bug." 49 | (let ((s #("e.\n" 0 2 (elisp-refs-start-pos 0) 2 3 (elisp-refs-start-pos 0)))) 50 | (elisp-refs--unindent-rigidly s))) 51 | 52 | (ert-deftest elisp-refs--sexp-positions () 53 | "Ensure we calculate positions correctly when we're considering 54 | the whole buffer." 55 | (with-temp-backed-buffer 56 | "(while list (setq len 1))" 57 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 58 | (sexp-positions 59 | (elisp-refs--sexp-positions elisp-refs-buf (point-min) (point-max)))) 60 | (should 61 | (equal sexp-positions (list '(1 26))))))) 62 | 63 | (ert-deftest elisp-refs--sexp-positions-comments () 64 | "Ensure we handle comments correctly when calculating sexp positions." 65 | (with-temp-backed-buffer 66 | "(while list 67 | ;; take the head of LIST 68 | (setq len 1))" 69 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 70 | (sexp-positions 71 | (elisp-refs--sexp-positions elisp-refs-buf (1+ (point-min)) (1- (point-max))))) 72 | ;; The position of the setq should take into account the comment. 73 | (should 74 | (equal (nth 2 sexp-positions) '(42 54)))))) 75 | 76 | (ert-deftest elisp-refs--find-calls-basic () 77 | "Find simple function calls." 78 | (with-temp-backed-buffer 79 | "(foo)" 80 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 81 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 82 | #'elisp-refs--function-p))) 83 | (should 84 | (equal calls (list (list '(foo) 1 6))))))) 85 | 86 | (ert-deftest elisp-refs--find-calls-sharp-quote () 87 | "Find function references using sharp quotes." 88 | (with-temp-backed-buffer 89 | "(bar #'foo)" 90 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 91 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 92 | #'elisp-refs--function-p))) 93 | (should 94 | (equal calls (list (list '(bar #'foo) 1 12))))))) 95 | 96 | (ert-deftest elisp-refs--find-calls-in-lambda () 97 | "Find function calls in lambda expressions." 98 | (with-temp-backed-buffer 99 | "(lambda (foo) (foo))" 100 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 101 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 102 | #'elisp-refs--function-p))) 103 | (should 104 | (equal calls (list (list '(foo) 15 20))))))) 105 | 106 | (ert-deftest elisp-refs--find-calls-in-backquote () 107 | "Find function calls in backquotes. 108 | Useful for finding references in macros, but this is primarily a 109 | regression test for bugs where we miscalculated position with 110 | backquote forms." 111 | (with-temp-backed-buffer 112 | "(baz `(biz (foo 1)))" 113 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 114 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 115 | #'elisp-refs--function-p))) 116 | (should 117 | (equal calls (list (list '(foo 1) 12 19))))))) 118 | 119 | (ert-deftest elisp-refs--find-macros-improper-list () 120 | "We shouldn't crash if the source code contains improper lists." 121 | (with-temp-backed-buffer 122 | "(destructuring-bind (start . end) region\n (when foo\n (bar)))" 123 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 124 | (calls (elisp-refs--read-and-find elisp-refs-buf 'when 125 | #'elisp-refs--macro-p))) 126 | (should 127 | (equal calls (list (list '(when foo (bar)) 44 64))))))) 128 | 129 | (ert-deftest elisp-refs--find-calls-nested () 130 | "Find nested function calls." 131 | (with-temp-backed-buffer 132 | "(baz (bar (foo)))" 133 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 134 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 135 | #'elisp-refs--function-p))) 136 | (should 137 | (equal calls (list (list '(foo) 11 16))))))) 138 | 139 | (ert-deftest elisp-refs--find-calls-funcall () 140 | "Find calls that use funcall." 141 | (with-temp-backed-buffer 142 | "(funcall 'foo)" 143 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 144 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 145 | #'elisp-refs--function-p))) 146 | (should 147 | (equal calls (list (list '(funcall 'foo) 1 15))))))) 148 | 149 | (ert-deftest elisp-refs--find-calls-apply () 150 | "Find calls that use apply." 151 | (with-temp-backed-buffer 152 | "(apply 'foo '(1 2))" 153 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 154 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 155 | #'elisp-refs--function-p))) 156 | (should 157 | (equal calls (list (list '(apply 'foo '(1 2)) 1 20))))))) 158 | 159 | (ert-deftest elisp-refs--find-calls-params () 160 | "Function or macro parameters should not be considered function calls." 161 | (with-temp-backed-buffer 162 | "(defun bar (foo)) (defsubst bar (foo)) (defmacro bar (foo)) (cl-defun bar (foo))" 163 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 164 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 165 | #'elisp-refs--function-p))) 166 | (should (null calls))))) 167 | 168 | (ert-deftest elisp-refs--find-calls-declare-function () 169 | "Don't confuse `declare-function' arguments with function calls." 170 | (with-temp-backed-buffer 171 | "(declare-function bbdb-record-field \"ext:bbdb\" (record field))" 172 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 173 | (calls (elisp-refs--read-and-find elisp-refs-buf 'record 174 | #'elisp-refs--function-p))) 175 | (should (null calls))))) 176 | 177 | (ert-deftest elisp-refs--find-calls-let-without-assignment () 178 | "We shouldn't confuse let declarations with function calls." 179 | (with-temp-backed-buffer 180 | "(let (foo)) (let* (foo))" 181 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 182 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 183 | #'elisp-refs--function-p))) 184 | (should (null calls))))) 185 | 186 | (ert-deftest elisp-refs--find-calls-let-with-assignment () 187 | "We shouldn't confuse let assignments with function calls." 188 | (with-temp-backed-buffer 189 | "(let ((foo nil))) (let* ((foo nil)))" 190 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 191 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 192 | #'elisp-refs--function-p))) 193 | (should (null calls))))) 194 | 195 | (ert-deftest elisp-refs--find-calls-let-with-assignment-call () 196 | "We should find function calls in let assignments." 197 | ;; TODO: actually check positions, this is error-prone. 198 | (with-temp-backed-buffer 199 | "(let ((bar (foo)))) (let* ((bar (foo))))" 200 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 201 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 202 | #'elisp-refs--function-p))) 203 | (should 204 | (equal (length calls) 2))))) 205 | 206 | (ert-deftest elisp-refs--find-calls-let-body () 207 | "We should find function calls in let body." 208 | (with-temp-backed-buffer 209 | "(let (bar) (foo)) (let* (bar) (foo))" 210 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 211 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 212 | #'elisp-refs--function-p))) 213 | (should (equal (length calls) 2))))) 214 | 215 | (ert-deftest elisp-refs--find-macros-basic () 216 | "Find simple function calls." 217 | (with-temp-backed-buffer 218 | "(foo)" 219 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 220 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 221 | #'elisp-refs--macro-p))) 222 | (should 223 | (equal calls (list (list '(foo) 1 6))))))) 224 | 225 | (ert-deftest elisp-refs--find-macros-in-lambda () 226 | "Find macros calls in lambda expressions." 227 | (with-temp-backed-buffer 228 | "(lambda (foo) (foo))" 229 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 230 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 231 | #'elisp-refs--macro-p))) 232 | (should 233 | (equal calls (list (list '(foo) 15 20))))))) 234 | 235 | (ert-deftest elisp-refs--find-macros-params () 236 | "Find simple function calls." 237 | (with-temp-backed-buffer 238 | "(defun bar (foo)) (defsubst bar (foo)) (defmacro bar (foo))" 239 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 240 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 241 | #'elisp-refs--macro-p))) 242 | (should (null calls))))) 243 | 244 | (ert-deftest elisp-refs--find-macros-let-without-assignment () 245 | "We shouldn't confuse let declarations with macro calls." 246 | (with-temp-backed-buffer 247 | "(let (foo)) (let* (foo))" 248 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 249 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 250 | #'elisp-refs--macro-p))) 251 | (should (null calls))))) 252 | 253 | (ert-deftest elisp-refs--find-macros-let-with-assignment () 254 | "We shouldn't confuse let assignments with macro calls." 255 | (with-temp-backed-buffer 256 | "(let ((foo nil) (foo nil))) (let* ((foo nil) (foo nil)))" 257 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 258 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 259 | #'elisp-refs--macro-p))) 260 | (should (null calls))))) 261 | 262 | (ert-deftest elisp-refs--find-macros-let-with-assignment-call () 263 | "We should find macro calls in let assignments." 264 | (with-temp-backed-buffer 265 | "(let ((bar (foo)))) (let* ((bar (foo))))" 266 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 267 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 268 | #'elisp-refs--macro-p))) 269 | (should 270 | (equal (length calls) 2))))) 271 | 272 | (ert-deftest elisp-refs--find-macro-calls-let-body () 273 | "We should find macro calls in let body." 274 | (with-temp-backed-buffer 275 | "(let (bar) (foo)) (let* (bar) (foo))" 276 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 277 | (calls (elisp-refs--read-and-find elisp-refs-buf 'foo 278 | #'elisp-refs--macro-p))) 279 | (should (equal (length calls) 2))))) 280 | 281 | (ert-deftest elisp-refs--find-symbols () 282 | "We should find symbols, not their containing forms." 283 | (with-temp-backed-buffer 284 | "(foo foo)" 285 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 286 | (matches (elisp-refs--read-and-find-symbol elisp-refs-buf 'foo))) 287 | (should 288 | (equal 289 | matches 290 | (list '(foo 2 5) '(foo 6 9))))))) 291 | 292 | (ert-deftest elisp-refs--find-var-basic () 293 | "Test the base case of finding variables" 294 | (with-temp-backed-buffer 295 | "foo" 296 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 297 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 298 | #'elisp-refs--variable-p))) 299 | (should 300 | (equal 301 | matches 302 | (list '(foo 1 4))))))) 303 | 304 | (ert-deftest elisp-refs--find-var-in-lambda () 305 | "Find variable references in lambda expressions." 306 | (with-temp-backed-buffer 307 | "(lambda (foo) (foo))" 308 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 309 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 310 | #'elisp-refs--variable-p))) 311 | (should 312 | (equal matches (list (list 'foo 10 13))))))) 313 | 314 | (ert-deftest elisp-refs--find-var-ignores-calls () 315 | "Function calls are not variable references." 316 | (with-temp-backed-buffer 317 | "(baz (foo))" 318 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 319 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 320 | #'elisp-refs--variable-p))) 321 | (should (null matches))))) 322 | 323 | (ert-deftest elisp-refs--find-var-ignores-defs () 324 | "Function definitions and macro definitions are not variable references." 325 | (with-temp-backed-buffer 326 | "(defun foo ()) (defmacro foo ())" 327 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 328 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 329 | #'elisp-refs--variable-p))) 330 | (should (null matches))))) 331 | 332 | (ert-deftest elisp-refs--find-var-let-without-assignments () 333 | "We should recognise let variables as variable references." 334 | (with-temp-backed-buffer 335 | "(let (foo)) (let* (foo))" 336 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 337 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 338 | #'elisp-refs--variable-p))) 339 | (should 340 | (equal 341 | matches 342 | (list '(foo 7 10) '(foo 20 23))))))) 343 | 344 | (ert-deftest elisp-refs--find-var-let-with-assignments () 345 | "We should recognise let variables as variable references." 346 | (with-temp-backed-buffer 347 | "(let ((foo 1))) (let* ((foo 2)))" 348 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 349 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 350 | #'elisp-refs--variable-p))) 351 | (should 352 | (equal 353 | matches 354 | (list '(foo 8 11) '(foo 25 28))))))) 355 | 356 | (ert-deftest elisp-refs--find-var-let-body () 357 | "We should recognise let variables as variable references." 358 | (with-temp-backed-buffer 359 | "(let ((x (1+ foo))) (+ x foo))" 360 | (let* ((elisp-refs-buf (elisp-refs--contents-buffer (buffer-file-name))) 361 | (matches (elisp-refs--read-and-find elisp-refs-buf 'foo 362 | #'elisp-refs--variable-p))) 363 | (should 364 | (equal 365 | (length matches) 366 | 2))))) 367 | 368 | (ert-deftest elisp-refs--unindent-rigidly () 369 | "Ensure we unindent by the right amount." 370 | ;; Take the smallest amount of indentation, (2 in this case), and 371 | ;; unindent by that amount. 372 | (should 373 | (equal 374 | (elisp-refs--unindent-rigidly " foo\n bar\n baz") 375 | " foo\nbar\n baz")) 376 | ;; If one of the lines has no indent, do nothing. 377 | (should 378 | (equal 379 | (elisp-refs--unindent-rigidly "foo\n bar") 380 | "foo\n bar")) 381 | ;; We should have set elisp-refs-unindented correctly. 382 | (should 383 | (equal 384 | (get-text-property 385 | 0 386 | 'elisp-refs-unindented 387 | (elisp-refs--unindent-rigidly " foo")) 388 | 2)) 389 | ;; We should still have elisp-refs-path properties in the entire string. 390 | (let ((result (elisp-refs--unindent-rigidly 391 | (propertize " foo\n bar" 'elisp-refs-path "/foo")))) 392 | (cl-loop for i from 0 below (length result) do 393 | (should 394 | (get-text-property i 'elisp-refs-path result))))) 395 | 396 | (ert-deftest elisp-refs--replace-tabs () 397 | "Ensure we replace all tabs in STRING." 398 | (let ((tab-width 4)) 399 | ;; zero tabs 400 | (should (equal (elisp-refs--replace-tabs " a ") " a ")) 401 | ;; many tabs 402 | (should (equal (elisp-refs--replace-tabs "a\t\tb") "a b")))) 403 | 404 | (ert-deftest elisp-refs--file-name-handler () 405 | "Ensure overriding `file-name-handler-alist' doesn't break our functionality." 406 | (let ((file-name-handler-alist nil) 407 | (jka-compr-verbose nil) 408 | (elisp-refs-verbose nil)) 409 | (elisp-refs-function 'buffer-substring-no-properties))) 410 | 411 | (ert-deftest elisp-refs-function () 412 | "Smoke test for `elisp-refs-function'." 413 | (let ((jka-compr-verbose nil) 414 | (elisp-refs-verbose nil)) 415 | (elisp-refs-function 'format)) 416 | (should 417 | (equal (buffer-name) "*refs: format*"))) 418 | 419 | (ert-deftest elisp-refs-macro () 420 | "Smoke test for `elisp-refs-macro'." 421 | (let ((jka-compr-verbose nil) 422 | (elisp-refs-verbose nil)) 423 | (elisp-refs-macro 'when)) 424 | (should 425 | (equal (buffer-name) "*refs: when*"))) 426 | 427 | (ert-deftest elisp-refs-variable () 428 | "Smoke test for `elisp-refs-variable'." 429 | (let ((jka-compr-verbose nil) 430 | (elisp-refs-verbose nil)) 431 | (elisp-refs-variable 'case-fold-search)) 432 | (should 433 | (equal (buffer-name) "*refs: case-fold-search*"))) 434 | 435 | (ert-deftest elisp-refs-special () 436 | "Smoke test for `elisp-refs-special'." 437 | (let ((jka-compr-verbose nil) 438 | (elisp-refs-verbose nil)) 439 | (elisp-refs-special 'prog2)) 440 | (should 441 | (equal (buffer-name) "*refs: prog2*"))) 442 | 443 | (ert-deftest elisp-refs-symbol () 444 | "Smoke test for `elisp-refs-symbol'." 445 | (let ((jka-compr-verbose nil) 446 | (elisp-refs-verbose nil)) 447 | (elisp-refs-symbol 'format-message)) 448 | (should 449 | (equal (buffer-name) "*refs: format-message*"))) 450 | 451 | (ert-deftest elisp-refs-function-prefix () 452 | "Smoke test for searching with a path prefix." 453 | (let ((jka-compr-verbose nil) 454 | (elisp-refs-verbose nil)) 455 | (elisp-refs-function 'format-message "/usr/share/emacs")) 456 | (should 457 | (equal (buffer-name) "*refs: format-message*"))) 458 | 459 | (ert-deftest elisp-refs-next-match () 460 | "Ensure movement commands move between results." 461 | (let ((jka-compr-verbose nil) 462 | (elisp-refs-verbose nil)) 463 | ;; First, get a results buffer. 464 | (elisp-refs-function 'ash)) 465 | ;; After moving to the first result, we should have a property that 466 | ;; tells us where the result came from. 467 | (elisp-refs-next-match) 468 | (should 469 | (not (null (get-text-property (point) 'elisp-refs-start-pos))))) 470 | 471 | (ert-deftest elisp-refs--start-pos () 472 | "Ensure `elisp-refs--start-pos' isn't confused by 473 | parentheses in comments." 474 | (with-temp-backed-buffer 475 | "(defun foo () 476 | ;; ( 477 | )" 478 | (should 479 | (equal 480 | (point-min) 481 | (elisp-refs--start-pos (point-max)))))) 482 | -------------------------------------------------------------------------------- /test/test-helper.el: -------------------------------------------------------------------------------- 1 | ;;; test-helper.el --- Helper for tests -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016 Wilfred Hughes 4 | 5 | ;; Author: 6 | 7 | ;;; Code: 8 | 9 | (require 'ert) 10 | (require 'f) 11 | 12 | (let ((elisp-refs-dir (f-parent (f-dirname (f-this-file))))) 13 | (add-to-list 'load-path elisp-refs-dir)) 14 | 15 | (require 'undercover) 16 | (undercover "elisp-refs.el" 17 | (:exclude "*-test.el") 18 | (:report-file "/tmp/undercover-report.json")) 19 | 20 | ;;; test-helper.el ends here 21 | --------------------------------------------------------------------------------