>From fea3ac6fcc199578ccf7c63f2a6b5685473a7c1e Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 12 Jul 2021 03:44:28 -0700 Subject: [PATCH 3/6] 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 already active as minor modes in an ERC buffer. * lisp/erc/erc.el (erc-migrate-modules): Add some missing mappings. (erc-update-modules): Add optional param that changes 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 toggle local modules (minor modes) unless `erc-mode' is the major mode. 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 asserting correct module-name mappings. (Bug#57955.) --- doc/misc/erc.texi | 11 +++++- etc/ERC-NEWS | 8 ++++ lisp/erc/erc-common.el | 56 +++++++++++++++++++++++---- lisp/erc/erc-compat.el | 12 ++++++ lisp/erc/erc-goodies.el | 1 + lisp/erc/erc.el | 77 ++++++++++++++++++++------------------ test/lisp/erc/erc-tests.el | 58 ++++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 45 deletions(-) diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 0d807e323e..dd15036b2e 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -390,8 +390,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 f638d4717a..4c4b154dca 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -104,6 +104,14 @@ messages during periods of heavy traffic no longer disappear. Although rare, server passwords containing white space are now handled correctly. +** ERC-mode hooks are more useful. +The function 'erc-update-modules' now supports an optional argument to +defer enabling of local modules and instead return their mode +commands. 'erc-open' relies on this to delay their activation, as +well as that of all 'erc-mode-hook' members, until most local session +variables have been initialized (minus those "server"- and +process-focused ones in erc-backend). + ** 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 23a1933798..b791866ee2 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -88,6 +88,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. @@ -101,7 +136,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: @@ -114,6 +151,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)))) @@ -137,16 +175,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 d23703394b..f7e6fb7aee 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -313,6 +313,18 @@ erc-compat--29-browse-url-irc (cons '("\\`irc6?s?://" . erc-compat--29-browse-url-irc) existing)))))) +(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 95212182b5..ef70aa1a21 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1791,10 +1791,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 @@ -1872,28 +1869,23 @@ 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) + (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))) + (push mode local-modes) + (funcall mode 1)))))) (defun erc-setup-buffer (buffer) "Consults `erc-join-buffer' to find out how to display `BUFFER'." @@ -1951,18 +1943,27 @@ 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) + (buffer-local-variables)))) (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 + (delete-dups (append (when continued-session + (erc-compat--local-module-modes)) + (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))) @@ -2019,14 +2020,20 @@ 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 (and erc--server-reconnecting + (alist-get 'erc-networks--id 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)) @@ -2038,8 +2045,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 a5100ec155..fecd17b10e 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -1178,4 +1178,62 @@ erc-handle-irc-url (kill-buffer "baznet") (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-deftest erc-compat--local-module-modes () + (with-temp-buffer + (if (< 27 emacs-major-version) + (let ((local-minor-modes '(font-lock-mode erc-fake-bar-mode))) + (should (equal (erc-compat--local-module-modes) + '(erc-fake-bar-mode)))) + (cl-letf (((symbol-function 'buffer-local-variables) + (lambda (&rest _) '((font-lock-mode) (erc-fake-bar-mode))))) + (should (equal (erc-compat--local-module-modes) + '(erc-fake-bar-mode))))))) + ;;; erc-tests.el ends here -- 2.38.1