while 循环 ¶let* 主体的第二部分处理向前移动,是一个只要 arg 值大于 0 就会重复执行的 while 循环。在该函数最常见的使用场景中,参数值为 1,因此 while 循环的主体只会被求值一次,光标向前移动一个段落。
这部分处理三种情况:光标位于段落之间、存在填充前缀、不存在填充前缀。
该 while 循环如下:
;; 向前移动且未到达缓冲区末尾 (while (and (> arg 0) (not (eobp))) ;; 段落之间 ;; 向前跳过分隔行... (while (and (not (eobp)) (progn (move-to-left-margin) (not (eobp))) (looking-at parsep)) (forward-line 1)) ;; 递减循环计数 (unless (eobp) (setq arg (1- arg))) ;; ... 再前进一行。 (forward-line 1)
(if fill-prefix-regexp
;; 存在填充前缀,优先级高于 parstart;
;; 逐行向前移动
(while (and (not (eobp))
(progn (move-to-left-margin) (not (eobp)))
(not (looking-at parsep))
(looking-at fill-prefix-regexp))
(forward-line 1))
;; 不存在填充前缀;
;; 逐字符向前移动
(while (and (re-search-forward sp-parstart nil 1)
(progn (setq start (match-beginning 0))
(goto-char start)
(not (eobp)))
(progn (move-to-left-margin)
(not (looking-at parsep)))
(or (not (looking-at parstart))
(and use-hard-newlines
(not (get-text-property (1- start) 'hard)))))
(forward-char 1))
;; 若无填充前缀且未到缓冲区末尾,
;; 跳转到正则表达式搜索 sp-parstart 找到的位置
(if (< (point) (point-max))
(goto-char start))))
可以看出这是一个递减计数器型 while 循环,使用表达式 (setq arg (1- arg)) 作为递减器。该表达式距离 while 不远,但隐藏在另一个 Lisp 宏 unless 中。除非我们到达缓冲区末尾(由 eobp 函数判断,是 ‘End Of Buffer P’ 的缩写),否则将 arg 的值减 1。
(如果已到缓冲区末尾,就无法再向前移动,由于判断条件包含 (not (eobp)),while 表达式的下一轮循环会判定为假。not 函数的含义正如其名,它是 null 的别名,在参数为假时返回真。)
有趣的是,循环计数直到我们离开段落之间的空白区域才会递减,除非到达缓冲区末尾或不再匹配段落分隔的局部值。
第二个 while 还包含一个 (move-to-left-margin) 表达式。该函数顾名思义,它位于 progn 表达式内部且不是主体最后一个元素,因此仅为执行其副作用:将光标移动到当前行的左页边距。
looking-at 函数也顾名思义:如果光标后的文本匹配其参数指定的正则表达式,则返回真。
循环主体的其余部分初看有些复杂,但理解后就会变得清晰。
首先看存在填充前缀时的情况:
(if fill-prefix-regexp
;; 存在填充前缀,优先级高于 parstart;
;; 逐行向前移动
(while (and (not (eobp))
(progn (move-to-left-margin) (not (eobp)))
(not (looking-at parsep))
(looking-at fill-prefix-regexp))
(forward-line 1))
该表达式会逐行向前移动光标,只要以下四个条件均成立:
最后一个条件初看有些费解,但别忘了在 forward-paragraph 函数开头光标已被移动到行首。这意味着如果文本存在填充前缀,looking-at 函数就能检测到。
再看不存在填充前缀时的情况。
(while (and (re-search-forward sp-parstart nil 1)
(progn (setq start (match-beginning 0))
(goto-char start)
(not (eobp)))
(progn (move-to-left-margin)
(not (looking-at parsep)))
(or (not (looking-at parstart))
(and use-hard-newlines
(not (get-text-property (1- start) 'hard)))))
(forward-char 1))
该 while 循环会向前搜索 sp-parstart,即可能的空白字符加上段落起始或段落分隔的局部值。(后两者位于以 \(?: 开头的表达式内,因此不会被 match-beginning 函数引用。)
以下两个表达式:
(setq start (match-beginning 0)) (goto-char start)
含义是跳转到正则表达式搜索匹配文本的开头。
(match-beginning 0) 表达式是新知识点。它返回一个数字,指定上一次搜索匹配文本的起始位置。
这里使用 match-beginning 函数是因为向前搜索的一个特性:无论普通搜索还是正则表达式搜索,成功的向前搜索都会将光标移动到找到文本的末尾。在本例中,成功的搜索会将光标移动到 sp-parstart 模式的末尾。
但我们希望将光标放在当前段落的末尾,而非其他位置。实际上,由于搜索可能包含段落分隔符,如果不使用包含 match-beginning 的表达式,光标可能会停在下一段落的开头。
当参数为 0 时,match-beginning 返回最近一次搜索匹配文本的起始位置。在本例中,最近一次搜索的目标是 sp-parstart。(match-beginning 0) 表达式返回该模式的起始位置,而非结束位置。
(顺便一提,当传入正数作为参数时,match-beginning 函数返回上一次搜索中对应括号表达式的光标位置,除非该括号表达式以 \(?: 开头。我不清楚此处为何出现 \(?:,因为参数是 0。)
不存在填充前缀时的最后一个表达式是:
(if (< (point) (point-max))
(goto-char start))))
(注意该代码片段直接复制自原始代码,因此末尾两个额外的右括号用于匹配前面的 if 和 while。)
含义是:如果不存在填充前缀且未到缓冲区末尾,光标应跳转到正则表达式搜索 sp-parstart 所找到内容的开头。
forward-paragraph 函数的完整定义不仅包含向前移动的代码,也包含向后移动的代码。
如果你正在 GNU Emacs 内阅读本文并想查看完整函数,可以按 C-h f(describe-function)并输入函数名。这会显示函数文档以及包含函数源码的库名称。将光标放在库名称上并按 RET 键,即可直接跳转到源码。(务必安装源码!没有源码,你就像闭着眼睛开车一样!)