unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#60954: 29.0.60; ERC 5.4.1: loading ERC clobbers customizations to erc-mode-hook
       [not found] <87pmb9wyz6.fsf@neverwas.me>
@ 2023-01-20  7:16 ` Eli Zaretskii
       [not found] ` <83cz79oeu7.fsf@gnu.org>
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: Eli Zaretskii @ 2023-01-20  7:16 UTC (permalink / raw)
  To: J.P.; +Cc: 60954, emacs-erc

> Cc: emacs-erc@gnu.org
> From: "J.P." <jp@neverwas.me>
> Date: Thu, 19 Jan 2023 21:34:37 -0800
> 
> I see two basic avenues of attack here. The first is a pretty safe
> stopgap and the second a slightly riskier comprehensive approach that
> should have been on the books the moment that bug was closed:
> 
>   hack: partially revert a tiny hunk from the commit above
> 
>   fix:  don't require goodies at all and instead update the module
>         mapping data and add all necessary autoloads and forward
>         declarations
> 
> Implementations of both are attached. (I didn't bother updating the
> required Compat version in the second patch, but that'd also need to
> accompany the change set.) For now, I propose we take the shorter route
> but with an eye toward revisiting the issue soon after the next (ERC)
> release. If anyone has an opinion here, now would be the time to speak
> up.

Your proposal is fine with me, thanks.





^ permalink raw reply	[flat|nested] 10+ messages in thread

* bug#60954: 29.0.60; ERC 5.4.1: loading ERC clobbers customizations to erc-mode-hook
       [not found] ` <83cz79oeu7.fsf@gnu.org>
@ 2023-01-20 14:14   ` J.P.
  0 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-01-20 14:14 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 60954, emacs-erc

Eli Zaretskii <eliz@gnu.org> writes:

>> I see two basic avenues of attack here. The first is a pretty safe
>> stopgap and the second a slightly riskier comprehensive approach that
>> should have been on the books the moment that bug was closed:
>> 
>>   hack: partially revert a tiny hunk from the commit above
>> 
>>   fix:  don't require goodies at all and instead update the module
>>         mapping data and add all necessary autoloads and forward
>>         declarations
>> 
>> Implementations of both are attached. (I didn't bother updating the
>> required Compat version in the second patch, but that'd also need to
>> accompany the change set.) For now, I propose we take the shorter route
>> but with an eye toward revisiting the issue soon after the next (ERC)
>> release. If anyone has an opinion here, now would be the time to speak
>> up.
>
> Your proposal is fine with me, thanks.

Great, thanks. I went ahead and added the lighter change to the release
branch.

Leaving this open until the rest is settled (whenever that may be).





^ permalink raw reply	[flat|nested] 10+ messages in thread

* bug#60954: 29.0.60; ERC 5.4.1: loading ERC clobbers customizations to erc-mode-hook
       [not found] <87pmb9wyz6.fsf@neverwas.me>
  2023-01-20  7:16 ` bug#60954: 29.0.60; ERC 5.4.1: loading ERC clobbers customizations to erc-mode-hook Eli Zaretskii
       [not found] ` <83cz79oeu7.fsf@gnu.org>
@ 2023-01-20 14:15 ` J.P.
  2023-01-21 15:03 ` bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el J.P.
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-01-20 14:15 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 744 bytes --]

"J.P." <jp@neverwas.me> writes:

> I see two basic avenues of attack here. The first is a pretty safe
> stopgap and the second a slightly riskier comprehensive approach that
> should have been on the books the moment that bug was closed:
>
>   hack: partially revert a tiny hunk from the commit above
>
>   fix:  don't require goodies at all and instead update the module
>         mapping data and add all necessary autoloads and forward
>         declarations

Under "fix," I forgot to mention also couching any top-level `add-hook'
calls in module-setup and teardown functions. Of particular concern are
those that mutate user options. Attached is an addition to the (5.6
portion of the) patch set that does this for the Imenu integration.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0004-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 4344 bytes --]

From e7507ed0777a1b346e0161399b077bc0904f1c80 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 4/4] [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-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.
---
 lisp/erc/erc-goodies.el |  8 --------
 lisp/erc/erc-imenu.el   | 19 +++++++++++++++++++
 lisp/erc/erc.el         |  4 +++-
 3 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 0bf4bb9537c..f6c1376bb58 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -29,8 +29,6 @@
 
 ;;; Code:
 
-;;; Imenu support
-
 (eval-when-compile (require 'cl-lib))
 (require 'erc-common)
 
@@ -48,12 +46,6 @@ erc-server-process
 (declare-function erc-extract-command-from-line "erc" (line))
 (declare-function erc-beg-of-input-line "erc" nil)
 
-(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..2514ffcc4d3 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -124,6 +124,25 @@ 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-local imenu-create-index-function #'erc-create-imenu-index))
+
+(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 007f70b8671..0d6634d38a9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1819,7 +1819,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
@@ -1864,6 +1864,7 @@ erc-modules
     (const :tag "identd: Launch an identd server on port 8113" identd)
     (const :tag "irccontrols: Highlight or remove IRC control characters"
            irccontrols)
+    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "keep-place: Leave point above un-viewed text" keep-place)
     (const :tag "list: List channels in a separate buffer" list)
     (const :tag "log: Save buffers in logs" log)
@@ -1899,6 +1900,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
   :group 'erc)
 
 (defun erc-update-modules ()
-- 
2.38.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (2 preceding siblings ...)
  2023-01-20 14:15 ` J.P.
@ 2023-01-21 15:03 ` J.P.
  2023-01-31 15:27 ` J.P.
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-01-21 15:03 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 3788 bytes --]

"J.P." <jp@neverwas.me> writes:

> diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
> index 05a21019042..0bf4bb9537c 100644
> --- a/lisp/erc/erc-goodies.el
> +++ b/lisp/erc/erc-goodies.el
> @@ -38,9 +38,10 @@ erc-controls-highlight-regexp
> [...]
>  (declare-function erc-buffer-list "erc" (&optional predicate proc))
>  (declare-function erc-error "erc" (&rest args))
> @@ -384,9 +385,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)
> +   (define-key erc-mode-map "\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-compat-call define-key erc-mode-map "\C-c\C-c" nil t)))

Actually, messing with `erc-mode-map' like this is sure to break
configs. Snippets like

  (with-eval-after-load 'erc
    (define-key erc-mode-map (kbd "RET") nil)
    (define-key erc-mode-map (kbd "C-c C-c") #'erc-send-current-line))

are fairly common [1]. Of course, mutating `erc-mode-map' is prevalent
throughout ERC [2], but we probably shouldn't perpetuate the practice.
If this were a brand new module, we could just give it its own
minor-mode map. But doing so now might just serve to confuse.

Here are two possibilities that seem relatively benign:

  1. Leave the original binding in `erc-mode-map' but autoload its
     command, `erc-toggle-interpret-controls', and make it smart enough
     to enable `erc-irccontrols-mode' when necessary and maybe print a
     message explaining why.

  2. Mutate the local map when the module's loaded, but only do so
     conditionally, by first checking to see if the key is taken
     (locally or in `erc-mode-map'). Likewise, only remove the binding
     if it's set to `erc-toggle-interpret-controls' locally.

Door #1 is more conservative because it introduces zero churn, AFAICT.
But without any plan for deprecation, it's tantamount to kicking the can
down the road. Door #2 keeps things more compartmentalized and walled
off (ERC does tout itself as being modular, after all), but there's a
nonzero chance of churn in some cases, for example, when

  - someone sets a binding for `erc-mode-map' after the module is loaded
  - some map depends on `erc-mode-map' as a parent

Otherwise, conditionally updating the current local map seems to have
the same functional effect here as using a minor-mode map and
conditionally overriding that in all ERC buffers (since this is a global
module).

The main downside I see for #2 is that it adds yet another facet of
complexity to an already cluttered landscape, seeing as minor-mode maps
are likely to become more common with the introduction of local modules.
Perhaps of less concern is that there's no precedent for it (in ERC).
That said, if we were to go bold and convert all `erc-mode-map'
mutations across all ERC libraries, it would at least offer some sense
of consistency and predictability going forward. (I suspect there may be
other downsides I've not yet considered, though, so if you're privy to
any, please do share.)

Because #1 is pretty easy to visualize, I've gone ahead and provided a
POC of #2 (for the `irccontrols' module only) in the third patch below.
Anyone interested, please have a look.

Thanks.


[1] See (info "(erc) Sample Configuration").

[2] See `erc-button-setup' and the modules toggles for `erc-sound' and
    `erc-ring' and top-level forms in erc-match.el and erc-log.el.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v1-v2.diff --]
[-- Type: text/x-patch, Size: 2242 bytes --]

From a8bd3a2964babe131cb6c04dd14d7b537e1a90b2 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 21 Jan 2023 06:35:53 -0800
Subject: [PATCH 0/4] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (4):
  Don't load erc-goodies atop erc.el
  [5.6] Copy over upstream Compat macros to erc-compat
  [5.6] Don't require erc-goodies in erc.el
  [5.6] Convert ERC's Imenu integration into proper module

 lisp/erc/erc-backend.el |  2 +-
 lisp/erc/erc-common.el  | 10 ++++++++-
 lisp/erc/erc-compat.el  | 49 +++++++++++++++++++++++++++++++++--------
 lisp/erc/erc-goodies.el | 31 ++++++++++++++++----------
 lisp/erc/erc-imenu.el   | 19 ++++++++++++++++
 lisp/erc/erc.el         | 13 +++++++----
 6 files changed, 97 insertions(+), 27 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index f6c1376bb58..01e51df0481 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -378,10 +378,22 @@ irccontrols
   "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)
-   (define-key erc-mode-map "\C-c\C-c" #'erc-toggle-interpret-controls))
+   (add-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode))
   ((remove-hook 'erc-insert-modify-hook #'erc-controls-highlight)
    (remove-hook 'erc-send-modify-hook #'erc-controls-highlight)
-   (erc-compat-call define-key erc-mode-map "\C-c\C-c" nil t)))
+   (remove-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode)
+   (erc-with-all-buffers-of-server nil nil
+      (erc--irccontrols-on-major-mode))))
+
+(defun erc--irccontrols-on-major-mode ()
+  ;; FIXME all these keymap-* functions require Compat 29
+  (if erc-irccontrols-mode
+      ;; Interrogate composed view of local map and `erc-mode-map'
+      (unless (keymap-lookup (current-local-map) "C-c C-c")
+        (keymap-local-set "C-c C-c" #'erc-toggle-interpret-controls))
+    (when (eq (keymap-local-lookup "C-c C-c")
+              #'erc-toggle-interpret-controls)
+      (keymap-local-unset "C-c C-c" t))))
 
 (defun erc-controls-interpret (str)
    "Return a copy of STR after dealing with IRC control characters.
-- 
2.38.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-Don-t-load-erc-goodies-atop-erc.el.patch --]
[-- Type: text/x-patch, Size: 1228 bytes --]

From 22e2577f4cd461d866254959572d85831715f61f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 20:19:40 -0800
Subject: [PATCH 1/4] Don't load erc-goodies atop erc.el

* lisp/erc/erc.el: Commit c2d657e7c4fd9685591f2120007eabf78745919d
"Move ERC's core dependencies to a separate file" ironed out ERC's
interwoven dependencies for the better but didn't cleanly sidestep the
goodies interdependency, specifically with regard to custom options.
This reverts the tiny portion impacting this aspect by once again
requiring `erc-goodies' at the very end of ERC's main library. Special
thanks to Libera.Chat user jrm for reporting this bug.
---
 lisp/erc/erc.el | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 7f51b7bfb2e..ff1820cfaf2 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -61,7 +61,6 @@
 (load "erc-loaddefs" 'noerror 'nomessage)
 
 (require 'erc-networks)
-(require 'erc-goodies)
 (require 'erc-backend)
 (require 'cl-lib)
 (require 'format-spec)
@@ -7386,4 +7385,6 @@ erc-handle-irc-url
 
 (provide 'erc)
 
+;; FIXME this is a temporary stopgap for Emacs 29.
+(require 'erc-goodies)
 ;;; erc.el ends here
-- 
2.38.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 3995 bytes --]

From 2d4dce2e09890ad28edf056df0bc23d818ace185 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 20:52:47 -0800
Subject: [PATCH 2/4] [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.
---
 lisp/erc/erc-backend.el |  2 +-
 lisp/erc/erc-compat.el  | 49 +++++++++++++++++++++++++++++++++--------
 2 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1da701aebc4..f00c8b2841a 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..fb354a1d3f7 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -34,6 +34,46 @@
 (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 explicit 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.
+
+An example is the function `plist-get' which got an additional
+predicate argument in Emacs 29.  The compatibility function,
+which supports this additional argument can be obtained
+via (compat-function plist-get) and called with the additional
+predicate argument via (compat-call plist-get plist prop
+predicate).  It is not possible to directly call (plist-get plist
+prop predicate), since the function does not yet support the
+predicate argument on older Emacs versions and the Compat library
+does not override existing 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.
+See `compat-function' for details."
+  (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 +408,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.38.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 5504 bytes --]

From 1bf40a08fbc49b5cadc856f8717cec642be1f06b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 3/4] [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: Sort `defvar' forward declarations for
maintainability.
(erc-irccontrols-mode, erc-enable-irccontrols,
erc-disable-irccontrols): Add and remove key for
`erc-toggle-interpret-controls' to `erc-mode-map'.
* 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-mode-map): Remove C-c C-c binding for
`erc-toggle-interpret-controls'.
(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.
---
 lisp/erc/erc-common.el  | 10 +++++++++-
 lisp/erc/erc-goodies.el | 23 +++++++++++++++++++----
 lisp/erc/erc.el         | 10 ++++++----
 3 files changed, 34 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 994555acecf..e567a108191 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -96,7 +96,15 @@ erc--features-to-modules
     (erc-page page ctcp-page)
     (erc-sound sound ctcp-sound)
     (erc-stamp stamp timestamp)
-    (erc-services services nickserv))
+    (erc-services services nickserv)
+    (erc-goodies scrolltobottom)
+    (erc-goodies readonly)
+    (erc-goodies move-to-prompt)
+    (erc-goodies keep-place)
+    (erc-goodies noncommands)
+    (erc-goodies irccontrols)
+    (erc-goodies smiley)
+    (erc-goodies unmorse))
   "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
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 05a21019042..801c3276499 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -38,9 +38,10 @@ 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)
+(defvar erc-mode-map)
+(defvar erc-modules)
+(defvar erc-server-process)
 
 (declare-function erc-buffer-list "erc" (&optional predicate proc))
 (declare-function erc-error "erc" (&rest args))
@@ -384,9 +385,23 @@ 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)
+   (add-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode))
   ((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)
+   (remove-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode)
+   (erc-with-all-buffers-of-server nil nil
+      (erc--irccontrols-on-major-mode))))
+
+(defun erc--irccontrols-on-major-mode ()
+  ;; FIXME all these keymap-* functions require Compat 29
+  (if erc-irccontrols-mode
+      ;; Interrogate composed view of local map and `erc-mode-map'
+      (unless (keymap-lookup (current-local-map) "C-c C-c")
+        (keymap-local-set "C-c C-c" #'erc-toggle-interpret-controls))
+    (when (eq (keymap-local-lookup "C-c C-c")
+              #'erc-toggle-interpret-controls)
+      (keymap-local-unset "C-c C-c" t))))
 
 (defun erc-controls-interpret (str)
    "Return a copy of STR after dealing with IRC control characters.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ff1820cfaf2..007f70b8671 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
@@ -1188,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)
@@ -6847,7 +6849,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 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))
@@ -7385,6 +7389,4 @@ erc-handle-irc-url
 
 (provide 'erc)
 
-;; FIXME this is a temporary stopgap for Emacs 29.
-(require 'erc-goodies)
 ;;; erc.el ends here
-- 
2.38.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 4344 bytes --]

From a8bd3a2964babe131cb6c04dd14d7b537e1a90b2 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 4/4] [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-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.
---
 lisp/erc/erc-goodies.el |  8 --------
 lisp/erc/erc-imenu.el   | 19 +++++++++++++++++++
 lisp/erc/erc.el         |  4 +++-
 3 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 801c3276499..01e51df0481 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -29,8 +29,6 @@
 
 ;;; Code:
 
-;;; Imenu support
-
 (eval-when-compile (require 'cl-lib))
 (require 'erc-common)
 
@@ -48,12 +46,6 @@ erc-server-process
 (declare-function erc-extract-command-from-line "erc" (line))
 (declare-function erc-beg-of-input-line "erc" nil)
 
-(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..2514ffcc4d3 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -124,6 +124,25 @@ 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-local imenu-create-index-function #'erc-create-imenu-index))
+
+(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 007f70b8671..0d6634d38a9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1819,7 +1819,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
@@ -1864,6 +1864,7 @@ erc-modules
     (const :tag "identd: Launch an identd server on port 8113" identd)
     (const :tag "irccontrols: Highlight or remove IRC control characters"
            irccontrols)
+    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "keep-place: Leave point above un-viewed text" keep-place)
     (const :tag "list: List channels in a separate buffer" list)
     (const :tag "log: Save buffers in logs" log)
@@ -1899,6 +1900,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
   :group 'erc)
 
 (defun erc-update-modules ()
-- 
2.38.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (3 preceding siblings ...)
  2023-01-21 15:03 ` bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el J.P.
@ 2023-01-31 15:27 ` J.P.
  2023-02-07 15:22 ` J.P.
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-01-31 15:27 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 154 bytes --]

v3. Require erc-goodies in erc-ibuffer. Fix bug in
`erc-update-mode-line-buffer'.

The issue of modules changing the major-mode map remains unaddressed.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v2-v3.diff --]
[-- Type: text/x-patch, Size: 4960 bytes --]

From 32113e49c46b8a0de0f3a9d1666c0a1147d3b2dc Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 31 Jan 2023 02:04:39 -0800
Subject: [PATCH 0/3] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (3):
  [5.6] Copy over upstream Compat macros to erc-compat
  [5.6] Don't require erc-goodies in erc.el
  [5.6] Convert ERC's Imenu integration into proper module

 lisp/erc/erc-backend.el  |  2 +-
 lisp/erc/erc-common.el   | 10 +++++++-
 lisp/erc/erc-compat.el   | 52 +++++++++++++++++++++++++++++++++-------
 lisp/erc/erc-goodies.el  | 31 ++++++++++++++----------
 lisp/erc/erc-ibuffer.el  |  1 +
 lisp/erc/erc-imenu.el    | 19 +++++++++++++++
 lisp/erc/erc-page.el     |  3 +++
 lisp/erc/erc-speedbar.el |  1 +
 lisp/erc/erc.el          | 14 +++++++----
 9 files changed, 105 insertions(+), 28 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index fb354a1d3f7..605ee701850 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -41,7 +41,7 @@
 ;;
 ;; BEGIN Compat macros
 
-;;;; Macros for explicit compatibility function calls
+;;;; Macros for extended compatibility function calls
 
 (defmacro erc-compat-function (fun)
   "Return compatibility function symbol for FUN.
@@ -54,21 +54,24 @@ erc-compat-function
 functions which implement the behavior and calling convention of
 Emacs 29.1.
 
-An example is the function `plist-get' which got an additional
-predicate argument in Emacs 29.  The compatibility function,
-which supports this additional argument can be obtained
-via (compat-function plist-get) and called with the additional
-predicate argument via (compat-call plist-get plist prop
-predicate).  It is not possible to directly call (plist-get plist
-prop predicate), since the function does not yet support the
-predicate argument on older Emacs versions and the Compat library
-does not override existing functions."
+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.
-See `compat-function' for details."
+
+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)))
 
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 308b3784ca5..8eb558ecda7 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)
@@ -69,6 +71,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 0d6634d38a9..61adfe2e2ec 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6851,7 +6851,7 @@ erc-update-mode-line-buffer
                   (?m . ,(erc-format-channel-modes))
                   (?n . ,(or (erc-current-nick) ""))
                   (?N . ,(erc-format-network))
-                  (?o . ,(or (and erc-irccontrols-mode
+                  (?o . ,(or (and (bound-and-true-p erc-irccontrols-mode)
                                   (erc-controls-strip erc-channel-topic))
                              ""))
                   (?p . ,(erc-port-to-string erc-session-port))
-- 
2.39.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4098 bytes --]

From 6fd8cd43513bd3a0accbe203eba6ee3a1021486c Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 20:52:47 -0800
Subject: [PATCH 1/3] [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.
---
 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 1da701aebc4..f00c8b2841a 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 7404 bytes --]

From ecd3e3da9349202cc59b7d9981fc7aa67def89e8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 2/3] [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: Sort `defvar' forward declarations for
maintainability.
* 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.
(erc-irccontrols-mode, erc-enable-irccontrols,
erc-disable-irccontrols): Add and remove key for
`erc-toggle-interpret-controls' to `erc-mode-map'.
* 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-mode-map): Remove C-c C-c binding for
`erc-toggle-interpret-controls'.
(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.
---
 lisp/erc/erc-common.el   | 10 +++++++++-
 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          | 10 ++++++----
 6 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 994555acecf..e567a108191 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -96,7 +96,15 @@ erc--features-to-modules
     (erc-page page ctcp-page)
     (erc-sound sound ctcp-sound)
     (erc-stamp stamp timestamp)
-    (erc-services services nickserv))
+    (erc-services services nickserv)
+    (erc-goodies scrolltobottom)
+    (erc-goodies readonly)
+    (erc-goodies move-to-prompt)
+    (erc-goodies keep-place)
+    (erc-goodies noncommands)
+    (erc-goodies irccontrols)
+    (erc-goodies smiley)
+    (erc-goodies unmorse))
   "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
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 05a21019042..801c3276499 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -38,9 +38,10 @@ 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)
+(defvar erc-mode-map)
+(defvar erc-modules)
+(defvar erc-server-process)
 
 (declare-function erc-buffer-list "erc" (&optional predicate proc))
 (declare-function erc-error "erc" (&rest args))
@@ -384,9 +385,23 @@ 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)
+   (add-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode))
   ((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)
+   (remove-hook 'erc-mode-hook #'erc--irccontrols-on-major-mode)
+   (erc-with-all-buffers-of-server nil nil
+      (erc--irccontrols-on-major-mode))))
+
+(defun erc--irccontrols-on-major-mode ()
+  ;; FIXME all these keymap-* functions require Compat 29
+  (if erc-irccontrols-mode
+      ;; Interrogate composed view of local map and `erc-mode-map'
+      (unless (keymap-lookup (current-local-map) "C-c C-c")
+        (keymap-local-set "C-c C-c" #'erc-toggle-interpret-controls))
+    (when (eq (keymap-local-lookup "C-c C-c")
+              #'erc-toggle-interpret-controls)
+      (keymap-local-unset "C-c C-c" t))))
 
 (defun erc-controls-interpret (str)
    "Return a copy of STR after dealing with IRC control characters.
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 308b3784ca5..8eb558ecda7 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)
@@ -69,6 +71,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 ff1820cfaf2..cc3de26e96e 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
@@ -1188,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)
@@ -6847,7 +6849,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))
@@ -7385,6 +7389,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 4344 bytes --]

From 32113e49c46b8a0de0f3a9d1666c0a1147d3b2dc Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 3/3] [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-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.
---
 lisp/erc/erc-goodies.el |  8 --------
 lisp/erc/erc-imenu.el   | 19 +++++++++++++++++++
 lisp/erc/erc.el         |  4 +++-
 3 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 801c3276499..01e51df0481 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -29,8 +29,6 @@
 
 ;;; Code:
 
-;;; Imenu support
-
 (eval-when-compile (require 'cl-lib))
 (require 'erc-common)
 
@@ -48,12 +46,6 @@ erc-server-process
 (declare-function erc-extract-command-from-line "erc" (line))
 (declare-function erc-beg-of-input-line "erc" nil)
 
-(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..2514ffcc4d3 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -124,6 +124,25 @@ 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-local imenu-create-index-function #'erc-create-imenu-index))
+
+(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 cc3de26e96e..61adfe2e2ec 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1819,7 +1819,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
@@ -1864,6 +1864,7 @@ erc-modules
     (const :tag "identd: Launch an identd server on port 8113" identd)
     (const :tag "irccontrols: Highlight or remove IRC control characters"
            irccontrols)
+    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "keep-place: Leave point above un-viewed text" keep-place)
     (const :tag "list: List channels in a separate buffer" list)
     (const :tag "log: Save buffers in logs" log)
@@ -1899,6 +1900,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
   :group 'erc)
 
 (defun erc-update-modules ()
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (4 preceding siblings ...)
  2023-01-31 15:27 ` J.P.
@ 2023-02-07 15:22 ` J.P.
  2023-02-19 15:07 ` J.P.
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-02-07 15:22 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 178 bytes --]

v4. Switch to autoloads-based module migration scheme. Add old patch for
extending control-color range. Move remaining top-level `define-key'
occurrences to module definitions.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v3-v4.diff --]
[-- Type: text/x-patch, Size: 43892 bytes --]

From 35f6fc539ba753b1742cbe8580c15cc9ade62541 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 7 Feb 2023 06:58:15 -0800
Subject: [PATCH 0/6] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (6):
  [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              |  20 +++
 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                    |  60 ++++---
 test/lisp/erc/erc-goodies-tests.el | 251 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el         | 155 ++++++++++++++++--
 18 files changed, 587 insertions(+), 125 deletions(-)
 create mode 100644 test/lisp/erc/erc-goodies-tests.el

Interdiff:
diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index 1be47c3e665..a5fa50d01df 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-common.el b/lisp/erc/erc-common.el
index e567a108191..3332d240f6e 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -88,48 +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)
-    (erc-goodies scrolltobottom)
-    (erc-goodies readonly)
-    (erc-goodies move-to-prompt)
-    (erc-goodies keep-place)
-    (erc-goodies noncommands)
-    (erc-goodies irccontrols)
-    (erc-goodies smiley)
-    (erc-goodies unmorse))
-  "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-goodies.el b/lisp/erc/erc-goodies.el
index 01e51df0481..3d018bb11e0 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -30,21 +30,7 @@
 ;;; Code:
 
 (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-log-p)
-(defvar erc-mode-map)
-(defvar erc-modules)
-(defvar erc-server-process)
-
-(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)
 
 
 ;;; Automatically scroll to bottom
@@ -58,6 +44,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)
@@ -109,6 +96,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)
@@ -124,6 +112,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)
@@ -148,6 +137,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))
@@ -186,6 +176,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
@@ -346,19 +337,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)."
@@ -366,34 +376,44 @@ 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
   "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-mode-hook #'erc--irccontrols-on-major-mode))
+   (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-mode-hook #'erc--irccontrols-on-major-mode)
-   (erc-with-all-buffers-of-server nil nil
-      (erc--irccontrols-on-major-mode))))
-
-(defun erc--irccontrols-on-major-mode ()
-  ;; FIXME all these keymap-* functions require Compat 29
-  (if erc-irccontrols-mode
-      ;; Interrogate composed view of local map and `erc-mode-map'
-      (unless (keymap-lookup (current-local-map) "C-c C-c")
-        (keymap-local-set "C-c C-c" #'erc-toggle-interpret-controls))
-    (when (eq (keymap-local-lookup "C-c C-c")
-              #'erc-toggle-interpret-controls)
-      (keymap-local-unset "C-c C-c" t))))
+   (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.
@@ -455,16 +475,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'.
@@ -544,6 +554,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)
@@ -560,6 +572,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
@@ -576,6 +589,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-imenu.el b/lisp/erc/erc-imenu.el
index 2514ffcc4d3..3b5dd988c18 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -134,6 +134,7 @@ erc-imenu-setup
     (setq erc-imenu--create-index-function imenu-create-index-function))
   (setq-local 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))
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 499bcaf5724..661471ea35c 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-page.el b/lisp/erc/erc-page.el
index 8eb558ecda7..a94678e5132 100644
--- a/lisp/erc/erc-page.el
+++ b/lisp/erc/erc-page.el
@@ -36,6 +36,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 1c2fc2fcdc8..87cb77ad774 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 61adfe2e2ec..ec1ea29a6af 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1214,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
@@ -1858,13 +1871,12 @@ 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)
+    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "irccontrols: Highlight or remove IRC control characters"
            irccontrols)
-    (const :tag "imenu: A simple Imenu integration" imenu)
     (const :tag "keep-place: Leave point above un-viewed text" keep-place)
     (const :tag "list: List channels in a separate buffer" list)
     (const :tag "log: Save buffers in logs" log)
@@ -1876,11 +1888,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)
@@ -1893,8 +1905,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 +1921,26 @@ erc-update-modules
   (erc--update-modules)
   nil)
 
+(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))))
+
 (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-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
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 40a2d2de657..816469d9894 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -471,6 +471,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
@@ -1203,6 +1247,66 @@ erc-handle-irc-url
     (kill-buffer "baznet")
     (kill-buffer "#chan")))
 
+(defconst erc-tests--modules
+  '( autoaway autojoin button capab-identify completion dcc fill identd
+     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))
+
+(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)))
@@ -1213,17 +1317,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)
@@ -1234,20 +1349,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4112 bytes --]

From 4fc6a763ee9d5213efb6468663a191b0125accd8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 20:52:47 -0800
Subject: [PATCH 1/6] [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 1da701aebc4..f00c8b2841a 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch --]
[-- Type: text/x-patch, Size: 16450 bytes --]

From 7a5c590ee62667a3fc2e35f368f329ab723ae181 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 4 Feb 2023 06:24:59 -0800
Subject: [PATCH 2/6] [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.
(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--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            |  33 ++++++-----
 test/lisp/erc/erc-tests.el | 111 +++++++++++++++++++++++++++++++------
 8 files changed, 130 insertions(+), 62 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 1c2fc2fcdc8..87cb77ad774 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 ff1820cfaf2..aba96e9a2ab 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1856,7 +1856,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)
@@ -1873,11 +1872,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)
@@ -1890,8 +1889,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)
@@ -1905,18 +1904,26 @@ erc-update-modules
   (erc--update-modules)
   nil)
 
+(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))))
+
 (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 40a2d2de657..7d6061bb16e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1203,6 +1203,66 @@ 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))
+
+(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)))
@@ -1213,17 +1273,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)
@@ -1234,20 +1305,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 8072 bytes --]

From e2766648b737382fc2ed923e13bc9e33f55c0ce9 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 3/6] [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          |  9 ++++++---
 5 files changed, 20 insertions(+), 17 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 aba96e9a2ab..5786007dbf1 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
@@ -6854,7 +6857,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))
@@ -7392,6 +7397,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Modify-erc-mode-map-in-module-definitions.patch --]
[-- Type: text/x-patch, Size: 9617 bytes --]

From b4a38603a4ceec5a331f27da30ce0d557f7297b8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 4/6] [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 1be47c3e665..a5fa50d01df 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 499bcaf5724..661471ea35c 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 5786007dbf1..796acbc36f7 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 7d6061bb16e..1cd15233a1d 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -471,6 +471,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch --]
[-- Type: text/x-patch, Size: 17239 bytes --]

From aac8d18e68f50eff986f9248ab8c7744ade78463 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 9 Jul 2021 20:03:51 -0700
Subject: [PATCH 5/6] [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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 4974 bytes --]

From 35f6fc539ba753b1742cbe8580c15cc9ade62541 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 19 Jan 2023 21:07:27 -0800
Subject: [PATCH 6/6] [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-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      | 20 ++++++++++++++++++++
 lisp/erc/erc.el            |  4 +++-
 test/lisp/erc/erc-tests.el |  2 +-
 4 files changed, 24 insertions(+), 8 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..3b5dd988c18 100644
--- a/lisp/erc/erc-imenu.el
+++ b/lisp/erc/erc-imenu.el
@@ -124,6 +124,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-local 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 796acbc36f7..ec1ea29a6af 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1832,7 +1832,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
@@ -1874,6 +1874,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)
@@ -1911,6 +1912,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
   :group 'erc)
 
 (defun erc-update-modules ()
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 1cd15233a1d..816469d9894 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1249,7 +1249,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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (5 preceding siblings ...)
  2023-02-07 15:22 ` J.P.
@ 2023-02-19 15:07 ` J.P.
  2023-03-09 14:43 ` J.P.
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-02-19 15:07 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 290 bytes --]

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'.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v4-v5.diff --]
[-- Type: text/x-patch, Size: 9055 bytes --]

From b60011843f6508f3a0a079795913de2d8ca7f903 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2684 bytes --]

From 409b844567c1fb93ff13366317677d9b5324682b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4112 bytes --]

From 0aa8258673eeb23877bbef0a3b5fab9d6f0276cb Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch --]
[-- Type: text/x-patch, Size: 19348 bytes --]

From 593c6b02bb1d1376a164ed91c33c2806da8ee906 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 8379 bytes --]

From a8b93f281f4c930b5f490e1ad47d71a8017c615e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Modify-erc-mode-map-in-module-definitions.patch --]
[-- Type: text/x-patch, Size: 9617 bytes --]

From 320394895103740fcb98e555d9c1a4c90e62ee7b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch --]
[-- Type: text/x-patch, Size: 17239 bytes --]

From b23cf15cdbb20bcd683d0d3e4099c07cc5a1641a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 5341 bytes --]

From b60011843f6508f3a0a079795913de2d8ca7f903 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (6 preceding siblings ...)
  2023-02-19 15:07 ` J.P.
@ 2023-03-09 14:43 ` J.P.
  2023-03-14 13:32 ` J.P.
  2023-03-15 14:04 ` J.P.
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-03-09 14:43 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 281 bytes --]

v6. Autoload `erc-controls-strip' in erc-loaddefs.el and also `require'
the latter in erc.el when compiling. Remove unneeded forward
declarations. Remove barely used `require's atop file (possibly
controversial). Use dedicated "spoilers" face to avoid appearance of
gaps in text.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v5-v6.diff --]
[-- Type: text/x-patch, Size: 6594 bytes --]

From 45e083eb4212cb77130d917a27f5f9985b08bdf7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 9 Mar 2023 06:13:56 -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            | 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                    | 105 ++++++++----
 test/lisp/erc/erc-goodies-tests.el | 253 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el         | 187 +++++++++++++++++++--
 18 files changed, 672 insertions(+), 142 deletions(-)
 create mode 100644 test/lisp/erc/erc-goodies-tests.el

Interdiff:
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 3d018bb11e0..7ea6c42ec65 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -235,6 +235,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)
@@ -467,6 +473,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
@@ -531,6 +538,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
@@ -548,14 +562,12 @@ 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)
-  (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)
@@ -632,3 +644,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.el b/lisp/erc/erc.el
index b8f8263f9c3..1315bce2e54 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"
@@ -135,13 +131,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))
+(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
 
@@ -3099,6 +3096,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
@@ -6891,8 +6890,7 @@ erc-update-mode-line-buffer
                   (?m . ,(erc-format-channel-modes))
                   (?n . ,(or (erc-current-nick) ""))
                   (?N . ,(erc-format-network))
-                  (?o . ,(or (and (bound-and-true-p erc-irccontrols-mode)
-                                  (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))
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
index 8cab1dd0857..46fcf82401b 100644
--- a/test/lisp/erc/erc-goodies-tests.el
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -156,11 +156,13 @@ erc-controls-highlight--inverse
          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))
+         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" '(fg:erc-color-face1 bg:erc-color-face1)
-         '(fg:erc-color-face0 bg:erc-color-face0)))
+         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)))))
 
-- 
2.39.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2684 bytes --]

From 2be509ddf594767a29ea3b16a31874e4078c3255 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4112 bytes --]

From 9f0c66057b527845f6a81204fb1fd2f00de5029f Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch --]
[-- Type: text/x-patch, Size: 19348 bytes --]

From e5092e3ee953bff28e5f391b647585e5f60cb0cd Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 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..6520e57ff3a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1830,12 +1830,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)
@@ -1847,7 +1855,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)))
@@ -1861,7 +1876,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 +1892,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 +1909,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 +1924,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 9911 bytes --]

From 8a3956c735952256ea9812a2fe15d39be4185223 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 6520e57ff3a..6acb3e0ab97 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
@@ -3082,6 +3082,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
@@ -6864,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)
@@ -6876,7 +6876,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))
@@ -7414,6 +7415,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Modify-erc-mode-map-in-module-definitions.patch --]
[-- Type: text/x-patch, Size: 9617 bytes --]

From 3c5d56edd89c4aa10f164c1e470fb49d88dcb3dd Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 6acb3e0ab97..03bab52ac07 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch --]
[-- Type: text/x-patch, Size: 18371 bytes --]

From a6ac60a30293b445e3080972899f7b04f26f79ae Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 5341 bytes --]

From 45e083eb4212cb77130d917a27f5f9985b08bdf7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 03bab52ac07..1315bce2e54 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
@@ -1891,6 +1891,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)
@@ -1928,6 +1929,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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (7 preceding siblings ...)
  2023-03-09 14:43 ` J.P.
@ 2023-03-14 13:32 ` J.P.
  2023-03-15 14:04 ` J.P.
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-03-14 13:32 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 125 bytes --]

v7. Prevent erroneous Custom "edit" state when initializing
`erc-modules' on account of `standard-value' ordering mismatch.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v6-v7.diff --]
[-- Type: text/x-patch, Size: 2720 bytes --]

From 636bec56194f964907c5cf5d37f2cb18e338e64e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2684 bytes --]

From 4b86ffb90b2cb12f96588aa3a6a0a59b3c425ab1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4112 bytes --]

From 927962b4199cff9d2f3a9852021dadd563eb5a91 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch --]
[-- Type: text/x-patch, Size: 19541 bytes --]

From 27194567216a079b2150b601d0aadbb9913466d4 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 9911 bytes --]

From 6560102c934cb72270c1133aaae8be759c0dbf4e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Modify-erc-mode-map-in-module-definitions.patch --]
[-- Type: text/x-patch, Size: 9617 bytes --]

From d43379e7cdf59de22ee370f8a71cc5180840937a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch --]
[-- Type: text/x-patch, Size: 18371 bytes --]

From fa2057c5ff35b42b59b83aa6a3df003ae87aac84 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 5341 bytes --]

From 636bec56194f964907c5cf5d37f2cb18e338e64e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el
       [not found] <87pmb9wyz6.fsf@neverwas.me>
                   ` (8 preceding siblings ...)
  2023-03-14 13:32 ` J.P.
@ 2023-03-15 14:04 ` J.P.
  9 siblings, 0 replies; 10+ messages in thread
From: J.P. @ 2023-03-15 14:04 UTC (permalink / raw)
  To: 60954; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 113 bytes --]

v8. Maintain sorted default value for `erc-modules' (instead of mutating
`standard-value'). Add test to verify.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v7-v8.diff --]
[-- Type: text/x-patch, Size: 3992 bytes --]

From 0d6a8a2a0751b0a4831fdbaf9fecb2e92c676d45 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 15 Mar 2023 06:37:43 -0700
Subject: [PATCH 0/7] *** NOT A PATCH ***

v8. Don't touch `standard-value'. Maintain sorted default value instead and add
test to verify.

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                    | 110 +++++++++----
 test/lisp/erc/erc-goodies-tests.el | 253 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el         | 195 ++++++++++++++++++++--
 18 files changed, 683 insertions(+), 144 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 e122f32d96a..34393189039 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1832,9 +1832,9 @@ erc-migrate-modules
   ;; each item is in the format '(old . new)
   (delete-dups (mapcar #'erc--normalize-module-symbol mods)))
 
-(defcustom erc-modules '(netsplit fill button match track completion readonly
-                                  networks ring autojoin noncommands irccontrols
-                                  move-to-prompt stamp menu list imenu)
+(defcustom erc-modules '( autojoin button completion fill imenu irccontrols
+                          list match menu move-to-prompt netsplit
+                          networks noncommands readonly ring stamp track)
   "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
@@ -1850,9 +1850,7 @@ erc-modules
                   (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)))))
+                (custom-initialize-reset symbol exp))
   :set (lambda (sym val)
          ;; disable modules which have just been removed
          (when (and (boundp 'erc-modules) erc-modules val)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 3e844f45f1a..acd470a1e17 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1275,6 +1275,14 @@ erc-tests--modules
      replace ring sasl scrolltobottom services smiley sound
      spelling stamp track truncate unmorse xdcc))
 
+;; Ensure that `:initialize' doesn't change the ordering of the
+;; members because otherwise the widget's state is "edited".
+
+(ert-deftest erc-modules--initialize ()
+  ;; This is `custom--standard-value' from Emacs 28.
+  (should (equal (eval (car (get 'erc-modules 'standard-value)) t)
+                 erc-modules)))
+
 ;; Ensure the `:initialize' function for `erc-modules' successfully
 ;; tags all built-in modules with the internal property `erc--module'.
 
-- 
2.39.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Honor-arbitrary-CHANTYPES-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2684 bytes --]

From d9c571221e2678ea512afad6cd2defa6da6cd09b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Copy-over-upstream-Compat-macros-to-erc-compat.patch --]
[-- Type: text/x-patch, Size: 4112 bytes --]

From d696c859fb5f3b84ee0ab1fb31bc2a21b5f38c66 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Leverage-loaddefs-for-migrating-ERC-modules.patch --]
[-- Type: text/x-patch, Size: 20684 bytes --]

From 857759f3c7998ada6944ff56b55a3e4dc8f86c9a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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): Reorder default value, sorted by
`string<' so that Customize does not consider the value to have been
edited.  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--initialize): New test.
(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            |  63 ++++++++++++------
 test/lisp/erc/erc-tests.el | 130 ++++++++++++++++++++++++++++++++-----
 8 files changed, 173 insertions(+), 68 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..9af038b048e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1820,9 +1820,9 @@ erc-migrate-modules
   ;; each item is in the format '(old . new)
   (delete-dups (mapcar #'erc--normalize-module-symbol mods)))
 
-(defcustom erc-modules '(netsplit fill button match track completion readonly
-                                  networks ring autojoin noncommands irccontrols
-                                  move-to-prompt stamp menu list)
+(defcustom erc-modules '( autojoin button completion fill irccontrols
+                          list match menu move-to-prompt netsplit
+                          networks noncommands readonly ring stamp track)
   "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
@@ -1830,12 +1830,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-reset 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)
@@ -1847,7 +1855,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 +1877,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 +1893,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 +1910,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 +1925,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..81381a0c800 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1224,6 +1224,85 @@ 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 that `:initialize' doesn't change the ordering of the
+;; members because otherwise the widget's state is "edited".
+
+(ert-deftest erc-modules--initialize ()
+  ;; This is `custom--standard-value' from Emacs 28.
+  (should (equal (eval (car (get 'erc-modules 'standard-value)) t)
+                 erc-modules)))
+
+;; 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 +1313,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 +1345,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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Don-t-require-erc-goodies-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 9911 bytes --]

From 6b32b19a6f9f4e7b30c24cee69e17b3910019b1b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 9af038b048e..680121657c1 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
@@ -3083,6 +3083,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
@@ -6865,8 +6867,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)
@@ -6877,7 +6877,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))
@@ -7415,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.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Modify-erc-mode-map-in-module-definitions.patch --]
[-- Type: text/x-patch, Size: 9617 bytes --]

From cedc5b6de3817dcf6c11d726008c4841123bc3b4 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 680121657c1..fa0bab7159e 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 81381a0c800..0c7b06da436 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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Add-missing-colors-to-erc-irccontrols-mode.patch --]
[-- Type: text/x-patch, Size: 18371 bytes --]

From 4c4e3bbac56027b868e4e56827fd25b3ce9be1cc Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-5.6-Convert-ERC-s-Imenu-integration-into-proper-modu.patch --]
[-- Type: text/x-patch, Size: 5289 bytes --]

From 0d6a8a2a0751b0a4831fdbaf9fecb2e92c676d45 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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 fa0bab7159e..34393189039 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1832,7 +1832,7 @@ erc-migrate-modules
   ;; each item is in the format '(old . new)
   (delete-dups (mapcar #'erc--normalize-module-symbol mods)))
 
-(defcustom erc-modules '( autojoin button completion fill irccontrols
+(defcustom erc-modules '( autojoin button completion fill imenu irccontrols
                           list match menu move-to-prompt netsplit
                           networks noncommands readonly ring stamp track)
   "A list of modules which ERC should enable.
@@ -1892,6 +1892,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)
@@ -1929,6 +1930,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 0c7b06da436..acd470a1e17 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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2023-03-15 14:04 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <87pmb9wyz6.fsf@neverwas.me>
2023-01-20  7:16 ` bug#60954: 29.0.60; ERC 5.4.1: loading ERC clobbers customizations to erc-mode-hook Eli Zaretskii
     [not found] ` <83cz79oeu7.fsf@gnu.org>
2023-01-20 14:14   ` J.P.
2023-01-20 14:15 ` J.P.
2023-01-21 15:03 ` bug#60954: 30.0.50; ERC >5.5: Stop requiring erc-goodies in erc.el J.P.
2023-01-31 15:27 ` J.P.
2023-02-07 15:22 ` J.P.
2023-02-19 15:07 ` J.P.
2023-03-09 14:43 ` J.P.
2023-03-14 13:32 ` J.P.
2023-03-15 14:04 ` J.P.

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).