在Elisp中退出当前调用栈并执行另一个函数的方法
Table of Contents
今天我想要分享一段有趣的Elisp代码. 想象一下这么一段代码:
(defun useful-command () (interactive) (do-thing-1) (do-thing-2 (funcall callback-function)) (do-thing-3))
有时候会有这种需求, 你在执行 callback-function
的时间点上,想跳出正在执行的这个函数(本例中指的是那个 useful-command
),在保持当前环境的情况下转而调用另一个函数.
我是这样来实现这个需求的:
(defmacro ivy-quit-and-run (&rest body) "Quit the minibuffer and run BODY afterwards." `(progn (put 'quit 'error-message "") (run-at-time nil nil (lambda () (put 'quit 'error-message "Quit") ,@body)) (minibuffer-keyboard-quit)))
让我们仔细分析一下:
minibuffer-keyboard-quit
会展开调用栈,并直接退出到command loop,从而阻止了do-thing-3
的执行. 这里的调用栈可以任意深,比如useful-command
可能又被utility-command
所调用,诸如此类.- 带nil参数的
run-at-time
会尽可能快的运行后面的代码,几乎就在我们回到command loop的那一刻就会运行. - 最后要留意的就是不要在minibuffer上显示Quit信息.
1 Sample application
假设我在ivy-mode激活的情况下调用了 find-file
函数. 一般情况下, 我会选择一个文件,然后按下 C-m
打开这个文件. 然而, 有时候我只是想用 dired
看一下这个文件而不是想打开这个文件.
下面这段代码来至于ivy multi-action interface, 它是 ivy-minibuffer-map
中绑定的一个命令:
(define-key ivy-minibuffer-map (kbd "C-:") 'ivy-dired) (defun ivy-dired () (interactive) (if ivy--directory (ivy-quit-and-run (dired ivy--directory) (when (re-search-forward (regexp-quote (substring ivy--current 0 -1)) nil t) (goto-char (match-beginning 0)))) (user-error "Not completing files currently")))
那么,在 C-:
被按下的那一刻,调用栈是这样子的: is pressed, the call stack is:
C-x C-f
调用find-file
函数.find-file
调用completion-read-function
函数,不过实际上调用的是ivy-completing-read
函数.ivy-completing-read
调用了ivy-read
函数.ivy-read
调用了read-from-minibuffer
.
宏 ivy-quit-and-run
让我能够展开所有的调用栈,确保从 read-from-minibuffer
退出后,不会再运行剩下的代码.
换句话说,Emacs不会打开该文件,反而会打开一个dired buffer,并选中这个文件.
我上面所描述的场景应该蛮普遍的, 你可以用类似的方法改造 helm
, avy
,以及 projectile
. 基本上,所有包含某种形式的补全(例如那些会等待输入的命令)并提供可定制keymap的东西,都能够应用这种方法.
美中不足的是: 这种方法只是一种 quick-and-dirty 的解决方案. 如果可以的话,我并不推荐使用这种方法. 不过如果你别无他法,那么这个宏也许能够帮到你.