Improve ERC's internal invisibility API

* etc/ERC-NEWS: Mention that line endings have moved from the end to
the beginning of hidden messages.
* lisp/erc/erc-fill.el (erc-fill--wrap-ensure-dependencies): Warn when
users have `erc-legacy-invisible-bounds-p' enabled, and force it to
its default value of nil in the current buffer.
(erc-fill-wrap-mode, erc-fill-wrap-enable): Move business involving
compat variable for enabling legacy hidden-message behavior to helper.
* lisp/erc/erc-match.el (erc-match--hide-fools-offset-bounds): Move
internal variable from to main library file and rename to
`erc-legacy-invisible-bounds-p'.  Also make obsolete and flip
semantics so a non-nil value enables the traditional behavior.
(erc-match--hide-message): Move to main library file and rename to
`erc--hide-message'.  Add a property-value parameter instead of
hard-coding to `erc-match'.  Also, condition behavior on renamed
compatibility flag `erc-legacy-invisible-bounds-p'.
(erc-hide-fools): Call `erc--hide-message' with own value for
`invisible' property specifically for fools.  That is, use
`match-fools' rather than `erc-match' or `erc-match-fools' to save
room when visually inspecting.  This retains the module name as a
prefix to hopefully minimize collisions with invisibility spec members
owned by non-ERC minor modes.  The `timestamp' spec member owned by
erc-stamp likewise lacks a namespace prefix, but its feature/group
affiliation is self-evident.
(erc-match--modify-invisibility-spec): Use toggle command
non-interactively for adding and removing invisibility spec member.
(erc-match-toggle-hidden-fools): Add explicit override argument and
defer to general helper for actually modifying spec.
(erc-match--toggle-hidden): New helper for toggling invisibility
spec.
* lisp/erc/erc.el (erc--merge-prop): If new value is a list, prepend
onto existing.  Add note about possible space optimization.
(erc-legacy-invisible-bounds-p): New obsolete compat variable to
enable traditional pre-5.6 invisibility interval on hidden messages.
Replaces `erc-match--hide-fools-offset-bounds-p' but has an inverted
meaning.  The new default value of nil means invisibility covers a
shifted interval consisting of the message body plus the line ending
immediately preceding it.
(erc--hide-message): New function, formerly `erc-match--hide-message'
from erc-match.el introduced in ERC 5.6.
* test/lisp/erc/erc-scenarios-match.el:
(erc-scenarios-match--invisible-stamp): Fix comment and use API
function in interactive convenience setup.
(erc-scenarios-match--find-bol): New test helper.
(erc-scenarios-match--find-eol): Fix bug affecting interactive use.
(erc-scenarios-match--stamp-left-fools-invisible,
erc-scenarios-match--stamp-right-fools-invisible,
erc-scenarios-match--stamp-right-invisible-fill-wrap,
erc-scenarios-match--stamp-both-invisible-fill-static): Update
`invisible' property from `erc-match' to `match-fools'.
(erc-scenarios-match--stamp-right-fools-invisible,
erc-scenarios-match--stamp-both-invisible-fill-static): Move test
body to function of same name for use in multiple cases.
(erc-scenarios-match--stamp-right-fools-invisible--nooffset,
erc-scenarios-match--stamp-both-invisible-fill-static--nooffset): New
test variants asserting proper hiding with old pre-5.6 invisibility
interval.
* test/lisp/erc/erc-tests.el (erc-tests--equal-including-properties):
Relocate macro higher in same file.
(erc--merge-prop): New test.  (Bug#64301)
This commit is contained in:
F. Jason Park 2023-07-14 21:08:31 -07:00
parent 63d8b2a59a
commit af547c4bbe
6 changed files with 184 additions and 64 deletions

View file

@ -295,6 +295,15 @@ The 'fill' module is now defined by 'define-erc-module'. The same
goes for ERC's imenu integration, which has 'imenu' now appearing in
the default value of 'erc-modules'.
*** Hidden messages contain a preceding rather than trailing newline.
ERC has traditionally only offered to hide messages involving fools,
but plans are to make hiding more powerful. Anyone depending on the
existing behavior should be aware that hidden messages now start and
end one character earlier, so that hidden line endings precede rather
than follow accompanying text. However, an escape hatch is available
in the variable 'erc-legacy-invisible-bounds-p'. It reinstates the
old behavior, which is unsupported by newer modules and features.
*** 'erc-display-message' optionally combines faces.
Users may notice that ERC now inserts some important error messages in
a combination of 'erc-error-face' and 'erc-notice-face'. This is

View file

@ -324,11 +324,17 @@ is 0, reset to value of `erc-fill-wrap-visual-keys'."
;; Not sure if this is problematic because `erc-bol' takes no args.
"<remap> <erc-bol>" #'erc-fill--wrap-beginning-of-line)
(defvar erc-match-mode)
(defvar erc-button-mode)
(defvar erc-match--hide-fools-offset-bounds)
(defvar erc-legacy-invisible-bounds-p)
(defun erc-fill--wrap-ensure-dependencies ()
(with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
(when erc-legacy-invisible-bounds-p
(erc--warn-once-before-connect 'erc-fill-wrap-mode
"Module `fill-wrap' is incompatible with the obsolete compatibility"
" flag `erc-legacy-invisible-bounds-p'. Disabling locally in %s."
(current-buffer))
(setq-local erc-legacy-invisible-bounds-p nil)))
(let (missing-deps)
(unless erc-fill-mode
(push 'fill missing-deps)
@ -389,9 +395,6 @@ them to every line."
(setq erc-fill--function #'erc-fill-wrap)
(add-function :after (local 'erc-stamp--insert-date-function)
#'erc-fill--wrap-stamp-insert-prefixed-date)
(when (or (bound-and-true-p erc-match-mode) (memq 'match erc-modules))
(require 'erc-match)
(setq erc-match--hide-fools-offset-bounds t))
(when erc-fill-wrap-merge
(add-hook 'erc-button--prev-next-predicate-functions
#'erc-fill--wrap-merged-button-p nil t))

View file

@ -655,24 +655,10 @@ See `erc-log-match-format'."
(get-buffer (car buffer-cons))))))
(switch-to-buffer buffer-name)))
(defvar-local erc-match--hide-fools-offset-bounds nil)
(defun erc-hide-fools (match-type _nickuserhost _message)
"Hide comments from designated fools."
(when (eq match-type 'fool)
(erc-match--hide-message)))
(defun erc-match--hide-message ()
(progn ; FIXME raise sexp
(if erc-match--hide-fools-offset-bounds
(let ((beg (point-min))
(end (point-max)))
(save-restriction
(widen)
(erc--merge-prop (1- beg) (1- end) 'invisible 'erc-match)))
;; Before ERC 5.6, this also used to add an `intangible'
;; property, but the docs say it's now obsolete.
(erc--merge-prop (point-min) (point-max) 'invisible 'erc-match))))
(erc--hide-message 'match-fools)))
(defun erc-beep-on-match (match-type _nickuserhost _message)
"Beep when text matches.
@ -682,19 +668,31 @@ This function is meant to be called from `erc-text-matched-hook'."
(defun erc-match--modify-invisibility-spec ()
"Add an `erc-match' property to the local spec."
;; Hopefully, this will be extended to do the same for other
;; invisible properties managed by this module.
(if erc-match-mode
(add-to-invisibility-spec 'erc-match)
(erc-match-toggle-hidden-fools +1)
(erc-with-all-buffers-of-server nil nil
(remove-from-invisibility-spec 'erc-match))))
(erc-match-toggle-hidden-fools -1))))
(defun erc-match-toggle-hidden-fools ()
(defun erc-match-toggle-hidden-fools (arg)
"Toggle fool visibility.
Expect `erc-hide-fools' or a function that does something similar
to be in `erc-text-matched-hook'."
(interactive)
(if (memq 'erc-match (ensure-list buffer-invisibility-spec))
(remove-from-invisibility-spec 'erc-match)
(add-to-invisibility-spec 'erc-match)))
Expect the function `erc-hide-fools' or similar to be present in
`erc-text-matched-hook'."
(interactive "P")
(erc-match--toggle-hidden 'match-fools arg))
(defun erc-match--toggle-hidden (prop arg)
"Toggle invisibility for spec member PROP.
Treat ARG in a manner similar to mode toggles defined by
`define-minor-mode'."
(when arg
(setq arg (prefix-numeric-value arg)))
(if (memq prop (ensure-list buffer-invisibility-spec))
(unless (natnump arg)
(remove-from-invisibility-spec prop))
(when (or (not arg) (natnump arg))
(add-to-invisibility-spec prop))))
(provide 'erc-match)

View file

@ -3011,22 +3011,51 @@ If STRING is nil, the function does nothing."
(defvar erc--compose-text-properties nil
"Non-nil when `erc-put-text-property' defers to `erc--merge-prop'.")
;; To save space, we could maintain a map of all readable property
;; values and optionally dispense archetypal constants in their place
;; in order to ensure all occurrences of some list (a b) across all
;; text-properties in all ERC buffers are actually the same object.
(defun erc--merge-prop (from to prop val &optional object)
"Compose existing PROP values with VAL between FROM and TO in OBJECT.
"Combine existing PROP values with VAL between FROM and TO in OBJECT.
For spans where PROP is non-nil, cons VAL onto the existing
value, ensuring a proper list. Otherwise, just set PROP to VAL.
See also `erc-button-add-face'."
When VAL is itself a list, prepend its members onto an existing
value. See also `erc-button-add-face'."
(let ((old (get-text-property from prop object))
(pos from)
(end (next-single-property-change from prop object to))
new)
(while (< pos to)
(setq new (if old (cons val (ensure-list old)) val))
(setq new (if old
(if (listp val)
(append val (ensure-list old))
(cons val (ensure-list old)))
val))
(put-text-property pos end prop new object)
(setq pos end
old (get-text-property pos prop object)
end (next-single-property-change pos prop object to)))))
(defvar erc-legacy-invisible-bounds-p nil
"Whether to hide trailing rather than preceding newlines.
Beginning in ERC 5.6, invisibility extends from a message's
preceding newline to its last non-newline character.")
(make-obsolete-variable 'erc-legacy-invisible-bounds-p
"decremented interval now permanent" "30.1")
(defun erc--hide-message (value)
"Apply `invisible' text-property with VALUE to current message.
Expect to run in a narrowed buffer during message insertion."
(if erc-legacy-invisible-bounds-p
;; Before ERC 5.6, this also used to add an `intangible'
;; property, but the docs say it's now obsolete.
(erc--merge-prop (point-min) (point-max) 'invisible value)
(let ((beg (point-min))
(end (point-max)))
(save-restriction
(widen)
(erc--merge-prop (1- beg) (1- end) 'invisible value)))))
(defun erc-display-message-highlight (type string)
"Highlight STRING according to TYPE, where erc-TYPE-face is an ERC face.

View file

@ -62,11 +62,15 @@
'erc-current-nick-face))))))
;; When hacking on tests that use this fixture, it's best to run it
;; interactively, and check for wierdness before and after doing
;; M-: (remove-from-invisibility-spec 'erc-match) RET.
;; interactively, and visually inspect the output with various
;; combinations of:
;;
;; M-x erc-match-toggle-hidden-fools RET
;; M-x erc-toggle-timestamps RET
;;
(defun erc-scenarios-match--invisible-stamp (hiddenp visiblep)
(unless noninteractive
(kill-new "(remove-from-invisibility-spec 'erc-match)"))
(kill-new "erc-match-toggle-hidden-fools"))
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "join/legacy")
@ -128,11 +132,11 @@
;; Leading stamp has combined `invisible' property value.
(should (equal (get-text-property (pos-bol) 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; Message proper has the `invisible' property `erc-match'.
;; Message proper has the `invisible' property `match-fools'.
(let ((msg-beg (next-single-property-change (pos-bol) 'invisible)))
(should (eq (get-text-property msg-beg 'invisible) 'erc-match))
(should (eq (get-text-property msg-beg 'invisible) 'match-fools))
(should (>= (next-single-property-change msg-beg 'invisible nil)
(pos-eol)))))
@ -147,19 +151,29 @@
(= (next-single-property-change msg-beg 'invisible nil (pos-eol))
(pos-eol))))))))
(defun erc-scenarios-match--find-bol ()
(save-excursion
(should (get-text-property (1- (point)) 'erc-command))
(goto-char (should (previous-single-property-change (point) 'erc-command)))
(pos-bol)))
(defun erc-scenarios-match--find-eol ()
(save-excursion
(goto-char (next-single-property-change (point) 'erc-command))
(if-let ((next (next-single-property-change (point) 'erc-command)))
(goto-char next)
;; We're already at the end of the message.
(should (get-text-property (1- (point)) 'erc-command)))
(pos-eol)))
;; In most cases, `erc-hide-fools' makes line endings invisible.
(ert-deftest erc-scenarios-match--stamp-right-fools-invisible ()
(defun erc-scenarios-match--stamp-right-fools-invisible ()
:tags '(:expensive-test)
(let ((erc-insert-timestamp-function #'erc-insert-timestamp-right))
(erc-scenarios-match--invisible-stamp
(lambda ()
(let ((end (erc-scenarios-match--find-eol)))
(let ((beg (erc-scenarios-match--find-bol))
(end (erc-scenarios-match--find-eol)))
;; The end of the message is a newline.
(should (= ?\n (char-after end)))
@ -168,19 +182,23 @@
;; Stamps have a combined `invisible' property value.
(should (equal (get-text-property (1- end) 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; The final newline is hidden by `match', not `stamps'
(should (equal (get-text-property end 'invisible) 'erc-match))
(with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
(if erc-legacy-invisible-bounds-p
(should (eq (get-text-property end 'invisible) 'match-fools))
(should (eq (get-text-property beg 'invisible) 'match-fools))
(should-not (get-text-property end 'invisible))))
;; The message proper has the `invisible' property `erc-match',
;; The message proper has the `invisible' property `match-fools',
;; and it starts after the preceding newline.
(should (eq (get-text-property (pos-bol) 'invisible) 'erc-match))
(should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
;; It ends just before the timestamp.
(let ((msg-end (next-single-property-change (pos-bol) 'invisible)))
(should (equal (get-text-property msg-end 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; Stamp's `invisible' property extends throughout the stamp
;; and ends before the trailing newline.
@ -197,6 +215,17 @@
(should (eq (get-text-property inv-beg 'invisible)
'timestamp))))))))
(ert-deftest erc-scenarios-match--stamp-right-fools-invisible ()
:tags '(:expensive-test)
(erc-scenarios-match--stamp-right-fools-invisible))
(ert-deftest erc-scenarios-match--stamp-right-fools-invisible--nooffset ()
:tags '(:expensive-test)
(with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
(should-not erc-legacy-invisible-bounds-p)
(let ((erc-legacy-invisible-bounds-p t))
(erc-scenarios-match--stamp-right-fools-invisible))))
;; This asserts that when `erc-fill-wrap-mode' is enabled, ERC hides
;; the preceding message's line ending.
(ert-deftest erc-scenarios-match--stamp-right-invisible-fill-wrap ()
@ -215,16 +244,16 @@
;; Stamps have a combined `invisible' property value.
(should (equal (get-text-property (1- (pos-eol)) 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; The message proper has the `invisible' property `erc-match',
;; The message proper has the `invisible' property `match-fools',
;; which starts at the preceding newline...
(should (eq (get-text-property (1- (pos-bol)) 'invisible) 'erc-match))
(should (eq (get-text-property (1- (pos-bol)) 'invisible) 'match-fools))
;; ... and ends just before the timestamp.
(let ((msgend (next-single-property-change (1- (pos-bol)) 'invisible)))
(should (equal (get-text-property msgend 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; The newline before `erc-insert-marker' is still visible.
(should-not (get-text-property (pos-eol) 'invisible))
@ -242,8 +271,7 @@
(let ((inv-beg (next-single-property-change (1- (pos-bol)) 'invisible)))
(should (eq (get-text-property inv-beg 'invisible) 'timestamp)))))))
(ert-deftest erc-scenarios-match--stamp-both-invisible-fill-static ()
:tags '(:expensive-test)
(defun erc-scenarios-match--stamp-both-invisible-fill-static ()
(should (eq erc-insert-timestamp-function
#'erc-insert-timestamp-left-and-right))
@ -265,8 +293,8 @@
(search-forward "[23:59]"))))
(ert-info ("Line endings in Bob's messages are invisible")
;; The message proper has the `invisible' property `erc-match'.
(should (eq (get-text-property (pos-bol) 'invisible) 'erc-match))
;; The message proper has the `invisible' property `match-fools'.
(should (eq (get-text-property (pos-bol) 'invisible) 'match-fools))
(let* ((mbeg (next-single-property-change (pos-bol) 'erc-command))
(mend (next-single-property-change mbeg 'erc-command)))
@ -283,9 +311,13 @@
(should (= (next-single-property-change (pos-bol) 'erc-timestamp)
mend))
;; Line ending has the `invisible' property `erc-match'.
;; Line ending has the `invisible' property `match-fools'.
(should (= (char-after mend) ?\n))
(should (eq (get-text-property mend'invisible) 'erc-match))))
(with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
(if erc-legacy-invisible-bounds-p
(should (eq (get-text-property mend 'invisible) 'match-fools))
(should (eq (get-text-property mbeg 'invisible) 'match-fools))
(should-not (get-text-property mend 'invisible))))))
;; Only the message right after Alice speaks contains stamps.
(when (= 1 bob-utterance-counter)
@ -298,7 +330,7 @@
;; Date stamp has a combined `invisible' property value
;; that extends until the start of the message proper.
(should (equal (get-text-property (point) 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
(should (= (next-single-property-change (point) 'invisible)
(1+ (pos-eol))))))
@ -314,7 +346,7 @@
(let ((msgend (next-single-property-change (pos-bol) 'invisible)))
;; Stamp has a combined `invisible' property value.
(should (equal (get-text-property msgend 'invisible)
'(timestamp erc-match)))
'(timestamp match-fools)))
;; Combined `invisible' property spans entire timestamp.
(should (= (next-single-property-change msgend 'invisible)
@ -331,4 +363,15 @@
(should-not (eq (field-at-pos (1- (pos-eol))) 'erc-timestamp))
(should-not (next-single-property-change (pos-bol) 'invisible))))))
(ert-deftest erc-scenarios-match--stamp-both-invisible-fill-static ()
:tags '(:expensive-test)
(erc-scenarios-match--stamp-both-invisible-fill-static))
(ert-deftest erc-scenarios-match--stamp-both-invisible-fill-static--nooffset ()
:tags '(:expensive-test)
(with-suppressed-warnings ((obsolete erc-legacy-invisible-bounds-p))
(should-not erc-legacy-invisible-bounds-p)
(let ((erc-legacy-invisible-bounds-p t))
(erc-scenarios-match--stamp-both-invisible-fill-static))))
;;; erc-scenarios-match.el ends here

View file

@ -1278,6 +1278,50 @@
(should-not calls))))))
(defmacro erc-tests--equal-including-properties (a b)
(list (if (< emacs-major-version 29)
'ert-equal-including-properties
'equal-including-properties)
a b))
(ert-deftest erc--merge-prop ()
(with-current-buffer (get-buffer-create "*erc-test*")
;; Baseline.
(insert "abc\n")
(erc--merge-prop 1 3 'erc-test 'x)
(should (erc-tests--equal-including-properties
(buffer-substring 1 4) #("abc" 0 2 (erc-test x))))
(erc--merge-prop 1 3 'erc-test 'y)
(should (erc-tests--equal-including-properties
(buffer-substring 1 4) #("abc" 0 2 (erc-test (y x)))))
;; Multiple intervals.
(goto-char (point-min))
(insert "def\n")
(erc--merge-prop 1 2 'erc-test 'x)
(erc--merge-prop 2 3 'erc-test 'y)
(should (erc-tests--equal-including-properties
(buffer-substring 1 4)
#("def" 0 1 (erc-test x) 1 2 (erc-test y))))
(erc--merge-prop 1 3 'erc-test 'z)
(should (erc-tests--equal-including-properties
(buffer-substring 1 4)
#("def" 0 1 (erc-test (z x)) 1 2 (erc-test (z y)))))
;; New val as list.
(goto-char (point-min))
(insert "ghi\n")
(erc--merge-prop 2 3 'erc-test '(y z))
(should (erc-tests--equal-including-properties
(buffer-substring 1 4) #("ghi" 1 2 (erc-test (y z)))))
(erc--merge-prop 1 3 'erc-test '(w x))
(should (erc-tests--equal-including-properties
(buffer-substring 1 4)
#("ghi" 0 1 (erc-test (w x)) 1 2 (erc-test (w x y z)))))
(when noninteractive
(kill-buffer))))
(ert-deftest erc--split-string-shell-cmd ()
;; Leading and trailing space
@ -1494,12 +1538,6 @@
(kill-buffer "ExampleNet")
(kill-buffer "#chan")))
(defmacro erc-tests--equal-including-properties (a b)
(list (if (< emacs-major-version 29)
'ert-equal-including-properties
'equal-including-properties)
a b))
(ert-deftest erc-format-privmessage ()
;; Basic PRIVMSG
(should (erc-tests--equal-including-properties