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

在之前的文章中,我们利用 walk-windowscompleting-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)

函数的相关参数如下:

  • 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 属性指定只在当前窗口显示

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-overlay
  • overlay-putoverlay-get
  • overlay-startoverlay-end
  • overlays-atoverlays-in
相关推荐
Masimaro4 天前
《elisp 每日一练》—— Major Mode 管理 abbrev-table
emacs
AI情绪识别开源6 天前
检信AI学业生涯智选系统技术方案概述
emacs
Masimaro9 天前
《elisp 每日一练》—— Minor Mode 回顾与综合练习
emacs
aluluka10 天前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs·lisp·elisp
aluluka11 天前
《elisp 每日一练》——minor mode 快捷键管理
emacs·lisp·elisp
Masimaro11 天前
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
emacs
Masimaro13 天前
《elisp 每日一练》——minor mode 开关变量的管理
emacs
Masimaro14 天前
《elisp 每日一练》—— Minor Mode 变量生成机制与状态控制
emacs
Masimaro15 天前
《elisp 每日一练》——文件与目录的回顾与综合练习
emacs
Masimaro22 天前
《elisp 每日一练》——minibuffer输入读取和set-transient-map 临时热键状态机
emacs