emacs是利用elisp写成的,而elisp是lisp的一个方言。lisp语言是出名的优美和晦涩,当然,更出名的是括号。。。
emacs利用elisp作为上层抽象。首先,emacs提供了基本的编辑器框架,包括文件操作函数API,buffer,frame,windows的API。而后,emacs附带了很多函数实现,并且和按键一一绑定。例如Ctrl+N(简写为C-N)就被绑定到“换到下一行”这个API上。于是,我们按下Ctrl+N的时候,就会触发“换到下一行”这个函数的执行。dired等插件也是基于类似的原理写成。
我们可以用类似的方法,来编写自己的函数,扩充emacs的功能。下面我们看一个例子:
(defun popup-term ()
(interactive)
(apply 'start-process "terminal" nil popup-terminal-command)
)
首先先说明一下,elisp的基于规则是利用括号匹配的s表达式,通过特定规则计算表达式。每个表达式由多个原子构成,一个原子可以是符号,对象(数字或者字符串),序对,表(包括空表),树,以及他们的嵌套。求值的时候,第一个原子做动词,先求值第一个原子,直到得到一个对象,再根据第一个原子的特性决定正则序和应用序。应用序的先对每个后续原子求值,再调用第一个原子对应的对象。正则序直接交给第一个原子对应对象处理。上文那个表达式,最外层的是(defun)列表,defun是函数定义函数,popup-term是符号。这部分混合起来,就是定义(interactive) (apply 'start-process "terminal" nil popup-terminal-command)为一个函数,并在上层框架空间内把内容赋值给popup-term这个符号。说的更直白一点,就是定义函数。
def apply(func, *param): return globals()[func](*param)
这个函数真正的部分,是从start-process到括号结束。其意义是启动一个子进程,名为terminal,没有对应的buffer(熟悉emacs的应该知道这是什么),命令为popup-terminal-command。这个命令在windows下和linux下有不同定义,所以我将这个定义放在了emacs-win.el和emacs-linux.el里面。在linux下,他是这么定义的。
(setq popup-terminal-command '("x-terminal-emulator"))
setq是设定一个全局变量。整句合起来的意思是,在执行popup-term的时候,启动一个子进程,执行x-terminal-emulator。最后,将popup-term绑定到keymap上。
(global-set-key [(control c) (s)] 'popup-term)
现在,在任何一个buffer中按下C-c s,就可以弹出当前目录对应的term了。
我们在emacs中所做的所有配置,插件安装,其实本质上是写代码控制其他代码的载入,变更环境变量。只要有合适的文档,或者有时间阅读源码,我们就可以对其他程序进行扩充。下面介绍一个对dired进行扩充的例子,我们向dired中加入copy-from和rename-from,还有dired-open功能。dired的copy和rename必须在源目录中,选择文件,按C,输入目标路径。有的时候我们在某个目录工作到一半,突然需要从另外一个目录复制一个文件过来。这时候打开对方目录进行复制动作太繁琐,因此我编写了两个函数,分别绑定到r和c上。dired-open则是另外一个文件,有时我们需要通过其他程序打开某个文件,例如播放电影。在dired中直接用&可以实现这个目标,但是需要自行输入播放命令,而且会新开一个buffer。以下是代码。
(defun dired-open-file (&optional arg)
(interactive)
(apply 'start-process "dired-open" nil
(append (split-string
(read-shell-command
"command: " (dired-guess-cmd (dired-get-filename))))
(list (dired-get-filename)))))
(defun dired-copy-from (&optional arg)
(interactive)
(let ((source-path (read-file-name "filepath: ")))
(copy-file source-path (file-name-nondirectory source-path))))
(defun dired-rename-from (&optional arg)
(interactive)
(let ((source-path (read-file-name "filepath: ")))
(rename-file source-path (file-name-nondirectory source-path))))
(add-hook 'dired-mode-hook
(lambda ()
(define-key dired-mode-map "b" 'dired-open-file)
(define-key dired-mode-map "c" 'dired-copy-from)
(define-key dired-mode-map "r" 'dired-rename-from)
(define-key dired-mode-map [(control c) (g)] 'dired-etags-tables)
))
如上文一样,我们定义了dired-open-file函数,这个函数的核心部分是start-process,但是在命令上,我们的命令是这个。
(append (split-string
(read-shell-command
"command: " (dired-guess-cmd (dired-get-filename))))
(list (dired-get-filename)))
这段代码,是将两个表进行混合。第一个是从mini-buffer读取入一个命令行,而后分解为列表,读取时的默认值由dired-guess-cmd这个函数确定。第二个表是当前光标所在的文件名。两者合起来,在执行的时候就会变成命令行后加上文件名的执行效果。而dired-guess-cmd这个函数,接受当前文件名作为参数,猜测一个正确的命令行。在windows下,大多数时候等于start,linux下则是大多数时候等于xdg-open。
下面的dired-copy-from和dired-rename-from函数非常简单,大家可以自行分析。不明白的函数可以查阅上文那份elisp的参考手册。
最后,我们把这些函数绑定到键上。由于不是全局绑定,因此不能使用global-set-key。非常幸运的,dired在加载的时候会调用dired-mode-hook。我们将一个lambda函数加入这个函数调用中,这个lambda函数中,使用define-key定义了键和函数的对应关系。于是,我们在dired模式下,通过b c r就可以调用以上三个我们编写的功能了。
elisp可以通过pymacs和python进行混编调用,但是通常并不需要这么复杂而强大的东西。我们可以通过进程级python调用的方式调用python脚本达成任务。例如一下这个脚本。
(defun dired-etags-tables ()
(interactive)
(let ((etags-path (expand-file-name (read-directory-name "etags path:")))
(python-command (expand-file-name "~/.emacs.d/gen_etags.py")))
(call-process "python" nil t nil python-command etags-path)))
在某个目录下,按下C-c g,会自动生成目录下所有可能的程序文件的TAG,供visit-tag-table使用。这个python脚本就不附上了,大家可以用自己喜欢的语言编写,例如ruby或者bash。
最后,在此提供一个比较简单的emacs脚本调试技巧。通常我们写好程序,在表达式的括号上按下C-M-x就可以eval这个表达式。用这个方法,可以在不重启emacs的情况下更新函数。类似的,用C-u C-M-x就可以对函数设定EDEBUG包装。这样当你调用这个函数时,会弹出断点窗口。按下SPC就可以单步执行脚本。