在之前的文章中,我们利用 walk-windows 和 completing-read 实现了一个可以跳转到任意窗口的程序。但是我们通过 buffer + window 的方式进行显示,如果buffer不重名还好,用户可以根据buffer名称来选择,而当buffer有重名,用户没办法直观的看出哪个项对应哪个窗口。如果读者尝试过配置自己的Emacs,在管理窗口方面一定绕不开 ace-window这个插件,它提供了一个非常优雅高效的方式。今天将介绍overlay机制,并且利用该机制实现一个类似的跳转功能。
背景知识介绍
对于buffer的显示效果,Emacs提供了两种改变的机制,一种是通过设置 Text properties 来改变文本的字号、字体大小等属性,是 buffer 文本的一部分,会随文本一起复制、保存。另一个就是 Overlay 机制,它只影响显示,不进入 undo 列表,也不随文本复制。
对于 Overlay 官方给它的定位是:
txt
You can use overlays to alter the appearance of a buffer's text on the screen, for the sake of presentation features. An overlay is an object that belongs to a particular buffer, and has a specified beginning and end.
你可以将它想象成一个贴在buffer上的便利贴。如果要使用的话,我们首先要创建一个便利贴、然后决定便利贴的位置,最后在不需要时销毁它。并且Emacs还提供了一些操作它的函数。下面我们来列举一些重要的函数。
创建可以使用函数 make-overlay,它的原形如下:
emacs-lisp
(make-overlay BEG END &optional BUFFER FRONT-ADVANCE REAR-ADVANCE)
函数的相关参数如下:
BEG和END: 指定overlay起始位置和终点位置BUFFER: 指定叠加层属于哪个缓冲区, 默认为当前缓冲区FRONT-ADVANCE和REAR-ADVANCE表示当开头或者结尾被插入新字符时,overlay 该如何变化
我们来了解一下最后两个参数的含义。我们还是将它想象成一个便利贴。假设便利贴我们从头到尾写满了文字,这个时候我们想要在便利贴开始位置或者结束位置写一个新的内容,该怎么办? FRONT-ADVANCE 和 REAR-ADVANCE 就是来处理这种情况的。一种办法是丢失这些内容,纸张的大小不变,这个时候对应着 FRONT-ADVANCE 为非 nil 而 REAR-ADVANCE 为 nil。还有一种办法就是纸张大小改变,我们在前面和后面插入内容,此时对应着 FRONT-ADVANCE 为 nil 而 REAR-ADVANCE 为非 nil
关于属性,我们有 overlay-put 和 overlay-get 来设置或者获取属性。
面对一个陌生的,不是我们创建的 overlay,我们可以通过 overlay-start 和 overlay-end 来获取它的起始和结束位置
另外我们可以通过 overlays-at 和 overlays-in 来获取区域内的所有 overlay。
我们常用的属性有:
face设置样式,包括背景色、前景色、字体等display: 替换overlay中的的显示内容priority: 重叠时的 z序window: 限定只在指定窗口生效
overlay 实际是对于 buffer 的修改,所以默认情况下,如果多个窗口都显示同一个buffer,那么每个窗口都会显示这样的内容,我们可以通过 window 属性,限定只在指定的窗口中显示
例如我们可以尝试写下如下代码,来创造一个显示内容为 "标记" 的黄低红色的标记,并且通过 window 属性指定只在当前窗口显示
emacs-lisp
(let ((overlay (make-overlay (point-min) (+ 10 (point-min)))))
(overlay-put overlay 'window (selected-window))
(overlay-put overlay 'face '(:background "red" :foreground "yellow"))
(overlay-put overlay 'display "A"))
练习
到此我们差不多已经掌握了 overlay 的绝大部分用法了。可以开始我们的改造行动了。
我们的思路仍然是先遍历,然后给每个窗口打上标签,并且记录到一个列表中,列表中每项的内容为 (LABLE . window) 后续根据用户输入的字母或者数字来找到窗口并跳转。
emacs-lisp
(defvar my/all-window-list '()
"保存窗口字典的列表")
(defun my/show-overlay(label win)
"在左上角创建显示字母的 overlay"
(with-selected-window win
(let* ((pos (window-start))
(ol (make-overlay pos (+ 10 pos) (window-buffer win))))
(overlay-put ol 'display label)
(overlay-put ol 'face '(:foreground "red" :weight bold :background "yellow"))
(overlay-put ol 'window win)
ol))) ;; 返回 ol 用于清理
(defun my/jump-to-selected-window()
"根据用户按下的字母标签进行跳转"
(interactive)
(let*((chars (mapcar #'identity "abcdefghijklmnopqrstuvwxyz")) ;; 构建一个字母的字典
(overlays '()) ;; 保存所有的overlay 方便后面删除
(index 0)) ;; 存储当前使用的字母的索引
(walk-windows (lambda(win)
(when (< index (length chars))
(let* ((ch (nth index chars))
(ol (my/show-overlay (string ch) win)))
(when ol
(push ol overlays)
(push (cons ch win) my/all-window-list)))
(setq index (1+ index)))))
;; 读取用户按键并跳转
(unwind-protect
(let* ((char (read-char "选择窗口: "))
(win (cdr (assoc char my/all-window-list))))
(if win
(select-window win)
(message "没有选择对应的窗口")))
;;清理资源
(mapc #'delete-overlay overlays)
(setq overlays nil))))
上述代码中使用了一个新的宏 unwind-protect。它的作用与try...catch 中的 finally 类似,它的定义如下:
emacs-lisp
(unwind-protect body-form cleanup-forms...)
它主要执行body-form 中的内容,并且保证会执行 cleanup-forms 中的清理代码。
总结
本节我们学习了如何给buffer 加上 overlay ,用来展示一些信息。本节介绍的函数有
make-overlayoverlay-put和overlay-getoverlay-start和overlay-endoverlays-at和overlays-in