aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/erc
diff options
context:
space:
mode:
authorF. Jason Park2022-03-21 05:40:16 -0700
committerF. Jason Park2022-06-30 15:19:53 -0700
commitf46547294d2684d80bb473bd4c85f273ff661a7d (patch)
tree9957e4f497d0588560cad7639441e0b01ca8b123 /lisp/erc
parenta9d89d083ac5bf0b9fd5568d42e565aba0b6e13f (diff)
downloademacs-f46547294d2684d80bb473bd4c85f273ff661a7d.tar.gz
emacs-f46547294d2684d80bb473bd4c85f273ff661a7d.zip
Improve ERC's handling of multiline prompt input
* lisp/erc/erc.el (erc--pre-send-split-functions): Add new internal hook allowing members to revise individual lines before sending. This was created with an eye toward possibly exporting it publicly as a customizable option. (erc-last-input-time): Tweak meaning of variable to match likely original intent, which is that it's only updated on successful calls to `erc-send-current-line'. (erc--discard-trailing-multiline-nulls): Conditionally truncate list of lines to be sent, skipping trailing blanks. This constitutes a behavioral change. But, considering the nature of the bug being fixed, it is thought to be justified. (erc--input-split): Add new internal struct containing split input lines and flag for command detection. (erc--input-line-delim-regexp): Add regex var for splitting multiline prompt input. (erc--blank-in-multiline-p): Add helper for detecting blank lines. (erc--check-prompt-input-for-multiline-blanks, erc--check-prompt-input-for-point-in-bounds, erc--check-prompt-input-for-running-process): New functions to encapsulate logic for various pre-flight idiot checks. (erc--check-prompt-input-functions): Add new hook for validating prompt input prior to clearing it, internal for now. (erc-send-current-line): Pre-screen for blank lines and bail out if necessary. (erc-send-input): Add optional param to skip checking for blank lines. Call hook `erc--pre-send-split-functions'. * test/lisp/erc/erc-tests.el (erc-ring-previous-command): Use new test helper. (erc--input-line-delim-regexp, erc--blank-in-multiline-input-p): Add tests. (erc-tests--send-prep, erc-tests--set-fake-server-process, erc-tests--with-process-input-spy): Add test helpers. (erc--check-prompt-input-functions, erc-send-current-line, erc-send-whitespace-lines): Add tests. (Bug#54536)
Diffstat (limited to 'lisp/erc')
-rw-r--r--lisp/erc/erc.el161
1 files changed, 120 insertions, 41 deletions
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 971d3f426fc..89ce713fe02 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1123,6 +1123,29 @@ The struct has three slots:
1123 :type 'hook 1123 :type 'hook
1124 :version "27.1") 1124 :version "27.1")
1125 1125
1126;; This is being auditioned for possible exporting (as a custom hook
1127;; option). Likewise for (public versions of) `erc--input-split' and
1128;; `erc--discard-trailing-multiline-nulls'. If unneeded, we'll just
1129;; run the latter on the input after `erc-pre-send-functions', and
1130;; remove this hook and the struct completely. IOW, if you need this,
1131;; please say so.
1132
1133(defvar erc--pre-send-split-functions '(erc--discard-trailing-multiline-nulls)
1134 "Special hook for modifying individual lines in multiline prompt input.
1135The functions are called with one argument, an `erc--input-split'
1136struct, which they can optionally modify.
1137
1138The struct has five slots:
1139
1140 `string': the input string delivered by `erc-pre-send-functions'
1141 `insertp': whether to insert the lines into the buffer
1142 `sendp': whether the lines should be sent to the IRC server
1143 `lines': a list of lines to be sent, each one a `string'
1144 `cmdp': whether to interpret input as a command, like /ignore
1145
1146The `string' field is effectively read-only. When `cmdp' is
1147non-nil, all but the first line will be discarded.")
1148
1126(defvar erc-insert-this t 1149(defvar erc-insert-this t
1127 "Insert the text into the target buffer or not. 1150 "Insert the text into the target buffer or not.
1128Functions on `erc-insert-pre-hook' can set this variable to nil 1151Functions on `erc-insert-pre-hook' can set this variable to nil
@@ -5835,7 +5858,7 @@ Specifically, return the position of `erc-insert-marker'."
5835 (point-max)) 5858 (point-max))
5836 5859
5837(defvar erc-last-input-time 0 5860(defvar erc-last-input-time 0
5838 "Time of last call to `erc-send-current-line'. 5861 "Time of last successful call to `erc-send-current-line'.
5839If that function has never been called, the value is 0.") 5862If that function has never been called, the value is 0.")
5840 5863
5841(defcustom erc-accidental-paste-threshold-seconds 0.2 5864(defcustom erc-accidental-paste-threshold-seconds 0.2
@@ -5851,6 +5874,50 @@ submitted line to be intentional."
5851 :version "26.1" 5874 :version "26.1"
5852 :type '(choice number (other :tag "disabled" nil))) 5875 :type '(choice number (other :tag "disabled" nil)))
5853 5876
5877(defvar erc--input-line-delim-regexp (rx (| (: (? ?\r) ?\n) ?\r)))
5878
5879(defun erc--blank-in-multiline-input-p (lines)
5880 "Detect whether LINES contains a blank line.
5881When `erc-send-whitespace-lines' is in effect, return nil if
5882LINES is multiline or the first line is non-empty. When
5883`erc-send-whitespace-lines' is nil, return non-nil when any line
5884is empty or consists of one or more spaces, tabs, or form-feeds."
5885 (catch 'return
5886 (let ((multilinep (cdr lines)))
5887 (dolist (line lines)
5888 (when (if erc-send-whitespace-lines
5889 (and (string-empty-p line) (not multilinep))
5890 (string-match (rx bot (* (in " \t\f")) eot) line))
5891 (throw 'return t))))))
5892
5893(defun erc--check-prompt-input-for-multiline-blanks (_ lines)
5894 "Return non-nil when multiline prompt input has blank LINES."
5895 (when (erc--blank-in-multiline-input-p lines)
5896 (if erc-warn-about-blank-lines
5897 "Blank line - ignoring..."
5898 'invalid)))
5899
5900(defun erc--check-prompt-input-for-point-in-bounds (_ _)
5901 "Return non-nil when point is before prompt."
5902 (when (< (point) (erc-beg-of-input-line))
5903 "Point is not in the input area"))
5904
5905(defun erc--check-prompt-input-for-running-process (string _)
5906 "Return non-nil unless in an active ERC server buffer."
5907 (unless (or (erc-server-buffer-live-p)
5908 (erc-command-no-process-p string))
5909 "ERC: No process running"))
5910
5911(defvar erc--check-prompt-input-functions
5912 '(erc--check-prompt-input-for-point-in-bounds
5913 erc--check-prompt-input-for-multiline-blanks
5914 erc--check-prompt-input-for-running-process)
5915 "Validators for user input typed at prompt.
5916Called with latest input string submitted by user and the list of
5917lines produced by splitting it. If any member function returns
5918non-nil, processing is abandoned and input is left untouched.
5919When the returned value is a string, pass it to `erc-error'.")
5920
5854(defun erc-send-current-line () 5921(defun erc-send-current-line ()
5855 "Parse current line and send it to IRC." 5922 "Parse current line and send it to IRC."
5856 (interactive) 5923 (interactive)
@@ -5864,20 +5931,21 @@ submitted line to be intentional."
5864 (eolp)) 5931 (eolp))
5865 (expand-abbrev)) 5932 (expand-abbrev))
5866 (widen) 5933 (widen)
5867 (if (< (point) (erc-beg-of-input-line)) 5934 (if-let* ((str (erc-user-input))
5868 (erc-error "Point is not in the input area") 5935 (msg (run-hook-with-args-until-success
5936 'erc--check-prompt-input-functions str
5937 (split-string str erc--input-line-delim-regexp))))
5938 (when (stringp msg)
5939 (erc-error msg))
5869 (let ((inhibit-read-only t) 5940 (let ((inhibit-read-only t)
5870 (str (erc-user-input))
5871 (old-buf (current-buffer))) 5941 (old-buf (current-buffer)))
5872 (if (and (not (erc-server-buffer-live-p)) 5942 (progn ; unprogn this during next major surgery
5873 (not (erc-command-no-process-p str)))
5874 (erc-error "ERC: No process running")
5875 (erc-set-active-buffer (current-buffer)) 5943 (erc-set-active-buffer (current-buffer))
5876 ;; Kill the input and the prompt 5944 ;; Kill the input and the prompt
5877 (delete-region (erc-beg-of-input-line) 5945 (delete-region (erc-beg-of-input-line)
5878 (erc-end-of-input-line)) 5946 (erc-end-of-input-line))
5879 (unwind-protect 5947 (unwind-protect
5880 (erc-send-input str) 5948 (erc-send-input str 'skip-ws-chk)
5881 ;; Fix the buffer if the command didn't kill it 5949 ;; Fix the buffer if the command didn't kill it
5882 (when (buffer-live-p old-buf) 5950 (when (buffer-live-p old-buf)
5883 (with-current-buffer old-buf 5951 (with-current-buffer old-buf
@@ -5892,8 +5960,8 @@ submitted line to be intentional."
5892 (set-buffer-modified-p buffer-modified)))))) 5960 (set-buffer-modified-p buffer-modified))))))
5893 5961
5894 ;; Only when last hook has been run... 5962 ;; Only when last hook has been run...
5895 (run-hook-with-args 'erc-send-completed-hook str)))) 5963 (run-hook-with-args 'erc-send-completed-hook str)))
5896 (setq erc-last-input-time now)) 5964 (setq erc-last-input-time now)))
5897 (switch-to-buffer "*ERC Accidental Paste Overflow*") 5965 (switch-to-buffer "*ERC Accidental Paste Overflow*")
5898 (lwarn 'erc :warning 5966 (lwarn 'erc :warning
5899 "You seem to have accidentally pasted some text!")))) 5967 "You seem to have accidentally pasted some text!"))))
@@ -5910,21 +5978,31 @@ submitted line to be intentional."
5910(cl-defstruct erc-input 5978(cl-defstruct erc-input
5911 string insertp sendp) 5979 string insertp sendp)
5912 5980
5913(defun erc-send-input (input) 5981(cl-defstruct (erc--input-split (:include erc-input))
5982 lines cmdp)
5983
5984(defun erc--discard-trailing-multiline-nulls (state)
5985 "Ensure last line of STATE's string is non-null.
5986But only when `erc-send-whitespace-lines' is non-nil. STATE is
5987an `erc--input-split' object."
5988 (when (and erc-send-whitespace-lines (erc--input-split-lines state))
5989 (let ((reversed (nreverse (erc--input-split-lines state))))
5990 (when (string-empty-p (car reversed))
5991 (pop reversed)
5992 (setf (erc--input-split-cmdp state) nil))
5993 (nreverse (seq-drop-while #'string-empty-p reversed)))))
5994
5995(defun erc-send-input (input &optional skip-ws-chk)
5914 "Treat INPUT as typed in by the user. 5996 "Treat INPUT as typed in by the user.
5915It is assumed that the input and the prompt is already deleted. 5997It is assumed that the input and the prompt is already deleted.
5916Return non-nil only if we actually send anything." 5998Return non-nil only if we actually send anything."
5917 ;; Handle different kinds of inputs 5999 ;; Handle different kinds of inputs
5918 (cond 6000 (if (and (not skip-ws-chk)
5919 ;; Ignore empty input 6001 (erc--check-prompt-input-for-multiline-blanks
5920 ((if erc-send-whitespace-lines 6002 input (split-string input erc--input-line-delim-regexp)))
5921 (string= input "") 6003 (when erc-warn-about-blank-lines
5922 (string-match "\\`[ \t\r\f\n]*\\'" input)) 6004 (message "Blank line - ignoring...") ; compat
5923 (when erc-warn-about-blank-lines 6005 (beep))
5924 (message "Blank line - ignoring...")
5925 (beep))
5926 nil)
5927 (t
5928 ;; This dynamic variable is used by `erc-send-pre-hook'. It's 6006 ;; This dynamic variable is used by `erc-send-pre-hook'. It's
5929 ;; obsolete, and when it's finally removed, this binding should 6007 ;; obsolete, and when it's finally removed, this binding should
5930 ;; also be removed. 6008 ;; also be removed.
@@ -5944,27 +6022,28 @@ Return non-nil only if we actually send anything."
5944 :insertp erc-insert-this 6022 :insertp erc-insert-this
5945 :sendp erc-send-this)) 6023 :sendp erc-send-this))
5946 (run-hook-with-args 'erc-pre-send-functions state) 6024 (run-hook-with-args 'erc-pre-send-functions state)
6025 (setq state (make-erc--input-split
6026 :string (erc-input-string state)
6027 :insertp (erc-input-insertp state)
6028 :sendp (erc-input-sendp state)
6029 :lines (split-string (erc-input-string state)
6030 erc--input-line-delim-regexp)
6031 :cmdp (string-match erc-command-regexp
6032 (erc-input-string state))))
6033 (run-hook-with-args 'erc--pre-send-split-functions state)
5947 (when (and (erc-input-sendp state) 6034 (when (and (erc-input-sendp state)
5948 erc-send-this) 6035 erc-send-this)
5949 (let ((string (erc-input-string state))) 6036 (let ((lines (erc--input-split-lines state)))
5950 (if (or (if (>= emacs-major-version 28) 6037 (if (and (erc--input-split-cmdp state) (not (cdr lines)))
5951 (string-search "\n" string) 6038 (erc-process-input-line (concat (car lines) "\n") t nil)
5952 (string-match "\n" string)) 6039 (dolist (line lines)
5953 (not (string-match erc-command-regexp string))) 6040 (dolist (line (or (and erc-flood-protect (erc-split-line line))
5954 (mapc 6041 (list line)))
5955 (lambda (line) 6042 (when (erc-input-insertp state)
5956 (mapc 6043 (erc-display-msg line))
5957 (lambda (line) 6044 (erc-process-input-line (concat line "\n")
5958 ;; Insert what has to be inserted for this. 6045 (null erc-flood-protect) t))))
5959 (when (erc-input-insertp state) 6046 t)))))
5960 (erc-display-msg line))
5961 (erc-process-input-line (concat line "\n")
5962 (null erc-flood-protect) t))
5963 (or (and erc-flood-protect (erc-split-line line))
5964 (list line))))
5965 (split-string string "\n"))
5966 (erc-process-input-line (concat string "\n") t nil))
5967 t))))))
5968 6047
5969(defun erc-display-msg (line) 6048(defun erc-display-msg (line)
5970 "Display LINE as a message of the user to the current target at point." 6049 "Display LINE as a message of the user to the current target at point."