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

"J.P." <[email protected]>
Newsgroups gmane.emacs.erc.general
Message-ID <[email protected]>
v5. Add (tangentially related) patch to allow arbitrary CHANTYPES. Fix
Customize widget bug by maintaining sorted `erc-modules' value. Tag
built-in modules in `erc-modules' :initialize form. Actually respect
autoloads when looking for module mode vars. Fix old bug in
`erc-unfill-notice'.
0000-v4-v5.diff (text/x-patch, 8.8 KB)
From b60011843f6508f3a0a079795913de2d8ca7f903 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 19 Feb 2023 07:01:40 -0800
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            |  97 ++++++-----
 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                    |  97 +++++++----
 test/lisp/erc/erc-goodies-tests.el | 251 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el         | 187 +++++++++++++++++++--
 18 files changed, 649 insertions(+), 135 deletions(-)
 create mode 100644 test/lisp/erc/erc-goodies-tests.el

Interdiff:
diff --git a/lisp/erc/erc-imenu.el b/lisp/erc/erc-imenu.el
index 3b5dd988c18..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))
@@ -132,7 +133,7 @@ erc-imenu-setup
   (when (and (local-variable-p 'imenu-create-index-function)
              imenu-create-index-function)
     (setq erc-imenu--create-index-function imenu-create-index-function))
-  (setq-local imenu-create-index-function #'erc-create-imenu-index))
+  (setq imenu-create-index-function #'erc-create-imenu-index))
 
 ;;;###autoload(autoload 'erc-imenu-mode "erc-imenu" nil t)
 (define-erc-module imenu nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 6ad1303d48b..fb99482cb7e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1544,10 +1544,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
@@ -1840,12 +1844,20 @@ 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-default symbol exp))
   :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)
@@ -1857,7 +1869,14 @@ 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)))
+           (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)))
@@ -1912,7 +1931,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.5") ; FIXME sync on release
+  :package-version '(ERC . "5.6") ; FIXME sync on release
   :group 'erc)
 
 (defun erc-update-modules ()
@@ -1923,15 +1942,17 @@ erc-update-modules
 
 (defun erc--find-mode (sym)
   (setq sym (erc--normalize-module-symbol sym))
-  (let ((mode (intern-soft (concat "erc-" (symbol-name sym) "-mode"))))
-    (or 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))))
+  (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)
@@ -6859,8 +6880,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)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 816469d9894..c57d7689ed4 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)))
@@ -1254,6 +1275,17 @@ erc-tests--modules
      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)))
-- 
2.39.1
0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch (text/x-patch, 2.6 KB)
From 409b844567c1fb93ff13366317677d9b5324682b 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 d35907a1677..0c20411cb2f 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1529,10 +1529,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 40a2d2de657..956f6a5e462 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.1
0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch (text/x-patch, 4 KB)
From 0aa8258673eeb23877bbef0a3b5fab9d6f0276cb 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 cf0b734bd28..f1e568068d8 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1878,7 +1878,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.1
0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch (text/x-patch, 18.9 KB)
From 593c6b02bb1d1376a164ed91c33c2806da8ee906 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            |  56 ++++++++++++-----
 test/lisp/erc/erc-tests.el | 122 ++++++++++++++++++++++++++++++++-----
 8 files changed, 161 insertions(+), 65 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 994555acecf..3332d240f6e 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -88,40 +88,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 0c20411cb2f..debec36a3af 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1829,12 +1829,20 @@ 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-default symbol exp))
   :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)
@@ -1846,7 +1854,14 @@ 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)))
+           (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)))
@@ -1860,7 +1875,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)
@@ -1877,11 +1891,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)
@@ -1894,8 +1908,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)
@@ -1909,18 +1923,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 956f6a5e462..c3c2c9207ca 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.1
0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch (text/x-patch, 8.2 KB)
From a8b93f281f4c930b5f490e1ad47d71a8017c615e 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'.
(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: Add some forward declarations from `erc-goodies'
and remove the `require' call for `erc-goodies' at the end of the
file.
(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  | 23 +++++++++--------------
 lisp/erc/erc-ibuffer.el  |  1 +
 lisp/erc/erc-page.el     |  3 +++
 lisp/erc/erc-speedbar.el |  1 +
 lisp/erc/erc.el          | 11 ++++++-----
 5 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 05a21019042..a7b296366f6 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)
@@ -553,6 +546,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 +563,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))
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 debec36a3af..9d21cc6e90b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -134,11 +134,14 @@ erc-scripts
 
 ;; Forward declarations
 (defvar erc-message-parsed)
+(defvar erc-irccontrols-mode)
 
 (defvar tabbar--local-hlf)
 (defvar motif-version-string)
 (defvar gtk-version-string)
 
+(declare-function erc-controls-strip "erc-goodies" (str))
+
 ;; tunable connection and authentication parameters
 
 (defcustom erc-server nil
@@ -6863,8 +6866,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)
@@ -6875,7 +6876,9 @@ 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 (and (bound-and-true-p erc-irccontrols-mode)
+                                  (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))
@@ -7413,6 +7416,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.1
0005-5.6-Modify-erc-mode-map-in-module-definitions.patch (text/x-patch, 9.4 KB)
From 320394895103740fcb98e555d9c1a4c90e62ee7b 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 a7b296366f6..d7cc76455bb 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 9d21cc6e90b..ecb85d83435 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1191,7 +1191,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)
@@ -1215,6 +1214,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 c3c2c9207ca..f7a29e39ce0 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.1
0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch (text/x-patch, 16.8 KB)
From b23cf15cdbb20bcd683d0d3e4099c07cc5a1641a 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--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            |  62 +++++--
 test/lisp/erc/erc-goodies-tests.el | 251 +++++++++++++++++++++++++++++
 2 files changed, 297 insertions(+), 16 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 d7cc76455bb..cb720663ea0 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 ()
@@ -345,19 +343,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 +382,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 +402,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."
@@ -443,16 +481,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'.
@@ -532,6 +560,8 @@ erc-controls-propertize
                (list (erc-get-bg-color-face bg))
              nil))
    str)
+  (when (and fg bg (equal fg bg))
+    (put-text-property from to 'mouse-face 'erc-inverse-face str))
   str)
 
 (defun erc-toggle-interpret-controls (&optional arg)
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
new file mode 100644
index 00000000000..8cab1dd0857
--- /dev/null
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -0,0 +1,251 @@
+;;; 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" '(fg:erc-color-face0 bg:erc-color-face0)
+         '(fg:erc-color-face1 bg:erc-color-face1))
+        (erc-goodies-tests--assert-face
+         18 " World" '(fg:erc-color-face1 bg:erc-color-face1)
+         '(fg:erc-color-face0 bg:erc-color-face0)))
+      (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.1
0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch (text/x-patch, 5.2 KB)
From b60011843f6508f3a0a079795913de2d8ca7f903 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 cb720663ea0..3d018bb11e0 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 ecb85d83435..fb99482cb7e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1836,7 +1836,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
@@ -1893,6 +1893,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)
@@ -1930,6 +1931,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 f7a29e39ce0..c57d7689ed4 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.1
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.