前进动作 while 循环

let* 的第二部分处理前进动作。它是一个 while 循环,只要 arg 的值大于零就重复执行。在函数的最常见用法中,参数的值为 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 的值减小一。

(如果我们在缓冲区末尾,我们无法再向前移动,由于测试是与 (not (eobp))and,下一个循环的表达式将测试为假,因为 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) 表达式是新的。它返回一个指定由最近的搜索匹配的文本的开头位置的数字。

当给定参数 0 时,match-beginning 返回最近搜索匹配的文本的开头位置。在这种情况下,最近的搜索寻找 sp-parstart(match-beginning 0) 表达式返回该模式的开头位置,而不是该模式的结束位置。

(顺便说一下,当作为参数传递一个正数时,match-beginning 函数返回最后一次搜索中括号表达式的位置,除非该括号表达式以 \(?: 开头。我不知道为什么这里出现 \(?:,因为参数是 0。)

当没有填充前缀时的最后一个表达式是

(if (< (point) (point-max))
    (goto-char start))))

这表示如果没有填充前缀且不在末尾,点应该移动到由正则表达式搜索 sp-parstart 找到的内容的开头。

forward-paragraph 函数的完整定义不仅包括前进的代码,还包括后退的代码。

如果您在 GNU Emacs 中阅读此内容并想查看整个函数,请键入 C-h fdescribe-function)和函数的名称。这会给您函数文档和包含函数源代码的库的名称。将点放在库的名称上,然后按 RET 键;您将直接转到源代码。(确保安装了源代码!没有源代码,您就像闭着眼睛开车的人一样!)