如何让Emacs只自动换行句子而不是整段?

31

我在StackOverflow上看到至少两个建议,当编辑LaTeX文档时,在句子之间插入换行符。原因是这种做法有助于源代码控制、差异比较和协同编辑。

我基本上被说服了,但我很懒,不想考虑这个问题。

因此,我正在寻找一些emacs命令来替代我的工作。可以是一个小模式,也可以是一组需要设置的变量。

我认为我想要的是:

  • 软换行文本(使用longlines(set long-lines-auto-wrap 't))。这是因为我不想对我的合作者的编辑器施加要求,而且我有时会使用其他Unix工具来检查这些文件。
我认为我想要的是:
  • 使 fill-paragraph 能够在看起来像是句子结尾的换行符之间填充。
  • 能够与 auto-fill-mode 兼容的解决方案将是一个额外的奖励。
也就是说:

聊天聊天聊天。
一个新句子
包装搞砸了需要修复。
咕噜咕噜

转换为:

聊天聊天聊天。
一个新句子,包装搞砸了需要修复。
咕噜咕噜

欢迎您提出评论和建议。
编辑:Jouni K. Seppänen 的建议让我注意到 LaTeX-fill-break-at-separators,这表明emacs几乎已经知道如何做到这一点了。无论如何,我要去阅读一些代码,并会回报。再次感谢。

同样问题的更一般版本: 编辑器对决:在句子末尾保留换行符。谢谢,dreeves


1
应该可以在不以句子开头的每一行上执行相当于 M-^ (delete-indentation) 的操作。但是为什么你不想要软换行呢? - ShreevatsaR
为什么你不想要软换行?嗯,我真的不知道。这个问题困扰我很长时间了。我为什么要软换行呢? - dmckee --- ex-moderator kitten
你觉得是否值得将这个问题概括一下,这样人们就可以为除了emacs之外的其他编辑器提供解决方案?这个问题也比LaTeX更加普遍。所有这样做的原因同样适用于HTML文档等其他情况。 - dreeves
我已经添加了一个更一般的问题。如果您想将其与此合并,我很乐意删除它。 - dreeves
这本来是个合理的想法,但现实插手了,我离开了电脑... 因此,我链接到了你的版本。 - dmckee --- ex-moderator kitten
10个回答

10

这是我使用的东西, 大部分是从Luca de Alfaro那里借鉴来的:

(defun fill-sentence ()
  (interactive)
  (save-excursion
    (or (eq (point) (point-max)) (forward-char))
    (forward-sentence -1)
    (indent-relative t)
    (let ((beg (point))
          (ix (string-match "LaTeX" mode-name)))
      (forward-sentence)
      (if (and ix (equal "LaTeX" (substring mode-name ix)))
          (LaTeX-fill-region-as-paragraph beg (point))
        (fill-region-as-paragraph beg (point))))))

我将其绑定到 M-j,使用以下代码:
(global-set-key (kbd "M-j") 'fill-sentence)

"LaTeX"的引用是为了AUCTeX支持。如果您不使用AUCTeX,则let可以简化为

(let (beg (point))
  (forward-sentence)
  (fill-region-as-paragraph beg (point)))

我使用AUCTeX。我会尝试一下这个。 - dmckee --- ex-moderator kitten
你的第二个 let 没有在 beg 周围加上足够的括号。 - Steve
fill-sentence 用于将段落填充到80列的限制,这正常吗? - alper

5

我一直想做这件事,最近我找到了这篇博客文章,对我来说效果还不错。所以这里是我已经使用了几天的(稍微修改过的)内容。

(defun auto-fill-by-sentences ()
  (if (looking-back (sentence-end))
      ;; Break at a sentence
      (progn
        (LaTeX-newline)
        t)
    ;; Fall back to the default
    (do-auto-fill)))
(add-hook 'LaTeX-mode-hook (lambda () (setq auto-fill-function 'auto-fill-by-sentences)))

;; Modified from http://pleasefindattached.blogspot.com/2011/12/emacsauctex-sentence-fill-greatly.html
(defadvice LaTeX-fill-region-as-paragraph (around LaTeX-sentence-filling)
  "Start each sentence on a new line."
  (let ((from (ad-get-arg 0))
        (to-marker (set-marker (make-marker) (ad-get-arg 1)))
        tmp-end)
    (while (< from (marker-position to-marker))
      (forward-sentence)
      ;; might have gone beyond to-marker---use whichever is smaller:
      (ad-set-arg 1 (setq tmp-end (min (point) (marker-position to-marker))))
      ad-do-it
      (ad-set-arg 0 (setq from (point)))
      (unless (or (looking-back "^\\s *")
                  (looking-at "\\s *$"))
        (LaTeX-newline)))
    (set-marker to-marker nil)))
(ad-activate 'LaTeX-fill-region-as-paragraph)

很抱歉我回答这个问题的速度非常慢,因为我有其他事情在我的工作队列的最顶端。当我打新文字时,这段代码确实非常好用——它完全符合我的要求。但是在fill-paragraph处理已有的文本时,它无法处理那些句子在行中间开始的情况,但如果我在每个句子开头加一个空格,它就可以处理了。谢谢。 - dmckee --- ex-moderator kitten
1
你的意思是它认为句子应该用双空格分隔吗?这可以通过设置 sentence-end-double-space 来更改。如果你的意思是想让它在遇到“。”、“!”、“?”等符号后无条件换行,那么你可以更改 sentence-end 的值,或者将对 sentence-end 的调用更改为你喜欢的任何正则表达式。 - Ivan Andrus
1
由于某些原因,这个解决方案仍然会对长行进行换行,但它会在句号后添加一个新行。我该如何使其不在单个句子内换行? - icks
@icks 嗯。这跟我在我的.emacs中的不一样,所以它们肯定在某个时候分歧了。尝试将(do-auto-fill)更改为nil,看看是否有帮助。 - Ivan Andrus

4

如果你在每个句子的结尾放置注释标记,Emacs就知道不要将下一行移到注释内:

chat chat chat.%
A new sentence
with goofed up wrapping that needs to be fixed.%
Mumble mumble%

然后,至少在AUCTeX 11.85中,M-q会分别填充每个句子。(如果您在Emacs中测试此功能,似乎存在一个错误,即如果这是缓冲区中的第一个段落并且您键入M-q,则会收到错误消息。只需在文本之前插入一个换行符即可解决。)

如果您不想键入注释字符,可以使用LaTeX-fill-paragraph并修改它,使得行末的句子结束标点符号与注释类似。


好的。虽然不是我预期的,但肯定能够实现。 - dmckee --- ex-moderator kitten
这难道不比关闭自动填充更麻烦吗?在这种情况下,M-q有什么用处? - Charles Stewart
这将吞噬句子之间的空格。您需要在“...chat.%”等之前加上一个空格。 - Andrew Swann
如何修改代码,使得行末的标点符号能像注释一样被识别? - alper

3
(defun wrap-at-sentences ()
  "Fills the current paragraph, but starts each sentence on a new line."
  (interactive)
  (save-excursion
    ;; Select the entire paragraph.
    (mark-paragraph)
    ;; Move to the start of the paragraph.
    (goto-char (region-beginning))
    ;; Record the location of the end of the paragraph.
    (setq end-of-paragraph (region-end))
    ;; Wrap lines with 'hard' newlines (i.e., real line breaks).
    (let ((use-hard-newlines 't))
      ;; Loop over each sentence in the paragraph.
      (while (< (point) end-of-paragraph)
        ;; Determine the region spanned by the sentence.
        (setq start-of-sentence (point))
        (forward-sentence)
        ;; Wrap the sentence with hard newlines.
        (fill-region start-of-sentence (point))
        ;; Delete the whitespace following the period, if any.
        (while (char-equal (char-syntax (preceding-char)) ?\s)
          (delete-char -1))
        ;; Insert a newline before the next sentence.
        (insert "\n")))))

(global-set-key (kbd "M-q") 'wrap-at-sentences)

为了个人使用,我删除了(insert "\n")。谢谢!这是目前对我来说最好的解决方案。 - yogsototh

2

不能保证在所有情况下都有效,但:

(defun my-fill-sentence ()
  "Fill sentence separated by punctuation or blank lines."
  (interactive)
  (let (start end)
    (save-excursion
      (re-search-backward "\\(^\\s-*$\\|[.?!]\\)" nil t)
      (skip-syntax-forward "^w")
      (setq start (point-at-bol)))
    (save-excursion
      (re-search-forward "\\(^\\s-*$\\|[.?!]\\)" nil t)
      (setq end (point-at-eol)))
    (save-restriction
      (narrow-to-region start end)
      (fill-paragraph nil))))

要使其与auto-fill-mode配合使用,请在你的LaTeX模式钩子中添加 (setq normal-auto-fill-function 'my-fill-sentence)(我想是这样)。

看起来 fill-paragraph 在结尾处会插入一个我不想要的换行符。但它只对光标下的句子起作用。 - dmckee --- ex-moderator kitten
它在你的实例上运行正常...是否有其他示例显示了问题? - scottfrazer
嗯,在相同的例子上对我没用。如果我在“句子”后面放置put并调用它,我会获得第二个句子的良好填充,并在“喃喃自语”之前换行。查看我的“.emacs”,我发现了很多无用信息。我将把它关掉然后再试一次。 - dmckee --- ex-moderator kitten

1

我非常喜欢Chris Conway的宏,但是它只能在手动换行每个句子后才能工作。我是一个懒人,所以我希望emacs可以为我完成这个任务。今天早上,我终于坐下来研究了一下这个问题。现在我有的解决方案是修改内置宏fill-region-as-paragraph

应用以下修改后,新选项newline-after-sentence将设置为true。标准的M-qfill-paragraph)将自动填充并在句子之间创建换行符。请注意,测试仅使用GNU Emacs 23.3.1进行——使用时需自担风险。

完整的宏很长,所以我不会在这里发布它。想法是在fill-region-as-paragraph中添加以下循环。

...

;; Insert a line break after each sentence
(while (< (point) to)
  (forward-sentence)
  (if (< (point) to) (fill-newline)))

;; This is the actual filling loop.
(goto-char from)
(let (sentbeg sentend)
  (while (< (point) to)
    (setq sentbeg (point))
    (end-of-line)
    (setq sentend (point))
    (fill-one-line sentbeg sentend justify) ;; original filling loop
    (forward-line)))))

...

你可以在我的git仓库中找到完整的宏。一些细节也写在了我的博客中。如果你不想阅读我糟糕的英语,你可以直接使用

$ curl http://fermi.mycloudnas.com/cgit.cgi/fill/plain/hack.el >> ~/.emacs

将这个hack添加到您的~/.emacs文件中并尝试一下。欢迎评论和错误报告。


网站宕机了,而且Wayback Machine没有存档。 - Seth Robertson

1
另一种方法是保留您的 .tex 文件,使用像 latexdiff(在 this StackExchange 帖子中描述)这样的工具,而不是 Unix diff。这将生成带有类似 Word 的修订标记的 .tex 文件,并正确处理空格,因此您不必担心句子结束的位置。

1

我假设你知道elisp。

有几种方法可以采取:

  • 钩入auto-fill-mode。那里有很多硬编码的条件语句,所以它可能不适用于您。您可以尝试使用auto-fill-function并查看是否有所需的钩子。

  • 使一个字符(可能是 . )“电动”,这样当您按下它时,它会插入自己,然后调用一个函数来确定如何填充您所在的行。

  • 设置一个after-change-hook来调用一个函数,该函数确定如何填充句子。每次更改缓冲区时都会调用此函数,因此要高效地执行。 (font-lock使用此机制,因此不要太担心。听起来很慢,但实际上并不慢-人们打字很慢。)

一旦您在正确的位置挂钩,您只需实现填充逻辑即可。 sentence-at-point(来自thingatpt)的源代码可能是有益的。

无论如何,我从未听说过有人这样做......但这确实是可能的。就像Emacs中的大多数事情一样,它只是一个简单的编程问题。

我从未花时间学习Lisp,所以我的Lisp非常薄弱。 - dmckee --- ex-moderator kitten
这可能是完美的学习项目,因为它很直接,但并不容易。 - jrockway

1
如果其他答案过于自动化,这里提供一种半自动化的方法。基本上,这是您手动重新格式化时会重复执行的操作,但压缩成了只需反复按一个键即可完成的方式。
;; - go to the end of the line,
;; - do ^d to suck the previous line onto this one, 
;; - make sure there's only one space between the now-concatenated
;;   lines, and then 
;; - jump to the end and hit space so that (with auto-fill-mode)
;;   the line nicely rewraps itself:
;;   (turn on auto-fill-mode with M-x auto-fill-mode)
(defalias 'fill-sentence
  (read-kbd-macro "C-e C-d SPC M-x just- one- space RET C-e SPC <backspace>"))

(define-key global-map [f4] 'fill-sentence)  ; or whatever key you like

1

我编写了下面的代码,它循环遍历一个区域并插入换行符。我没有使用forward-sentence,因为它对我无效,而是使用re-search-forward "[.?!][]\"')}]*\\( \\)",该正则表达式找到所有只由两个空格(该正则表达式是sentence-end修改后的版本)紧随其后的句子。通过newline-and-indent实现换行。

(defun fill-sentences-in-paragraph ()
  "Put a newline at the end of each sentence in paragraph."
  (interactive)
  (save-excursion
    (mark-paragraph)
    (call-interactively 'fill-sentences-in-region)))

(defun fill-sentences-in-region (start end)
  "Put a newline at the end of each sentence in region."
  (interactive "*r")
  (call-interactively 'unfill-region)
  (save-excursion
    (goto-char start)
    (while (re-search-forward "[.?!][]\"')}]*\\(  \\)" end t)
      (newline-and-indent))))

为了能够修复格式不正确的文本,比如例子中的“chat chat chat...”,fill-sentences-in-region首先调用unfill-region,该函数消除了断句的空格。
   (defun unfill-region (beg end)
      "Unfill the region, joining text paragraphs into a
       single logical line.  This is useful, e.g., for use
       with 'visual-line-mode'."
      (interactive "*r")
      (let ((fill-column (point-max)))
        (fill-region beg end)))

我使用 visual-line-mode,并将默认的段落填充命令 M-q 替换为 fill-sentences-in-paragraph,使用 (global-set-key "\M-q" 'fill-sentences-in-paragraph) 实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接