From af5dd1ceb407c445bfa6f27ec737f989329bbdc4 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Fri, 1 Nov 2024 06:30:22 -0700 Subject: [PATCH 0/3] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (3): [5.7] Use speaker-end marker in ERC insertion hooks [5.7] Introduce lower level erc-match API [5.7] Use erc-match-type API for erc-desktop-notifications doc/misc/erc.texi | 343 ++++++++++++--- etc/ERC-NEWS | 22 + lisp/erc/erc-desktop-notifications.el | 69 ++- lisp/erc/erc-fill.el | 20 +- lisp/erc/erc-match.el | 416 ++++++++++++++++-- lisp/erc/erc.el | 48 +- .../erc/erc-desktop-notifications-tests.el | 115 +++++ test/lisp/erc/erc-match-tests.el | 214 ++++++++- 8 files changed, 1137 insertions(+), 110 deletions(-) create mode 100644 test/lisp/erc/erc-desktop-notifications-tests.el Interdiff: diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index b0cb6b0a815..49dbfe3623a 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -2081,11 +2081,12 @@ Match API the message being processed. That message's formatted body occupies the narrowed buffer when ERC creates and provides access to each @code{erc-match} instance. To use this interface, you add a - @dfn{constructor}-like function to the list @code{erc-match-types}: + @dfn{constructor}-like function to the hook + @code{erc-match-functions}: - @defopt erc-match-types + @defopt erc-match-functions - A hook-like list of functions, where each accepts the parameters named + An abnormal hook for which each member accepts the parameters named above as an @samp{&rest}-style plist and returns a new @code{erc-match} instance. A function can also be a traditional @code{cl-defstruct}-provided constructor belonging to a @dfn{subtype} @@ -2141,8 +2142,8 @@ Match API :handler (lambda (_) (cl-incf my-mentions)) plist)) -(setopt erc-match-types (add-to-list 'erc-match-types #'my-match) - erc-prompt (lambda () (format "%d!" my-mentions))) +(add-hook 'erc-match-functions #'my-match) +(setopt erc-prompt (lambda () (format "%d!" my-mentions))) @end lisp @noindent @@ -2153,14 +2154,16 @@ Match API on chat content by filtering out non-@samp{PRIVMSG} messages via the slot @samp{command}. -For a detailed example showing how to use this API for more involved -matching that doesn't involve highlighting, see the @samp{notifications} -module, which lives in @file{erc-desktop-notifications.el}. Ignore the -parts that involve adapting the global setup (and teardown) business to -a buffer-local context. Since your module is declared @code{local}, as -per the modern convention, you won't be needing such code, so feel free -to use utility functions like @code{erc-match-add-local-type} directly -in your module's definition. +For a detailed example of matching without highlighting, see the +@samp{jabbycat} demo module, available on ERC's dev-oriented package +archive: @uref{https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html}. +If you're in a hurry, check out @file{erc-desktop-notifications.el}, +which ships with ERC, but please ignore the parts that involve adapting +the global setup (and teardown) business to a buffer-local context. +Since your module is declared @code{local}, as per the modern +convention, you won't be needing such code, so feel free to do things +like add local members to @code{erc-match-functions} in your module's +definition. @anchor{highlighting} @subsection Highlighting @@ -2188,7 +2191,7 @@ Match API :face 'my-face plist)) -(setopt erc-match-types (add-to-list 'erc-match-types #'my-match)) +(add-hook 'erc-match-functions #'my-match) @end lisp @noindent @@ -2210,6 +2213,11 @@ Match API @code{nick}, @code{message}, @code{all}, @code{keyword}, @code{nick-or-keyword}, and @code{nick-or-mention}. +The complement to the @samp{part} slot is @samp{data}, which holds the +value of the module's option corresponding to the specific type. For +example, ERC initializes the @samp{data} slot for the +@code{erc-match-opt-pal} type with the value of @code{erc-pals}. + The default handler, @code{erc-match-highlight}, does its work by deferring to a purpose-built @dfn{method} meant to handle @samp{part}-based highlighting: @@ -2254,12 +2262,11 @@ Match API ((cdr (assoc (erc-target) chans)))))) (cl-defstruct (my-match (:include erc-match-opt-keyword - (part 'keyword) (data (my-get-keyword)) (face 'my-keyword)) (:constructor my-match))) -(setopt erc-match-types (add-to-list 'erc-match-types #'my-match)) +(add-hook 'erc-match-functions #'my-match) (cl-defmethod erc-match-highlight-by-part ((instance my-match) (_ (eql keyword))) @@ -2272,9 +2279,13 @@ Match API @end lisp @noindent -(Note that in the method body, you @emph{could} technically skip to the +Note that in the method body, you @emph{could} technically skip to the beginning of the last match for the first go around because the match -data from the @samp{predicate} is still fresh.) +data from the @samp{predicate} is still fresh. Also, while the method +could simply call @code{my-get-keyword} directly instead of accessing +the @samp{data} slot and also reference the @code{my-keyword} face +instead of using the @samp{face} slot, other methods may need to share +@samp{data} or alter @samp{face}. @node Options diff --git a/lisp/erc/erc-desktop-notifications.el b/lisp/erc/erc-desktop-notifications.el index adc90e1f544..2d605ced5f5 100644 --- a/lisp/erc/erc-desktop-notifications.el +++ b/lisp/erc/erc-desktop-notifications.el @@ -128,9 +128,10 @@ notifications (defun erc-desktop-notifications--setup () (if erc-notifications-mode - (erc-match-add-local-type #'erc-desktop-notifications--match-type-query) - (erc-match-remove-local-type - #'erc-desktop-notifications--match-type-query))) + (add-hook 'erc-match-functions + #'erc-desktop-notifications--match-type-query 0 t) + (remove-hook 'erc-match-functions + #'erc-desktop-notifications--match-type-query t))) (defvar erc-desktop-notifications-match-query-commands '(PRIVMSG) "IRC commands considered in query buffers for notification. diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el index c59eaa0ad6c..33be982477c 100644 --- a/lisp/erc/erc-match.el +++ b/lisp/erc/erc-match.el @@ -274,21 +274,20 @@ erc-match-quote-when-adding (const t) (const nil))) -(defcustom erc-match-types '(erc-match-opt-pal - erc-match-opt-fool - erc-match-opt-dangerous-host - erc-match-opt-keyword - erc-match-opt-current-nick) +(defcustom erc-match-functions '(erc-match-opt-pal + erc-match-opt-fool + erc-match-opt-dangerous-host + erc-match-opt-keyword + erc-match-opt-current-nick) "Type constructors for \\+`match' processing. See the struct `erc-match' as well as Info node `(erc) Match API' for -further details." +details." :package-version '(ERC . "5.7") ; FIXME sync on release - :type '(set (function-item erc-match-opt-pal) - (function-item erc-match-opt-fool) - (function-item erc-match-opt-dangerous-host) - (function-item erc-match-opt-keyword) - (function-item erc-match-opt-current-nick) - (repeat :tag "User-specified constructor" :inline t function))) + :type '(hook :options (erc-match-opt-pal + erc-match-opt-fool + erc-match-opt-dangerous-host + erc-match-opt-keyword + erc-match-opt-current-nick))) ;; Internal variables: @@ -497,10 +496,10 @@ erc-match-directed-at-fool-p (cl-defstruct (erc-match (:constructor erc-match)) "Base type for text and user matching performed by the \\+`match' module. Users wishing to perform custom matching should add a constructor that -returns an instance of this type to the list `erc-match-types'. If the -`:predicate' slot's predicate returns non-nil after being called with -its own instance in the narrowed single-message buffer, ERC calls the -`:handler' slot's function with the same instance and with the match +returns an instance of this type to the hook `erc-match-functions'. If +the `:predicate' slot's predicate returns non-nil after being called +with its own instance in the narrowed single-message buffer, ERC calls +the `:handler' slot's function with the same instance and with the match data still intact. More details in Info node `(erc) Match API'." ( predicate (error "Keyword `:predicate' missing") :type function :documentation "Called in narrowed buffer with own instance.") @@ -771,22 +770,28 @@ erc-match-highlight (point-min)) (point-max)))))) -(defvar-local erc-match--types nil - "Additional `erc-match-types' for use by other modules.") - -(defun erc-match-add-local-type (function) - "Add FUNCTION to registered type in current buffer." - (push function erc-match--types)) - -(defun erc-match-remove-local-type (function) - "Remove FUNCTION from registered types in current buffer." - (unless (setq erc-match--types (delete function erc-match--types)) - (kill-local-variable 'erc-match--types))) - (defun erc-match-get-message-body (instance) "Return the message body in the narrowed buffer for match INSTANCE." (buffer-substring (erc-match-body-beg instance) (1- (point-max)))) +(defun erc-match--run-match (constructor spkr-beg spkr-end body-beg + nick sender command) + "Run :handler for for `erc-match' instance if :predicate returns non-nil. +Call CONSTRUCTOR with SPKR-BEG, SPKR-END, BODY-BEG, NICK SENDER, and +COMMAND to create said instance." + (when-let* ((instance (funcall constructor + :spkr-beg spkr-beg + :spkr-end spkr-end + :body-beg body-beg + :nick nick + :sender sender + :command command)) + ((or nick (not (erc-match-user-p instance)))) + ((goto-char (point-min))) + ((funcall (erc-match-predicate instance) instance))) + (funcall (erc-match-handler instance) instance) + nil)) + (defun erc-match--message () "Highlight matches in narrowed buffer's current message." (goto-char (point-min)) @@ -808,20 +813,9 @@ erc-match--message ((point-min))))) (command (erc--check-msg-prop 'erc--cmd))) (with-syntax-table erc-match-syntax-table - (dolist (type (if erc-match--types - (append erc-match--types erc-match-types) - erc-match-types)) - (when-let* ((instance (funcall type - :spkr-beg spkr-beg - :spkr-end spkr-end - :body-beg body-beg - :nick nick - :sender (erc-response.sender response) - :command command)) - ((or user-nuh (not (erc-match-user-p instance)))) - ((goto-char (point-min))) - ((funcall (erc-match-predicate instance) instance))) - (funcall (erc-match-handler instance) instance)))) + (run-hook-wrapped 'erc-match-functions #'erc-match--run-match + spkr-beg spkr-end body-beg nick + (erc-response.sender response) command)) (when (and erc--offset-marker (/= body-beg erc--offset-marker)) (setq erc--offset-marker body-beg)))) @@ -1067,9 +1061,7 @@ erc-match--setup ;; invisible properties managed by this module. (if erc-match-mode (erc-match-toggle-hidden-fools +1) - (erc-match-toggle-hidden-fools -1) - (when (null erc-match--types) - (kill-local-variable 'erc-match--types)))) + (erc-match-toggle-hidden-fools -1))) (defun erc-match-toggle-hidden-fools (arg) "Toggle fool visibility. diff --git a/test/lisp/erc/erc-match-tests.el b/test/lisp/erc/erc-match-tests.el index e8726ca148e..0b90867b32d 100644 --- a/test/lisp/erc/erc-match-tests.el +++ b/test/lisp/erc/erc-match-tests.el @@ -724,17 +724,17 @@ erc-log-matches/legacy (let ((erc-match-use-legacy-logic-p t)) (erc-match-tests--log-matches)))) -;; This demos bare bones usage of the `erc-match-types' API that opts -;; out of the "parts-based" framework. The user does not have to -;; provide a `:part' keyword because they've overridden the `:handler', -;; meaning `erc-match-highlight-by-part' never runs. This is somewhat -;; analogous but ultimately orthogonal to `erc-text-matched-hook' not -;; running because that happens on account of the user not specifying a -;; `:category' field. -(ert-deftest erc-match-types/api/non-parts-based () +;; This demos bare-bones usage of the `erc-match' API that implicitly +;; opts out of the traditional options and "parts"-based mechanism. The +;; user does not have to provide a `:part' keyword because they've +;; overridden the `:handler', meaning `erc-match-highlight-by-part' +;; never runs. This is somewhat analogous but ultimately orthogonal to +;; `erc-text-matched-hook' not running because that happens on account +;; of the user not specifying a `:category' field. +(ert-deftest erc-match-functions/api/non-parts-based () (let* ((results ()) (erc-text-matched-hook (lambda (&rest r) (push r results))) - (erc-match-types + (erc-match-functions (list (lambda (&rest plist) ;; Doing everything in `:pred' would also work if @@ -760,24 +760,26 @@ erc-match-types/api/non-parts-based ;; This one piggybacks on infrastructure supporting the traditional ;; `match' interface. -(ert-deftest erc-match-types/api/parts-based () +(ert-deftest erc-match-functions/api/parts-based () (let* ((results ()) (erc-text-matched-hook (lambda (&rest r) (push r results))) - (erc-match-types ())) + (erc-match-functions ())) (erc-match-tests--perform (lambda () ;; Use local setter for no particular reason. - (erc-match-add-local-type - (lambda (&rest plist) - (apply #'erc-match-traditional - :category 'keyword - :part 'keyword - :data '("alice") - :face 'error - :predicate (lambda (_) (search-forward "alice" nil t)) - plist))) + (add-hook 'erc-match-functions + (lambda (&rest plist) + (apply #'erc-match-traditional + :category 'keyword + :part 'keyword + :data '("alice") + :face 'error + :predicate (lambda (_) + (search-forward "alice" nil t)) + plist)) + 0 t) (erc-tests-common-add-cmem "bob") (erc-tests-common-add-cmem "Alice") -- 2.46.2