diff options
| author | F. Jason Park | 2022-03-21 05:40:16 -0700 |
|---|---|---|
| committer | F. Jason Park | 2022-06-30 15:19:53 -0700 |
| commit | f46547294d2684d80bb473bd4c85f273ff661a7d (patch) | |
| tree | 9957e4f497d0588560cad7639441e0b01ca8b123 /lisp/erc | |
| parent | a9d89d083ac5bf0b9fd5568d42e565aba0b6e13f (diff) | |
| download | emacs-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.el | 161 |
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. | ||
| 1135 | The functions are called with one argument, an `erc--input-split' | ||
| 1136 | struct, which they can optionally modify. | ||
| 1137 | |||
| 1138 | The 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 | |||
| 1146 | The `string' field is effectively read-only. When `cmdp' is | ||
| 1147 | non-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. |
| 1128 | Functions on `erc-insert-pre-hook' can set this variable to nil | 1151 | Functions 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'. |
| 5839 | If that function has never been called, the value is 0.") | 5862 | If 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. | ||
| 5881 | When `erc-send-whitespace-lines' is in effect, return nil if | ||
| 5882 | LINES is multiline or the first line is non-empty. When | ||
| 5883 | `erc-send-whitespace-lines' is nil, return non-nil when any line | ||
| 5884 | is 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. | ||
| 5916 | Called with latest input string submitted by user and the list of | ||
| 5917 | lines produced by splitting it. If any member function returns | ||
| 5918 | non-nil, processing is abandoned and input is left untouched. | ||
| 5919 | When 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. | ||
| 5986 | But only when `erc-send-whitespace-lines' is non-nil. STATE is | ||
| 5987 | an `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. |
| 5915 | It is assumed that the input and the prompt is already deleted. | 5997 | It is assumed that the input and the prompt is already deleted. |
| 5916 | Return non-nil only if we actually send anything." | 5998 | Return 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." |