YE11: Fix find-function for Emacs Lisp from org-babel or scratch
| org, emacs, elisp, stream, yay-emacs
Where can you define an Emacs Lisp function so
that you can use find-function to jump to it
again later?
- A: In an indirect buffer from Org Mode source
block with your favorite eval function like
eval-defunC-c '(org-edit-special) inside the block; execute the defun withC-M-x(eval-defun),C-x C-e(eval-last-sexp), oreval-buffer.(defun my-test-1 () (message "Hello"))
B: In an Org Mode file by executing the block with C-c C-c
(defun my-test-2 () (message "Hello"))C: In a .el file
file:///tmp/test-search-function.el : execute the defun with
C-M-x(eval-defun),C-x C-e(eval-last-sexp), oreval-bufferD: In a scratch buffer, other temporary buffer, or really any buffer thanks to eval-last-sexp
(defun my-test-4 () (message "Hello"))
Only option C works - it's gotta be in an .el file for
find-function to find it. But I love jumping to
function definitions using find-function or
lispy-goto-symbol (which is bound to M-. if
you use lispy and set up lispy-mode) so
that I can look at or change how something works.
It can be a little frustrating when I try to jump
to a definition and it says, "Don't know where
blahblahblah is defined." I just defined it five
minutes ago! It's there in one of my other
buffers, don't make me look for it myself.
Probably this will get fixed in Emacs core
someday, but no worries, we can work around it
today with a little bit of advice.
I did some digging around in the source code.
Turns out that symbol-file can't find the
function definition in the load-history variable
if you're not in a .el file, so
find-function-search-for-symbol gets called with
nil for the library, which causes the error.
(emacs:subr.el)
I wrote some advice that searches in any open
emacs-lisp-mode buffers or in a list of other
files, like my Emacs configuration.
This is how I activate it:
(setq sacha-elisp-find-function-search-extra '("~/sync/emacs/Sacha.org"))
(advice-add 'find-function-search-for-symbol :around #'sacha-elisp-find-function-search-for-symbol)
Now I should be able to jump to all those functions wherever they're defined.
(my-test-1)
(my-test-2)
(my-test-3)
(my-test-4)
Note that by default, M-. in emacs-lisp-mode uses xref-find-definitions, which seems to really want files. I haven't figured out a good workaround for that yet, but lispy-mode makes M-. work and gives me a bunch of other great shortcuts, so I'd recommend checking that out.
Here's the source code for the find function thing:
(defvar sacha-elisp-find-function-search-extra
nil
"List of filenames to search for functions.")
;;;###autoload
(defun sacha-elisp-find-function-search-for-symbol (fn symbol type library &rest _)
"Find SYMBOL with TYPE in Emacs Lisp buffers or `sacha-find-function-search-extra'.
Prioritize buffers that do not have associated files, such as Org Src
buffers or *scratch*. Note that the fallback search uses \"^([^ )]+\" so that
it isn't confused by preceding forms.
If LIBRARY is specified, fall back to FN.
Activate this with:
(advice-add 'find-function-search-for-symbol
:around #'sacha-org-babel-find-function-search-for-symbol-in-dotemacs)"
(if (null library)
;; Could not find library; search my-dotemacs-file just in case
(progn
(while (and (symbolp symbol) (get symbol 'definition-name))
(setq symbol (get symbol 'definition-name)))
(catch 'found
(mapc
(lambda (buffer-or-file)
(with-current-buffer (if (bufferp buffer-or-file)
buffer-or-file
(find-file-noselect buffer-or-file))
(let* ((regexp-symbol
(or (and (symbolp symbol)
(alist-get type (get symbol 'find-function-type-alist)))
(alist-get type find-function-regexp-alist)))
(form-matcher-factory
(and (functionp (cdr-safe regexp-symbol))
(cdr regexp-symbol)))
(regexp-symbol (if form-matcher-factory
(car regexp-symbol)
regexp-symbol))
(case-fold-search)
(regexp (if (functionp regexp-symbol) regexp-symbol
(format (symbol-value regexp-symbol)
;; Entry for ` (backquote) macro in loaddefs.el,
;; (defalias (quote \`)..., has a \ but
;; (symbol-name symbol) doesn't. Add an
;; optional \ to catch this.
(concat "\\\\?"
(regexp-quote (symbol-name symbol)))))))
(save-restriction
(widen)
(with-syntax-table emacs-lisp-mode-syntax-table
(goto-char (point-min))
(if (if (functionp regexp)
(funcall regexp symbol)
(or (re-search-forward regexp nil t)
;; `regexp' matches definitions using known forms like
;; `defun', or `defvar'. But some functions/variables
;; are defined using special macros (or functions), so
;; if `regexp' can't find the definition, we look for
;; something of the form "(SOMETHING <symbol> ...)".
;; This fails to distinguish function definitions from
;; variable declarations (or even uses thereof), but is
;; a good pragmatic fallback.
(re-search-forward
(concat "^([^ )]+" find-function-space-re "['(]?"
(regexp-quote (symbol-name symbol))
"\\_>")
nil t)))
(progn
(beginning-of-line)
(throw 'found
(cons (current-buffer) (point))))
(when-let* ((find-expanded
(when (trusted-content-p)
(find-function--search-by-expanding-macros
(current-buffer) symbol type
form-matcher-factory))))
(throw 'found
(cons (current-buffer)
find-expanded)))))))))
(delq nil
(append
(sort
(match-buffers '(derived-mode . emacs-lisp-mode))
:key (lambda (o) (or (buffer-file-name o) "")))
sacha-elisp-find-function-search-extra)))))
(funcall fn symbol type library)))
I even figured out how to write tests for it:
(ert-deftest sacha-elisp--find-function-search-for-symbol--in-buffer ()
(let ((sym (make-temp-name "--test-fn"))
buffer)
(unwind-protect
(with-temp-buffer
(emacs-lisp-mode)
(insert (format ";; Comment\n(defun %s () (message \"Hello\"))" sym))
(eval-last-sexp nil)
(setq buffer (current-buffer))
(with-temp-buffer
(let ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
(should (equal (car pos) buffer))
(should (equal (cdr pos) 12)))))
(fmakunbound (intern sym)))))
(ert-deftest sacha-elisp--find-function-search-for-symbol--in-file ()
(let* ((sym (make-temp-name "--test-fn"))
(temp-file (make-temp-file
"test-" nil ".org"
(format
"#+begin_src emacs-lisp\n;; Comment\n(defun %s () (message \"Hello\"))\n#+end_src"
sym)))
(sacha-elisp-find-function-search-extra (list temp-file))
buffer)
(unwind-protect
(with-temp-buffer
(let ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
(should (equal (buffer-file-name (car pos)) temp-file))
(should (equal (cdr pos) 35))))
(delete-file temp-file))))