《elisp 每日一练》—— overlay 机制

《elisp 每日一练》—— overlay 机制

Masimaro
2026-08-14 / 0 评论 / 0 阅读 / 正在检测是否收录...

在之前的文章中,我们利用 walk-windowscompleting-read 实现了一个可以跳转到任意窗口的程序。但是我们通过 buffer + window 的方式进行显示,如果buffer不重名还好,用户可以根据buffer名称来选择,而当buffer有重名,用户没办法直观的看出哪个项对应哪个窗口。如果读者尝试过配置自己的Emacs,在管理窗口方面一定绕不开 ace-window这个插件,它提供了一个非常优雅高效的方式。今天将介绍overlay机制,并且利用该机制实现一个类似的跳转功能。

背景知识介绍

对于buffer的显示效果,Emacs提供了两种改变的机制,一种是通过设置 Text properties 来改变文本的字号、字体大小等属性,是 buffer 文本的一部分,会随文本一起复制、保存。另一个就是 Overlay 机制,它只影响显示,不进入 undo 列表,也不随文本复制。

对于 Overlay 官方给它的定位是:

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,它的原形如下:

(make-overlay BEG END &optional BUFFER FRONT-ADVANCE REAR-ADVANCE)

函数的相关参数如下:

  • BEGEND : 指定overlay起始位置和终点位置
  • BUFFER : 指定叠加层属于哪个缓冲区, 默认为当前缓冲区
  • FRONT-ADVANCEREAR-ADVANCE 表示当开头或者结尾被插入新字符时,overlay 该如何变化

我们来了解一下最后两个参数的含义。我们还是将它想象成一个便利贴。假设便利贴我们从头到尾写满了文字,这个时候我们想要在便利贴开始位置或者结束位置写一个新的内容,该怎么办? FRONT-ADVANCEREAR-ADVANCE 就是来处理这种情况的。一种办法是丢失这些内容,纸张的大小不变,这个时候对应着 FRONT-ADVANCE 为非 nilREAR-ADVANCEnil。还有一种办法就是纸张大小改变,我们在前面和后面插入内容,此时对应着 FRONT-ADVANCEnilREAR-ADVANCE 为非 nil

关于属性,我们有 overlay-putoverlay-get 来设置或者获取属性。

面对一个陌生的,不是我们创建的 overlay,我们可以通过 overlay-startoverlay-end 来获取它的起始和结束位置

另外我们可以通过 overlays-atoverlays-in 来获取区域内的所有 overlay

我们常用的属性有:

  • face 设置样式,包括背景色、前景色、字体等
  • display : 替换overlay中的的显示内容
  • priority : 重叠时的 z序
  • window : 限定只在指定窗口生效

overlay 实际是对于 buffer 的修改,所以默认情况下,如果多个窗口都显示同一个buffer,那么每个窗口都会显示这样的内容,我们可以通过 window 属性,限定只在指定的窗口中显示

例如我们可以尝试写下如下代码,来创造一个显示内容为 "标记" 的黄低红色的标记,并且通过 window 属性指定只在当前窗口显示

(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) 后续根据用户输入的字母或者数字来找到窗口并跳转。

(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 类似,它的定义如下:

(unwind-protect body-form cleanup-forms...)

它主要执行body-form 中的内容,并且保证会执行 cleanup-forms 中的清理代码。

总结

本节我们学习了如何给buffer 加上 overlay ,用来展示一些信息。本节介绍的函数有

  • make-overlay
  • overlay-putoverlay-get
  • overlay-startoverlay-end
  • overlays-atoverlays-in
0

评论 (0)

取消