bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el

"J.P." <[email protected]>
Newsgroups gmane.emacs.bugs,gmane.emacs.erc.general
Message-ID <[email protected]>
v7. Prevent erroneous Custom "edit" state when initializing
`erc-modules' on account of `standard-value' ordering mismatch.
0000-v6-v7.diff (text/x-patch, 2.7 KB)
From 636bec56194f964907c5cf5d37f2cb18e338e64e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 14 Mar 2023 06:14:10 -0700
Subject: [PATCH 0/7] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (7):
  [5.6] Honor arbitrary CHANTYPES in ERC
  [5.6] Copy over upstream Compat macros to erc-compat
  [5.6] Leverage loaddefs for migrating ERC modules
  [5.6] Don't require erc-goodies in erc.el
  [5.6] Modify erc-mode-map in module definitions
  [5.6] Add missing colors to erc-irccontrols-mode
  [5.6] Convert ERC's Imenu integration into proper module

 lisp/erc/erc-backend.el            |   2 +-
 lisp/erc/erc-button.el             |   6 +-
 lisp/erc/erc-common.el             |  39 +----
 lisp/erc/erc-compat.el             |  52 +++++-
 lisp/erc/erc-goodies.el            | 117 ++++++++-----
 lisp/erc/erc-ibuffer.el            |   1 +
 lisp/erc/erc-imenu.el              |  23 ++-
 lisp/erc/erc-log.el                |   8 +-
 lisp/erc/erc-match.el              |   8 +-
 lisp/erc/erc-page.el               |   4 +
 lisp/erc/erc-pcomplete.el          |   2 +
 lisp/erc/erc-services.el           |   1 +
 lisp/erc/erc-sound.el              |   1 +
 lisp/erc/erc-speedbar.el           |   1 +
 lisp/erc/erc-stamp.el              |   4 +
 lisp/erc/erc.el                    | 108 ++++++++----
 test/lisp/erc/erc-goodies-tests.el | 253 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el         | 187 +++++++++++++++++++--
 18 files changed, 675 insertions(+), 142 deletions(-)
 create mode 100644 test/lisp/erc/erc-goodies-tests.el

Interdiff:
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 1315bce2e54..e122f32d96a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1850,7 +1850,9 @@ erc-modules
                   (when-let* (((eq (car entry) 'const))
                               (s (cadddr entry))) ; (const :tag "..." ,s)
                     (put s 'erc--module s)))
-                (custom-initialize-default symbol exp))
+                (custom-initialize-reset symbol exp)
+                (put symbol 'standard-value
+                     (list (custom-quote (default-toplevel-value symbol)))))
   :set (lambda (sym val)
          ;; disable modules which have just been removed
          (when (and (boundp 'erc-modules) erc-modules val)
@@ -1873,6 +1875,7 @@ erc-modules
              (if (get v 'erc--module)
                  (push v built-in)
                (push v third-party)))
+           ;; Calling `set-default-toplevel-value' complicates testing
            (set sym (append (sort built-in #'string-lessp)
                             (nreverse third-party))))
          ;; this test is for the case where erc hasn't been loaded yet
-- 
2.39.2
0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch (text/x-patch, 2.6 KB)
From 4b86ffb90b2cb12f96588aa3a6a0a59b3c425ab1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sat, 18 Feb 2023 19:32:36 -0800
Subject: [PATCH 1/7] [5.6] Honor arbitrary CHANTYPES in ERC

* lisp/erc/erc.el (erc-channel-p): Favor "CHANTYPES" ISUPPORT item
before falling back to well known prefixes.
* test/lisp/erc/erc-tests.el (erc-channel-p): Add test.
---
 lisp/erc/erc.el            | 12 ++++++++----
 test/lisp/erc/erc-tests.el | 21 +++++++++++++++++++++
 2 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 69bdb5d71b1..dd3697ac50b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1530,10 +1530,14 @@ erc-reuse-frames
 (defun erc-channel-p (channel)
   "Return non-nil if CHANNEL seems to be an IRC channel name."
   (cond ((stringp channel)
-         (memq (aref channel 0) '(?# ?& ?+ ?!)))
-        ((and (bufferp channel) (buffer-live-p channel))
-         (with-current-buffer channel
-           (erc-channel-p (erc-default-target))))
+         (memq (aref channel 0)
+               (if-let ((types (erc--get-isupport-entry 'CHANTYPES 'single)))
+                   (append types nil)
+                 '(?# ?& ?+ ?!))))
+        ((and-let* (((bufferp channel))
+                    ((buffer-live-p channel))
+                    (target (buffer-local-value 'erc--target channel)))
+           (erc-channel-p (erc--target-string target))))
         (t nil)))
 
 ;; For the sake of compatibility, a historical quirk concerning this
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index d6c63934163..bbf3269161d 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -447,6 +447,27 @@ erc-downcase
       (should (equal (erc-downcase "Tilde~") "tilde~" ))
       (should (equal (erc-downcase "\\O/") "|o/" )))))
 
+(ert-deftest erc-channel-p ()
+  (let ((erc--isupport-params (make-hash-table))
+        erc-server-parameters)
+
+    (should (erc-channel-p "#chan"))
+    (should (erc-channel-p "##chan"))
+    (should (erc-channel-p "&chan"))
+    (should (erc-channel-p "+chan"))
+    (should (erc-channel-p "!chan"))
+    (should-not (erc-channel-p "@chan"))
+
+    (push '("CHANTYPES" . "#&@+!") erc-server-parameters)
+
+    (should (erc-channel-p "!chan"))
+    (should (erc-channel-p "#chan"))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (setq erc--target (erc--target-from-string "#chan")))
+    (should (erc-channel-p (get-buffer "#chan"))))
+  (kill-buffer "#chan"))
+
 (ert-deftest erc--valid-local-channel-p ()
   (ert-info ("Local channels not supported")
     (let ((erc--isupport-params (make-hash-table)))
-- 
2.39.2
0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch (text/x-patch, 4 KB)
From 927962b4199cff9d2f3a9852021dadd563eb5a91 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 19 Jan 2023 20:52:47 -0800
Subject: [PATCH 2/7] [5.6] Copy over upstream Compat macros to erc-compat

* lisp/erc/erc-backend: (erc--get-isupport-entry): Replace call to
`erc-compat--with-memoization' with the built-in `with-memoization'.
* lisp/erc/erc-compat.el: (erc-compat-function, erc-compat-call): Add
new macros from Compat 29.1.2.0.
(erc-compat--with-memoization): Remove because it's now provided by
Compat.  (Bug#60954.)
---
 lisp/erc/erc-backend.el |  2 +-
 lisp/erc/erc-compat.el  | 52 ++++++++++++++++++++++++++++++++++-------
 2 files changed, 44 insertions(+), 10 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 567443f5329..8c90e8eebb9 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1876,7 +1876,7 @@ erc--get-isupport-entry
 primitive value."
   (if-let* ((table (or erc--isupport-params
                        (erc-with-server-buffer erc--isupport-params)))
-            (value (erc-compat--with-memoization (gethash key table)
+            (value (with-memoization (gethash key table)
                      (when-let ((v (assoc (symbol-name key)
                                           erc-server-parameters)))
                        (if (cdr v)
diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index 5601ede27a5..605ee701850 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -34,6 +34,49 @@
 (require 'compat nil 'noerror)
 (eval-when-compile (require 'cl-lib) (require 'url-parse))
 
+;; Except for the "erc-" namespacing, these two definitions should be
+;; continuously updated to match the latest upstream ones verbatim.
+;; Although they're pretty simple, it's likely not worth checking for
+;; and possibly deferring to the non-prefixed versions.
+;;
+;; BEGIN Compat macros
+
+;;;; Macros for extended compatibility function calls
+
+(defmacro erc-compat-function (fun)
+  "Return compatibility function symbol for FUN.
+
+If the Emacs version provides a sufficiently recent version of
+FUN, the symbol FUN is returned itself.  Otherwise the macro
+returns the symbol of a compatibility function which supports the
+behavior and calling convention of the current stable Emacs
+version.  For example Compat 29.1 will provide compatibility
+functions which implement the behavior and calling convention of
+Emacs 29.1.
+
+See also `compat-call' to directly call compatibility functions."
+  (let ((compat (intern (format "compat--%s" fun))))
+    `#',(if (fboundp compat) compat fun)))
+
+(defmacro erc-compat-call (fun &rest args)
+  "Call compatibility function or macro FUN with ARGS.
+
+A good example function is `plist-get' which was extended with an
+additional predicate argument in Emacs 29.1.  The compatibility
+function, which supports this additional argument, can be
+obtained via (compat-function plist-get) and called
+via (compat-call plist-get plist prop predicate).  It is not
+possible to directly call (plist-get plist prop predicate) on
+Emacs older than 29.1, since the original `plist-get' function
+does not yet support the predicate argument.  Note that the
+Compat library never overrides existing functions.
+
+See also `compat-function' to lookup compatibility functions."
+  (let ((compat (intern (format "compat--%s" fun))))
+    `(,(if (fboundp compat) compat fun) ,@args)))
+
+;; END Compat macros
+
 ;;;###autoload(autoload 'erc-define-minor-mode "erc-compat")
 (define-obsolete-function-alias 'erc-define-minor-mode
   #'define-minor-mode "28.1")
@@ -368,15 +411,6 @@ erc-compat--29-sasl-scram--client-final-message
 
 ;;;; Misc 29.1
 
-(defmacro erc-compat--with-memoization (table &rest forms)
-  (declare (indent defun))
-  (cond
-   ((fboundp 'with-memoization)
-    `(with-memoization ,table ,@forms)) ; 29.1
-   ((fboundp 'cl--generic-with-memoization)
-    `(cl--generic-with-memoization ,table ,@forms))
-   (t `(progn ,@forms))))
-
 (defvar url-irc-function)
 
 (defun erc-compat--29-browse-url-irc (string &rest _)
-- 
2.39.2
0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch (text/x-patch, 19.1 KB)
From 27194567216a079b2150b601d0aadbb9913466d4 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sat, 4 Feb 2023 06:24:59 -0800
Subject: [PATCH 3/7] [5.6] Leverage loaddefs for migrating ERC modules

* lisp/erc/erc-common.el (erc--features-to-modules,
erc--modules-to-features, erc--module-name-migrations): Remove unused
internal functions.
(erc--normalize-module-symbol): Make aware of new symbol-prop-based
migration scheme.
* lisp/erc/erc-page.el: Add autoload cookie for module migration.
* lisp/erc/erc-services.el: Add autoload cookie for module migration.
* lisp/erc/erc-sound.el: Add autoload cookie for module migration.
* lisp/erc/erc-stamp.el: Add autoload cookie for module migration.
* lisp/erc/erc.el (erc-modules): Remove non-existent module
`hecomplete' from lineup.  Swap a couple more to maintain sorted
order.  Change `:initialize' function to tag all symbols for built-in
modules with an `erc--module' property.  Likewise, in the `:set'
function, ensure third-party modules appear after the sorted and
normalized built-ins, but in user-defined order. Do this to prevent
all modules, built-ins included, from ending up as populated form
fields for the "other" checkbox in the Customize interface.
(erc--find-mode): Add helper function for `erc--update-modules'.
(erc--update-modules): Always resolve module names and only
conditionally attempt to require corresponding features.
* test/lisp/erc/erc-tests.el
(erc-tests--module): Add manifest for asserting built-in modules and
features.  This is easier to verify visually than looking at the
custom-type set for `erc-modules'.
(erc-modules--internal-property): Add test.
(erc--normalize-module-symbol): Make more comprehensive.
(erc--find-mode): New test.  (Bug#60954.)
---
 lisp/erc/erc-common.el     |  39 ++----------
 lisp/erc/erc-page.el       |   1 +
 lisp/erc/erc-pcomplete.el  |   2 +
 lisp/erc/erc-services.el   |   1 +
 lisp/erc/erc-sound.el      |   1 +
 lisp/erc/erc-stamp.el      |   4 ++
 lisp/erc/erc.el            |  59 +++++++++++++-----
 test/lisp/erc/erc-tests.el | 122 ++++++++++++++++++++++++++++++++-----
 8 files changed, 164 insertions(+), 65 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 0279b0a0bc4..f01db9d8bbc 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -85,40 +85,13 @@ erc--target
   (contents "" :type string)
   (tags '() :type list))
 
-;; TODO move goodies modules here after 29 is released.
-(defconst erc--features-to-modules
-  '((erc-pcomplete completion pcomplete)
-    (erc-capab capab-identify)
-    (erc-join autojoin)
-    (erc-page page ctcp-page)
-    (erc-sound sound ctcp-sound)
-    (erc-stamp stamp timestamp)
-    (erc-services services nickserv))
-  "Migration alist mapping a library feature to module names.
-Keys need not be unique: a library may define more than one
-module.  Sometimes a module's downcased alias will be its
-canonical name.")
-
-(defconst erc--modules-to-features
-  (let (pairs)
-    (pcase-dolist (`(,feature . ,names) erc--features-to-modules)
-      (dolist (name names)
-        (push (cons name feature) pairs)))
-    (nreverse pairs))
-  "Migration alist mapping a module's name to its home library feature.")
-
-(defconst erc--module-name-migrations
-  (let (pairs)
-    (pcase-dolist (`(,_ ,canonical . ,rest) erc--features-to-modules)
-      (dolist (obsolete rest)
-        (push (cons obsolete canonical) pairs)))
-    pairs)
-  "Association list of obsolete module names to canonical names.")
-
+;; After deprecating 28, we can use prefixed "erc-autoload" cookies.
 (defun erc--normalize-module-symbol (symbol)
-  "Return preferred SYMBOL for `erc-modules'."
-  (setq symbol (intern (downcase (symbol-name symbol))))
-  (or (cdr (assq symbol erc--module-name-migrations)) symbol))
+  "Return preferred SYMBOL for `erc--modules'."
+  (while-let ((canonical (get symbol 'erc--module))
+              ((not (eq canonical symbol))))
+    (setq symbol canonical))
+  symbol)
 
 (defun erc--assemble-toggle (localp name ablsym mode val body)
   (let ((arg (make-symbol "arg")))
diff --git a/lisp/erc/erc-page.el b/lisp/erc/erc-page.el
index 308b3784ca5..6cba59c6946 100644
--- a/lisp/erc/erc-page.el
+++ b/lisp/erc/erc-page.el
@@ -34,6 +34,7 @@ erc-page
   "React to CTCP PAGE messages."
   :group 'erc)
 
+;;;###autoload(put 'ctcp-page 'erc--module 'page)
 ;;;###autoload(autoload 'erc-page-mode "erc-page")
 (define-erc-module page ctcp-page
   "Process CTCP PAGE requests from IRC."
diff --git a/lisp/erc/erc-pcomplete.el b/lisp/erc/erc-pcomplete.el
index 0bce856018c..7eb7431fb91 100644
--- a/lisp/erc/erc-pcomplete.el
+++ b/lisp/erc/erc-pcomplete.el
@@ -56,6 +56,8 @@ erc-pcomplete-order-nickname-completions
   "If t, order nickname completions with the most recent speakers first."
   :type 'boolean)
 
+;;;###autoload(put 'Completion 'erc--module 'completion)
+;;;###autoload(put 'pcomplete 'erc--module 'completion)
 ;;;###autoload(autoload 'erc-completion-mode "erc-pcomplete" nil t)
 (define-erc-module pcomplete Completion
   "In ERC Completion mode, the TAB key does completion whenever possible."
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index 2e6959cc3f0..5408ba405db 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -102,6 +102,7 @@ erc-nickserv-identify-mode
 	 (when (featurep 'erc-services)
 	   (erc-nickserv-identify-mode val))))
 
+;;;###autoload(put 'nickserv 'erc--module 'services)
 ;;;###autoload(autoload 'erc-services-mode "erc-services" nil t)
 (define-erc-module services nickserv
   "This mode automates communication with services."
diff --git a/lisp/erc/erc-sound.el b/lisp/erc/erc-sound.el
index 0abdbfd959c..9da9202f0cf 100644
--- a/lisp/erc/erc-sound.el
+++ b/lisp/erc/erc-sound.el
@@ -47,6 +47,7 @@
 
 (require 'erc)
 
+;;;###autoload(put 'ctcp-sound 'erc--module 'sound)
 ;;;###autoload(autoload 'erc-sound-mode "erc-sound")
 (define-erc-module sound ctcp-sound
   "In ERC sound mode, the client will respond to CTCP SOUND requests
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index 0aa1590f801..d1a1507f700 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -147,6 +147,10 @@ erc-timestamp-face
   "ERC timestamp face."
   :group 'erc-faces)
 
+;; New libraries should only autoload the minor mode for a module's
+;; preferred name (rather than its alias).
+
+;;;###autoload(put 'timestamp 'erc--module 'stamp)
 ;;;###autoload(autoload 'erc-timestamp-mode "erc-stamp" nil t)
 (define-erc-module stamp timestamp
   "This mode timestamps messages in the channel buffers."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index dd3697ac50b..a729ba14f6e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1830,12 +1830,22 @@ erc-modules
   :get (lambda (sym)
          ;; replace outdated names with their newer equivalents
          (erc-migrate-modules (symbol-value sym)))
-  :initialize #'custom-initialize-default
+  ;; Expect every built-in module to have the symbol property
+  ;; `erc--module' set to its canonical symbol (often itself).
+  :initialize (lambda (symbol exp)
+                ;; Use `cdddr' because (set :greedy t . ,entries)
+                (dolist (entry (cdddr (get 'erc-modules 'custom-type)))
+                  (when-let* (((eq (car entry) 'const))
+                              (s (cadddr entry))) ; (const :tag "..." ,s)
+                    (put s 'erc--module s)))
+                (custom-initialize-reset symbol exp)
+                (put symbol 'standard-value
+                     (list (custom-quote (default-toplevel-value symbol)))))
   :set (lambda (sym val)
          ;; disable modules which have just been removed
          (when (and (boundp 'erc-modules) erc-modules val)
            (dolist (module erc-modules)
-             (unless (member module val)
+             (unless (memq module val)
                (let ((f (intern-soft (format "erc-%s-mode" module))))
                  (when (and (fboundp f) (boundp f))
                    (when (symbol-value f)
@@ -1847,7 +1857,15 @@ erc-modules
                                           (when (symbol-value f)
                                             (funcall f 0))
                                           (kill-local-variable f)))))))))
-         (set sym val)
+         (let (built-in third-party)
+           (dolist (v val)
+             (setq v (erc--normalize-module-symbol v))
+             (if (get v 'erc--module)
+                 (push v built-in)
+               (push v third-party)))
+           ;; Calling `set-default-toplevel-value' complicates testing
+           (set sym (append (sort built-in #'string-lessp)
+                            (nreverse third-party))))
          ;; this test is for the case where erc hasn't been loaded yet
          (when (fboundp 'erc-update-modules)
            (erc-update-modules)))
@@ -1861,7 +1879,6 @@ erc-modules
            capab-identify)
     (const :tag "completion: Complete nicknames and commands (programmable)"
            completion)
-    (const :tag "hecomplete: Complete nicknames and commands (obsolete, use \"completion\")" hecomplete)
     (const :tag "dcc: Provide Direct Client-to-Client support" dcc)
     (const :tag "fill: Wrap long lines" fill)
     (const :tag "identd: Launch an identd server on port 8113" identd)
@@ -1878,11 +1895,11 @@ erc-modules
     (const :tag "networks: Provide data about IRC networks" networks)
     (const :tag "noncommands: Don't display non-IRC commands after evaluation"
            noncommands)
+    (const :tag "notifications: Desktop alerts on PRIVMSG or mentions"
+           notifications)
     (const :tag
            "notify: Notify when the online status of certain users changes"
            notify)
-    (const :tag "notifications: Send notifications on PRIVMSG or nickname mentions"
-           notifications)
     (const :tag "page: Process CTCP PAGE requests from IRC" page)
     (const :tag "readonly: Make displayed lines read-only" readonly)
     (const :tag "replace: Replace text in messages" replace)
@@ -1895,8 +1912,8 @@ erc-modules
     (const :tag "smiley: Convert smileys to pretty icons" smiley)
     (const :tag "sound: Play sounds when you receive CTCP SOUND requests"
            sound)
-    (const :tag "stamp: Add timestamps to messages" stamp)
     (const :tag "spelling: Check spelling" spelling)
+    (const :tag "stamp: Add timestamps to messages" stamp)
     (const :tag "track: Track channel activity in the mode-line" track)
     (const :tag "truncate: Truncate buffers to a certain size" truncate)
     (const :tag "unmorse: Translate morse code in messages" unmorse)
@@ -1910,18 +1927,28 @@ erc-update-modules
   (erc--update-modules)
   nil)
 
+(defun erc--find-mode (sym)
+  (setq sym (erc--normalize-module-symbol sym))
+  (if-let* ((mode (intern-soft (concat "erc-" (symbol-name sym) "-mode")))
+            ((or (boundp mode)
+                 (and (fboundp mode)
+                      (autoload-do-load (symbol-function mode) mode)))))
+      mode
+    (and (require (or (get sym 'erc--feature)
+                      (intern (concat "erc-" (symbol-name sym))))
+                  nil 'noerror)
+         (setq mode (intern-soft (concat "erc-" (symbol-name sym) "-mode")))
+         (fboundp mode)
+         mode)))
+
 (defun erc--update-modules ()
   (let (local-modes)
     (dolist (module erc-modules local-modes)
-      (require (or (alist-get module erc--modules-to-features)
-                   (intern (concat "erc-" (symbol-name module))))
-               nil 'noerror) ; some modules don't have a corresponding feature
-      (let ((mode (intern-soft (concat "erc-" (symbol-name module) "-mode"))))
-        (unless (and mode (fboundp mode))
-          (error "`%s' is not a known ERC module" module))
-        (if (custom-variable-p mode)
-            (funcall mode 1)
-          (push mode local-modes))))))
+      (if-let ((mode (erc--find-mode module)))
+          (if (custom-variable-p mode)
+              (funcall mode 1)
+            (push mode local-modes))
+        (error "`%s' is not a known ERC module" module)))))
 
 (defun erc-setup-buffer (buffer)
   "Consults `erc-join-buffer' to find out how to display `BUFFER'."
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index bbf3269161d..6ab6d9ee40f 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1224,6 +1224,77 @@ erc-handle-irc-url
     (kill-buffer "baznet")
     (kill-buffer "#chan")))
 
+(defconst erc-tests--modules
+  '( autoaway autojoin button capab-identify completion dcc fill identd
+     irccontrols keep-place list log match menu move-to-prompt netsplit
+     networks noncommands notifications notify page readonly
+     replace ring sasl scrolltobottom services smiley sound
+     spelling stamp track truncate unmorse xdcc))
+
+;; Ensure the `:initialize' function for `erc-modules' successfully
+;; tags all built-in modules with the internal property `erc--module'.
+
+(ert-deftest erc-modules--internal-property ()
+  (let (ours)
+    (mapatoms (lambda (s)
+                (when-let ((v (get s 'erc--module))
+                           ((eq v s)))
+                  (push s ours))))
+    (should (equal (sort ours #'string-lessp) erc-tests--modules))))
+
+(ert-deftest erc--normalize-module-symbol ()
+  (dolist (mod erc-tests--modules)
+    (should (eq (erc--normalize-module-symbol mod) mod)))
+  (should (eq (erc--normalize-module-symbol 'pcomplete) 'completion))
+  (should (eq (erc--normalize-module-symbol 'Completion) 'completion))
+  (should (eq (erc--normalize-module-symbol 'ctcp-page) 'page))
+  (should (eq (erc--normalize-module-symbol 'ctcp-sound) 'sound))
+  (should (eq (erc--normalize-module-symbol 'timestamp) 'stamp))
+  (should (eq (erc--normalize-module-symbol 'nickserv) 'services)))
+
+;; Worrying about which library a module comes from is mostly not
+;; worth the hassle so long as ERC can find its minor mode.  However,
+;; bugs involving multiple modules living in the same library may slip
+;; by because a module's loading problems may remain hidden on account
+;; of its place in the default ordering.
+
+(ert-deftest erc--find-mode ()
+  (let* ((package (if-let* ((found (getenv "ERC_PACKAGE_NAME"))
+                            ((string-prefix-p "erc-" found)))
+                      (intern found)
+                    'erc))
+         (prog
+          `(,@(and (featurep 'compat)
+                   `((progn
+                       (require 'package)
+                       (let ((package-load-list '((compat t) (,package t))))
+                         (package-initialize)))))
+            (require 'erc)
+            (let ((mods (mapcar #'cadddr
+                                (cdddr (get 'erc-modules 'custom-type))))
+                  moded)
+              (setq mods
+                    (sort mods (lambda (a b) (if (zerop (random 2)) a b))))
+              (dolist (mod mods)
+                (unless (keywordp mod)
+                  (push (if-let ((mode (erc--find-mode mod)))
+                            mod
+                          (list :missing mod))
+                        moded)))
+              (message "%S"
+                       (sort moded
+                             (lambda (a b)
+                               (string< (symbol-name a) (symbol-name b))))))))
+         (proc (start-process "erc--module-mode-autoloads"
+                              (current-buffer)
+                              (concat invocation-directory invocation-name)
+                              "-batch" "-Q"
+                              "-eval" (format "%S" (cons 'progn prog)))))
+    (set-process-query-on-exit-flag proc t)
+    (while (accept-process-output proc 10))
+    (goto-char (point-min))
+    (should (equal (read (current-buffer)) erc-tests--modules))))
+
 (ert-deftest erc-migrate-modules ()
   (should (equal (erc-migrate-modules '(autojoin timestamp button))
                  '(autojoin stamp button)))
@@ -1234,17 +1305,28 @@ erc--update-modules
   (let (calls
         erc-modules
         erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    ;; This `lbaz' module is unknown, so ERC looks for it via the
+    ;; symbol proerty `erc--feature' and, failing that, by
+    ;; `require'ing its "erc-" prefixed symbol.
+    (should-not (intern-soft "erc-lbaz-mode"))
+
     (cl-letf (((symbol-function 'require)
-               (lambda (s &rest _) (push s calls)))
+               (lambda (s &rest _)
+                 (when (eq s 'erc--lbaz-feature)
+                   (fset (intern "erc-lbaz-mode") ; local module
+                         (lambda (n) (push (cons 'lbaz n) calls))))
+                 (push s calls)))
 
               ;; Local modules
-              ((symbol-function 'erc-fake-bar-mode)
-               (lambda (n) (push (cons 'fake-bar n) calls)))
+              ((symbol-function 'erc-lbar-mode)
+               (lambda (n) (push (cons 'lbar n) calls)))
+              ((get 'lbaz 'erc--feature) 'erc--lbaz-feature)
 
               ;; Global modules
-              ((symbol-function 'erc-fake-foo-mode)
-               (lambda (n) (push (cons 'fake-foo n) calls)))
-              ((get 'erc-fake-foo-mode 'standard-value) 'ignore)
+              ((symbol-function 'erc-gfoo-mode)
+               (lambda (n) (push (cons 'gfoo n) calls)))
+              ((get 'erc-gfoo-mode 'standard-value) 'ignore)
               ((symbol-function 'erc-autojoin-mode)
                (lambda (n) (push (cons 'autojoin n) calls)))
               ((get 'erc-autojoin-mode 'standard-value) 'ignore)
@@ -1255,20 +1337,28 @@ erc--update-modules
                (lambda (n) (push (cons 'completion n) calls)))
               ((get 'erc-completion-mode 'standard-value) 'ignore))
 
+      (ert-info ("Unknown module")
+        (setq erc-modules '(lfoo))
+        (should-error (erc--update-modules))
+        (should (equal (pop calls) 'erc-lfoo))
+        (should-not calls))
+
       (ert-info ("Local modules")
-        (setq erc-modules '(fake-foo fake-bar))
-        (should (equal (erc--update-modules) '(erc-fake-bar-mode)))
-        ;; Bar the feature is still required but the mode is not activated
-        (should (equal (nreverse calls)
-                       '(erc-fake-foo (fake-foo . 1) erc-fake-bar)))
+        (setq erc-modules '(gfoo lbar lbaz))
+        ;; Don't expose the mode here
+        (should (equal (mapcar #'symbol-name (erc--update-modules))
+                       '("erc-lbaz-mode" "erc-lbar-mode")))
+        ;; Lbaz required because unknown.
+        (should (equal (nreverse calls) '((gfoo . 1) erc--lbaz-feature)))
+        (fmakunbound (intern "erc-lbaz-mode"))
+        (unintern (intern "erc-lbaz-mode") obarray)
         (setq calls nil))
 
-      (ert-info ("Module name overrides")
-        (setq erc-modules '(completion autojoin networks))
+      (ert-info ("Global modules") ; `pcomplete' resolved to `completion'
+        (setq erc-modules '(pcomplete autojoin networks))
         (should-not (erc--update-modules)) ; no locals
-        (should (equal (nreverse calls) '( erc-pcomplete (completion . 1)
-                                           erc-join (autojoin . 1)
-                                           erc-networks (networks . 1))))
+        (should (equal (nreverse calls)
+                       '((completion . 1) (autojoin . 1) (networks . 1))))
         (setq calls nil)))))
 
 (ert-deftest erc--merge-local-modes ()
-- 
2.39.2
0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch (text/x-patch, 9.7 KB)
From 6560102c934cb72270c1133aaae8be759c0dbf4e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 4/7] [5.6] Don't require erc-goodies in erc.el

* lisp/erc/erc-common.el: (erc--features-to-modules): Add mappings
from erc-goodies.
* lisp/erc/erc-goodies.el: Remove forward declarations.  Add
minor-mode autoloads for `scrolltobottom', `readonly',
`move-to-prompt', `keep-place', `noncommands', `irccontrols',
`smiley', and `unmorse'.  Add Local variables footer with
`generated-autoload-file'.
(erc-controls-strip): Autoload this function.
(erc-irccontrols-mode, erc-irccontrols-enable,
erc-irccontrols-disable): Add and remove key for
`erc-toggle-interpret-controls' to `erc-mode-map'.
(erc--irccontrols-on-major-mode): New helper to add or remove toggle
command from local map.
* lisp/erc/erc-ibuffer.el: Require `erc-goodies' for
`erc-control-interpret'.  The justification for the blanket `require'
is this module isn't a member of `erc-modules' by default.
* lisp/erc/erc-page.el: (erc-ctcp-query-PAGE): Require `erc-goodies'
and put forward declaration for `erc-control-interpret' atop file.
* lisp/erc/erc-speedbar.el: Require `erc-goodies' for the same reason
in erc-ibuffer.el.
* lisp/erc/erc.el: Remove `require' for `erc-goodies' at end of file
and `pp' at top of file because `pp-to-string' is autoloaded on Emacs
27.  Also remove `require's for `thingatpt', `time-date', and
`iso8601'.  They're all used sparingly and the latter two have only
been around for one major release, so their removal likely won't cause
much churn.  And `thingatpt' already has a call-site `require', so the
top-level one was redundant.  Also wrap local loaddefs `require' call
in `eval-and-compile'.
(erc-update-mode-line-buffer): Only strip control chars when
`erc-irccontrols-mode' is active.  This is arguably a minor breaking
change perhaps deserving of a NEWS entry.  (Bug#60954.)
---
 lisp/erc/erc-goodies.el  | 28 ++++++++++++++--------------
 lisp/erc/erc-ibuffer.el  |  1 +
 lisp/erc/erc-page.el     |  3 +++
 lisp/erc/erc-speedbar.el |  1 +
 lisp/erc/erc.el          | 19 +++++++++----------
 5 files changed, 28 insertions(+), 24 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 05a21019042..7ca155ef9d0 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -29,23 +29,10 @@
 
 ;;; Code:
 
-;;; Imenu support
-
 (eval-when-compile (require 'cl-lib))
-(require 'erc-common)
-
 (defvar erc-controls-highlight-regexp)
 (defvar erc-controls-remove-regexp)
-(defvar erc-input-marker)
-(defvar erc-insert-marker)
-(defvar erc-server-process)
-(defvar erc-modules)
-(defvar erc-log-p)
-
-(declare-function erc-buffer-list "erc" (&optional predicate proc))
-(declare-function erc-error "erc" (&rest args))
-(declare-function erc-extract-command-from-line "erc" (line))
-(declare-function erc-beg-of-input-line "erc" nil)
+(require 'erc)
 
 (defun erc-imenu-setup ()
   "Setup Imenu support in an ERC buffer."
@@ -65,6 +52,7 @@ erc-input-line-position
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
@@ -116,6 +104,7 @@ erc-scroll-to-bottom
 	  (recenter (or erc-input-line-position -1)))))))
 
 ;;; Make read only
+;;;###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)
@@ -131,6 +120,7 @@ erc-make-read-only
   (put-text-property (point-min) (point-max) 'rear-nonsticky t))
 
 ;;; Move to prompt when typing text
+;;;###autoload(autoload 'erc-move-to-prompt-mode "erc-goodies" nil t)
 (define-erc-module move-to-prompt nil
   "This mode causes the point to be moved to the prompt when typing text."
   ((add-hook 'erc-mode-hook #'erc-move-to-prompt-setup)
@@ -155,6 +145,7 @@ erc-move-to-prompt-setup
   (add-hook 'pre-command-hook #'erc-move-to-prompt nil t))
 
 ;;; Keep place in unvisited channels
+;;;###autoload(autoload 'erc-keep-place-mode "erc-goodies" nil t)
 (define-erc-module keep-place nil
   "Leave point above un-viewed text in other channels."
   ((add-hook 'erc-insert-pre-hook  #'erc-keep-place))
@@ -193,6 +184,7 @@ erc-noncommands-list
 If a command's function symbol is in this list, the typed command
 does not appear in the ERC buffer after the user presses ENTER.")
 
+;;;###autoload(autoload 'erc-noncommands-mode "erc-goodies" nil t)
 (define-erc-module noncommands nil
   "This mode distinguishes non-commands.
 Commands listed in `erc-insert-this' know how to display
@@ -381,6 +373,7 @@ erc-get-fg-color-face
       (intern (concat "fg:erc-color-face" (number-to-string n))))
      (t (erc-log (format "   Wrong color: %s" n)) 'default))))
 
+;;;###autoload(autoload 'erc-irccontrols-mode "erc-goodies" nil t)
 (define-erc-module irccontrols nil
   "This mode enables the interpretation of IRC control chars."
   ((add-hook 'erc-insert-modify-hook #'erc-controls-highlight)
@@ -440,6 +433,7 @@ erc-controls-interpret
                 s))
              (t s)))))
 
+;;;###autoload
 (defun erc-controls-strip (str)
   "Return a copy of STR with all IRC control characters removed."
   (when str
@@ -553,6 +547,7 @@ erc-toggle-interpret-controls
            (if erc-interpret-controls-p "ON" "OFF")))
 
 ;; Smiley
+;;;###autoload(autoload 'erc-smiley-mode "erc-goodies" nil t)
 (define-erc-module smiley nil
   "This mode translates text-smileys such as :-) into pictures.
 This requires the function `smiley-region', which is defined in
@@ -569,6 +564,7 @@ erc-smiley
     (smiley-region (point-min) (point-max))))
 
 ;; Unmorse
+;;;###autoload(autoload 'erc-unmorse-mode "erc-goodies" nil t)
 (define-erc-module unmorse nil
   "This mode causes morse code in the current channel to be unmorsed."
   ((add-hook 'erc-insert-modify-hook #'erc-unmorse))
@@ -611,3 +607,7 @@ erc-occur
 (provide 'erc-goodies)
 
 ;;; erc-goodies.el ends here
+
+;; Local Variables:
+;; generated-autoload-file: "erc-loaddefs.el"
+;; End:
diff --git a/lisp/erc/erc-ibuffer.el b/lisp/erc/erc-ibuffer.el
index 6699afe36a0..612814ac6da 100644
--- a/lisp/erc/erc-ibuffer.el
+++ b/lisp/erc/erc-ibuffer.el
@@ -32,6 +32,7 @@
 (require 'ibuffer)
 (require 'ibuf-ext)
 (require 'erc)
+(require 'erc-goodies) ; `erc-controls-interpret'
 
 (defgroup erc-ibuffer nil
   "The Ibuffer group for ERC."
diff --git a/lisp/erc/erc-page.el b/lisp/erc/erc-page.el
index 6cba59c6946..a94678e5132 100644
--- a/lisp/erc/erc-page.el
+++ b/lisp/erc/erc-page.el
@@ -30,6 +30,8 @@
 
 (require 'erc)
 
+(declare-function erc-controls-interpret "erc-goodies" (str))
+
 (defgroup erc-page nil
   "React to CTCP PAGE messages."
   :group 'erc)
@@ -70,6 +72,7 @@ erc-ctcp-query-PAGE
 This will call `erc-page-function', if defined, or it will just print
 a message and `beep'.  In addition to that, the page message is also
 inserted into the server buffer."
+  (require 'erc-goodies) ; for `erc-controls-interpret'
   (when (and erc-page-mode
 	     (string-match "PAGE\\(\\s-+.*\\)?$" msg))
     (let* ((m (match-string 1 msg))
diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index 5fca14e2365..a9443e0ea17 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -36,6 +36,7 @@
 ;;; Code:
 
 (require 'erc)
+(require 'erc-goodies)
 (require 'speedbar)
 (condition-case nil (require 'dframe) (error nil))
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a729ba14f6e..38865b46a47 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -58,17 +58,13 @@
 
 ;;; Code:
 
-(load "erc-loaddefs" 'noerror 'nomessage)
+(eval-and-compile (load "erc-loaddefs" 'noerror 'nomessage))
 
 (require 'erc-networks)
 (require 'erc-backend)
 (require 'cl-lib)
 (require 'format-spec)
-(require 'pp)
-(require 'thingatpt)
 (require 'auth-source)
-(require 'time-date)
-(require 'iso8601)
 (eval-when-compile (require 'subr-x) (require 'url-parse))
 
 (defconst erc-version "5.5"
@@ -140,6 +136,10 @@ tabbar--local-hlf
 (defvar motif-version-string)
 (defvar gtk-version-string)
 
+(declare-function decoded-time-period "time-date" (time))
+(declare-function iso8601-parse-duration "iso8601" (string))
+(declare-function word-at-point "thingatpt" (&optional no-properties))
+
 ;; tunable connection and authentication parameters
 
 (defcustom erc-server nil
@@ -3085,6 +3085,8 @@ erc--read-time-period
       (string-to-number period))
      ;; Parse as a time spec.
      (t
+      (require 'time-date)
+      (require 'iso8601)
       (let ((time (condition-case nil
                       (iso8601-parse-duration
                        (concat (cond
@@ -6867,8 +6869,6 @@ erc-format-lag-time
     (cond (lag (format "lag:%.0f" lag))
           (t ""))))
 
-;; erc-goodies is required at end of this file.
-
 ;; TODO when ERC drops Emacs 28, replace the expressions in the format
 ;; spec below with functions.
 (defun erc-update-mode-line-buffer (buffer)
@@ -6879,7 +6879,8 @@ erc-update-mode-line-buffer
                   (?m . ,(erc-format-channel-modes))
                   (?n . ,(or (erc-current-nick) ""))
                   (?N . ,(erc-format-network))
-                  (?o . ,(or (erc-controls-strip erc-channel-topic) ""))
+                  (?o . ,(or (erc-controls-strip erc-channel-topic)
+                             ""))
                   (?p . ,(erc-port-to-string erc-session-port))
                   (?s . ,(erc-format-target-and/or-server))
                   (?S . ,(erc-format-target-and/or-network))
@@ -7417,6 +7418,4 @@ erc-handle-irc-url
 
 (provide 'erc)
 
-;; FIXME this is a temporary stopgap for Emacs 29.
-(require 'erc-goodies)
 ;;; erc.el ends here
-- 
2.39.2
0005-5.6-Modify-erc-mode-map-in-module-definitions.patch (text/x-patch, 9.4 KB)
From d43379e7cdf59de22ee370f8a71cc5180840937a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 5/7] [5.6] Modify erc-mode-map in module definitions

* lisp/erc/erc-button.el (erc-button-mode, erc-button-enable,
erc-button-disable): Replace call to `erc-button-setup' with one to
`erc--modify-local-map'.  This means `erc-button-setup' is now dead
from a client perspective.
* lisp/erc/erc-goodies.el (erc-irccontrols-enable,
erc-irccontrols-disable, erc-irccontrols-mode): Bind
`erc-toggle-interpret-controls' in module definition so it's only
available when the module is active.
* lisp/erc/erc-log.el (erc-log-mode, erc-log-enable, erc-log-disable):
Move top-level `define-key' into module definition.
* lisp/erc/erc-match.el (erc-match-mode, erc-match-enable,
erc-match-disable): Move top-level `define-key' into module
definition.
* lisp/erc/erc.el (erc-mode-map): Remove C-c C-c binding for
`erc-toggle-interpret-controls'.
(erc--modify-local-map): Add helper for global modules to use when
modifying `erc-mode-map'.
* test/lisp/erc/erc-tests.el (erc--modify-local-map): Add test.
Ensure modifications to `erc-mode-map' on loading `erc' and via
`erc-mode-hook' still work.  (Bug#60954.)
---
 lisp/erc/erc-button.el     |  6 ++++--
 lisp/erc/erc-goodies.el    |  6 ++++--
 lisp/erc/erc-log.el        |  8 +++----
 lisp/erc/erc-match.el      |  8 +++----
 lisp/erc/erc.el            | 14 +++++++++++-
 test/lisp/erc/erc-tests.el | 44 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 73 insertions(+), 13 deletions(-)

diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index c28dddefa0e..1be56f5dc21 100644
--- a/lisp/erc/erc-button.el
+++ b/lisp/erc/erc-button.el
@@ -55,11 +55,11 @@ button
   ((add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 'append)
    (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 'append)
    (add-hook 'erc-complete-functions #'erc-button-next-function)
-   (add-hook 'erc-mode-hook #'erc-button-setup))
+   (erc--modify-local-map t "<backtab>" #'erc-button-previous))
   ((remove-hook 'erc-insert-modify-hook #'erc-button-add-buttons)
    (remove-hook 'erc-send-modify-hook #'erc-button-add-buttons)
    (remove-hook 'erc-complete-functions #'erc-button-next-function)
-   (remove-hook 'erc-mode-hook #'erc-button-setup)))
+   (erc--modify-local-map nil "<backtab>" #'erc-button-previous)))
 
 ;;; Variables
 
@@ -233,6 +233,8 @@ erc-button-keys-added
   "Internal variable used to keep track of whether we've added the
 global-level ERC button keys yet.")
 
+;; Maybe deprecate this function and `erc-button-keys-added' if they
+;; continue to go unused for a another version (currently 5.6).
 (defun erc-button-setup ()
   "Add ERC mode-level button movement keys.  This is only done once."
   ;; Add keys.
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 7ca155ef9d0..7ff5b1aecdf 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -377,9 +377,11 @@ erc-get-fg-color-face
 (define-erc-module irccontrols nil
   "This mode enables the interpretation of IRC control chars."
   ((add-hook 'erc-insert-modify-hook #'erc-controls-highlight)
-   (add-hook 'erc-send-modify-hook #'erc-controls-highlight))
+   (add-hook 'erc-send-modify-hook #'erc-controls-highlight)
+   (erc--modify-local-map t "C-c C-c" #'erc-toggle-interpret-controls))
   ((remove-hook 'erc-insert-modify-hook #'erc-controls-highlight)
-   (remove-hook 'erc-send-modify-hook #'erc-controls-highlight)))
+   (remove-hook 'erc-send-modify-hook #'erc-controls-highlight)
+   (erc--modify-local-map nil "C-c C-c" #'erc-toggle-interpret-controls)))
 
 (defun erc-controls-interpret (str)
    "Return a copy of STR after dealing with IRC control characters.
diff --git a/lisp/erc/erc-log.el b/lisp/erc/erc-log.el
index 2cb9031640d..a44437ddcf7 100644
--- a/lisp/erc/erc-log.el
+++ b/lisp/erc/erc-log.el
@@ -230,7 +230,8 @@ log
    ;; append, so that 'erc-initialize-log-marker runs first
    (add-hook 'erc-connect-pre-hook #'erc-log-setup-logging 'append)
    (dolist (buffer (erc-buffer-list))
-     (erc-log-setup-logging buffer)))
+     (erc-log-setup-logging buffer))
+   (erc--modify-local-map t "C-c C-l" #'erc-save-buffer-in-logs))
   ;; disable
   ((remove-hook 'erc-insert-post-hook #'erc-save-buffer-in-logs)
    (remove-hook 'erc-send-post-hook #'erc-save-buffer-in-logs)
@@ -241,9 +242,8 @@ log
    (remove-hook 'erc-part-hook #'erc-conditional-save-buffer)
    (remove-hook 'erc-connect-pre-hook #'erc-log-setup-logging)
    (dolist (buffer (erc-buffer-list))
-     (erc-log-disable-logging buffer))))
-
-(define-key erc-mode-map "\C-c\C-l" #'erc-save-buffer-in-logs)
+     (erc-log-disable-logging buffer))
+   (erc--modify-local-map nil "C-c C-l" #'erc-save-buffer-in-logs)))
 
 ;;; functionality referenced from erc.el
 (defun erc-log-setup-logging (buffer)
diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el
index 52ee5c855f3..7ec9078d493 100644
--- a/lisp/erc/erc-match.el
+++ b/lisp/erc/erc-match.el
@@ -52,8 +52,10 @@ match
 `erc-current-nick-highlight-type'.  For all these highlighting types,
 you can decide whether the entire message or only the sending nick is
 highlighted."
-  ((add-hook 'erc-insert-modify-hook #'erc-match-message 'append))
-  ((remove-hook 'erc-insert-modify-hook #'erc-match-message)))
+  ((add-hook 'erc-insert-modify-hook #'erc-match-message 'append)
+   (erc--modify-local-map t "C-c C-k" #'erc-go-to-log-matches-buffer))
+  ((remove-hook 'erc-insert-modify-hook #'erc-match-message)
+   (erc--modify-local-map nil "C-c C-k" #'erc-go-to-log-matches-buffer)))
 
 ;; Remaining customizations
 
@@ -647,8 +649,6 @@ erc-go-to-log-matches-buffer
 					(get-buffer (car buffer-cons))))))
     (switch-to-buffer buffer-name)))
 
-(define-key erc-mode-map "\C-c\C-k" #'erc-go-to-log-matches-buffer)
-
 (defun erc-hide-fools (match-type _nickuserhost _message)
  "Hide foolish comments.
 This function should be called from `erc-text-matched-hook'."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 38865b46a47..4f985da9651 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1189,7 +1189,6 @@ erc-mode-map
     (define-key map [home] #'erc-bol)
     (define-key map "\C-c\C-a" #'erc-bol)
     (define-key map "\C-c\C-b" #'erc-switch-to-buffer)
-    (define-key map "\C-c\C-c" #'erc-toggle-interpret-controls)
     (define-key map "\C-c\C-d" #'erc-input-action)
     (define-key map "\C-c\C-e" #'erc-toggle-ctcp-autoresponse)
     (define-key map "\C-c\C-f" #'erc-toggle-flood-control)
@@ -1213,6 +1212,19 @@ erc-mode-map
     map)
   "ERC keymap.")
 
+(defun erc--modify-local-map (mode &rest bindings)
+  "Modify `erc-mode-map' on behalf of a global module.
+Add or remove `key-valid-p' BINDINGS when toggling MODE."
+  (declare (indent 1))
+  (while (pcase-let* ((`(,key ,def . ,rest) bindings)
+                      (existing (keymap-lookup erc-mode-map key)))
+           (if mode
+               (when (or (not existing) (eq existing #'undefined))
+                 (keymap-set erc-mode-map key def))
+             (when (eq existing def)
+               (keymap-unset erc-mode-map key t)))
+           (setq bindings rest))))
+
 ;; Faces
 
 ; Honestly, I have a horrible sense of color and the "defaults" below
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 6ab6d9ee40f..2c5c2541bca 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -492,6 +492,50 @@ erc--target-from-string
     (should (equal (erc--target-from-string "&Bitlbee")
                    #s(erc--target-channel-local "&Bitlbee" &bitlbee)))))
 
+(ert-deftest erc--modify-local-map ()
+  (when (and (bound-and-true-p erc-irccontrols-mode)
+             (fboundp 'erc-irccontrols-mode))
+    (erc-irccontrols-mode -1))
+  (when (and (bound-and-true-p erc-match-mode)
+             (fboundp 'erc-match-mode))
+    (erc-match-mode -1))
+  (let* (calls
+         (inhibit-message noninteractive)
+         (cmd-foo (lambda () (interactive) (push 'foo calls)))
+         (cmd-bar (lambda () (interactive) (push 'bar calls))))
+
+    (ert-info ("Add non-existing")
+      (erc--modify-local-map t "C-c C-c" cmd-foo "C-c C-k" cmd-bar)
+      (with-temp-buffer
+        (set-window-buffer (selected-window) (current-buffer))
+        (use-local-map erc-mode-map)
+        (execute-kbd-macro "\C-c\C-c")
+        (execute-kbd-macro "\C-c\C-k"))
+      (should (equal calls '(bar foo))))
+    (setq calls nil)
+
+    (ert-info ("Add existing") ; Attempt to swap definitions fails
+      (erc--modify-local-map t "C-c C-c" cmd-bar "C-c C-k" cmd-foo)
+      (with-temp-buffer
+        (set-window-buffer (selected-window) (current-buffer))
+        (use-local-map erc-mode-map)
+        (execute-kbd-macro "\C-c\C-c")
+        (execute-kbd-macro "\C-c\C-k"))
+      (should (equal calls '(bar foo))))
+    (setq calls nil)
+
+    (ert-info ("Remove existing")
+      (ert-with-message-capture messages
+        (erc--modify-local-map nil "C-c C-c" cmd-foo "C-c C-k" cmd-bar)
+        (with-temp-buffer
+          (set-window-buffer (selected-window) (current-buffer))
+          (use-local-map erc-mode-map)
+          (execute-kbd-macro "\C-c\C-c")
+          (execute-kbd-macro "\C-c\C-k"))
+        (should (string-search "C-c C-c is undefined" messages))
+        (should (string-search "C-c C-k is undefined" messages))
+        (should-not calls)))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.39.2
0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch (text/x-patch, 17.9 KB)
From fa2057c5ff35b42b59b83aa6a3df003ae87aac84 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Fri, 9 Jul 2021 20:03:51 -0700
Subject: [PATCH 6/7] [5.6] Add missing colors to erc-irccontrols-mode

* lisp/erc/erc-goodies.el (erc-spoiler-face): Add new face.
(erc--controls-additional-colors): Add
remaining 16-99 colors.
(erc-get-bg-color-face, erc-get-fg-color-face): Look up colors in
table.
(erc-controls-remove-regexp, erc-controls-highlight-regexp): Convert
to `rx' forms and move above first use to eliminate intra-file forward
declarations.
(erc-controls-propertize): Support spoilers.
* test/lisp/erc/erc-goodies-tests.el: New file.  (Bug#60954.)
---
 lisp/erc/erc-goodies.el            |  77 +++++++--
 test/lisp/erc/erc-goodies-tests.el | 253 +++++++++++++++++++++++++++++
 2 files changed, 312 insertions(+), 18 deletions(-)
 create mode 100644 test/lisp/erc/erc-goodies-tests.el

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 7ff5b1aecdf..5ddacb643fd 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -30,8 +30,6 @@
 ;;; Code:
 
 (eval-when-compile (require 'cl-lib))
-(defvar erc-controls-highlight-regexp)
-(defvar erc-controls-remove-regexp)
 (require 'erc)
 
 (defun erc-imenu-setup ()
@@ -243,6 +241,12 @@ erc-inverse-face
   "ERC inverse face."
   :group 'erc-faces)
 
+(defface erc-spoiler-face
+  '((((background light)) :foreground "DimGray" :background "DimGray")
+    (((background dark)) :foreground "LightGray" :background "LightGray"))
+  "ERC spoiler face."
+  :group 'erc-faces)
+
 (defface erc-underline-face '((t :underline t))
   "ERC underline face."
   :group 'erc-faces)
@@ -345,19 +349,38 @@ bg:erc-color-face15
   "ERC face."
   :group 'erc-faces)
 
+;; https://lists.gnu.org/archive/html/emacs-erc/2021-07/msg00005.html
+(defvar erc--controls-additional-colors
+  ["#470000" "#472100" "#474700" "#324700" "#004700" "#00472c"
+   "#004747" "#002747" "#000047" "#2e0047" "#470047" "#47002a"
+   "#740000" "#743a00" "#747400" "#517400" "#007400" "#007449"
+   "#007474" "#004074" "#000074" "#4b0074" "#740074" "#740045"
+   "#b50000" "#b56300" "#b5b500" "#7db500" "#00b500" "#00b571"
+   "#00b5b5" "#0063b5" "#0000b5" "#7500b5" "#b500b5" "#b5006b"
+   "#ff0000" "#ff8c00" "#ffff00" "#b2ff00" "#00ff00" "#00ffa0"
+   "#00ffff" "#008cff" "#0000ff" "#a500ff" "#ff00ff" "#ff0098"
+   "#ff5959" "#ffb459" "#ffff71" "#cfff60" "#6fff6f" "#65ffc9"
+   "#6dffff" "#59b4ff" "#5959ff" "#c459ff" "#ff66ff" "#ff59bc"
+   "#ff9c9c" "#ffd39c" "#ffff9c" "#e2ff9c" "#9cff9c" "#9cffdb"
+   "#9cffff" "#9cd3ff" "#9c9cff" "#dc9cff" "#ff9cff" "#ff94d3"
+   "#000000" "#131313" "#282828" "#363636" "#4d4d4d" "#656565"
+   "#818181" "#9f9f9f" "#bcbcbc" "#e2e2e2" "#ffffff"])
+
 (defun erc-get-bg-color-face (n)
   "Fetches the right face for background color N (0-15)."
   (if (stringp n) (setq n (string-to-number n)))
   (if (not (numberp n))
       (prog1 'default
         (erc-error "erc-get-bg-color-face: n is NaN: %S" n))
-    (when (> n 16)
+    (when (> n 99)
       (erc-log (format "   Wrong color: %s" n))
       (setq n (mod n 16)))
     (cond
      ((and (>= n 0) (< n 16))
       (intern (concat "bg:erc-color-face" (number-to-string n))))
-     (t (erc-log (format "   Wrong color: %s" n)) 'default))))
+     ((< 15 n 99)
+      (list :background (aref erc--controls-additional-colors (- n 16))))
+     (t (erc-log (format "   Wrong color: %s" n)) '(default)))))
 
 (defun erc-get-fg-color-face (n)
   "Fetches the right face for foreground color N (0-15)."
@@ -365,13 +388,15 @@ erc-get-fg-color-face
   (if (not (numberp n))
       (prog1 'default
         (erc-error "erc-get-fg-color-face: n is NaN: %S" n))
-    (when (> n 16)
+    (when (> n 99)
       (erc-log (format "   Wrong color: %s" n))
       (setq n (mod n 16)))
     (cond
      ((and (>= n 0) (< n 16))
       (intern (concat "fg:erc-color-face" (number-to-string n))))
-     (t (erc-log (format "   Wrong color: %s" n)) 'default))))
+     ((< 15 n 99)
+      (list :foreground (aref erc--controls-additional-colors (- n 16))))
+     (t (erc-log (format "   Wrong color: %s" n)) '(default)))))
 
 ;;;###autoload(autoload 'erc-irccontrols-mode "erc-goodies" nil t)
 (define-erc-module irccontrols nil
@@ -383,6 +408,25 @@ irccontrols
    (remove-hook 'erc-send-modify-hook #'erc-controls-highlight)
    (erc--modify-local-map nil "C-c C-c" #'erc-toggle-interpret-controls)))
 
+;; These patterns were moved here to circumvent compiler warnings but
+;; otherwise translated verbatim from their original string-literal
+;; definitions (minus a small bug fix to satisfy newly added tests).
+(defvar erc-controls-remove-regexp
+  (rx (or ?\C-b ?\C-\] ?\C-_ ?\C-v ?\C-g ?\C-o
+          (: ?\C-c (? (any "0-9")) (? (any "0-9"))
+             (? (group ?, (any "0-9") (? (any "0-9")))))))
+  "Regular expression matching control characters to remove.")
+
+;; Before the change to `rx', group 3 used to be a sibling of group 2.
+;; This was assumed to be a bug.  A few minor simplifications were
+;; also performed.  If incorrect, please admonish.
+(defvar erc-controls-highlight-regexp
+  (rx (group (or ?\C-b ?\C-\] ?\C-v ?\C-_ ?\C-g ?\C-o
+                 (: ?\C-c (? (group (** 1 2 (any "0-9")))
+                             (? (group ?, (group (** 1 2 (any "0-9")))))))))
+      (group (* (not (any ?\C-b ?\C-c ?\C-g ?\n ?\C-o ?\C-v ?\C-\] ?\C-_)))))
+  "Regular expression matching control chars to highlight.")
+
 (defun erc-controls-interpret (str)
    "Return a copy of STR after dealing with IRC control characters.
 See `erc-interpret-controls-p' and `erc-interpret-mirc-color' for options."
@@ -444,16 +488,6 @@ erc-controls-strip
         (setq s (replace-match "" nil nil s)))
       s)))
 
-(defvar erc-controls-remove-regexp
-  "\C-b\\|\C-]\\|\C-_\\|\C-v\\|\C-g\\|\C-o\\|\C-c[0-9]?[0-9]?\\(,[0-9][0-9]?\\)?"
-  "Regular expression which matches control characters to remove.")
-
-(defvar erc-controls-highlight-regexp
-  (concat "\\(\C-b\\|\C-]\\|\C-v\\|\C-_\\|\C-g\\|\C-o\\|"
-          "\C-c\\([0-9][0-9]?\\)?\\(,\\([0-9][0-9]?\\)\\)?\\)"
-          "\\([^\C-b\C-]\C-v\C-_\C-c\C-g\C-o\n]*\\)")
-  "Regular expression which matches control chars and the text to highlight.")
-
 (defun erc-controls-highlight ()
   "Highlight IRC control chars in the buffer.
 This is useful for `erc-insert-modify-hook' and `erc-send-modify-hook'.
@@ -510,6 +544,13 @@ erc-controls-propertize
   "Prepend properties from IRC control characters between FROM and TO.
 If optional argument STR is provided, apply to STR, otherwise prepend properties
 to a region in the current buffer."
+  (if (and fg bg (equal fg bg))
+      (progn
+        (setq fg 'erc-spoiler-face
+              bg nil)
+        (put-text-property from to 'mouse-face 'erc-inverse-face str))
+    (when fg (setq fg (erc-get-fg-color-face fg)))
+    (when bg (setq bg (erc-get-bg-color-face bg))))
   (font-lock-prepend-text-property
    from
    to
@@ -527,10 +568,10 @@ erc-controls-propertize
                '(erc-underline-face)
              nil)
            (if fg
-               (list (erc-get-fg-color-face fg))
+               (list fg)
              nil)
            (if bg
-               (list (erc-get-bg-color-face bg))
+               (list bg)
              nil))
    str)
   str)
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
new file mode 100644
index 00000000000..46fcf82401b
--- /dev/null
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -0,0 +1,253 @@
+;;; erc-goodies-tests.el --- Tests for erc-goodies  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;; Code:
+(require 'ert-x)
+(require 'erc-goodies)
+(declare-function erc--initialize-markers "erc" (old-point continued) t)
+
+(defun erc-goodies-tests--assert-face (beg end-str present &optional absent)
+  (setq beg (+ beg (point-min)))
+  (let ((end (+ beg (1- (length end-str)))))
+    (while (and beg (< beg end))
+      (let* ((val (get-text-property beg 'font-lock-face))
+             (ft (flatten-tree (ensure-list val))))
+        (dolist (p (ensure-list present))
+          (if (consp p)
+              (should (member p val))
+            (should (memq p ft))))
+        (dolist (a (ensure-list absent))
+          (if (consp a)
+              (should-not (member a val))
+            (should-not (memq a ft))))
+        (setq beg (text-property-not-all beg (point-max)
+                                         'font-lock-face val))))))
+
+;; These are from the "Examples" section of
+;; https://modern.ircdocs.horse/formatting.html
+
+(ert-deftest erc-controls-highlight--examples ()
+  ;; FIXME remove after adding
+  (unless (fboundp 'erc--initialize-markers)
+    (ert-skip "Missing required function"))
+  (should (eq t erc-interpret-controls-p))
+  (let ((erc-insert-modify-hook '(erc-controls-highlight))
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq-local erc-interpret-mirc-color t)
+      (erc--initialize-markers (point) nil)
+
+      (let* ((m "I love \C-c3IRC!\C-c It is the \C-c7best protocol ever!")
+             (msg (erc-format-privmessage "bob" m nil t)))
+        (erc-display-message nil nil (current-buffer) msg))
+      (forward-line -1)
+      (should (search-forward "<bob> " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 "I love" 'erc-default-face 'fg:erc-color-face3)
+        (erc-goodies-tests--assert-face
+         7 " IRC!" 'fg:erc-color-face3)
+        (erc-goodies-tests--assert-face
+         11 " It is the " 'erc-default-face 'fg:erc-color-face7)
+        (erc-goodies-tests--assert-face
+         22 "best protocol ever!" 'fg:erc-color-face7))
+
+      (let* ((m "This is a \C-]\C-c13,9cool \C-cmessage")
+             (msg (erc-format-privmessage "alice" m nil t)))
+        (erc-display-message nil nil (current-buffer) msg))
+      (should (search-forward "<alice> " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 "this is a " 'erc-default-face 'erc-italic-face)
+        (erc-goodies-tests--assert-face
+         10 "cool " '(erc-italic-face fg:erc-color-face13 bg:erc-color-face9))
+        (erc-goodies-tests--assert-face
+         15 "message" 'erc-italic-face
+         '(fg:erc-color-face13 bg:erc-color-face9)))
+
+      (let* ((m "IRC \C-bis \C-c4,12so \C-cgreat\C-o!")
+             (msg (erc-format-privmessage "bob" m nil t)))
+        (erc-display-message nil nil (current-buffer) msg))
+      (should (search-forward "<bob> " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 "IRC " 'erc-default-face 'erc-bold-face)
+        (erc-goodies-tests--assert-face
+         4 "is " 'erc-bold-face '(fg:erc-color-face4 bg:erc-color-face12))
+        (erc-goodies-tests--assert-face
+         7 "so " '(erc-bold-face fg:erc-color-face4 bg:erc-color-face12))
+        (erc-goodies-tests--assert-face
+         10 "great" 'erc-bold-face '(fg:erc-color-face4 bg:erc-color-face12))
+        (erc-goodies-tests--assert-face
+         15 "!" 'erc-default-face 'erc-bold-face))
+
+      (let* ((m (concat "Rules: Don't spam 5\C-c13,8,6\C-c,7,8, "
+                        "and especially not \C-b9\C-b\C-]!"))
+             (msg (erc-format-privmessage "alice" m nil t)))
+        (erc-display-message nil nil (current-buffer) msg))
+      (should (search-forward "<alice> " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 "Rules: Don't spam 5" 'erc-default-face
+         '(fg:erc-color-face13 bg:erc-color-face8))
+        (erc-goodies-tests--assert-face
+         19 ",6" '(fg:erc-color-face13 bg:erc-color-face8))
+        (erc-goodies-tests--assert-face
+         21 ",7,8, and especially not " 'erc-default-face
+         '(fg:erc-color-face13 bg:erc-color-face8 erc-bold-face))
+        (erc-goodies-tests--assert-face
+         44 "9" 'erc-bold-face 'erc-italic-face)
+        (erc-goodies-tests--assert-face
+         45 "!" 'erc-italic-face 'erc-bold-face))
+
+      (when noninteractive
+        (kill-buffer)))))
+
+;; Like the test above, this is most intuitive when run interactively.
+;; Hovering over the redacted area should reveal its underlying text
+;; in a high-contrast face.
+
+(ert-deftest erc-controls-highlight--inverse ()
+  ;; FIXME remove after adding
+  (unless (fboundp 'erc--initialize-markers)
+    (ert-skip "Missing required function"))
+  (should (eq t erc-interpret-controls-p))
+  (let ((erc-insert-modify-hook '(erc-controls-highlight))
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq-local erc-interpret-mirc-color t)
+      (erc--initialize-markers (point) nil)
+
+      (let* ((m "Spoiler: \C-c0,0Hello\C-c1,1World!")
+             (msg (erc-format-privmessage "bob" m nil t)))
+        (erc-display-message nil nil (current-buffer) msg))
+      (forward-line -1)
+      (should (search-forward "<bob> " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (should (eq (get-text-property (+ 9 (point)) 'mouse-face)
+                    'erc-inverse-face))
+        (should (eq (get-text-property (1- (pos-eol)) 'mouse-face)
+                    'erc-inverse-face))
+        (erc-goodies-tests--assert-face
+         0 "Spoiler: " 'erc-default-face
+         '(fg:erc-color-face0 bg:erc-color-face0))
+        (erc-goodies-tests--assert-face
+         9 "Hello" '(erc-spoiler-face)
+         '( fg:erc-color-face0 bg:erc-color-face0
+            fg:erc-color-face1 bg:erc-color-face1))
+        (erc-goodies-tests--assert-face
+         18 " World" '(erc-spoiler-face)
+         '( fg:erc-color-face0 bg:erc-color-face0
+            fg:erc-color-face1 bg:erc-color-face1 )))
+      (when noninteractive
+        (kill-buffer)))))
+
+(defvar erc-goodies-tests--motd
+  ;; This is from ergo's MOTD
+  '((":- - this is \2bold text\17.")
+    (":- - this is \35italics text\17.")
+    (":- - this is \0034red\3 and \0032blue\3 text.")
+    (":- - this is \0034,12red text with a light blue background\3.")
+    (":- - this is a normal escaped dollarsign: $")
+    (":- ")
+    (":- "
+     "\0031,0 00 \0030,1 01 \0030,2 02 \0030,3 03 "
+     "\0031,4 04 \0030,5 05 \0030,6 06 \0031,7 07 ")
+    (":- "
+     "\0031,8 08 \0031,9 09 \0030,10 10 \0031,11 11 "
+     "\0030,12 12 \0031,13 13 \0031,14 14 \0031,15 15 ")
+    (":- ")
+    (":- "
+     "\0030,16 16 \0030,17 17 \0030,18 18 \0030,19 19 "
+     "\0030,20 20 \0030,21 21 \0030,22 22 \0030,23 23 "
+     "\0030,24 24 \0030,25 25 \0030,26 26 \0030,27 27 ")
+    (":- "
+     "\0030,28 28 \0030,29 29 \0030,30 30 \0030,31 31 "
+     "\0030,32 32 \0030,33 33 \0030,34 34 \0030,35 35 "
+     "\0030,36 36 \0030,37 37 \0030,38 38 \0030,39 39 ")
+    (":- "
+     "\0030,40 40 \0030,41 41 \0030,42 42 \0030,43 43 "
+     "\0030,44 44 \0030,45 45 \0030,46 46 \0030,47 47 "
+     "\0030,48 48 \0030,49 49 \0030,50 50 \0030,51 51 ")
+    (":- "
+     "\0030,52 52 \0030,53 53 \0031,54 54 \0031,55 55 "
+     "\0031,56 56 \0031,57 57 \0031,58 58 \0030,59 59 "
+     "\0030,60 60 \0030,61 61 \0030,62 62 \0030,63 63 ")
+    (":- "
+     "\0030,64 64 \0031,65 65 \0031,66 66 \0031,67 67 "
+     "\0031,68 68 \0031,69 69 \0031,70 70 \0031,71 71 "
+     "\0030,72 72 \0030,73 73 \0030,74 74 \0030,75 75 ")
+    (":- "
+     "\0031,76 76 \0031,77 77 \0031,78 78 \0031,79 79 "
+     "\0031,80 80 \0031,81 81 \0031,82 82 \0031,83 83 "
+     "\0031,84 84 \0031,85 85 \0031,86 86 \0031,87 87 ")
+    (":- "
+     "\0030,88 88 \0030,89 89 \0030,90 90 \0030,91 91 "
+     "\0030,92 92 \0030,93 93 \0030,94 94 \0030,95 95 "
+     "\0031,96 96 \0031,97 97 \0031,98 98 \399,99 99 ")
+    (":- ")))
+
+(ert-deftest erc-controls-highlight--motd ()
+  ;; FIXME remove after adding
+  (unless (fboundp 'erc--initialize-markers)
+    (ert-skip "Missing required function"))
+  (should (eq t erc-interpret-controls-p))
+  (let ((erc-insert-modify-hook '(erc-controls-highlight))
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq-local erc-interpret-mirc-color t)
+      (erc--initialize-markers (point) nil)
+
+      (dolist (parts erc-goodies-tests--motd)
+        (erc-display-message nil 'notice (current-buffer) (string-join parts)))
+
+      ;; Spot check
+      (goto-char (point-min))
+      (should (search-forward " 16 " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 " 17 " '(fg:erc-color-face0 (:background "#472100")))
+        (erc-goodies-tests--assert-face
+         4 " 18 " '(fg:erc-color-face0 (:background "#474700"))
+         '((:background "#472100"))))
+
+      (should (search-forward " 71 " nil t))
+      (save-restriction
+        (narrow-to-region (point) (pos-eol))
+        (erc-goodies-tests--assert-face
+         0 " 72 " '(fg:erc-color-face0 (:background "#5959ff")))
+        (erc-goodies-tests--assert-face
+         4 " 73 " '(fg:erc-color-face0 (:background "#c459ff"))
+         '((:background "#5959ff"))))
+
+      (goto-char (point-min))
+      (when noninteractive
+        (kill-buffer)))))
+
+;;; erc-goodies-tests.el ends here
-- 
2.39.2
0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch (text/x-patch, 5.2 KB)
From 636bec56194f964907c5cf5d37f2cb18e338e64e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 7/7] [5.6] Convert ERC's Imenu integration into proper module

TODO: add news item once a section for 5.6 has been added.

* lisp/erc/erc-goodies.el: Don't add Imenu hooks to `erc-mode-hook' at
top level. Remove autoload for `erc-create-imenu-index' because it
already exists in the `erc-imenu' library.
(erc-imenu-setup) Move to erc-imenu.
* lisp/erc/erc-imenu.el (erc-unfill-notice): Allow modifications to
read-only text.  Thanks to Yusef Aslam for reporting this bug.
(erc-imenu-setup): Move here from goodies.
(erc-imenu-mode, erc-imenu-enable, erc-imenu-disable): Create new
ERC module for Imenu.
* lisp/erc/erc.el (erc-modules): Add `imenu' to default value and
create menu item.  Update package-version.
* test/lisp/erc/erc-tests.el (erc-tests--modules): Add
`imenu'.  (Bug#60954.)
---
 lisp/erc/erc-goodies.el    |  6 ------
 lisp/erc/erc-imenu.el      | 23 ++++++++++++++++++++++-
 lisp/erc/erc.el            |  4 +++-
 test/lisp/erc/erc-tests.el |  2 +-
 4 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 5ddacb643fd..7ea6c42ec65 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -32,12 +32,6 @@
 (eval-when-compile (require 'cl-lib))
 (require 'erc)
 
-(defun erc-imenu-setup ()
-  "Setup Imenu support in an ERC buffer."
-  (setq-local imenu-create-index-function #'erc-create-imenu-index))
-
-(add-hook 'erc-mode-hook #'erc-imenu-setup)
-(autoload 'erc-create-imenu-index "erc-imenu" "Imenu index creation function")
 
 ;;; Automatically scroll to bottom
 (defcustom erc-input-line-position nil
diff --git a/lisp/erc/erc-imenu.el b/lisp/erc/erc-imenu.el
index 6223cd3d06f..526afd32249 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -52,7 +52,8 @@ erc-unfill-notice
 			 (forward-line 1)
 			 (looking-at "    "))
 		  (forward-line 1))
-		(end-of-line) (point)))))
+		(end-of-line) (point))))
+	(inhibit-read-only t))
     (with-temp-buffer
       (insert str)
       (goto-char (point-min))
@@ -124,6 +125,26 @@ erc-create-imenu-index
 				  index-alist))
     index-alist))
 
+(defvar-local erc-imenu--create-index-function nil
+  "Previous local value of `imenu-create-index-function', if any.")
+
+(defun erc-imenu-setup ()
+  "Wire up support for Imenu in an ERC buffer."
+  (when (and (local-variable-p 'imenu-create-index-function)
+             imenu-create-index-function)
+    (setq erc-imenu--create-index-function imenu-create-index-function))
+  (setq imenu-create-index-function #'erc-create-imenu-index))
+
+;;;###autoload(autoload 'erc-imenu-mode "erc-imenu" nil t)
+(define-erc-module imenu nil
+  "Simple Imenu integration for ERC."
+  ((add-hook 'erc-mode-hook #'erc-imenu-setup))
+  ((remove-hook 'erc-mode-hook #'erc-imenu-setup)
+   (erc-with-all-buffers-of-server erc-server-process nil
+     (when erc-imenu--create-index-function
+       (setq imenu-create-index-function erc-imenu--create-index-function)
+       (kill-local-variable 'erc-imenu--create-index-function)))))
+
 (provide 'erc-imenu)
 
 ;;; erc-imenu.el ends here
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4f985da9651..e122f32d96a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1834,7 +1834,7 @@ erc-migrate-modules
 
 (defcustom erc-modules '(netsplit fill button match track completion readonly
                                   networks ring autojoin noncommands irccontrols
-                                  move-to-prompt stamp menu list)
+                                  move-to-prompt stamp menu list imenu)
   "A list of modules which ERC should enable.
 If you set the value of this without using `customize' remember to call
 \(erc-update-modules) after you change it.  When using `customize', modules
@@ -1894,6 +1894,7 @@ erc-modules
     (const :tag "dcc: Provide Direct Client-to-Client support" dcc)
     (const :tag "fill: Wrap long lines" fill)
     (const :tag "identd: Launch an identd server on port 8113" identd)
+    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "irccontrols: Highlight or remove IRC control characters"
            irccontrols)
     (const :tag "keep-place: Leave point above un-viewed text" keep-place)
@@ -1931,6 +1932,7 @@ erc-modules
     (const :tag "unmorse: Translate morse code in messages" unmorse)
     (const :tag "xdcc: Act as an XDCC file-server" xdcc)
     (repeat :tag "Others" :inline t symbol))
+  :package-version '(ERC . "5.6") ; FIXME sync on release
   :group 'erc)
 
 (defun erc-update-modules ()
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2c5c2541bca..3e844f45f1a 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1270,7 +1270,7 @@ erc-handle-irc-url
 
 (defconst erc-tests--modules
   '( autoaway autojoin button capab-identify completion dcc fill identd
-     irccontrols keep-place list log match menu move-to-prompt netsplit
+     imenu irccontrols keep-place list log match menu move-to-prompt netsplit
      networks noncommands notifications notify page readonly
      replace ring sasl scrolltobottom services smiley sound
      spelling stamp track truncate unmorse xdcc))
-- 
2.39.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.