在前面我们了解了 mark 的机制以及通过 mark 和 point 来组成region,这次我们将深入了解 Emacs 是如何 管理 mark 的。
背景知识铺垫
我们通过 push-mark 和 pop-mark 来管理mark。根据这两个名词我们可能会认为Emacs 是通过栈来管理的,push-mark 表示压栈而 pop-mark 表示出栈。
其实Emacs管理mark的真实手段确实遵循后进先出的栈规则,但是pop-mark 并不会从栈结构中删除对应的mark记录。想象一下这个场景:我们之前记录了 A、B位置,现在又记录了C位置,如果我想回到上一次的B位置,要通过 pop-mark 让C位置出栈,但是如果后续我又想回到C位置怎么办呢?如果出栈的时候销毁了数据就回不来了。所以实际上 push-mark 会放入新的mark但是 pop-mark 只会改变指向栈顶的位置。并且可以在A、B、C之间循环也就是可以从C回到A。
考虑到这些场景,我们能想到应该是一个环形的链表,或者是一个首尾相连的数组,构成一个环形的数据结构。这也就是 Mark Ring 这个名称的由来。
实际上mark ring 并不能无限增长,它有一个大小的上限, 通过变量 mark-ring-max 指定,一般默认的是16。如果 ring 的长度超过了 mark-ring-max , 最旧的一个会被抛弃。
我们还是以A、B、C三个位置来举例说明每次 push-mark 和 pop-mark 之后,Emacs 如何组织环。
- 首先调用
(push-mark A)来入栈A位置,此时栈的值为'(A) - 结着调用
(push-mark B)来入栈B位置,此时栈的值为'(B A) - 然后调用
(push-mark C)来入栈C位置,此时栈的值为'(C B A) - 如果要回到C位置,直接调用
(goto-char (mark))此时栈不变 - 如果要回到B位置, 先调用 (pop-mark) ,Emacs 会取出C的值,然后放到列表的尾部,此时整个栈会变成
'(B A C) - 如果要回到A位置,与上面类似,先出栈,Emacs会改变栈数据,变为
'(A C B)
所以说我们如果想要回到上一步一个办法是不断调用 pop-mark 使对应位置。
另一个办法就是回归 mark-ring 的本质,我们在 C/C++ 语言中可以通过数组的方式来实现,但是在Emacs Lisp中,它就是一个列表。我们可以通过操作列表方式来做到这点。实际上,Emacs 通过 mark-ring 这个变量记录着当前所维护的栈。我们可以通过 describe-variable 来查看变量,并结合push-mark 和 pop-mark 函数来观看每次改变之后具体的变量值来观察上述过程。
另外,Emacs 实际维护了两组 mark-ring,一组是与buffer相关的,一组是全局的。这个很好理解,如果我们要在多个buffer中进行跳转,自然使用全局的 global-mark-ring 。但是我们并不需要特别的执行global-push-mark 这样的函数来操作全局,实际上也没有这个函数。当我们执行 push-mark 时Emacs会自己考虑是否要加入 global-mark-ring。也就说我们通过 push-mark 和 pop-mark 任意操作,Emacs会保证我们能正确记录。
一个好的习惯是涉及到大范围的光标转移不要直接使用 goto-char 就完事了,而是要考虑后续是否需要回来,也就是提前调用 push-mark 记录一下。
练习
有了这些知识作为铺垫,我们今天来做一个相对复杂一点的练习。我们写一个函数,它主要功能如下:
- 在minibuffer 中列举
mark-ring中的值,以buffer名称、行号和 point 值的形式显示 - 用户在选择某一项之后跳转到当前位置
本次练习我们用到的函数和变量如下:
mark-ring:记录当前所有保存的mark记录marker-position: 获取当前mark记录的point位置line-number-at-pos: 计算当前point 在buffer的第几行marker-buffer: 获取当前 mark 记录的buffer 对象buffer-name: 获取对应buffer 的名称
前面几个函数比较简单,我们看名称和用法大概能知道怎么用,下面我们重点介绍一个与minibuffer相关的函数: completing-read
它的函数原形如下:
(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH
INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)它用来弹出一个minibuffer的补全菜单,根据用户选择返回对应选项的字符串。PROMPT 是提示词,用于提示用户这个菜单是干什么用的。COLLECTION 是一个列表,它可以存储字符串或者以字符串为 key 的列表/哈希表/符号表,我们这里可以构建一个每项都是 '(提示词 . mark对象) 的关联CELL,后续根据用户选择所关联的 mark 进行跳转
至此我的实现思路如下:
- 遍历
mark-ring取出其中的每个marker - 针对
marker调用函数获取buffer名称、行号、位置等信息 - 根据上述信息组织字符串并组成一个
'(字符串 . mark对象)的关联CELL - 将关联CELL传入
completing-read,弹出提示 - 根据用户选择提示,获取返回选中的字符串
- 根据字符串查找关联的 mark 对象
- 最后执行 goto-char 跳转到mark对象所对应的位置
以下是我实现的代码:
(defun my/browser-mark-ring ()
(interactive)
(if (null mark-ring)
(message "mark-ring 为空")
(let ((candidates '()))
(dolist (m mark-ring) ;; 遍历列表
(when (and (markerp m) (marker-position m) (marker-buffer m))
(let* ((position (marker-position m))
(buf (marker-buffer m))
(line (line-number-at-pos position buf))
(bname (buffer-name buf))
(label (format "%s | Line %d, pos %d" bname line position)))
;; 组织对应数据结构 (label . marker)
(push (cons label m) candidates))))
(if (null candidates)
(message "没有有效的 Mark 位置")
(let* ((selected (completing-read "请选择要跳转的位置: " candidates nil t)))
(when selected
(let ((target (cdr (assoc selected candidates))))
(push-mark)
(switch-to-buffer (marker-buffer target))
(goto-char target))))))))最终它的效果如下:

总结
本篇我们详细了解了 mark-ring 的机制,它就是一个列表,我们可以通过操作列表的方式操作它。并且Emacs自己为每个buffer维护了专属的 mark-ring 和通用的全局 global-mark-ring
另外通过这个例子我们知道下面几个函数:
mark-ring:记录当前所有保存的mark记录marker-position: 获取当前mark记录的point位置line-number-at-pos: 计算当前point 在buffer的第几行marker-buffer: 获取当前 mark 记录的buffer 对象buffer-name: 获取对应buffer 的名称completing-read: 负责与minibuffer 交互并返回选中的字符串值
评论 (0)