向前移动的 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))

该表达式会逐行向前移动光标,只要以下四个条件均成立:

  1. 光标未到达缓冲区末尾。
  2. 可以移动到文本左页边距且未到缓冲区末尾。
  3. 光标后的文本不用于分隔段落。
  4. 光标后的模式为填充前缀正则表达式。

最后一个条件初看有些费解,但别忘了在 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))))

(注意该代码片段直接复制自原始代码,因此末尾两个额外的右括号用于匹配前面的 ifwhile。)

含义是:如果不存在填充前缀且未到缓冲区末尾,光标应跳转到正则表达式搜索 sp-parstart 所找到内容的开头。

forward-paragraph 函数的完整定义不仅包含向前移动的代码,也包含向后移动的代码。

如果你正在 GNU Emacs 内阅读本文并想查看完整函数,可以按 C-h fdescribe-function)并输入函数名。这会显示函数文档以及包含函数源码的库名称。将光标放在库名称上并按 RET 键,即可直接跳转到源码。(务必安装源码!没有源码,你就像闭着眼睛开车一样!)