Elisp 动态生成函数的跳转

Emacs 每加载一个文件,都会把其中定义的变量、函数等等记录到 load-history:

("/path/to/foo.el"
 (defun . foo-fun1)
 (defun . foo-fun2)
 ...)

这就是跳转的关键。通过这些信息,定位到文件,然后查找定义所在。

然而,如果 defun 语句写在另一个函数里面,Emacs 是不会有任何记录的。(这种情况在我的配置中有不少,为此我写了几个函数,在必要时扫描配置文件,生成结果作为 load-history 的补充。细节暂不表。)

还有另一种情况:如果一个函数连名字都是在另外一个宏里拼接的,Emacs 就更不会记录了:

(defmacro def-greeting (name)
  "Generate a greeting function."
  `(defun ,(intern (format "hello-%s" name)) ()
     ,(format "Say hello to ‘%s’." name)
     (message "hello, %s" ',name)))

(def-greeting foo)
;; => hello-foo

这就不能简单扩充 load-history 了,因为它要求具体的名称以便查找,而上边的例子当中并没有 hello-foo 字样出现。除非改造跳转的查找方式,然后在 load-history 里边记录:

("/path/to/greeting.el"
 (defun . (hello-foo :search-pattern "(def-greeting foo)")))

后续还有不少工作要做,与其这样不如在 docstring 记录下相关信息(反正也是要写 docstring 的),这样即便不跳转到定义,在 describe-function 也能得到足够的信息:

(defun gen-docstring ()
  "Generate docstring."
  (let* ((fram (backtrace-frame 5))
         (defn (nth 2 fram))
         (file (or load-file-name (buffer-file-name))))
    (format "This function is generated by `%s' in file `%s:%s'."
            (car defn) file defn)))

(defmacro def-greeting (name)
  "Generate a greeting function."
  (let ((gdoc (gen-docstring)))
    `(defun ,(intern (format "hello-%s" name)) ()
       ,(concat (format "Say hello to ‘%s’.\n\n" name) gdoc)
       (message "hello, %s" ',name))))

(def-greeting foo)
;; => hello-foo

gen-docstring 生成的信息可跳转(参考了 @xuchunyang 添加个删除 Advice 函数的按钮):

(define-advice describe-function-1 (:after (function) add-jump-link)
  "Add a jump link."
  (when (get-buffer "*Help*")
    (with-current-buffer "*Help*"
      (save-excursion
        (goto-char (point-min))
        (when (re-search-forward "This function is generated by ‘[^’]+’ in file ‘\\(\\([^:]+\\):\\([^’]+\\)\\)’." nil t)
          (let ((file (match-string 2))
                (defn (match-string 3))
                (inhibit-read-only t))
            (replace-match "" nil nil nil 1)
            (insert-text-button
             file
             'cursor-sensor-functions `((lambda (&rest _) (message "%s" ',defn)))
             'help-echo (format "%s" defn)
             'action
             `(lambda (_)
                (find-file-other-window ,file)
                (goto-char (point-min))
                (search-forward ,defn))
             'follow-link t)))))))

5 个赞