Subject: 30.0.50; ERC >5.5: Add erc-fill style based on visual-line-mode

From: "J.P." <[email-masked]>
Date: Wed, 18 Jan 2023 06:53:48 -0800

Tags: patch

This bug is broadly related to

  bug#51969: 29.0.50; Add command for refilling ERC buffers

Hi people,

Newcomers to ERC are sometimes disappointed to discover that messages
are "filled" in the traditional sense, meaning white space is
permanently added and removed to produce "folded" lines as if M-q'd in
an editing mode. Unfortunately, much of IRC involves dealing with
preformatted messages sent by bots or a server (think "figlet" banners
in MOTD bursts or /msg NickServ help). While it's always been possible
to turn off filling everywhere (`fill' is global module, remember),
doing so necessarily means surrendering any and all filling, whose very
purpose is to make it easy to distinguish between speakers at a glance.

This patch aims to offer a compromise of sorts, assuming users are
willing to tolerate some opinionated choices. The first is that, for
now, per-message lefty timestamps are out. If you want timestamps, they
must go on the right (except for the occasional dateline break).
Moreover, right-hand timestamps basically look awful unless you accept a
new paradigm that places them all in the right margin. (This can be
toggled off when space is tight.) Yet another catch involves
`visual-line-mode' itself, which is managed for you. Basically, users of
modal editing packages may suffer from basic navigation issues without
taking extra care to cope with its idiosyncrasies.

An ancillary goal of this patch is to have this mode double as a
reference implementation for a certain flavor of local module, namely
one that's "tunably persistent" per buffer. Also on display will be an
added degree of versatility in terms of activation. While users can
still add `fill-wrap' to `erc-modules' or enable it manually via
minor-mode toggle, they can also elect to allow the global `fill' module
to control it transparently, as a child module, simply by setting
`erc-fill-function' to `erc-fill-wrap'.

If you'd like to try this, do the following after applying these
patches and before connecting:

  (setopt erc-fill-function #'erc-fill-wrap
          erc-timestamp-user-align-to 'margin)

Screenshots to follow (possibly).

Thanks,
J.P.

P.S. These patches come bundled with the so-called "edge" edition of
ERC, an alpha-quality hodgepodge of WIP patches available as an ELPA
package ("https://emacs-erc.gitlab.io/bugs/archive/").

In GNU Emacs 30.0.50 (build 2, x86_64-pc-linux-gnu, GTK+ Version
 3.24.35, cairo version 1.17.6) of 2023-01-17 built on localhost
Repository revision: 281f48f19ecad706a639d57cb937afb0b97eded7
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 36 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NOTIFY
INOTIFY PDUMPER PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINPUT2 XPM GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived epg rfc6068 epg-config
gnus-util text-property-search mm-decode mm-bodies mm-encode mail-parse
rfc2231 mailabbrev gmm-utils mailheader sendmail rfc2047 rfc2045
ietf-drums mm-util mail-prsvr mail-utils erc iso8601 time-date
auth-source cl-seq eieio eieio-core cl-macs password-cache json subr-x
map thingatpt pp format-spec cl-loaddefs cl-lib erc-backend erc-goodies
erc-networks byte-opt gv bytecomp byte-compile erc-common erc-compat
erc-loaddefs rmc iso-transl tooltip cconv eldoc paren electric uniquify
ediff-hook vc-hooks lisp-float-type elisp-mode mwheel term/x-win x-win
term/common-win x-dnd tool-bar dnd fontset image regexp-opt fringe
tabulated-list replace newcomment text-mode lisp-mode prog-mode register
page tab-bar menu-bar rfn-eshadow isearch easymenu timer select
scroll-bar mouse jit-lock font-lock syntax font-core term/tty-colors
frame minibuffer nadvice seq simple cl-generic indonesian philippine
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help abbrev obarray oclosure
cl-preloaded button loaddefs theme-loaddefs faces cus-face macroexp
files window text-properties overlay sha1 md5 base64 format env
code-pages mule custom widget keymap hashtable-print-readable backquote
threads dbusbind inotify lcms2 dynamic-setting system-font-setting
font-render-setting cairo move-toolbar gtk x-toolkit xinput2 x multi-tty
make-network-process emacs)

Memory information:
((conses 16 64390 6319)
 (symbols 48 8639 0)
 (strings 32 23673 1623)
 (string-bytes 1 685926)
 (vectors 16 15259)
 (vector-slots 8 209777 7692)
 (floats 8 24 35)
 (intervals 56 232 0)
 (buffers 976 10))

>From 9a619878c0f56c996fb2d7f5b6b63b03fb979071 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 16 Jun 2022 01:20:49 -0700
Subject: [PATCH 1/4] [5.6] Adjust some old text properties in ERC buffers

TODO: because these have been around forever, we should mention
their deletion in the misc-library section of ERC-NEWS for 5.6.

* lisp/erc/erc.el (erc-display-message): Remove the confusing
`rear-sticky' text property, which has been around since 2002.
(erc-display-prompt): Make the `field' text property more meaningful
to aid in searching, although this makes the `erc-prompt' property
somewhat redundant.
---
 lisp/erc/erc.el | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ba7db15cf8c..ab2cd2be3a7 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2854,7 +2854,6 @@ erc-display-message
         (erc-display-line string buffer)
       (unless (erc-hide-current-message-p parsed)
         (erc-put-text-property 0 (length string) 'erc-parsed parsed string)
-        (erc-put-text-property 0 (length string) 'rear-sticky t string)
 	(when (erc-response.tags parsed)
 	  (erc-put-text-property 0 (length string) 'tags (erc-response.tags parsed)
 				 string))
@@ -4283,7 +4282,7 @@ erc-display-prompt
         (setq prompt (propertize prompt
                                  'rear-nonsticky t
                                  'erc-prompt t
-                                 'field t
+                                 'field 'erc-prompt
                                  'front-sticky t
                                  'read-only t))
         (erc-put-text-property 0 (1- (length prompt))
-- 
2.38.1

>From f152137282edb9ecfab95ac647763b789c56e141 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Wed, 24 Nov 2021 05:35:35 -0800
Subject: [PATCH 2/4] [5.6] Leverage display properties better in erc-stamp

(erc-timestamp-use-align-to): Enhance meaning of option to accept
numeric value for dynamically aligned right-side stamps.  Use
`graphic-display-p' to determine default value even though, as stated
in the manual, terminal Emacs also supports the "space" display spec.
(erc-timestamp--display-margin-mode): Add internal minor mode to help
other modules quickly ensure stamps are showing correctly.
(erc-stamp--inherited-props): Add internal const to hold properties
that should be inherited from message being inserted.
(erc-insert-aligned): Deprecate function and remove from primary
client code path.
(erc-insert-timestamp-right): Account for new display-related values
of `erc-timestamp-use-align-to'.

* test/lisp/erc/erc-stamp-tests.el: New file.
---
 lisp/erc/erc-stamp.el            |  66 ++++++++++--
 test/lisp/erc/erc-stamp-tests.el | 177 +++++++++++++++++++++++++++++++
 2 files changed, 235 insertions(+), 8 deletions(-)
 create mode 100644 test/lisp/erc/erc-stamp-tests.el

diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index 0aa1590f801..e9592448a33 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -217,14 +217,44 @@ erc-timestamp-right-column
 	  (integer :tag "Column number")
 	  (const :tag "Unspecified" nil)))
 
-(defcustom erc-timestamp-use-align-to (eq window-system 'x)
+(defcustom erc-timestamp-use-align-to (and (display-graphic-p) t)
   "If non-nil, use the :align-to display property to align the stamp.
 This gives better results when variable-width characters (like
 Asian language characters and math symbols) precede a timestamp.
 
+This option only matters when `erc-insert-timestamp-function' is
+set to `erc-insert-timestamp-right' or that option's default,
+`erc-insert-timestamp-left-and-right'.  If the value is a
+positive integer, alignment occurs that many columns from the
+right edge.  If the value is `margin', the stamp appears in the
+right margin when visible.
+
 A side effect of enabling this is that there will only be one
 space before a right timestamp in any saved logs."
-  :type 'boolean)
+  :type '(choice boolean integer (const margin))
+  :package-version '(ERC . "5.4.1")) ; FIXME update when merging
+
+;; If people want to use this directly, we can offer an option to set
+;; the margin's width.
+(define-minor-mode erc-timestamp--display-margin-mode
+  "Internal minor mode for built-in modules integrating with `stamp'."
+  :interactive nil
+  (if-let ((erc-timestamp--display-margin-mode)
+           (width (if erc-timestamp-last-inserted-right
+                      (length erc-timestamp-last-inserted-right)
+                    (1+ (length (erc-format-timestamp
+                                 (current-time)
+                                 erc-timestamp-format-right))))))
+      (progn
+        (setq right-margin-width width
+              right-fringe-width 0)
+        (unless noninteractive
+          (set-window-margins nil left-margin-width width)
+          (set-window-fringes nil left-fringe-width 0)))
+    (kill-local-variable 'right-margin-width)
+    (unless noninteractive
+      (set-window-margins nil nil)
+      (set-window-fringes nil nil))))
 
 (defun erc-insert-timestamp-left (string)
   "Insert timestamps at the beginning of the line."
@@ -243,6 +273,7 @@ erc-insert-aligned
 
 If `erc-timestamp-use-align-to' is t, use the :align-to display
 property to get to the POSth column."
+  (declare (obsolete "inlined and removed from client code path" "30.1"))
   (if (not erc-timestamp-use-align-to)
       (indent-to pos)
     (insert " ")
@@ -253,6 +284,8 @@ erc-insert-aligned
 ;; Silence byte-compiler
 (defvar erc-fill-column)
 
+(defvar erc-stamp--inherited-props '(line-prefix wrap-prefix))
+
 (defun erc-insert-timestamp-right (string)
   "Insert timestamp on the right side of the screen.
 STRING is the timestamp to insert.  This function is a possible
@@ -304,12 +337,29 @@ erc-insert-timestamp-right
       ;; some margin of error if what is displayed on the line differs
       ;; from the number of characters on the line.
       (setq col (+ col (ceiling (/ (- col (- (point) (line-beginning-position))) 1.6))))
-      (if (< col pos)
-	  (erc-insert-aligned string pos)
-	(newline)
-	(indent-to pos)
-	(setq from (point))
-	(insert string))
+      ;; For compatibility reasons, the `erc-timestamp' field includes
+      ;; intervening white space unless a hard break is warranted.
+      (pcase erc-timestamp-use-align-to
+        ((and 't (guard (< col pos)))
+         (insert " ")
+         (put-text-property from (point) 'display `(space :align-to ,pos)))
+        ((pred integerp) ; (cl-type (integer 0 *))
+         (insert " ")
+         (when (eq ?\s (aref string 0))
+           (setq string (substring string 1)))
+         (let ((s (+ erc-timestamp-use-align-to (string-width string))))
+           (put-text-property from (point) 'display
+                              `(space :align-to (- right ,s)))))
+        ('margin
+         (put-text-property 0 (length string)
+                            'display `((margin right-margin) ,string)
+                            string))
+        ((guard (>= col pos)) (newline) (indent-to pos) (setq from (point)))
+        (_ (indent-to pos)))
+      (insert string)
+      (dolist (p erc-stamp--inherited-props)
+        (when-let ((v (get-text-property (1- from) p)))
+          (put-text-property from (point) p v)))
       (erc-put-text-property from (point) 'field 'erc-timestamp)
       (erc-put-text-property from (point) 'rear-nonsticky t)
       (when erc-timestamp-intangible
diff --git a/test/lisp/erc/erc-stamp-tests.el b/test/lisp/erc/erc-stamp-tests.el
new file mode 100644
index 00000000000..a45f3531586
--- /dev/null
+++ b/test/lisp/erc/erc-stamp-tests.el
@@ -0,0 +1,177 @@
+;;; erc-stamp-tests.el --- Tests for erc-stamp.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'ert)
+(require 'erc-stamp)
+
+;; These display-oriented tests are brittle because many factors
+;; influence how text properties are applied.  We should just
+;; rework these into full scenarios.
+
+(defun erc-stamp-tests--insert-right (test)
+  (let ((val (list 0 0))
+        (erc-insert-modify-hook '(erc-add-timestamp))
+        (erc-insert-post-hook '(erc-make-read-only)) ; see comment above
+        (erc-timestamp-only-if-changed-flag nil)
+        ;;
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (advice-add 'erc-format-timestamp :filter-args
+                (lambda (args) (cons (cl-incf (cadr val) 60) (cdr args)))
+                '((name . ert-deftest--erc-timestamp-use-align-to)))
+
+    (with-current-buffer (get-buffer-create "*erc-stamp-tests--insert-right*")
+      (erc-mode)
+      (erc-munge-invisibility-spec)
+      (setq erc-server-process (start-process "p" (current-buffer)
+                                              "sleep" "1")
+            erc-input-marker (make-marker)
+            erc-insert-marker (make-marker))
+      (set-process-query-on-exit-flag erc-server-process nil)
+      (set-marker erc-insert-marker (point-max))
+      (erc-display-prompt)
+
+      (funcall test)
+
+      (when noninteractive
+        (kill-buffer)))
+
+    (advice-remove 'erc-format-timestamp
+                   'ert-deftest--erc-timestamp-use-align-to)))
+
+(ert-deftest erc-timestamp-use-align-to--nil ()
+  (erc-stamp-tests--insert-right
+   (lambda ()
+
+     (ert-info ("nil, normal")
+       (let ((erc-timestamp-use-align-to nil))
+         (erc-display-message nil 'notice (current-buffer) "begin"))
+       (goto-char (point-min))
+       (should (search-forward-regexp
+                (rx "begin" (+ "\t") (* " ") " [") nil t))
+       ;; Field includes intervening spaces
+       (should (eql ?n (char-before (field-beginning (point)))))
+       ;; Timestamp extends to the end of the line
+       (should (eql ?\n (char-after (field-end (point))))))
+
+     ;; The option `erc-timestamp-right-column' is normally nil by
+     ;; default, but it's a convenient stand in for a sufficiently
+     ;; small `erc-fill-column' (we can force a line break without
+     ;; involving that module).
+     (should-not erc-timestamp-right-column)
+
+     (ert-info ("nil, overlong (hard wrap)")
+       (let ((erc-timestamp-use-align-to nil)
+             (erc-timestamp-right-column 20))
+         (erc-display-message nil 'notice (current-buffer)
+                              "twenty characters"))
+       (should (search-forward-regexp (rx bol (+ "\t") (* " ") " [") nil t))
+       ;; Field excludes leading whitespace (arguably undesirable).
+       (should (eql ?\[ (char-after (1+ (field-beginning (point))))))
+       ;; Timestamp extends to the end of the line.
+       (should (eql ?\n (char-after (field-end (point)))))))))
+
+(ert-deftest erc-timestamp-use-align-to--t ()
+  (erc-stamp-tests--insert-right
+   (lambda ()
+
+     (ert-info ("t, normal")
+       (let ((erc-timestamp-use-align-to t))
+         (let ((msg (erc-format-privmessage "bob" "msg one" nil t)))
+           (erc-display-message nil nil (current-buffer) msg)))
+       (goto-char (point-min))
+       ;; Exactly two spaces, one from format, one added by erc-stamp.
+       (should (search-forward "msg one  [" nil t))
+       ;; Field covers space between.
+       (should (eql ?e (char-before (field-beginning (point)))))
+       (should (eql ?\n (char-after (field-end (point))))))
+
+     (ert-info ("t, overlong (hard wrap)")
+       (let ((erc-timestamp-use-align-to t)
+             (erc-timestamp-right-column 20))
+         (let ((msg (erc-format-privmessage "bob" "tttt wwww oooo" nil t)))
+           (erc-display-message nil nil (current-buffer) msg)))
+       ;; Indented to pos (this is arguably a bug).
+       (should (search-forward-regexp (rx bol (+ "\t") (* " ") " [") nil t))
+       ;; Field starts *after* leading space (arguably bad).
+       (should (eql ?\[ (char-after (1+ (field-beginning (point))))))
+       (should (eql ?\n (char-after (field-end (point)))))))))
+
+(ert-deftest erc-timestamp-use-align-to--integer ()
+  (erc-stamp-tests--insert-right
+   (lambda ()
+
+     (ert-info ("integer, normal")
+       (let ((erc-timestamp-use-align-to 1))
+         (let ((msg (erc-format-privmessage "bob" "msg one" nil t)))
+           (erc-display-message nil nil (current-buffer) msg)))
+       (goto-char (point-min))
+       ;; Space not added because included in format string.
+       (should (search-forward "msg one [" nil t))
+       ;; Field covers space between.
+       (should (eql ?e (char-before (field-beginning (point)))))
+       (should (eql ?\n (char-after (field-end (point))))))
+
+     (ert-info ("integer, overlong (hard wrap)")
+       (let ((erc-timestamp-use-align-to 1)
+             (erc-timestamp-right-column 20))
+         (let ((msg (erc-format-privmessage "bob" "tttt wwww oooo" nil t)))
+           (erc-display-message nil nil (current-buffer) msg)))
+       ;; No hard wrap
+       (should (search-forward "oooo [" nil t))
+       ;; Field starts at leading space.
+       (should (eql ?\s (char-after (field-beginning (point)))))
+       (should (eql ?\n (char-after (field-end (point)))))))))
+
+(ert-deftest erc-timestamp-use-align-to--margin ()
+  (erc-stamp-tests--insert-right
+   (lambda ()
+     (erc-timestamp--display-margin-mode +1)
+
+     (ert-info ("margin, normal")
+       (let ((erc-timestamp-use-align-to 'margin))
+         (let ((msg (erc-format-privmessage "bob" "msg one" nil t)))
+           (put-text-property 0 (length msg) 'wrap-prefix 10 msg)
+           (erc-display-message nil nil (current-buffer) msg)))
+       (goto-char (point-min))
+       ;; Space not added (treated as opaque string).
+       (should (search-forward "msg one [" nil t))
+       ;; Field covers stamp alone
+       (should (eql ?e (char-before (field-beginning (point)))))
+       ;; Vanity props extended
+       (should (get-text-property (field-beginning (point)) 'wrap-prefix))
+       (should (get-text-property (1+ (field-beginning (point))) 'wrap-prefix))
+       (should (get-text-property (1- (field-end (point))) 'wrap-prefix))
+       (should (eql ?\n (char-after (field-end (point))))))
+
+     (ert-info ("margin, overlong (hard wrap)")
+       (let ((erc-timestamp-use-align-to 'margin)
+             (erc-timestamp-right-column 20))
+         (let ((msg (erc-format-privmessage "bob" "tttt wwww oooo" nil t)))
+           (erc-display-message nil nil (current-buffer) msg)))
+       ;; No hard wrap
+       (should (search-forward "oooo [" nil t))
+       ;; Field starts at leading space.
+       (should (eql ?\s (char-after (field-beginning (point)))))
+       (should (eql ?\n (char-after (field-end (point)))))))))
+
+;;; erc-stamp-tests.el ends here
-- 
2.38.1

>From 3a73c80f3043b46398269b777c2ec545c9f38bf7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Sun, 24 Apr 2022 02:38:12 -0700
Subject: [PATCH 3/4] [5.6] Convert erc-fill minor mode into a proper module

* lisp/erc/erc-fill.el (erc-fill-mode, erc-fill-enable,
erc-fill-disable): Use API to create these.
(erc-fill-static): Save restriction instead of caller's match data.
---
 lisp/erc/erc-fill.el | 34 +++++++++++-----------------------
 1 file changed, 11 insertions(+), 23 deletions(-)

diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index e10b7d790f6..caf401bf222 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -38,30 +38,18 @@ erc-fill
   :group 'erc)
 
 ;;;###autoload(autoload 'erc-fill-mode "erc-fill" nil t)
-(define-minor-mode erc-fill-mode
-  "Toggle ERC fill mode.
-With a prefix argument ARG, enable ERC fill mode if ARG is
-positive, and disable it otherwise.  If called from Lisp, enable
-the mode if ARG is omitted or nil.
-
+(define-erc-module fill nil
+  "Manage filling in ERC buffers.
 ERC fill mode is a global minor mode.  When enabled, messages in
 the channel buffers are filled."
-  :global t
-  (if erc-fill-mode
-      (erc-fill-enable)
-    (erc-fill-disable)))
-
-(defun erc-fill-enable ()
-  "Setup hooks for `erc-fill-mode'."
-  (interactive)
-  (add-hook 'erc-insert-modify-hook #'erc-fill)
-  (add-hook 'erc-send-modify-hook #'erc-fill))
-
-(defun erc-fill-disable ()
-  "Cleanup hooks, disable `erc-fill-mode'."
-  (interactive)
-  (remove-hook 'erc-insert-modify-hook #'erc-fill)
-  (remove-hook 'erc-send-modify-hook #'erc-fill))
+  ;; FIXME ensure a consistent ordering relative to hook members from
+  ;; other modules.  Ideally, this module's processing should happen
+  ;; after "morphological" modifications to a message's text but
+  ;; before superficial decorations.
+  ((add-hook 'erc-insert-modify-hook #'erc-fill)
+   (add-hook 'erc-send-modify-hook #'erc-fill))
+  ((remove-hook 'erc-insert-modify-hook #'erc-fill)
+   (remove-hook 'erc-send-modify-hook #'erc-fill)))
 
 (defcustom erc-fill-prefix nil
   "Values used as `fill-prefix' for `erc-fill-variable'.
@@ -130,7 +118,7 @@ erc-fill
 
 (defun erc-fill-static ()
   "Fills a text such that messages start at column `erc-fill-static-center'."
-  (save-match-data
+  (save-restriction
     (goto-char (point-min))
     (looking-at "^\\(\\S-+\\)")
     (let ((nick (match-string 1)))
-- 
2.38.1

>From a108605cad5c054a68c0ddbe2f576094d6eaa526 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 13 Jan 2023 00:00:56 -0800
Subject: [PATCH 4/4] [5.6] Add erc-fill style based on visual-line-mode

* lisp/erc/erc-common.el (erc--features-to-modules): Add mapping for
local module `fill-wrap'.
* lisp/erc/erc-fill.el (erc-fill-function): Add new value,
`erc-fill-wrap'.
(erc-fill-static-center): Extend meaning of option to also affect
`erc-wrap-mode'.
(erc-fill-wrap-mode, erc-fill--wrap-prefix, erc-fill--wrap-value): New
minor mode and variables to support it.
(erc-fill-wrap): New function implementing
`erc-fill-function' (behavioral) interface.
(erc-fill-wrap-nudge, erc-fill--wrap-nudge): New command and helper
for growing and shrinking visual fill prefix.
---
 lisp/erc/erc-common.el |   1 +
 lisp/erc/erc-fill.el   | 159 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 158 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 9eb4f1a9000..456d2bc204d 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -96,6 +96,7 @@ erc--features-to-modules
     (erc-page page ctcp-page)
     (erc-sound sound ctcp-sound)
     (erc-stamp stamp timestamp)
+    (erc-fill fill-wrap)
     (erc-services services nickserv))
   "Migration alist mapping a library feature to module names.
 Keys need not be unique: a library may define more than one
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index caf401bf222..95b388cbf84 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -79,16 +79,27 @@ erc-fill-function
 These two styles are implemented using `erc-fill-variable' and
 `erc-fill-static'.  You can, of course, define your own filling
 function.  Narrowing to the region in question is in effect while your
-function is called."
+function is called.
+
+A third style resembles static filling but \"wraps\" instead of
+fills, courtesy of `visual-line-mode' mode, which ERC
+automatically enables when this option is `erc-fill-wrap' or
+`erc-fill-wrap-mode' is active.  Set `erc-fill-static-center' to
+your preferred initial \"prefix\" width.  For adjusting the width
+during a session, see the command `erc-fill-wrap-nudge'."
   :type '(choice (const :tag "Variable Filling" erc-fill-variable)
                  (const :tag "Static Filling" erc-fill-static)
+                 (const :tag "Dynamic word-wrap" erc-fill-wrap)
                  function))
 
 (defcustom erc-fill-static-center 27
   "Column around which all statically filled messages will be centered.
 This column denotes the point where the ` ' character between
 <nickname> and the entered text will be put, thus aligning nick
-names right and text left."
+names right and text left.
+
+Also used by the `erc-fill-function' variant `erc-fill-wrap' for
+its initial leading \"prefix\" width."
   :type 'integer)
 
 (defcustom erc-fill-variable-maximum-indentation 17
@@ -155,6 +166,150 @@ erc-fill-variable
           (erc-fill-regarding-timestamp))))
     (erc-restore-text-properties)))
 
+(defvar-local erc-fill--wrap-prefix nil)
+(defvar-local erc-fill--wrap-value nil)
+
+(define-erc-module fill-wrap nil
+  "Fill style leveraging `visual-line-mode'.
+This local module depends on the global `fill' module.  To use
+it, either include `fill-wrap' in `erc-modules' or set
+`erc-fill-function' to `erc-fill-wrap'.  You can also manually
+invoke one of the minor-mode toggles."
+  ((let (msg)
+     (unless erc-fill-mode
+       (unless (memq 'fill erc-modules)
+         (setq msg
+               (concat "WARNING: enabling default global module `fill' needed "
+                       " by local module `fill-wrap'.  This will impact all"
+                       " ERC sessions.  Add `fill' to `erc-modules' to avoid "
+                       " this warning. See Info:\"(erc) Modules\" for more.")))
+       (erc-fill-mode +1))
+     (unless (eq erc-fill-function #'erc-fill-wrap)
+       (setq-local erc-fill-function #'erc-fill-wrap))
+     (when-let* ((vars (or erc--server-reconnecting erc--target-priors))
+                 ((alist-get 'erc-fill-wrap-mode vars)))
+       (setq erc-fill--wrap-value (alist-get 'erc-fill--wrap-value vars)
+             erc-fill--wrap-prefix (alist-get 'erc-fill--wrap-prefix vars)))
+     (when (eq erc-timestamp-use-align-to 'margin)
+       (erc-timestamp--display-margin-mode +1))
+     (setq erc-fill--wrap-value
+           (or erc-fill--wrap-value erc-fill-static-center)
+           ;;
+           erc-fill--wrap-prefix
+           (or erc-fill--wrap-prefix
+               (list 'space :width erc-fill--wrap-value)))
+     (visual-line-mode +1)
+     (when msg
+       (erc-display-error-notice nil msg))))
+  ((when erc-timestamp--display-margin-mode
+     (erc-timestamp--display-margin-mode -1))
+   (kill-local-variable 'erc-button--add-nickname-face-function)
+   (kill-local-variable 'erc-fill--wrap-prefix)
+   (kill-local-variable 'erc-fill--wrap-value)
+   (kill-local-variable 'erc-fill-function)
+   (visual-line-mode -1))
+  'local)
+
+(defvar-local erc-fill--wrap-length-function nil
+  "Function to determine length of perceived nickname.
+It should return an integer representing the length of the
+nickname, including any enclosing brackets, or nil, to fall back
+to the default behavior of taking the length from the first word.")
+
+(defun erc-fill-wrap ()
+  "Use text props to mimic the effect of `erc-fill-static'.
+See `erc-fill-wrap-mode' for details."
+  (unless erc-fill-wrap-mode
+    (erc-fill-wrap-mode +1))
+  (save-excursion
+    (goto-char (point-min))
+    (let ((len (or (and erc-fill--wrap-length-function
+                        (funcall erc-fill--wrap-length-function))
+                   (progn (skip-syntax-forward "^-")
+                          (- (point) (point-min))))))
+      (erc-put-text-properties (point-min) (point-max)
+                               '(line-prefix wrap-prefix) nil
+                               `((space :width ,(- erc-fill--wrap-value 1 len))
+                                 ,erc-fill--wrap-prefix)))))
+
+;; This is an experimental helper for third-party modules.  You could,
+;; for example, use this to automatically resize the prefix to a
+;; fraction of the window's width on some event change.
+
+(defun erc-fill--wrap-fix (&optional value)
+  "Re-wrap from `point-min' to `point-max'.
+Reset prefix to VALUE, when given."
+  (save-excursion
+    (when value
+      (setq erc-fill--wrap-value value
+            erc-fill--wrap-prefix (list 'space :width value)))
+    (let ((inhibit-field-text-motion t)
+          (inhibit-read-only t))
+      (goto-char (point-min))
+      (while (and (zerop (forward-line))
+                  (< (point) (min (point-max) erc-insert-marker)))
+        (save-restriction
+          (narrow-to-region (pos-bol) (pos-eol))
+          (erc-fill-wrap))))))
+
+(defun erc-fill--wrap-nudge (arg)
+  (save-excursion
+    (save-restriction
+      (widen)
+      (let ((inhibit-field-text-motion t)
+            (inhibit-read-only t) ; necessary?
+            (p (goto-char (point-min))))
+        (when (zerop arg)
+          (setq arg (- erc-fill-static-center erc-fill--wrap-value)))
+        (cl-incf (caddr erc-fill--wrap-prefix) arg)
+        (cl-incf erc-fill--wrap-value arg)
+        (while (setq p (next-single-property-change p 'line-prefix))
+          (when-let ((v (get-text-property p 'line-prefix)))
+            (cl-incf (caddr v) arg)
+            (when-let
+                ((e (text-property-not-all p (point-max) 'line-prefix v)))
+              (goto-char e)))))))
+  arg)
+
+(defun erc-fill-wrap-nudge (arg)
+  "Adjust `erc-fill-wrap' by ARG columns.
+Offer to repeat command in a manner similar to
+`text-scale-adjust'.  Note that misalignment may occur when
+messages contain decorations applied by third-party modules.
+See `erc-fill--wrap-fix' for a workaround."
+  (interactive "p")
+  (unless erc-fill--wrap-value
+    (cl-assert (not erc-fill-wrap-mode))
+    (user-error "Minor mode `erc-fill-wrap-mode' disabled"))
+  (let ((total (erc-fill--wrap-nudge arg))
+        (start (window-start))
+        (marker (set-marker (make-marker) (point))))
+    (when (zerop arg)
+      (setq arg 1))
+    (set-transient-map
+     (let ((map (make-sparse-keymap)))
+       (dolist (key '(?+ ?= ?- ?0))
+         (let ((a (pcase key
+                    (?0 0)
+                    (?- (- (abs arg)))
+                    (_ (abs arg)))))
+           (define-key map (vector (list key))
+                       (lambda ()
+                         (interactive)
+                         (cl-incf total (erc-fill--wrap-nudge a))
+                         (set-window-start (selected-window) start)
+                         (goto-char marker)))))
+       map)
+     t
+     (lambda ()
+       (set-marker marker nil)
+       (message "Fill prefix: %d (%+d col%s)"
+                erc-fill--wrap-value total (if (> (abs total) 1) "s" "")))
+     "Use %k for further adjustment"
+     1)
+    (goto-char marker)
+    (set-window-start (selected-window) start)))
+
 (defun erc-fill-regarding-timestamp ()
   "Fills a text such that messages start at column `erc-fill-static-center'."
   (fill-region (point-min) (point-max) t t)
-- 
2.38.1