Subject: bug#56197: [PATCH v3] lisp: Introduce a `lisp-fill-paragraph-as-displayed' variable.

From: Maxim Cournoyer <[email-masked]>
Date: Tue, 21 Jan 2025 11:50:44 +0900

Starting with Emacs 28, filling strings now happens in a
narrowed scope, and looses the leading indentation and can cause
the string to extend past the fill-column value.  Introduce
`lisp-fill-paragraph-as-displayed' as a new variable allowing
opting out of this new behavior in specific scenarios (such as
when using the Scheme major mode, say).

* lisp/emacs-lisp/lisp-mode.el
(lisp-fill-paragraph-as-displayed): New variable.
(lisp-fill-paragraph): Honor it, by avoiding the logic narrow to
strings before applying fill-paragraph.
* test/lisp/emacs-lisp/lisp-mode-tests.el
(lisp-fill-paragraph-respects-fill-column): Test it.
(lisp-fill-paragraph-docstring-boundaries): New test, as a
safeguard to avoid regressions.

Fixes: bug#56197
---
 lisp/emacs-lisp/lisp-mode.el            | 75 +++++++++++++++----------
 test/lisp/emacs-lisp/lisp-mode-tests.el | 47 ++++++++++++++++
 2 files changed, 92 insertions(+), 30 deletions(-)

diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 2b75a5fd038..9e8292b992a 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -1431,6 +1431,19 @@ emacs-lisp-docstring-fill-column
   :group 'lisp
   :version "30.1")
 
+(defvar lisp-fill-paragraph-as-displayed nil
+  "Modify the behavior of `lisp-fill-paragraph'.
+The default behavior of `lisp-fill-paragraph' is tuned for filling Emacs
+Lisp doc strings, with their special treatment for the first line.
+Particularly, strings are filled in a narrowed context to avoid filling
+surrounding code, which means any leading indent is disregarded, which
+can cause the filled string to extend passed the configured
+`fill-column' variable value.  If you would rather fill the string in
+its original context and ensure the `fill-column' value is more strictly
+respected, set this variable to true.  Doing so makes
+`lisp-fill-paragraph' behave as it used to in Emacs 27 and prior
+versions.")
+
 (defun lisp-fill-paragraph (&optional justify)
   "Like \\[fill-paragraph], but handle Emacs Lisp comments and docstrings.
 If any of the current line is a comment, fill the comment or the
@@ -1480,42 +1493,44 @@ lisp-fill-paragraph
                                   (derived-mode-p 'emacs-lisp-mode))
                              emacs-lisp-docstring-fill-column
                            fill-column)))
-        (let ((ppss (syntax-ppss))
-              (start (point))
-              ;; Avoid recursion if we're being called directly with
-              ;; `M-x lisp-fill-paragraph' in an `emacs-lisp-mode' buffer.
-              (fill-paragraph-function t))
+        (let* ((ppss (syntax-ppss))
+               (start (point))
+               ;; Avoid recursion if we're being called directly with
+               ;; `M-x lisp-fill-paragraph' in an `emacs-lisp-mode' buffer.
+               (fill-paragraph-function t)
+               (string-start (ppss-comment-or-string-start ppss)))
           (save-excursion
             (save-restriction
               ;; If we're not inside a string, then do very basic
               ;; filling.  This avoids corrupting embedded strings in
               ;; code.
-              (if (not (ppss-comment-or-string-start ppss))
+              (if (not string-start)
                   (lisp--fill-line-simple)
-                ;; If we're in a string, then narrow (roughly) to that
-                ;; string before filling.  This avoids filling Lisp
-                ;; statements that follow the string.
-                (when (ppss-string-terminator ppss)
-                  (goto-char (ppss-comment-or-string-start ppss))
-                  ;; The string may be unterminated -- in that case, don't
-                  ;; narrow.
-                  (when (ignore-errors
-                          (progn
-                            (forward-sexp 1)
-                            t))
-                    (narrow-to-region (1+ (ppss-comment-or-string-start ppss))
-                                      (1- (point)))))
-                ;; Move back to where we were.
-                (goto-char start)
-                ;; We should fill the first line of a string
-                ;; separately (since it's usually a doc string).
-                (if (= (line-number-at-pos) 1)
-                    (narrow-to-region (line-beginning-position)
-                                      (line-beginning-position 2))
-                  (save-excursion
-                    (goto-char (point-min))
-                    (forward-line 1)
-                    (narrow-to-region (point) (point-max))))
+                (unless lisp-fill-paragraph-as-displayed
+                  ;; If we're in a string, then narrow (roughly) to that
+                  ;; string before filling.  This avoids filling Lisp
+                  ;; statements that follow the string.
+                  (when (ppss-string-terminator ppss)
+                    (goto-char string-start)
+                    ;; The string may be unterminated -- in that case, don't
+                    ;; narrow.
+                    (when (ignore-errors
+                            (progn
+                              (forward-sexp 1)
+                              t))
+                      (narrow-to-region (1+ string-start)
+                                        (1- (point)))))
+                  ;; Move back to where we were.
+                  (goto-char start)
+                  ;; We should fill the first line of a string
+                  ;; separately (since it's usually a doc string).
+                  (if (= (line-number-at-pos) 1)
+                      (narrow-to-region (line-beginning-position)
+                                        (line-beginning-position 2))
+                    (save-excursion
+                      (goto-char (point-min))
+                      (forward-line 1)
+                      (narrow-to-region (point) (point-max)))))
 	        (fill-paragraph justify)))))))
   ;; Never return nil.
   t)
diff --git a/test/lisp/emacs-lisp/lisp-mode-tests.el b/test/lisp/emacs-lisp/lisp-mode-tests.el
index 3a765eab625..96e37114276 100644
--- a/test/lisp/emacs-lisp/lisp-mode-tests.el
+++ b/test/lisp/emacs-lisp/lisp-mode-tests.el
@@ -308,6 +308,53 @@ lisp-indent-defun
       (indent-region (point-min) (point-max))
       (should (equal (buffer-string) orig)))))
 
+
+;;; Filling
+
+(ert-deftest lisp-fill-paragraph-docstring-boundaries ()
+  "Test bug#28937, ensuring filling the docstring filled is properly
+bounded."
+  (with-temp-buffer
+    (insert "\
+(defun test ()
+  \"This is a test docstring.
+Here is some more text.\"
+  1
+  2
+  3
+  4
+  5)")
+    (let ((correct (buffer-string)))
+      (emacs-lisp-mode)
+      (search-backward "This is a test docstring")
+      (fill-paragraph)                  ;function under test
+      (should (equal (buffer-string) correct)))))
+
+(ert-deftest lisp-fill-paragraph-as-displayed ()
+  "Test bug#56197 -- more specifically, validate that a leading indentation
+for a string is preserved in the filled string."
+  (let ((lisp-fill-paragraph-as-displayed t) ;variable under test
+        ;; The following is a contrived example that demonstrates the
+        ;; fill-column problem when the string to fill is indented.
+        (source "\
+'(description \"This is a very long string which is indented by a considerable value, causing it to
+protrude from the configured `fill-column' since
+lisp-fill-paragraph was refactored in version 28.\")"))
+    (with-temp-buffer
+      (insert source)
+      (emacs-lisp-mode)
+      (search-backward "This is a very long string")
+      (fill-paragraph)                  ;function under test
+      (goto-char (point-min))
+      (message "%s" (buffer-substring-no-properties (point-min) (point-max)))
+      (let ((i 1)
+            (lines-count (count-lines (point-min) (point-max))))
+        (while (< i lines-count)
+          (beginning-of-line i)
+          (end-of-line)
+          (should (<= (current-column) fill-column))
+          (setq i (1+ i)))))))
+
 
 ;;; Fontification
 
-- 
2.46.0