>From e65ce41a8418591a026525dea53d1b950c74ec12 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Fri, 15 Sep 2023 06:08:55 -0700 Subject: [PATCH] [5.6] Improve erc-warn-about-blank-lines behavior * lisp/erc/erc-common.el (erc--input-split): Add `abortp' slot. Its purpose is to allow for making a premature exit while validating prompt input without having to trap or signaling `user-error' with an empty string. * lisp/erc/erc.el (erc-warn-about-blank-lines): Clarify meaning of "blank lines" and mention interaction with `erc-send-whitespace-lines'. (erc--input-review-functions): Move `erc--discard-trailing-multiline-nulls' after `erc--run-input-validation-checks'. (erc--blank-in-multiline-input-p): Remove function. (erc--check-prompt-input-for-something): New trivial validation function to check if the input is empty. (erc--count-blank-lines): New function that tallies up the number of blank lines and whitespace lines in the current input. (erc--check-prompt-input-for-multiline-blanks): Rework to provide more informative messages and more sensible behavior for common cases with respect to relevant option values. (erc--check-prompt-input-functions): Add new validation function `erc--check-prompt-for-something'. (erc--run-input-validation-checks): Set `abortp' slot of `erc--input-split' when returned object is a symbol, rather than printing a fallback error. Also accept a list of `message' arguments as another new return type. (erc-send-current-line): When the `abortp' slot of the current `erc--input-split' object is non-nil, forgo normal input processing. This fixes a regression in 5.6-git, which emits an error message when it should instead just exit the command. (erc--discard-trailing-multiline-nulls): Always run, regardless of `erc-send-whitespace-lines', and leave a blank line behind when stripping a message consisting of only blank lines. (erc--run-send-hooks): Always run hooks and adjacent logic rather than only when hooks are populated. * test/lisp/erc/erc-tests.el (erc--blank-in-multiline-input-p): Remove test. (erc--check-prompt-input-functions): Update expected message. (erc--discard-trailing-multiline-nulls, erc--count-blank-lines): New tests. (erc-tests--check-prompt-input--expect, erc-tests--check-prompt-input-messages): New helper variables. (erc--check-prompt-input-for-multiline-blanks, erc--check-prompt-input-for-multiline-blanks/explanations): New tests. --- lisp/erc/erc-common.el | 1 + lisp/erc/erc.el | 119 +++++++++++++++++++------- test/lisp/erc/erc-tests.el | 171 ++++++++++++++++++++++++++++--------- 3 files changed, 220 insertions(+), 71 deletions(-) diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index 67c2cf8535b..8d896e663b5 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -60,6 +60,7 @@ erc-input ((obsolete erc-send-this)) erc-send-this)))) (lines nil :type (list-of string)) + (abortp nil :type (list-of symbol)) (cmdp nil :type boolean)) (cl-defstruct (erc-server-user (:type vector) :named) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index ec4fae548c7..7165f38189e 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -249,7 +249,14 @@ erc-prompt-for-password :type 'boolean) (defcustom erc-warn-about-blank-lines t - "Warn the user if they attempt to send a blank line." + "Warn the user if they attempt to send a blank line. +When non-nil, ERC signals a `user-error' upon encountering prompt +input containing empty or whitespace-only lines. When nil, ERC +still inhibits sending but does so silently. With the companion +option `erc-send-whitespace-lines' enabled, ERC sends pending +input and prints a message in the echo area indicating the amount +of padding and/or stripping applied, if any. Setting this option +to nil suppresses such reporting." :group 'erc :type 'boolean) @@ -1092,9 +1099,9 @@ erc-pre-send-functions (define-obsolete-variable-alias 'erc--pre-send-split-functions 'erc--input-review-functions "30.1") -(defvar erc--input-review-functions '(erc--discard-trailing-multiline-nulls - erc--split-lines - erc--run-input-validation-checks) +(defvar erc--input-review-functions '(erc--split-lines + erc--run-input-validation-checks + erc--discard-trailing-multiline-nulls) "Special hook for reviewing and modifying prompt input. ERC runs this before clearing the prompt and before running any send-related hooks, such as `erc-pre-send-functions'. Thus, it's @@ -6421,20 +6428,6 @@ erc--input-line-delim-regexp (defvar erc-command-regexp "^/\\([A-Za-z']+\\)\\(\\s-+.*\\|\\s-*\\)$" "Regular expression used for matching commands in ERC.") -(defun erc--blank-in-multiline-input-p (lines) - "Detect whether LINES contains a blank line. -When `erc-send-whitespace-lines' is in effect, return nil if -LINES is multiline or the first line is non-empty. When -`erc-send-whitespace-lines' is nil, return non-nil when any line -is empty or consists of one or more spaces, tabs, or form-feeds." - (catch 'return - (let ((multilinep (cdr lines))) - (dolist (line lines) - (when (if erc-send-whitespace-lines - (and (string-empty-p line) (not multilinep)) - (string-match (rx bot (* (in " \t\f")) eot) line)) - (throw 'return t)))))) - (defun erc--check-prompt-input-for-excess-lines (_ lines) "Return non-nil when trying to send too many LINES." (when erc-inhibit-multiline-input @@ -6454,13 +6447,72 @@ erc--check-prompt-input-for-excess-lines (y-or-n-p (concat "Send input " msg "?"))) (concat "Too many lines " msg)))))) -(defun erc--check-prompt-input-for-multiline-blanks (_ lines) - "Return non-nil when multiline prompt input has blank LINES." - (when (erc--blank-in-multiline-input-p lines) +(defun erc--check-prompt-input-for-something (string _) + (when (string-empty-p string) (if erc-warn-about-blank-lines "Blank line - ignoring..." 'invalid))) +(defun erc--count-blank-lines (lines) + "Report on the number of whitespace-only and empty LINES. +Return a list of (BLANKS TO-PAD TO-STRIP). Expect caller to know +that BLANKS includes non-empty whitespace-only lines and that no +padding or stripping has yet occurred." + (let ((real 0) (total 0) (pad 0) (strip 0)) + (dolist (line lines) + (if (string-match (rx bot (* (in " \t\f")) eot) line) + (progn + (cl-incf total) + (if (zerop (match-end 0)) + (cl-incf strip) + (cl-incf pad strip) + (setq strip 0))) + (cl-incf real) + (unless (zerop strip) + (cl-incf pad strip) + (setq strip 0)))) + (when (and (zerop real) (not (zerop total)) (= total (+ pad strip))) + (cl-incf strip (1- pad)) + (setq pad 1)) + (list total pad strip))) + +(defun erc--check-prompt-input-for-multiline-blanks (_ lines) + "Return non-nil when multiline prompt input has blank LINES. +Consider newlines to be intervening delimiters, meaning the +zero-width logical line between a trailing newline and `eob' +constitutes a separate message." + (pcase-let ((`(,total ,pad ,strip)(erc--count-blank-lines lines))) + (cond ((zerop total) nil) + ((and erc-warn-about-blank-lines erc-send-whitespace-lines) + (let (msg args) + (unless (zerop strip) + (push "stripping (%d)" msg) + (push strip args)) + (unless (zerop pad) + (when msg + (push "and" msg)) + (push "padding (%d)" msg) + (push pad args)) + (when msg + (push "blank" msg) + (push (if (> (apply #'+ args) 1) "lines" "line") msg)) + (when msg + (setf msg (nreverse msg) + (car msg) (capitalize (car msg)))) + (and msg `(message ,(string-join msg " ") ,@(nreverse args))))) + (erc-warn-about-blank-lines + (concat (if (= total 1) + (if (zerop strip) "Blank" "Trailing") + (if (= total strip) + (format "%d trailing" strip) + (format "%d blank" total))) + (and (> total 1) (/= total strip) (not (zerop strip)) + (format " (%d trailing)" strip)) + (if (= total 1) " line" " lines") + " detected (see `erc-send-whitespace-lines')")) + (erc-send-whitespace-lines nil) + (t 'invalid)))) + (defun erc--check-prompt-input-for-point-in-bounds (_ _) "Return non-nil when point is before prompt." (when (< (point) (erc-beg-of-input-line)) @@ -6481,6 +6533,7 @@ erc--check-prompt-input-for-multiline-command (defvar erc--check-prompt-input-functions '(erc--check-prompt-input-for-point-in-bounds + erc--check-prompt-input-for-something erc--check-prompt-input-for-multiline-blanks erc--check-prompt-input-for-running-process erc--check-prompt-input-for-excess-lines @@ -6497,9 +6550,11 @@ erc--run-input-validation-checks 'erc--check-prompt-input-functions (erc--input-split-string state) (erc--input-split-lines state)))) - (unless (stringp msg) - (setq msg (format "Input error: %S" msg))) - (user-error msg))) + (cond ((eq (car-safe msg) 'message) + (apply 'message (cdr msg))) + ((stringp msg) + (user-error msg)) + (t (push msg (erc--input-split-abortp state)))))) (defun erc-send-current-line () "Parse current line and send it to IRC." @@ -6523,8 +6578,9 @@ erc-send-current-line str erc--input-line-delim-regexp) :cmdp (string-match erc-command-regexp str)))) (run-hook-with-args 'erc--input-review-functions state) - (let ((inhibit-read-only t) - (old-buf (current-buffer))) + (when-let (((not (erc--input-split-abortp state))) + (inhibit-read-only t) + (old-buf (current-buffer))) (progn ; unprogn this during next major surgery (erc-set-active-buffer (current-buffer)) ;; Kill the input and the prompt @@ -6553,12 +6609,11 @@ erc-user-input (erc-end-of-input-line))) (defun erc--discard-trailing-multiline-nulls (state) - "Ensure last line of STATE's string is non-null. -But only when `erc-send-whitespace-lines' is non-nil. STATE is -an `erc--input-split' object." - (when (and erc-send-whitespace-lines (erc--input-split-lines state)) + "Remove trailing empty lines from STATE, an `erc--input-split' object. +When all lines are empty, remove all but the first." + (when (erc--input-split-lines state) (let ((reversed (nreverse (erc--input-split-lines state)))) - (while (and reversed (string-empty-p (car reversed))) + (while (and (cdr reversed) (string-empty-p (car reversed))) (setq reversed (cdr reversed))) (setf (erc--input-split-lines state) (nreverse reversed))))) @@ -6578,7 +6633,7 @@ erc--run-send-hooks limits and pad empty ones, knowing full well that additional processing may still corrupt messages before they reach the send queue. Expect LINES-OBJ to be an `erc--input-split' object." - (when (or erc-send-pre-hook erc-pre-send-functions) + (progn ; FIXME remove `progn' after code review. (with-suppressed-warnings ((lexical str) (obsolete erc-send-this)) (defvar str) ; see note in string `erc-send-input'. (let* ((str (string-join (erc--input-split-lines lines-obj) "\n")) diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 05d45b2d027..bb7e3259608 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -1056,43 +1056,6 @@ erc--input-line-delim-regexp (should (equal '("" "" "") (split-string "\n\n" p))) (should (equal '("" "" "") (split-string "\n\r" p))))) -(ert-deftest erc--blank-in-multiline-input-p () - (let ((check (lambda (s) - (erc--blank-in-multiline-input-p - (split-string s erc--input-line-delim-regexp))))) - - (ert-info ("With `erc-send-whitespace-lines'") - (let ((erc-send-whitespace-lines t)) - (should (funcall check "")) - (should-not (funcall check "\na")) - (should-not (funcall check "/msg a\n")) ; real /cmd - (should-not (funcall check "a\n\nb")) ; "" allowed - (should-not (funcall check "/msg a\n\nb")) ; non-/cmd - (should-not (funcall check " ")) - (should-not (funcall check "\t")) - (should-not (funcall check "a\nb")) - (should-not (funcall check "a\n ")) - (should-not (funcall check "a\n \t")) - (should-not (funcall check "a\n \f")) - (should-not (funcall check "a\n \nb")) - (should-not (funcall check "a\n \t\nb")) - (should-not (funcall check "a\n \f\nb")))) - - (should (funcall check "")) - (should (funcall check " ")) - (should (funcall check "\t")) - (should (funcall check "a\n\nb")) - (should (funcall check "a\n\nb")) - (should (funcall check "a\n ")) - (should (funcall check "a\n \t")) - (should (funcall check "a\n \f")) - (should (funcall check "a\n \nb")) - (should (funcall check "a\n \t\nb")) - - (should-not (funcall check "a\rb")) - (should-not (funcall check "a\nb")) - (should-not (funcall check "a\r\nb")))) - (defun erc-tests--with-process-input-spy (test) (with-current-buffer (get-buffer-create "FakeNet") (let* ((erc--input-review-functions @@ -1138,7 +1101,7 @@ erc--check-prompt-input-functions (delete-region (point) (point-max)) (insert "one\n") (let ((e (should-error (erc-send-current-line)))) - (should (equal "Blank line - ignoring..." (cadr e)))) + (should (string-prefix-p "Trailing line detected" (cadr e)))) (goto-char (point-max)) (ert-info ("Input remains untouched") (should (save-excursion (goto-char erc-input-marker) @@ -1180,6 +1143,136 @@ erc-send-current-line (should (consp erc-last-input-time))))) +(ert-deftest erc--discard-trailing-multiline-nulls () + (pcase-dolist (`(,input ,want) '((("") ("")) + (("" "") ("")) + (("a") ("a")) + (("a" "") ("a")) + (("" "a") ("" "a")) + (("" "a" "") ("" "a")))) + (ert-info ((format "Input: %S, want: %S" input want)) + (let ((s (make-erc--input-split :lines input))) + (erc--discard-trailing-multiline-nulls s) + (should (equal (erc--input-split-lines s) want)))))) + +(ert-deftest erc--count-blank-lines () + (pcase-dolist (`(,input ,want) '((() (0 0 0)) + (("") (1 1 0)) + (("" "") (2 1 1)) + (("" "" "") (3 1 2)) + ((" " "") (2 0 1)) + ((" " "" "") (3 0 2)) + (("" " " "") (3 1 1)) + (("" "" " ") (3 2 0)) + (("a") (0 0 0)) + (("a" "") (1 0 1)) + (("a" " " "") (2 0 1)) + (("a" "" "") (2 0 2)) + (("a" "b") (0 0 0)) + (("a" "" "b") (1 1 0)) + (("a" " " "b") (1 0 0)) + (("" "a") (1 1 0)) + ((" " "a") (1 0 0)) + (("" "a" "") (2 1 1)) + (("" " " "a" "" " ") (4 2 0)) + (("" " " "a" "" " " "") (5 2 1)))) + (ert-info ((format "Input: %S, want: %S" input want)) + (should (equal (erc--count-blank-lines input) want))))) + +;; Opt `wb': `erc-warn-about-blank-lines' +;; Opt `sw': `erc-send-whitespace-lines' +;; `s': " \n",`a': "a\n",`b': "b\n" +(defvar erc-tests--check-prompt-input--expect + ;; opts "" " " "\n" "\n " " \n" "\n\n" "a\n" "a\n " "a\n \nb" + '(((+wb -sw) err err err err err err err err err) + ((-wb -sw) nop nop nop nop nop nop nop nop nop) + ((+wb +sw) err (s) (0 s) (1 s s) (s) (0 s) (0 a) (a s) (a s b)) + ((-wb +sw) nop (s) (s) (s s) (s) (s) (a) (a s) (a s b)))) + +;; Help messages echoed (not IRC message) was emitted +(defvar erc-tests--check-prompt-input-messages + '("Stripping" "Padding")) + +(ert-deftest erc--check-prompt-input-for-multiline-blanks () + (erc-tests--with-process-input-spy + (lambda (next) + (erc-tests--set-fake-server-process "sleep" "1") + (should-not erc-send-whitespace-lines) + (should erc-warn-about-blank-lines) + + (pcase-dolist (`((,wb ,sw) . ,ex) erc-tests--check-prompt-input--expect) + (let ((print-escape-newlines t) + (erc-warn-about-blank-lines (eq wb '+wb)) + (erc-send-whitespace-lines (eq sw '+sw)) + (samples '("" " " "\n" "\n " " \n" "\n\n" + "a\n" "a\n " "a\n \nb"))) + (setq ex `(,@ex (a) (a b)) ; baseline, same for all combos + samples `(,@samples "a" "a\nb")) + (dolist (input samples) + (insert input) + (ert-info ((format "Opts: %S, Input: %S, want: %S" + (list wb sw) input (car ex))) + (ert-with-message-capture messages + (pcase-exhaustive (pop ex) + ('err (let ((e (should-error (erc-send-current-line)))) + (should (string-match (rx (| "trailing" "blank")) + (cadr e)))) + (should (equal (erc-user-input) input)) + (should-not (funcall next))) + ('nop (erc-send-current-line) + (should (equal (erc-user-input) input)) + (should-not (funcall next))) + ('clr (erc-send-current-line) + (should (string-empty-p (erc-user-input))) + (should-not (funcall next))) + ((and (pred consp) v) + (erc-send-current-line) + (should (string-empty-p (erc-user-input))) + (setq v (reverse v)) ; don't use `nreverse' here + (while v + (pcase (pop v) + ((and (pred integerp) n) + (should (string-search + (nth n erc-tests--check-prompt-input-messages) + messages))) + ('s (should (equal " \n" (car (funcall next))))) + ('a (should (equal "a\n" (car (funcall next))))) + ('b (should (equal "b\n" (car (funcall next))))))) + (should-not (funcall next)))))) + (delete-region erc-input-marker (point-max)))))))) + +(ert-deftest erc--check-prompt-input-for-multiline-blanks/explanations () + (should erc-warn-about-blank-lines) + (should-not erc-send-whitespace-lines) + + (let ((erc-send-whitespace-lines t)) + (pcase-dolist (`(,input ,msg) + '((("") "Padding (1) blank line") + (("" " ") "Padding (1) blank line") + ((" " "") "Stripping (1) blank line") + (("a" "") "Stripping (1) blank line") + (("" "") "Stripping (1) and padding (1) blank lines") + (("" "" "") "Stripping (2) and padding (1) blank lines") + (("" "a" "" "b" "" "c" "" "") + "Stripping (2) and padding (3) blank lines"))) + (ert-info ((format "Input: %S, Msg: %S" input msg)) + (let ((rv (erc--check-prompt-input-for-multiline-blanks nil input))) + (should (equal (apply #'format (cdr rv)) msg)))))) + + (pcase-dolist (`(,input ,msg) + '((("") "Blank line detected") + (("" " ") "2 blank lines detected") + ((" " "") "2 blank (1 trailing) lines detected") + (("a" "") "Trailing line detected") + (("" "") "2 blank (1 trailing) lines detected") + (("a" "" "") "2 trailing lines detected") + (("" "a" "" "b" "" "c" "" "") + "5 blank (2 trailing) lines detected"))) + (ert-info ((format "Input: %S, Msg: %S" input msg)) + (let ((rv (erc--check-prompt-input-for-multiline-blanks nil input))) + (should (equal (concat msg " (see `erc-send-whitespace-lines')") + rv )))))) + (ert-deftest erc-send-whitespace-lines () (erc-tests--with-process-input-spy (lambda (next) @@ -1196,7 +1289,7 @@ erc-send-whitespace-lines (erc-bol) (should (eq (point) (point-max)))) (should (equal (funcall next) '("two\n" nil t))) - (should (equal (funcall next) '("\n" nil t))) + (should (equal (funcall next) '(" \n" nil t))) (should (equal (funcall next) '("one\n" nil t)))) (ert-info ("Multiline hunk with trailing newline filtered") -- 2.41.0