From 21145f307c90c0231b8564e7f6517d2782a8cf17 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 12 Jul 2021 03:44:28 -0700 Subject: [PATCH 3/5] Support local ERC modules in erc-mode buffers * doc/misc/erc.texi: Mention local modules in Modules Chapter. * lisp/erc/erc-compat.el (erc-compat--local-module-modes): Add helper for finding local modules active in an ERC buffer. * lisp/erc/erc.el (erc-migrate-modules): Add some missing mappings. (erc-update-modules): Change return value from nil to a list of minor-mode commands for local modules. Use `custom-variable-p' to detect flavor. Currently, all modules are global and so are their accompanying minor modes. (erc-open): Defer enabling of local modules via `erc-update-modules' until after buffer is initialized with other local vars. Also defer major mode hooks so they can detect things like whether the buffer is a server or target buffer. Also ensure local module setup code can detect when `erc-open' was called with a non-nil `erc--server-reconnecting'. It's reset to nil by `erc-server-connect'. * lisp/erc/erc-common.el (erc--module-name-migrations, erc--features-to-modules, erc--modules-to-features): Add alists of old-to-new module names to support module-name migrations. (define-erc-modules): Don't enable local modules (minor modes) unless `erc-mode' is the major mode. And don't disable them unless the minor mode is actually active. Also, don't mutate `erc-modules' when dealing with a local module. (erc--normalize-module-symbol): Add helper for `erc-migrate-modules'. * lisp/erc/erc-goodies.el: Require cl-lib. * test/lisp/erc/erc-tests.el (erc-migrate-modules, erc-update-modules): Add rudimentary unit tests. (Bug#57955.) --- doc/misc/erc.texi | 11 +++++- etc/ERC-NEWS | 14 +++++++ lisp/erc/erc-common.el | 56 ++++++++++++++++++++++++---- lisp/erc/erc-compat.el | 12 ++++++ lisp/erc/erc-goodies.el | 1 + lisp/erc/erc.el | 75 ++++++++++++++++++++------------------ test/lisp/erc/erc-tests.el | 58 +++++++++++++++++++++++++++++ 7 files changed, 182 insertions(+), 45 deletions(-) diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 3db83197f9..5049710b32 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -389,8 +389,15 @@ Modules There is a spiffy customize interface, which may be reached by typing @kbd{M-x customize-option @key{RET} erc-modules @key{RET}}. -Alternatively, set @code{erc-modules} manually and then call -@code{erc-update-modules}. +Alternatively, set @code{erc-modules} manually, and ERC will load them +and run their setup code during buffer initialization. Third-party +code may need to call the function @code{erc-update-modules} +explicitly, although this is typically unnecessary. + +All modules operate as minor modes under the hood, and some newer ones +are defined as buffer-local. For everyday use, the only practical +difference is that local modules can only be enabled in ERC buffers, +and their toggle commands never mutate @code{erc-modules}. The following is a list of available modules. diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 5cabb9b015..e14cd3492a 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -97,6 +97,20 @@ messages during periods of heavy traffic no longer disappear. Although rare, server passwords containing white space are now handled correctly. +** Local modules and ERC-mode hooks are more useful. +The 'local-p' parameter of 'define-erc-module' now affects more than +the scope of a module's minor-mode. This currently has little direct +impact on the user experience, but third-party packages may wish to +take note. + +More importantly, the function 'erc-update-modules' now supports an +optional argument to defer the enabling of local modules and instead +return their mode-activation commands. 'erc-open' leverages this new +functionality to delay their activation, as well as that of all +'erc-mode-hook' members, until most of ERC's mode-related variables +have been initialized. This does not include connection-specific +variables defined in erc-backend, however. + ** Miscellaneous behavioral changes in the library API. A number of core macros and other definitions have been moved to a new file called erc-common.el. This was done to further lessen the diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index d8aac36eab..e5fabdc67f 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -85,6 +85,41 @@ 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.") + +(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)) + (defmacro define-erc-module (name alias doc enable-body disable-body &optional local-p) "Define a new minor mode using ERC conventions. @@ -98,7 +133,9 @@ define-erc-module This will define a minor mode called erc-NAME-mode, possibly an alias erc-ALIAS-mode, as well as the helper functions -erc-NAME-enable, and erc-NAME-disable. +erc-NAME-enable, and erc-NAME-disable. Beware that for global +modules, these helpers, as well as the minor-mode toggle, all mutate +the user option `erc-modules'. Example: @@ -111,6 +148,7 @@ define-erc-module #\\='erc-replace-insert)))" (declare (doc-string 3) (indent defun)) (let* ((sn (symbol-name name)) + (mod (erc--normalize-module-symbol name)) (mode (intern (format "erc-%s-mode" (downcase sn)))) (group (intern (format "erc-%s" (downcase sn)))) (enable (intern (format "erc-%s-enable" (downcase sn)))) @@ -134,16 +172,20 @@ define-erc-module ,(format "Enable ERC %S mode." name) (interactive) - (add-to-list 'erc-modules (quote ,name)) - (setq ,mode t) - ,@enable-body) + ,@(unless local-p `((cl-pushnew ',mod erc-modules))) + ,@(if local-p + `((when (setq ,mode (and (derived-mode-p 'erc-mode) t)) + ,@enable-body)) + `((setq ,mode t) ,@enable-body))) (defun ,disable () ,(format "Disable ERC %S mode." name) (interactive) - (setq erc-modules (delq (quote ,name) erc-modules)) - (setq ,mode nil) - ,@disable-body) + ,@(unless local-p `((setq erc-modules (delq ',mod erc-modules)))) + ,@(macroexp-unprogn + `(,@(if local-p '(when (derived-mode-p 'erc-mode)) '(progn)) + (setq ,mode nil) + ,@disable-body))) ,(when (and alias (not (eq name alias))) `(defalias ',(intern diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el index 03bd8f1352..96b862c8c5 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -168,6 +168,18 @@ erc-compat--with-memoization `(cl--generic-with-memoization ,table ,@forms)) (t `(progn ,@forms)))) +(defun erc-compat--local-module-modes () + (delq nil + (if (boundp 'local-minor-modes) + (mapcar (lambda (m) + (and (string-prefix-p "erc-" (symbol-name m)) m)) + local-minor-modes) + (mapcar (pcase-lambda (`(,k . _)) + (and (string-prefix-p "erc-" (symbol-name k)) + (string-suffix-p "-mode" (symbol-name k)) + k)) + (buffer-local-variables))))) + (provide 'erc-compat) ;;; erc-compat.el ends here diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el index 59b5f01f23..1af83b58ba 100644 --- a/lisp/erc/erc-goodies.el +++ b/lisp/erc/erc-goodies.el @@ -31,6 +31,7 @@ ;;; Imenu support +(eval-when-compile (require 'cl-lib)) (require 'erc-common) (defvar erc-controls-highlight-regexp) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 63379af141..3d8afe8df6 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1784,10 +1784,7 @@ erc-migrate-modules "Migrate old names of ERC modules to new ones." ;; modify `transforms' to specify what needs to be changed ;; each item is in the format '(old . new) - (let ((transforms '((pcomplete . completion)))) - (delete-dups - (mapcar (lambda (m) (or (cdr (assoc m transforms)) m)) - mods)))) + (delete-dups (mapcar #'erc--normalize-module-symbol mods))) (defcustom erc-modules '(netsplit fill button match track completion readonly networks ring autojoin noncommands irccontrols @@ -1865,28 +1862,25 @@ erc-modules (repeat :tag "Others" :inline t symbol)) :group 'erc) -(defun erc-update-modules () - "Run this to enable erc-foo-mode for all modules in `erc-modules'." - (let (req) - (dolist (mod erc-modules) - (setq req (concat "erc-" (symbol-name mod))) - (cond - ;; yuck. perhaps we should bring the filenames into sync? - ((string= req "erc-capab-identify") - (setq req "erc-capab")) - ((string= req "erc-completion") - (setq req "erc-pcomplete")) - ((string= req "erc-pcomplete") - (setq mod 'completion)) - ((string= req "erc-autojoin") - (setq req "erc-join"))) - (condition-case nil - (require (intern req)) - (error nil)) - (let ((sym (intern-soft (concat "erc-" (symbol-name mod) "-mode")))) - (if (fboundp sym) - (funcall sym 1) - (error "`%s' is not a known ERC module" mod)))))) +(defun erc-update-modules (&optional defer-locals) + "Enable global minor mode for all global modules in `erc-modules'. +With DEFER-LOCALS, return minor-mode commands for all local +modules, possibly for deferred invocation, as done by `erc-open' +whenever a new ERC buffer is created. Local modules were +introduced in ERC 5.5." + (let ((local-modes + (when (and defer-locals (derived-mode-p 'erc-mode)) + (erc-compat--local-module-modes)))) + (dolist (module erc-modules (and defer-locals 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 (and defer-locals (not (custom-variable-p mode))) + (cl-pushnew mode local-modes) + (funcall mode 1)))))) (defun erc-setup-buffer (buffer) "Consults `erc-join-buffer' to find out how to display `BUFFER'." @@ -1942,18 +1936,24 @@ erc-open (let* ((target (and channel (erc--target-from-string channel))) (buffer (erc-get-buffer-create server port nil target id)) (old-buffer (current-buffer)) - old-point + (old-recon-count erc-server-reconnect-count) + (old-point nil) + (delayed-modules nil) (continued-session (and erc--server-reconnecting (with-suppressed-warnings ((obsolete erc-reuse-buffers)) - erc-reuse-buffers)))) + erc-reuse-buffers) + erc-networks--id))) (when connect (run-hook-with-args 'erc-before-connect server port nick)) - (erc-update-modules) (set-buffer buffer) (setq old-point (point)) - (let ((old-recon-count erc-server-reconnect-count)) - (erc-mode) - (setq erc-server-reconnect-count old-recon-count)) + (setq delayed-modules (erc-update-modules 'defer-locals)) + + (delay-mode-hooks (erc-mode)) + + (setq erc-server-reconnect-count old-recon-count + erc--server-reconnecting continued-session) + (when (setq erc-server-connected (not connect)) (setq erc-server-announced-name (buffer-local-value 'erc-server-announced-name old-buffer))) @@ -2010,14 +2010,19 @@ erc-open (setq erc-session-client-certificate client-certificate) (setq erc-networks--id (if connect - (or (and continued-session - (buffer-local-value 'erc-networks--id old-buffer)) + (or erc--server-reconnecting (and id (erc-networks--id-create id))) (buffer-local-value 'erc-networks--id old-buffer))) ;; debug output buffer (setq erc-dbuf (when erc-log-p (get-buffer-create (concat "*ERC-DEBUG: " server "*")))) + + (erc-determine-parameters server port nick full-name user passwd) + + (save-excursion (run-mode-hooks)) + (dolist (mod delayed-modules) (funcall mod +1)) + ;; set up prompt (unless continued-session (goto-char (point-max)) @@ -2029,8 +2034,6 @@ erc-open (erc-display-prompt) (goto-char (point-max))) - (erc-determine-parameters server port nick full-name user passwd) - ;; Saving log file on exit (run-hook-with-args 'erc-connect-pre-hook buffer) diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index c88dd9888d..d074b36c8b 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -953,4 +953,62 @@ erc-message (kill-buffer "ExampleNet") (kill-buffer "#chan"))) +(ert-deftest erc-migrate-modules () + (should (equal (erc-migrate-modules '(autojoin timestamp button)) + '(autojoin stamp button))) + ;; Default unchanged + (should (equal (erc-migrate-modules erc-modules) erc-modules))) + +(ert-deftest erc-update-modules () + (let (calls + erc-modules + erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook) + (cl-letf (((symbol-function 'require) + (lambda (s &rest _) (push s calls))) + + ;; Local modules + ((symbol-function 'erc-fake-bar-mode) + (lambda (n) (push (cons 'fake-bar n) calls))) + + ;; 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-autojoin-mode) + (lambda (n) (push (cons 'autojoin n) calls))) + ((get 'erc-autojoin-mode 'standard-value) 'ignore) + ((symbol-function 'erc-networks-mode) + (lambda (n) (push (cons 'networks n) calls))) + ((get 'erc-networks-mode 'standard-value) 'ignore) + ((symbol-function 'erc-completion-mode) + (lambda (n) (push (cons 'completion n) calls))) + ((get 'erc-completion-mode 'standard-value) 'ignore)) + + (ert-info ("Local modules") + (setq erc-modules '(fake-foo fake-bar)) + (should (equal (erc-update-modules t) '(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 calls nil)) + + (ert-info ("Module name overrides") + (setq erc-modules '(completion autojoin networks)) + (should-not (erc-update-modules t)) ; no locals + (should (equal (nreverse calls) '( erc-pcomplete (completion . 1) + erc-join (autojoin . 1) + erc-networks (networks . 1)))) + (setq calls nil)) + + (ert-info ("Reenabling of local minor modes by `erc-open'") + (with-temp-buffer + (erc-mode) + (setq erc-modules '(completion autojoin networks)) + (let ((local-minor-modes '(font-lock-mode erc-fake-bar-mode))) + (should (equal (erc-update-modules t) '(erc-fake-bar-mode)))) + (should (equal (nreverse calls) + '( erc-pcomplete (completion . 1) + erc-join (autojoin . 1) + erc-networks (networks . 1))))))))) + ;;; erc-tests.el ends here -- 2.38.1