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

From: "J.P." <[email-masked]>
Date: Fri, 06 Oct 2023 08:17:23 -0700

v3. Move new meta-data related text properties to a single-character
interval at the head of every message. Add facility for managing such
props on behalf of modification hooks. Add utilities for retrieving data
from message-delimiting props and for traversing inserted messages.

In an attempt to tamp down on the growing mound of complications
involved in wrangling text properties across modules, I'm proposing a
general facility for managing certain props going forward. It works as
follows:

  1. confine meta-data related props to a one-char interval that, along
     with a preceding newline, delimit all message boundaries [1]

  2. apply nonessential message-spanning props, like
     `cursor-sensor-functions', lazily and only as needed by their
     controlling options [2]

  3. offer a means of passing state between hook stages, optionally to
     end up as properties in the meta-data interval

  4. keep this mechanism internal for the time being, but have it manage
     most props introduced in 5.6

In some ways, this amounts to a major reworking of how ERC handles
messages during and after insertion. Initially, I wanted to defer such
an endeavor to 5.7, but it's become clear to me that doing this now will
immensely fortify the implementation of various features shipping with
this release. If you're a module author or would-be contributor, it's in
your interest to keep an eye on how this unfolds. Happy to answer
questions or concerns, as always. Thanks.

[1] In an ideal world, a message's properties would live on its
    preceding newline. However, ERC's hooks have always visited messages
    along with their trailing newline. Obviously, having hooks see
    properties for the message to follow (or having the current
    message's props live on its trailing newline) would never work.

[2] Props whose intervals inform their role, such as buttons, faces, and
    display/formatting attributes, can't easily conform to this system.
    But, we can still benefit from formally declaring the hook stage
    (and maybe specific depth range) at which such props should be
    added. For example, message-spanning props ought to be applied no
    earlier than post-modification (e.g., `erc-send-post-hook' and
    `erc-insert-post-hook').

>From fcb34a45afd872361b0dbc8e6bd92ba53b910faa Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Fri, 6 Oct 2023 06:52:03 -0700
Subject: [PATCH 0/7] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (7):
  [5.6] Allow spoofing process marker in erc-display-line-1
  [5.6] Honor nil values in erc--restore-initialize-priors
  [5.6] Preserve format-spec args in erc-server-JOIN
  [5.6] Deprecate option erc-remove-parsed-property
  [5.6] Add helper for removing list-valued text props in ERC
  [5.6] Manage meta-data text props for ERC hook members
  [5.6] Add command to refill buffer with erc-fill-wrap-mode

 etc/ERC-NEWS                                  |  36 ++-
 lisp/erc/erc-backend.el                       |  11 +-
 lisp/erc/erc-fill.el                          | 167 ++++++++----
 lisp/erc/erc-goodies.el                       |   4 +-
 lisp/erc/erc-stamp.el                         | 237 ++++++++++++++----
 lisp/erc/erc-truncate.el                      |   2 +-
 lisp/erc/erc.el                               | 223 +++++++++++++---
 test/lisp/erc/erc-fill-tests.el               |  78 ++++--
 test/lisp/erc/erc-scenarios-log.el            |   1 +
 test/lisp/erc/erc-scenarios-match.el          | 205 ++++++++++++---
 test/lisp/erc/erc-stamp-tests.el              |   2 +-
 test/lisp/erc/erc-tests.el                    | 229 ++++++++++++++++-
 .../resources/base/assoc/multi-net/barnet.eld |  12 +-
 .../resources/base/assoc/multi-net/foonet.eld |  12 +-
 .../base/netid/bouncer/barnet-drop.eld        |   4 +-
 .../base/netid/bouncer/foonet-drop.eld        |   4 +-
 .../fill/snapshots/merge-01-start.eld         |   2 +-
 .../fill/snapshots/merge-02-right.eld         |   2 +-
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../fill/snapshots/monospace-01-start.eld     |   2 +-
 .../fill/snapshots/monospace-02-right.eld     |   2 +-
 .../fill/snapshots/monospace-03-left.eld      |   2 +-
 .../fill/snapshots/monospace-04-reset.eld     |   2 +-
 .../fill/snapshots/spacing-01-mono.eld        |   2 +-
 .../fill/snapshots/stamps-left-01.eld         |   2 +-
 25 files changed, 992 insertions(+), 253 deletions(-)

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index a8f7ee8a944..81c94467f25 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -153,13 +153,9 @@ easily determining what right-sided stamps would look like before
 insertion, which is knowledge needed for certain UI decisions.  The
 way ERC has chosen to address this is imperfect and boils down to
 asking users who've customized this option to switch to
-'erc-timestamp-format' instead.  Somewhat relatedly, the companion
-option 'erc-timestamp-format-left', which determines the look of date
-stamps, must now end in a newline.  Although this has long been the
-case in practice, it's now been made official.  As always, if you're
-affected by these changes and feel that other solutions, like
-automatic migration, are justified, please make that known on the bug
-list.
+'erc-timestamp-format' instead.  If you're affected by this and feel
+that some other solution, like automatic migration, is justified,
+please make that known on the bug list.
 
 ** 'erc-button-alist' and 'erc-nick-popup-alist' have evolved slightly.
 It's no secret that the 'buttons' module treats potential nicknames
@@ -225,6 +221,14 @@ atop any message.  The new companion option 'erc-echo-timestamp-zone'
 determines the default timezone when not specified with a prefix
 argument.
 
+** Option 'erc-remove-parsed-property' deprecated.
+This option's nil behavior serves no practical purpose yet has the
+potential to degrade the user experience by competing for space with
+forthcoming features powered by next generation extensions.  Anyone
+with a legitimate use for this option likely also possesses the
+knowledge to rig up a suitable analog with minimal effort.  That said,
+the road to removal is long.
+
 ** Option 'erc-warn-about-blank-lines' is more informative.
 Enabled by default, this option now produces more useful feedback
 whenever ERC rejects prompt input containing whitespace-only lines.
@@ -287,11 +291,13 @@ continue to modify non-ERC hooks locally whenever possible, especially
 in new code.
 
 *** ERC now manages timestamp-related properties a bit differently.
-For starters, the 'cursor-sensor-functions' property no longer
+For starters, the 'cursor-sensor-functions' text property is absent by
+default unless the option 'erc-echo-timestamps' is already enabled on
+module init.  And when present, the property's value no longer
 contains unique closures and thus no longer proves effective for
-traversing messages.  To compensate, a new property, 'erc-timestamp',
-now spans message bodies but not the newlines delimiting them.  Also
-affecting the 'stamp' module is the deprecation of the function
+traversing inserted messages.  For now, ERC only provides an internal
+means of visiting messages, but a public interface is forthcoming.
+Also affecting the 'stamp' module is the deprecation of the function
 'erc-insert-aligned' and its removal from client code.  Additionally,
 the module now merges its 'invisible' property with existing ones and
 includes all white space around stamps when doing so.
@@ -306,6 +312,22 @@ folded onto the next line.  Such inconsistency made stamp detection
 overly complex and produced uneven results when toggling stamp
 visibility.
 
+*** Date stamps are independent messages.
+ERC now inserts "date stamps" generated from the option
+'erc-timestamp-format-left' as separate, standalone messages.  (This
+only matters if 'erc-insert-timestamp-function' is set to its default
+value of 'erc-insert-timestamp-left-and-right'.)  ERC's near-term UI
+goals require exposing these stamps to existing code designed to
+operate on complete messages.  For example, users likely expect date
+stamps to be togglable with 'erc-toggle-timestamps' while also being
+immune to hiding from commands like 'erc-match-toggle-hidden-fools'.
+Before this change, meeting such expectations demanded brittle
+heuristics that checked for the presence of these stamps in the
+leading portion of message bodies as well as special casing to act on
+these areas without inflicting collateral damage.  From now on, third
+parties can instead use the function 'erc-stamp-date-left-p' to detect
+and reuse existing code to operate.
+
 *** The role of a module's Custom group is now more clearly defined.
 Associating built-in modules with Custom groups and provided library
 features has improved.  More specifically, a module's group now enjoys
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index fb10ee31c78..bc42917375a 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1718,7 +1718,7 @@ erc--server-determine-join-display-context
       (if (string-match "^\\(.*\\)\^g.*$" chnl)
           (setq chnl (match-string 1 chnl)))
       (save-excursion
-        (let* ((str (cond
+        (let ((args (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
                       (let ((erc--display-context
@@ -1735,18 +1735,15 @@ erc--server-determine-join-display-context
                         (erc-channel-begin-receiving-names))
                       (erc-update-mode-line)
                       (run-hooks 'erc-join-hook)
-                      (erc-make-notice
-                       (erc-format-message 'JOIN-you ?c chnl)))
+                      (list 'JOIN-you ?c chnl))
                      (t
                       (setq buffer (erc-get-buffer chnl proc))
-                      (erc-make-notice
-                       (erc-format-message
-                        'JOIN ?n nick ?u login ?h host ?c chnl))))))
+                      (list 'JOIN ?n nick ?u login ?h host ?c chnl)))))
           (when buffer (set-buffer buffer))
           (erc-update-channel-member chnl nick nick t nil nil nil nil nil host login)
           ;; on join, we want to stay in the new channel buffer
           ;;(set-buffer ob)
-          (erc-display-message parsed nil buffer str))))))
+          (apply #'erc-display-message parsed 'notice buffer args))))))
 
 (define-erc-response-handler (KICK)
   "Handle kick messages received from the server." nil
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 608119c8d6e..8b86cf30bf4 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -158,6 +158,11 @@ erc-fill
     (when (or erc-fill--function erc-fill-function)
       ;; skip initial empty lines
       (goto-char (point-min))
+      ;; Note the following search pattern was altered in 5.6 to adapt
+      ;; to a change in Emacs regexp behavior that turned out to be a
+      ;; regression (which has since been fixed).  The patterns appear
+      ;; to be equivalent in practice, so this was left as is (wasn't
+      ;; reverted) to avoid additional git-blame(1)-related churn.
       (while (and (looking-at (rx bol (* (in " \t")) eol))
                   (zerop (forward-line 1))))
       (unless (eobp)
@@ -167,12 +172,10 @@ erc-fill
           (when-let* ((erc-fill-line-spacing)
                       (p (point-min)))
             (widen)
-            (when (or (and-let* ((cmd (get-text-property p 'erc-command)))
-                        (memq cmd erc-fill--spaced-commands))
+            (when (or (erc--check-msg-prop 'erc-cmd erc-fill--spaced-commands)
                       (and-let* ((cmd (save-excursion
                                         (forward-line -1)
-                                        (get-text-property (point)
-                                                           'erc-command))))
+                                        (get-text-property (point) 'erc-cmd))))
                         (memq cmd erc-fill--spaced-commands)))
               (put-text-property (1- p) p
                                  'line-spacing erc-fill-line-spacing))))))))
@@ -181,15 +184,17 @@ erc-fill-static
   "Fills a text such that messages start at column `erc-fill-static-center'."
   (save-restriction
     (goto-char (point-min))
-    (looking-at "^\\(\\S-+\\)")
-    (let ((nick (match-string 1)))
+    (when-let (((looking-at "^\\(\\S-+\\)"))
+               ((not (erc--check-msg-prop 'erc-msg 'datestamp)))
+               (nick (match-string 1)))
+      (progn
         (let ((fill-column (- erc-fill-column (erc-timestamp-offset)))
               (fill-prefix (make-string erc-fill-static-center 32)))
           (insert (make-string (max 0 (- erc-fill-static-center
                                          (length nick) 1))
                                32))
           (erc-fill-regarding-timestamp))
-        (erc-restore-text-properties))))
+        (erc-restore-text-properties)))))
 
 (defun erc-fill-variable ()
   "Fill from `point-min' to `point-max'."
@@ -423,8 +428,6 @@ fill-wrap
              (eq (default-value 'erc-insert-timestamp-function)
                  #'erc-insert-timestamp-left)))
    (setq erc-fill--function #'erc-fill-wrap)
-   (add-function :after (local 'erc-stamp--insert-date-function)
-                 #'erc-fill--wrap-stamp-insert-prefixed-date)
    (when erc-fill-wrap-merge
      (add-hook 'erc-button--prev-next-predicate-functions
                #'erc-fill--wrap-merged-button-p nil t))
@@ -436,9 +439,7 @@ fill-wrap
    (kill-local-variable 'erc-fill--function)
    (kill-local-variable 'erc-fill--wrap-visual-keys)
    (remove-hook 'erc-button--prev-next-predicate-functions
-                #'erc-fill--wrap-merged-button-p t)
-   (remove-function (local 'erc-stamp--insert-date-function)
-                    #'erc-fill--wrap-stamp-insert-prefixed-date))
+                #'erc-fill--wrap-merged-button-p t))
   'local)
 
 (defvar-local erc-fill--wrap-length-function nil
@@ -456,6 +457,9 @@ erc-fill--wrap-last-msg
 (defvar-local erc-fill--wrap-max-lull (* 24 60 60))
 
 (defun erc-fill--wrap-continued-message-p ()
+  "Return non-nil when the current speaker hasn't changed.
+That is, indicate whether the text just inserted is from the same
+sender as that of the previous \"PRIVMSG\"."
   (prog1 (and-let*
              ((m (or erc-fill--wrap-last-msg
                      (setq erc-fill--wrap-last-msg (point-min-marker))
@@ -463,14 +467,11 @@ erc-fill--wrap-continued-message-p
               ((< (1+ (point-min)) (- (point) 2)))
               (props (save-restriction
                        (widen)
-                       (when (eq 'erc-timestamp (field-at-pos m))
-                         (set-marker m (field-end m)))
                        (and-let*
-                           (((eq 'PRIVMSG (get-text-property m 'erc-command)))
-                            ((not (eq (get-text-property m 'erc-ctcp)
-                                      'ACTION)))
+                           (((eq 'PRIVMSG (get-text-property m 'erc-cmd)))
+                            ((not (eq (get-text-property m 'erc-msg) 'ACTION)))
                             (spr (next-single-property-change m 'erc-speaker)))
-                         (cons (get-text-property m 'erc-timestamp)
+                         (cons (get-text-property m 'erc-ts)
                                (get-text-property spr 'erc-speaker)))))
               (ts (pop props))
               (props)
@@ -478,7 +479,7 @@ erc-fill--wrap-continued-message-p
               ((time-less-p (time-subtract (erc-stamp--current-time) ts)
                             erc-fill--wrap-max-lull))
               (speaker (next-single-property-change (point-min) 'erc-speaker))
-              ((not (eq (get-text-property speaker 'erc-ctcp) 'ACTION)))
+              ((not (erc--check-msg-prop 'erc-ctcp 'ACTION)))
               (nick (get-text-property speaker 'erc-speaker))
               ((erc-nick-equal-p props nick))))
     (set-marker erc-fill--wrap-last-msg (point-min))))
@@ -491,27 +492,11 @@ erc-fill--wrap-measure
       (save-excursion
         (save-restriction
           (narrow-to-region beg end)
-          (let (buffer-invisibility-spec)
-            (list (car (buffer-text-pixel-size))))))
+          (let* ((buffer-invisibility-spec)
+                 (rv (car (buffer-text-pixel-size))))
+            (if (zerop rv) 0 (list rv)))))
     (- end beg)))
 
-(defun erc-fill--wrap-stamp-insert-prefixed-date (&rest _)
-  "Apply `line-prefix' property to args.
-Expect a multiline \"date\" stamp ending in a newline, similar to
-the default value of `erc-timestamp-format-left'.  Omit the
-`line-prefix' from any trailing newlines."
-  (let* ((beg)
-         ;; Insert " " to simulate gap between <speaker> and msg beg.
-         (end (save-excursion (skip-chars-backward "\n")
-                              (setq beg (pos-bol))
-                              (insert " ")
-                              (point)))
-         (width (erc-fill--wrap-measure beg end)))
-    (delete-region (1- end) end)
-    ;; Use `point-min' instead of `beg' to cover leading newilnes.
-    (put-text-property (point-min) (1- end) 'line-prefix
-                       `(space :width (- erc-fill--wrap-value ,width)))))
-
 ;; An escape hatch for third-party code expecting speakers of ACTION
 ;; messages to be exempt from `line-prefix'.  This could be converted
 ;; into a user option if users feel similarly.
@@ -531,15 +516,22 @@ erc-fill-wrap
                      (when-let ((e (erc--get-speaker-bounds))
                                 (b (pop e))
                                 ((or erc-fill--wrap-action-dedent-p
-                                     (not (eq (get-text-property b 'erc-ctcp)
-                                              'ACTION)))))
+                                     (not (erc--check-msg-prop 'erc-ctcp
+                                                               'ACTION)))))
                        (goto-char e))
                      (skip-syntax-forward "^-")
                      (forward-char)
-                     ;; Using the `invisible' property might make more
-                     ;; sense, but that would require coordination
-                     ;; with other modules, like `erc-match'.
-                     (cond ((and erc-fill-wrap-merge
+                     (cond ((erc--check-msg-prop 'erc-msg 'datestamp)
+                            (when erc-fill--wrap-last-msg
+                              (set-marker erc-fill--wrap-last-msg (point-min)))
+                            (save-excursion
+                              (goto-char (point-max))
+                              (skip-chars-backward "\n")
+                              (let ((beg (pos-bol)))
+                                (insert " ")
+                                (prog1 (erc-fill--wrap-measure beg (point))
+                                  (delete-region (1- (point)) (point))))))
+                           ((and erc-fill-wrap-merge
                                  (erc-fill--wrap-continued-message-p))
                             (put-text-property (point-min) (point)
                                                'display "")
@@ -554,11 +546,12 @@ erc-fill-wrap
 (defvar erc-fill--wrap-rejigger-last-message nil
   "Temporary working instance of `erc-fill--wrap-last-msg'.")
 
-(defun erc-fill--wrap-rejigger-region (start finish on-next)
+(defun erc-fill--wrap-rejigger-region (start finish on-next repairp)
   "Recalculate `line-prefix' from START to FINISH.
 After refilling each message, call ON-NEXT with no args.  But
 stash and restore `erc-fill--wrap-last-msg' before doing so, in
-case this module's insert hooks run by way of the process filter."
+case this module's insert hooks run by way of the process filter.
+With REPAIRP, destructively fill gaps and re-merge speakers."
   (goto-char start)
   (cl-assert (null erc-fill--wrap-rejigger-last-message))
   (let (erc-fill--wrap-rejigger-last-message)
@@ -571,24 +564,41 @@ erc-fill--wrap-rejigger-region
          (end (text-property-not-all beg finish 'line-prefix val)))
       ;; If this is a left-side stamp on its own line.
       (remove-text-properties beg (1+ end) '(line-prefix nil wrap-prefix nil))
-      (save-restriction
-        (narrow-to-region beg (1+ end))
-        (if-let (((eq 'erc-timestamp (field-at-pos beg)))
-                 ((eq 'date-left (get-text-property beg 'erc-stamp-type))))
-            (progn
-              (goto-char (field-end beg))
-              (erc-fill--wrap-stamp-insert-prefixed-date))
+      (when-let ((repairp)
+                 (dbeg (text-property-not-all beg end 'display nil))
+                 ((get-text-property (1+ dbeg) 'erc-speaker))
+                 (dval (get-text-property dbeg 'display))
+                 ((equal "" dval)))
+        (remove-text-properties
+         dbeg (text-property-not-all dbeg end 'display dval) '(display)))
+      (let* ((pos (if (eq 'date-left (get-text-property beg 'erc-stamp-type))
+                      (field-beginning beg)
+                    beg))
+             (erc--msg-props (map-into (text-properties-at pos) 'hash-table))
+             (erc-stamp--current-time (gethash 'erc-ts erc--msg-props)))
+        (save-restriction
+          (narrow-to-region beg (1+ end))
           (let ((erc-fill--wrap-last-msg erc-fill--wrap-rejigger-last-message))
             (erc-fill-wrap)
             (setq erc-fill--wrap-rejigger-last-message
                   erc-fill--wrap-last-msg))))
       (when on-next
         (funcall on-next))
-      (goto-char end))))
-
-(defun erc-fill-wrap-refill-buffer ()
-  "Recalculate all `fill-wrap' prefixes in the current buffer."
-  (interactive)
+      ;; Skip to end of message upon encountering accidental gaps
+      ;; introduced by third parties (or bugs).
+      (if-let (((/= ?\n (char-after end)))
+               (next (erc--get-inserted-msg-bounds 'end beg)))
+          (progn
+            (cl-assert (= ?\n (char-after next)))
+            (when repairp ; eol <= next
+              (put-text-property end (pos-eol) 'line-prefix val))
+            (goto-char next))
+        (goto-char end)))))
+
+(defun erc-fill-wrap-refill-buffer (repair)
+  "Recalculate all `fill-wrap' prefixes in the current buffer.
+With REPAIR, attempt to destructively fix merged properties."
+  (interactive "P")
   (unless erc-fill-wrap-mode
     (user-error "Module `fill-wrap' not active in current buffer."))
   (save-excursion
@@ -599,7 +609,8 @@ erc-fill-wrap-refill-buffer
              (callback (lambda ()
                          (progress-reporter-update rep (cl-incf seen))
                          (accept-process-output nil 0.000001))))
-        (erc-fill--wrap-rejigger-region (point-min) erc-insert-marker callback)
+        (erc-fill--wrap-rejigger-region (point-min) erc-insert-marker
+                                        callback repair)
         (progress-reporter-done rep)))))
 
 ;; FIXME use own text property to avoid false positives.
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index b77176d8ac7..d112e63c316 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -339,8 +339,8 @@ erc-scroll-to-bottom
 ;;;###autoload(autoload 'erc-readonly-mode "erc-goodies" nil t)
 (define-erc-module readonly nil
   "This mode causes all inserted text to be read-only."
-  ((add-hook 'erc-insert-post-hook #'erc-make-read-only)
-   (add-hook 'erc-send-post-hook #'erc-make-read-only))
+  ((add-hook 'erc-insert-post-hook #'erc-make-read-only 70)
+   (add-hook 'erc-send-post-hook #'erc-make-read-only 70))
   ((remove-hook 'erc-insert-post-hook #'erc-make-read-only)
    (remove-hook 'erc-send-post-hook #'erc-make-read-only)))
 
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index 68dd1f287cf..7fc76eb2d73 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -55,23 +55,14 @@ erc-timestamp-format
   :type '(choice (const nil)
 		 (string)))
 
-(defun erc-stamp--custom-trailing-newline-p (_ value)
-  "Return non-nil if VALUE ends in a newline."
-  (string-suffix-p "\n" value))
-
-(defun erc-stamp--custom-validate-date-stamp (widget)
-  "Fail unless WIDGET's value ends in a newline."
-  (unless (string-suffix-p "\n" (widget-value widget))
-    (widget-put widget :error "Value lacks a trailing newline")
-    widget))
-
 (defcustom erc-timestamp-format-left "\n[%a %b %e %Y]\n"
   "Format recognized by `format-time-string' for date stamps.
 Only considered when `erc-insert-timestamp-function' is set to
 `erc-insert-timestamp-left-and-right'.  Used for displaying date
-stamps on their own line, between messages.  As of ERC 5.6, this
-module appends a trailing newline on insertion if needed.  Any
-extra newlines, leading or trailing, become empty lines.  For
+stamps on their own line, between messages.  ERC inserts this
+flavor of stamp as a separate \"psuedo message\", so a final
+newline isn't necessary.  For compatibility, only additional
+trailing newlines beyond the first become empty lines.  For
 example, the default value results in an empty line after the
 previous message, followed by the timestamp on its own line,
 followed immediately by the next message on the next line.  ERC
@@ -79,11 +70,7 @@ erc-timestamp-format-left
 formatting specifiers should reflect that.  To omit these stamps
 entirely, use a different `erc-insert-timestamp-function', such
 as `erc-timestamp-format-right'."
-  :type '(string :validate erc-stamp--custom-validate-date-stamp
-                 :match erc-stamp--custom-trailing-newline-p)
-  :set (lambda (sym val)
-         (set-default sym
-                      (if (string-suffix-p "\n" val) val (concat val "\n")))))
+  :type 'string)
 
 (defcustom erc-timestamp-format-right nil
   "If set to a string, messages will be timestamped.
@@ -189,9 +176,9 @@ erc-timestamp-face
 ;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t)
 (define-erc-module stamp timestamp
   "This mode timestamps messages in the channel buffers."
-  ((add-hook 'erc-mode-hook #'erc-munge-invisibility-spec)
-   (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 60)
-   (add-hook 'erc-send-modify-hook #'erc-add-timestamp 60)
+  ((add-hook 'erc-mode-hook #'erc-stamp--setup)
+   (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 79)
+   (add-hook 'erc-send-modify-hook #'erc-add-timestamp 79)
    (add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
    (add-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-stamp--setup)))
@@ -228,18 +215,27 @@ erc-stamp--current-time
 
 (cl-defgeneric erc-stamp--current-time ()
   "Return a lisp time object to associate with an IRC message.
-This becomes the message's `erc-timestamp' text property."
+This becomes the message's `erc-ts' text property."
   (erc-compat--current-lisp-time))
 
 (cl-defmethod erc-stamp--current-time :around ()
   (or erc-stamp--current-time (cl-call-next-method)))
 
+(defvar erc-stamp--skip nil
+  "Non-nil means inhibit `erc-add-timestamp' completely.")
+
+(defvar erc-stamp--allow-unmanaged nil
+  "Non-nil means `erc-add-timestamp' runs unconditionally.
+Escape hatch for third-parties using lower-level API functions,
+such as `erc-display-line', directly.")
+
 (defun erc-add-timestamp ()
   "Add timestamp and text-properties to message.
 
 This function is meant to be called from `erc-insert-modify-hook'
 or `erc-send-modify-hook'."
-  (progn ; remove this `progn' on next major refactor
+  (unless (or erc-stamp--skip (and erc-stamp--allow-unmanaged
+                                   (not erc--msg-props)))
     (let* ((ct (erc-stamp--current-time))
            (invisible (get-text-property (point-min) 'invisible))
            (erc-stamp--invisible-property
@@ -247,6 +243,8 @@ erc-add-timestamp
             (if invisible `(timestamp ,@(ensure-list invisible)) 'timestamp))
            (skipp (and erc-stamp--skip-when-invisible invisible))
            (erc-stamp--current-time ct))
+      (when erc--msg-props
+        (puthash 'erc-ts ct erc--msg-props))
       (unless skipp
         (funcall erc-insert-timestamp-function
                  (erc-format-timestamp ct erc-timestamp-format)))
@@ -258,12 +256,13 @@ erc-add-timestamp
                  (erc-away-time))
 	(funcall erc-insert-away-timestamp-function
 		 (erc-format-timestamp ct erc-away-timestamp-format)))
-      (add-text-properties (point-min) (1- (point-max))
+      (when erc-stamp--allow-unmanaged
+        (add-text-properties (point-min) (1- (point-max))
 			   ;; It's important for the function to
 			   ;; be different on different entries (bug#22700).
 			   (list 'cursor-sensor-functions
                                  ;; Regions are no longer contiguous ^
-                                 '(erc--echo-ts-csf) 'erc-timestamp ct)))))
+                                 '(erc--echo-ts-csf) 'erc-ts ct))))))
 
 (defvar-local erc-timestamp-last-window-width nil
   "The width of the last window that showed the current buffer.
@@ -376,14 +375,14 @@ erc-stamp-prefix-log-filter
   (goto-char (point-min))
   (while
       (progn
-        (when-let* (((< (point) (pos-eol)))
-                    (end (1- (pos-eol)))
-                    ((eq 'erc-timestamp (field-at-pos end)))
-                    (beg (field-beginning end))
-                    ;; Skip a line that's just a timestamp.
-                    ((> beg (point))))
+        (when-let (((< (point) (pos-eol)))
+                   (end (1- (pos-eol)))
+                   ((eq 'erc-timestamp (field-at-pos end)))
+                   (beg (field-beginning end))
+                   ;; Skip a line that's just a timestamp.
+                   ((> beg (point))))
           (delete-region beg (1+ end)))
-        (when-let (time (get-text-property (point) 'erc-timestamp))
+        (when-let (time (erc--get-inserted-msg-prop 'erc-ts))
           (insert (format-time-string "[%H:%M:%S] " time)))
         (zerop (forward-line))))
   "")
@@ -595,8 +594,11 @@ erc-insert-timestamp-right
       ;; intervening white space unless a hard break is warranted.
       (pcase erc-timestamp-use-align-to
         ((guard erc-stamp--display-margin-mode)
-         (put-text-property 0 (length string)
-                            'display `((margin right-margin) ,string) string))
+         (let ((s (propertize (substring-no-properties string)
+                              'invisible erc-stamp--invisible-property)))
+           (put-text-property 0 (length string) 'display
+                              `((margin right-margin) ,s)
+                              string)))
         ((and 't (guard (< col pos)))
          (insert " ")
          (put-text-property from (point) 'display `(space :align-to ,pos)))
@@ -621,38 +623,77 @@ erc-insert-timestamp-right
       (when erc-timestamp-intangible
 	(erc-put-text-property from (1+ (point)) 'cursor-intangible t)))))
 
-(defvar erc-stamp--insert-date-function #'insert
-  "Function to insert left \"left-right date\" stamp.
-A local module might use this to modify text properties,
-`insert-before-markers' or renarrow the region after insertion.")
-
-(defun erc-stamp--decrement-date-invisibility-bounds ()
-  "Extend `invisible' prop to previous newline before date stamp.
-And apply original prop value from message body to any trailing
-newlines after date."
-  (let ((beg (point-min)))
+(defvar erc-stamp--insert-date-hook nil
+  "Functions appended to send and modify hooks when inserting date stamp.")
+
+(defvar-local erc-stamp--date-format-end nil
+  "Substring index marking usable portion of date stamp format.")
+
+(defun erc-stamp--propertize-left-date-stamp ()
+  (add-text-properties (point-min) (1- (point-max))
+                       '(field erc-timestamp erc-stamp-type date-left))
+  (erc--hide-message 'timestamp))
+
+(defun erc-stamp-date-left-p (&optional point)
+  "Return non-nil if the current message is a \"date stamp\".
+Expect callers to know that such stamps originate from
+`erc-insert-timestamp-left-and-right' using the format string
+`erc-timestamp-format-left'.  Expect POINT, when non-nil, to
+reside at some known or suspected time stamp.  When POINT is nil,
+expect to be called from a member of `erc-insert-modify-hook' or
+similar."
+  (cond ((erc--check-msg-prop 'erc-msg 'datestamp))
+        (point (eq 'date-left (get-text-property point 'erc-stamp-type)))
+        (t (erc--with-inserted-msg
+            (and-let* ((p (text-property-not-all
+                           (point-min) (point-max) 'field 'erc-timestamp)))
+              (eq 'date-left (get-text-property p 'erc-stamp-type)))))))
+
+;; A kludge to pass state from insert hook to nested insert hook.
+(defvar erc-stamp--current-datestamp-left nil)
+
+;; Calling `erc-display-message' from within a hook it's currently
+;; running is roundabout, but it's a definite means of ensuring hooks
+;; can act on the date stamp as a standalone message to do things like
+;; adjust invisibility props.
+(defun erc-stamp--insert-date-stamp-as-phony-message (string)
+  (cl-assert (string-empty-p string))
+  (setq string erc-stamp--current-datestamp-left)
+  (cl-assert string)
+  (let ((erc-stamp--skip t)
+        (erc--msg-props (map-into `((erc-msg . datestamp)
+                                    (erc-ts . ,erc-stamp--current-time))
+                                  'hash-table))
+        (erc-send-modify-hook `(,@erc-send-modify-hook
+                                erc-stamp--propertize-left-date-stamp
+                                ,@erc-stamp--insert-date-hook))
+        (erc-insert-modify-hook `(,@erc-insert-modify-hook
+                                  erc-stamp--propertize-left-date-stamp
+                                  ,@erc-stamp--insert-date-hook)))
+    (erc-display-message nil nil (current-buffer) string)
+    (setq erc-timestamp-last-inserted-left string)))
+
+(defun erc-stamp--lr-date-on-pre-modify (_)
+  (unless erc-stamp--date-format-end
+    ;; Don't add text properties to the trailing newline.
+    (setq erc-stamp--date-format-end
+          (if (string-suffix-p "\n" erc-timestamp-format-left) -1 0)))
+  (when-let ((ct (or erc-stamp--current-time (erc-stamp--current-time)))
+             ;; Ignore existing prop value because date stamps should
+             ;; never be hideable except via `timestamp'.
+             (rendered (let (erc-stamp--invisible-property)
+                         (erc-format-timestamp
+                          ct (substring erc-timestamp-format-left
+                                        0 erc-stamp--date-format-end))))
+             ((not (string-equal rendered erc-timestamp-last-inserted-left)))
+             (erc-stamp--current-datestamp-left rendered)
+             (erc-insert-timestamp-function
+              #'erc-stamp--insert-date-stamp-as-phony-message))
     (save-restriction
-      (widen)
-      (when (and (> beg 4) (= (char-before beg) ?\n))
-        (when-let ((this (get-text-property (point) 'invisible))
-                   (prev (get-text-property (1- beg) 'invisible))
-                   ((not (equal this prev))))
-          (put-text-property (1- beg) beg 'invisible
-                             (seq-difference (ensure-list prev)
-                                             (ensure-list this))))
-        (put-text-property (1- beg) beg 'invisible 'timestamp)))
-    (cl-assert (= ?\n (char-before (point))))
-    ;; Only decrement bounds by one.  Additional newlines in the
-    ;; timestamp must be hidden.
-    (if-let ((existing (remq 'timestamp
-                             (ensure-list erc-stamp--invisible-property))))
-        (put-text-property (1- (point)) (point) 'invisible
-                           (if (cdr existing) existing (car existing)))
-      (erc--remove-from-prop-value-list
-       (1- (point)) (point) 'invisible 'timestamp))))
-
-(defvar-local erc-stamp--checked-date-string-p nil
-  "Non-nil if date string has been validated for current buffer.")
+      (narrow-to-region (or erc--insert-marker erc-insert-marker)
+                        (or erc--insert-marker erc-insert-marker))
+      (let (erc-timestamp-format erc-away-timestamp-format)
+        (erc-add-timestamp)))))
 
 (defun erc-insert-timestamp-left-and-right (string)
   "Insert a stamp on either side when it changes.
@@ -668,44 +709,23 @@ erc-insert-timestamp-left-and-right
 property to span a different interval, in order to satisfy newer
 folding requirements related to `erc-legacy-invisible-bounds-p'.
 Additionally, ensure every date stamp formatted with the option
-`erc-timestamp-format-left' has the property `erc-stamp-type' set
-to the symbol `date-left' so that modules can easily distinguish
-between other left-sided stamps and date stamps inserted by this
-function."
-  (unless erc-stamp--checked-date-string-p
-    (setq erc-stamp--checked-date-string-p t)
-    (unless (string-suffix-p "\n" erc-timestamp-format-left)
-      (setq erc-timestamp-format-left
-            (concat erc-timestamp-format-left "\n"))
-      (unless erc--target
-        (erc-button--display-error-notice-with-keys
-         (current-buffer)
-         "ERC only supports values of `%s' that end in a ?\\n."
-         " Changing value for current session to: %s."
-         " Update your config accordingly to silence this message."
-         'erc-timestamp-format-left
-         (let ((print-escape-newlines t))
-           (prin1-to-string erc-timestamp-format-left))))))
+`erc-timestamp-format-left' is marked as such so that modules can
+easily distinguish between other left-sided stamps and date
+stamps inserted by this function."
+  (unless erc-stamp--date-format-end
+    (add-hook 'erc-insert-pre-hook #'erc-stamp--lr-date-on-pre-modify -95 t)
+    (add-hook 'erc-send-pre-functions #'erc-stamp--lr-date-on-pre-modify -95 t)
+    (let ((erc--insert-marker (point-min-marker)))
+      (set-marker-insertion-type erc--insert-marker t)
+      (erc-stamp--lr-date-on-pre-modify nil)
+      (narrow-to-region erc--insert-marker (point-max))
+      (set-marker erc--insert-marker nil)))
   (let* ((ct (or erc-stamp--current-time (erc-stamp--current-time)))
-         (ts-left (let ((erc-stamp--invisible-property 'timestamp))
-                    (erc-format-timestamp ct erc-timestamp-format-left)))
          (ts-right (with-suppressed-warnings
                        ((obsolete erc-timestamp-format-right))
                      (if erc-timestamp-format-right
                          (erc-format-timestamp ct erc-timestamp-format-right)
                        string))))
-    ;; insert left timestamp
-    (unless (string-equal ts-left erc-timestamp-last-inserted-left)
-      (goto-char (point-min))
-      (add-text-properties 0 (length ts-left)
-                           '(field erc-timestamp erc-stamp-type date-left)
-                           ts-left)
-      (funcall erc-stamp--insert-date-function ts-left)
-      (unless (with-suppressed-warnings
-                  ((obsolete erc-legacy-invisible-bounds-p))
-                erc-legacy-invisible-bounds-p)
-        (erc-stamp--decrement-date-invisibility-bounds))
-      (setq erc-timestamp-last-inserted-left ts-left))
     ;; insert right timestamp
     (let ((erc-timestamp-only-if-changed-flag t)
 	  (erc-timestamp-last-inserted erc-timestamp-last-inserted-right))
@@ -722,8 +742,9 @@ erc-format-timestamp
       (let ((ts (format-time-string format time erc-stamp--tz)))
 	(erc-put-text-property 0 (length ts)
 			       'font-lock-face 'erc-timestamp-face ts)
-        (erc-put-text-property 0 (length ts) 'invisible
-                               erc-stamp--invisible-property ts)
+        (when erc-stamp--invisible-property
+          (erc-put-text-property 0 (length ts) 'invisible
+                                 erc-stamp--invisible-property ts))
 	;; N.B. Later use categories instead of this harmless, but
 	;; inelegant, hack. -- BPT
 	(and erc-timestamp-intangible
@@ -732,6 +753,8 @@ erc-format-timestamp
 	ts)
     ""))
 
+(defvar-local erc-stamp--csf-props-updated-p nil)
+
 ;; This function is used to munge `buffer-invisibility-spec' to an
 ;; appropriate value. Currently, it only handles timestamps, thus its
 ;; location.  If you add other features which affect invisibility,
@@ -744,10 +767,23 @@ erc-munge-invisibility-spec
       (cursor-intangible-mode -1)))
   (if erc-echo-timestamps
       (progn
+        (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+          (add-hook hook #'erc-stamp--add-csf-on-post-modify nil t))
+        (erc--restore-initialize-priors erc-stamp-mode
+          erc-stamp--csf-props-updated-p nil)
+        (unless (or erc-stamp--allow-unmanaged erc-stamp--csf-props-updated-p)
+          (setq erc-stamp--csf-props-updated-p t)
+          (let ((erc--msg-props (map-into '((erc-ts . t)) 'hash-table)))
+            (with-silent-modifications
+              (erc--traverse-inserted (point-min) erc-insert-marker
+                                      #'erc-stamp--add-csf-on-post-modify))))
         (cursor-sensor-mode +1) ; idempotent
         (when (>= emacs-major-version 29)
           (add-function :before-until (local 'clear-message-function)
                         #'erc-stamp--on-clear-message)))
+    (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+      (remove-hook hook #'erc-stamp--add-csf-on-post-modify t))
+    (kill-local-variable 'erc-stamp--csf-props-updated-p)
     (when (bound-and-true-p cursor-sensor-mode)
       (cursor-sensor-mode -1))
     (remove-function (local 'clear-message-function)
@@ -756,12 +792,22 @@ erc-munge-invisibility-spec
       (add-to-invisibility-spec 'timestamp)
     (remove-from-invisibility-spec 'timestamp)))
 
+(defun erc-stamp--add-csf-on-post-modify ()
+  "Add `cursor-sensor-functions' to narrowed buffer."
+  (when (erc--check-msg-prop 'erc-ts)
+    (put-text-property (point-min) (1- (point-max))
+                       'cursor-sensor-functions '(erc--echo-ts-csf))))
+
 (defun erc-stamp--setup ()
   "Enable or disable buffer-local `erc-stamp-mode' modifications."
   (if erc-stamp-mode
       (erc-munge-invisibility-spec)
     (let (erc-echo-timestamps erc-hide-timestamps erc-timestamp-intangible)
-      (erc-munge-invisibility-spec))))
+      (erc-munge-invisibility-spec))
+    ;; Undo local mods from `erc-insert-timestamp-left-and-right'.
+    (remove-hook 'erc-insert-pre-hook #'erc-stamp--lr-date-on-pre-modify t)
+    (remove-hook 'erc-send-pre-functions #'erc-stamp--lr-date-on-pre-modify t)
+    (kill-local-variable 'erc-stamp--date-format-end)))
 
 (defun erc-hide-timestamps ()
   "Hide timestamp information from display."
@@ -797,7 +843,7 @@ erc-stamp--last-stamp
 (defun erc-stamp--on-clear-message (&rest _)
   "Return `dont-clear-message' when operating inside the same stamp."
   (and erc-stamp--last-stamp erc-echo-timestamps
-       (eq (get-text-property (point) 'erc-timestamp) erc-stamp--last-stamp)
+       (eq (erc--get-inserted-msg-prop 'erc-ts) erc-stamp--last-stamp)
        'dont-clear-message))
 
 (defun erc-echo-timestamp (dir stamp &optional zone)
@@ -807,7 +853,7 @@ erc-echo-timestamp
 interpret a \"raw\" prefix as UTC.  To specify a zone for use
 with the option `erc-echo-timestamps', see the companion option
 `erc-echo-timestamp-zone'."
-  (interactive (list nil (get-text-property (point) 'erc-timestamp)
+  (interactive (list nil (erc--get-inserted-msg-prop 'erc-ts)
                      (pcase current-prefix-arg
                        ((and (pred numberp) v)
                         (if (<= (abs v) 14) (* v 3600) v))
@@ -821,18 +867,18 @@ erc-echo-timestamp
       (setq erc-stamp--last-stamp nil))))
 
 (defun erc--echo-ts-csf (_window _before dir)
-  (erc-echo-timestamp dir (get-text-property (point) 'erc-timestamp)))
+  (erc-echo-timestamp dir (erc--get-inserted-msg-prop 'erc-ts)))
 
 (defun erc-stamp--update-saved-position (&rest _)
-  (remove-function (local 'erc-stamp--insert-date-function)
-                   #'erc-stamp--update-saved-position)
-  (move-marker erc-last-saved-position (1- (point))))
+  (remove-hook 'erc-stamp--insert-date-hook
+               #'erc-stamp--update-saved-position t)
+  (move-marker erc-last-saved-position (1- (point-max))))
 
 (defun erc-stamp--reset-on-clear (pos)
   "Forget last-inserted stamps when POS is at insert marker."
   (when (= pos (1- erc-insert-marker))
-    (add-function :after (local 'erc-stamp--insert-date-function)
-                  #'erc-stamp--update-saved-position)
+    (add-hook 'erc-stamp--insert-date-hook
+              #'erc-stamp--update-saved-position 0 t)
     (setq erc-timestamp-last-inserted nil
           erc-timestamp-last-inserted-left nil
           erc-timestamp-last-inserted-right nil)))
diff --git a/lisp/erc/erc-truncate.el b/lisp/erc/erc-truncate.el
index 48d8408a85a..3350cbd13b7 100644
--- a/lisp/erc/erc-truncate.el
+++ b/lisp/erc/erc-truncate.el
@@ -102,7 +102,7 @@ erc-truncate-buffer-to-size
           ;; Truncate at message boundary (formerly line boundary
           ;; before 5.6).
 	  (goto-char end)
-          (goto-char (or (previous-single-property-change (point) 'erc-command)
+          (goto-char (or (erc--get-inserted-msg-bounds 'beg)
                          (pos-bol)))
 	  (setq end (point))
 	  ;; try to save the current buffer using
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a3321d9aabe..891689d8faa 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -135,9 +135,11 @@ erc-scripts
   "Running scripts at startup and with /LOAD."
   :group 'erc)
 
-;; Forward declarations
-(defvar erc-message-parsed)
+(defvar erc-message-parsed) ; only known to this file
+(defvar erc--msg-props nil)
+(defvar erc--msg-prop-overrides nil)
 
+;; Forward declarations
 (defvar tabbar--local-hlf)
 (defvar motif-version-string)
 (defvar gtk-version-string)
@@ -1370,16 +1372,15 @@ erc--target-priors
 (defmacro erc--restore-initialize-priors (mode &rest vars)
   "Restore local VARS for MODE from a previous session."
   (declare (indent 1))
-  (let ((existing (make-symbol "existing"))
+  (let ((priors (make-symbol "priors"))
+        (initp (make-symbol "initp"))
         ;;
-        restore initialize)
-    (while-let ((k (pop vars)) (v (pop vars)))
-      (push `(,k (alist-get ',k ,existing)) restore)
-      (push `(,k ,v) initialize))
-    `(if-let* ((,existing (or erc--server-reconnecting erc--target-priors))
-               ((alist-get ',mode ,existing)))
-         (setq ,@(mapcan #'identity (nreverse restore)))
-       (setq ,@(mapcan #'identity (nreverse initialize))))))
+        forms)
+    (while-let ((k (pop vars)))
+      (push `(,k (if ,initp (alist-get ',k ,priors) ,(pop vars))) forms))
+    `(let* ((,priors (or erc--server-reconnecting erc--target-priors))
+            (,initp (and ,priors (alist-get ',mode ,priors))))
+       (setq ,@(mapcan #'identity (nreverse forms))))))
 
 (defun erc--target-from-string (string)
   "Construct an `erc--target' variant from STRING."
@@ -2859,11 +2860,10 @@ erc-toggle-debug-irc-protocol
 (defun erc-send-action (tgt str &optional force)
   "Send CTCP ACTION information described by STR to TGT."
   (erc-send-ctcp-message tgt (format "ACTION %s" str) force)
-  (let ((erc-insert-pre-hook
-         (cons (lambda (s) ; Leave newline be.
-                 (put-text-property 0 (1- (length s)) 'erc-command 'PRIVMSG s)
-                 (put-text-property 0 (1- (length s)) 'erc-ctcp 'ACTION s))
-               erc-insert-pre-hook))
+  ;; Allow hooks that act on inserted PRIVMSG and NOTICES to process us.
+  (let ((erc--msg-prop-overrides '((erc-msg . msg)
+                                   (erc-cmd . PRIVMSG)
+                                   (erc-ctcp . ACTION)))
         (nick (erc-current-nick)))
     (setq nick (propertize nick 'erc-speaker nick))
     (erc-display-message nil '(t action input) (current-buffer)
@@ -2881,9 +2881,18 @@ erc-remove-parsed-property
 
 The default is to remove it, since it causes ERC to take up extra
 memory.  If you have code that relies on this property, then set
-this option to nil."
+this option to nil.
+
+Note that this option is deprecated because a value of nil is
+impractical in prolonged sessions with more than a few channels.
+Use `erc-insert-post-hook' or similar and the helper function
+`erc-find-parsed-property' and friends to stash the current
+`erc-response' object as needed.  And instead of using this for
+debugging purposes, try `erc-debug-irc-protocol'."
   :type 'boolean
   :group 'erc)
+(make-obsolete-variable 'erc-remove-parsed-property
+                        "impractical when non-nil" "30.1")
 
 (define-inline erc--assert-input-bounds ()
   (inline-quote
@@ -2913,6 +2922,68 @@ erc--refresh-prompt
         (delete-region (point) (1- erc-input-marker))))
     (run-hooks 'erc--refresh-prompt-hook)))
 
+(define-inline erc--check-msg-prop (prop &optional val)
+  "Return value for PROP in `erc--msg-props' when populated.
+If VAL is a list, return non-nil if PROP appears in VAL.  If VAL
+is otherwise non-nil, return non-nil if VAL compares `eq' to the
+stored value.  Otherwise, return the stored value."
+  (inline-letevals (prop val)
+    (let ((v (make-symbol "v")))
+      `(and-let* ((erc--msg-props)
+                  (,v (gethash ,prop erc--msg-props)))
+         (if (consp ,val) (memq ,v ,val) (if ,val (eq ,v ,val) ,v))))))
+
+(defmacro erc--get-inserted-msg-bounds (&optional only point)
+  `(let* ((point ,(or point '(point)))
+          (at-start-p (get-text-property point 'erc-msg)))
+     (and-let*
+         (,@(and (member only '(nil 'beg))
+                 '((b (or (and at-start-p point)
+                          (and-let*
+                              ((p (previous-single-property-change point
+                                                                   'erc-msg)))
+                            (if (= p (1- point)) point (1- p)))))))
+          ,@(and (member only '(nil 'end))
+                 '((e (1- (next-single-property-change
+                           (if at-start-p (1+ point) point)
+                           'erc-msg nil erc-insert-marker))))))
+       ,(pcase only
+          ('(quote beg) 'b)
+          ('(quote end) 'e)
+          (_ '(cons b e))))))
+
+(defun erc--get-inserted-msg-prop (prop)
+  "Return the value of text property PROP for some message at point."
+  (and-let* ((stack-pos (erc--get-inserted-msg-bounds 'beg)))
+    (get-text-property stack-pos prop)))
+
+(defmacro erc--with-inserted-msg (&rest body)
+  "Simulate buffer narrowing of send insert hooks for BODY.
+Note that this does not wrap BODY in `with-silent-modifications'.
+Similarly, it does not bind a temporary `erc--msg-props' table."
+  `(when-let ((bounds (erc--get-inserted-msg-bounds)))
+     (save-restriction
+       (narrow-to-region (car bounds) (1+ (cdr bounds)))
+       ,@body)))
+
+(defun erc--traverse-inserted (beg end fn)
+  "Visit messages between BEG and END and run FN in narrowed buffer."
+  (setq end (min end (marker-position erc-insert-marker)))
+  (save-excursion
+    (goto-char beg)
+    (let ((b (if (get-text-property (point) 'erc-msg)
+                 (point)
+               (next-single-property-change (point) 'erc-msg nil end))))
+      (while-let ((b)
+                  ((< b end))
+                  (e (next-single-property-change (1+ b) 'erc-msg nil end)))
+        (save-restriction
+          (narrow-to-region b e)
+          (funcall fn))
+        (setq b e)))))
+
+(defvar erc--insert-marker nil)
+
 (defun erc-display-line-1 (string buffer)
   "Display STRING in `erc-mode' BUFFER.
 Auxiliary function used in `erc-display-line'.  The line gets filtered to
@@ -2936,6 +3007,8 @@ erc-display-line-1
                            (format "%s" buffer)))
           (setq erc-insert-this t)
           (run-hook-with-args 'erc-insert-pre-hook string)
+          (setq insert-position (marker-position (or erc--insert-marker
+                                                     erc-insert-marker)))
           (if (null erc-insert-this)
               ;; Leave erc-insert-this set to t as much as possible.  Fran
               ;; Litterio <franl> has seen erc-insert-this set to nil while
@@ -2955,10 +3028,17 @@ erc-display-line-1
                   (run-hooks 'erc-insert-post-hook)
                   (when erc-remove-parsed-property
                     (remove-text-properties (point-min) (point-max)
-                                            '(erc-parsed nil))))
+                                            '(erc-parsed nil tags nil)))
+                  (cl-assert (> (- (point-max) (point-min)) 1))
+                  (let ((props (if erc--msg-props
+                                   (erc--order-text-properties-from-hash
+                                    erc--msg-props)
+                                 '(erc-msg unknown))))
+                    (add-text-properties (point-min) (1+ (point-min)) props)))
                 (erc--refresh-prompt)))))
         (run-hooks 'erc-insert-done-hook)
-        (erc-update-undo-list (- (or (marker-position erc-insert-marker)
+        (erc-update-undo-list (- (or (marker-position (or erc--insert-marker
+                                                          erc-insert-marker))
                                      (point-max))
                                  insert-position))))))
 
@@ -3102,6 +3182,21 @@ erc--hide-message
           (cl-incf beg))
         (erc--merge-prop (1- beg) (1- end) 'invisible value)))))
 
+(defvar erc--ranked-properties '(erc-msg erc-ts erc-cmd))
+
+(defun erc--order-text-properties-from-hash (table)
+  "Return a plist of text props from items in table.
+Ensure props in `erc--ranked-properties' appear last and in
+reverse order so that they end up sorted in buffer interval
+plists for retrieval by `text-properties-at' and friends."
+  (let (out)
+    (dolist (k erc--ranked-properties)
+      (when-let ((v (gethash k table)))
+        (remhash k table)
+        (setq out (nconc (list k v) out))))
+    (maphash (lambda (k v) (setq out (nconc (list k v) out))) table)
+    out))
+
 (defun erc-display-message-highlight (type string)
   "Highlight STRING according to TYPE, where erc-TYPE-face is an ERC face.
 
@@ -3332,6 +3427,21 @@ erc-display-message
   (let ((string (if (symbolp msg)
                     (apply #'erc-format-message msg args)
                   msg))
+        (erc--msg-props
+         (or erc--msg-props
+             (let* ((table (make-hash-table :size 5))
+                    (cmd (and parsed (erc--get-eq-comparable-cmd
+                                      (erc-response.command parsed))))
+                    (m (cond ((and msg (symbolp msg)) msg)
+                             ((and cmd (memq cmd '(PRIVMSG NOTICE)) 'msg))
+                             (t 'unknown))))
+               (puthash 'erc-msg m table)
+               (when cmd
+                 (puthash 'erc-cmd cmd table))
+               (and erc--msg-prop-overrides
+                    (pcase-dolist (`(,k . ,v) erc--msg-prop-overrides)
+                      (puthash k v table)))
+               table)))
         (erc-message-parsed parsed))
     (setq string
           (cond
@@ -3350,9 +3460,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)
-        (put-text-property
-         0 (length string) 'erc-command
-         (erc--get-eq-comparable-cmd (erc-response.command parsed)) string)
 	(when (erc-response.tags parsed)
 	  (erc-put-text-property 0 (length string) 'tags (erc-response.tags parsed)
 				 string))
@@ -5303,7 +5410,7 @@ erc--get-speaker-bounds
 Assume buffer is narrowed to the confines of an inserted message."
   (inline-quote
    (and-let*
-       (((memq (get-text-property (point) 'erc-command) '(PRIVMSG NOTICE)))
+       (((erc--check-msg-prop 'erc-msg 'msg))
         (beg (or (and (get-text-property (point-min) 'erc-speaker) (point-min))
                  (next-single-property-change (point-min) 'erc-speaker))))
      (cons beg (next-single-property-change beg 'erc-speaker)))))
@@ -5628,11 +5735,8 @@ erc-process-ctcp-query
         (while queries
           (let* ((type (upcase (car (split-string (car queries)))))
                  (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))
-                 (erc-insert-pre-hook
-                  (cons (lambda (s)
-                          (put-text-property 0 (1- (length s)) 'erc-ctcp
-                                             (intern type) s))
-                        erc-insert-pre-hook)))
+                 (erc--msg-prop-overrides `((erc-msg . msg)
+                                            (erc-ctcp . ,(intern type)))))
             (if (and hook (boundp hook))
                 (if (string-equal type "ACTION")
                     (run-hook-with-args-until-success
@@ -6637,7 +6741,8 @@ erc-send-current-line
             (when-let (((not (erc--input-split-abortp state)))
                        (inhibit-read-only t)
                        (old-buf (current-buffer)))
-              (progn ; unprogn this during next major surgery
+              (let ((erc--msg-prop-overrides '((erc-cmd . PRIVMSG)
+                                               (erc-msg . msg))))
                 (erc-set-active-buffer (current-buffer))
                 ;; Kill the input and the prompt
                 (delete-region erc-input-marker (erc-end-of-input-line))
@@ -6784,17 +6889,24 @@ erc-display-msg
     (save-excursion
       (erc--assert-input-bounds)
       (let ((insert-position (marker-position (goto-char erc-insert-marker)))
+            (erc--msg-props (or erc--msg-props
+                                (map-into (cons '(erc-msg . self)
+                                                erc--msg-prop-overrides)
+                                          'hash-table)))
             beg)
         (insert (erc-format-my-nick))
         (setq beg (point))
         (insert line)
         (erc-put-text-property beg (point) 'font-lock-face 'erc-input-face)
-        (erc-put-text-property insert-position (point) 'erc-command 'PRIVMSG)
         (insert "\n")
         (save-restriction
           (narrow-to-region insert-position (point))
           (run-hooks 'erc-send-modify-hook)
-          (run-hooks 'erc-send-post-hook))
+          (run-hooks 'erc-send-post-hook)
+          (cl-assert (> (- (point-max) (point-min)) 1))
+          (add-text-properties (point-min) (1+ (point-min))
+                               (erc--order-text-properties-from-hash
+                                erc--msg-props)))
         (erc--refresh-prompt)))))
 
 (defun erc-command-symbol (command)
@@ -8181,21 +8293,13 @@ erc-find-parsed-property
   "Find the next occurrence of the `erc-parsed' text property."
   (text-property-not-all (point-min) (point-max) 'erc-parsed nil))
 
-(defvar erc--persistent-message-properties '(erc-command))
-
 (defun erc-restore-text-properties ()
-  "Ensure the `erc-parsed' property covers the narrowed buffer.
-Do this for other properties added by `erc-display-message' and
-for those named in `erc--persistent-message-properties'."
+  "Ensure the `erc-parsed' and `tags' props cover the entire message."
   (when-let ((parsed-posn (erc-find-parsed-property))
-             (found (erc-get-parsed-vector parsed-posn)))
+              (found (erc-get-parsed-vector parsed-posn)))
     (put-text-property (point-min) (point-max) 'erc-parsed found)
     (when-let ((tags (get-text-property parsed-posn 'tags)))
-      (put-text-property (point-min) (point-max) 'tags tags))
-    (let ((to (max (point-min) (1- (point-max)))))
-      (dolist (prop erc--persistent-message-properties)
-        (when-let ((val (get-text-property parsed-posn prop)))
-          (put-text-property (point-min) to prop val))))))
+      (put-text-property (point-min) (point-max) 'tags tags))))
 
 (defun erc-get-parsed-vector (point)
   "Return the whole parsed vector on POINT."
diff --git a/test/lisp/erc/erc-fill-tests.el b/test/lisp/erc/erc-fill-tests.el
index b81d0c15558..f6c4c268017 100644
--- a/test/lisp/erc/erc-fill-tests.el
+++ b/test/lisp/erc/erc-fill-tests.el
@@ -31,10 +31,14 @@ erc-fill-tests--time-vals
 
 (defun erc-fill-tests--insert-privmsg (speaker &rest msg-parts)
   (declare (indent 1))
-  (let ((msg (erc-format-privmessage speaker
-                                     (apply #'concat msg-parts) nil t)))
-    (put-text-property 0 (length msg) 'erc-command 'PRIVMSG msg)
-    (erc-display-message nil nil (current-buffer) msg)))
+  (let* ((msg (erc-format-privmessage speaker
+                                      (apply #'concat msg-parts) nil t))
+         ;; (erc--msg-prop-overrides '((erc-msg . msg) (erc-cmd . PRIVMSG)))
+         (parsed (make-erc-response :unparsed msg :sender speaker
+                                    :command "PRIVMSG"
+                                    :command-args (list "#chan" msg)
+                                    :contents msg)))
+    (erc-display-message parsed nil (current-buffer) msg)))
 
 (defun erc-fill-tests--wrap-populate (test)
   (let ((original-window-buffer (window-buffer (selected-window)))
@@ -75,8 +79,8 @@ erc-fill-tests--wrap-populate
 
           (erc-fill-tests--insert-privmsg "alice"
             "bob: come, you are a tedious fool: to the purpose. "
-            "What was done to Elbow's wife, that he hath cause to complain of? "
-            "Come me to what was done to her.")
+            "What was done to Elbow's wife, that he hath cause to complain of?"
+            " Come me to what was done to her.")
 
           ;; Introduce an artificial gap in properties `line-prefix' and
           ;; `wrap-prefix' and later ensure they're not incremented twice.
@@ -111,6 +115,14 @@ erc-fill-tests--wrap-check-prefixes
       (should (get-text-property (pos-bol) 'line-prefix))
       (should (get-text-property (1- (pos-eol)) 'line-prefix))
       (should-not (get-text-property (pos-eol) 'line-prefix))
+      ;; Spans entire line uninterrupted.
+      (let* ((val (get-text-property (pos-bol) 'line-prefix))
+             (end (text-property-not-all (pos-bol) (point-max)
+                                         'line-prefix val)))
+        (when (and (/= end (pos-eol)) (= ?? (char-before end)))
+          (setq end (text-property-not-all (1+ end) (point-max)
+                                           'line-prefix val)))
+        (should (eq end (pos-eol))))
       (should (equal (get-text-property (pos-bol) 'wrap-prefix)
                      '(space :width erc-fill--wrap-value)))
       (should-not (get-text-property (pos-eol) 'wrap-prefix))
@@ -145,7 +157,7 @@ erc-fill-tests--compare
                                (number-to-string erc-fill--wrap-value)
                                (prin1-to-string got))))
     (with-current-buffer (generate-new-buffer name)
-      (push name erc-fill-tests--buffers)
+      (push (current-buffer) erc-fill-tests--buffers)
       (with-silent-modifications
         (insert (setq got (read repr))))
       (erc-mode))
@@ -153,15 +165,31 @@ erc-fill-tests--compare
         (with-temp-file expect-file
           (insert repr))
       (if (file-exists-p expect-file)
-          ;; Compare set-equal over intervals.  This comparison is
-          ;; less useful for messages treated by other modules because
-          ;; it doesn't compare "nested" props belonging to
-          ;; string-valued properties, like timestamps.
-          (should (equal-including-properties
-                   (read repr)
-                   (read (with-temp-buffer
-                           (insert-file-contents-literally expect-file)
-                           (buffer-string)))))
+          ;; Ensure string-valued properties, like timestamps, aren't
+          ;; recursive (signals `max-lisp-eval-depth' exceeded).
+          (named-let assert-equal
+              ((latest (read repr))
+               (expect (read (with-temp-buffer
+                               (insert-file-contents-literally expect-file)
+                               (buffer-string)))))
+            (pcase latest
+              ((or "" 'nil) t)
+              ((pred stringp)
+               (should (equal-including-properties latest expect))
+               (let ((latest-intervals (object-intervals latest))
+                     (expect-intervals (object-intervals expect)))
+                 (while-let ((l-iv (pop latest-intervals))
+                             (x-iv (pop expect-intervals))
+                             (l-tab (map-into (nth 2 l-iv) 'hash-table))
+                             (x-tab (map-into (nth 2 x-iv) 'hash-table)))
+                   (pcase-dolist (`(,l-k . ,l-v) (map-pairs l-tab))
+                     (assert-equal l-v (gethash l-k x-tab))
+                     (remhash l-k x-tab))
+                   (should (zerop (hash-table-count x-tab))))))
+              ((pred sequencep)
+               (assert-equal (seq-first latest) (seq-first expect))
+               (assert-equal (seq-rest latest) (seq-rest expect)))
+              (_ (should (equal latest expect)))))
         (message "Snapshot file missing: %S" expect-file)))))
 
 ;; To inspect variable pitch, set `erc-mode-hook' to
@@ -206,6 +234,13 @@ erc-fill-wrap--monospace
        (erc-fill-tests--wrap-check-prefixes "*** " "<alice> " "<bob> ")
        (erc-fill-tests--compare "monospace-04-reset")))))
 
+(defun erc-fill-tests--simulate-refill ()
+  ;; Simulate `erc-fill-wrap-refill-buffer' synchronously and without
+  ;; a progress reporter.
+  (save-excursion
+    (with-silent-modifications
+      (erc-fill--wrap-rejigger-region (point-min) erc-insert-marker nil nil))))
+
 (ert-deftest erc-fill-wrap--merge ()
   :tags '(:unstable)
   (unless (>= emacs-major-version 29)
@@ -217,7 +252,9 @@ erc-fill-wrap--merge
      (erc-update-channel-member
       "#chan" "Dummy" "Dummy" t nil nil nil nil nil "fake" "~u" nil nil t)
 
-     ;; Set this here so that the first few messages are from 1970
+     ;; Set this here so that the first few messages are from 1970.
+     ;; Following the current date stamp, the speaker isn't merged
+     ;; even though it's continued: "<bob> zero."
      (let ((erc-fill-tests--time-vals (lambda () 1680332400)))
        (erc-fill-tests--insert-privmsg "bob" "zero.")
        (erc-fill-tests--insert-privmsg "alice" "one.")
@@ -239,7 +276,12 @@ erc-fill-wrap--merge
        (erc-fill-tests--wrap-check-prefixes
         "*** " "<alice> " "<bob> "
         "<bob> " "<alice> " "<alice> " "<bob> " "<bob> " "<Dummy> " "<Dummy> ")
-       (erc-fill-tests--compare "merge-02-right")))))
+       (erc-fill-tests--compare "merge-02-right")
+
+       (ert-info ("Command `erc-fill-wrap-refill-buffer' is idempotent")
+         (kill-buffer (pop erc-fill-tests--buffers))
+         (erc-fill-tests--simulate-refill) ; idempotent
+         (erc-fill-tests--compare "merge-02-right"))))))
 
 (ert-deftest erc-fill-wrap--merge-action ()
   :tags '(:unstable)
diff --git a/test/lisp/erc/erc-scenarios-match.el b/test/lisp/erc/erc-scenarios-match.el
index bc06d58c3e9..864f3881ab1 100644
--- a/test/lisp/erc/erc-scenarios-match.el
+++ b/test/lisp/erc/erc-scenarios-match.el
@@ -55,7 +55,8 @@ erc-scenarios-match--stamp-left-current-nick
                                 :nick "tester")
         ;; Module `timestamp' follows `match' in insertion hooks.
         (should (memq 'erc-add-timestamp
-                      (memq 'erc-match-message erc-insert-modify-hook)))
+                      (memq 'erc-match-message
+                            (default-value 'erc-insert-modify-hook))))
         ;; The "match type" is `current-nick'.
         (funcall expect 5 "tester")
         (should (eq (get-text-property (1- (point)) 'font-lock-face)
@@ -91,7 +92,8 @@ erc-scenarios-match--invisible-stamp
                                 :nick "tester")
         ;; Module `timestamp' follows `match' in insertion hooks.
         (should (memq 'erc-add-timestamp
-                      (memq 'erc-match-message erc-insert-modify-hook)))
+                      (memq 'erc-match-message
+                            (default-value 'erc-insert-modify-hook))))
         (funcall expect 5 "This server is in debug mode")))
 
     (ert-info ("Ensure lines featuring \"bob\" are invisible")
@@ -151,28 +153,13 @@ erc-scenarios-match--stamp-left-fools-invisible
           (= (next-single-property-change msg-beg 'invisible nil (pos-eol))
              (pos-eol))))))))
 
-(defun erc-scenarios-match--find-bol ()
-  (save-excursion
-    (should (get-text-property (1- (point)) 'erc-command))
-    (goto-char (should (previous-single-property-change (point) 'erc-command)))
-    (pos-bol)))
-
-(defun erc-scenarios-match--find-eol ()
-  (save-excursion
-    (if-let ((next (next-single-property-change (point) 'erc-command)))
-        (goto-char next)
-      ;; We're already at the end of the message.
-      (should (get-text-property (1- (point)) 'erc-command)))
-    (pos-eol)))
-
 ;; In most cases, `erc-hide-fools' makes line endings invisible.
 (defun erc-scenarios-match--stamp-right-fools-invisible ()
   (let ((erc-insert-timestamp-function #'erc-insert-timestamp-right))
     (erc-scenarios-match--invisible-stamp
 
      (lambda ()
-       (let ((beg (erc-scenarios-match--find-bol))
-             (end (erc-scenarios-match--find-eol)))
+       (pcase-let ((`(,beg . ,end) (erc--get-inserted-msg-bounds)))
          ;; The end of the message is a newline.
          (should (= ?\n (char-after end)))
 
@@ -204,7 +191,7 @@ erc-scenarios-match--stamp-right-fools-invisible
            (should (= (next-single-property-change msg-end 'invisible) end)))))
 
      (lambda ()
-       (let ((end (erc-scenarios-match--find-eol)))
+       (let ((end (cdr (erc--get-inserted-msg-bounds))))
          ;; This message has a time stamp like all the others.
          (should (eq (field-at-pos (1- end)) 'erc-timestamp))
 
@@ -279,7 +266,8 @@ erc-scenarios-match--fill-wrap-stamp-dedented-p
 
 (ert-deftest erc-scenarios-match--stamp-both-invisible-fill-wrap ()
 
-  ;; Rewind the clock to known date artificially.
+  ;; Rewind the clock to known date artificially.  We should probably
+  ;; use a ticks/hz cons on 29+.
   (let ((erc-stamp--current-time 704591940)
         (erc-stamp--tz t)
         (erc-fill-function #'erc-fill-wrap)
@@ -305,29 +293,22 @@ erc-scenarios-match--stamp-both-invisible-fill-wrap
        (ert-info ("Line endings in Bob's messages are invisible")
          ;; The message proper has the `invisible' property `match-fools'.
          (should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
-         (let* ((mbeg (or (and (get-text-property (pos-bol) 'erc-command)
-                               (pos-bol))
-                          (next-single-property-change (pos-bol)
-                                                       'erc-command)))
-                (mend (text-property-not-all
-                       mbeg (point-max) 'erc-command
-                       (get-text-property mbeg 'erc-command))))
-
-           (if (/= 1 bob-utterance-counter)
-               (should-not (field-at-pos mend))
-             ;; For Bob's stamped message, check newline after stamp.
-             (should (eq (field-at-pos mend) 'erc-timestamp))
-             (setq mend (field-end mend)))
+         (pcase-let ((`(,mbeg . ,mend) (erc--get-inserted-msg-bounds)))
+           (should (= (char-after mend) ?\n))
+           (should-not (field-at-pos mend))
+           (should-not (field-at-pos mbeg))
+
+           (when (= bob-utterance-counter 1)
+             (let ((right-stamp (field-end mbeg)))
+               (should (eq 'erc-timestamp (field-at-pos right-stamp)))
+               (should (= mend (field-end right-stamp)))
+               (should (eq (field-at-pos (1- mend)) 'erc-timestamp))))
 
-           ;; The `erc-timestamp' property spans entire messages,
-           ;; including stamps and filled text, which makes for
-           ;; convenient traversal when `erc-stamp-mode' is enabled.
-           (should (get-text-property (pos-bol) 'erc-timestamp))
-           (should (= (next-single-property-change (pos-bol) 'erc-timestamp)
-                      mend))
+           ;; The `erc-ts' property is present in prop stack.
+           (should (get-text-property (pos-bol) 'erc-ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
 
            ;; Line ending has the `invisible' property `match-fools'.
-           (should (= (char-after mend) ?\n))
            (should (eq (get-text-property mbeg 'invisible) 'match-fools))
            (should-not (get-text-property mend 'invisible))))
 
@@ -410,22 +391,20 @@ erc-scenarios-match--stamp-both-invisible-fill-static
        (ert-info ("Line endings in Bob's messages are invisible")
          ;; The message proper has the `invisible' property `match-fools'.
          (should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
-         (let* ((mbeg (and (get-text-property (pos-bol) 'erc-command)
-                           (pos-bol)))
-                (mend (next-single-property-change mbeg 'erc-command)))
+         (pcase-let ((`(,mbeg . ,mend) (erc--get-inserted-msg-bounds)))
 
-           (if (/= 1 bob-utterance-counter)
-               (should-not (field-at-pos mend))
+           (should (= (char-after mend) ?\n))
+           (should-not (field-at-pos mbeg))
+           (should-not (field-at-pos mend))
+           (when (= 1 bob-utterance-counter)
              ;; For Bob's stamped message, check newline after stamp.
-             (should (eq (field-at-pos mend) 'erc-timestamp))
-             (setq mend (field-end mend)))
+             (should (eq (field-at-pos (field-end mbeg)) 'erc-timestamp))
+             (should (eq (field-at-pos (1- mend)) 'erc-timestamp)))
 
-           ;; The `erc-timestamp' property spans entire messages,
-           ;; including stamps and filled text, which makes for
-           ;; convenient traversal when `erc-stamp-mode' is enabled.
-           (should (get-text-property (pos-bol) 'erc-timestamp))
-           (should (= (next-single-property-change (pos-bol) 'erc-timestamp)
-                      mend))
+           ;; The `erc-ts' property is present in the message's
+           ;; width 1 prop collection at its first char.
+           (should (get-text-property (pos-bol) 'erc-ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
 
            ;; Line ending has the `invisible' property `match-fools'.
            (should (= (char-after mend) ?\n))
@@ -510,9 +489,12 @@ erc-scenarios-match--stamp-both-invisible-fill-static--nooffset
                       (field-beginning (point))))
            (should (equal 'timestamp
                           (get-text-property (1- (point)) 'invisible)))
+           ;; Field stops before final newline because the date stamp
+           ;; is (now, as of ERC 5.6) its own standalone message.
+           (should (= ?\n (char-after (field-end (point)))))
            ;; Stamp-only invisibility includes last newline.
            (should (= (text-property-not-all (1- (point)) (point-max)
                                              'invisible 'timestamp)
-                      (field-end (point))))))))))
+                      (1+ (field-end (point)))))))))))
 
 ;;; erc-scenarios-match.el ends here
diff --git a/test/lisp/erc/erc-stamp-tests.el b/test/lisp/erc/erc-stamp-tests.el
index 46a05729066..cc61d599387 100644
--- a/test/lisp/erc/erc-stamp-tests.el
+++ b/test/lisp/erc/erc-stamp-tests.el
@@ -279,7 +279,7 @@ erc-echo-timestamp
 
   (should-not erc-echo-timestamps)
   (should-not erc-stamp--last-stamp)
-  (insert (propertize "abc" 'erc-timestamp 433483200))
+  (insert (propertize "a" 'erc-ts 433483200 'erc-msg 'msg) "bc")
   (goto-char (point-min))
   (let ((inhibit-message t)
         (erc-echo-timestamp-format "%Y-%m-%d %H:%M:%S %Z")
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index bd2d656e8da..408cc4db10c 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -292,6 +292,8 @@ erc--refresh-prompt
                                (cl-incf counter))))
          erc-accidental-paste-threshold-seconds
          erc-insert-modify-hook
+         (erc-modules (remq 'stamp erc-modules))
+         (erc-send-input-line-function #'ignore)
          (erc--input-review-functions erc--input-review-functions)
          erc-send-completed-hook)
 
@@ -356,7 +358,8 @@ erc--refresh-prompt
         (should (looking-back "#chan@ServNet 11> "))
         (should (= (point) erc-input-marker))
         (insert "/query bob")
-        (erc-send-current-line)
+        (let (erc-modules)
+          (erc-send-current-line))
         ;; Last command not inserted
         (save-excursion (forward-line -1)
                         (should (looking-at "<tester> Howdy")))
@@ -796,18 +799,15 @@ erc--valid-local-channel-p
       (should (erc--valid-local-channel-p "&local")))))
 
 (ert-deftest erc--restore-initialize-priors ()
-  ;; This `pcase' expands to 100+k.  Guess we could do something like
-  ;; (and `(,_ ((,e . ,_) . ,_) . ,_) v) first and then return a
-  ;; (equal `(if-let* ((,e ...)...)...) v) to cut it down to < 1k.
   (should (pcase (macroexpand-1 '(erc--restore-initialize-priors erc-my-mode
                                    foo (ignore 1 2 3)
-                                   bar #'spam))
-            (`(if-let* ((,e (or erc--server-reconnecting erc--target-priors))
-                        ((alist-get 'erc-my-mode ,e)))
-                  (setq foo (alist-get 'foo ,e)
-                        bar (alist-get 'bar ,e))
-                (setq foo (ignore 1 2 3)
-                      bar #'spam))
+                                   bar #'spam
+                                   baz nil))
+            (`(let* ((,p (or erc--server-reconnecting erc--target-priors))
+                     (,q (and ,p (alist-get 'erc-my-mode ,p))))
+                (setq foo (if ,q (alist-get 'foo ,p) (ignore 1 2 3))
+                      bar (if ,q (alist-get 'bar ,p) #'spam)
+                      baz (if ,q (alist-get 'baz ,p) nil)))
              t))))
 
 (ert-deftest erc--target-from-string ()
@@ -1434,6 +1434,44 @@ erc-process-input-line
 
           (should-not calls))))))
 
+(ert-deftest erc--order-text-properties-from-hash ()
+  (let ((table (map-into '((a . 1)
+                           (erc-ts . 0)
+                           (erc-msg . s005)
+                           (b . 2)
+                           (erc-cmd . 5)
+                           (c . 3))
+                         'hash-table)))
+    (with-temp-buffer
+      (erc-mode)
+      (insert "abc\n")
+      (add-text-properties 1 2 (erc--order-text-properties-from-hash table))
+      (should (equal '( erc-msg s005
+                        erc-ts 0
+                        erc-cmd 5
+                        a 1
+                        b 2
+                        c 3)
+                     (text-properties-at (point-min)))))))
+
+(ert-deftest erc--check-msg-prop ()
+  (let ((erc--msg-props (map-into '((a . 1) (b . x)) 'hash-table)))
+    (should (eq 1 (erc--check-msg-prop 'a)))
+    (should (erc--check-msg-prop 'a 1))
+    (should-not (erc--check-msg-prop 'a 2))
+
+    (should (eq 'x (erc--check-msg-prop 'b)))
+    (should (erc--check-msg-prop 'b 'x))
+    (should-not (erc--check-msg-prop 'b 1))
+
+    (should (erc--check-msg-prop 'a '(1 42)))
+    (should-not (erc--check-msg-prop 'a '(2 42)))
+
+    (let ((props '(42 x)))
+      (should (erc--check-msg-prop 'b props)))
+    (let ((v '(42 y)))
+      (should-not (erc--check-msg-prop 'b v)))))
+
 (defmacro erc-tests--equal-including-properties (a b)
   (list (if (< emacs-major-version 29)
             'ert-equal-including-properties
diff --git a/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld b/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
index c62a22a11c7..4c2b1d61e24 100644
--- a/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
+++ b/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
@@ -1,7 +1,7 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((pass 10 "PASS :changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
  (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
@@ -18,16 +18,16 @@
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
  (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
 
-((mode-user 8 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.barnet.org 221 tester +i")
  (0 ":irc.barnet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((join 2 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
  (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
 
-((mode 2 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
  (0 ":irc.barnet.org 329 tester #chan 1620104779")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
diff --git a/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld b/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
index f30b7deca11..bfa324642ce 100644
--- a/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
+++ b/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
@@ -1,7 +1,7 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((pass 10 "PASS :changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
  (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
@@ -18,16 +18,16 @@
  (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
  (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
 
-((mode-user 8 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.foonet.org 221 tester +i")
  (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((join 2 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
  (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
 
-((mode 2 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
  (0 ":irc.foonet.org 329 tester #chan 1620104779")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
diff --git a/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld b/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
index 686a47f68a3..04959954c4f 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
@@ -22,14 +22,14 @@
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
 
-((join 1 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
  (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
  (0 ":[email-masked] PRIVMSG #chan :tester, welcome!"))
 
-((mode 1 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
  (0 ":irc.barnet.org 329 tester #chan 1620805269")
  (0.1 ":[email-masked] PRIVMSG #chan :joe: But you have outfaced them all.")
diff --git a/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld b/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
index b99621cc311..7b9b3bdee6c 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
@@ -22,14 +22,14 @@
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
 
-((join 1 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
  (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
  (0 ":[email-masked] PRIVMSG #chan :tester, welcome!"))
 
-((mode 1 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
  (0 ":irc.foonet.org 329 tester #chan 1620805271")
  (0.1 ":[email-masked] PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index 689bacc7012..238d8cc73c2 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 27 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 27 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 474 475 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 27 (8))) erc-command PRIVMSG) 475 480 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 480 486 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 487 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 27 0)) display #11="" erc-command PRIVMSG) 488 493 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 493 495 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 495 499 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 500 501 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 27 (6))) erc-command PRIVMSG) 501 504 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 504 512 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 513 514 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13=(space :width (- 27 0)) display #11# erc-command PRIVMSG) 514 517 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 517 519 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 519 524 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# erc-command PRIVMSG) 525 526 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14=(space :width (- 27 (8))) erc-command PRIVMSG) 526 531 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 531 538 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 539 540 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15=(space :width (- 27 0)) display #11# erc-command PRIVMSG) 540 545 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 545 547 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 547 551 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index 9fa23a7d332..d1ce9198e69 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 29 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 29) line-prefix #3=(space :width (- 29 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 29 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 29 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 29 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 29 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 474 475 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 29 (8))) erc-command PRIVMSG) 475 480 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 480 486 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 487 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 29 0)) display #11="" erc-command PRIVMSG) 488 493 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 493 495 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 495 499 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 500 501 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 29 (6))) erc-command PRIVMSG) 501 504 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 504 512 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 513 514 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13=(space :width (- 29 0)) display #11# erc-command PRIVMSG) 514 517 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 517 519 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 519 524 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# erc-command PRIVMSG) 525 526 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14=(space :width (- 29 (8))) erc-command PRIVMSG) 526 531 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 531 538 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 539 540 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15=(space :width (- 29 0)) display #11# erc-command PRIVMSG) 540 545 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 545 547 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 547 551 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 29 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 29 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 29 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 29 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 29 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index a3d533c87b5..d70184724ba 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n* bob one\n<bob> two.\n* bob three\n<bob> four.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 27 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 27 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# invisible timestamp font-lock-face erc-timestamp-face)))) 474 476 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 27 (6))) erc-ctcp ACTION erc-command PRIVMSG) 476 479 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-ctcp ACTION erc-command PRIVMSG) 479 483 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-ctcp ACTION erc-command PRIVMSG) 484 485 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 27 (6))) erc-command PRIVMSG) 485 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 488 494 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 495 497 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11=(space :width (- 27 (2))) erc-ctcp ACTION erc-command PRIVMSG) 497 500 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11# erc-ctcp ACTION erc-command PRIVMSG) 500 506 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11# erc-ctcp ACTION erc-command PRIVMSG) 507 508 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 27 (6))) erc-command PRIVMSG) 508 511 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 511 518 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n* bob one\n<bob> two.\n* bob three\n<bob> four.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 475 476 (wrap-prefix #1# line-prefix #7#) 476 479 (wrap-prefix #1# line-prefix #7#) 479 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 485 488 (wrap-prefix #1# line-prefix #8# display #9#) 488 490 (wrap-prefix #1# line-prefix #8# display #9#) 490 494 (wrap-prefix #1# line-prefix #8#) 495 496 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #10=(space :width (- 27 (2)))) 496 497 (wrap-prefix #1# line-prefix #10#) 497 500 (wrap-prefix #1# line-prefix #10#) 500 506 (wrap-prefix #1# line-prefix #10#) 507 508 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 508 511 (wrap-prefix #1# line-prefix #11# display #9#) 511 513 (wrap-prefix #1# line-prefix #11# display #9#) 513 518 (wrap-prefix #1# line-prefix #11#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 80c9e1d80f5..def97738ce6 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index e675695f660..be3e2b33cfd 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 29 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 29) line-prefix #3=(space :width (- 29 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 29 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 29 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index a6070c2e3ff..098257d0b49 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 25 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 25) line-prefix #3=(space :width (- 25 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 25 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 25 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 80c9e1d80f5..def97738ce6 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 2b8766c27f4..360b3dafafd 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 190 191 (line-spacing 0.5) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 348 349 (line-spacing 0.5) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 435 436 (line-spacing 0.5) 436 437 (erc-timestamp 0 wrap-prefix #2# line-prefix #6=(space :width (- 27 0)) display #7="" erc-command PRIVMSG) 437 440 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# display #7# erc-command PRIVMSG) 440 442 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# display #7# erc-command PRIVMSG) 442 466 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 467 (line-spacing 0.5) 467 484 (erc-timestamp 0 wrap-prefix #2# line-prefix (space :width (- 27 (4)))) 485 502 (erc-timestamp 0 wrap-prefix #2# line-prefix (space :width (- 27 (4)))) 502 503 (line-spacing 0.5) 503 504 (erc-timestamp 0 wrap-prefix #2# line-prefix #8=(space :width (- 27 (6))) erc-command PRIVMSG) 504 507 (erc-timestamp 0 wrap-prefix #2# line-prefix #8# erc-command PRIVMSG) 507 525 (erc-timestamp 0 wrap-prefix #2# line-prefix #8# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index f62b65cd170..cd3537d3c94 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 9 (erc-timestamp 0 display (#4=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 9 171 (erc-timestamp 0 wrap-prefix #1# line-prefix #2#) 172 179 (erc-timestamp 0 display (#4# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 179 180 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 180 185 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 185 187 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 187 190 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 190 303 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 303 304 (erc-timestamp 0 erc-command PRIVMSG) 304 336 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 337 344 (erc-timestamp 0 display (#4# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 344 345 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 345 348 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 348 350 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 350 355 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 355 430 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg unknown erc-ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
\ No newline at end of file
-- 
2.41.0

>From 69aa1ebcac9044efc78c922dcb7805144cc237a7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Mon, 2 Oct 2023 22:59:22 -0700
Subject: [PATCH 1/7] [5.6] Allow spoofing process marker in erc-display-line-1

* lisp/erc/erc.el (erc--insert-marker): New internal variable for
overriding `erc-insert-marker' when displaying messages at a
non-default location in the buffer.
(erc-display-line-1): Favor `erc--insert-marker' over
`erc-insert-marker' when non-nil.
* test/lisp/erc/resources/base/assoc/multi-net/barnet.eld: Timeouts.
* test/lisp/erc/resources/base/assoc/multi-net/foonet.eld: Timeouts.
* test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld: Timeouts.
* test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld: Timeouts.
---
 lisp/erc/erc.el                                      |  7 ++++++-
 .../erc/resources/base/assoc/multi-net/barnet.eld    | 12 ++++++------
 .../erc/resources/base/assoc/multi-net/foonet.eld    | 12 ++++++------
 .../erc/resources/base/netid/bouncer/barnet-drop.eld |  4 ++--
 .../erc/resources/base/netid/bouncer/foonet-drop.eld |  4 ++--
 5 files changed, 22 insertions(+), 17 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index fb236f1f189..b78f8bc6210 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2909,6 +2909,8 @@ erc--refresh-prompt
         (delete-region (point) (1- erc-input-marker))))
     (run-hooks 'erc--refresh-prompt-hook)))
 
+(defvar erc--insert-marker nil)
+
 (defun erc-display-line-1 (string buffer)
   "Display STRING in `erc-mode' BUFFER.
 Auxiliary function used in `erc-display-line'.  The line gets filtered to
@@ -2932,6 +2934,8 @@ erc-display-line-1
                            (format "%s" buffer)))
           (setq erc-insert-this t)
           (run-hook-with-args 'erc-insert-pre-hook string)
+          (setq insert-position (marker-position (or erc--insert-marker
+                                                     erc-insert-marker)))
           (if (null erc-insert-this)
               ;; Leave erc-insert-this set to t as much as possible.  Fran
               ;; Litterio <franl> has seen erc-insert-this set to nil while
@@ -2954,7 +2958,8 @@ erc-display-line-1
                                             '(erc-parsed nil))))
                 (erc--refresh-prompt)))))
         (run-hooks 'erc-insert-done-hook)
-        (erc-update-undo-list (- (or (marker-position erc-insert-marker)
+        (erc-update-undo-list (- (or (marker-position (or erc--insert-marker
+                                                          erc-insert-marker))
                                      (point-max))
                                  insert-position))))))
 
diff --git a/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld b/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
index c62a22a11c7..4c2b1d61e24 100644
--- a/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
+++ b/test/lisp/erc/resources/base/assoc/multi-net/barnet.eld
@@ -1,7 +1,7 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((pass 10 "PASS :changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
  (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
@@ -18,16 +18,16 @@
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
  (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
 
-((mode-user 8 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.barnet.org 221 tester +i")
  (0 ":irc.barnet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((join 2 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
  (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
 
-((mode 2 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
  (0 ":irc.barnet.org 329 tester #chan 1620104779")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
diff --git a/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld b/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
index f30b7deca11..bfa324642ce 100644
--- a/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
+++ b/test/lisp/erc/resources/base/assoc/multi-net/foonet.eld
@@ -1,7 +1,7 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((pass 10 "PASS :changeme"))
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
  (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
@@ -18,16 +18,16 @@
  (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
  (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
 
-((mode-user 8 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.foonet.org 221 tester +i")
  (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((join 2 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
  (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
 
-((mode 2 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
  (0 ":irc.foonet.org 329 tester #chan 1620104779")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
diff --git a/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld b/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
index 686a47f68a3..04959954c4f 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/barnet-drop.eld
@@ -22,14 +22,14 @@
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
 
-((join 1 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
  (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
  (0 ":[email-masked] PRIVMSG #chan :tester, welcome!"))
 
-((mode 1 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
  (0 ":irc.barnet.org 329 tester #chan 1620805269")
  (0.1 ":[email-masked] PRIVMSG #chan :joe: But you have outfaced them all.")
diff --git a/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld b/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
index b99621cc311..7b9b3bdee6c 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/foonet-drop.eld
@@ -22,14 +22,14 @@
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
 
-((join 1 "JOIN #chan")
+((join 10 "JOIN #chan")
  (0 ":[email-masked] JOIN #chan")
  (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
  (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
  (0.1 ":[email-masked] PRIVMSG #chan :tester, welcome!")
  (0 ":[email-masked] PRIVMSG #chan :tester, welcome!"))
 
-((mode 1 "MODE #chan")
+((mode 10 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
  (0 ":irc.foonet.org 329 tester #chan 1620805271")
  (0.1 ":[email-masked] PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
-- 
2.41.0

>From ffcc811bdc69f089059ff907c4a265c406c965fc Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 5 Oct 2023 00:16:46 -0700
Subject: [PATCH 2/7] [5.6] Honor nil values in erc--restore-initialize-priors

* lisp/erc/erc.el (erc--restore-initialize-priors): Don't produce
invalid empty `setq' when given VARS that initialize to nil.
* test/lisp/erc/erc-tests.el (erc--restore-initialize-priors): Fix
expected expansion.
---
 lisp/erc/erc.el            | 17 ++++++++---------
 test/lisp/erc/erc-tests.el | 17 +++++++----------
 2 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index b78f8bc6210..a3ba1548084 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1366,16 +1366,15 @@ erc--target-priors
 (defmacro erc--restore-initialize-priors (mode &rest vars)
   "Restore local VARS for MODE from a previous session."
   (declare (indent 1))
-  (let ((existing (make-symbol "existing"))
+  (let ((priors (make-symbol "priors"))
+        (initp (make-symbol "initp"))
         ;;
-        restore initialize)
-    (while-let ((k (pop vars)) (v (pop vars)))
-      (push `(,k (alist-get ',k ,existing)) restore)
-      (push `(,k ,v) initialize))
-    `(if-let* ((,existing (or erc--server-reconnecting erc--target-priors))
-               ((alist-get ',mode ,existing)))
-         (setq ,@(mapcan #'identity (nreverse restore)))
-       (setq ,@(mapcan #'identity (nreverse initialize))))))
+        forms)
+    (while-let ((k (pop vars)))
+      (push `(,k (if ,initp (alist-get ',k ,priors) ,(pop vars))) forms))
+    `(let* ((,priors (or erc--server-reconnecting erc--target-priors))
+            (,initp (and ,priors (alist-get ',mode ,priors))))
+       (setq ,@(mapcan #'identity (nreverse forms))))))
 
 (defun erc--target-from-string (string)
   "Construct an `erc--target' variant from STRING."
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 8a68eca6196..64b503832f3 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -796,18 +796,15 @@ erc--valid-local-channel-p
       (should (erc--valid-local-channel-p "&local")))))
 
 (ert-deftest erc--restore-initialize-priors ()
-  ;; This `pcase' expands to 100+k.  Guess we could do something like
-  ;; (and `(,_ ((,e . ,_) . ,_) . ,_) v) first and then return a
-  ;; (equal `(if-let* ((,e ...)...)...) v) to cut it down to < 1k.
   (should (pcase (macroexpand-1 '(erc--restore-initialize-priors erc-my-mode
                                    foo (ignore 1 2 3)
-                                   bar #'spam))
-            (`(if-let* ((,e (or erc--server-reconnecting erc--target-priors))
-                        ((alist-get 'erc-my-mode ,e)))
-                  (setq foo (alist-get 'foo ,e)
-                        bar (alist-get 'bar ,e))
-                (setq foo (ignore 1 2 3)
-                      bar #'spam))
+                                   bar #'spam
+                                   baz nil))
+            (`(let* ((,p (or erc--server-reconnecting erc--target-priors))
+                     (,q (and ,p (alist-get 'erc-my-mode ,p))))
+                (setq foo (if ,q (alist-get 'foo ,p) (ignore 1 2 3))
+                      bar (if ,q (alist-get 'bar ,p) #'spam)
+                      baz (if ,q (alist-get 'baz ,p) nil)))
              t))))
 
 (ert-deftest erc--target-from-string ()
-- 
2.41.0

>From 62c6585251a1d0a604499f103f87884a1b33de3b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Wed, 4 Oct 2023 20:39:03 -0700
Subject: [PATCH 3/7] [5.6] Preserve format-spec args in erc-server-JOIN

* lisp/erc/erc-backend.el (erc-server-JOIN): Let `erc-display-message'
handle formatting instead of baking out a string.  The text ultimately
inserted remains unchanged, but forwarding the original `format-spec'
arguments now has the side effect of influencing text properties, which
conveys richer meaning for modules to act upon when doing things like
deciding whether to hide a message.
---
 lisp/erc/erc-backend.el | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index fb10ee31c78..bc42917375a 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1718,7 +1718,7 @@ erc--server-determine-join-display-context
       (if (string-match "^\\(.*\\)\^g.*$" chnl)
           (setq chnl (match-string 1 chnl)))
       (save-excursion
-        (let* ((str (cond
+        (let ((args (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
                       (let ((erc--display-context
@@ -1735,18 +1735,15 @@ erc--server-determine-join-display-context
                         (erc-channel-begin-receiving-names))
                       (erc-update-mode-line)
                       (run-hooks 'erc-join-hook)
-                      (erc-make-notice
-                       (erc-format-message 'JOIN-you ?c chnl)))
+                      (list 'JOIN-you ?c chnl))
                      (t
                       (setq buffer (erc-get-buffer chnl proc))
-                      (erc-make-notice
-                       (erc-format-message
-                        'JOIN ?n nick ?u login ?h host ?c chnl))))))
+                      (list 'JOIN ?n nick ?u login ?h host ?c chnl)))))
           (when buffer (set-buffer buffer))
           (erc-update-channel-member chnl nick nick t nil nil nil nil nil host login)
           ;; on join, we want to stay in the new channel buffer
           ;;(set-buffer ob)
-          (erc-display-message parsed nil buffer str))))))
+          (apply #'erc-display-message parsed 'notice buffer args))))))
 
 (define-erc-response-handler (KICK)
   "Handle kick messages received from the server." nil
-- 
2.41.0

>From 866a2681dacc4307d9f6b177dbab5beccc740f4c Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Tue, 3 Oct 2023 00:00:19 -0700
Subject: [PATCH 4/7] [5.6] Deprecate option erc-remove-parsed-property

* etc/ERC-NEWS: Add entry for `erc-remove-parsed-property'.
* lisp/erc/erc.el (erc-remove-parsed-property): Deprecate option
because the potential for inadvertent self harm outweighs the
potential benefits.  Additionally, replicating this functionality via
hooks is trivial.
(erc-display-line-1): Remove quasi-deprecated `tags' property.
---
 etc/ERC-NEWS    |  8 ++++++++
 lisp/erc/erc.el | 13 +++++++++++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index fadd97b65df..284b91bb41f 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -221,6 +221,14 @@ atop any message.  The new companion option 'erc-echo-timestamp-zone'
 determines the default timezone when not specified with a prefix
 argument.
 
+** Option 'erc-remove-parsed-property' deprecated.
+This option's nil behavior serves no practical purpose yet has the
+potential to degrade the user experience by competing for space with
+forthcoming features powered by next generation extensions.  Anyone
+with a legitimate use for this option likely also possesses the
+knowledge to rig up a suitable analog with minimal effort.  That said,
+the road to removal is long.
+
 ** Option 'erc-warn-about-blank-lines' is more informative.
 Enabled by default, this option now produces more useful feedback
 whenever ERC rejects prompt input containing whitespace-only lines.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a3ba1548084..aedec60321b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2876,9 +2876,18 @@ erc-remove-parsed-property
 
 The default is to remove it, since it causes ERC to take up extra
 memory.  If you have code that relies on this property, then set
-this option to nil."
+this option to nil.
+
+Note that this option is deprecated because a value of nil is
+impractical in prolonged sessions with more than a few channels.
+Use `erc-insert-post-hook' or similar and the helper function
+`erc-find-parsed-property' and friends to stash the current
+`erc-response' object as needed.  And instead of using this for
+debugging purposes, try `erc-debug-irc-protocol'."
   :type 'boolean
   :group 'erc)
+(make-obsolete-variable 'erc-remove-parsed-property
+                        "impractical when non-nil" "30.1")
 
 (define-inline erc--assert-input-bounds ()
   (inline-quote
@@ -2954,7 +2963,7 @@ erc-display-line-1
                   (run-hooks 'erc-insert-post-hook)
                   (when erc-remove-parsed-property
                     (remove-text-properties (point-min) (point-max)
-                                            '(erc-parsed nil))))
+                                            '(erc-parsed nil tags nil))))
                 (erc--refresh-prompt)))))
         (run-hooks 'erc-insert-done-hook)
         (erc-update-undo-list (- (or (marker-position (or erc--insert-marker
-- 
2.41.0

>From a9638d22c67ffed2fd25f4ecf10f0d3a2aac5ea9 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Tue, 3 Oct 2023 23:15:40 -0700
Subject: [PATCH 5/7] [5.6] Add helper for removing list-valued text props in
 ERC

* lisp/erc/erc.el (erc--remove-from-prop-value-list): New function for
removing `invisible' and `face' prop members cleanly.
* test/lisp/erc/erc-tests.el (erc--remove-from-prop-value-list,
erc--remove-from-prop-value-list/many): New tests.  (Bug#60936)
---
 lisp/erc/erc.el            |  24 ++++++
 test/lisp/erc/erc-tests.el | 169 +++++++++++++++++++++++++++++++++++++
 2 files changed, 193 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index aedec60321b..f3c480f918b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3061,6 +3061,30 @@ erc--merge-prop
             old (get-text-property pos prop object)
             end (next-single-property-change pos prop object to)))))
 
+(defun erc--remove-from-prop-value-list (from to prop val &optional object)
+  "Remove VAL from text prop value between FROM and TO.
+If current value is VAL itself, remove the property entirely.
+When VAL is a list, act as if this function were called
+repeatedly with VAL set to each of VAL's members."
+  (let ((old (get-text-property from prop object))
+        (pos from)
+        (end (next-single-property-change from prop object to))
+        new)
+    (while (< pos to)
+      (when old
+        (if (setq new (and (consp old) (if (consp val)
+                                           (seq-difference old val)
+                                         (remq val old))))
+            (put-text-property pos end prop
+                               (if (cdr new) new (car new)) object)
+          (when (pcase val
+                  ((pred consp) (or (consp old) (memq old val)))
+                  (_ (if (consp old) (memq val old) (eq old val))))
+            (remove-text-properties pos end (list prop nil) object))))
+      (setq pos end
+            old (get-text-property pos prop object)
+            end (next-single-property-change pos prop object to)))))
+
 (defvar erc-legacy-invisible-bounds-p nil
   "Whether to hide trailing rather than preceding newlines.
 Beginning in ERC 5.6, invisibility extends from a message's
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 64b503832f3..11717217eb2 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1475,6 +1475,175 @@ erc--merge-prop
     (when noninteractive
       (kill-buffer))))
 
+(ert-deftest erc--remove-from-prop-value-list ()
+  (with-current-buffer (get-buffer-create "*erc-test*")
+    ;; Non-list match.
+    (insert "abc\n")
+    (put-text-property 1 2 'erc-test 'a)
+    (put-text-property 2 3 'erc-test 'b)
+    (put-text-property 3 4 'erc-test 'c)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc"
+                                      0 1 (erc-test a)
+                                      1 2 (erc-test b)
+                                      2 3 (erc-test c))))
+
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'b)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc"
+                                      0 1 (erc-test a)
+                                      2 3 (erc-test c))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'a)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc" 2 3 (erc-test c))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'c)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) "abc"))
+
+    ;; List match.
+    (goto-char (point-min))
+    (insert "def\n")
+    (put-text-property 1 2 'erc-test '(d x))
+    (put-text-property 2 3 'erc-test '(e y))
+    (put-text-property 3 4 'erc-test '(f z))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("def"
+                                      0 1 (erc-test (d x))
+                                      1 2 (erc-test (e y))
+                                      2 3 (erc-test (f z)))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'y)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("def"
+                                      0 1 (erc-test (d x))
+                                      1 2 (erc-test e)
+                                      2 3 (erc-test (f z)))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'd)
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'f)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("def"
+                                      0 1 (erc-test x)
+                                      1 2 (erc-test e)
+                                      2 3 (erc-test z))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'e)
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'z)
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'x)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) "def"))
+
+    ;; List match.
+    (goto-char (point-min))
+    (insert "ghi\n")
+    (put-text-property 1 2 'erc-test '(g x))
+    (put-text-property 2 3 'erc-test '(h x))
+    (put-text-property 3 4 'erc-test '(i y))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("ghi"
+                                      0 1 (erc-test (g x))
+                                      1 2 (erc-test (h x))
+                                      2 3 (erc-test (i y)))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'x)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("ghi"
+                                      0 1 (erc-test g)
+                                      1 2 (erc-test h)
+                                      2 3 (erc-test (i y)))))
+    (erc--remove-from-prop-value-list 1 2 'erc-test 'g) ; narrowed
+    (erc--remove-from-prop-value-list 3 4 'erc-test 'i) ; narrowed
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("ghi"
+                                      1 2 (erc-test h)
+                                      2 3 (erc-test y))))
+
+    ;; Pathological (,c) case (hopefully not created by ERC)
+    (goto-char (point-min))
+    (insert "jkl\n")
+    (put-text-property 1 2 'erc-test '(j x))
+    (put-text-property 2 3 'erc-test '(k))
+    (put-text-property 3 4 'erc-test '(k))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'k)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("jkl" 0 1 (erc-test (j x)))))
+
+    (when noninteractive
+      (kill-buffer))))
+
+(ert-deftest erc--remove-from-prop-value-list/many ()
+  (with-current-buffer (get-buffer-create "*erc-test*")
+    ;; Non-list match.
+    (insert "abc\n")
+    (put-text-property 1 2 'erc-test 'a)
+    (put-text-property 2 3 'erc-test 'b)
+    (put-text-property 3 4 'erc-test 'c)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc"
+                                      0 1 (erc-test a)
+                                      1 2 (erc-test b)
+                                      2 3 (erc-test c))))
+
+    (erc--remove-from-prop-value-list 1 4 'erc-test '(a b))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc" 2 3 (erc-test c))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test 'a)
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("abc" 2 3 (erc-test c))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test '(c))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) "abc"))
+
+    ;; List match.
+    (goto-char (point-min))
+    (insert "def\n")
+    (put-text-property 1 2 'erc-test '(d x y))
+    (put-text-property 2 3 'erc-test '(e y))
+    (put-text-property 3 4 'erc-test '(f z))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("def"
+                                      0 1 (erc-test (d x y))
+                                      1 2 (erc-test (e y))
+                                      2 3 (erc-test (f z)))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test '(d y f))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("def"
+                                      0 1 (erc-test x)
+                                      1 2 (erc-test e)
+                                      2 3 (erc-test z))))
+    (erc--remove-from-prop-value-list 1 4 'erc-test '(e z x))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) "def"))
+
+    ;; Narrowed beg.
+    (goto-char (point-min))
+    (insert "ghi\n")
+    (put-text-property 1 2 'erc-test '(g x))
+    (put-text-property 2 3 'erc-test '(h x))
+    (put-text-property 3 4 'erc-test '(i x))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("ghi"
+                                      0 1 (erc-test (g x))
+                                      1 2 (erc-test (h x))
+                                      2 3 (erc-test (i x)))))
+    (erc--remove-from-prop-value-list 1 3 'erc-test '(x g i))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("ghi"
+                                      1 2 (erc-test h)
+                                      2 3 (erc-test (i x)))))
+
+    ;; Narrowed middle.
+    (goto-char (point-min))
+    (insert "jkl\n")
+    (put-text-property 1 2 'erc-test '(j x))
+    (put-text-property 2 3 'erc-test '(k))
+    (put-text-property 3 4 'erc-test '(l y z))
+    (erc--remove-from-prop-value-list 3 4 'erc-test '(k x y z))
+    (should (erc-tests--equal-including-properties
+             (buffer-substring 1 4) #("jkl"
+                                      0 1 (erc-test (j x))
+                                      1 2 (erc-test (k))
+                                      2 3 (erc-test l))))
+
+    (when noninteractive
+      (kill-buffer))))
+
 (ert-deftest erc--split-string-shell-cmd ()
 
   ;; Leading and trailing space
-- 
2.41.0

>From ef4974d8e232b0d5e5df31a30f2fd904f970c60f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 21 Sep 2023 23:54:31 -0700
Subject: [PATCH 6/7] [5.6] Manage meta-data text props for ERC hook members

* etc/ERC-NEWS: Mention that `cursor-sensor-functions' is only added
when `erc-echo-timestamps' is enabled, and mention that date stamps
are now inserted as separate messages.

* lisp/erc/erc-fill.el (erc-fill): Look for `erc-cmd' instead of
`erc-command' text prop.
(erc-fill-static): Skip date stamps.
(erc-fill-wrap-mode, erc-fill-wrap-enable, erc-fill-wrap-disable):
Don't use removed hook `erc-stamp--insert-date-function' because date
stamps are now separate messages.
(erc-fill--wrap-continued-message-p): Restore accidentally excised doc
string.  Derive context about current message from text props at
`point-min', and use updated names and utility functions.
(erc-fill--wrap-stamp-insert-prefixed-date): Remove function,
originally meant to be new in ERC 5.6, and move logic for date-stamp
measuring directly to `erc-fill-wrap' itself.
(erc-fill--wrap-measure): New helper function.
(erc-fill-wrap): Use helper `erc-fill--wrap-measure' and incorporate
date-stamp detection and width measuring from removed helper.

* lisp/erc/erc-goodies.el (erc-readonly-mode, erc-readonly-enable):
Set hook depth to an explicit 70.

* lisp/erc/erc-stamp.el (erc-timestamp-format-left): Mention that a
trailing newline is implicit if not provided and that users who don't
want date stamps should use `erc-timestamp-format-right' instead.
(erc-stamp-mode, erc-stamp-enable): Call `erc-stamp--setup' instead
of `erc-munge-invisibility-spec', and bump hook depth for
`erc-add-timestamp' to 79.
(erc-stamp--skip): New internal variable.
(erc-stamp--allow-unmanaged): New variable for legacy code to force
`erc-add-timestamps' to run when `erc--msg-props' is nil.
(erc-add-timestamp): Gate on new flags `erc-stamp--skip' and
`erc-stamp--allow-unmanaged'.  Don't add `erc-ts' text prop directly.
Instead, use `erc--msg-props' facility to defer until after
modification hooks.  Don't add `cursor-senor-functions' directly
either unless compatibility flag is enabled.  Instead, expect this to
be handled by a post-modify hook.
(erc-stamp-prefix-log-filter): Use updated name for timestamp
property.
(erc-stamp--inherited-props): Add doc string.
(erc-insert-timestamp-right): Fix bug involving object cycle where
the time-stamp string would appear in its own `display' property.
(erc-stamp--insert-date-function, erc-stamp--insert-date-hook): Remove
unused internal function-valued interface variable and replace with
the latter, a normal hook.
(erc-stamp--date-format-end, erc-stamp--propertize-left-date-stamp):
New function and auxiliary variable to apply date stamp properties at
the post-modify stage.  Add text property `erc-stamp-type' to inserted
date stamps to help folks distinguish between them and other
left-sided stamps.
(erc-stamp-date-left-p): New public function for third-party code to
detect whether a message is a date stamp.
(erc-stamp--current-datestamp-left,
erc-stamp--insert-date-stamp-as-phony-message,
erc-stamp--lr-date-on-pre-modify): New functions and state variable to
help ERC treat date stamps as separate messages while working within
the established mechanism for processing inserted messages.  Shadow
`erc-stamp--invisible-property' when calling `erc-format-timestamp' in
order to prevent date stamps from inheriting other `invisible' props.
These date stamps are special in that they have no business being
hidden along with the current message.
(erc-insert-timestamp-left-and-right): On initial run in any buffer,
record whether date stamp needs massaging on insertion.  Move all
business for inserting date stamps to post-modify hooks, but run them
forcibly if this is the very first date stamp in the current buffer.
Also mention intervals of relevant text props in doc string.
(erc-format-timestamp): Don't add `invisible' prop to stamp unless
`erc-stamp--invisible-property' is non-nil.
(erc-stamp--csf-props-updated-p): New local variable.
(erc-munge-invisibility-spec): Restore `cursor-sensor-functions' text
property for existing messages when a user enables the option
mid-session.  Add and remove hooks for use with automatic timestamp
echoing.
(erc-stamp--add-csf-on-post-modify): New function to add
`cursor-sensor-functions' property on post-modify hooks.
(erc-stamp--setup): Perform some additional teardown.
(erc-stamp--on-clear-message): Update timestamp text-property name to
`erc-ts'.
(erc-echo-timestamp, erc--echo-ts-csf): Use utility to find time-stamp
text prop in current message.
(erc-stamp--update-saved-position, erc-stamp--reset-on-clear): Use
hook `erc-stamp--insert-date-hook' instead of excised variable
`erc-stamp--insert-date-function'.

* lisp/erc/erc-truncate.el (erc-truncate-buffer-to-size): Use internal
utility to find beginning of message.

* lisp/erc/erc.el (erc--msg-props, erc--msg-props-overrides): New
internal variables for initializing and conveying message meta-data
text properties among insert and send hooks.
(erc-insert-modify-hook): Mention reserved depth ranges for built-in
members in doc string.
(erc-send-action):  Use convenience variable to modifying text props
instead of overriding `erc-insert-pre-hook'.
(erc--check-msg-prop, erc--get-inserted-msg-bounds,
erc--get-inserted-msg-prop, erc--with-inserted-msg,
erc--traverse-inserted): New utility functions and macros to help
modules find meta-data and message-delimiting text props.
(erc-display-line-1): Ensure the first character of every message in
an ERC buffer has the `erc-msg' property.
(erc--hide-message): Don't bother offsetting start of first message in
a buffer.
(erc--ranked-properties, erc--order-text-properties-from-hash): New
variable and function to convert `erc--msg-props' into a plist
suitable for `add-text-properties'.
(erc-display-message): Bind `erc--msg-props' for use by all hooks.
Respect `erc--msg-prop-overrides' when non-nil.  Don't add
`erc-command' property.
(erc--own-property-names): Add `erc-stamp-type'.
(erc--get-speaker-bounds): Use helper to find message start.
(erc-process-ctcp-query, erc-send-current-line): Use convenience
variable to leverage framework for manipulating message meta-data
instead of overriding `erc-insert-pre-hook'.
(erc-display-msg): Bind `erc--msg-props' for use by all send-related
hooks.  Add text props from table after `erc-send-post-hook'.
(erc-restore-text-properties): Improve doc string.
(erc--get-eq-comparable-cmd): Use `if-let' instead of `if-let*'.

* test/lisp/erc/erc-fill-tests.el (erc-fill-tests--insert-privmsg):
Make fake message more realistic.
(erc-fill-tests--wrap-populate): Shorten overlong line.
(erc-fill-tests--wrap-check-prefixes): Make test utility more vigilant
in asserting no gaps exist in `line-prefix' property interval.
(erc-fill-tests--compare): Compare text props on text prop values that
are themselves strings.

* test/lisp/erc/erc-scenarios-log.el (erc-scenarios-log--clear-stamp):
Ensure `erc-stamp' is loaded.

* test/lisp/erc/erc-scenarios-match.el
(erc-scenarios-match--stamp-left-current-nick,
erc-scenarios-match--invisible-stamp): Use `default-value' for
`erc-insert-modify-hook' in ordering assertion.
(erc-scenarios-match--find-bol, erc-scenarios-match--find-eol): Remove
unused assertion helper functions.
(erc-scenarios-match--stamp-right-fools-invisible): Remove misplaced
ERT tag from function and use utility to find message bounds.
(erc-scenarios-match--stamp-right-fools-invisible): Use utility to
find message end.
(erc-scenarios-match--fill-wrap-stamp-dedented-p): New assertion
utility function.
(erc-scenarios-match--stamp-both-invisible-fill-wrap) New test.
(erc-scenarios-match--stamp-both-invisible-fill-static): Expect
`erc-cmd' at beginning of inserted message's filled line, even if it
starts with whitespace.  Also, add new function parameter `assert-ds',
a callback to run when visiting the second date stamp, which is
followed by a hidden message.  In the test of the same name, expect
the date stamp's invisibility interval to begin at the newline after
the previous message and to not contain any existing invisibility
props, namely, those belonging to the subsequent hidden "fools"
message.  Also use unified meta-data text prop names.
(erc-scenarios-match--stamp-both-invisible-fill-static--nooffset):
Expect the date stamp's invisibility interval to match its field's
instead of starting and ending sooner.

* test/lisp/erc/erc-stamp-tests.el: Put well-known meta-data prop at
the start of the message.

* test/lisp/erc/erc-tests.el (erc--refresh-prompt): Prevent modules
from mutating hooks.
(erc--order-text-properties-from-hash, erc--check-msg-props): New
tests.

* test/lisp/erc/resources/fill/snapshots/merge-01-start.eld: Update
test data.
* test/lisp/erc/resources/fill/snapshots/merge-02-right.eld: Update
test data.
* test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld: Update.
* test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld:
Update.
* test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld:
Update.
* test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld:
Update.
* test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld:
Update.
* test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld: Update.
* test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld: Update.
(Bug#60936)
---
 etc/ERC-NEWS                                  |  28 ++-
 lisp/erc/erc-fill.el                          |  97 ++++---
 lisp/erc/erc-goodies.el                       |   4 +-
 lisp/erc/erc-stamp.el                         | 237 ++++++++++++++----
 lisp/erc/erc-truncate.el                      |   2 +-
 lisp/erc/erc.el                               | 164 ++++++++++--
 test/lisp/erc/erc-fill-tests.el               |  60 +++--
 test/lisp/erc/erc-scenarios-log.el            |   1 +
 test/lisp/erc/erc-scenarios-match.el          | 205 ++++++++++++---
 test/lisp/erc/erc-stamp-tests.el              |   2 +-
 test/lisp/erc/erc-tests.el                    |  43 +++-
 .../fill/snapshots/merge-01-start.eld         |   2 +-
 .../fill/snapshots/merge-02-right.eld         |   2 +-
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../fill/snapshots/monospace-01-start.eld     |   2 +-
 .../fill/snapshots/monospace-02-right.eld     |   2 +-
 .../fill/snapshots/monospace-03-left.eld      |   2 +-
 .../fill/snapshots/monospace-04-reset.eld     |   2 +-
 .../fill/snapshots/spacing-01-mono.eld        |   2 +-
 .../fill/snapshots/stamps-left-01.eld         |   2 +-
 20 files changed, 654 insertions(+), 207 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 284b91bb41f..81c94467f25 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -149,7 +149,7 @@ minor-mode maps, and new third-party modules should do the same.
 
 ** Option 'erc-timestamp-format-right' deprecated.
 Having to account for this option prevented other ERC modules from
-easily determining what right-hand stamps would look like before
+easily determining what right-sided stamps would look like before
 insertion, which is knowledge needed for certain UI decisions.  The
 way ERC has chosen to address this is imperfect and boils down to
 asking users who've customized this option to switch to
@@ -291,11 +291,13 @@ continue to modify non-ERC hooks locally whenever possible, especially
 in new code.
 
 *** ERC now manages timestamp-related properties a bit differently.
-For starters, the 'cursor-sensor-functions' property no longer
+For starters, the 'cursor-sensor-functions' text property is absent by
+default unless the option 'erc-echo-timestamps' is already enabled on
+module init.  And when present, the property's value no longer
 contains unique closures and thus no longer proves effective for
-traversing messages.  To compensate, a new property, 'erc-timestamp',
-now spans message bodies but not the newlines delimiting them.  Also
-affecting the 'stamp' module is the deprecation of the function
+traversing inserted messages.  For now, ERC only provides an internal
+means of visiting messages, but a public interface is forthcoming.
+Also affecting the 'stamp' module is the deprecation of the function
 'erc-insert-aligned' and its removal from client code.  Additionally,
 the module now merges its 'invisible' property with existing ones and
 includes all white space around stamps when doing so.
@@ -310,6 +312,22 @@ folded onto the next line.  Such inconsistency made stamp detection
 overly complex and produced uneven results when toggling stamp
 visibility.
 
+*** Date stamps are independent messages.
+ERC now inserts "date stamps" generated from the option
+'erc-timestamp-format-left' as separate, standalone messages.  (This
+only matters if 'erc-insert-timestamp-function' is set to its default
+value of 'erc-insert-timestamp-left-and-right'.)  ERC's near-term UI
+goals require exposing these stamps to existing code designed to
+operate on complete messages.  For example, users likely expect date
+stamps to be togglable with 'erc-toggle-timestamps' while also being
+immune to hiding from commands like 'erc-match-toggle-hidden-fools'.
+Before this change, meeting such expectations demanded brittle
+heuristics that checked for the presence of these stamps in the
+leading portion of message bodies as well as special casing to act on
+these areas without inflicting collateral damage.  From now on, third
+parties can instead use the function 'erc-stamp-date-left-p' to detect
+and reuse existing code to operate.
+
 *** The role of a module's Custom group is now more clearly defined.
 Associating built-in modules with Custom groups and provided library
 features has improved.  More specifically, a module's group now enjoys
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 0e6b5a3efb8..62a9597d481 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -158,6 +158,11 @@ erc-fill
     (when (or erc-fill--function erc-fill-function)
       ;; skip initial empty lines
       (goto-char (point-min))
+      ;; Note the following search pattern was altered in 5.6 to adapt
+      ;; to a change in Emacs regexp behavior that turned out to be a
+      ;; regression (which has since been fixed).  The patterns appear
+      ;; to be equivalent in practice, so this was left as is (wasn't
+      ;; reverted) to avoid additional git-blame(1)-related churn.
       (while (and (looking-at (rx bol (* (in " \t")) eol))
                   (zerop (forward-line 1))))
       (unless (eobp)
@@ -167,12 +172,10 @@ erc-fill
           (when-let* ((erc-fill-line-spacing)
                       (p (point-min)))
             (widen)
-            (when (or (and-let* ((cmd (get-text-property p 'erc-command)))
-                        (memq cmd erc-fill--spaced-commands))
+            (when (or (erc--check-msg-prop 'erc-cmd erc-fill--spaced-commands)
                       (and-let* ((cmd (save-excursion
                                         (forward-line -1)
-                                        (get-text-property (point)
-                                                           'erc-command))))
+                                        (get-text-property (point) 'erc-cmd))))
                         (memq cmd erc-fill--spaced-commands)))
               (put-text-property (1- p) p
                                  'line-spacing erc-fill-line-spacing))))))))
@@ -181,15 +184,17 @@ erc-fill-static
   "Fills a text such that messages start at column `erc-fill-static-center'."
   (save-restriction
     (goto-char (point-min))
-    (looking-at "^\\(\\S-+\\)")
-    (let ((nick (match-string 1)))
+    (when-let (((looking-at "^\\(\\S-+\\)"))
+               ((not (erc--check-msg-prop 'erc-msg 'datestamp)))
+               (nick (match-string 1)))
+      (progn
         (let ((fill-column (- erc-fill-column (erc-timestamp-offset)))
               (fill-prefix (make-string erc-fill-static-center 32)))
           (insert (make-string (max 0 (- erc-fill-static-center
                                          (length nick) 1))
                                32))
           (erc-fill-regarding-timestamp))
-        (erc-restore-text-properties))))
+        (erc-restore-text-properties)))))
 
 (defun erc-fill-variable ()
   "Fill from `point-min' to `point-max'."
@@ -423,8 +428,6 @@ fill-wrap
              (eq (default-value 'erc-insert-timestamp-function)
                  #'erc-insert-timestamp-left)))
    (setq erc-fill--function #'erc-fill-wrap)
-   (add-function :after (local 'erc-stamp--insert-date-function)
-                 #'erc-fill--wrap-stamp-insert-prefixed-date)
    (when erc-fill-wrap-merge
      (add-hook 'erc-button--prev-next-predicate-functions
                #'erc-fill--wrap-merged-button-p nil t))
@@ -436,9 +439,7 @@ fill-wrap
    (kill-local-variable 'erc-fill--function)
    (kill-local-variable 'erc-fill--wrap-visual-keys)
    (remove-hook 'erc-button--prev-next-predicate-functions
-                #'erc-fill--wrap-merged-button-p t)
-   (remove-function (local 'erc-stamp--insert-date-function)
-                    #'erc-fill--wrap-stamp-insert-prefixed-date))
+                #'erc-fill--wrap-merged-button-p t))
   'local)
 
 (defvar-local erc-fill--wrap-length-function nil
@@ -456,6 +457,9 @@ erc-fill--wrap-last-msg
 (defvar-local erc-fill--wrap-max-lull (* 24 60 60))
 
 (defun erc-fill--wrap-continued-message-p ()
+  "Return non-nil when the current speaker hasn't changed.
+That is, indicate whether the text just inserted is from the same
+sender as that of the previous \"PRIVMSG\"."
   (prog1 (and-let*
              ((m (or erc-fill--wrap-last-msg
                      (setq erc-fill--wrap-last-msg (point-min-marker))
@@ -463,14 +467,11 @@ erc-fill--wrap-continued-message-p
               ((< (1+ (point-min)) (- (point) 2)))
               (props (save-restriction
                        (widen)
-                       (when (eq 'erc-timestamp (field-at-pos m))
-                         (set-marker m (field-end m)))
                        (and-let*
-                           (((eq 'PRIVMSG (get-text-property m 'erc-command)))
-                            ((not (eq (get-text-property m 'erc-ctcp)
-                                      'ACTION)))
+                           (((eq 'PRIVMSG (get-text-property m 'erc-cmd)))
+                            ((not (eq (get-text-property m 'erc-msg) 'ACTION)))
                             (spr (next-single-property-change m 'erc-speaker)))
-                         (cons (get-text-property m 'erc-timestamp)
+                         (cons (get-text-property m 'erc-ts)
                                (get-text-property spr 'erc-speaker)))))
               (ts (pop props))
               (props)
@@ -478,30 +479,23 @@ erc-fill--wrap-continued-message-p
               ((time-less-p (time-subtract (erc-stamp--current-time) ts)
                             erc-fill--wrap-max-lull))
               (speaker (next-single-property-change (point-min) 'erc-speaker))
-              ((not (eq (get-text-property speaker 'erc-ctcp) 'ACTION)))
+              ((not (erc--check-msg-prop 'erc-ctcp 'ACTION)))
               (nick (get-text-property speaker 'erc-speaker))
               ((erc-nick-equal-p props nick))))
     (set-marker erc-fill--wrap-last-msg (point-min))))
 
-(defun erc-fill--wrap-stamp-insert-prefixed-date (&rest args)
-  "Apply `line-prefix' property to args."
-  (let* ((ts-left (car args))
-         (start)
-         ;; Insert " " to simulate gap between <speaker> and msg beg.
-         (end (save-excursion (skip-chars-backward "\n")
-                              (setq start (pos-bol))
-                              (insert " ")
-                              (point)))
-         (width (if (and erc-fill-wrap-use-pixels
-                         (fboundp 'buffer-text-pixel-size))
-                    (save-restriction (narrow-to-region start end)
-                                      (list (car (buffer-text-pixel-size))))
-                  (length (string-trim-left ts-left)))))
-    (delete-region (1- end) end)
-    ;; Use `point-min' instead of `start' to cover leading newilnes.
-    (put-text-property (point-min) (point) 'line-prefix
-                       `(space :width (- erc-fill--wrap-value ,width))))
-  args)
+(defun erc-fill--wrap-measure (beg end)
+  "Return display spec width for inserted region between BEG and END.
+Ignore any `invisible' props that may be present when figuring."
+  (if (and erc-fill-wrap-use-pixels (fboundp 'buffer-text-pixel-size))
+      ;; `buffer-text-pixel-size' can move point!
+      (save-excursion
+        (save-restriction
+          (narrow-to-region beg end)
+          (let* ((buffer-invisibility-spec)
+                 (rv (car (buffer-text-pixel-size))))
+            (if (zerop rv) 0 (list rv)))))
+    (- end beg)))
 
 ;; An escape hatch for third-party code expecting speakers of ACTION
 ;; messages to be exempt from `line-prefix'.  This could be converted
@@ -522,25 +516,28 @@ erc-fill-wrap
                      (when-let ((e (erc--get-speaker-bounds))
                                 (b (pop e))
                                 ((or erc-fill--wrap-action-dedent-p
-                                     (not (eq (get-text-property b 'erc-ctcp)
-                                              'ACTION)))))
+                                     (not (erc--check-msg-prop 'erc-ctcp
+                                                               'ACTION)))))
                        (goto-char e))
                      (skip-syntax-forward "^-")
                      (forward-char)
-                     ;; Using the `invisible' property might make more
-                     ;; sense, but that would require coordination
-                     ;; with other modules, like `erc-match'.
-                     (cond ((and erc-fill-wrap-merge
+                     (cond ((erc--check-msg-prop 'erc-msg 'datestamp)
+                            (when erc-fill--wrap-last-msg
+                              (set-marker erc-fill--wrap-last-msg (point-min)))
+                            (save-excursion
+                              (goto-char (point-max))
+                              (skip-chars-backward "\n")
+                              (let ((beg (pos-bol)))
+                                (insert " ")
+                                (prog1 (erc-fill--wrap-measure beg (point))
+                                  (delete-region (1- (point)) (point))))))
+                           ((and erc-fill-wrap-merge
                                  (erc-fill--wrap-continued-message-p))
                             (put-text-property (point-min) (point)
                                                'display "")
                             0)
-                           ((and erc-fill-wrap-use-pixels
-                                 (fboundp 'buffer-text-pixel-size))
-                            (save-restriction
-                              (narrow-to-region (point-min) (point))
-                              (list (car (buffer-text-pixel-size)))))
-                           (t (- (point) (point-min))))))))
+                           (t
+                            (erc-fill--wrap-measure (point-min) (point))))))))
       (erc-put-text-properties (point-min) (1- (point-max)) ; exclude "\n"
                                '(line-prefix wrap-prefix) nil
                                `((space :width (- erc-fill--wrap-value ,len))
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index b77176d8ac7..d112e63c316 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -339,8 +339,8 @@ erc-scroll-to-bottom
 ;;;###autoload(autoload 'erc-readonly-mode "erc-goodies" nil t)
 (define-erc-module readonly nil
   "This mode causes all inserted text to be read-only."
-  ((add-hook 'erc-insert-post-hook #'erc-make-read-only)
-   (add-hook 'erc-send-post-hook #'erc-make-read-only))
+  ((add-hook 'erc-insert-post-hook #'erc-make-read-only 70)
+   (add-hook 'erc-send-post-hook #'erc-make-read-only 70))
   ((remove-hook 'erc-insert-post-hook #'erc-make-read-only)
    (remove-hook 'erc-send-post-hook #'erc-make-read-only)))
 
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index 0f3163bf68d..7fc76eb2d73 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -55,21 +55,22 @@ erc-timestamp-format
   :type '(choice (const nil)
 		 (string)))
 
-;; FIXME remove surrounding whitespace from default value and have
-;; `erc-insert-timestamp-left-and-right' add it before insertion.
-
 (defcustom erc-timestamp-format-left "\n[%a %b %e %Y]\n"
-  "If set to a string, messages will be timestamped.
-This string is processed using `format-time-string'.
-Good examples are \"%T\" and \"%H:%M\".
-
-This timestamp is used for timestamps on the left side of the
-screen when `erc-insert-timestamp-function' is set to
-`erc-insert-timestamp-left-and-right'.
-
-If nil, timestamping is turned off."
-  :type '(choice (const nil)
-		 (string)))
+  "Format recognized by `format-time-string' for date stamps.
+Only considered when `erc-insert-timestamp-function' is set to
+`erc-insert-timestamp-left-and-right'.  Used for displaying date
+stamps on their own line, between messages.  ERC inserts this
+flavor of stamp as a separate \"psuedo message\", so a final
+newline isn't necessary.  For compatibility, only additional
+trailing newlines beyond the first become empty lines.  For
+example, the default value results in an empty line after the
+previous message, followed by the timestamp on its own line,
+followed immediately by the next message on the next line.  ERC
+expects to display these stamps less frequently, so the
+formatting specifiers should reflect that.  To omit these stamps
+entirely, use a different `erc-insert-timestamp-function', such
+as `erc-timestamp-format-right'."
+  :type 'string)
 
 (defcustom erc-timestamp-format-right nil
   "If set to a string, messages will be timestamped.
@@ -175,9 +176,9 @@ erc-timestamp-face
 ;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t)
 (define-erc-module stamp timestamp
   "This mode timestamps messages in the channel buffers."
-  ((add-hook 'erc-mode-hook #'erc-munge-invisibility-spec)
-   (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 60)
-   (add-hook 'erc-send-modify-hook #'erc-add-timestamp 60)
+  ((add-hook 'erc-mode-hook #'erc-stamp--setup)
+   (add-hook 'erc-insert-modify-hook #'erc-add-timestamp 79)
+   (add-hook 'erc-send-modify-hook #'erc-add-timestamp 79)
    (add-hook 'erc-mode-hook #'erc-stamp--recover-on-reconnect)
    (add-hook 'erc--pre-clear-functions #'erc-stamp--reset-on-clear)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-stamp--setup)))
@@ -214,18 +215,27 @@ erc-stamp--current-time
 
 (cl-defgeneric erc-stamp--current-time ()
   "Return a lisp time object to associate with an IRC message.
-This becomes the message's `erc-timestamp' text property."
+This becomes the message's `erc-ts' text property."
   (erc-compat--current-lisp-time))
 
 (cl-defmethod erc-stamp--current-time :around ()
   (or erc-stamp--current-time (cl-call-next-method)))
 
+(defvar erc-stamp--skip nil
+  "Non-nil means inhibit `erc-add-timestamp' completely.")
+
+(defvar erc-stamp--allow-unmanaged nil
+  "Non-nil means `erc-add-timestamp' runs unconditionally.
+Escape hatch for third-parties using lower-level API functions,
+such as `erc-display-line', directly.")
+
 (defun erc-add-timestamp ()
   "Add timestamp and text-properties to message.
 
 This function is meant to be called from `erc-insert-modify-hook'
 or `erc-send-modify-hook'."
-  (progn ; remove this `progn' on next major refactor
+  (unless (or erc-stamp--skip (and erc-stamp--allow-unmanaged
+                                   (not erc--msg-props)))
     (let* ((ct (erc-stamp--current-time))
            (invisible (get-text-property (point-min) 'invisible))
            (erc-stamp--invisible-property
@@ -233,6 +243,8 @@ erc-add-timestamp
             (if invisible `(timestamp ,@(ensure-list invisible)) 'timestamp))
            (skipp (and erc-stamp--skip-when-invisible invisible))
            (erc-stamp--current-time ct))
+      (when erc--msg-props
+        (puthash 'erc-ts ct erc--msg-props))
       (unless skipp
         (funcall erc-insert-timestamp-function
                  (erc-format-timestamp ct erc-timestamp-format)))
@@ -244,12 +256,13 @@ erc-add-timestamp
                  (erc-away-time))
 	(funcall erc-insert-away-timestamp-function
 		 (erc-format-timestamp ct erc-away-timestamp-format)))
-      (add-text-properties (point-min) (1- (point-max))
+      (when erc-stamp--allow-unmanaged
+        (add-text-properties (point-min) (1- (point-max))
 			   ;; It's important for the function to
 			   ;; be different on different entries (bug#22700).
 			   (list 'cursor-sensor-functions
                                  ;; Regions are no longer contiguous ^
-                                 '(erc--echo-ts-csf) 'erc-timestamp ct)))))
+                                 '(erc--echo-ts-csf) 'erc-ts ct))))))
 
 (defvar-local erc-timestamp-last-window-width nil
   "The width of the last window that showed the current buffer.
@@ -362,19 +375,27 @@ erc-stamp-prefix-log-filter
   (goto-char (point-min))
   (while
       (progn
-        (when-let* (((< (point) (pos-eol)))
-                    (end (1- (pos-eol)))
-                    ((eq 'erc-timestamp (field-at-pos end)))
-                    (beg (field-beginning end))
-                    ;; Skip a line that's just a timestamp.
-                    ((> beg (point))))
+        (when-let (((< (point) (pos-eol)))
+                   (end (1- (pos-eol)))
+                   ((eq 'erc-timestamp (field-at-pos end)))
+                   (beg (field-beginning end))
+                   ;; Skip a line that's just a timestamp.
+                   ((> beg (point))))
           (delete-region beg (1+ end)))
-        (when-let (time (get-text-property (point) 'erc-timestamp))
+        (when-let (time (erc--get-inserted-msg-prop 'erc-ts))
           (insert (format-time-string "[%H:%M:%S] " time)))
         (zerop (forward-line))))
   "")
 
-(defvar erc-stamp--inherited-props '(line-prefix wrap-prefix))
+;; These are currently extended manually, but we could also bind
+;; `text-property-default-nonsticky' and call `insert-and-inherit'
+;; instead of `insert', but we'd have to pair the props with differing
+;; boolean values for left and right stamps.  Also, since this hook
+;; runs last, we can't expect overriding sticky props to be absent,
+;; even though, as of 5.6, `front-sticky' is only added by the
+;; `readonly' module after hooks run.
+(defvar erc-stamp--inherited-props '(line-prefix wrap-prefix)
+  "Extant properties at the start of a message inherited by the stamp.")
 
 (declare-function erc--remove-text-properties "erc" (string))
 
@@ -573,8 +594,11 @@ erc-insert-timestamp-right
       ;; intervening white space unless a hard break is warranted.
       (pcase erc-timestamp-use-align-to
         ((guard erc-stamp--display-margin-mode)
-         (put-text-property 0 (length string)
-                            'display `((margin right-margin) ,string) string))
+         (let ((s (propertize (substring-no-properties string)
+                              'invisible erc-stamp--invisible-property)))
+           (put-text-property 0 (length string) 'display
+                              `((margin right-margin) ,s)
+                              string)))
         ((and 't (guard (< col pos)))
          (insert " ")
          (put-text-property from (point) 'display `(space :align-to ,pos)))
@@ -599,30 +623,109 @@ erc-insert-timestamp-right
       (when erc-timestamp-intangible
 	(erc-put-text-property from (1+ (point)) 'cursor-intangible t)))))
 
-(defvar erc-stamp--insert-date-function #'insert
-  "Function to insert left \"left-right date\" stamp.
-A local module might use this to modify text properties,
-`insert-before-markers' or renarrow the region after insertion.")
+(defvar erc-stamp--insert-date-hook nil
+  "Functions appended to send and modify hooks when inserting date stamp.")
+
+(defvar-local erc-stamp--date-format-end nil
+  "Substring index marking usable portion of date stamp format.")
+
+(defun erc-stamp--propertize-left-date-stamp ()
+  (add-text-properties (point-min) (1- (point-max))
+                       '(field erc-timestamp erc-stamp-type date-left))
+  (erc--hide-message 'timestamp))
+
+(defun erc-stamp-date-left-p (&optional point)
+  "Return non-nil if the current message is a \"date stamp\".
+Expect callers to know that such stamps originate from
+`erc-insert-timestamp-left-and-right' using the format string
+`erc-timestamp-format-left'.  Expect POINT, when non-nil, to
+reside at some known or suspected time stamp.  When POINT is nil,
+expect to be called from a member of `erc-insert-modify-hook' or
+similar."
+  (cond ((erc--check-msg-prop 'erc-msg 'datestamp))
+        (point (eq 'date-left (get-text-property point 'erc-stamp-type)))
+        (t (erc--with-inserted-msg
+            (and-let* ((p (text-property-not-all
+                           (point-min) (point-max) 'field 'erc-timestamp)))
+              (eq 'date-left (get-text-property p 'erc-stamp-type)))))))
+
+;; A kludge to pass state from insert hook to nested insert hook.
+(defvar erc-stamp--current-datestamp-left nil)
+
+;; Calling `erc-display-message' from within a hook it's currently
+;; running is roundabout, but it's a definite means of ensuring hooks
+;; can act on the date stamp as a standalone message to do things like
+;; adjust invisibility props.
+(defun erc-stamp--insert-date-stamp-as-phony-message (string)
+  (cl-assert (string-empty-p string))
+  (setq string erc-stamp--current-datestamp-left)
+  (cl-assert string)
+  (let ((erc-stamp--skip t)
+        (erc--msg-props (map-into `((erc-msg . datestamp)
+                                    (erc-ts . ,erc-stamp--current-time))
+                                  'hash-table))
+        (erc-send-modify-hook `(,@erc-send-modify-hook
+                                erc-stamp--propertize-left-date-stamp
+                                ,@erc-stamp--insert-date-hook))
+        (erc-insert-modify-hook `(,@erc-insert-modify-hook
+                                  erc-stamp--propertize-left-date-stamp
+                                  ,@erc-stamp--insert-date-hook)))
+    (erc-display-message nil nil (current-buffer) string)
+    (setq erc-timestamp-last-inserted-left string)))
+
+(defun erc-stamp--lr-date-on-pre-modify (_)
+  (unless erc-stamp--date-format-end
+    ;; Don't add text properties to the trailing newline.
+    (setq erc-stamp--date-format-end
+          (if (string-suffix-p "\n" erc-timestamp-format-left) -1 0)))
+  (when-let ((ct (or erc-stamp--current-time (erc-stamp--current-time)))
+             ;; Ignore existing prop value because date stamps should
+             ;; never be hideable except via `timestamp'.
+             (rendered (let (erc-stamp--invisible-property)
+                         (erc-format-timestamp
+                          ct (substring erc-timestamp-format-left
+                                        0 erc-stamp--date-format-end))))
+             ((not (string-equal rendered erc-timestamp-last-inserted-left)))
+             (erc-stamp--current-datestamp-left rendered)
+             (erc-insert-timestamp-function
+              #'erc-stamp--insert-date-stamp-as-phony-message))
+    (save-restriction
+      (narrow-to-region (or erc--insert-marker erc-insert-marker)
+                        (or erc--insert-marker erc-insert-marker))
+      (let (erc-timestamp-format erc-away-timestamp-format)
+        (erc-add-timestamp)))))
 
 (defun erc-insert-timestamp-left-and-right (string)
   "Insert a stamp on either side when it changes.
 When the deprecated option `erc-timestamp-format-right' is nil,
 use STRING, which originates from `erc-timestamp-format', for the
 right-hand stamp.  Use `erc-timestamp-format-left' for the
-left-hand stamp and expect it to change less frequently."
+left-hand stamp and expect it to change less frequently.  Include
+line endings found in `erc-timestamp-format-left' (or affixed by
+ERC) as part of the `erc-timestamp' field, which extends to the
+start of the message proper.  Do this so other code knows the
+stamp is part of the subsequent IRC message even though it may
+appear on its own line.  However, allow the stamp's `invisible'
+property to span a different interval, in order to satisfy newer
+folding requirements related to `erc-legacy-invisible-bounds-p'.
+Additionally, ensure every date stamp formatted with the option
+`erc-timestamp-format-left' is marked as such so that modules can
+easily distinguish between other left-sided stamps and date
+stamps inserted by this function."
+  (unless erc-stamp--date-format-end
+    (add-hook 'erc-insert-pre-hook #'erc-stamp--lr-date-on-pre-modify -95 t)
+    (add-hook 'erc-send-pre-functions #'erc-stamp--lr-date-on-pre-modify -95 t)
+    (let ((erc--insert-marker (point-min-marker)))
+      (set-marker-insertion-type erc--insert-marker t)
+      (erc-stamp--lr-date-on-pre-modify nil)
+      (narrow-to-region erc--insert-marker (point-max))
+      (set-marker erc--insert-marker nil)))
   (let* ((ct (or erc-stamp--current-time (erc-stamp--current-time)))
-         (ts-left (erc-format-timestamp ct erc-timestamp-format-left))
          (ts-right (with-suppressed-warnings
                        ((obsolete erc-timestamp-format-right))
                      (if erc-timestamp-format-right
                          (erc-format-timestamp ct erc-timestamp-format-right)
                        string))))
-    ;; insert left timestamp
-    (unless (string-equal ts-left erc-timestamp-last-inserted-left)
-      (goto-char (point-min))
-      (erc-put-text-property 0 (length ts-left) 'field 'erc-timestamp ts-left)
-      (funcall erc-stamp--insert-date-function ts-left)
-      (setq erc-timestamp-last-inserted-left ts-left))
     ;; insert right timestamp
     (let ((erc-timestamp-only-if-changed-flag t)
 	  (erc-timestamp-last-inserted erc-timestamp-last-inserted-right))
@@ -639,8 +742,9 @@ erc-format-timestamp
       (let ((ts (format-time-string format time erc-stamp--tz)))
 	(erc-put-text-property 0 (length ts)
 			       'font-lock-face 'erc-timestamp-face ts)
-        (erc-put-text-property 0 (length ts) 'invisible
-                               erc-stamp--invisible-property ts)
+        (when erc-stamp--invisible-property
+          (erc-put-text-property 0 (length ts) 'invisible
+                                 erc-stamp--invisible-property ts))
 	;; N.B. Later use categories instead of this harmless, but
 	;; inelegant, hack. -- BPT
 	(and erc-timestamp-intangible
@@ -649,6 +753,8 @@ erc-format-timestamp
 	ts)
     ""))
 
+(defvar-local erc-stamp--csf-props-updated-p nil)
+
 ;; This function is used to munge `buffer-invisibility-spec' to an
 ;; appropriate value. Currently, it only handles timestamps, thus its
 ;; location.  If you add other features which affect invisibility,
@@ -661,10 +767,23 @@ erc-munge-invisibility-spec
       (cursor-intangible-mode -1)))
   (if erc-echo-timestamps
       (progn
+        (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+          (add-hook hook #'erc-stamp--add-csf-on-post-modify nil t))
+        (erc--restore-initialize-priors erc-stamp-mode
+          erc-stamp--csf-props-updated-p nil)
+        (unless (or erc-stamp--allow-unmanaged erc-stamp--csf-props-updated-p)
+          (setq erc-stamp--csf-props-updated-p t)
+          (let ((erc--msg-props (map-into '((erc-ts . t)) 'hash-table)))
+            (with-silent-modifications
+              (erc--traverse-inserted (point-min) erc-insert-marker
+                                      #'erc-stamp--add-csf-on-post-modify))))
         (cursor-sensor-mode +1) ; idempotent
         (when (>= emacs-major-version 29)
           (add-function :before-until (local 'clear-message-function)
                         #'erc-stamp--on-clear-message)))
+    (dolist (hook '(erc-insert-post-hook erc-send-post-hook))
+      (remove-hook hook #'erc-stamp--add-csf-on-post-modify t))
+    (kill-local-variable 'erc-stamp--csf-props-updated-p)
     (when (bound-and-true-p cursor-sensor-mode)
       (cursor-sensor-mode -1))
     (remove-function (local 'clear-message-function)
@@ -673,12 +792,22 @@ erc-munge-invisibility-spec
       (add-to-invisibility-spec 'timestamp)
     (remove-from-invisibility-spec 'timestamp)))
 
+(defun erc-stamp--add-csf-on-post-modify ()
+  "Add `cursor-sensor-functions' to narrowed buffer."
+  (when (erc--check-msg-prop 'erc-ts)
+    (put-text-property (point-min) (1- (point-max))
+                       'cursor-sensor-functions '(erc--echo-ts-csf))))
+
 (defun erc-stamp--setup ()
   "Enable or disable buffer-local `erc-stamp-mode' modifications."
   (if erc-stamp-mode
       (erc-munge-invisibility-spec)
     (let (erc-echo-timestamps erc-hide-timestamps erc-timestamp-intangible)
-      (erc-munge-invisibility-spec))))
+      (erc-munge-invisibility-spec))
+    ;; Undo local mods from `erc-insert-timestamp-left-and-right'.
+    (remove-hook 'erc-insert-pre-hook #'erc-stamp--lr-date-on-pre-modify t)
+    (remove-hook 'erc-send-pre-functions #'erc-stamp--lr-date-on-pre-modify t)
+    (kill-local-variable 'erc-stamp--date-format-end)))
 
 (defun erc-hide-timestamps ()
   "Hide timestamp information from display."
@@ -714,7 +843,7 @@ erc-stamp--last-stamp
 (defun erc-stamp--on-clear-message (&rest _)
   "Return `dont-clear-message' when operating inside the same stamp."
   (and erc-stamp--last-stamp erc-echo-timestamps
-       (eq (get-text-property (point) 'erc-timestamp) erc-stamp--last-stamp)
+       (eq (erc--get-inserted-msg-prop 'erc-ts) erc-stamp--last-stamp)
        'dont-clear-message))
 
 (defun erc-echo-timestamp (dir stamp &optional zone)
@@ -724,7 +853,7 @@ erc-echo-timestamp
 interpret a \"raw\" prefix as UTC.  To specify a zone for use
 with the option `erc-echo-timestamps', see the companion option
 `erc-echo-timestamp-zone'."
-  (interactive (list nil (get-text-property (point) 'erc-timestamp)
+  (interactive (list nil (erc--get-inserted-msg-prop 'erc-ts)
                      (pcase current-prefix-arg
                        ((and (pred numberp) v)
                         (if (<= (abs v) 14) (* v 3600) v))
@@ -738,18 +867,18 @@ erc-echo-timestamp
       (setq erc-stamp--last-stamp nil))))
 
 (defun erc--echo-ts-csf (_window _before dir)
-  (erc-echo-timestamp dir (get-text-property (point) 'erc-timestamp)))
+  (erc-echo-timestamp dir (erc--get-inserted-msg-prop 'erc-ts)))
 
 (defun erc-stamp--update-saved-position (&rest _)
-  (remove-function (local 'erc-stamp--insert-date-function)
-                   #'erc-stamp--update-saved-position)
-  (move-marker erc-last-saved-position (1- (point))))
+  (remove-hook 'erc-stamp--insert-date-hook
+               #'erc-stamp--update-saved-position t)
+  (move-marker erc-last-saved-position (1- (point-max))))
 
 (defun erc-stamp--reset-on-clear (pos)
   "Forget last-inserted stamps when POS is at insert marker."
   (when (= pos (1- erc-insert-marker))
-    (add-function :after (local 'erc-stamp--insert-date-function)
-                  #'erc-stamp--update-saved-position)
+    (add-hook 'erc-stamp--insert-date-hook
+              #'erc-stamp--update-saved-position 0 t)
     (setq erc-timestamp-last-inserted nil
           erc-timestamp-last-inserted-left nil
           erc-timestamp-last-inserted-right nil)))
diff --git a/lisp/erc/erc-truncate.el b/lisp/erc/erc-truncate.el
index 48d8408a85a..3350cbd13b7 100644
--- a/lisp/erc/erc-truncate.el
+++ b/lisp/erc/erc-truncate.el
@@ -102,7 +102,7 @@ erc-truncate-buffer-to-size
           ;; Truncate at message boundary (formerly line boundary
           ;; before 5.6).
 	  (goto-char end)
-          (goto-char (or (previous-single-property-change (point) 'erc-command)
+          (goto-char (or (erc--get-inserted-msg-bounds 'beg)
                          (pos-bol)))
 	  (setq end (point))
 	  ;; try to save the current buffer using
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index f3c480f918b..891689d8faa 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -135,9 +135,11 @@ erc-scripts
   "Running scripts at startup and with /LOAD."
   :group 'erc)
 
-;; Forward declarations
-(defvar erc-message-parsed)
+(defvar erc-message-parsed) ; only known to this file
+(defvar erc--msg-props nil)
+(defvar erc--msg-prop-overrides nil)
 
+;; Forward declarations
 (defvar tabbar--local-hlf)
 (defvar motif-version-string)
 (defvar gtk-version-string)
@@ -1139,9 +1141,13 @@ erc-insert-modify-hook
   "Insertion hook for functions that will change the text's appearance.
 This hook is called just after `erc-insert-pre-hook' when the value
 of `erc-insert-this' is t.
-While this hook is run, narrowing is in effect and `current-buffer' is
-the buffer where the text got inserted.  One possible value to add here
-is `erc-fill'."
+
+ERC runs this hook with the buffer narrowed to the bounds of the
+inserted message plus a trailing newline.  Built-in modules place
+their hook members at depths between 20 and 80, with those from
+the stamp module always running last.  Use the functions
+`erc-find-parsed-property' and `erc-get-parsed-vector' to locate
+and extract the `erc-response' object for the inserted message."
   :group 'erc-hooks
   :type 'hook)
 
@@ -2854,11 +2860,10 @@ erc-toggle-debug-irc-protocol
 (defun erc-send-action (tgt str &optional force)
   "Send CTCP ACTION information described by STR to TGT."
   (erc-send-ctcp-message tgt (format "ACTION %s" str) force)
-  (let ((erc-insert-pre-hook
-         (cons (lambda (s) ; Leave newline be.
-                 (put-text-property 0 (1- (length s)) 'erc-command 'PRIVMSG s)
-                 (put-text-property 0 (1- (length s)) 'erc-ctcp 'ACTION s))
-               erc-insert-pre-hook))
+  ;; Allow hooks that act on inserted PRIVMSG and NOTICES to process us.
+  (let ((erc--msg-prop-overrides '((erc-msg . msg)
+                                   (erc-cmd . PRIVMSG)
+                                   (erc-ctcp . ACTION)))
         (nick (erc-current-nick)))
     (setq nick (propertize nick 'erc-speaker nick))
     (erc-display-message nil '(t action input) (current-buffer)
@@ -2917,6 +2922,66 @@ erc--refresh-prompt
         (delete-region (point) (1- erc-input-marker))))
     (run-hooks 'erc--refresh-prompt-hook)))
 
+(define-inline erc--check-msg-prop (prop &optional val)
+  "Return value for PROP in `erc--msg-props' when populated.
+If VAL is a list, return non-nil if PROP appears in VAL.  If VAL
+is otherwise non-nil, return non-nil if VAL compares `eq' to the
+stored value.  Otherwise, return the stored value."
+  (inline-letevals (prop val)
+    (let ((v (make-symbol "v")))
+      `(and-let* ((erc--msg-props)
+                  (,v (gethash ,prop erc--msg-props)))
+         (if (consp ,val) (memq ,v ,val) (if ,val (eq ,v ,val) ,v))))))
+
+(defmacro erc--get-inserted-msg-bounds (&optional only point)
+  `(let* ((point ,(or point '(point)))
+          (at-start-p (get-text-property point 'erc-msg)))
+     (and-let*
+         (,@(and (member only '(nil 'beg))
+                 '((b (or (and at-start-p point)
+                          (and-let*
+                              ((p (previous-single-property-change point
+                                                                   'erc-msg)))
+                            (if (= p (1- point)) point (1- p)))))))
+          ,@(and (member only '(nil 'end))
+                 '((e (1- (next-single-property-change
+                           (if at-start-p (1+ point) point)
+                           'erc-msg nil erc-insert-marker))))))
+       ,(pcase only
+          ('(quote beg) 'b)
+          ('(quote end) 'e)
+          (_ '(cons b e))))))
+
+(defun erc--get-inserted-msg-prop (prop)
+  "Return the value of text property PROP for some message at point."
+  (and-let* ((stack-pos (erc--get-inserted-msg-bounds 'beg)))
+    (get-text-property stack-pos prop)))
+
+(defmacro erc--with-inserted-msg (&rest body)
+  "Simulate buffer narrowing of send insert hooks for BODY.
+Note that this does not wrap BODY in `with-silent-modifications'.
+Similarly, it does not bind a temporary `erc--msg-props' table."
+  `(when-let ((bounds (erc--get-inserted-msg-bounds)))
+     (save-restriction
+       (narrow-to-region (car bounds) (1+ (cdr bounds)))
+       ,@body)))
+
+(defun erc--traverse-inserted (beg end fn)
+  "Visit messages between BEG and END and run FN in narrowed buffer."
+  (setq end (min end (marker-position erc-insert-marker)))
+  (save-excursion
+    (goto-char beg)
+    (let ((b (if (get-text-property (point) 'erc-msg)
+                 (point)
+               (next-single-property-change (point) 'erc-msg nil end))))
+      (while-let ((b)
+                  ((< b end))
+                  (e (next-single-property-change (1+ b) 'erc-msg nil end)))
+        (save-restriction
+          (narrow-to-region b e)
+          (funcall fn))
+        (setq b e)))))
+
 (defvar erc--insert-marker nil)
 
 (defun erc-display-line-1 (string buffer)
@@ -2963,7 +3028,13 @@ erc-display-line-1
                   (run-hooks 'erc-insert-post-hook)
                   (when erc-remove-parsed-property
                     (remove-text-properties (point-min) (point-max)
-                                            '(erc-parsed nil tags nil))))
+                                            '(erc-parsed nil tags nil)))
+                  (cl-assert (> (- (point-max) (point-min)) 1))
+                  (let ((props (if erc--msg-props
+                                   (erc--order-text-properties-from-hash
+                                    erc--msg-props)
+                                 '(erc-msg unknown))))
+                    (add-text-properties (point-min) (1+ (point-min)) props)))
                 (erc--refresh-prompt)))))
         (run-hooks 'erc-insert-done-hook)
         (erc-update-undo-list (- (or (marker-position (or erc--insert-marker
@@ -3094,7 +3165,11 @@ erc-legacy-invisible-bounds-p
 
 (defun erc--hide-message (value)
   "Apply `invisible' text-property with VALUE to current message.
-Expect to run in a narrowed buffer during message insertion."
+Expect to run in a narrowed buffer during message insertion.
+Begin the invisible interval at the previous message's trailing
+newline and end before the current message's.  If the preceding
+message ends in a double newline or there is no previous message,
+don't bother including the preceding newline."
   (if erc-legacy-invisible-bounds-p
       ;; Before ERC 5.6, this also used to add an `intangible'
       ;; property, but the docs say it's now obsolete.
@@ -3103,8 +3178,25 @@ erc--hide-message
           (end (point-max)))
       (save-restriction
         (widen)
+        (when (or (<= beg 4) (= ?\n (char-before (- beg 2))))
+          (cl-incf beg))
         (erc--merge-prop (1- beg) (1- end) 'invisible value)))))
 
+(defvar erc--ranked-properties '(erc-msg erc-ts erc-cmd))
+
+(defun erc--order-text-properties-from-hash (table)
+  "Return a plist of text props from items in table.
+Ensure props in `erc--ranked-properties' appear last and in
+reverse order so that they end up sorted in buffer interval
+plists for retrieval by `text-properties-at' and friends."
+  (let (out)
+    (dolist (k erc--ranked-properties)
+      (when-let ((v (gethash k table)))
+        (remhash k table)
+        (setq out (nconc (list k v) out))))
+    (maphash (lambda (k v) (setq out (nconc (list k v) out))) table)
+    out))
+
 (defun erc-display-message-highlight (type string)
   "Highlight STRING according to TYPE, where erc-TYPE-face is an ERC face.
 
@@ -3335,6 +3427,21 @@ erc-display-message
   (let ((string (if (symbolp msg)
                     (apply #'erc-format-message msg args)
                   msg))
+        (erc--msg-props
+         (or erc--msg-props
+             (let* ((table (make-hash-table :size 5))
+                    (cmd (and parsed (erc--get-eq-comparable-cmd
+                                      (erc-response.command parsed))))
+                    (m (cond ((and msg (symbolp msg)) msg)
+                             ((and cmd (memq cmd '(PRIVMSG NOTICE)) 'msg))
+                             (t 'unknown))))
+               (puthash 'erc-msg m table)
+               (when cmd
+                 (puthash 'erc-cmd cmd table))
+               (and erc--msg-prop-overrides
+                    (pcase-dolist (`(,k . ,v) erc--msg-prop-overrides)
+                      (puthash k v table)))
+               table)))
         (erc-message-parsed parsed))
     (setq string
           (cond
@@ -3353,9 +3460,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)
-        (put-text-property
-         0 (length string) 'erc-command
-         (erc--get-eq-comparable-cmd (erc-response.command parsed)) string)
 	(when (erc-response.tags parsed)
 	  (erc-put-text-property 0 (length string) 'tags (erc-response.tags parsed)
 				 string))
@@ -4818,6 +4922,7 @@ erc--own-property-names
      rear-nonsticky erc-prompt field front-sticky read-only
      ;; stamp
      cursor-intangible cursor-sensor-functions isearch-open-invisible
+     erc-stamp-type
      ;; match
      invisible intangible
      ;; button
@@ -5305,7 +5410,7 @@ erc--get-speaker-bounds
 Assume buffer is narrowed to the confines of an inserted message."
   (inline-quote
    (and-let*
-       (((memq (get-text-property (point) 'erc-command) '(PRIVMSG NOTICE)))
+       (((erc--check-msg-prop 'erc-msg 'msg))
         (beg (or (and (get-text-property (point-min) 'erc-speaker) (point-min))
                  (next-single-property-change (point-min) 'erc-speaker))))
      (cons beg (next-single-property-change beg 'erc-speaker)))))
@@ -5630,11 +5735,8 @@ erc-process-ctcp-query
         (while queries
           (let* ((type (upcase (car (split-string (car queries)))))
                  (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))
-                 (erc-insert-pre-hook
-                  (cons (lambda (s)
-                          (put-text-property 0 (1- (length s)) 'erc-ctcp
-                                             (intern type) s))
-                        erc-insert-pre-hook)))
+                 (erc--msg-prop-overrides `((erc-msg . msg)
+                                            (erc-ctcp . ,(intern type)))))
             (if (and hook (boundp hook))
                 (if (string-equal type "ACTION")
                     (run-hook-with-args-until-success
@@ -6639,7 +6741,8 @@ erc-send-current-line
             (when-let (((not (erc--input-split-abortp state)))
                        (inhibit-read-only t)
                        (old-buf (current-buffer)))
-              (progn ; unprogn this during next major surgery
+              (let ((erc--msg-prop-overrides '((erc-cmd . PRIVMSG)
+                                               (erc-msg . msg))))
                 (erc-set-active-buffer (current-buffer))
                 ;; Kill the input and the prompt
                 (delete-region erc-input-marker (erc-end-of-input-line))
@@ -6786,17 +6889,24 @@ erc-display-msg
     (save-excursion
       (erc--assert-input-bounds)
       (let ((insert-position (marker-position (goto-char erc-insert-marker)))
+            (erc--msg-props (or erc--msg-props
+                                (map-into (cons '(erc-msg . self)
+                                                erc--msg-prop-overrides)
+                                          'hash-table)))
             beg)
         (insert (erc-format-my-nick))
         (setq beg (point))
         (insert line)
         (erc-put-text-property beg (point) 'font-lock-face 'erc-input-face)
-        (erc-put-text-property insert-position (point) 'erc-command 'PRIVMSG)
         (insert "\n")
         (save-restriction
           (narrow-to-region insert-position (point))
           (run-hooks 'erc-send-modify-hook)
-          (run-hooks 'erc-send-post-hook))
+          (run-hooks 'erc-send-post-hook)
+          (cl-assert (> (- (point-max) (point-min)) 1))
+          (add-text-properties (point-min) (1+ (point-min))
+                               (erc--order-text-properties-from-hash
+                                erc--msg-props)))
         (erc--refresh-prompt)))))
 
 (defun erc-command-symbol (command)
@@ -8184,8 +8294,8 @@ erc-find-parsed-property
   (text-property-not-all (point-min) (point-max) 'erc-parsed nil))
 
 (defun erc-restore-text-properties ()
-  "Restore the property `erc-parsed' for the region."
-  (when-let* ((parsed-posn (erc-find-parsed-property))
+  "Ensure the `erc-parsed' and `tags' props cover the entire message."
+  (when-let ((parsed-posn (erc-find-parsed-property))
               (found (erc-get-parsed-vector parsed-posn)))
     (put-text-property (point-min) (point-max) 'erc-parsed found)
     (when-let ((tags (get-text-property parsed-posn 'tags)))
@@ -8214,7 +8324,7 @@ erc--get-eq-comparable-cmd
 See also `erc-message-type'."
   ;; IRC numerics are three-digit numbers, possibly with leading 0s.
   ;; To invert: (if (numberp o) (format "%03d" o) (symbol-name o))
-  (if-let* ((n (string-to-number command)) ((zerop n))) (intern command) n))
+  (if-let ((n (string-to-number command)) ((zerop n))) (intern command) n))
 
 ;; Teach url.el how to open irc:// URLs with ERC.
 ;; To activate, customize `url-irc-function' to `url-irc-erc'.
diff --git a/test/lisp/erc/erc-fill-tests.el b/test/lisp/erc/erc-fill-tests.el
index b81d0c15558..8f0c8f9ccf4 100644
--- a/test/lisp/erc/erc-fill-tests.el
+++ b/test/lisp/erc/erc-fill-tests.el
@@ -31,10 +31,14 @@ erc-fill-tests--time-vals
 
 (defun erc-fill-tests--insert-privmsg (speaker &rest msg-parts)
   (declare (indent 1))
-  (let ((msg (erc-format-privmessage speaker
-                                     (apply #'concat msg-parts) nil t)))
-    (put-text-property 0 (length msg) 'erc-command 'PRIVMSG msg)
-    (erc-display-message nil nil (current-buffer) msg)))
+  (let* ((msg (erc-format-privmessage speaker
+                                      (apply #'concat msg-parts) nil t))
+         ;; (erc--msg-prop-overrides '((erc-msg . msg) (erc-cmd . PRIVMSG)))
+         (parsed (make-erc-response :unparsed msg :sender speaker
+                                    :command "PRIVMSG"
+                                    :command-args (list "#chan" msg)
+                                    :contents msg)))
+    (erc-display-message parsed nil (current-buffer) msg)))
 
 (defun erc-fill-tests--wrap-populate (test)
   (let ((original-window-buffer (window-buffer (selected-window)))
@@ -75,8 +79,8 @@ erc-fill-tests--wrap-populate
 
           (erc-fill-tests--insert-privmsg "alice"
             "bob: come, you are a tedious fool: to the purpose. "
-            "What was done to Elbow's wife, that he hath cause to complain of? "
-            "Come me to what was done to her.")
+            "What was done to Elbow's wife, that he hath cause to complain of?"
+            " Come me to what was done to her.")
 
           ;; Introduce an artificial gap in properties `line-prefix' and
           ;; `wrap-prefix' and later ensure they're not incremented twice.
@@ -111,6 +115,14 @@ erc-fill-tests--wrap-check-prefixes
       (should (get-text-property (pos-bol) 'line-prefix))
       (should (get-text-property (1- (pos-eol)) 'line-prefix))
       (should-not (get-text-property (pos-eol) 'line-prefix))
+      ;; Spans entire line uninterrupted.
+      (let* ((val (get-text-property (pos-bol) 'line-prefix))
+             (end (text-property-not-all (pos-bol) (point-max)
+                                         'line-prefix val)))
+        (when (and (/= end (pos-eol)) (= ?? (char-before end)))
+          (setq end (text-property-not-all (1+ end) (point-max)
+                                           'line-prefix val)))
+        (should (eq end (pos-eol))))
       (should (equal (get-text-property (pos-bol) 'wrap-prefix)
                      '(space :width erc-fill--wrap-value)))
       (should-not (get-text-property (pos-eol) 'wrap-prefix))
@@ -145,7 +157,7 @@ erc-fill-tests--compare
                                (number-to-string erc-fill--wrap-value)
                                (prin1-to-string got))))
     (with-current-buffer (generate-new-buffer name)
-      (push name erc-fill-tests--buffers)
+      (push (current-buffer) erc-fill-tests--buffers)
       (with-silent-modifications
         (insert (setq got (read repr))))
       (erc-mode))
@@ -153,15 +165,31 @@ erc-fill-tests--compare
         (with-temp-file expect-file
           (insert repr))
       (if (file-exists-p expect-file)
-          ;; Compare set-equal over intervals.  This comparison is
-          ;; less useful for messages treated by other modules because
-          ;; it doesn't compare "nested" props belonging to
-          ;; string-valued properties, like timestamps.
-          (should (equal-including-properties
-                   (read repr)
-                   (read (with-temp-buffer
-                           (insert-file-contents-literally expect-file)
-                           (buffer-string)))))
+          ;; Ensure string-valued properties, like timestamps, aren't
+          ;; recursive (signals `max-lisp-eval-depth' exceeded).
+          (named-let assert-equal
+              ((latest (read repr))
+               (expect (read (with-temp-buffer
+                               (insert-file-contents-literally expect-file)
+                               (buffer-string)))))
+            (pcase latest
+              ((or "" 'nil) t)
+              ((pred stringp)
+               (should (equal-including-properties latest expect))
+               (let ((latest-intervals (object-intervals latest))
+                     (expect-intervals (object-intervals expect)))
+                 (while-let ((l-iv (pop latest-intervals))
+                             (x-iv (pop expect-intervals))
+                             (l-tab (map-into (nth 2 l-iv) 'hash-table))
+                             (x-tab (map-into (nth 2 x-iv) 'hash-table)))
+                   (pcase-dolist (`(,l-k . ,l-v) (map-pairs l-tab))
+                     (assert-equal l-v (gethash l-k x-tab))
+                     (remhash l-k x-tab))
+                   (should (zerop (hash-table-count x-tab))))))
+              ((pred sequencep)
+               (assert-equal (seq-first latest) (seq-first expect))
+               (assert-equal (seq-rest latest) (seq-rest expect)))
+              (_ (should (equal latest expect)))))
         (message "Snapshot file missing: %S" expect-file)))))
 
 ;; To inspect variable pitch, set `erc-mode-hook' to
diff --git a/test/lisp/erc/erc-scenarios-log.el b/test/lisp/erc/erc-scenarios-log.el
index fd030d90c2f..f7e7d61c92e 100644
--- a/test/lisp/erc/erc-scenarios-log.el
+++ b/test/lisp/erc/erc-scenarios-log.el
@@ -81,6 +81,7 @@ erc-scenarios-log--kill-hook
 
 (ert-deftest erc-scenarios-log--clear-stamp ()
   :tags '(:expensive-test)
+  (require 'erc-stamp)
   (erc-scenarios-common-with-cleanup
       ((erc-scenarios-common-dialog "base/assoc/bouncer-history")
        (dumb-server (erc-d-run "localhost" t 'foonet))
diff --git a/test/lisp/erc/erc-scenarios-match.el b/test/lisp/erc/erc-scenarios-match.el
index cd899fddb98..864f3881ab1 100644
--- a/test/lisp/erc/erc-scenarios-match.el
+++ b/test/lisp/erc/erc-scenarios-match.el
@@ -55,7 +55,8 @@ erc-scenarios-match--stamp-left-current-nick
                                 :nick "tester")
         ;; Module `timestamp' follows `match' in insertion hooks.
         (should (memq 'erc-add-timestamp
-                      (memq 'erc-match-message erc-insert-modify-hook)))
+                      (memq 'erc-match-message
+                            (default-value 'erc-insert-modify-hook))))
         ;; The "match type" is `current-nick'.
         (funcall expect 5 "tester")
         (should (eq (get-text-property (1- (point)) 'font-lock-face)
@@ -91,7 +92,8 @@ erc-scenarios-match--invisible-stamp
                                 :nick "tester")
         ;; Module `timestamp' follows `match' in insertion hooks.
         (should (memq 'erc-add-timestamp
-                      (memq 'erc-match-message erc-insert-modify-hook)))
+                      (memq 'erc-match-message
+                            (default-value 'erc-insert-modify-hook))))
         (funcall expect 5 "This server is in debug mode")))
 
     (ert-info ("Ensure lines featuring \"bob\" are invisible")
@@ -151,29 +153,13 @@ erc-scenarios-match--stamp-left-fools-invisible
           (= (next-single-property-change msg-beg 'invisible nil (pos-eol))
              (pos-eol))))))))
 
-(defun erc-scenarios-match--find-bol ()
-  (save-excursion
-    (should (get-text-property (1- (point)) 'erc-command))
-    (goto-char (should (previous-single-property-change (point) 'erc-command)))
-    (pos-bol)))
-
-(defun erc-scenarios-match--find-eol ()
-  (save-excursion
-    (if-let ((next (next-single-property-change (point) 'erc-command)))
-        (goto-char next)
-      ;; We're already at the end of the message.
-      (should (get-text-property (1- (point)) 'erc-command)))
-    (pos-eol)))
-
 ;; In most cases, `erc-hide-fools' makes line endings invisible.
 (defun erc-scenarios-match--stamp-right-fools-invisible ()
-  :tags '(:expensive-test)
   (let ((erc-insert-timestamp-function #'erc-insert-timestamp-right))
     (erc-scenarios-match--invisible-stamp
 
      (lambda ()
-       (let ((beg (erc-scenarios-match--find-bol))
-             (end (erc-scenarios-match--find-eol)))
+       (pcase-let ((`(,beg . ,end) (erc--get-inserted-msg-bounds)))
          ;; The end of the message is a newline.
          (should (= ?\n (char-after end)))
 
@@ -205,7 +191,7 @@ erc-scenarios-match--stamp-right-fools-invisible
            (should (= (next-single-property-change msg-end 'invisible) end)))))
 
      (lambda ()
-       (let ((end (erc-scenarios-match--find-eol)))
+       (let ((end (cdr (erc--get-inserted-msg-bounds))))
          ;; This message has a time stamp like all the others.
          (should (eq (field-at-pos (1- end)) 'erc-timestamp))
 
@@ -271,7 +257,117 @@ erc-scenarios-match--stamp-right-invisible-fill-wrap
        (let ((inv-beg (next-single-property-change (1- (pos-bol)) 'invisible)))
          (should (eq (get-text-property inv-beg 'invisible) 'timestamp)))))))
 
-(defun erc-scenarios-match--stamp-both-invisible-fill-static ()
+(defun erc-scenarios-match--fill-wrap-stamp-dedented-p (point)
+  (pcase (get-text-property point 'line-prefix)
+    (`(space :width (- erc-fill--wrap-value (,n)))
+     (if (display-graphic-p) (< 100 n 200) (< 10 n 30)))
+    (`(space :width (- erc-fill--wrap-value ,n))
+     (< 10 n 30))))
+
+(ert-deftest erc-scenarios-match--stamp-both-invisible-fill-wrap ()
+
+  ;; Rewind the clock to known date artificially.  We should probably
+  ;; use a ticks/hz cons on 29+.
+  (let ((erc-stamp--current-time 704591940)
+        (erc-stamp--tz t)
+        (erc-fill-function #'erc-fill-wrap)
+        (bob-utterance-counter 0))
+
+    (erc-scenarios-match--invisible-stamp
+
+     (lambda ()
+       (ert-info ("Baseline check")
+         ;; False date printed initially before anyone speaks.
+         (when (zerop bob-utterance-counter)
+           (save-excursion
+             (goto-char (point-min))
+             (search-forward "[Wed Apr 29 1992]")
+             ;; First stamp in a buffer is not invisible from previous
+             ;; newline (before stamp's own leading newline).
+             (should (= 4 (match-beginning 0)))
+             (should (get-text-property 3 'invisible))
+             (should-not (get-text-property 2 'invisible))
+             (should (erc-scenarios-match--fill-wrap-stamp-dedented-p 4))
+             (search-forward "[23:59]"))))
+
+       (ert-info ("Line endings in Bob's messages are invisible")
+         ;; The message proper has the `invisible' property `match-fools'.
+         (should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
+         (pcase-let ((`(,mbeg . ,mend) (erc--get-inserted-msg-bounds)))
+           (should (= (char-after mend) ?\n))
+           (should-not (field-at-pos mend))
+           (should-not (field-at-pos mbeg))
+
+           (when (= bob-utterance-counter 1)
+             (let ((right-stamp (field-end mbeg)))
+               (should (eq 'erc-timestamp (field-at-pos right-stamp)))
+               (should (= mend (field-end right-stamp)))
+               (should (eq (field-at-pos (1- mend)) 'erc-timestamp))))
+
+           ;; The `erc-ts' property is present in prop stack.
+           (should (get-text-property (pos-bol) 'erc-ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
+
+           ;; Line ending has the `invisible' property `match-fools'.
+           (should (eq (get-text-property mbeg 'invisible) 'match-fools))
+           (should-not (get-text-property mend 'invisible))))
+
+       ;; Only the message right after Alice speaks contains stamps.
+       (when (= 1 bob-utterance-counter)
+
+         (ert-info ("Date stamp occupying previous line is invisible")
+           (should (eq 'match-fools (get-text-property (point) 'invisible)))
+           (save-excursion
+             (forward-line -1)
+             (goto-char (pos-bol))
+             (should (looking-at (rx "[Mon May  4 1992]")))
+             (ert-info ("Stamp's NL `invisible' as fool, not timestamp")
+               (let ((end (match-end 0)))
+                 (should (eq (char-after end) ?\n))
+                 (should (eq 'timestamp
+                             (get-text-property (1- end) 'invisible)))
+                 (should (eq 'match-fools
+                             (get-text-property end 'invisible)))))
+             (should (erc-scenarios-match--fill-wrap-stamp-dedented-p (point)))
+             ;; Date stamp has a combined `invisible' property value
+             ;; that starts at the previous message's trailing newline
+             ;; and extends until the start of the message proper.
+             (should (equal ?\n (char-before (point))))
+             (should (equal ?\n (char-before (1- (point)))))
+             (let ((val (get-text-property (- (point) 2) 'invisible)))
+               (should (equal val 'timestamp))
+               (should (= (text-property-not-all (- (point) 2) (point-max)
+                                                 'invisible val)
+                          (pos-eol))))))
+
+         (ert-info ("Current message's RHS stamp is hidden")
+           ;; Right stamp has `match-fools' property.
+           (save-excursion
+             (should-not (field-at-pos (point)))
+             (should (eq (field-at-pos (1- (pos-eol))) 'erc-timestamp)))
+
+           ;; Stamp invisibility starts where message's ends.
+           (let ((msgend (next-single-property-change (pos-bol) 'invisible)))
+             ;; Stamp has a combined `invisible' property value.
+             (should (equal (get-text-property msgend 'invisible)
+                            '(timestamp match-fools)))
+
+             ;; Combined `invisible' property spans entire timestamp.
+             (should (= (next-single-property-change msgend 'invisible)
+                        (pos-eol))))))
+
+       (cl-incf bob-utterance-counter))
+
+     ;; Alice.
+     (lambda ()
+       ;; Set clock ahead a week or so.
+       (setq erc-stamp--current-time 704962800)
+
+       ;; This message has no time stamp and is completely visible.
+       (should-not (eq (field-at-pos (1- (pos-eol))) 'erc-timestamp))
+       (should-not (next-single-property-change (pos-bol) 'invisible))))))
+
+(defun erc-scenarios-match--stamp-both-invisible-fill-static (assert-ds)
   (should (eq erc-insert-timestamp-function
               #'erc-insert-timestamp-left-and-right))
 
@@ -295,21 +391,20 @@ erc-scenarios-match--stamp-both-invisible-fill-static
        (ert-info ("Line endings in Bob's messages are invisible")
          ;; The message proper has the `invisible' property `match-fools'.
          (should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
-         (let* ((mbeg (next-single-property-change (pos-bol) 'erc-command))
-                (mend (next-single-property-change mbeg 'erc-command)))
+         (pcase-let ((`(,mbeg . ,mend) (erc--get-inserted-msg-bounds)))
 
-           (if (/= 1 bob-utterance-counter)
-               (should-not (field-at-pos mend))
+           (should (= (char-after mend) ?\n))
+           (should-not (field-at-pos mbeg))
+           (should-not (field-at-pos mend))
+           (when (= 1 bob-utterance-counter)
              ;; For Bob's stamped message, check newline after stamp.
-             (should (eq (field-at-pos mend) 'erc-timestamp))
-             (setq mend (field-end mend)))
+             (should (eq (field-at-pos (field-end mbeg)) 'erc-timestamp))
+             (should (eq (field-at-pos (1- mend)) 'erc-timestamp)))
 
-           ;; The `erc-timestamp' property spans entire messages,
-           ;; including stamps and filled text, which makes for
-           ;; convenient traversal when `erc-stamp-mode' is enabled.
-           (should (get-text-property (pos-bol) 'erc-timestamp))
-           (should (= (next-single-property-change (pos-bol) 'erc-timestamp)
-                      mend))
+           ;; The `erc-ts' property is present in the message's
+           ;; width 1 prop collection at its first char.
+           (should (get-text-property (pos-bol) 'erc-ts))
+           (should-not (next-single-property-change (1+ (pos-bol)) 'erc-ts))
 
            ;; Line ending has the `invisible' property `match-fools'.
            (should (= (char-after mend) ?\n))
@@ -327,12 +422,8 @@ erc-scenarios-match--stamp-both-invisible-fill-static
              (forward-line -1)
              (goto-char (pos-bol))
              (should (looking-at (rx "[Mon May  4 1992]")))
-             ;; Date stamp has a combined `invisible' property value
-             ;; that extends until the start of the message proper.
-             (should (equal (get-text-property (point) 'invisible)
-                            '(timestamp match-fools)))
-             (should (= (next-single-property-change (point) 'invisible)
-                        (1+ (pos-eol))))))
+             (should (= ?\n (char-after (- (point) 2)))) ; welcome!\n
+             (funcall assert-ds))) ; "assert date stamp"
 
          (ert-info ("Folding preserved despite invisibility")
            ;; Message has a trailing time stamp, but it's been folded
@@ -365,13 +456,45 @@ erc-scenarios-match--stamp-both-invisible-fill-static
 
 (ert-deftest erc-scenarios-match--stamp-both-invisible-fill-static ()
   :tags '(:expensive-test)
-  (erc-scenarios-match--stamp-both-invisible-fill-static))
+  (erc-scenarios-match--stamp-both-invisible-fill-static
+
+   (lambda ()
+     ;; Date stamp has an `invisible' property that starts from the
+     ;; newline delimiting the current and previous messages and
+     ;; extends until the stamp's final newline.  It is not combined
+     ;; with the old value, `match-fools'.
+     (let ((delim-pos (- (point) 2)))
+       (should (equal 'timestamp (get-text-property delim-pos 'invisible)))
+       ;; Stamp-only invisibility ends before its last newline.
+       (should (= (text-property-not-all delim-pos (point-max)
+                                         'invisible 'timestamp)
+                  (match-end 0))))))) ; pos-eol
 
 (ert-deftest erc-scenarios-match--stamp-both-invisible-fill-static--nooffset ()
   :tags '(:expensive-test)
   (with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
     (should-not erc-legacy-invisible-bounds-p)
+
     (let ((erc-legacy-invisible-bounds-p t))
-      (erc-scenarios-match--stamp-both-invisible-fill-static))))
+      (erc-scenarios-match--stamp-both-invisible-fill-static
+
+       (lambda ()
+         ;; Date stamp has an `invisible' property that covers its
+         ;; format string exactly.  It is not combined with the old
+         ;; value, `match-fools'.
+         (let ((delim-prev (- (point) 2)))
+           (should-not (get-text-property delim-prev 'invisible))
+           (should (eq 'erc-timestamp (field-at-pos (point))))
+           (should (= (next-single-property-change delim-prev 'invisible)
+                      (field-beginning (point))))
+           (should (equal 'timestamp
+                          (get-text-property (1- (point)) 'invisible)))
+           ;; Field stops before final newline because the date stamp
+           ;; is (now, as of ERC 5.6) its own standalone message.
+           (should (= ?\n (char-after (field-end (point)))))
+           ;; Stamp-only invisibility includes last newline.
+           (should (= (text-property-not-all (1- (point)) (point-max)
+                                             'invisible 'timestamp)
+                      (1+ (field-end (point)))))))))))
 
 ;;; erc-scenarios-match.el ends here
diff --git a/test/lisp/erc/erc-stamp-tests.el b/test/lisp/erc/erc-stamp-tests.el
index 46a05729066..cc61d599387 100644
--- a/test/lisp/erc/erc-stamp-tests.el
+++ b/test/lisp/erc/erc-stamp-tests.el
@@ -279,7 +279,7 @@ erc-echo-timestamp
 
   (should-not erc-echo-timestamps)
   (should-not erc-stamp--last-stamp)
-  (insert (propertize "abc" 'erc-timestamp 433483200))
+  (insert (propertize "a" 'erc-ts 433483200 'erc-msg 'msg) "bc")
   (goto-char (point-min))
   (let ((inhibit-message t)
         (erc-echo-timestamp-format "%Y-%m-%d %H:%M:%S %Z")
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 11717217eb2..408cc4db10c 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -292,6 +292,8 @@ erc--refresh-prompt
                                (cl-incf counter))))
          erc-accidental-paste-threshold-seconds
          erc-insert-modify-hook
+         (erc-modules (remq 'stamp erc-modules))
+         (erc-send-input-line-function #'ignore)
          (erc--input-review-functions erc--input-review-functions)
          erc-send-completed-hook)
 
@@ -356,7 +358,8 @@ erc--refresh-prompt
         (should (looking-back "#chan@ServNet 11> "))
         (should (= (point) erc-input-marker))
         (insert "/query bob")
-        (erc-send-current-line)
+        (let (erc-modules)
+          (erc-send-current-line))
         ;; Last command not inserted
         (save-excursion (forward-line -1)
                         (should (looking-at "<tester> Howdy")))
@@ -1431,6 +1434,44 @@ erc-process-input-line
 
           (should-not calls))))))
 
+(ert-deftest erc--order-text-properties-from-hash ()
+  (let ((table (map-into '((a . 1)
+                           (erc-ts . 0)
+                           (erc-msg . s005)
+                           (b . 2)
+                           (erc-cmd . 5)
+                           (c . 3))
+                         'hash-table)))
+    (with-temp-buffer
+      (erc-mode)
+      (insert "abc\n")
+      (add-text-properties 1 2 (erc--order-text-properties-from-hash table))
+      (should (equal '( erc-msg s005
+                        erc-ts 0
+                        erc-cmd 5
+                        a 1
+                        b 2
+                        c 3)
+                     (text-properties-at (point-min)))))))
+
+(ert-deftest erc--check-msg-prop ()
+  (let ((erc--msg-props (map-into '((a . 1) (b . x)) 'hash-table)))
+    (should (eq 1 (erc--check-msg-prop 'a)))
+    (should (erc--check-msg-prop 'a 1))
+    (should-not (erc--check-msg-prop 'a 2))
+
+    (should (eq 'x (erc--check-msg-prop 'b)))
+    (should (erc--check-msg-prop 'b 'x))
+    (should-not (erc--check-msg-prop 'b 1))
+
+    (should (erc--check-msg-prop 'a '(1 42)))
+    (should-not (erc--check-msg-prop 'a '(2 42)))
+
+    (let ((props '(42 x)))
+      (should (erc--check-msg-prop 'b props)))
+    (let ((v '(42 y)))
+      (should-not (erc--check-msg-prop 'b v)))))
+
 (defmacro erc-tests--equal-including-properties (a b)
   (list (if (< emacs-major-version 29)
             'ert-equal-including-properties
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index 689bacc7012..238d8cc73c2 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 27 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 27 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 474 475 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 27 (8))) erc-command PRIVMSG) 475 480 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 480 486 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 487 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 27 0)) display #11="" erc-command PRIVMSG) 488 493 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 493 495 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 495 499 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 500 501 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 27 (6))) erc-command PRIVMSG) 501 504 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 504 512 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 513 514 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13=(space :width (- 27 0)) display #11# erc-command PRIVMSG) 514 517 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 517 519 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 519 524 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# erc-command PRIVMSG) 525 526 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14=(space :width (- 27 (8))) erc-command PRIVMSG) 526 531 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 531 538 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 539 540 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15=(space :width (- 27 0)) display #11# erc-command PRIVMSG) 540 545 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 545 547 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 547 551 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 27 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 27 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 27 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 27 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index 9fa23a7d332..d1ce9198e69 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 29 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 29) line-prefix #3=(space :width (- 29 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 29 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 29 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 29 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 29 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 474 475 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 29 (8))) erc-command PRIVMSG) 475 480 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 480 486 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-command PRIVMSG) 487 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 29 0)) display #11="" erc-command PRIVMSG) 488 493 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 493 495 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# display #11# erc-command PRIVMSG) 495 499 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 500 501 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 29 (6))) erc-command PRIVMSG) 501 504 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 504 512 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 513 514 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13=(space :width (- 29 0)) display #11# erc-command PRIVMSG) 514 517 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 517 519 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# display #11# erc-command PRIVMSG) 519 524 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #13# erc-command PRIVMSG) 525 526 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14=(space :width (- 29 (8))) erc-command PRIVMSG) 526 531 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 531 538 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #14# erc-command PRIVMSG) 539 540 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15=(space :width (- 29 0)) display #11# erc-command PRIVMSG) 540 545 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 545 547 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# display #11# erc-command PRIVMSG) 547 551 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #15# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n<alice> one.\n<alice> two.\n<bob> three.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 29 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 29 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #7=(space :width (- 29 (8)))) 475 480 (wrap-prefix #1# line-prefix #7#) 480 486 (wrap-prefix #1# line-prefix #7#) 487 488 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 29 0)) display #9="") 488 493 (wrap-prefix #1# line-prefix #8# display #9#) 493 495 (wrap-prefix #1# line-prefix #8# display #9#) 495 499 (wrap-prefix #1# line-prefix #8#) 500 501 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #10=(space :width (- 29 (6)))) 501 504 (wrap-prefix #1# line-prefix #10#) 504 512 (wrap-prefix #1# line-prefix #10#) 513 514 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 29 0)) display #9#) 514 517 (wrap-prefix #1# line-prefix #11# display #9#) 517 519 (wrap-prefix #1# line-prefix #11# display #9#) 519 524 (wrap-prefix #1# line-prefix #11#) 525 526 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #12=(space :width (- 29 (8)))) 526 531 (wrap-prefix #1# line-prefix #12#) 531 538 (wrap-prefix #1# line-prefix #12#) 539 540 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #13=(space :width (- 29 0)) display #9#) 540 545 (wrap-prefix #1# line-prefix #13# display #9#) 545 547 (wrap-prefix #1# line-prefix #13# display #9#) 547 551 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index a3d533c87b5..d70184724ba 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n* bob one\n<bob> two.\n* bob three\n<bob> four.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=(#7=(margin right-margin) #("[00:00]" 0 7 (display #1# invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 436 454 (erc-timestamp 1680332400 line-prefix (space :width (- 27 (18))) field erc-timestamp) 454 455 (erc-timestamp 1680332400 field erc-timestamp) 455 456 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6=(space :width (- 27 (6))) erc-command PRIVMSG) 456 459 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 459 466 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 473 (erc-timestamp 1680332400 field erc-timestamp wrap-prefix #2# line-prefix #6# display #8=(#7# #("[07:00]" 0 7 (display #8# invisible timestamp font-lock-face erc-timestamp-face)))) 474 476 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9=(space :width (- 27 (6))) erc-ctcp ACTION erc-command PRIVMSG) 476 479 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-ctcp ACTION erc-command PRIVMSG) 479 483 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #9# erc-ctcp ACTION erc-command PRIVMSG) 484 485 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10=(space :width (- 27 (6))) erc-command PRIVMSG) 485 488 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 488 494 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #10# erc-command PRIVMSG) 495 497 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11=(space :width (- 27 (2))) erc-ctcp ACTION erc-command PRIVMSG) 497 500 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11# erc-ctcp ACTION erc-command PRIVMSG) 500 506 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #11# erc-ctcp ACTION erc-command PRIVMSG) 507 508 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12=(space :width (- 27 (6))) erc-command PRIVMSG) 508 511 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG) 511 518 (erc-timestamp 1680332400 wrap-prefix #2# line-prefix #12# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n\n[Sat Apr  1 2023]\n<bob> zero.[07:00]\n* bob one\n<bob> two.\n* bob three\n<bob> four.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 436 437 (erc-msg datestamp erc-ts 1680332400 field erc-timestamp) 437 454 (field erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 455 456 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 456 459 (wrap-prefix #1# line-prefix #5#) 459 466 (wrap-prefix #1# line-prefix #5#) 466 473 (field erc-timestamp wrap-prefix #1# line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 474 475 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 475 476 (wrap-prefix #1# line-prefix #7#) 476 479 (wrap-prefix #1# line-prefix #7#) 479 483 (wrap-prefix #1# line-prefix #7#) 484 485 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #8=(space :width (- 27 0)) display #9="") 485 488 (wrap-prefix #1# line-prefix #8# display #9#) 488 490 (wrap-prefix #1# line-prefix #8# display #9#) 490 494 (wrap-prefix #1# line-prefix #8#) 495 496 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG erc-ctcp ACTION wrap-prefix #1# line-prefix #10=(space :width (- 27 (2)))) 496 497 (wrap-prefix #1# line-prefix #10#) 497 500 (wrap-prefix #1# line-prefix #10#) 500 506 (wrap-prefix #1# line-prefix #10#) 507 508 (erc-msg msg erc-ts 1680332400 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #11=(space :width (- 27 0)) display #9#) 508 511 (wrap-prefix #1# line-prefix #11# display #9#) 511 513 (wrap-prefix #1# line-prefix #11# display #9#) 513 518 (wrap-prefix #1# line-prefix #11#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 80c9e1d80f5..def97738ce6 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index e675695f660..be3e2b33cfd 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 29 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 29) line-prefix #3=(space :width (- 29 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 29 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 29 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 29) line-prefix (space :width (- 29 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 29 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 29 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index a6070c2e3ff..098257d0b49 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 25 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 25) line-prefix #3=(space :width (- 25 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 25 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 25 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 25) line-prefix (space :width (- 25 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 25 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 25 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 80c9e1d80f5..def97738ce6 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 349 350 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 2b8766c27f4..360b3dafafd 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 20 (erc-timestamp 0 line-prefix (space :width (- 27 (18))) field erc-timestamp) 20 21 (erc-timestamp 0 field erc-timestamp) 21 183 (erc-timestamp 0 wrap-prefix #2=(space :width 27) line-prefix #3=(space :width (- 27 (4)))) 183 190 (erc-timestamp 0 field erc-timestamp wrap-prefix #2# line-prefix #3# display #1=((margin right-margin) #("[00:00]" 0 7 (display #1# isearch-open-invisible timestamp invisible timestamp font-lock-face erc-timestamp-face)))) 190 191 (line-spacing 0.5) 191 192 (erc-timestamp 0 wrap-prefix #2# line-prefix #4=(space :width (- 27 (8))) erc-command PRIVMSG) 192 197 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 197 199 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 199 202 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 202 315 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 315 316 (erc-timestamp 0 erc-command PRIVMSG) 316 348 (erc-timestamp 0 wrap-prefix #2# line-prefix #4# erc-command PRIVMSG) 348 349 (line-spacing 0.5) 349 350 (erc-timestamp 0 wrap-prefix #2# line-prefix #5=(space :width (- 27 (6))) erc-command PRIVMSG) 350 353 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 353 355 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 355 360 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 360 435 (erc-timestamp 0 wrap-prefix #2# line-prefix #5# erc-command PRIVMSG) 435 436 (line-spacing 0.5) 436 437 (erc-timestamp 0 wrap-prefix #2# line-prefix #6=(space :width (- 27 0)) display #7="" erc-command PRIVMSG) 437 440 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# display #7# erc-command PRIVMSG) 440 442 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# display #7# erc-command PRIVMSG) 442 466 (erc-timestamp 0 wrap-prefix #2# line-prefix #6# erc-command PRIVMSG) 466 467 (line-spacing 0.5) 467 484 (erc-timestamp 0 wrap-prefix #2# line-prefix (space :width (- 27 (4)))) 485 502 (erc-timestamp 0 wrap-prefix #2# line-prefix (space :width (- 27 (4)))) 502 503 (line-spacing 0.5) 503 504 (erc-timestamp 0 wrap-prefix #2# line-prefix #8=(space :width (- 27 (6))) erc-command PRIVMSG) 504 507 (erc-timestamp 0 wrap-prefix #2# line-prefix #8# erc-command PRIVMSG) 507 525 (erc-timestamp 0 wrap-prefix #2# line-prefix #8# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.[00:00]\n<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n<bob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> Somebody stop me\n" 2 3 (erc-msg datestamp erc-ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #2=(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 190 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 190 191 (line-spacing 0.5) 191 192 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 192 197 (wrap-prefix #1# line-prefix #3#) 197 199 (wrap-prefix #1# line-prefix #3#) 199 202 (wrap-prefix #1# line-prefix #3#) 202 315 (wrap-prefix #1# line-prefix #3#) 316 348 (wrap-prefix #1# line-prefix #3#) 348 349 (line-spacing 0.5) 349 350 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #4=(space :width (- 27 (6)))) 350 353 (wrap-prefix #1# line-prefix #4#) 353 355 (wrap-prefix #1# line-prefix #4#) 355 360 (wrap-prefix #1# line-prefix #4#) 360 435 (wrap-prefix #1# line-prefix #4#) 435 436 (line-spacing 0.5) 436 437 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #5=(space :width (- 27 0)) display #6="") 437 440 (wrap-prefix #1# line-prefix #5# display #6#) 440 442 (wrap-prefix #1# line-prefix #5# display #6#) 442 466 (wrap-prefix #1# line-prefix #5#) 466 467 (line-spacing 0.5) 467 468 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #7=(space :width (- 27 (4)))) 468 484 (wrap-prefix #1# line-prefix #7#) 485 486 (erc-msg unknown erc-ts 0 wrap-prefix #1# line-prefix #8=(space :width (- 27 (4)))) 486 502 (wrap-prefix #1# line-prefix #8#) 502 503 (line-spacing 0.5) 503 504 (erc-msg msg erc-cmd PRIVMSG erc-ts 0 wrap-prefix #1# line-prefix #9=(space :width (- 27 (6)))) 504 507 (wrap-prefix #1# line-prefix #9#) 507 525 (wrap-prefix #1# line-prefix #9#))
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index f62b65cd170..cd3537d3c94 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 9 (erc-timestamp 0 display (#4=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 9 171 (erc-timestamp 0 wrap-prefix #1# line-prefix #2#) 172 179 (erc-timestamp 0 display (#4# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #3=(space :width (- 27 (8)))) 179 180 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 180 185 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 185 187 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 187 190 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 190 303 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 303 304 (erc-timestamp 0 erc-command PRIVMSG) 304 336 (erc-timestamp 0 wrap-prefix #1# line-prefix #3# erc-command PRIVMSG) 337 344 (erc-timestamp 0 display (#4# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #5=(space :width (- 27 (6)))) 344 345 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 345 348 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 348 350 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 350 355 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG) 355 430 (erc-timestamp 0 wrap-prefix #1# line-prefix #5# erc-command PRIVMSG))
\ No newline at end of file
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fool: to the purpose. What was done to Elbow's wife, that he hath cause to complain of? Come me to what was done to her.\n[00:00]<bob> alice: Either your unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc-msg unknown erc-ts 0 display #3=(#5=(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1=(space :width 27) line-prefix #2=(space :width (- 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #6=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #4=(space :width (- 27 (8)))) 173 179 (display #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #4#) 337 338 (erc-msg msg erc-ts 0 erc-cmd PRIVMSG display #8=(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=(space :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefix #7#))
\ No newline at end of file
-- 
2.41.0

>From fcb34a45afd872361b0dbc8e6bd92ba53b910faa Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email-masked]>
Date: Thu, 21 Sep 2023 06:54:27 -0700
Subject: [PATCH 7/7] [5.6] Add command to refill buffer with
 erc-fill-wrap-mode

* lisp/erc/erc-fill.el (erc-fill--wrap-rejigger-last-message):
New internal variable.
(erc-fill--wrap-rejigger-region,
erc-fill-wrap-refill-buffer): New command and helper function.
* test/lisp/erc/erc-fill-tests.el (erc-fill-tests--simulate-refill):
New function for approximating `erc-fill-wrap-refill-buffer' without
pauses to accept process output.
(erc-fill-wrap--merge): Assert refilling is idempotent.  (Bug#60936)
---
 lisp/erc/erc-fill.el            | 70 +++++++++++++++++++++++++++++++++
 test/lisp/erc/erc-fill-tests.el | 18 ++++++++-
 2 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 62a9597d481..8b86cf30bf4 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -543,6 +543,76 @@ erc-fill-wrap
                                `((space :width (- erc-fill--wrap-value ,len))
                                  (space :width erc-fill--wrap-value))))))
 
+(defvar erc-fill--wrap-rejigger-last-message nil
+  "Temporary working instance of `erc-fill--wrap-last-msg'.")
+
+(defun erc-fill--wrap-rejigger-region (start finish on-next repairp)
+  "Recalculate `line-prefix' from START to FINISH.
+After refilling each message, call ON-NEXT with no args.  But
+stash and restore `erc-fill--wrap-last-msg' before doing so, in
+case this module's insert hooks run by way of the process filter.
+With REPAIRP, destructively fill gaps and re-merge speakers."
+  (goto-char start)
+  (cl-assert (null erc-fill--wrap-rejigger-last-message))
+  (let (erc-fill--wrap-rejigger-last-message)
+    (while-let
+        (((< (point) finish))
+         (beg (if (get-text-property (point) 'line-prefix)
+                  (point)
+                (next-single-property-change (point) 'line-prefix)))
+         (val (get-text-property beg 'line-prefix))
+         (end (text-property-not-all beg finish 'line-prefix val)))
+      ;; If this is a left-side stamp on its own line.
+      (remove-text-properties beg (1+ end) '(line-prefix nil wrap-prefix nil))
+      (when-let ((repairp)
+                 (dbeg (text-property-not-all beg end 'display nil))
+                 ((get-text-property (1+ dbeg) 'erc-speaker))
+                 (dval (get-text-property dbeg 'display))
+                 ((equal "" dval)))
+        (remove-text-properties
+         dbeg (text-property-not-all dbeg end 'display dval) '(display)))
+      (let* ((pos (if (eq 'date-left (get-text-property beg 'erc-stamp-type))
+                      (field-beginning beg)
+                    beg))
+             (erc--msg-props (map-into (text-properties-at pos) 'hash-table))
+             (erc-stamp--current-time (gethash 'erc-ts erc--msg-props)))
+        (save-restriction
+          (narrow-to-region beg (1+ end))
+          (let ((erc-fill--wrap-last-msg erc-fill--wrap-rejigger-last-message))
+            (erc-fill-wrap)
+            (setq erc-fill--wrap-rejigger-last-message
+                  erc-fill--wrap-last-msg))))
+      (when on-next
+        (funcall on-next))
+      ;; Skip to end of message upon encountering accidental gaps
+      ;; introduced by third parties (or bugs).
+      (if-let (((/= ?\n (char-after end)))
+               (next (erc--get-inserted-msg-bounds 'end beg)))
+          (progn
+            (cl-assert (= ?\n (char-after next)))
+            (when repairp ; eol <= next
+              (put-text-property end (pos-eol) 'line-prefix val))
+            (goto-char next))
+        (goto-char end)))))
+
+(defun erc-fill-wrap-refill-buffer (repair)
+  "Recalculate all `fill-wrap' prefixes in the current buffer.
+With REPAIR, attempt to destructively fix merged properties."
+  (interactive "P")
+  (unless erc-fill-wrap-mode
+    (user-error "Module `fill-wrap' not active in current buffer."))
+  (save-excursion
+    (with-silent-modifications
+      (let* ((rep (make-progress-reporter
+                   "Rewrap" 0 (line-number-at-pos erc-insert-marker) 1))
+             (seen 0)
+             (callback (lambda ()
+                         (progress-reporter-update rep (cl-incf seen))
+                         (accept-process-output nil 0.000001))))
+        (erc-fill--wrap-rejigger-region (point-min) erc-insert-marker
+                                        callback repair)
+        (progress-reporter-done rep)))))
+
 ;; FIXME use own text property to avoid false positives.
 (defun erc-fill--wrap-merged-button-p (point)
   (equal "" (get-text-property point 'display)))
diff --git a/test/lisp/erc/erc-fill-tests.el b/test/lisp/erc/erc-fill-tests.el
index 8f0c8f9ccf4..f6c4c268017 100644
--- a/test/lisp/erc/erc-fill-tests.el
+++ b/test/lisp/erc/erc-fill-tests.el
@@ -234,6 +234,13 @@ erc-fill-wrap--monospace
        (erc-fill-tests--wrap-check-prefixes "*** " "<alice> " "<bob> ")
        (erc-fill-tests--compare "monospace-04-reset")))))
 
+(defun erc-fill-tests--simulate-refill ()
+  ;; Simulate `erc-fill-wrap-refill-buffer' synchronously and without
+  ;; a progress reporter.
+  (save-excursion
+    (with-silent-modifications
+      (erc-fill--wrap-rejigger-region (point-min) erc-insert-marker nil nil))))
+
 (ert-deftest erc-fill-wrap--merge ()
   :tags '(:unstable)
   (unless (>= emacs-major-version 29)
@@ -245,7 +252,9 @@ erc-fill-wrap--merge
      (erc-update-channel-member
       "#chan" "Dummy" "Dummy" t nil nil nil nil nil "fake" "~u" nil nil t)
 
-     ;; Set this here so that the first few messages are from 1970
+     ;; Set this here so that the first few messages are from 1970.
+     ;; Following the current date stamp, the speaker isn't merged
+     ;; even though it's continued: "<bob> zero."
      (let ((erc-fill-tests--time-vals (lambda () 1680332400)))
        (erc-fill-tests--insert-privmsg "bob" "zero.")
        (erc-fill-tests--insert-privmsg "alice" "one.")
@@ -267,7 +276,12 @@ erc-fill-wrap--merge
        (erc-fill-tests--wrap-check-prefixes
         "*** " "<alice> " "<bob> "
         "<bob> " "<alice> " "<alice> " "<bob> " "<bob> " "<Dummy> " "<Dummy> ")
-       (erc-fill-tests--compare "merge-02-right")))))
+       (erc-fill-tests--compare "merge-02-right")
+
+       (ert-info ("Command `erc-fill-wrap-refill-buffer' is idempotent")
+         (kill-buffer (pop erc-fill-tests--buffers))
+         (erc-fill-tests--simulate-refill) ; idempotent
+         (erc-fill-tests--compare "merge-02-right"))))))
 
 (ert-deftest erc-fill-wrap--merge-action ()
   :tags '(:unstable)
-- 
2.41.0