unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
@ 2023-07-25 13:40 J.P.
  2023-07-26 13:27 ` J.P.
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: J.P. @ 2023-07-25 13:40 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 4768 bytes --]

Tags: patch

A minor bug fix involving the `scrolltobottom' module (and originally
lumped in with the mostly unrelated bug#63595) eventually materialized
on HEAD as [1]. While it did fix the bug, it didn't really address any
of the related foundational issues that have haunted `scrolltobottom'
since the beginning. The attached patch is meant to make the module more
predictable and consistent overall, while specifically addressing

  - the effect only being applied in the selected window

  - the effect not responding to changes in window size

  - the gradual scrolling of point toward the centermost line when
    away from the prompt (on graphical displays)

People miffed by this module's rough edges are encouraged to try these
changes and give feedback.

Thanks.


[1] commit e51e43b7046b56c58310854182a1d589ee4c770c
    Author: F. Jason Park <jp@neverwas.me>
    Date:   Wed May 17 19:48:02 2023 -0700

      Fix buffer-mismatch bug in erc-scroll-to-bottom

      * lisp/erc/erc-goodies.el (erc-scroll-to-bottom): Only
      `recenter' when the selected window's buffer is current.
      Previously, the module `scrolltobottom' signaled an "Error in
      `post-command-hook'" when a user clicked a channel indicator in
      the mode line from a window showing another ERC buffer.
    [...]
      (Bug#63595)

    lisp/erc/erc-goodies.el | 1 +
    lisp/erc/erc-track.el   | 9 ++++++++-
    2 files changed, 9 insertions(+), 1 deletion(-)


In GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.38, cairo version 1.17.6) of 2023-07-25 built on localhost
Repository revision: 89558533683a100ca7946c4a35bf4ef50463efef
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 37 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NOTIFY
INOTIFY PDUMPER PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINPUT2 XPM GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived epg rfc6068 epg-config
gnus-util text-property-search time-date mm-decode mm-bodies mm-encode
mail-parse rfc2231 mailabbrev gmm-utils mailheader sendmail rfc2047
rfc2045 ietf-drums mm-util mail-prsvr mail-utils erc auth-source cl-seq
eieio eieio-core cl-macs password-cache json subr-x map format-spec
cl-loaddefs cl-lib erc-backend erc-networks byte-opt gv bytecomp
byte-compile erc-common erc-compat erc-loaddefs rmc iso-transl tooltip
cconv eldoc paren electric uniquify ediff-hook vc-hooks lisp-float-type
elisp-mode mwheel term/x-win x-win term/common-win x-dnd tool-bar dnd
fontset image regexp-opt fringe tabulated-list replace newcomment
text-mode lisp-mode prog-mode register page tab-bar menu-bar rfn-eshadow
isearch easymenu timer select scroll-bar mouse jit-lock font-lock syntax
font-core term/tty-colors frame minibuffer nadvice seq simple cl-generic
indonesian philippine cham georgian utf-8-lang misc-lang vietnamese
tibetan thai tai-viet lao korean japanese eucjp-ms cp51932 hebrew greek
romanian slovak czech european ethiopic indian cyrillic chinese
composite emoji-zwj charscript charprop case-table epa-hook
jka-cmpr-hook help abbrev obarray oclosure cl-preloaded button loaddefs
theme-loaddefs faces cus-face macroexp files window text-properties
overlay sha1 md5 base64 format env code-pages mule custom widget keymap
hashtable-print-readable backquote threads dbusbind inotify lcms2
dynamic-setting system-font-setting font-render-setting cairo gtk
x-toolkit xinput2 x multi-tty move-toolbar make-network-process emacs)

Memory information:
((conses 16 64796 9162) (symbols 48 8637 0) (strings 32 23386 1972)
 (string-bytes 1 680274) (vectors 16 15042)
 (vector-slots 8 207499 8948) (floats 8 24 41) (intervals 56 225 0)
 (buffers 976 10))


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch --]
[-- Type: text/x-patch, Size: 6054 bytes --]

From 5ce39aa8adfefc85208448ff1d254766f15892e1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 22 Jul 2023 00:46:44 -0700
Subject: [PATCH] [5.6] Consider all windows in erc-scrolltobottom-mode

* lisp/erc/erc-goodies.el (erc-scrolltobottom-mode,
erc-scrolltobottom-enable, erc-scrolltobottom-disable): Use
`erc--scroll-to-bottom-all' instead of `erc-possibly-scroll-to-bottom'
for `erc-insert-done-hook' and now also `erc-send-completed-hook'.
Call `erc-add-scroll-to-bottom' for teardown as well.
(erc--scroll-to-bottom-debounce-expire): New variable.
(erc-possibly-scroll-to-bottom): Don't bother scrolling when user has
done so recently.
(erc--scroll-to-bottom-all): New function to scroll in all windows of
the current buffer.
(erc-add-scroll-to-bottom): Perform teardown as well when mode is
disabled.  Also run on `window-configuration-changed-hook'.
(erc-scroll-to-bottom): Use `window-point' instead of `point'.
---
 lisp/erc/erc-goodies.el | 81 +++++++++++++++++++++++++++++++++++------
 1 file changed, 70 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index d9ededa8e68..af44b98e0bf 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -52,34 +52,93 @@ erc-input-line-position
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
+   (add-hook 'erc-insert-pre-hook #'erc--on-pre-insert)
+   (add-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
+   (add-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (dolist (buffer (erc-buffer-list))
-     (with-current-buffer buffer
-       (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))))
+   (remove-hook 'erc-insert-pre-hook #'erc--on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
+   (remove-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
+   (erc-buffer-do #'erc-add-scroll-to-bottom)))
+
+(defvar-local erc--scroll-to-bottom-debounce-expire nil
+  "Time after which `scrolltobottom' is allowed to run.
+Set to a fraction of a second in the future on every refresh.")
+
+(defvar-local erc--scroll-to-bottom-last-window-start nil
+  "A cons of a window and a starting position.")
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
+(defun erc--possibly-scroll-to-bottom ()
+  "Call `erc-scroll-to-bottom' when buffer occupies selected window.
+Skip when `erc--scroll-to-bottom-debounce-expire' has not yet
+arrived."
+  (when (eq (selected-window) (get-buffer-window))
+    (unless (eq this-command 'recenter-top-bottom)
+      (let (erc--scroll-to-bottom-last-window-start)
+        (erc--scroll-to-bottom)))))
+
+(defun erc--scroll-to-bottom-all (&rest _)
+  "Run `erc-scroll-to-bottom' in all windows showing current buffer."
+  (dolist (window (get-buffer-window-list nil nil 'visible))
+    (with-selected-window window
+      (erc--scroll-to-bottom))))
+
 (defun erc-add-scroll-to-bottom ()
-  "A hook function for `erc-mode-hook' to recenter output at bottom of window.
+  "Arrange for `scrolltobottom' to refresh on window configuration changes.
+Undo that arrangement when `erc-scrolltobottom-mode' is disabled.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
 
-This works whenever scrolling happens, so it's added to
-`window-scroll-functions' rather than `erc-insert-post-hook'."
-  (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+Note that the prior suggestion comes from a time when this
+function used `window-scroll-functions', which was replaced by
+`post-command-hook' in ERC 5.3."
+  (if erc-scrolltobottom-mode
+      (progn
+        (add-hook 'window-configuration-change-hook
+                  #'erc--possibly-scroll-to-bottom nil t)
+        (add-hook 'post-command-hook
+                  #'erc--possibly-scroll-to-bottom nil t))
+    (remove-hook 'window-configuration-change-hook
+                 #'erc--possibly-scroll-to-bottom t)
+    (remove-hook 'post-command-hook
+                 #'erc--possibly-scroll-to-bottom t)))
+
+(defun erc--on-pre-insert (&rest _)
+  (when (eq (selected-window) (get-buffer-window))
+    (setq erc--scroll-to-bottom-last-window-start
+          (cons (selected-window) (window-start)))))
+
+(defun erc--scroll-to-bottom ()
+  "Like `erc-scroll-to-bottom', but use `window-point'.
+Expect to run in some window, not necessarily the user-selected
+one."
+  (when erc-insert-marker
+    (let ((resize-mini-windows nil))
+      (save-restriction
+        (widen)
+        (if (>= (window-point) erc-input-marker)
+            (save-excursion
+              (goto-char (point-max))
+              (recenter (or erc-input-line-position -1)))
+          (when (and erc--scroll-to-bottom-last-window-start
+                     ;; `selected-window' means the one being visited.
+                     (eq (selected-window)
+                         (car erc--scroll-to-bottom-last-window-start)))
+            (save-excursion
+              (goto-char (cdr erc--scroll-to-bottom-last-window-start))
+              (let ((recenter-positions '(top)))
+                (recenter-top-bottom)))))))))
 
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
 
-This is added to `window-scroll-functions' by `erc-add-scroll-to-bottom'.
-
 You can control which line is recentered to by customizing the
 variable `erc-input-line-position'."
       ;; Temporarily bind resize-mini-windows to nil so that users who have it
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
  2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
@ 2023-07-26 13:27 ` J.P.
  2023-08-09 15:00 ` J.P.
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-07-26 13:27 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 79 bytes --]

v2. Stash and restore `window-start' for all windows when inserting
messages.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v1-v2.diff --]
[-- Type: text/x-patch, Size: 6649 bytes --]

From 49e66fee68a4ad69418dae329e24e5611ea8de6e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 26 Jul 2023 05:33:54 -0700
Subject: [PATCH 0/1] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6] Consider all windows in erc-scrolltobottom-mode

 lisp/erc/erc-goodies.el | 102 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 91 insertions(+), 11 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index af44b98e0bf..e82dc73f82f 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -52,22 +52,26 @@ erc-input-line-position
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-pre-hook #'erc--on-pre-insert)
+   (add-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
+   (add-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
    (add-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
    (add-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-pre-hook #'erc--on-pre-insert)
+   (remove-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
+   (remove-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
    (remove-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
    (remove-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
    (erc-buffer-do #'erc-add-scroll-to-bottom)))
 
 (defvar-local erc--scroll-to-bottom-debounce-expire nil
-  "Time after which `scrolltobottom' is allowed to run.
-Set to a fraction of a second in the future on every refresh.")
+  "A cons of some window and the expiration time in seconds.
+The CAR is a window in whose buffer this variable is local.  The
+CDR is a half second after the last attempted refresh of such a
+window.")
 
 (defvar-local erc--scroll-to-bottom-last-window-start nil
-  "A cons of a window and a starting position.")
+  "Alist whose members are a cons of a window and a starting position.")
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
@@ -76,18 +80,36 @@ erc-possibly-scroll-to-bottom
 
 (defun erc--possibly-scroll-to-bottom ()
   "Call `erc-scroll-to-bottom' when buffer occupies selected window.
-Skip when `erc--scroll-to-bottom-debounce-expire' has not yet
-arrived."
+Expect narrowing not to be in effect.  Skip when
+`erc--scroll-to-bottom-debounce-expire' has not yet transpired."
   (when (eq (selected-window) (get-buffer-window))
-    (unless (eq this-command 'recenter-top-bottom)
-      (let (erc--scroll-to-bottom-last-window-start)
-        (erc--scroll-to-bottom)))))
+    (unless (or (eq this-command 'recenter-top-bottom) (input-pending-p))
+      (let ((now (erc-current-time))
+            (at-prompt-p (>= (point) erc-input-marker)))
+        (unless (and at-prompt-p
+                     erc--scroll-to-bottom-debounce-expire
+                     (eq (car erc--scroll-to-bottom-debounce-expire)
+                         (selected-window))
+                     (< now (cdr erc--scroll-to-bottom-debounce-expire)))
+          (erc--scroll-to-bottom))
+        (setq erc--scroll-to-bottom-debounce-expire
+              (and at-prompt-p (cons (selected-window) (+ now 0.5))))))))
 
 (defun erc--scroll-to-bottom-all (&rest _)
-  "Run `erc-scroll-to-bottom' in all windows showing current buffer."
+  "Maybe put prompt on last line in all windows displaying current buffer.
+Expect to run when narrowing is in effect, such as on insertion
+or send-related hooks.  When recentering has not been performed,
+attempt to restore last `window-start', if known."
   (dolist (window (get-buffer-window-list nil nil 'visible))
     (with-selected-window window
-      (erc--scroll-to-bottom))))
+      (when-let
+          (((not (erc--scroll-to-bottom)))
+           (erc--scroll-to-bottom-last-window-start)
+           (found (assq window erc--scroll-to-bottom-last-window-start)))
+        (setf (window-start window) (cdr found)))))
+  ;; Necessary unless we're sure `erc--scroll-to-bottom-on-pre-insert'
+  ;; always runs between calls to this function.
+  (setq erc--scroll-to-bottom-last-window-start nil))
 
 (defun erc-add-scroll-to-bottom ()
   "Arrange for `scrolltobottom' to refresh on window configuration changes.
@@ -110,31 +132,30 @@ erc-add-scroll-to-bottom
     (remove-hook 'post-command-hook
                  #'erc--possibly-scroll-to-bottom t)))
 
-(defun erc--on-pre-insert (&rest _)
-  (when (eq (selected-window) (get-buffer-window))
-    (setq erc--scroll-to-bottom-last-window-start
-          (cons (selected-window) (window-start)))))
+(cl-defmethod erc--scroll-to-bottom-on-pre-insert (_input-or-string)
+  "Remember the `window-start' before inserting a message."
+  (setq erc--scroll-to-bottom-last-window-start
+        (mapcar (lambda (w) (cons w (window-start w)))
+                (get-buffer-window-list nil nil 'visible))))
+
+(cl-defmethod erc--scroll-to-bottom-on-pre-insert ((input erc-input))
+  "Remember the `window-start' before inserting a message."
+  (when (erc-input-insertp input)
+    (cl-call-next-method)))
 
 (defun erc--scroll-to-bottom ()
   "Like `erc-scroll-to-bottom', but use `window-point'.
 Expect to run in some window, not necessarily the user-selected
-one."
+one.  Return non-nil when recentering has occurred."
   (when erc-insert-marker
     (let ((resize-mini-windows nil))
       (save-restriction
         (widen)
-        (if (>= (window-point) erc-input-marker)
-            (save-excursion
-              (goto-char (point-max))
-              (recenter (or erc-input-line-position -1)))
-          (when (and erc--scroll-to-bottom-last-window-start
-                     ;; `selected-window' means the one being visited.
-                     (eq (selected-window)
-                         (car erc--scroll-to-bottom-last-window-start)))
-            (save-excursion
-              (goto-char (cdr erc--scroll-to-bottom-last-window-start))
-              (let ((recenter-positions '(top)))
-                (recenter-top-bottom)))))))))
+        (when (>= (window-point) erc-input-marker)
+          (save-excursion
+            (goto-char (point-max))
+            (recenter (or erc-input-line-position -1))
+            t))))))
 
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch --]
[-- Type: text/x-patch, Size: 8150 bytes --]

From 49e66fee68a4ad69418dae329e24e5611ea8de6e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 22 Jul 2023 00:46:44 -0700
Subject: [PATCH 1/1] [5.6] Consider all windows in erc-scrolltobottom-mode

* lisp/erc/erc-goodies.el (erc-scrolltobottom-mode,
erc-scrolltobottom-enable, erc-scrolltobottom-disable): Use
`erc--scroll-to-bottom-all' instead of `erc-possibly-scroll-to-bottom'
for `erc-insert-done-hook' and now also `erc-send-completed-hook'.
Add `erc--scroll-to-bottom-on-pre-insert' to `erc-insert-pre-hook' and
`erc-send-pre-functions'.
Call `erc-add-scroll-to-bottom' for teardown as well.
(erc--scroll-to-bottom-debounce-expire): New variable to help ERC
refrain from calling `recenter' when the user is typing at the prompt.
(erc--scroll-to-bottom-last-window-start): New variable used by
`erc--scroll-to-bottom-on-pre-insert' and `erc--scroll-to-bottom' for
stashing and restoring the `window-start' of all windows displaying
the buffer.
(erc--possibly-scroll-to-bottom): New function resembling
`erc-possibly-scroll-to-bottom' that tries to avoid scrolling again
when user has done so recently.
(erc--scroll-to-bottom-all): New function to scroll all windows
displaying the current buffer.
(erc-add-scroll-to-bottom): Perform teardown as well when mode is
disabled.  Also run on `window-configuration-changed-hook'.
(erc-scroll-to-bottom): Use `window-point' instead of `point'.
(erc--scroll-to-bottom-on-pre-insert): New generic function that
remembers the `window-start' before inserting a message in order to
restore it afterward.
(erc--scroll-to-bottom): New function, a replacement for
`erc-scroll-to-bottom', that takes returns non-nil when it's actually
performed a recentering.  (Bug#64855)
---
 lisp/erc/erc-goodies.el | 102 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 91 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index d9ededa8e68..e82dc73f82f 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -52,34 +52,114 @@ erc-input-line-position
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
+   (add-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
+   (add-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
+   (add-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
+   (add-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (dolist (buffer (erc-buffer-list))
-     (with-current-buffer buffer
-       (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))))
+   (remove-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
+   (remove-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
+   (remove-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
+   (erc-buffer-do #'erc-add-scroll-to-bottom)))
+
+(defvar-local erc--scroll-to-bottom-debounce-expire nil
+  "A cons of some window and the expiration time in seconds.
+The CAR is a window in whose buffer this variable is local.  The
+CDR is a half second after the last attempted refresh of such a
+window.")
+
+(defvar-local erc--scroll-to-bottom-last-window-start nil
+  "Alist whose members are a cons of a window and a starting position.")
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
+(defun erc--possibly-scroll-to-bottom ()
+  "Call `erc-scroll-to-bottom' when buffer occupies selected window.
+Expect narrowing not to be in effect.  Skip when
+`erc--scroll-to-bottom-debounce-expire' has not yet transpired."
+  (when (eq (selected-window) (get-buffer-window))
+    (unless (or (eq this-command 'recenter-top-bottom) (input-pending-p))
+      (let ((now (erc-current-time))
+            (at-prompt-p (>= (point) erc-input-marker)))
+        (unless (and at-prompt-p
+                     erc--scroll-to-bottom-debounce-expire
+                     (eq (car erc--scroll-to-bottom-debounce-expire)
+                         (selected-window))
+                     (< now (cdr erc--scroll-to-bottom-debounce-expire)))
+          (erc--scroll-to-bottom))
+        (setq erc--scroll-to-bottom-debounce-expire
+              (and at-prompt-p (cons (selected-window) (+ now 0.5))))))))
+
+(defun erc--scroll-to-bottom-all (&rest _)
+  "Maybe put prompt on last line in all windows displaying current buffer.
+Expect to run when narrowing is in effect, such as on insertion
+or send-related hooks.  When recentering has not been performed,
+attempt to restore last `window-start', if known."
+  (dolist (window (get-buffer-window-list nil nil 'visible))
+    (with-selected-window window
+      (when-let
+          (((not (erc--scroll-to-bottom)))
+           (erc--scroll-to-bottom-last-window-start)
+           (found (assq window erc--scroll-to-bottom-last-window-start)))
+        (setf (window-start window) (cdr found)))))
+  ;; Necessary unless we're sure `erc--scroll-to-bottom-on-pre-insert'
+  ;; always runs between calls to this function.
+  (setq erc--scroll-to-bottom-last-window-start nil))
+
 (defun erc-add-scroll-to-bottom ()
-  "A hook function for `erc-mode-hook' to recenter output at bottom of window.
+  "Arrange for `scrolltobottom' to refresh on window configuration changes.
+Undo that arrangement when `erc-scrolltobottom-mode' is disabled.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
 
-This works whenever scrolling happens, so it's added to
-`window-scroll-functions' rather than `erc-insert-post-hook'."
-  (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+Note that the prior suggestion comes from a time when this
+function used `window-scroll-functions', which was replaced by
+`post-command-hook' in ERC 5.3."
+  (if erc-scrolltobottom-mode
+      (progn
+        (add-hook 'window-configuration-change-hook
+                  #'erc--possibly-scroll-to-bottom nil t)
+        (add-hook 'post-command-hook
+                  #'erc--possibly-scroll-to-bottom nil t))
+    (remove-hook 'window-configuration-change-hook
+                 #'erc--possibly-scroll-to-bottom t)
+    (remove-hook 'post-command-hook
+                 #'erc--possibly-scroll-to-bottom t)))
+
+(cl-defmethod erc--scroll-to-bottom-on-pre-insert (_input-or-string)
+  "Remember the `window-start' before inserting a message."
+  (setq erc--scroll-to-bottom-last-window-start
+        (mapcar (lambda (w) (cons w (window-start w)))
+                (get-buffer-window-list nil nil 'visible))))
+
+(cl-defmethod erc--scroll-to-bottom-on-pre-insert ((input erc-input))
+  "Remember the `window-start' before inserting a message."
+  (when (erc-input-insertp input)
+    (cl-call-next-method)))
+
+(defun erc--scroll-to-bottom ()
+  "Like `erc-scroll-to-bottom', but use `window-point'.
+Expect to run in some window, not necessarily the user-selected
+one.  Return non-nil when recentering has occurred."
+  (when erc-insert-marker
+    (let ((resize-mini-windows nil))
+      (save-restriction
+        (widen)
+        (when (>= (window-point) erc-input-marker)
+          (save-excursion
+            (goto-char (point-max))
+            (recenter (or erc-input-line-position -1))
+            t))))))
 
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
 
-This is added to `window-scroll-functions' by `erc-add-scroll-to-bottom'.
-
 You can control which line is recentered to by customizing the
 variable `erc-input-line-position'."
       ;; Temporarily bind resize-mini-windows to nil so that users who have it
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
  2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
  2023-07-26 13:27 ` J.P.
@ 2023-08-09 15:00 ` J.P.
  2023-08-18 13:50 ` J.P.
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-08-09 15:00 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 161 bytes --]

v3. Add new option to forgo forcing prompt to window's bottom. Remove
debouncing mechanism. Prefer setting window-start instead of
`recenter'ing when possible.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v2-v3.diff --]
[-- Type: text/x-patch, Size: 16173 bytes --]

From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 9 Aug 2023 06:31:29 -0700
Subject: [PATCH 0/1] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6] Consider all windows in erc-scrolltobottom-mode

 lisp/erc/erc-goodies.el | 222 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 209 insertions(+), 13 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index e82dc73f82f..129e17fb5ec 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -44,58 +44,143 @@ erc-input-line-position
 This should be an integer specifying the line of the buffer on which
 the input line should stay.  A value of \"-1\" would keep the input
 line positioned on the last line in the buffer.  This is passed as an
-argument to `recenter'."
+argument to `recenter', unless `erc-scrolltobottom-relaxed' is
+non-nil, in which case, ERC interprets it as additional lines to
+scroll down by per message insertion (minus one for the prompt)."
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+(defcustom erc-scrolltobottom-relaxed nil
+  "Whether to forgo forcing prompt to the bottom of the window.
+When non-nil, and point is at the prompt, ERC scrolls the window
+up when inserting messages, making the prompt appear stationary.
+Users who find this effect too \"stagnant\" can adjust the option
+`erc-input-line-position', which ERC borrows to express a scroll
+offset when this option is non-nil.  Setting that value to zero
+lets the prompt drift toward the bottom by one line per message,
+which is generally slow enough not to distract while composing
+input.  Of course, this doesn't apply when receiving a large
+influx of messages, such as after typing \"/msg NickServ help\".
+Note that ERC only considers this option when initializing the
+`scrolltobottom' module and enabling `erc-scrolltobottom-mode'."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
-   (add-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
-   (add-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
-   (add-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
+   (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-pre-hook #'erc--scroll-to-bottom-on-pre-insert)
-   (remove-hook 'erc-send-pre-functions #'erc--scroll-to-bottom-on-pre-insert)
-   (remove-hook 'erc-insert-done-hook #'erc--scroll-to-bottom-all)
-   (remove-hook 'erc-send-completed-hook #'erc--scroll-to-bottom-all)
+   (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
    (erc-buffer-do #'erc-add-scroll-to-bottom)))
 
-(defvar-local erc--scroll-to-bottom-debounce-expire nil
-  "A cons of some window and the expiration time in seconds.
-The CAR is a window in whose buffer this variable is local.  The
-CDR is a half second after the last attempted refresh of such a
-window.")
-
-(defvar-local erc--scroll-to-bottom-last-window-start nil
-  "Alist whose members are a cons of a window and a starting position.")
-
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
-(defun erc--possibly-scroll-to-bottom ()
-  "Call `erc-scroll-to-bottom' when buffer occupies selected window.
-Expect narrowing not to be in effect.  Skip when
-`erc--scroll-to-bottom-debounce-expire' has not yet transpired."
-  (when (eq (selected-window) (get-buffer-window))
-    (unless (or (eq this-command 'recenter-top-bottom) (input-pending-p))
-      (let ((now (erc-current-time))
-            (at-prompt-p (>= (point) erc-input-marker)))
-        (unless (and at-prompt-p
-                     erc--scroll-to-bottom-debounce-expire
-                     (eq (car erc--scroll-to-bottom-debounce-expire)
-                         (selected-window))
-                     (< now (cdr erc--scroll-to-bottom-debounce-expire)))
-          (erc--scroll-to-bottom))
-        (setq erc--scroll-to-bottom-debounce-expire
-              (and at-prompt-p (cons (selected-window) (+ now 0.5))))))))
-
-(defun erc--scroll-to-bottom-all (&rest _)
+(defvar-local erc--scrolltobottom-relaxed-commands '(end-of-buffer)
+  "Commands triggering a force scroll to prompt.
+Only applies with `erc-scrolltobottom-relaxed' when away from prompt.")
+
+(defvar-local erc--scrolltobottom-window-info nil
+  "Alist with windows as keys and lists of window-related info as values.
+Values are lists containing the last window start position and
+the last \"window line\" of point.  The \"window line\", which
+may be nil, is the number of lines between `window-start' and
+`window-point', inclusive.")
+
+(defvar erc--scrolltobottom-post-force-commands
+  '(electric-newline-and-maybe-indent)
+  "Commands that force a scroll after execution at prompt.")
+
+(defvar erc--scrolltobottom-relaxed-skip-commands '(recenter-top-bottom))
+
+(defun erc--scrolltobottom-on-pre-command ()
+  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when (> (point) erc-input-marker)
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point)))))))
+
+(defun erc--scrolltobottom-on-post-command ()
+  "Restore window start or scroll to prompt and recenter.
+When `erc--scrolltobottom-window-info' is non-nil and its first
+item is associated with the selected window, restore start of
+window so long as prompt hasn't moved.  Expect buffer to be
+unnarrowed."
+  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (if-let (((not (input-pending-p)))
+           (erc--scrolltobottom-window-info)
+           (found (car erc--scrolltobottom-window-info))
+           ((eq (car found) (selected-window)))
+           ((not (memq this-command erc--scrolltobottom-post-force-commands)))
+           ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
+      (set-window-start (selected-window) (nth 1 found))
+    (erc--scrolltobottom-confirm))
+  (setq erc--scrolltobottom-window-info nil))
+
+(defun erc--scrolltobottom-on-pre-command-relaxed ()
+  "Maybe scroll to bottom when away from prompt in an unnarrowed buffer.
+When `erc-scrolltobottom-relaxed' is active, only scroll when
+prompt is past window's end and the command is `end-of-buffer' or
+`self-insert-command' (assuming `move-to-prompt' is active).
+When at prompt and current command is not `recenter-top-bottom',
+stash `erc--scrolltobottom-window-info' for the selected window."
+  (cl-assert erc-scrolltobottom-relaxed) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when (and (not (input-pending-p))
+             (< (point) erc-input-marker)
+             (memq this-command erc--scrolltobottom-relaxed-commands)
+             (< (window-end nil t) erc-input-marker))
+    (save-excursion
+      (goto-char (point-max))
+      (recenter (or erc-input-line-position -1))))
+  (when (and (> (point) erc-input-marker)
+             (not (memq this-command
+                        erc--scrolltobottom-relaxed-skip-commands)))
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point-max)))))))
+
+(defun erc--scrolltobottom-on-post-command-relaxed ()
+  "Set window start or scroll when data was captured on pre-command."
+  (cl-assert erc-scrolltobottom-relaxed) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when-let ((erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window))))
+    (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
+             (= (nth 2 found)
+                (count-screen-lines (window-start) (point-max))))
+        (set-window-start (selected-window) (nth 1 found))
+      (recenter (nth 2 found)))
+    (setq erc--scrolltobottom-window-info nil)))
+
+;; FIXME this is currently of little value because it doesn't restore
+;; the relative position of window point after changing dimensions.
+;; It would be preferable to instead stash the previous ratio of
+;; window line to body height and later recenter proportionally.  But
+;; that may not be worth the added bookkeeping.
+(defun erc--scrolltobottom-away-from-prompt ()
+  "Scroll to bottom unless at prompt."
+  (unless (input-pending-p)
+    (erc--scrolltobottom-confirm)))
+
+(defun erc--scrolltobottom-all (&rest _)
   "Maybe put prompt on last line in all windows displaying current buffer.
 Expect to run when narrowing is in effect, such as on insertion
 or send-related hooks.  When recentering has not been performed,
@@ -103,17 +188,17 @@ erc--scroll-to-bottom-all
   (dolist (window (get-buffer-window-list nil nil 'visible))
     (with-selected-window window
       (when-let
-          (((not (erc--scroll-to-bottom)))
-           (erc--scroll-to-bottom-last-window-start)
-           (found (assq window erc--scroll-to-bottom-last-window-start)))
-        (setf (window-start window) (cdr found)))))
-  ;; Necessary unless we're sure `erc--scroll-to-bottom-on-pre-insert'
+          ((erc--scrolltobottom-window-info)
+           (found (assq window erc--scrolltobottom-window-info))
+           ((not (erc--scrolltobottom-confirm (nth 2 found)))))
+        (setf (window-start window) (cadr found)))))
+  ;; Necessary unless we're sure `erc--scrolltobottom-on-pre-insert'
   ;; always runs between calls to this function.
-  (setq erc--scroll-to-bottom-last-window-start nil))
+  (setq erc--scrolltobottom-window-info nil))
 
 (defun erc-add-scroll-to-bottom ()
-  "Arrange for `scrolltobottom' to refresh on window configuration changes.
-Undo that arrangement when `erc-scrolltobottom-mode' is disabled.
+  "Arrange for scrolling to bottom on window configuration changes.
+Undo that arrangement when disabling `erc-scrolltobottom-mode'.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
@@ -124,29 +209,57 @@ erc-add-scroll-to-bottom
   (if erc-scrolltobottom-mode
       (progn
         (add-hook 'window-configuration-change-hook
-                  #'erc--possibly-scroll-to-bottom nil t)
-        (add-hook 'post-command-hook
-                  #'erc--possibly-scroll-to-bottom nil t))
+                  #'erc--scrolltobottom-away-from-prompt nil t)
+        (if erc-scrolltobottom-relaxed
+            (progn
+              (when (or (bound-and-true-p erc-move-to-prompt-mode)
+                        (memq 'move-to-prompt erc-modules))
+                (cl-pushnew 'self-insert-command
+                            erc--scrolltobottom-relaxed-commands))
+              (add-hook 'post-command-hook
+                        #'erc--scrolltobottom-on-post-command-relaxed -90 t)
+              (add-hook 'pre-command-hook ; preempt `move-to-prompt'
+                        #'erc--scrolltobottom-on-pre-command-relaxed -90 t))
+          (add-hook 'pre-command-hook
+                    #'erc--scrolltobottom-on-pre-command nil t)
+          (add-hook 'post-command-hook
+                    #'erc--scrolltobottom-on-post-command nil t)))
     (remove-hook 'window-configuration-change-hook
-                 #'erc--possibly-scroll-to-bottom t)
+                 #'erc--scrolltobottom-away-from-prompt t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command-relaxed t)
     (remove-hook 'post-command-hook
-                 #'erc--possibly-scroll-to-bottom t)))
+                 #'erc--scrolltobottom-on-post-command-relaxed t)
+    (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
+    (kill-local-variable 'erc--scrolltobottom-window-info)))
 
-(cl-defmethod erc--scroll-to-bottom-on-pre-insert (_input-or-string)
+(cl-defmethod erc--scrolltobottom-on-pre-insert (_input-or-string)
   "Remember the `window-start' before inserting a message."
-  (setq erc--scroll-to-bottom-last-window-start
-        (mapcar (lambda (w) (cons w (window-start w)))
+  (setq erc--scrolltobottom-window-info
+        (mapcar (lambda (w)
+                  (list w
+                        (window-start w)
+                        (and-let*
+                            ((erc-scrolltobottom-relaxed)
+                             (c (count-screen-lines (window-start w)
+                                                    (window-point w) nil w)))
+                          (if (= ?\n (char-before (point-max))) (1+ c) c))))
                 (get-buffer-window-list nil nil 'visible))))
 
-(cl-defmethod erc--scroll-to-bottom-on-pre-insert ((input erc-input))
+(cl-defmethod erc--scrolltobottom-on-pre-insert ((input erc-input))
   "Remember the `window-start' before inserting a message."
   (when (erc-input-insertp input)
     (cl-call-next-method)))
 
-(defun erc--scroll-to-bottom ()
+(defun erc--scrolltobottom-confirm (&optional scroll-to)
   "Like `erc-scroll-to-bottom', but use `window-point'.
 Expect to run in some window, not necessarily the user-selected
-one.  Return non-nil when recentering has occurred."
+one.  Scroll to SCROLL-TO (or 0) lines from the window's top.
+Return non-nil when recentering has occurred."
   (when erc-insert-marker
     (let ((resize-mini-windows nil))
       (save-restriction
@@ -154,7 +267,7 @@ erc--scroll-to-bottom
         (when (>= (window-point) erc-input-marker)
           (save-excursion
             (goto-char (point-max))
-            (recenter (or erc-input-line-position -1))
+            (recenter (+ (or scroll-to 0) (or erc-input-line-position -1)))
             t))))))
 
 (defun erc-scroll-to-bottom ()
@@ -293,12 +406,15 @@ erc--keep-place-indicator-setup
   (add-hook 'window-configuration-change-hook
             #'erc--keep-place-indicator-on-window-configuration-change nil t)
   (when-let* (((memq erc-keep-place-indicator-style '(t arrow)))
+              (ov-property (if (zerop (fringe-columns 'left))
+                               'after-string
+                             'before-string))
               (display (if (zerop (fringe-columns 'left))
                            `((margin left-margin) ,overlay-arrow-string)
                          '(left-fringe right-triangle
                                        erc-keep-place-indicator-arrow)))
               (bef (propertize " " 'display display)))
-    (overlay-put erc--keep-place-indicator-overlay 'before-string bef))
+    (overlay-put erc--keep-place-indicator-overlay ov-property bef))
   (when (memq erc-keep-place-indicator-style '(t face))
     (overlay-put erc--keep-place-indicator-overlay 'face
                  'erc-keep-place-indicator-line)))
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch --]
[-- Type: text/x-patch, Size: 15983 bytes --]

From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 22 Jul 2023 00:46:44 -0700
Subject: [PATCH 1/1] [5.6] Consider all windows in erc-scrolltobottom-mode

* lisp/erc/erc-goodies.el (erc-input-line-position): Mention secondary
role when new option `erc-scroll-to-bottom-relaxed' is non-nil.
(erc-scrolltobottom-relaxed): New option to leave the prompt
stationary instead of forcing it to the bottom of the window.
(erc-scrolltobottom-mode, erc-scrolltobottom-enable,
erc-scrolltobottom-disable): Use `erc--scrolltobottom-all' instead
of `erc-possibly-scroll-to-bottom' for `erc-insert-done-hook' and now
also `erc-send-completed-hook'.  Add
`erc--scrolltobottom-on-pre-insert' to `erc-insert-pre-hook' and
`erc-pre-send-functions'.  Call `erc-add-scroll-to-bottom' for
teardown as well.
(erc--scrolltobottom-relaxed-commands,
erc--scrolltobottom-window-info,
erc--scrolltobottom-post-force-commands,
erc--scrolltobottom-relaxed-skip-commands): New internal variables.
(erc--scrolltobottom-on-pre-command
erc--scrolltobottom-on-post-command): New functions resembling
`erc-possibly-scroll-to-bottom' that try to avoid scrolling repeatedly
for no reason.
(erc--scrolltobottom-on-pre-command-relaxed,
erc--scrolltobottom-on-post-command-relaxed): New commands to help
implement `erc-scroll-to-bottom-relaxed'.
(erc--scrolltobottom-away-from-prompt): New function to scroll to
bottom on window configuration changes.
(erc--scrolltobottom-all): New function to scroll all windows
displaying the current buffer.
(erc-add-scroll-to-bottom): Perform teardown as well when mode is
disabled.  Also scroll on `window-configuration-changed-hook'.
(erc--scrolltobottom-on-pre-insert): New generic function that
remembers the last `window-start' and maybe the current line before
inserting a message in order to restore it afterward.
(erc--scrolltobottom-confirm): New function, a replacement for
`erc-scroll-to-bottom', that returns non-nil when it's actually
recentered the window.
(erc--keep-place-indicator-setup): Add overlay arrow `after-string' in
non-graphical settings in case users have time stamps or other content
occupying the left margin.  (Bug#64855)
---
 lisp/erc/erc-goodies.el | 222 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 209 insertions(+), 13 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index d9ededa8e68..129e17fb5ec 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -44,42 +44,235 @@ erc-input-line-position
 This should be an integer specifying the line of the buffer on which
 the input line should stay.  A value of \"-1\" would keep the input
 line positioned on the last line in the buffer.  This is passed as an
-argument to `recenter'."
+argument to `recenter', unless `erc-scrolltobottom-relaxed' is
+non-nil, in which case, ERC interprets it as additional lines to
+scroll down by per message insertion (minus one for the prompt)."
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+(defcustom erc-scrolltobottom-relaxed nil
+  "Whether to forgo forcing prompt to the bottom of the window.
+When non-nil, and point is at the prompt, ERC scrolls the window
+up when inserting messages, making the prompt appear stationary.
+Users who find this effect too \"stagnant\" can adjust the option
+`erc-input-line-position', which ERC borrows to express a scroll
+offset when this option is non-nil.  Setting that value to zero
+lets the prompt drift toward the bottom by one line per message,
+which is generally slow enough not to distract while composing
+input.  Of course, this doesn't apply when receiving a large
+influx of messages, such as after typing \"/msg NickServ help\".
+Note that ERC only considers this option when initializing the
+`scrolltobottom' module and enabling `erc-scrolltobottom-mode'."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
+   (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (dolist (buffer (erc-buffer-list))
-     (with-current-buffer buffer
-       (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))))
+   (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
+   (erc-buffer-do #'erc-add-scroll-to-bottom)))
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
+(defvar-local erc--scrolltobottom-relaxed-commands '(end-of-buffer)
+  "Commands triggering a force scroll to prompt.
+Only applies with `erc-scrolltobottom-relaxed' when away from prompt.")
+
+(defvar-local erc--scrolltobottom-window-info nil
+  "Alist with windows as keys and lists of window-related info as values.
+Values are lists containing the last window start position and
+the last \"window line\" of point.  The \"window line\", which
+may be nil, is the number of lines between `window-start' and
+`window-point', inclusive.")
+
+(defvar erc--scrolltobottom-post-force-commands
+  '(electric-newline-and-maybe-indent)
+  "Commands that force a scroll after execution at prompt.")
+
+(defvar erc--scrolltobottom-relaxed-skip-commands '(recenter-top-bottom))
+
+(defun erc--scrolltobottom-on-pre-command ()
+  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when (> (point) erc-input-marker)
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point)))))))
+
+(defun erc--scrolltobottom-on-post-command ()
+  "Restore window start or scroll to prompt and recenter.
+When `erc--scrolltobottom-window-info' is non-nil and its first
+item is associated with the selected window, restore start of
+window so long as prompt hasn't moved.  Expect buffer to be
+unnarrowed."
+  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (if-let (((not (input-pending-p)))
+           (erc--scrolltobottom-window-info)
+           (found (car erc--scrolltobottom-window-info))
+           ((eq (car found) (selected-window)))
+           ((not (memq this-command erc--scrolltobottom-post-force-commands)))
+           ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
+      (set-window-start (selected-window) (nth 1 found))
+    (erc--scrolltobottom-confirm))
+  (setq erc--scrolltobottom-window-info nil))
+
+(defun erc--scrolltobottom-on-pre-command-relaxed ()
+  "Maybe scroll to bottom when away from prompt in an unnarrowed buffer.
+When `erc-scrolltobottom-relaxed' is active, only scroll when
+prompt is past window's end and the command is `end-of-buffer' or
+`self-insert-command' (assuming `move-to-prompt' is active).
+When at prompt and current command is not `recenter-top-bottom',
+stash `erc--scrolltobottom-window-info' for the selected window."
+  (cl-assert erc-scrolltobottom-relaxed) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when (and (not (input-pending-p))
+             (< (point) erc-input-marker)
+             (memq this-command erc--scrolltobottom-relaxed-commands)
+             (< (window-end nil t) erc-input-marker))
+    (save-excursion
+      (goto-char (point-max))
+      (recenter (or erc-input-line-position -1))))
+  (when (and (> (point) erc-input-marker)
+             (not (memq this-command
+                        erc--scrolltobottom-relaxed-skip-commands)))
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point-max)))))))
+
+(defun erc--scrolltobottom-on-post-command-relaxed ()
+  "Set window start or scroll when data was captured on pre-command."
+  (cl-assert erc-scrolltobottom-relaxed) ; delete me
+  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
+  (when-let ((erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window))))
+    (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
+             (= (nth 2 found)
+                (count-screen-lines (window-start) (point-max))))
+        (set-window-start (selected-window) (nth 1 found))
+      (recenter (nth 2 found)))
+    (setq erc--scrolltobottom-window-info nil)))
+
+;; FIXME this is currently of little value because it doesn't restore
+;; the relative position of window point after changing dimensions.
+;; It would be preferable to instead stash the previous ratio of
+;; window line to body height and later recenter proportionally.  But
+;; that may not be worth the added bookkeeping.
+(defun erc--scrolltobottom-away-from-prompt ()
+  "Scroll to bottom unless at prompt."
+  (unless (input-pending-p)
+    (erc--scrolltobottom-confirm)))
+
+(defun erc--scrolltobottom-all (&rest _)
+  "Maybe put prompt on last line in all windows displaying current buffer.
+Expect to run when narrowing is in effect, such as on insertion
+or send-related hooks.  When recentering has not been performed,
+attempt to restore last `window-start', if known."
+  (dolist (window (get-buffer-window-list nil nil 'visible))
+    (with-selected-window window
+      (when-let
+          ((erc--scrolltobottom-window-info)
+           (found (assq window erc--scrolltobottom-window-info))
+           ((not (erc--scrolltobottom-confirm (nth 2 found)))))
+        (setf (window-start window) (cadr found)))))
+  ;; Necessary unless we're sure `erc--scrolltobottom-on-pre-insert'
+  ;; always runs between calls to this function.
+  (setq erc--scrolltobottom-window-info nil))
+
 (defun erc-add-scroll-to-bottom ()
-  "A hook function for `erc-mode-hook' to recenter output at bottom of window.
+  "Arrange for scrolling to bottom on window configuration changes.
+Undo that arrangement when disabling `erc-scrolltobottom-mode'.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
 
-This works whenever scrolling happens, so it's added to
-`window-scroll-functions' rather than `erc-insert-post-hook'."
-  (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+Note that the prior suggestion comes from a time when this
+function used `window-scroll-functions', which was replaced by
+`post-command-hook' in ERC 5.3."
+  (if erc-scrolltobottom-mode
+      (progn
+        (add-hook 'window-configuration-change-hook
+                  #'erc--scrolltobottom-away-from-prompt nil t)
+        (if erc-scrolltobottom-relaxed
+            (progn
+              (when (or (bound-and-true-p erc-move-to-prompt-mode)
+                        (memq 'move-to-prompt erc-modules))
+                (cl-pushnew 'self-insert-command
+                            erc--scrolltobottom-relaxed-commands))
+              (add-hook 'post-command-hook
+                        #'erc--scrolltobottom-on-post-command-relaxed -90 t)
+              (add-hook 'pre-command-hook ; preempt `move-to-prompt'
+                        #'erc--scrolltobottom-on-pre-command-relaxed -90 t))
+          (add-hook 'pre-command-hook
+                    #'erc--scrolltobottom-on-pre-command nil t)
+          (add-hook 'post-command-hook
+                    #'erc--scrolltobottom-on-post-command nil t)))
+    (remove-hook 'window-configuration-change-hook
+                 #'erc--scrolltobottom-away-from-prompt t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command-relaxed t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command-relaxed t)
+    (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
+    (kill-local-variable 'erc--scrolltobottom-window-info)))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert (_input-or-string)
+  "Remember the `window-start' before inserting a message."
+  (setq erc--scrolltobottom-window-info
+        (mapcar (lambda (w)
+                  (list w
+                        (window-start w)
+                        (and-let*
+                            ((erc-scrolltobottom-relaxed)
+                             (c (count-screen-lines (window-start w)
+                                                    (window-point w) nil w)))
+                          (if (= ?\n (char-before (point-max))) (1+ c) c))))
+                (get-buffer-window-list nil nil 'visible))))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert ((input erc-input))
+  "Remember the `window-start' before inserting a message."
+  (when (erc-input-insertp input)
+    (cl-call-next-method)))
+
+(defun erc--scrolltobottom-confirm (&optional scroll-to)
+  "Like `erc-scroll-to-bottom', but use `window-point'.
+Expect to run in some window, not necessarily the user-selected
+one.  Scroll to SCROLL-TO (or 0) lines from the window's top.
+Return non-nil when recentering has occurred."
+  (when erc-insert-marker
+    (let ((resize-mini-windows nil))
+      (save-restriction
+        (widen)
+        (when (>= (window-point) erc-input-marker)
+          (save-excursion
+            (goto-char (point-max))
+            (recenter (+ (or scroll-to 0) (or erc-input-line-position -1)))
+            t))))))
 
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
 
-This is added to `window-scroll-functions' by `erc-add-scroll-to-bottom'.
-
 You can control which line is recentered to by customizing the
 variable `erc-input-line-position'."
       ;; Temporarily bind resize-mini-windows to nil so that users who have it
@@ -213,12 +406,15 @@ erc--keep-place-indicator-setup
   (add-hook 'window-configuration-change-hook
             #'erc--keep-place-indicator-on-window-configuration-change nil t)
   (when-let* (((memq erc-keep-place-indicator-style '(t arrow)))
+              (ov-property (if (zerop (fringe-columns 'left))
+                               'after-string
+                             'before-string))
               (display (if (zerop (fringe-columns 'left))
                            `((margin left-margin) ,overlay-arrow-string)
                          '(left-fringe right-triangle
                                        erc-keep-place-indicator-arrow)))
               (bef (propertize " " 'display display)))
-    (overlay-put erc--keep-place-indicator-overlay 'before-string bef))
+    (overlay-put erc--keep-place-indicator-overlay ov-property bef))
   (when (memq erc-keep-place-indicator-style '(t face))
     (overlay-put erc--keep-place-indicator-overlay 'face
                  'erc-keep-place-indicator-line)))
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
  2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
  2023-07-26 13:27 ` J.P.
  2023-08-09 15:00 ` J.P.
@ 2023-08-18 13:50 ` J.P.
  2023-08-24 14:11 ` J.P.
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-08-18 13:50 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 254 bytes --]

v4. Fix bug involving new option `erc-scrolltobottom-relaxed'. Specify
explicit hook depths for `erc--scrolltobottom-on-pre-command{,-relaxed}'
and `erc-move-to-prompt' in `pre-command-hook'. Remove assertions.

This still needs tests and a news entry.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v3-v4.diff --]
[-- Type: text/x-patch, Size: 7366 bytes --]

From 78e5fa32620b386b283f16383f16c713e628a0ff Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 18 Aug 2023 06:36:44 -0700
Subject: [PATCH 0/1] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6] Consider all windows in erc-scrolltobottom-mode

 lisp/erc/erc-goodies.el | 222 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 208 insertions(+), 14 deletions(-)

Interdiff:
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 129e17fb5ec..891641f77f7 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -100,15 +100,15 @@ erc--scrolltobottom-window-info
 `window-point', inclusive.")
 
 (defvar erc--scrolltobottom-post-force-commands
-  '(electric-newline-and-maybe-indent)
+  '(electric-newline-and-maybe-indent default-indent-new-line)
   "Commands that force a scroll after execution at prompt.")
 
-(defvar erc--scrolltobottom-relaxed-skip-commands '(recenter-top-bottom))
+(defvar erc--scrolltobottom-relaxed-skip-commands
+  '(recenter-top-bottom scroll-down-command))
 
 (defun erc--scrolltobottom-on-pre-command ()
-  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
-  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
-  (when (> (point) erc-input-marker)
+  (when (and (eq (selected-window) (get-buffer-window))
+             (>= (point) erc-input-marker))
     (setq erc--scrolltobottom-window-info
           (list (list (selected-window)
                       (window-start)
@@ -120,17 +120,17 @@ erc--scrolltobottom-on-post-command
 item is associated with the selected window, restore start of
 window so long as prompt hasn't moved.  Expect buffer to be
 unnarrowed."
-  (cl-assert (not erc-scrolltobottom-relaxed)) ; delete me
-  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
-  (if-let (((not (input-pending-p)))
-           (erc--scrolltobottom-window-info)
-           (found (car erc--scrolltobottom-window-info))
-           ((eq (car found) (selected-window)))
-           ((not (memq this-command erc--scrolltobottom-post-force-commands)))
-           ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
-      (set-window-start (selected-window) (nth 1 found))
-    (erc--scrolltobottom-confirm))
-  (setq erc--scrolltobottom-window-info nil))
+  (when (eq (selected-window) (get-buffer-window))
+    (if-let (((not (input-pending-p)))
+             (erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window)))
+             ((not (memq this-command
+                         erc--scrolltobottom-post-force-commands)))
+             ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
+        (set-window-start (selected-window) (nth 1 found))
+      (erc--scrolltobottom-confirm))
+    (setq erc--scrolltobottom-window-info nil)))
 
 (defun erc--scrolltobottom-on-pre-command-relaxed ()
   "Maybe scroll to bottom when away from prompt in an unnarrowed buffer.
@@ -139,28 +139,26 @@ erc--scrolltobottom-on-pre-command-relaxed
 `self-insert-command' (assuming `move-to-prompt' is active).
 When at prompt and current command is not `recenter-top-bottom',
 stash `erc--scrolltobottom-window-info' for the selected window."
-  (cl-assert erc-scrolltobottom-relaxed) ; delete me
-  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
-  (when (and (not (input-pending-p))
-             (< (point) erc-input-marker)
-             (memq this-command erc--scrolltobottom-relaxed-commands)
-             (< (window-end nil t) erc-input-marker))
-    (save-excursion
-      (goto-char (point-max))
-      (recenter (or erc-input-line-position -1))))
-  (when (and (> (point) erc-input-marker)
-             (not (memq this-command
-                        erc--scrolltobottom-relaxed-skip-commands)))
-    (setq erc--scrolltobottom-window-info
-          (list (list (selected-window)
-                      (window-start)
-                      (count-screen-lines (window-start) (point-max)))))))
+  (when (eq (selected-window) (get-buffer-window))
+    (when (and (not (input-pending-p))
+               (< (point) erc-input-marker)
+               (memq this-command erc--scrolltobottom-relaxed-commands)
+               (< (window-end nil t) erc-input-marker))
+      (save-excursion
+        (goto-char (point-max))
+        (recenter (or erc-input-line-position -1))))
+    (when (and (>= (point) erc-input-marker)
+               (not (memq this-command
+                          erc--scrolltobottom-relaxed-skip-commands)))
+      (setq erc--scrolltobottom-window-info
+            (list (list (selected-window)
+                        (window-start)
+                        (count-screen-lines (window-start) (point-max))))))))
 
 (defun erc--scrolltobottom-on-post-command-relaxed ()
   "Set window start or scroll when data was captured on pre-command."
-  (cl-assert erc-scrolltobottom-relaxed) ; delete me
-  (cl-assert (eq (selected-window) (get-buffer-window))) ; delete me
-  (when-let ((erc--scrolltobottom-window-info)
+  (when-let (((eq (selected-window) (get-buffer-window)))
+             (erc--scrolltobottom-window-info)
              (found (car erc--scrolltobottom-window-info))
              ((eq (car found) (selected-window))))
     (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
@@ -217,13 +215,13 @@ erc-add-scroll-to-bottom
                 (cl-pushnew 'self-insert-command
                             erc--scrolltobottom-relaxed-commands))
               (add-hook 'post-command-hook
-                        #'erc--scrolltobottom-on-post-command-relaxed -90 t)
+                        #'erc--scrolltobottom-on-post-command-relaxed 60 t)
               (add-hook 'pre-command-hook ; preempt `move-to-prompt'
-                        #'erc--scrolltobottom-on-pre-command-relaxed -90 t))
+                        #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
           (add-hook 'pre-command-hook
-                    #'erc--scrolltobottom-on-pre-command nil t)
+                    #'erc--scrolltobottom-on-pre-command 60 t)
           (add-hook 'post-command-hook
-                    #'erc--scrolltobottom-on-post-command nil t)))
+                    #'erc--scrolltobottom-on-post-command 60 t)))
     (remove-hook 'window-configuration-change-hook
                  #'erc--scrolltobottom-away-from-prompt t)
     (remove-hook 'pre-command-hook
@@ -246,7 +244,7 @@ erc--scrolltobottom-on-pre-insert
                         (and-let*
                             ((erc-scrolltobottom-relaxed)
                              (c (count-screen-lines (window-start w)
-                                                    (window-point w) nil w)))
+                                                    (point-max) nil w)))
                           (if (= ?\n (char-before (point-max))) (1+ c) c))))
                 (get-buffer-window-list nil nil 'visible))))
 
@@ -328,7 +326,7 @@ erc-move-to-prompt
 
 (defun erc-move-to-prompt-setup ()
   "Initialize the move-to-prompt module."
-  (add-hook 'pre-command-hook #'erc-move-to-prompt nil t))
+  (add-hook 'pre-command-hook #'erc-move-to-prompt 70 t))
 
 ;;; Keep place in unvisited channels
 ;;;###autoload(autoload 'erc-keep-place-mode "erc-goodies" nil t)
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch --]
[-- Type: text/x-patch, Size: 16254 bytes --]

From 78e5fa32620b386b283f16383f16c713e628a0ff Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 22 Jul 2023 00:46:44 -0700
Subject: [PATCH 1/1] [5.6] Consider all windows in erc-scrolltobottom-mode

* lisp/erc/erc-goodies.el (erc-input-line-position): Mention secondary
role when new option `erc-scroll-to-bottom-relaxed' is non-nil.
(erc-scrolltobottom-relaxed): New option to leave the prompt
stationary instead of forcing it to the bottom of the window.
(erc-scrolltobottom-mode, erc-scrolltobottom-enable,
erc-scrolltobottom-disable): Use `erc--scrolltobottom-all' instead
of `erc-possibly-scroll-to-bottom' for `erc-insert-done-hook' and now
also `erc-send-completed-hook'.  Add
`erc--scrolltobottom-on-pre-insert' to `erc-insert-pre-hook' and
`erc-pre-send-functions'.  Call `erc-add-scroll-to-bottom' for
teardown as well.
(erc--scrolltobottom-relaxed-commands,
erc--scrolltobottom-window-info,
erc--scrolltobottom-post-force-commands,
erc--scrolltobottom-relaxed-skip-commands): New internal variables.
(erc--scrolltobottom-on-pre-command
erc--scrolltobottom-on-post-command): New functions resembling
`erc-possibly-scroll-to-bottom' that try to avoid scrolling repeatedly
for no reason.
(erc--scrolltobottom-on-pre-command-relaxed,
erc--scrolltobottom-on-post-command-relaxed): New commands to help
implement `erc-scroll-to-bottom-relaxed'.
(erc--scrolltobottom-away-from-prompt): New function to scroll to
bottom on window configuration changes.
(erc--scrolltobottom-all): New function to scroll all windows
displaying the current buffer.
(erc-add-scroll-to-bottom): Perform teardown as well when mode is
disabled.  Also scroll on `window-configuration-changed-hook'.
(erc--scrolltobottom-on-pre-insert): New generic function that
remembers the last `window-start' and maybe the current line before
inserting a message in order to restore it afterward.
(erc--scrolltobottom-confirm): New function, a replacement for
`erc-scroll-to-bottom', that returns non-nil when it's actually
recentered the window.
(erc-move-to-prompt-setup): Add `erc-move-to-prompt' to
`pre-command-hook' at a depth of 70.
(erc--keep-place-indicator-setup): Add overlay arrow `after-string' in
non-graphical settings in case users have time stamps or other content
occupying the left margin.  (Bug#64855)
---
 lisp/erc/erc-goodies.el | 222 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 208 insertions(+), 14 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index d9ededa8e68..891641f77f7 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -44,42 +44,233 @@ erc-input-line-position
 This should be an integer specifying the line of the buffer on which
 the input line should stay.  A value of \"-1\" would keep the input
 line positioned on the last line in the buffer.  This is passed as an
-argument to `recenter'."
+argument to `recenter', unless `erc-scrolltobottom-relaxed' is
+non-nil, in which case, ERC interprets it as additional lines to
+scroll down by per message insertion (minus one for the prompt)."
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+(defcustom erc-scrolltobottom-relaxed nil
+  "Whether to forgo forcing prompt to the bottom of the window.
+When non-nil, and point is at the prompt, ERC scrolls the window
+up when inserting messages, making the prompt appear stationary.
+Users who find this effect too \"stagnant\" can adjust the option
+`erc-input-line-position', which ERC borrows to express a scroll
+offset when this option is non-nil.  Setting that value to zero
+lets the prompt drift toward the bottom by one line per message,
+which is generally slow enough not to distract while composing
+input.  Of course, this doesn't apply when receiving a large
+influx of messages, such as after typing \"/msg NickServ help\".
+Note that ERC only considers this option when initializing the
+`scrolltobottom' module and enabling `erc-scrolltobottom-mode'."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
+   (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
    (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (dolist (buffer (erc-buffer-list))
-     (with-current-buffer buffer
-       (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))))
+   (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
+   (erc-buffer-do #'erc-add-scroll-to-bottom)))
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
+(defvar-local erc--scrolltobottom-relaxed-commands '(end-of-buffer)
+  "Commands triggering a force scroll to prompt.
+Only applies with `erc-scrolltobottom-relaxed' when away from prompt.")
+
+(defvar-local erc--scrolltobottom-window-info nil
+  "Alist with windows as keys and lists of window-related info as values.
+Values are lists containing the last window start position and
+the last \"window line\" of point.  The \"window line\", which
+may be nil, is the number of lines between `window-start' and
+`window-point', inclusive.")
+
+(defvar erc--scrolltobottom-post-force-commands
+  '(electric-newline-and-maybe-indent default-indent-new-line)
+  "Commands that force a scroll after execution at prompt.")
+
+(defvar erc--scrolltobottom-relaxed-skip-commands
+  '(recenter-top-bottom scroll-down-command))
+
+(defun erc--scrolltobottom-on-pre-command ()
+  (when (and (eq (selected-window) (get-buffer-window))
+             (>= (point) erc-input-marker))
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point)))))))
+
+(defun erc--scrolltobottom-on-post-command ()
+  "Restore window start or scroll to prompt and recenter.
+When `erc--scrolltobottom-window-info' is non-nil and its first
+item is associated with the selected window, restore start of
+window so long as prompt hasn't moved.  Expect buffer to be
+unnarrowed."
+  (when (eq (selected-window) (get-buffer-window))
+    (if-let (((not (input-pending-p)))
+             (erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window)))
+             ((not (memq this-command
+                         erc--scrolltobottom-post-force-commands)))
+             ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
+        (set-window-start (selected-window) (nth 1 found))
+      (erc--scrolltobottom-confirm))
+    (setq erc--scrolltobottom-window-info nil)))
+
+(defun erc--scrolltobottom-on-pre-command-relaxed ()
+  "Maybe scroll to bottom when away from prompt in an unnarrowed buffer.
+When `erc-scrolltobottom-relaxed' is active, only scroll when
+prompt is past window's end and the command is `end-of-buffer' or
+`self-insert-command' (assuming `move-to-prompt' is active).
+When at prompt and current command is not `recenter-top-bottom',
+stash `erc--scrolltobottom-window-info' for the selected window."
+  (when (eq (selected-window) (get-buffer-window))
+    (when (and (not (input-pending-p))
+               (< (point) erc-input-marker)
+               (memq this-command erc--scrolltobottom-relaxed-commands)
+               (< (window-end nil t) erc-input-marker))
+      (save-excursion
+        (goto-char (point-max))
+        (recenter (or erc-input-line-position -1))))
+    (when (and (>= (point) erc-input-marker)
+               (not (memq this-command
+                          erc--scrolltobottom-relaxed-skip-commands)))
+      (setq erc--scrolltobottom-window-info
+            (list (list (selected-window)
+                        (window-start)
+                        (count-screen-lines (window-start) (point-max))))))))
+
+(defun erc--scrolltobottom-on-post-command-relaxed ()
+  "Set window start or scroll when data was captured on pre-command."
+  (when-let (((eq (selected-window) (get-buffer-window)))
+             (erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window))))
+    (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
+             (= (nth 2 found)
+                (count-screen-lines (window-start) (point-max))))
+        (set-window-start (selected-window) (nth 1 found))
+      (recenter (nth 2 found)))
+    (setq erc--scrolltobottom-window-info nil)))
+
+;; FIXME this is currently of little value because it doesn't restore
+;; the relative position of window point after changing dimensions.
+;; It would be preferable to instead stash the previous ratio of
+;; window line to body height and later recenter proportionally.  But
+;; that may not be worth the added bookkeeping.
+(defun erc--scrolltobottom-away-from-prompt ()
+  "Scroll to bottom unless at prompt."
+  (unless (input-pending-p)
+    (erc--scrolltobottom-confirm)))
+
+(defun erc--scrolltobottom-all (&rest _)
+  "Maybe put prompt on last line in all windows displaying current buffer.
+Expect to run when narrowing is in effect, such as on insertion
+or send-related hooks.  When recentering has not been performed,
+attempt to restore last `window-start', if known."
+  (dolist (window (get-buffer-window-list nil nil 'visible))
+    (with-selected-window window
+      (when-let
+          ((erc--scrolltobottom-window-info)
+           (found (assq window erc--scrolltobottom-window-info))
+           ((not (erc--scrolltobottom-confirm (nth 2 found)))))
+        (setf (window-start window) (cadr found)))))
+  ;; Necessary unless we're sure `erc--scrolltobottom-on-pre-insert'
+  ;; always runs between calls to this function.
+  (setq erc--scrolltobottom-window-info nil))
+
 (defun erc-add-scroll-to-bottom ()
-  "A hook function for `erc-mode-hook' to recenter output at bottom of window.
+  "Arrange for scrolling to bottom on window configuration changes.
+Undo that arrangement when disabling `erc-scrolltobottom-mode'.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
 
-This works whenever scrolling happens, so it's added to
-`window-scroll-functions' rather than `erc-insert-post-hook'."
-  (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+Note that the prior suggestion comes from a time when this
+function used `window-scroll-functions', which was replaced by
+`post-command-hook' in ERC 5.3."
+  (if erc-scrolltobottom-mode
+      (progn
+        (add-hook 'window-configuration-change-hook
+                  #'erc--scrolltobottom-away-from-prompt nil t)
+        (if erc-scrolltobottom-relaxed
+            (progn
+              (when (or (bound-and-true-p erc-move-to-prompt-mode)
+                        (memq 'move-to-prompt erc-modules))
+                (cl-pushnew 'self-insert-command
+                            erc--scrolltobottom-relaxed-commands))
+              (add-hook 'post-command-hook
+                        #'erc--scrolltobottom-on-post-command-relaxed 60 t)
+              (add-hook 'pre-command-hook ; preempt `move-to-prompt'
+                        #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
+          (add-hook 'pre-command-hook
+                    #'erc--scrolltobottom-on-pre-command 60 t)
+          (add-hook 'post-command-hook
+                    #'erc--scrolltobottom-on-post-command 60 t)))
+    (remove-hook 'window-configuration-change-hook
+                 #'erc--scrolltobottom-away-from-prompt t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command-relaxed t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command-relaxed t)
+    (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
+    (kill-local-variable 'erc--scrolltobottom-window-info)))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert (_input-or-string)
+  "Remember the `window-start' before inserting a message."
+  (setq erc--scrolltobottom-window-info
+        (mapcar (lambda (w)
+                  (list w
+                        (window-start w)
+                        (and-let*
+                            ((erc-scrolltobottom-relaxed)
+                             (c (count-screen-lines (window-start w)
+                                                    (point-max) nil w)))
+                          (if (= ?\n (char-before (point-max))) (1+ c) c))))
+                (get-buffer-window-list nil nil 'visible))))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert ((input erc-input))
+  "Remember the `window-start' before inserting a message."
+  (when (erc-input-insertp input)
+    (cl-call-next-method)))
+
+(defun erc--scrolltobottom-confirm (&optional scroll-to)
+  "Like `erc-scroll-to-bottom', but use `window-point'.
+Expect to run in some window, not necessarily the user-selected
+one.  Scroll to SCROLL-TO (or 0) lines from the window's top.
+Return non-nil when recentering has occurred."
+  (when erc-insert-marker
+    (let ((resize-mini-windows nil))
+      (save-restriction
+        (widen)
+        (when (>= (window-point) erc-input-marker)
+          (save-excursion
+            (goto-char (point-max))
+            (recenter (+ (or scroll-to 0) (or erc-input-line-position -1)))
+            t))))))
 
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
 
-This is added to `window-scroll-functions' by `erc-add-scroll-to-bottom'.
-
 You can control which line is recentered to by customizing the
 variable `erc-input-line-position'."
       ;; Temporarily bind resize-mini-windows to nil so that users who have it
@@ -135,7 +326,7 @@ erc-move-to-prompt
 
 (defun erc-move-to-prompt-setup ()
   "Initialize the move-to-prompt module."
-  (add-hook 'pre-command-hook #'erc-move-to-prompt nil t))
+  (add-hook 'pre-command-hook #'erc-move-to-prompt 70 t))
 
 ;;; Keep place in unvisited channels
 ;;;###autoload(autoload 'erc-keep-place-mode "erc-goodies" nil t)
@@ -213,12 +404,15 @@ erc--keep-place-indicator-setup
   (add-hook 'window-configuration-change-hook
             #'erc--keep-place-indicator-on-window-configuration-change nil t)
   (when-let* (((memq erc-keep-place-indicator-style '(t arrow)))
+              (ov-property (if (zerop (fringe-columns 'left))
+                               'after-string
+                             'before-string))
               (display (if (zerop (fringe-columns 'left))
                            `((margin left-margin) ,overlay-arrow-string)
                          '(left-fringe right-triangle
                                        erc-keep-place-indicator-arrow)))
               (bef (propertize " " 'display display)))
-    (overlay-put erc--keep-place-indicator-overlay 'before-string bef))
+    (overlay-put erc--keep-place-indicator-overlay ov-property bef))
   (when (memq erc-keep-place-indicator-style '(t face))
     (overlay-put erc--keep-place-indicator-overlay 'face
                  'erc-keep-place-indicator-line)))
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
  2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
                   ` (2 preceding siblings ...)
  2023-08-18 13:50 ` J.P.
@ 2023-08-24 14:11 ` J.P.
       [not found] ` <87il948r8x.fsf@neverwas.me>
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-08-24 14:11 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 136 bytes --]

v5. Make new behavior hinge on new option, disabled by default. Assign
some module functions explicit hook depths. Add news and tests.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v4-v5.diff --]
[-- Type: text/x-patch, Size: 48092 bytes --]

From 3ed5a748d76989ec2ed79311e3550da5c6d1aaa1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 24 Aug 2023 06:49:46 -0700
Subject: [PATCH 0/1] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (1):
  [5.6] Consider all windows in erc-scrolltobottom-mode

 etc/ERC-NEWS                                  |  19 +-
 lisp/erc/erc-backend.el                       |   2 +-
 lisp/erc/erc-goodies.el                       | 267 ++++++++++++++++--
 .../erc/erc-scenarios-scrolltobottom-all.el   |  48 ++++
 .../erc-scenarios-scrolltobottom-relaxed.el   | 140 +++++++++
 test/lisp/erc/erc-scenarios-scrolltobottom.el |  44 +++
 .../erc/resources/base/assoc/bumped/again.eld |  10 +-
 .../resources/base/assoc/bumped/foisted.eld   |  10 +-
 .../resources/base/assoc/bumped/refoisted.eld |   8 +-
 .../resources/base/netid/bouncer/barnet.eld   |   2 +-
 .../resources/base/netid/bouncer/foonet.eld   |   2 +-
 test/lisp/erc/resources/erc-d/erc-d-t.el      |   7 +-
 .../erc/resources/erc-scenarios-common.el     | 196 +++++++++++++
 .../erc/resources/scrolltobottom/help.eld     |  46 +++
 14 files changed, 761 insertions(+), 40 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom-all.el
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom.el
 create mode 100644 test/lisp/erc/resources/scrolltobottom/help.eld

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 94452514e6d..72b768b02ec 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -178,6 +178,15 @@ been restored with a slightly revised role contingent on a few
 assumptions explained in its doc string.  For clarity, it has been
 renamed 'erc-ensure-target-buffer-on-privmsg'.
 
+** Module 'scrolltobottom' attempts to behave more sensibly.
+Enabling the experimental option 'erc-scrolltobottom-all' tells
+'scrolltobottom' to be more vigilant about recentering and to do so in
+all ERC windows.  The dependent option 'erc-scrolltobottom-relaxed',
+likewise experimental, makes ERC's prompt stationary wherever it
+resides instead of forcing it to the bottom of the window.  That is,
+new input appears above the prompt, scrolling existing messages upward
+to compensate.
+
 ** Subtle changes in two fundamental faces.
 Users of the default theme may notice that 'erc-action-face' and
 'erc-notice-face' now appear slightly less bold on systems supporting
@@ -232,9 +241,9 @@ property of the same name has been retained and now has a value of
 Built-in and third-party modules rely on certain hooks for adjusting
 incoming and outgoing messages upon insertion.  And some modules only
 want to do so after others have done their damage.  Traditionally,
-this required various hacks and finagling to achieve.  And while this
-release makes an effort to load modules in a more consistent order,
-that alone isn't enough to ensure similar predictability among
+this has required various hacks and finagling to achieve.  And while
+this release makes an effort to load modules in a more consistent
+order, that alone isn't enough to ensure similar predictability among
 essential members of important hooks.
 
 Luckily, ERC now leverages a feature introduced in Emacs 27, "hook
@@ -248,6 +257,10 @@ the first two, 'erc-button-add-buttons' and 'erc-fill', which have
 been swapped with respect to their previous places in recent ERC
 versions.
 
+The same depth interval is now also provisionally reserved for
+'erc-insert-pre-hook' and for the non-ERC hooks 'pre-command-hook' and
+'post-command-hook', but only locally, in ERC buffers.
+
 *** ERC now manages timestamp-related properties a bit differently.
 For starters, the 'cursor-sensor-functions' property no longer
 contains unique closures and thus no longer proves effective for
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index eb3ec39fedd..9e121ec1e92 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1089,7 +1089,7 @@ erc--hide-prompt
         (put-text-property erc-insert-marker (1- erc-input-marker)
                            'erc-prompt 'hidden)
         (erc--conceal-prompt))
-      (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 91 t))))
+      (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 80 t))))
 
 (defun erc-process-sentinel (cproc event)
   "Sentinel function for ERC process."
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 30089915c19..07a93b06f2c 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -50,19 +50,31 @@ erc-input-line-position
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+(defcustom erc-scrolltobottom-all nil
+  "Whether to scroll all windows or just the selected one.
+A value of nil preserves pre-5.6 behavior, in which scrolling
+only affects the selected window.  Users should consider its
+non-nil behavior experimental for the time being.  Note also that
+ERC expects this option to be configured before module
+initialization."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
 (defcustom erc-scrolltobottom-relaxed nil
   "Whether to forgo forcing prompt to the bottom of the window.
 When non-nil, and point is at the prompt, ERC scrolls the window
 up when inserting messages, making the prompt appear stationary.
 Users who find this effect too \"stagnant\" can adjust the option
 `erc-input-line-position', which ERC borrows to express a scroll
-offset when this option is non-nil.  Setting that value to zero
-lets the prompt drift toward the bottom by one line per message,
-which is generally slow enough not to distract while composing
-input.  Of course, this doesn't apply when receiving a large
-influx of messages, such as after typing \"/msg NickServ help\".
-Note that ERC only considers this option when initializing the
-`scrolltobottom' module and enabling `erc-scrolltobottom-mode'."
+step offset when this option is non-nil.  Setting that value to
+zero lets the prompt drift toward the bottom by one line per
+message, which is generally slow enough not to distract while
+composing input.  Of course, this doesn't apply when receiving a
+large influx of messages, such as after typing \"/msg NickServ
+help\".  Note that ERC only considers this option when the
+experimental companion option `erc-scrolltobottom-all' is enabled
+and, only then, during module setup."
   :group 'erc-display
   :package-version '(ERC . "5.6") ; FIXME sync on release
   :type 'boolean)
@@ -70,18 +82,25 @@ erc-scrolltobottom-relaxed
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
-  ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
-   (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
-   (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
-   (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
-   (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
-  ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
-   (remove-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
-   (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
-   (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
-   (erc-buffer-do #'erc-add-scroll-to-bottom)))
+  ((add-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
+   (unless erc--updating-modules-p (erc-buffer-do #'erc--scrolltobottom-setup))
+   (if erc-scrolltobottom-all
+       (progn
+         (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert 25)
+         (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+         (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+         (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all))
+     (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)))
+  ((remove-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
+   (erc-buffer-do #'erc--scrolltobottom-setup)
+   (if erc-scrolltobottom-all
+       (progn
+         (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+         (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
+         (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+         (remove-hook 'erc-pre-send-functions
+                      #'erc--scrolltobottom-on-pre-insert))
+     (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom))))
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
@@ -100,8 +119,12 @@ erc--scrolltobottom-window-info
 `window-point', inclusive.")
 
 (defvar erc--scrolltobottom-post-force-commands
-  '(electric-newline-and-maybe-indent default-indent-new-line)
-  "Commands that force a scroll after execution at prompt.")
+  '(beginning-of-buffer
+    electric-newline-and-maybe-indent
+    default-indent-new-line)
+  "Commands that force a scroll after execution at prompt.
+That is, ERC recalculates the window's start instead of blindly
+restoring it.")
 
 (defvar erc--scrolltobottom-relaxed-skip-commands
   '(recenter-top-bottom scroll-down-command))
@@ -112,7 +135,7 @@ erc--scrolltobottom-on-pre-command
     (setq erc--scrolltobottom-window-info
           (list (list (selected-window)
                       (window-start)
-                      (count-screen-lines (window-start) (point)))))))
+                      (count-screen-lines (window-start) (point-max)))))))
 
 (defun erc--scrolltobottom-on-post-command ()
   "Restore window start or scroll to prompt and recenter.
@@ -127,7 +150,8 @@ erc--scrolltobottom-on-post-command
              ((eq (car found) (selected-window)))
              ((not (memq this-command
                          erc--scrolltobottom-post-force-commands)))
-             ((= (nth 2 found) (count-screen-lines (window-start) (point)))))
+             ((= (nth 2 found)
+                 (count-screen-lines (window-start) (point-max)))))
         (set-window-start (selected-window) (nth 1 found))
       (erc--scrolltobottom-confirm))
     (setq erc--scrolltobottom-window-info nil)))
@@ -137,8 +161,9 @@ erc--scrolltobottom-on-pre-command-relaxed
 When `erc-scrolltobottom-relaxed' is active, only scroll when
 prompt is past window's end and the command is `end-of-buffer' or
 `self-insert-command' (assuming `move-to-prompt' is active).
-When at prompt and current command is not `recenter-top-bottom',
-stash `erc--scrolltobottom-window-info' for the selected window."
+When at prompt and current command does not appear in
+`erc--scrolltobottom-relaxed-skip-commands', stash
+`erc--scrolltobottom-window-info' for the selected window."
   (when (eq (selected-window) (get-buffer-window))
     (when (and (not (input-pending-p))
                (< (point) erc-input-marker)
@@ -175,7 +200,9 @@ erc--scrolltobottom-on-post-command-relaxed
 ;; that may not be worth the added bookkeeping.
 (defun erc--scrolltobottom-away-from-prompt ()
   "Scroll to bottom unless at prompt."
-  (unless (input-pending-p)
+  (unless (or (input-pending-p)
+              (minibuffer-window-active-p (minibuffer-window))
+              (eq (old-selected-window) (minibuffer-window)))
     (erc--scrolltobottom-confirm)))
 
 (defun erc--scrolltobottom-all (&rest _)
@@ -195,8 +222,7 @@ erc--scrolltobottom-all
   (setq erc--scrolltobottom-window-info nil))
 
 (defun erc-add-scroll-to-bottom ()
-  "Arrange for scrolling to bottom on window configuration changes.
-Undo that arrangement when disabling `erc-scrolltobottom-mode'.
+  "A hook function for `erc-mode-hook' to recenter output at bottom of window.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
@@ -204,24 +230,35 @@ erc-add-scroll-to-bottom
 Note that the prior suggestion comes from a time when this
 function used `window-scroll-functions', which was replaced by
 `post-command-hook' in ERC 5.3."
+  (declare (obsolete erc--scrolltobottom-setup "30.1"))
+  (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+
+(cl-defgeneric erc--scrolltobottom-setup ()
+  "Arrange for scrolling to bottom on window configuration changes.
+Undo that arrangement when disabling `erc-scrolltobottom-mode'."
   (if erc-scrolltobottom-mode
-      (progn
+      (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t)
+    (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))
+
+(cl-defmethod erc--scrolltobottom-setup (&context
+                                         (erc-scrolltobottom-all (eql t)))
+  (if erc-scrolltobottom-mode
+      (if erc-scrolltobottom-relaxed
+          (progn
+            (when (or (bound-and-true-p erc-move-to-prompt-mode)
+                      (memq 'move-to-prompt erc-modules))
+              (cl-pushnew 'self-insert-command
+                          erc--scrolltobottom-relaxed-commands))
+            (add-hook 'post-command-hook
+                      #'erc--scrolltobottom-on-post-command-relaxed 60 t)
+            (add-hook 'pre-command-hook ; preempt `move-to-prompt'
+                      #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
         (add-hook 'window-configuration-change-hook
                   #'erc--scrolltobottom-away-from-prompt nil t)
-        (if erc-scrolltobottom-relaxed
-            (progn
-              (when (or (bound-and-true-p erc-move-to-prompt-mode)
-                        (memq 'move-to-prompt erc-modules))
-                (cl-pushnew 'self-insert-command
-                            erc--scrolltobottom-relaxed-commands))
-              (add-hook 'post-command-hook
-                        #'erc--scrolltobottom-on-post-command-relaxed 60 t)
-              (add-hook 'pre-command-hook ; preempt `move-to-prompt'
-                        #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
-          (add-hook 'pre-command-hook
-                    #'erc--scrolltobottom-on-pre-command 60 t)
-          (add-hook 'post-command-hook
-                    #'erc--scrolltobottom-on-post-command 60 t)))
+        (add-hook 'pre-command-hook
+                  #'erc--scrolltobottom-on-pre-command 60 t)
+        (add-hook 'post-command-hook
+                  #'erc--scrolltobottom-on-post-command 60 t))
     (remove-hook 'window-configuration-change-hook
                  #'erc--scrolltobottom-away-from-prompt t)
     (remove-hook 'pre-command-hook
@@ -255,9 +292,9 @@ erc--scrolltobottom-on-pre-insert
 
 (defun erc--scrolltobottom-confirm (&optional scroll-to)
   "Like `erc-scroll-to-bottom', but use `window-point'.
-Expect to run in some window, not necessarily the user-selected
-one.  Scroll to SCROLL-TO (or 0) lines from the window's top.
-Return non-nil when recentering has occurred."
+Position current line (with `recenter') SCROLL-TO lines below
+window's top.  Return nil if point is not in prompt area or if
+prompt isn't ready."
   (when erc-insert-marker
     (let ((resize-mini-windows nil))
       (save-restriction
@@ -332,7 +369,7 @@ erc-move-to-prompt-setup
 ;;;###autoload(autoload 'erc-keep-place-mode "erc-goodies" nil t)
 (define-erc-module keep-place nil
   "Leave point above un-viewed text in other channels."
-  ((add-hook 'erc-insert-pre-hook  #'erc-keep-place))
+  ((add-hook 'erc-insert-pre-hook  #'erc-keep-place 85))
   ((remove-hook 'erc-insert-pre-hook  #'erc-keep-place)))
 
 (defcustom erc-keep-place-indicator-style t
@@ -427,7 +464,7 @@ keep-place-indicator
          ((memq 'keep-place erc-modules)
           (erc-keep-place-mode +1))
          ;; Enable a local version of `keep-place-mode'.
-         (t (add-hook 'erc-insert-pre-hook  #'erc-keep-place 90 t)))
+         (t (add-hook 'erc-insert-pre-hook  #'erc-keep-place 85 t)))
    (if (pcase erc-keep-place-indicator-buffer-type
          ('target erc--target)
          ('server (not erc--target))
@@ -450,7 +487,7 @@ erc--keep-place-indicator-on-global-module
 global one."
   (if erc-keep-place-mode
       (remove-hook 'erc-insert-pre-hook  #'erc-keep-place t)
-    (add-hook 'erc-insert-pre-hook  #'erc-keep-place 90 t)))
+    (add-hook 'erc-insert-pre-hook  #'erc-keep-place 85 t)))
 
 (defun erc-keep-place-move (pos)
   "Move keep-place indicator to current line or POS.
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom-all.el b/test/lisp/erc/erc-scenarios-scrolltobottom-all.el
new file mode 100644
index 00000000000..33f232e64d9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom-all.el
@@ -0,0 +1,48 @@
+;;; erc-scenarios-scrolltobottom-all.el --- erc-scrolltobottom-all test -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--all ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (let ((erc-scrolltobottom-all t))
+
+    (erc-scenarios-scrolltobottom--normal
+     (lambda ()
+       (ert-info ("New insertion anchors prompt in other window")
+         (let ((w (next-window)))
+           ;; We're at prompt and aligned to bottom.
+           (should (>= (window-point w) erc-input-marker))
+           (erc-d-t-wait-for 10
+               (erc-scenarios-common--at-win-end-p w))
+           (erc-d-t-ensure-for 0.5
+               (erc-scenarios-common--at-win-end-p w))))))))
+
+;;; erc-scenarios-scrolltobottom-all.el ends here
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
new file mode 100644
index 00000000000..7d256bf711b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
@@ -0,0 +1,140 @@
+;;; erc-scenarios-scrolltobottom-relaxed.el --- erc-scrolltobottom-relaxed -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;; TODO assert behavior of prompt input spanning multiple lines, with
+;; and without line endings.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--relaxed ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (erc-scenarios-common-with-noninteractive-in-term
+      ((erc-scenarios-common-dialog "scrolltobottom")
+       (dumb-server (erc-d-run "localhost" t 'help))
+       (port (process-contact dumb-server :service))
+       (erc-modules `(scrolltobottom fill-wrap ,@erc-modules))
+       (erc-scrolltobottom-all t)
+       (erc-scrolltobottom-relaxed t)
+       (erc-server-flood-penalty 0.1)
+       (expect (erc-d-t-make-expecter))
+       lower upper)
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :nick "tester")
+        (funcall expect 10 "debug mode")))
+
+    (with-current-buffer "foonet"
+      (should (looking-at " and"))
+      (set-window-buffer nil (current-buffer))
+      (delete-other-windows)
+      (split-window-below 15)
+      (recenter 0)
+
+      (ert-info ("Moving into prompt does not trigger scroll")
+        (with-selected-window (next-window)
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (recenter 0)
+          (goto-char (1- erc-insert-marker))
+          (execute-kbd-macro "\C-n")
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (should (= (point) (point-max)))
+          (setq lower (count-screen-lines (window-start) (window-point)))))
+
+      (ert-info ("Module `move-to-prompt' still works")
+        ;; Prompt is somewhere in the middle of the window.
+        (should (erc-scenarios-common--above-win-end-p))
+        (should-not (= (point-max) (point)))
+        ;; Hitting a self-insert key triggers `move-to-prompt' but not
+        ;; a scroll (to bottom).
+        (execute-kbd-macro "hi")
+        ;; Prompt and input appear on same line.
+        (should (= (point-max) (point)))
+        (setq upper (count-screen-lines (window-start) (window-point)))
+        (should-not (= upper (window-body-height))))
+
+      (ert-info ("Command `recenter-top-bottom' allowed at prompt")
+        ;; Hitting C-l recenters the window.
+        (should (= upper (count-screen-lines (window-start) (window-point))))
+        (let ((lines (list upper)))
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (setq lines (delete-dups lines))
+          (should (= (length lines) 4))))
+
+      (ert-info ("Command `beginning-of-buffer' allowed at prompt")
+        ;; Hitting C-< goes to beginning of buffer.
+        (execute-kbd-macro "\M-<")
+        (should (= 1 (point)))
+        (redisplay)
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New message doesn't trigger scroll when away from prompt")
+        ;; Arriving insertions don't trigger a scroll when away from the
+        ;; prompt.  New output not seen.
+        (erc-cmd-MSG "NickServ help register")
+        (save-excursion (erc-d-t-search-for 10 "End of NickServ"))
+        (should (= 1 (point)))
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New insertion keeps prompt stationary in other window")
+        (let ((w (next-window)))
+          ;; We're at prompt and completely stationary.
+          (should (>= (window-point w) erc-input-marker))
+          (erc-d-t-wait-for 10
+              (= lower (count-screen-lines (window-start w) (window-point w))))
+          (erc-d-t-ensure-for 0.5
+              (= lower (count-screen-lines (window-start w)
+                                           (window-point w))))))
+
+      (should (= 2 (length (window-list))))
+      (ert-info ("New message does not trigger a scroll when at prompt")
+        ;; Recenter so prompt is above rather than at window's end.
+        (funcall expect 10 "End of NickServ HELP")
+        (recenter 0)
+        (set-window-point nil (point-max))
+        (setq upper (count-screen-lines (window-start) (window-point)))
+        ;; Prompt is somewhere in the middle of the window.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--above-win-end-p))
+        (erc-scenarios-common-say "/msg NickServ help identify")
+        ;; New arriving messages don't move prompt.
+        (erc-d-t-ensure-for 1
+            (= upper (count-screen-lines (window-start) (window-point))))
+        (funcall expect 10 "IDENTIFY lets you login")))))
+
+;;; erc-scenarios-scrolltobottom-relaxed.el ends here
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom.el b/test/lisp/erc/erc-scenarios-scrolltobottom.el
new file mode 100644
index 00000000000..44e64204fb1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom.el
@@ -0,0 +1,44 @@
+;;; erc-scenarios-scrolltobottom.el --- erc-scrolltobottom-mode -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--normal ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (erc-scenarios-scrolltobottom--normal
+   (lambda ()
+     (ert-info ("New insertion doesn't anchor prompt in other window")
+       (let ((w (next-window)))
+         ;; We're at prompt but not aligned to bottom.
+         (should (>= (window-point w) erc-input-marker))
+         (erc-d-t-wait-for 10
+             (not (erc-scenarios-common--at-win-end-p w))))))))
+
+;;; erc-scenarios-scrolltobottom.el ends here
diff --git a/test/lisp/erc/resources/base/assoc/bumped/again.eld b/test/lisp/erc/resources/base/assoc/bumped/again.eld
index ab3c7b06214..aef164b6237 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/again.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/again.eld
@@ -1,10 +1,10 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
  (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
 
-((nick 3 "NICK tester`")
+((nick 10 "NICK tester`")
  (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
  (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -21,10 +21,10 @@
  (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
  (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
 
-((mode-user 3.2 "MODE tester` +i")
+((mode-user 10 "MODE tester` +i")
  (0.0 ":irc.foonet.org 221 tester` +i")
  (0.0 ":irc.foonet.org NOTICE tester` :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+((privmsg 10 "PRIVMSG NickServ :IDENTIFY tester changeme")
  (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
  (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/resources/base/assoc/bumped/foisted.eld b/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
index 5c36e58d9d3..0f7aadac564 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
@@ -1,6 +1,6 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
  (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -17,14 +17,14 @@
  (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
  (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
 
-((mode-user 1.2 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0.0 ":irc.foonet.org 221 tester +i")
  (0.0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((privmsg 17.21 "PRIVMSG bob :hi")
+((privmsg 10 "PRIVMSG bob :hi")
  (0.02 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :hola")
  (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :how r u?"))
 
-((quit 18.19 "QUIT :" quit)
+((quit 10 "QUIT :" quit)
  (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
 ((drop 1 DROP))
diff --git a/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld b/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
index 33e4168ac46..63366d3f576 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
@@ -1,6 +1,6 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.1 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
  (0.0 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 dummy :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -22,10 +22,10 @@
  (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG dummy :back?")
  )
 
-((mode-user 1.2 "MODE dummy +i")
+((mode-user 10 "MODE dummy +i")
  (0.0 ":irc.foonet.org 221 dummy +i")
  (0.0 ":irc.foonet.org NOTICE dummy :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((renick 42.6 "NICK tester")
+((renick 10 "NICK tester")
  (0.01 ":dummy!~u@rpaau95je67ci.irc NICK tester")
  (0.0 ":NickServ!NickServ@localhost NOTICE dummy :You're now logged in as tester"))
diff --git a/test/lisp/erc/resources/base/netid/bouncer/barnet.eld b/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
index 204d01fef77..596383c2699 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
@@ -38,4 +38,4 @@
  (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
  (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonor in doing it."))
 
-((linger 1 LINGER))
+((linger 2 LINGER))
diff --git a/test/lisp/erc/resources/base/netid/bouncer/foonet.eld b/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
index 4445350ca0c..2e1a3ac27da 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
@@ -43,4 +43,4 @@
  (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
  (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
 
-((linger 1 LINGER))
+((linger 2 LINGER))
diff --git a/test/lisp/erc/resources/erc-d/erc-d-t.el b/test/lisp/erc/resources/erc-d/erc-d-t.el
index 7b2adf4f07b..cf869fb3c70 100644
--- a/test/lisp/erc/resources/erc-d/erc-d-t.el
+++ b/test/lisp/erc/resources/erc-d/erc-d-t.el
@@ -83,6 +83,8 @@ erc-d-t-with-cleanup
                (ignore-errors (kill-buffer buf)))))
          (sleep-for erc-d-t-cleanup-sleep-secs)))))
 
+(defvar erc-d-t--wait-message-prefix "Awaiting: ")
+
 (defmacro erc-d-t-wait-for (max-secs msg &rest body)
   "Wait for BODY to become non-nil.
 Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
@@ -99,7 +101,7 @@ erc-d-t-wait-for
   (let ((inverted (make-symbol "inverted"))
         (time-out (make-symbol "time-out"))
         (result (make-symbol "result")))
-    `(ert-info ((concat "Awaiting: " ,msg))
+    `(ert-info ((concat erc-d-t--wait-message-prefix ,msg))
        (let ((,time-out (abs ,max-secs))
              (,inverted (< ,max-secs 0))
              (,result ',result))
@@ -120,7 +122,8 @@ erc-d-t-ensure-for
   (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
     (push msg body)
     (setq msg (prin1-to-string body)))
-  `(erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body))))
+  `(let ((erc-d-t--wait-message-prefix "Sustaining: "))
+     (erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body)))))
 
 (defun erc-d-t-search-for (timeout text &optional from on-success)
   "Wait for TEXT to appear in current buffer before TIMEOUT secs.
diff --git a/test/lisp/erc/resources/erc-scenarios-common.el b/test/lisp/erc/resources/erc-scenarios-common.el
index 972faa5c73f..b92acdd81e8 100644
--- a/test/lisp/erc/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/resources/erc-scenarios-common.el
@@ -183,6 +183,103 @@ erc-scenarios-common-with-cleanup
 
        ,@body)))
 
+(defvar erc-scenarios-common--term-size '(34 . 80))
+(declare-function term-char-mode "term" nil)
+(declare-function term-line-mode "term" nil)
+
+(defun erc-scenarios-common--run-in-term (&optional debug)
+  (require 'term)
+  (let* ((default-directory (getenv "EMACS_TEST_DIRECTORY"))
+         (emacs (expand-file-name invocation-name invocation-directory))
+         (process-environment (cons "HOME=/nonexistent"
+                                    (cons "ERC_TESTS_SUBPROCESS=1"
+                                          process-environment)))
+         (name (ert-test-name (ert-running-test)))
+         (temp-file (make-temp-file "erc-term-test-"))
+         (cmd `(let ((stats 1))
+                 (setq enable-dir-local-variables nil)
+                 (unwind-protect
+                     (setq stats (ert-run-tests-batch ',name))
+                   (unless ',debug
+                     (let ((buf (with-current-buffer (messages-buffer)
+                                  (buffer-string))))
+                       (with-temp-file ,temp-file
+                         (insert buf)))
+                     (kill-emacs (ert-stats-completed-unexpected stats))))))
+         ;; The `ert-test' object in Emacs 29 has a `file-name' field.
+         (file-name (symbol-file name 'ert--test))
+         (default-directory (expand-file-name (file-name-directory file-name)))
+         (package (if-let* ((found (getenv "ERC_PACKAGE_NAME"))
+                            ((string-prefix-p "erc-" found)))
+                      (intern found)
+                    'erc))
+         ;; This is for testing ERC's ELPA-package on older Emacsen we
+         ;; still support.  It won't run inside the emacs.git tree.
+         (setup (and (featurep 'compat)
+                     `(progn
+                        (require 'package)
+                        (let ((package-load-list '((compat t) (,package t))))
+                          (package-initialize)))))
+         ;; Make subprocess terminal bigger than controlling.
+         (buf (cl-letf (((symbol-function 'window-screen-lines)
+                         (lambda () (car erc-scenarios-common--term-size)))
+                        ((symbol-function 'window-max-chars-per-line)
+                         (lambda () (cdr erc-scenarios-common--term-size))))
+                (make-term (symbol-name name) emacs nil "-Q" "-nw"
+                           "-eval" (prin1-to-string setup)
+                           "-l" file-name "-eval" (format "%S" cmd))))
+         (proc (get-buffer-process buf))
+         (err (lambda ()
+                (with-temp-buffer
+                  (insert-file-contents temp-file)
+                  (message "Subprocess: %s" (buffer-string))
+                  (delete-file temp-file)))))
+    (set-window-buffer (selected-window) buf)
+    (delete-other-windows)
+    (with-current-buffer buf
+      (set-process-query-on-exit-flag proc nil)
+      (unless noninteractive (term-char-mode))
+      (with-timeout (30 (funcall err) (error "Timed out awaiting result"))
+        (while (process-live-p proc)
+          (accept-process-output proc 0.1)
+          (unless noninteractive
+            (redisplay))))
+      (while (accept-process-output proc))
+      (term-line-mode)
+      (goto-char (point-min))
+      ;; Otherwise gives process exited abnormally with exit-code >0
+      (unless (search-forward (format "Process %s finished" name) nil t)
+        (funcall err)
+        (ert-fail (when (search-forward "exited" nil t)
+                    (buffer-substring-no-properties (line-beginning-position)
+                                                    (line-end-position)))))
+      (delete-file temp-file)
+      (when noninteractive
+        (kill-buffer)))))
+
+(defvar erc-scenarios-common-interactive-debug-term-p nil
+  "When non-nil, run interactive ")
+
+(defmacro erc-scenarios-common-with-noninteractive-in-term (&rest body)
+  "Run BODY via `erc-scenarios-common-with-cleanup' in a `term' subprocess.
+Also do so when `erc-scenarios-common-interactive-debug-term-p'
+is non-nil.  When debugging, leave the `term-mode' buffer around
+for inspection and name it after the test, bounded by asterisks.
+When debugging, ensure the test always fails, as a reminder to
+disable `erc-scenarios-common-interactive-debug-term-p'.
+
+See Info node `(emacs) Term Mode' for the various commands."
+  (declare (indent 1))
+  `(if (and (or erc-scenarios-common-interactive-debug-term-p
+                noninteractive)
+            (not (getenv "ERC_TESTS_SUBPROCESS")))
+       (progn
+         (when (memq system-type '(windows-nt ms-dos))
+           (ert-skip "System must be UNIX"))
+         (erc-scenarios-common--run-in-term
+          erc-scenarios-common-interactive-debug-term-p))
+     (erc-scenarios-common-with-cleanup ,@body)))
+
 (defun erc-scenarios-common-assert-initial-buf-name (id port)
   ;; Assert no limbo period when explicit ID given
   (should (string= (if id
@@ -209,9 +306,108 @@ erc-scenarios-common-say
     (insert str)
     (erc-send-current-line)))
 
+(defun erc-scenarios-common--at-win-end-p (&optional window)
+  (= (window-body-height window)
+     (count-screen-lines (window-start window) (point-max) nil window)))
+
+(defun erc-scenarios-common--above-win-end-p (&optional window)
+  (> (window-body-height window)
+     (count-screen-lines (window-start window) (point-max))))
+
+(defun erc-scenarios-common--prompt-past-win-end-p (&optional window)
+  (< (window-body-height window)
+     (count-screen-lines (window-start window) (point-max))))
+
+(defun erc-scenarios-common--recenter-top-bottom-around (orig &rest args)
+  (let (this-command last-command) (apply orig args)))
+
+(defun erc-scenarios-common--recenter-top-bottom ()
+  (advice-add 'recenter-top-bottom
+              :around #'erc-scenarios-common--recenter-top-bottom-around)
+  (execute-kbd-macro "\C-l")
+  (advice-remove 'recenter-top-bottom
+                 #'erc-scenarios-common--recenter-top-bottom-around))
+
 
 ;;;; Fixtures
 
+(defun erc-scenarios-scrolltobottom--normal (test)
+  (erc-scenarios-common-with-noninteractive-in-term
+      ((erc-scenarios-common-dialog "scrolltobottom")
+       (dumb-server (erc-d-run "localhost" t 'help))
+       (port (process-contact dumb-server :service))
+       (erc-modules `(scrolltobottom fill-wrap ,@erc-modules))
+       (erc-server-flood-penalty 0.1)
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :nick "tester")
+        (funcall expect 10 "debug mode")))
+
+    (with-current-buffer "foonet"
+      (should (looking-at " and"))
+      (set-window-buffer nil (current-buffer))
+      (delete-other-windows)
+      (split-window-below 15)
+      (recenter 0)
+
+      (ert-info ("Moving into prompt in other window triggers scroll")
+        (with-selected-window (next-window)
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (goto-char (1- erc-insert-marker))
+          (execute-kbd-macro "\C-n")
+          ;; Ensure point is at prompt and aligned to bottom.
+          (should (erc-scenarios-common--at-win-end-p))))
+
+      (ert-info ("Module `move-to-prompt' still works")
+        ;; Prompt is somewhere in the middle of the window.
+        (should (erc-scenarios-common--above-win-end-p))
+        ;; Hitting a self-insert key triggers `move-to-prompt' as well
+        ;; as a scroll (to bottom).
+        (execute-kbd-macro "hi")
+        ;; Prompt and input appear on last line of window.
+        (should (erc-scenarios-common--at-win-end-p)))
+
+      (ert-info ("Command `recenter-top-bottom' disallowed at prompt")
+        ;; Hitting C-l does not recenter the window.
+        (erc-scenarios-common--recenter-top-bottom)
+        (should (erc-scenarios-common--at-win-end-p))
+        (erc-scenarios-common--recenter-top-bottom)
+        (should (erc-scenarios-common--at-win-end-p)))
+
+      (ert-info ("Command `beginning-of-buffer' allowed at prompt")
+        ;; Hitting C-< goes to beginning of buffer.
+        (call-interactively #'beginning-of-buffer)
+        (should (= 1 (point)))
+        (redisplay)
+        (should (zerop (count-screen-lines (window-start) (point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New message doesn't trigger scroll when away from prompt")
+        ;; Arriving insertions don't trigger a scroll when away from the
+        ;; prompt.  New output not seen.
+        (erc-cmd-MSG "NickServ help register")
+        (save-excursion (erc-d-t-search-for 10 "End of NickServ"))
+        (should (= 1 (point)))
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (funcall test)
+
+      (ert-info ("New message does trigger a scroll when at prompt")
+        ;; Recenter so prompt is above rather than at window's end.
+        (funcall expect 10 "If you are currently logged in")
+        (recenter 0)
+        ;; Prompt is somewhere in the middle of the window.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--above-win-end-p))
+        (erc-scenarios-common-say "/msg NickServ help identify")
+        ;; New arriving messages trigger a snap when inserted.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--at-win-end-p))
+        (funcall expect 10 "IDENTIFY lets you login")))))
+
 (cl-defun erc-scenarios-common--base-network-id-bouncer
     ((&key autop foo-id bar-id after
            &aux
diff --git a/test/lisp/erc/resources/scrolltobottom/help.eld b/test/lisp/erc/resources/scrolltobottom/help.eld
new file mode 100644
index 00000000000..ba44a0def39
--- /dev/null
+++ b/test/lisp/erc/resources/scrolltobottom/help.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.01 ":irc.foonet.org 003 tester :This server was created Mon, 21 Aug 2023 06:18:36 UTC")
+ (0.02 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=1000 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=1000 :are supported by this server")
+ (0.01 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)")
+ (0.01 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.01 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.01 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.01 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0.01 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0.01 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0.01 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode 10 "MODE tester +i")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.01 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0.02 ":irc.foonet.org 221 tester +i"))
+
+((privmsg-help-register 10 "PRIVMSG NickServ :help register")
+ (0.05 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 ***")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2REGISTER <password> [email]\2")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :REGISTER lets you register your current nickname as a user account. If the")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :server allows anonymous registration, you can omit the e-mail address.")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :If you are currently logged in with a TLS client certificate and wish to use")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :it instead of a password to log in, send * as the password.")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ HELP\2 ***"))
+
+((privmsg-help-identify 20 "PRIVMSG NickServ :help identify")
+ (0.06 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 ***")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2IDENTIFY <username> [password]\2")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :IDENTIFY lets you login to the given username using either password auth, or")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :certfp (your client certificate) if a password is not given.")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ HELP\2 ***"))
+
+((quit 10 "QUIT :\2ERC\2 ")
+ (0.07 ":tester!~u@26axz8nh8zaag.irc QUIT :Quit: \2ERC\2")
+ (0.02 "ERROR :Quit: \2ERC\2"))
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch --]
[-- Type: text/x-patch, Size: 56848 bytes --]

From 3ed5a748d76989ec2ed79311e3550da5c6d1aaa1 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 22 Jul 2023 00:46:44 -0700
Subject: [PATCH 1/1] [5.6] Consider all windows in erc-scrolltobottom-mode

* etc/ERC-NEWS: Add entry for option `erc-scrolltobottom-all'.  Also
mention explicit hook-depth intervals reserved by ERC.
* lisp/erc/erc-backend.el (erc--hide-prompt): Change hook depth on
`pre-command-hook' from 91 to 80.
* lisp/erc/erc-goodies.el (erc-input-line-position): Mention secondary
role when new option `erc-scroll-to-bottom-relaxed' is non-nil.
(erc-scrolltobottom-all): New option that decides whether module
`scrolltobottom' affects all windows or just the selected one, as it
always has.
(erc-scrolltobottom-relaxed): New option to leave the prompt
stationary instead of forcing it to the bottom of the window.
(erc-scrolltobottom-mode, erc-scrolltobottom-enable,
erc-scrolltobottom-disable): Use `erc--scrolltobottom-setup' instead
of `erc-add-scroll-to-bottom' for adding and removing local hooks,
instead of ranging over buffers when removing them.  Also add and
remove new hook members when `erc-scrolltobottom-all' is non-nil.
(erc--scrolltobottom-relaxed-commands,
erc--scrolltobottom-window-info,
erc--scrolltobottom-post-force-commands,
erc--scrolltobottom-relaxed-skip-commands): New internal variables.
(erc--scrolltobottom-on-pre-command
erc--scrolltobottom-on-post-command): New functions resembling
`erc-possibly-scroll-to-bottom' that try to avoid scrolling repeatedly
for no reason.
(erc--scrolltobottom-on-pre-command-relaxed,
erc--scrolltobottom-on-post-command-relaxed): New commands to help
implement `erc-scroll-to-bottom-relaxed'.
(erc--scrolltobottom-away-from-prompt): New function to scroll to
bottom on window configuration changes.
(erc--scrolltobottom-all): New function to scroll all windows
displaying the current buffer.
(erc-add-scroll-to-bottom): Deprecate this function because it is now
unused.
(erc--scrolltobottom-setup): New generic function to perform teardown
as well as setup, depending on module mode var.  Add scrolling on
`window-configuration-changed-hook' when `erc-scrolltobottom-all' is
non-nil and `erc-scrolltobottom-relaxed' is nil.
(erc--scrolltobottom-on-pre-insert): New generic function that
remembers the last `window-start' and maybe the current screen line
before inserting a message, in order to restore it afterward.
(erc--scrolltobottom-confirm): New function, a replacement for
`erc-scroll-to-bottom', that returns non-nil when it's actually
recentered the window.
(erc-move-to-prompt-setup): Add `erc-move-to-prompt' to
`pre-command-hook' at a depth of 70.
(erc-keep-place-mode, erc-keep-place-enable): Change hook depth from 0
to 85.
(erc--keep-place-indicator-setup): Add overlay arrow `after-string' in
non-graphical settings in case users have time stamps or other content
occupying the left margin.
(erc-keep-place-indicator-mode, erc-keep-place-indicator-enable):
Change hook depth from 90 to 85 so as not to conflict with t.
(erc--keep-place-indicator-on-global-module): Change hook depth from
90 to 85.
* test/lisp/erc/erc-scenarios-scrolltobottom.el: New file.
* test/lisp/erc/erc-scenarios-scrolltobottom-all.el: New file.
* test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el: New file.
* test/lisp/erc/resources/base/assoc/bumped/again.eld: Adjust timeouts.
* test/lisp/erc/resources/base/assoc/bumped/foisted.eld: Adjust timeouts.
* test/lisp/erc/resources/base/assoc/bumped/refoisted.eld: Adjust timeouts.
* test/lisp/erc/resources/base/netid/bouncer/barnet.eld: Adjust timeouts.
* test/lisp/erc/resources/base/netid/bouncer/foonet.eld: Adjust timeouts.
* test/lisp/erc/resources/erc-d/erc-d-t.el
(erc-d-t--wait-message-prefix, erc-d-t-wait-for, erc-d-t-ensure-for):
Add and use new variable to make `ert-info' message prefix adjustable.
* test/lisp/erc/resources/erc-scenarios-common.el
(erc-scenarios-common--term-size, erc-scenarios-common--run-in-term,
erc-scenarios-common-interactive-debug-term-p,
erc-scenarios-common-with-noninteractive-in-term): New test macro and
supporting helper function and variables to facilitate running
scenario-based tests in a subprocess.
(erc-scenarios-common--at-win-end-p,
erc-scenarios-common--above-win-end-p,
erc-scenarios-common--prompt-past-win-end-p,
erc-scenarios-common--recenter-top-bottom-around,
erc-scenarios-common--recenter-top-bottom,
erc-scenarios-scrolltobottom--normal): New test fixture and assertion
helper functions.
* test/lisp/erc/resources/scrolltobottom/help.eld: New file.
(Bug#64855)
---
 etc/ERC-NEWS                                  |  19 +-
 lisp/erc/erc-backend.el                       |   2 +-
 lisp/erc/erc-goodies.el                       | 267 ++++++++++++++++--
 .../erc/erc-scenarios-scrolltobottom-all.el   |  48 ++++
 .../erc-scenarios-scrolltobottom-relaxed.el   | 140 +++++++++
 test/lisp/erc/erc-scenarios-scrolltobottom.el |  44 +++
 .../erc/resources/base/assoc/bumped/again.eld |  10 +-
 .../resources/base/assoc/bumped/foisted.eld   |  10 +-
 .../resources/base/assoc/bumped/refoisted.eld |   8 +-
 .../resources/base/netid/bouncer/barnet.eld   |   2 +-
 .../resources/base/netid/bouncer/foonet.eld   |   2 +-
 test/lisp/erc/resources/erc-d/erc-d-t.el      |   7 +-
 .../erc/resources/erc-scenarios-common.el     | 196 +++++++++++++
 .../erc/resources/scrolltobottom/help.eld     |  46 +++
 14 files changed, 761 insertions(+), 40 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom-all.el
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
 create mode 100644 test/lisp/erc/erc-scenarios-scrolltobottom.el
 create mode 100644 test/lisp/erc/resources/scrolltobottom/help.eld

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 94452514e6d..72b768b02ec 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -178,6 +178,15 @@ been restored with a slightly revised role contingent on a few
 assumptions explained in its doc string.  For clarity, it has been
 renamed 'erc-ensure-target-buffer-on-privmsg'.
 
+** Module 'scrolltobottom' attempts to behave more sensibly.
+Enabling the experimental option 'erc-scrolltobottom-all' tells
+'scrolltobottom' to be more vigilant about recentering and to do so in
+all ERC windows.  The dependent option 'erc-scrolltobottom-relaxed',
+likewise experimental, makes ERC's prompt stationary wherever it
+resides instead of forcing it to the bottom of the window.  That is,
+new input appears above the prompt, scrolling existing messages upward
+to compensate.
+
 ** Subtle changes in two fundamental faces.
 Users of the default theme may notice that 'erc-action-face' and
 'erc-notice-face' now appear slightly less bold on systems supporting
@@ -232,9 +241,9 @@ property of the same name has been retained and now has a value of
 Built-in and third-party modules rely on certain hooks for adjusting
 incoming and outgoing messages upon insertion.  And some modules only
 want to do so after others have done their damage.  Traditionally,
-this required various hacks and finagling to achieve.  And while this
-release makes an effort to load modules in a more consistent order,
-that alone isn't enough to ensure similar predictability among
+this has required various hacks and finagling to achieve.  And while
+this release makes an effort to load modules in a more consistent
+order, that alone isn't enough to ensure similar predictability among
 essential members of important hooks.
 
 Luckily, ERC now leverages a feature introduced in Emacs 27, "hook
@@ -248,6 +257,10 @@ the first two, 'erc-button-add-buttons' and 'erc-fill', which have
 been swapped with respect to their previous places in recent ERC
 versions.
 
+The same depth interval is now also provisionally reserved for
+'erc-insert-pre-hook' and for the non-ERC hooks 'pre-command-hook' and
+'post-command-hook', but only locally, in ERC buffers.
+
 *** ERC now manages timestamp-related properties a bit differently.
 For starters, the 'cursor-sensor-functions' property no longer
 contains unique closures and thus no longer proves effective for
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index eb3ec39fedd..9e121ec1e92 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1089,7 +1089,7 @@ erc--hide-prompt
         (put-text-property erc-insert-marker (1- erc-input-marker)
                            'erc-prompt 'hidden)
         (erc--conceal-prompt))
-      (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 91 t))))
+      (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 80 t))))
 
 (defun erc-process-sentinel (cproc event)
   "Sentinel function for ERC process."
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index b37855cbecc..07a93b06f2c 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -44,42 +44,270 @@ erc-input-line-position
 This should be an integer specifying the line of the buffer on which
 the input line should stay.  A value of \"-1\" would keep the input
 line positioned on the last line in the buffer.  This is passed as an
-argument to `recenter'."
+argument to `recenter', unless `erc-scrolltobottom-relaxed' is
+non-nil, in which case, ERC interprets it as additional lines to
+scroll down by per message insertion (minus one for the prompt)."
   :group 'erc-display
   :type '(choice integer (const nil)))
 
+(defcustom erc-scrolltobottom-all nil
+  "Whether to scroll all windows or just the selected one.
+A value of nil preserves pre-5.6 behavior, in which scrolling
+only affects the selected window.  Users should consider its
+non-nil behavior experimental for the time being.  Note also that
+ERC expects this option to be configured before module
+initialization."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
+(defcustom erc-scrolltobottom-relaxed nil
+  "Whether to forgo forcing prompt to the bottom of the window.
+When non-nil, and point is at the prompt, ERC scrolls the window
+up when inserting messages, making the prompt appear stationary.
+Users who find this effect too \"stagnant\" can adjust the option
+`erc-input-line-position', which ERC borrows to express a scroll
+step offset when this option is non-nil.  Setting that value to
+zero lets the prompt drift toward the bottom by one line per
+message, which is generally slow enough not to distract while
+composing input.  Of course, this doesn't apply when receiving a
+large influx of messages, such as after typing \"/msg NickServ
+help\".  Note that ERC only considers this option when the
+experimental companion option `erc-scrolltobottom-all' is enabled
+and, only then, during module setup."
+  :group 'erc-display
+  :package-version '(ERC . "5.6") ; FIXME sync on release
+  :type 'boolean)
+
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
-  ((add-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (unless erc--updating-modules-p (erc-buffer-do #'erc-add-scroll-to-bottom)))
-  ((remove-hook 'erc-mode-hook #'erc-add-scroll-to-bottom)
-   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)
-   (dolist (buffer (erc-buffer-list))
-     (with-current-buffer buffer
-       (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))))
+  ((add-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
+   (unless erc--updating-modules-p (erc-buffer-do #'erc--scrolltobottom-setup))
+   (if erc-scrolltobottom-all
+       (progn
+         (add-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert 25)
+         (add-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+         (add-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+         (add-hook 'erc-send-completed-hook #'erc--scrolltobottom-all))
+     (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)))
+  ((remove-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
+   (erc-buffer-do #'erc--scrolltobottom-setup)
+   (if erc-scrolltobottom-all
+       (progn
+         (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+         (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
+         (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+         (remove-hook 'erc-pre-send-functions
+                      #'erc--scrolltobottom-on-pre-insert))
+     (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom))))
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
+(defvar-local erc--scrolltobottom-relaxed-commands '(end-of-buffer)
+  "Commands triggering a force scroll to prompt.
+Only applies with `erc-scrolltobottom-relaxed' when away from prompt.")
+
+(defvar-local erc--scrolltobottom-window-info nil
+  "Alist with windows as keys and lists of window-related info as values.
+Values are lists containing the last window start position and
+the last \"window line\" of point.  The \"window line\", which
+may be nil, is the number of lines between `window-start' and
+`window-point', inclusive.")
+
+(defvar erc--scrolltobottom-post-force-commands
+  '(beginning-of-buffer
+    electric-newline-and-maybe-indent
+    default-indent-new-line)
+  "Commands that force a scroll after execution at prompt.
+That is, ERC recalculates the window's start instead of blindly
+restoring it.")
+
+(defvar erc--scrolltobottom-relaxed-skip-commands
+  '(recenter-top-bottom scroll-down-command))
+
+(defun erc--scrolltobottom-on-pre-command ()
+  (when (and (eq (selected-window) (get-buffer-window))
+             (>= (point) erc-input-marker))
+    (setq erc--scrolltobottom-window-info
+          (list (list (selected-window)
+                      (window-start)
+                      (count-screen-lines (window-start) (point-max)))))))
+
+(defun erc--scrolltobottom-on-post-command ()
+  "Restore window start or scroll to prompt and recenter.
+When `erc--scrolltobottom-window-info' is non-nil and its first
+item is associated with the selected window, restore start of
+window so long as prompt hasn't moved.  Expect buffer to be
+unnarrowed."
+  (when (eq (selected-window) (get-buffer-window))
+    (if-let (((not (input-pending-p)))
+             (erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window)))
+             ((not (memq this-command
+                         erc--scrolltobottom-post-force-commands)))
+             ((= (nth 2 found)
+                 (count-screen-lines (window-start) (point-max)))))
+        (set-window-start (selected-window) (nth 1 found))
+      (erc--scrolltobottom-confirm))
+    (setq erc--scrolltobottom-window-info nil)))
+
+(defun erc--scrolltobottom-on-pre-command-relaxed ()
+  "Maybe scroll to bottom when away from prompt in an unnarrowed buffer.
+When `erc-scrolltobottom-relaxed' is active, only scroll when
+prompt is past window's end and the command is `end-of-buffer' or
+`self-insert-command' (assuming `move-to-prompt' is active).
+When at prompt and current command does not appear in
+`erc--scrolltobottom-relaxed-skip-commands', stash
+`erc--scrolltobottom-window-info' for the selected window."
+  (when (eq (selected-window) (get-buffer-window))
+    (when (and (not (input-pending-p))
+               (< (point) erc-input-marker)
+               (memq this-command erc--scrolltobottom-relaxed-commands)
+               (< (window-end nil t) erc-input-marker))
+      (save-excursion
+        (goto-char (point-max))
+        (recenter (or erc-input-line-position -1))))
+    (when (and (>= (point) erc-input-marker)
+               (not (memq this-command
+                          erc--scrolltobottom-relaxed-skip-commands)))
+      (setq erc--scrolltobottom-window-info
+            (list (list (selected-window)
+                        (window-start)
+                        (count-screen-lines (window-start) (point-max))))))))
+
+(defun erc--scrolltobottom-on-post-command-relaxed ()
+  "Set window start or scroll when data was captured on pre-command."
+  (when-let (((eq (selected-window) (get-buffer-window)))
+             (erc--scrolltobottom-window-info)
+             (found (car erc--scrolltobottom-window-info))
+             ((eq (car found) (selected-window))))
+    (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
+             (= (nth 2 found)
+                (count-screen-lines (window-start) (point-max))))
+        (set-window-start (selected-window) (nth 1 found))
+      (recenter (nth 2 found)))
+    (setq erc--scrolltobottom-window-info nil)))
+
+;; FIXME this is currently of little value because it doesn't restore
+;; the relative position of window point after changing dimensions.
+;; It would be preferable to instead stash the previous ratio of
+;; window line to body height and later recenter proportionally.  But
+;; that may not be worth the added bookkeeping.
+(defun erc--scrolltobottom-away-from-prompt ()
+  "Scroll to bottom unless at prompt."
+  (unless (or (input-pending-p)
+              (minibuffer-window-active-p (minibuffer-window))
+              (eq (old-selected-window) (minibuffer-window)))
+    (erc--scrolltobottom-confirm)))
+
+(defun erc--scrolltobottom-all (&rest _)
+  "Maybe put prompt on last line in all windows displaying current buffer.
+Expect to run when narrowing is in effect, such as on insertion
+or send-related hooks.  When recentering has not been performed,
+attempt to restore last `window-start', if known."
+  (dolist (window (get-buffer-window-list nil nil 'visible))
+    (with-selected-window window
+      (when-let
+          ((erc--scrolltobottom-window-info)
+           (found (assq window erc--scrolltobottom-window-info))
+           ((not (erc--scrolltobottom-confirm (nth 2 found)))))
+        (setf (window-start window) (cadr found)))))
+  ;; Necessary unless we're sure `erc--scrolltobottom-on-pre-insert'
+  ;; always runs between calls to this function.
+  (setq erc--scrolltobottom-window-info nil))
+
 (defun erc-add-scroll-to-bottom ()
   "A hook function for `erc-mode-hook' to recenter output at bottom of window.
 
 If you find that ERC hangs when using this function, try customizing
 the value of `erc-input-line-position'.
 
-This works whenever scrolling happens, so it's added to
-`window-scroll-functions' rather than `erc-insert-post-hook'."
+Note that the prior suggestion comes from a time when this
+function used `window-scroll-functions', which was replaced by
+`post-command-hook' in ERC 5.3."
+  (declare (obsolete erc--scrolltobottom-setup "30.1"))
   (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
 
+(cl-defgeneric erc--scrolltobottom-setup ()
+  "Arrange for scrolling to bottom on window configuration changes.
+Undo that arrangement when disabling `erc-scrolltobottom-mode'."
+  (if erc-scrolltobottom-mode
+      (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t)
+    (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))
+
+(cl-defmethod erc--scrolltobottom-setup (&context
+                                         (erc-scrolltobottom-all (eql t)))
+  (if erc-scrolltobottom-mode
+      (if erc-scrolltobottom-relaxed
+          (progn
+            (when (or (bound-and-true-p erc-move-to-prompt-mode)
+                      (memq 'move-to-prompt erc-modules))
+              (cl-pushnew 'self-insert-command
+                          erc--scrolltobottom-relaxed-commands))
+            (add-hook 'post-command-hook
+                      #'erc--scrolltobottom-on-post-command-relaxed 60 t)
+            (add-hook 'pre-command-hook ; preempt `move-to-prompt'
+                      #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
+        (add-hook 'window-configuration-change-hook
+                  #'erc--scrolltobottom-away-from-prompt nil t)
+        (add-hook 'pre-command-hook
+                  #'erc--scrolltobottom-on-pre-command 60 t)
+        (add-hook 'post-command-hook
+                  #'erc--scrolltobottom-on-post-command 60 t))
+    (remove-hook 'window-configuration-change-hook
+                 #'erc--scrolltobottom-away-from-prompt t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command t)
+    (remove-hook 'pre-command-hook
+                 #'erc--scrolltobottom-on-pre-command-relaxed t)
+    (remove-hook 'post-command-hook
+                 #'erc--scrolltobottom-on-post-command-relaxed t)
+    (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
+    (kill-local-variable 'erc--scrolltobottom-window-info)))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert (_input-or-string)
+  "Remember the `window-start' before inserting a message."
+  (setq erc--scrolltobottom-window-info
+        (mapcar (lambda (w)
+                  (list w
+                        (window-start w)
+                        (and-let*
+                            ((erc-scrolltobottom-relaxed)
+                             (c (count-screen-lines (window-start w)
+                                                    (point-max) nil w)))
+                          (if (= ?\n (char-before (point-max))) (1+ c) c))))
+                (get-buffer-window-list nil nil 'visible))))
+
+(cl-defmethod erc--scrolltobottom-on-pre-insert ((input erc-input))
+  "Remember the `window-start' before inserting a message."
+  (when (erc-input-insertp input)
+    (cl-call-next-method)))
+
+(defun erc--scrolltobottom-confirm (&optional scroll-to)
+  "Like `erc-scroll-to-bottom', but use `window-point'.
+Position current line (with `recenter') SCROLL-TO lines below
+window's top.  Return nil if point is not in prompt area or if
+prompt isn't ready."
+  (when erc-insert-marker
+    (let ((resize-mini-windows nil))
+      (save-restriction
+        (widen)
+        (when (>= (window-point) erc-input-marker)
+          (save-excursion
+            (goto-char (point-max))
+            (recenter (+ (or scroll-to 0) (or erc-input-line-position -1)))
+            t))))))
+
 (defun erc-scroll-to-bottom ()
   "Recenter WINDOW so that `point' is on the last line.
 
-This is added to `window-scroll-functions' by `erc-add-scroll-to-bottom'.
-
 You can control which line is recentered to by customizing the
 variable `erc-input-line-position'."
       ;; Temporarily bind resize-mini-windows to nil so that users who have it
@@ -135,13 +363,13 @@ erc-move-to-prompt
 
 (defun erc-move-to-prompt-setup ()
   "Initialize the move-to-prompt module."
-  (add-hook 'pre-command-hook #'erc-move-to-prompt nil t))
+  (add-hook 'pre-command-hook #'erc-move-to-prompt 70 t))
 
 ;;; Keep place in unvisited channels
 ;;;###autoload(autoload 'erc-keep-place-mode "erc-goodies" nil t)
 (define-erc-module keep-place nil
   "Leave point above un-viewed text in other channels."
-  ((add-hook 'erc-insert-pre-hook  #'erc-keep-place))
+  ((add-hook 'erc-insert-pre-hook  #'erc-keep-place 85))
   ((remove-hook 'erc-insert-pre-hook  #'erc-keep-place)))
 
 (defcustom erc-keep-place-indicator-style t
@@ -213,12 +441,15 @@ erc--keep-place-indicator-setup
   (add-hook 'window-configuration-change-hook
             #'erc--keep-place-indicator-on-window-configuration-change nil t)
   (when-let* (((memq erc-keep-place-indicator-style '(t arrow)))
+              (ov-property (if (zerop (fringe-columns 'left))
+                               'after-string
+                             'before-string))
               (display (if (zerop (fringe-columns 'left))
                            `((margin left-margin) ,overlay-arrow-string)
                          '(left-fringe right-triangle
                                        erc-keep-place-indicator-arrow)))
               (bef (propertize " " 'display display)))
-    (overlay-put erc--keep-place-indicator-overlay 'before-string bef))
+    (overlay-put erc--keep-place-indicator-overlay ov-property bef))
   (when (memq erc-keep-place-indicator-style '(t face))
     (overlay-put erc--keep-place-indicator-overlay 'face
                  'erc-keep-place-indicator-line)))
@@ -233,7 +464,7 @@ keep-place-indicator
          ((memq 'keep-place erc-modules)
           (erc-keep-place-mode +1))
          ;; Enable a local version of `keep-place-mode'.
-         (t (add-hook 'erc-insert-pre-hook  #'erc-keep-place 90 t)))
+         (t (add-hook 'erc-insert-pre-hook  #'erc-keep-place 85 t)))
    (if (pcase erc-keep-place-indicator-buffer-type
          ('target erc--target)
          ('server (not erc--target))
@@ -256,7 +487,7 @@ erc--keep-place-indicator-on-global-module
 global one."
   (if erc-keep-place-mode
       (remove-hook 'erc-insert-pre-hook  #'erc-keep-place t)
-    (add-hook 'erc-insert-pre-hook  #'erc-keep-place 90 t)))
+    (add-hook 'erc-insert-pre-hook  #'erc-keep-place 85 t)))
 
 (defun erc-keep-place-move (pos)
   "Move keep-place indicator to current line or POS.
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom-all.el b/test/lisp/erc/erc-scenarios-scrolltobottom-all.el
new file mode 100644
index 00000000000..33f232e64d9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom-all.el
@@ -0,0 +1,48 @@
+;;; erc-scenarios-scrolltobottom-all.el --- erc-scrolltobottom-all test -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--all ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (let ((erc-scrolltobottom-all t))
+
+    (erc-scenarios-scrolltobottom--normal
+     (lambda ()
+       (ert-info ("New insertion anchors prompt in other window")
+         (let ((w (next-window)))
+           ;; We're at prompt and aligned to bottom.
+           (should (>= (window-point w) erc-input-marker))
+           (erc-d-t-wait-for 10
+               (erc-scenarios-common--at-win-end-p w))
+           (erc-d-t-ensure-for 0.5
+               (erc-scenarios-common--at-win-end-p w))))))))
+
+;;; erc-scenarios-scrolltobottom-all.el ends here
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
new file mode 100644
index 00000000000..7d256bf711b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
@@ -0,0 +1,140 @@
+;;; erc-scenarios-scrolltobottom-relaxed.el --- erc-scrolltobottom-relaxed -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;; TODO assert behavior of prompt input spanning multiple lines, with
+;; and without line endings.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--relaxed ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (erc-scenarios-common-with-noninteractive-in-term
+      ((erc-scenarios-common-dialog "scrolltobottom")
+       (dumb-server (erc-d-run "localhost" t 'help))
+       (port (process-contact dumb-server :service))
+       (erc-modules `(scrolltobottom fill-wrap ,@erc-modules))
+       (erc-scrolltobottom-all t)
+       (erc-scrolltobottom-relaxed t)
+       (erc-server-flood-penalty 0.1)
+       (expect (erc-d-t-make-expecter))
+       lower upper)
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :nick "tester")
+        (funcall expect 10 "debug mode")))
+
+    (with-current-buffer "foonet"
+      (should (looking-at " and"))
+      (set-window-buffer nil (current-buffer))
+      (delete-other-windows)
+      (split-window-below 15)
+      (recenter 0)
+
+      (ert-info ("Moving into prompt does not trigger scroll")
+        (with-selected-window (next-window)
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (recenter 0)
+          (goto-char (1- erc-insert-marker))
+          (execute-kbd-macro "\C-n")
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (should (= (point) (point-max)))
+          (setq lower (count-screen-lines (window-start) (window-point)))))
+
+      (ert-info ("Module `move-to-prompt' still works")
+        ;; Prompt is somewhere in the middle of the window.
+        (should (erc-scenarios-common--above-win-end-p))
+        (should-not (= (point-max) (point)))
+        ;; Hitting a self-insert key triggers `move-to-prompt' but not
+        ;; a scroll (to bottom).
+        (execute-kbd-macro "hi")
+        ;; Prompt and input appear on same line.
+        (should (= (point-max) (point)))
+        (setq upper (count-screen-lines (window-start) (window-point)))
+        (should-not (= upper (window-body-height))))
+
+      (ert-info ("Command `recenter-top-bottom' allowed at prompt")
+        ;; Hitting C-l recenters the window.
+        (should (= upper (count-screen-lines (window-start) (window-point))))
+        (let ((lines (list upper)))
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (erc-scenarios-common--recenter-top-bottom)
+          (push (count-screen-lines (window-start) (window-point)) lines)
+          (setq lines (delete-dups lines))
+          (should (= (length lines) 4))))
+
+      (ert-info ("Command `beginning-of-buffer' allowed at prompt")
+        ;; Hitting C-< goes to beginning of buffer.
+        (execute-kbd-macro "\M-<")
+        (should (= 1 (point)))
+        (redisplay)
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New message doesn't trigger scroll when away from prompt")
+        ;; Arriving insertions don't trigger a scroll when away from the
+        ;; prompt.  New output not seen.
+        (erc-cmd-MSG "NickServ help register")
+        (save-excursion (erc-d-t-search-for 10 "End of NickServ"))
+        (should (= 1 (point)))
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New insertion keeps prompt stationary in other window")
+        (let ((w (next-window)))
+          ;; We're at prompt and completely stationary.
+          (should (>= (window-point w) erc-input-marker))
+          (erc-d-t-wait-for 10
+              (= lower (count-screen-lines (window-start w) (window-point w))))
+          (erc-d-t-ensure-for 0.5
+              (= lower (count-screen-lines (window-start w)
+                                           (window-point w))))))
+
+      (should (= 2 (length (window-list))))
+      (ert-info ("New message does not trigger a scroll when at prompt")
+        ;; Recenter so prompt is above rather than at window's end.
+        (funcall expect 10 "End of NickServ HELP")
+        (recenter 0)
+        (set-window-point nil (point-max))
+        (setq upper (count-screen-lines (window-start) (window-point)))
+        ;; Prompt is somewhere in the middle of the window.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--above-win-end-p))
+        (erc-scenarios-common-say "/msg NickServ help identify")
+        ;; New arriving messages don't move prompt.
+        (erc-d-t-ensure-for 1
+            (= upper (count-screen-lines (window-start) (window-point))))
+        (funcall expect 10 "IDENTIFY lets you login")))))
+
+;;; erc-scenarios-scrolltobottom-relaxed.el ends here
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom.el b/test/lisp/erc/erc-scenarios-scrolltobottom.el
new file mode 100644
index 00000000000..44e64204fb1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom.el
@@ -0,0 +1,44 @@
+;;; erc-scenarios-scrolltobottom.el --- erc-scrolltobottom-mode -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-goodies)
+
+(ert-deftest erc-scenarios-scrolltobottom--normal ()
+  :tags '(:expensive-test)
+  (when (version< emacs-version "29") (ert-skip "Times out"))
+
+  (should-not erc-scrolltobottom-all)
+
+  (erc-scenarios-scrolltobottom--normal
+   (lambda ()
+     (ert-info ("New insertion doesn't anchor prompt in other window")
+       (let ((w (next-window)))
+         ;; We're at prompt but not aligned to bottom.
+         (should (>= (window-point w) erc-input-marker))
+         (erc-d-t-wait-for 10
+             (not (erc-scenarios-common--at-win-end-p w))))))))
+
+;;; erc-scenarios-scrolltobottom.el ends here
diff --git a/test/lisp/erc/resources/base/assoc/bumped/again.eld b/test/lisp/erc/resources/base/assoc/bumped/again.eld
index ab3c7b06214..aef164b6237 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/again.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/again.eld
@@ -1,10 +1,10 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
  (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
 
-((nick 3 "NICK tester`")
+((nick 10 "NICK tester`")
  (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
  (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -21,10 +21,10 @@
  (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
  (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
 
-((mode-user 3.2 "MODE tester` +i")
+((mode-user 10 "MODE tester` +i")
  (0.0 ":irc.foonet.org 221 tester` +i")
  (0.0 ":irc.foonet.org NOTICE tester` :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+((privmsg 10 "PRIVMSG NickServ :IDENTIFY tester changeme")
  (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
  (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/resources/base/assoc/bumped/foisted.eld b/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
index 5c36e58d9d3..0f7aadac564 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/foisted.eld
@@ -1,6 +1,6 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
  (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -17,14 +17,14 @@
  (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
  (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
 
-((mode-user 1.2 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0.0 ":irc.foonet.org 221 tester +i")
  (0.0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((privmsg 17.21 "PRIVMSG bob :hi")
+((privmsg 10 "PRIVMSG bob :hi")
  (0.02 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :hola")
  (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :how r u?"))
 
-((quit 18.19 "QUIT :" quit)
+((quit 10 "QUIT :" quit)
  (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
 ((drop 1 DROP))
diff --git a/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld b/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
index 33e4168ac46..63366d3f576 100644
--- a/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
+++ b/test/lisp/erc/resources/base/assoc/bumped/refoisted.eld
@@ -1,6 +1,6 @@
 ;; -*- mode: lisp-data; -*-
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
  (0.1 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
  (0.0 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
  (0.0 ":irc.foonet.org 003 dummy :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
@@ -22,10 +22,10 @@
  (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG dummy :back?")
  )
 
-((mode-user 1.2 "MODE dummy +i")
+((mode-user 10 "MODE dummy +i")
  (0.0 ":irc.foonet.org 221 dummy +i")
  (0.0 ":irc.foonet.org NOTICE dummy :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
 
-((renick 42.6 "NICK tester")
+((renick 10 "NICK tester")
  (0.01 ":dummy!~u@rpaau95je67ci.irc NICK tester")
  (0.0 ":NickServ!NickServ@localhost NOTICE dummy :You're now logged in as tester"))
diff --git a/test/lisp/erc/resources/base/netid/bouncer/barnet.eld b/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
index 204d01fef77..596383c2699 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/barnet.eld
@@ -38,4 +38,4 @@
  (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
  (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonor in doing it."))
 
-((linger 1 LINGER))
+((linger 2 LINGER))
diff --git a/test/lisp/erc/resources/base/netid/bouncer/foonet.eld b/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
index 4445350ca0c..2e1a3ac27da 100644
--- a/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
+++ b/test/lisp/erc/resources/base/netid/bouncer/foonet.eld
@@ -43,4 +43,4 @@
  (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
  (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
 
-((linger 1 LINGER))
+((linger 2 LINGER))
diff --git a/test/lisp/erc/resources/erc-d/erc-d-t.el b/test/lisp/erc/resources/erc-d/erc-d-t.el
index 7b2adf4f07b..cf869fb3c70 100644
--- a/test/lisp/erc/resources/erc-d/erc-d-t.el
+++ b/test/lisp/erc/resources/erc-d/erc-d-t.el
@@ -83,6 +83,8 @@ erc-d-t-with-cleanup
                (ignore-errors (kill-buffer buf)))))
          (sleep-for erc-d-t-cleanup-sleep-secs)))))
 
+(defvar erc-d-t--wait-message-prefix "Awaiting: ")
+
 (defmacro erc-d-t-wait-for (max-secs msg &rest body)
   "Wait for BODY to become non-nil.
 Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
@@ -99,7 +101,7 @@ erc-d-t-wait-for
   (let ((inverted (make-symbol "inverted"))
         (time-out (make-symbol "time-out"))
         (result (make-symbol "result")))
-    `(ert-info ((concat "Awaiting: " ,msg))
+    `(ert-info ((concat erc-d-t--wait-message-prefix ,msg))
        (let ((,time-out (abs ,max-secs))
              (,inverted (< ,max-secs 0))
              (,result ',result))
@@ -120,7 +122,8 @@ erc-d-t-ensure-for
   (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
     (push msg body)
     (setq msg (prin1-to-string body)))
-  `(erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body))))
+  `(let ((erc-d-t--wait-message-prefix "Sustaining: "))
+     (erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body)))))
 
 (defun erc-d-t-search-for (timeout text &optional from on-success)
   "Wait for TEXT to appear in current buffer before TIMEOUT secs.
diff --git a/test/lisp/erc/resources/erc-scenarios-common.el b/test/lisp/erc/resources/erc-scenarios-common.el
index 972faa5c73f..b92acdd81e8 100644
--- a/test/lisp/erc/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/resources/erc-scenarios-common.el
@@ -183,6 +183,103 @@ erc-scenarios-common-with-cleanup
 
        ,@body)))
 
+(defvar erc-scenarios-common--term-size '(34 . 80))
+(declare-function term-char-mode "term" nil)
+(declare-function term-line-mode "term" nil)
+
+(defun erc-scenarios-common--run-in-term (&optional debug)
+  (require 'term)
+  (let* ((default-directory (getenv "EMACS_TEST_DIRECTORY"))
+         (emacs (expand-file-name invocation-name invocation-directory))
+         (process-environment (cons "HOME=/nonexistent"
+                                    (cons "ERC_TESTS_SUBPROCESS=1"
+                                          process-environment)))
+         (name (ert-test-name (ert-running-test)))
+         (temp-file (make-temp-file "erc-term-test-"))
+         (cmd `(let ((stats 1))
+                 (setq enable-dir-local-variables nil)
+                 (unwind-protect
+                     (setq stats (ert-run-tests-batch ',name))
+                   (unless ',debug
+                     (let ((buf (with-current-buffer (messages-buffer)
+                                  (buffer-string))))
+                       (with-temp-file ,temp-file
+                         (insert buf)))
+                     (kill-emacs (ert-stats-completed-unexpected stats))))))
+         ;; The `ert-test' object in Emacs 29 has a `file-name' field.
+         (file-name (symbol-file name 'ert--test))
+         (default-directory (expand-file-name (file-name-directory file-name)))
+         (package (if-let* ((found (getenv "ERC_PACKAGE_NAME"))
+                            ((string-prefix-p "erc-" found)))
+                      (intern found)
+                    'erc))
+         ;; This is for testing ERC's ELPA-package on older Emacsen we
+         ;; still support.  It won't run inside the emacs.git tree.
+         (setup (and (featurep 'compat)
+                     `(progn
+                        (require 'package)
+                        (let ((package-load-list '((compat t) (,package t))))
+                          (package-initialize)))))
+         ;; Make subprocess terminal bigger than controlling.
+         (buf (cl-letf (((symbol-function 'window-screen-lines)
+                         (lambda () (car erc-scenarios-common--term-size)))
+                        ((symbol-function 'window-max-chars-per-line)
+                         (lambda () (cdr erc-scenarios-common--term-size))))
+                (make-term (symbol-name name) emacs nil "-Q" "-nw"
+                           "-eval" (prin1-to-string setup)
+                           "-l" file-name "-eval" (format "%S" cmd))))
+         (proc (get-buffer-process buf))
+         (err (lambda ()
+                (with-temp-buffer
+                  (insert-file-contents temp-file)
+                  (message "Subprocess: %s" (buffer-string))
+                  (delete-file temp-file)))))
+    (set-window-buffer (selected-window) buf)
+    (delete-other-windows)
+    (with-current-buffer buf
+      (set-process-query-on-exit-flag proc nil)
+      (unless noninteractive (term-char-mode))
+      (with-timeout (30 (funcall err) (error "Timed out awaiting result"))
+        (while (process-live-p proc)
+          (accept-process-output proc 0.1)
+          (unless noninteractive
+            (redisplay))))
+      (while (accept-process-output proc))
+      (term-line-mode)
+      (goto-char (point-min))
+      ;; Otherwise gives process exited abnormally with exit-code >0
+      (unless (search-forward (format "Process %s finished" name) nil t)
+        (funcall err)
+        (ert-fail (when (search-forward "exited" nil t)
+                    (buffer-substring-no-properties (line-beginning-position)
+                                                    (line-end-position)))))
+      (delete-file temp-file)
+      (when noninteractive
+        (kill-buffer)))))
+
+(defvar erc-scenarios-common-interactive-debug-term-p nil
+  "When non-nil, run interactive ")
+
+(defmacro erc-scenarios-common-with-noninteractive-in-term (&rest body)
+  "Run BODY via `erc-scenarios-common-with-cleanup' in a `term' subprocess.
+Also do so when `erc-scenarios-common-interactive-debug-term-p'
+is non-nil.  When debugging, leave the `term-mode' buffer around
+for inspection and name it after the test, bounded by asterisks.
+When debugging, ensure the test always fails, as a reminder to
+disable `erc-scenarios-common-interactive-debug-term-p'.
+
+See Info node `(emacs) Term Mode' for the various commands."
+  (declare (indent 1))
+  `(if (and (or erc-scenarios-common-interactive-debug-term-p
+                noninteractive)
+            (not (getenv "ERC_TESTS_SUBPROCESS")))
+       (progn
+         (when (memq system-type '(windows-nt ms-dos))
+           (ert-skip "System must be UNIX"))
+         (erc-scenarios-common--run-in-term
+          erc-scenarios-common-interactive-debug-term-p))
+     (erc-scenarios-common-with-cleanup ,@body)))
+
 (defun erc-scenarios-common-assert-initial-buf-name (id port)
   ;; Assert no limbo period when explicit ID given
   (should (string= (if id
@@ -209,9 +306,108 @@ erc-scenarios-common-say
     (insert str)
     (erc-send-current-line)))
 
+(defun erc-scenarios-common--at-win-end-p (&optional window)
+  (= (window-body-height window)
+     (count-screen-lines (window-start window) (point-max) nil window)))
+
+(defun erc-scenarios-common--above-win-end-p (&optional window)
+  (> (window-body-height window)
+     (count-screen-lines (window-start window) (point-max))))
+
+(defun erc-scenarios-common--prompt-past-win-end-p (&optional window)
+  (< (window-body-height window)
+     (count-screen-lines (window-start window) (point-max))))
+
+(defun erc-scenarios-common--recenter-top-bottom-around (orig &rest args)
+  (let (this-command last-command) (apply orig args)))
+
+(defun erc-scenarios-common--recenter-top-bottom ()
+  (advice-add 'recenter-top-bottom
+              :around #'erc-scenarios-common--recenter-top-bottom-around)
+  (execute-kbd-macro "\C-l")
+  (advice-remove 'recenter-top-bottom
+                 #'erc-scenarios-common--recenter-top-bottom-around))
+
 
 ;;;; Fixtures
 
+(defun erc-scenarios-scrolltobottom--normal (test)
+  (erc-scenarios-common-with-noninteractive-in-term
+      ((erc-scenarios-common-dialog "scrolltobottom")
+       (dumb-server (erc-d-run "localhost" t 'help))
+       (port (process-contact dumb-server :service))
+       (erc-modules `(scrolltobottom fill-wrap ,@erc-modules))
+       (erc-server-flood-penalty 0.1)
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :nick "tester")
+        (funcall expect 10 "debug mode")))
+
+    (with-current-buffer "foonet"
+      (should (looking-at " and"))
+      (set-window-buffer nil (current-buffer))
+      (delete-other-windows)
+      (split-window-below 15)
+      (recenter 0)
+
+      (ert-info ("Moving into prompt in other window triggers scroll")
+        (with-selected-window (next-window)
+          (should-not (erc-scenarios-common--at-win-end-p))
+          (goto-char (1- erc-insert-marker))
+          (execute-kbd-macro "\C-n")
+          ;; Ensure point is at prompt and aligned to bottom.
+          (should (erc-scenarios-common--at-win-end-p))))
+
+      (ert-info ("Module `move-to-prompt' still works")
+        ;; Prompt is somewhere in the middle of the window.
+        (should (erc-scenarios-common--above-win-end-p))
+        ;; Hitting a self-insert key triggers `move-to-prompt' as well
+        ;; as a scroll (to bottom).
+        (execute-kbd-macro "hi")
+        ;; Prompt and input appear on last line of window.
+        (should (erc-scenarios-common--at-win-end-p)))
+
+      (ert-info ("Command `recenter-top-bottom' disallowed at prompt")
+        ;; Hitting C-l does not recenter the window.
+        (erc-scenarios-common--recenter-top-bottom)
+        (should (erc-scenarios-common--at-win-end-p))
+        (erc-scenarios-common--recenter-top-bottom)
+        (should (erc-scenarios-common--at-win-end-p)))
+
+      (ert-info ("Command `beginning-of-buffer' allowed at prompt")
+        ;; Hitting C-< goes to beginning of buffer.
+        (call-interactively #'beginning-of-buffer)
+        (should (= 1 (point)))
+        (redisplay)
+        (should (zerop (count-screen-lines (window-start) (point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (ert-info ("New message doesn't trigger scroll when away from prompt")
+        ;; Arriving insertions don't trigger a scroll when away from the
+        ;; prompt.  New output not seen.
+        (erc-cmd-MSG "NickServ help register")
+        (save-excursion (erc-d-t-search-for 10 "End of NickServ"))
+        (should (= 1 (point)))
+        (should (zerop (count-screen-lines (window-start) (window-point))))
+        (should (erc-scenarios-common--prompt-past-win-end-p)))
+
+      (funcall test)
+
+      (ert-info ("New message does trigger a scroll when at prompt")
+        ;; Recenter so prompt is above rather than at window's end.
+        (funcall expect 10 "If you are currently logged in")
+        (recenter 0)
+        ;; Prompt is somewhere in the middle of the window.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--above-win-end-p))
+        (erc-scenarios-common-say "/msg NickServ help identify")
+        ;; New arriving messages trigger a snap when inserted.
+        (erc-d-t-wait-for 10 (erc-scenarios-common--at-win-end-p))
+        (funcall expect 10 "IDENTIFY lets you login")))))
+
 (cl-defun erc-scenarios-common--base-network-id-bouncer
     ((&key autop foo-id bar-id after
            &aux
diff --git a/test/lisp/erc/resources/scrolltobottom/help.eld b/test/lisp/erc/resources/scrolltobottom/help.eld
new file mode 100644
index 00000000000..ba44a0def39
--- /dev/null
+++ b/test/lisp/erc/resources/scrolltobottom/help.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :tester")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.01 ":irc.foonet.org 003 tester :This server was created Mon, 21 Aug 2023 06:18:36 UTC")
+ (0.02 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=1000 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=1000 :are supported by this server")
+ (0.01 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)")
+ (0.01 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.01 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.01 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.01 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0.01 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0.01 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0.01 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode 10 "MODE tester +i")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.01 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0.02 ":irc.foonet.org 221 tester +i"))
+
+((privmsg-help-register 10 "PRIVMSG NickServ :help register")
+ (0.05 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 ***")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2REGISTER <password> [email]\2")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :REGISTER lets you register your current nickname as a user account. If the")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :server allows anonymous registration, you can omit the e-mail address.")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :If you are currently logged in with a TLS client certificate and wish to use")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :it instead of a password to log in, send * as the password.")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ HELP\2 ***"))
+
+((privmsg-help-identify 20 "PRIVMSG NickServ :help identify")
+ (0.06 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 ***")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2IDENTIFY <username> [password]\2")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :IDENTIFY lets you login to the given username using either password auth, or")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :certfp (your client certificate) if a password is not given.")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ HELP\2 ***"))
+
+((quit 10 "QUIT :\2ERC\2 ")
+ (0.07 ":tester!~u@26axz8nh8zaag.irc QUIT :Quit: \2ERC\2")
+ (0.02 "ERROR :Quit: \2ERC\2"))
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
       [not found] ` <87il948r8x.fsf@neverwas.me>
@ 2023-09-13 14:05   ` J.P.
       [not found]   ` <871qf2183j.fsf@neverwas.me>
  1 sibling, 0 replies; 11+ messages in thread
From: J.P. @ 2023-09-13 14:05 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 1424 bytes --]

"J.P." <jp@neverwas.me> writes:

> v5. Make new behavior hinge on new option, disabled by default. Assign
> some module functions explicit hook depths. Add news and tests.

I've added a version of this as

  commit 617ddb808999a71c925b68f5369d77aebfcd9254
  Consider all windows in erc-scrolltobottom-mode

But it comes with a few known problems.

The first can be observed during the rapid insertion of newly arriving
messages, like you get with history playback or large swaths of help
text. Basically, when point is away from the prompt and you issue a
command that changes `window-start' significantly (e.g., M-<), point
hardly moves at all, maybe half a screenful at most.

Another issue is only noticeable if you have code running that
suppresses message insertion after prompt submissions (see bug#49860's
`echo-message' for one example). Basically, if you've got
`erc-scrolltobottom-relaxed' enabled and you stare long enough, you'll
notice that the prompt drifts downward with every submitted round of
multi-line input.

Attached is a patch that attempts to address both of these issues, along
with a couple more to get at some unrelated odds and ends. I'm also sort
of thinking we ought to temporarily change the default of the new option
`erc-scrolltobottom-all' to t for a few weeks to help flush out any
other glaring bugs introduced by this feature. If anyone thinks that's a
bad idea, please say so. Thanks.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-doc-misc-erc.texi-Fix-display-buffer-example.patch --]
[-- Type: text/x-patch, Size: 769 bytes --]

From 88430ddf866031366ccda5439d007fef6b579d8b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 10 Sep 2023 22:55:16 -0700
Subject: [PATCH 1/4] ; * doc/misc/erc.texi: Fix display-buffer example.

---
 doc/misc/erc.texi | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 6d7785a9b54..1cbecfa891d 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -1821,7 +1821,7 @@ display-buffer
 
 (defun my-erc-disp-chan-p (_ action)
   (or (assq 'erc-autojoin-mode action)
-      (and (memq (cdr (assq 'erc-buffer-display alist)) 'JOIN)
+      (and (eq (cdr (assq 'erc-buffer-display action)) 'JOIN)
            (member (erc-default-target) '("#emacs" "#fsf")))))
 @end lisp
 
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-5.6-Simplify-erc-fill-module-docstring.patch --]
[-- Type: text/x-patch, Size: 1679 bytes --]

From 118e2e4aea66d0c54f0966d5e658086654e9807a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 13 Sep 2023 02:50:28 -0700
Subject: [PATCH 2/4] [5.6] Simplify erc--fill-module-docstring

* lisp/erc/erc-common.el (erc--fill-module-docstring): Don't run hooks
for major mode when filling.  Prefer `lisp-data-mode' to
`emacs-lisp-mode'.
---
 lisp/erc/erc-common.el | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 85971797c2f..67c2cf8535b 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -270,18 +270,20 @@ erc--prepare-custom-module-type
              " above."))))))
 
 (defun erc--fill-module-docstring (&rest strings)
+  "Concatenate STRINGS and fill as a doc string."
+  ;; Perhaps it's better to mimic `internal--format-docstring-line'
+  ;; and use basic filling instead of applying a major mode?
   (with-temp-buffer
-    (emacs-lisp-mode)
-    (insert "(defun foo ()\n"
-            (format "%S" (apply #'concat strings))
-            "\n(ignore))")
+    (delay-mode-hooks
+      (if (fboundp 'lisp-data-mode) (lisp-data-mode) (emacs-lisp-mode)))
+    (insert (format "%S" (apply #'concat strings)))
     (goto-char (point-min))
-    (forward-line 2)
-    (let ((emacs-lisp-docstring-fill-column 65)
+    (forward-line)
+    (let ((fill-column 65)
           (sentence-end-double-space t))
       (fill-paragraph))
     (goto-char (point-min))
-    (nth 3 (read (current-buffer)))))
+    (read (current-buffer))))
 
 (defmacro erc--find-feature (name alias)
   `(pcase (erc--find-group ',name ,(and alias (list 'quote alias)))
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-5.6-Skip-erc-ignored-user-p-when-erc-ignore-list-is-.patch --]
[-- Type: text/x-patch, Size: 4568 bytes --]

From 801306861920124a4ca68d925829f957d748aa37 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 11 Sep 2023 21:21:42 -0700
Subject: [PATCH 3/4] [5.6] Skip erc-ignored-user-p when erc-ignore-list is
 empty

* lisp/erc/erc-backend.el (erc-server-PRIVMSG): Don't bother with
`erc-ignored-user-p' and `erc-ignored-reply-p' when their associated
options are null.
* lisp/erc/erc-common.el (erc-get-server-user): Rearrange so
`erc-with-server-buffer' doesn't have to switch to the server buffer
because `erc-downcase' can run in channels as well.
* lisp/erc/erc.el (erc-ignored-user-p): Add comment.
* test/lisp/erc/erc-scenarios-base-buffer-display.el
(erc-scenarios-base-buffer-display--defwin-recbury-intbuf): Extend
timeout.
---
 lisp/erc/erc-backend.el                            | 7 ++++---
 lisp/erc/erc-common.el                             | 5 +++--
 lisp/erc/erc.el                                    | 2 ++
 test/lisp/erc/erc-scenarios-base-buffer-display.el | 2 +-
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 9e121ec1e92..596e504b39f 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -113,6 +113,8 @@ erc-ensure-target-buffer-on-privmsg
 (defvar erc-format-nick-function)
 (defvar erc-format-query-as-channel-p)
 (defvar erc-hide-prompt)
+(defvar erc-ignore-list)
+(defvar erc-ignore-reply-list)
 (defvar erc-input-marker)
 (defvar erc-insert-marker)
 (defvar erc-invitation)
@@ -1902,8 +1904,8 @@ erc--server-determine-join-display-context
         (cmd (erc-response.command parsed))
         (tgt (car (erc-response.command-args parsed)))
         (msg (erc-response.contents parsed)))
-    (if (or (erc-ignored-user-p sender-spec)
-            (erc-ignored-reply-p msg tgt proc))
+    (if (or (and erc-ignore-list (erc-ignored-user-p sender-spec))
+            (and erc-ignore-reply-list (erc-ignored-reply-p msg tgt proc)))
         (when erc-minibuffer-ignored
           (message "Ignored %s from %s to %s" cmd sender-spec tgt))
       (let* ((sndr (erc-parse-user sender-spec))
@@ -1918,7 +1920,6 @@ erc--server-determine-join-display-context
                                      ,@erc--display-context))
              s buffer
              fnick)
-        (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         ;; Even worth checking for empty target here? (invalid anyway)
         (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0))
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 67c2cf8535b..e680666156b 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -472,8 +472,9 @@ erc-get-channel-user
 (define-inline erc-get-server-user (nick)
   "Find NICK in the current server's `erc-server-users' hash table."
   (inline-letevals (nick)
-    (inline-quote (erc-with-server-buffer
-                    (gethash (erc-downcase ,nick) erc-server-users)))))
+    (inline-quote
+     (gethash (erc-downcase ,nick)
+              (erc-with-server-buffer erc-server-users)))))
 
 (defmacro erc--with-dependent-type-match (type &rest features)
   "Massage Custom :type TYPE with :match function that pre-loads FEATURES."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 07ba32d1cca..a65739cf861 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6832,6 +6832,8 @@ erc-delete-query
         (setq erc-default-recipients d2)
       (error "Current target is not a QUERY"))))
 
+;; FIXME spin this ignore stuff off into its own module, enabled by
+;; default until some major version change.
 (defun erc-ignored-user-p (spec)
   "Return non-nil if SPEC matches something in `erc-ignore-list'.
 
diff --git a/test/lisp/erc/erc-scenarios-base-buffer-display.el b/test/lisp/erc/erc-scenarios-base-buffer-display.el
index df292a8c113..ef544b4dcd0 100644
--- a/test/lisp/erc/erc-scenarios-base-buffer-display.el
+++ b/test/lisp/erc/erc-scenarios-base-buffer-display.el
@@ -109,7 +109,7 @@ erc-scenarios-base-buffer-display--defwin-recbury-intbuf
          (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
            (should (eq (window-buffer) (get-buffer "#spam")))
            ;; Option `buffer' replaces entire window (no split)
-           (erc-d-t-wait-for 5 (frame-root-window-p (selected-window)))))))))
+           (erc-d-t-wait-for 10 (frame-root-window-p (selected-window)))))))))
 
 (ert-deftest erc-scenarios-base-buffer-display--defwino-recbury-intbuf ()
   :tags '(:expensive-test)
-- 
2.41.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-5.6-Run-erc-scrolltobottom-on-pre-insert-uncondition.patch --]
[-- Type: text/x-patch, Size: 2535 bytes --]

From 88681f79c2a6fcfb9343c85e2d8ba58ed8090680 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 13 Sep 2023 05:42:24 -0700
Subject: [PATCH 4/4] [5.6] Run erc--scrolltobottom-on-pre-insert
 unconditionally

* lisp/erc/erc-goodies.el (erc--scrolltobottom-all): Pass `no-force'
argument to `set-window-start'.
(erc--scrolltobottom-on-pre-insert): Convert function from generic to
normal and drop `erc-input' method completely.  A non-nil `insertp'
slot means a message is marked for insertion in the read-only portion
of the buffer, above the prompt.  But conditioning the restoring of
window parameters on the latter is not enough: the window still needs
adjusting whenever input is typed, regardless of whether it's erased
or "inserted."  (Bug#64855)
---
 lisp/erc/erc-goodies.el | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 6353b813805..6eb015fdd64 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -223,7 +223,7 @@ erc--scrolltobottom-all
           ((erc--scrolltobottom-window-info)
            (found (assq window erc--scrolltobottom-window-info))
            ((not (erc--scrolltobottom-confirm (nth 2 found)))))
-        (setf (window-start window) (cadr found)))))
+        (set-window-start window (cadr found) 'no-force))))
   ;; Necessary unless we're sure `erc--scrolltobottom-on-pre-insert'
   ;; always runs between calls to this function.
   (setq erc--scrolltobottom-window-info nil))
@@ -280,7 +280,7 @@ erc--scrolltobottom-setup
     (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
     (kill-local-variable 'erc--scrolltobottom-window-info)))
 
-(cl-defmethod erc--scrolltobottom-on-pre-insert (_input-or-string)
+(defun erc--scrolltobottom-on-pre-insert (_)
   "Remember the `window-start' before inserting a message."
   (setq erc--scrolltobottom-window-info
         (mapcar (lambda (w)
@@ -293,11 +293,6 @@ erc--scrolltobottom-on-pre-insert
                           (if (= ?\n (char-before (point-max))) (1+ c) c))))
                 (get-buffer-window-list nil nil 'visible))))
 
-(cl-defmethod erc--scrolltobottom-on-pre-insert ((input erc-input))
-  "Remember the `window-start' before inserting a message."
-  (when (erc-input-insertp input)
-    (cl-call-next-method)))
-
 (defun erc--scrolltobottom-confirm (&optional scroll-to)
   "Like `erc-scroll-to-bottom', but use `window-point'.
 Position current line (with `recenter') SCROLL-TO lines below
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
       [not found]   ` <871qf2183j.fsf@neverwas.me>
@ 2023-09-19 13:38     ` J.P.
  0 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-09-19 13:38 UTC (permalink / raw)
  To: 64855-done; +Cc: emacs-erc

"J.P." <jp@neverwas.me> writes:

> But it comes with a few known problems.
>
> The first can be observed during the rapid insertion of newly arriving
> messages, like you get with history playback or large swaths of help
> text. Basically, when point is away from the prompt and you issue a
> command that changes `window-start' significantly (e.g., M-<), point
> hardly moves at all, maybe half a screenful at most.
>
> Another issue is only noticeable if you have code running that
> suppresses message insertion after prompt submissions (see bug#49860's
> `echo-message' for one example). Basically, if you've got
> `erc-scrolltobottom-relaxed' enabled and you stare long enough, you'll
> notice that the prompt drifts downward with every submitted round of
> multi-line input.
>
> Attached is a patch that attempts to address both of these issues, along
> with a couple more to get at some unrelated odds and ends. I'm also sort
> of thinking we ought to temporarily change the default of the new option
> `erc-scrolltobottom-all' to t for a few weeks to help flush out any
> other glaring bugs introduced by this feature. If anyone thinks that's a
> bad idea, please say so. Thanks.

I've installed this as

  69a1546 Run erc--scrolltobottom-on-pre-insert unconditionally

along with two others from that set. However, I left out

  [3/4] [5.6] Skip erc-ignored-user-p when erc-ignore-list is empty

because even a minor change like this to such a widely used feature
should probably be accompanied by tests and extra scrutiny, especially
since I myself am not really familiar with it. The optimization itself
seems simple enough, but one of those options is buffer-local and set
via the /IGNORE command, which complicates matters.

I'm thinking we may end up folding that patch into a larger initiative
aimed at moving everything related to message skipping and hiding to
another module. Indeed, there seems to be a good deal of overlap in
functionality among the lurker, hide-list, and /IGNORE features (and
also the fools stuff in erc-match). It'd be nice to get all that out of
erc.el because it can mostly be implemented using hooks, which should
help with maintenance and maybe save uninterested users some cycles.





^ permalink raw reply	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
  2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
                   ` (4 preceding siblings ...)
       [not found] ` <87il948r8x.fsf@neverwas.me>
@ 2023-10-11  2:53 ` J.P.
       [not found] ` <87o7h5euo8.fsf@neverwas.me>
  6 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-10-11  2:53 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 644 bytes --]

Due to various outstanding issues related to this feature, I've decided
to abandon almost all window twiddling in `post-command-hook' because
blacklisting and whitelisting various commands was becoming a sad game
of whack-a-mole. Rather, I think it's best if we restrict this option's
availability to Emacs 28+ and leverage `read-minibuffer-restore-windows'
to keep windows stable when executing extended commands. I also think it
makes sense to consolidate knobs and have `erc-scrolltobottom-all'
subsume the do-little `erc-scrolltobottom-relaxed' as a third value
state (boolean + the symbol `relaxed'). Please see attached changes.
Thanks.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Skip-restore-post-minibuf-read-in-erc-scrolltobo.patch --]
[-- Type: text/x-patch, Size: 18192 bytes --]

From f39edd3d8f223be7a8f9574f37cd0f16abdeb26a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 10 Oct 2023 18:14:53 -0700
Subject: [PATCH] [5.6] Skip restore post minibuf read in erc-scrolltobottom

* etc/ERC-NEWS: Remove mention of `erc-scrolltobottom-relaxed' in
entry for module `scrolltobottom'.
* lisp/erc/erc-goodies.el (erc-input-line-position): Fix mention of
subsumed option `erc-scrolltobottom-relaxed'.
(erc-scrolltobottom-all): Subsume option `erc-scrolltobottom-relaxed',
which depended on this option being non-nil anyway.
(erc-scrolltobottom-relaxed): Remove redundant option, which was new
in ERC 5.6.
(erc-scrolltobottom-enable, erc-scrolltobottom-mode): Warn if user
attempts to enable `erc-scrolltobottom-all' on Emacs 27, which is not
supported.
(erc--scrolltobottom-relaxed-commands,
erc--scrolltobottom-post-force-commands,
erc--scrolltobottom-relaxed-skip-commands): Remove unused variables.
(erc--scrolltobottom-on-pre-command,
erc--scrolltobottom-on-pre-command-relaxed,
erc--scrolltobottom-on-post-command-relaxed): Remove unused functions.
(erc--scrolltobottom-on-post-command): Remove conditional branch for
dealing with a non-nil `erc--scrolltobottom-window-info'.
(erc--scrolltobottom-setup): Convert from generic to normal function
and remove setup and teardown for unused hooks.  Set variable
`read-minibuffer-restore-windows' locally when option
`erc-scrolltobottom-all' is non-nil.
(erc--scrolltobottom-on-pre-insert): Replace reference to subsumed
option `erc-scrolltobottom-relaxed' with new value `relaxed' for
existing option `erc-scrolltobottom-all'.
* test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
(erc-scenarios-scrolltobottom--relaxed): Replace subsumed option
`erc-scrolltobottom-relaxed' with new value `relaxed' for
`erc-scrolltobottom-all'.  (Bug#64855)
---
 etc/ERC-NEWS                                  |  13 +-
 lisp/erc/erc-goodies.el                       | 201 +++++-------------
 .../erc-scenarios-scrolltobottom-relaxed.el   |   5 +-
 3 files changed, 60 insertions(+), 159 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index fadd97b65df..1fd516dcacf 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -179,13 +179,12 @@ assumptions explained in its doc string.  For clarity, it has been
 renamed 'erc-ensure-target-buffer-on-privmsg'.
 
 ** Module 'scrolltobottom' can attempt to be more aggressive.
-Enabling the experimental option 'erc-scrolltobottom-all' tells
-'scrolltobottom' to be more vigilant about staking down the input area
-and to do so in all ERC windows.  The dependent option
-'erc-scrolltobottom-relaxed', also experimental, makes ERC's prompt
-stationary wherever it happens to reside instead of forcing it to the
-bottom of a window.  That is, new input appears above the prompt,
-scrolling existing messages upward to compensate.
+Enabling the experimental option 'erc-scrolltobottom-all' makes ERC
+more vigilant about staking down the input area in all ERC windows.
+And the option's 'relaxed' variant makes ERC's prompt stationary
+wherever it happens to reside, instead of forcing it to the bottom of
+a window.  That is, new input appears above the prompt, scrolling
+existing messages upward to compensate.
 
 ** Subtle changes in two fundamental faces.
 Users of the default theme may notice that 'erc-action-face' and
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index b77176d8ac7..863429de202 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -44,45 +44,46 @@ erc-input-line-position
 This should be an integer specifying the line of the buffer on which
 the input line should stay.  A value of \"-1\" would keep the input
 line positioned on the last line in the buffer.  This is passed as an
-argument to `recenter', unless `erc-scrolltobottom-relaxed' is
-non-nil, in which case, ERC interprets it as additional lines to
-scroll down by per message insertion (minus one for the prompt)."
+argument to `recenter', unless `erc-scrolltobottom-all' is
+`relaxed', in which case, ERC interprets it as additional lines
+to scroll down by per message insertion (minus one for the
+prompt)."
   :group 'erc-display
   :type '(choice integer (const nil)))
 
 (defcustom erc-scrolltobottom-all nil
   "Whether to scroll all windows or just the selected one.
-A value of nil preserves pre-5.6 behavior, in which scrolling
-only affects the selected window.  Users should consider its
-non-nil behavior experimental for the time being.  Note also that
 ERC expects this option to be configured before module
-initialization."
+initialization.  A value of nil preserves pre-5.6 behavior, in
+which scrolling only affects the selected window.  A value of t
+means ERC attempts to recenter all visible windows whose point
+resides in the input area.
+
+A value of `relaxed' tells ERC to forgo forcing prompt to the
+bottom of the window.  When point is at the prompt, ERC scrolls
+the window up when inserting messages, making the prompt appear
+stationary.  Users who find this effect too \"stagnant\" can
+adjust the option `erc-input-line-position', borrowed here to
+express a scroll step offset.  Setting that value to zero lets
+the prompt drift toward the bottom by one line per message, which
+is generally slow enough not to distract while composing input.
+Of course, this doesn't apply when receiving a large influx of
+messages, such as after typing \"/msg NickServ help\".
+
+Note that users should consider this option's non-nil behavior to
+be experimental.  It currently only works with Emacs 28+."
   :group 'erc-display
   :package-version '(ERC . "5.6") ; FIXME sync on release
-  :type 'boolean)
-
-(defcustom erc-scrolltobottom-relaxed nil
-  "Whether to forgo forcing prompt to the bottom of the window.
-When non-nil, and point is at the prompt, ERC scrolls the window
-up when inserting messages, making the prompt appear stationary.
-Users who find this effect too \"stagnant\" can adjust the option
-`erc-input-line-position', which ERC borrows to express a scroll
-step offset when this option is non-nil.  Setting that value to
-zero lets the prompt drift toward the bottom by one line per
-message, which is generally slow enough not to distract while
-composing input.  Of course, this doesn't apply when receiving a
-large influx of messages, such as after typing \"/msg NickServ
-help\".  Note that ERC only considers this option when the
-experimental companion option `erc-scrolltobottom-all' is enabled
-and, only then, during module setup."
-  :group 'erc-display
-  :package-version '(ERC . "5.6") ; FIXME sync on release
-  :type 'boolean)
+  :type '(choice boolean (const relaxed)))
 
 ;;;###autoload(autoload 'erc-scrolltobottom-mode "erc-goodies" nil t)
 (define-erc-module scrolltobottom nil
   "This mode causes the prompt to stay at the end of the window."
   ((add-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
+   (when (and erc-scrolltobottom-all (< emacs-major-version 28))
+     (erc-button--display-error-notice-with-keys
+      "Option `erc-scrolltobottom-all' requires Emacs 28+. Disabling.")
+     (setopt erc-scrolltobottom-all nil))
    (unless erc--updating-modules-p (erc-buffer-do #'erc--scrolltobottom-setup))
    (if erc-scrolltobottom-all
        (progn
@@ -93,25 +94,17 @@ scrolltobottom
      (add-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)))
   ((remove-hook 'erc-mode-hook #'erc--scrolltobottom-setup)
    (erc-buffer-do #'erc--scrolltobottom-setup)
-   (if erc-scrolltobottom-all
-       (progn
-         (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
-         (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
-         (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
-         (remove-hook 'erc-pre-send-functions
-                      #'erc--scrolltobottom-on-pre-insert))
-     (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom))))
+   (remove-hook 'erc-insert-pre-hook #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-send-completed-hook #'erc--scrolltobottom-all)
+   (remove-hook 'erc-insert-done-hook #'erc--scrolltobottom-all)
+   (remove-hook 'erc-pre-send-functions #'erc--scrolltobottom-on-pre-insert)
+   (remove-hook 'erc-insert-done-hook #'erc-possibly-scroll-to-bottom)))
 
 (defun erc-possibly-scroll-to-bottom ()
   "Like `erc-add-scroll-to-bottom', but only if window is selected."
   (when (eq (selected-window) (get-buffer-window))
     (erc-scroll-to-bottom)))
 
-(defvar-local erc--scrolltobottom-relaxed-commands '(end-of-buffer)
-  "Commands triggering a forced scroll to prompt.
-Only applies with `erc-scrolltobottom-relaxed' while away from
-prompt.")
-
 (defvar-local erc--scrolltobottom-window-info nil
   "Alist with windows as keys and lists of window-related info as values.
 Values are lists containing the last window start position and
@@ -119,34 +112,12 @@ erc--scrolltobottom-window-info
 may be nil, is the number of lines between `window-start' and
 `window-point', inclusive.")
 
-(defvar erc--scrolltobottom-post-force-commands
-  '(beginning-of-buffer
-    electric-newline-and-maybe-indent
-    newline
-    default-indent-new-line)
-  "Commands that force a scroll after execution at prompt.
-That is, ERC recalculates the window's start instead of blindly
-restoring it.")
-
-;; Unfortunately, this doesn't work when `erc-scrolltobottom-relaxed'
-;; is enabled (scaling up still moves the prompt).
+;; FIXME treat `end-of-buffer' specially and always recenter -1.
+;; FIXME make this work when `erc-scrolltobottom-all' is set to
+;; `relaxed'.
 (defvar erc--scrolltobottom-post-ignore-commands '(text-scale-adjust)
   "Commands to skip instead of force-scroll on `post-command-hook'.")
 
-(defvar erc--scrolltobottom-relaxed-skip-commands
-  '(recenter-top-bottom scroll-down-command)
-  "Commands exempt from triggering a stash and restore of `window-start'.
-Only applies with `erc-scrolltobottom-relaxed' while in the input
-area.")
-
-(defun erc--scrolltobottom-on-pre-command ()
-  (when (and (eq (selected-window) (get-buffer-window))
-             (>= (point) erc-input-marker))
-    (setq erc--scrolltobottom-window-info
-          (list (list (selected-window)
-                      (window-start)
-                      (count-screen-lines (window-start) (point-max)))))))
-
 (defun erc--scrolltobottom-on-post-command ()
   "Restore window start or scroll to prompt and recenter.
 When `erc--scrolltobottom-window-info' is non-nil and its first
@@ -154,55 +125,8 @@ erc--scrolltobottom-on-post-command
 window so long as prompt hasn't moved.  Expect buffer to be
 unnarrowed."
   (when (eq (selected-window) (get-buffer-window))
-    (if-let (((not (input-pending-p)))
-             (erc--scrolltobottom-window-info)
-             (found (car erc--scrolltobottom-window-info))
-             ((eq (car found) (selected-window)))
-             ((not (memq this-command
-                         erc--scrolltobottom-post-force-commands)))
-             ((= (nth 2 found)
-                 (count-screen-lines (window-start) (point-max)))))
-        (set-window-start (selected-window) (nth 1 found))
-      (unless (memq this-command erc--scrolltobottom-post-ignore-commands)
-        (erc--scrolltobottom-confirm)))
-    (setq erc--scrolltobottom-window-info nil)))
-
-(defun erc--scrolltobottom-on-pre-command-relaxed ()
-  "Maybe scroll to bottom when away from prompt.
-When `erc-scrolltobottom-relaxed' is active, only scroll when
-prompt is past window's end and the command is `end-of-buffer' or
-`self-insert-command' (assuming `move-to-prompt' is active).
-When at prompt and current command does not appear in
-`erc--scrolltobottom-relaxed-skip-commands', stash
-`erc--scrolltobottom-window-info' for the selected window.
-Assume an unnarrowed buffer."
-  (when (eq (selected-window) (get-buffer-window))
-    (when (and (not (input-pending-p))
-               (< (point) erc-input-marker)
-               (memq this-command erc--scrolltobottom-relaxed-commands)
-               (< (window-end nil t) erc-input-marker))
-      (save-excursion
-        (goto-char (point-max))
-        (recenter (or erc-input-line-position -1))))
-    (when (and (>= (point) erc-input-marker)
-               (not (memq this-command
-                          erc--scrolltobottom-relaxed-skip-commands)))
-      (setq erc--scrolltobottom-window-info
-            (list (list (selected-window)
-                        (window-start)
-                        (count-screen-lines (window-start) (point-max))))))))
-
-(defun erc--scrolltobottom-on-post-command-relaxed ()
-  "Set window start or scroll when data was captured on pre-command."
-  (when-let (((eq (selected-window) (get-buffer-window)))
-             (erc--scrolltobottom-window-info)
-             (found (car erc--scrolltobottom-window-info))
-             ((eq (car found) (selected-window))))
-    (if (and (not (memq this-command erc--scrolltobottom-post-force-commands))
-             (= (nth 2 found)
-                (count-screen-lines (window-start) (point-max))))
-        (set-window-start (selected-window) (nth 1 found))
-      (recenter (nth 2 found)))
+    (unless (memq this-command erc--scrolltobottom-post-ignore-commands)
+      (erc--scrolltobottom-confirm))
     (setq erc--scrolltobottom-window-info nil)))
 
 ;; It may be desirable to also restore the relative line position of
@@ -246,54 +170,33 @@ erc-add-scroll-to-bottom
   (declare (obsolete erc--scrolltobottom-setup "30.1"))
   (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
 
-(cl-defgeneric erc--scrolltobottom-setup ()
-  "Arrange for scrolling to bottom on window configuration changes.
-Undo that arrangement when disabling `erc-scrolltobottom-mode'."
-  (if erc-scrolltobottom-mode
-      (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t)
-    (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)))
-
-(cl-defmethod erc--scrolltobottom-setup (&context
-                                         (erc-scrolltobottom-all (eql t)))
-  "Add and remove local hooks specific to `erc-scrolltobottom-all'."
+(defun erc--scrolltobottom-setup ()
+  "Perform buffer-local setup for module `scrolltobottom'."
   (if erc-scrolltobottom-mode
-      (if erc-scrolltobottom-relaxed
+      (if erc-scrolltobottom-all
           (progn
-            (when (or (bound-and-true-p erc-move-to-prompt-mode)
-                      (memq 'move-to-prompt erc-modules))
-              (cl-pushnew 'self-insert-command
-                          erc--scrolltobottom-relaxed-commands))
-            (add-hook 'post-command-hook
-                      #'erc--scrolltobottom-on-post-command-relaxed 60 t)
-            (add-hook 'pre-command-hook ; preempt `move-to-prompt'
-                      #'erc--scrolltobottom-on-pre-command-relaxed 60 t))
-        (add-hook 'window-configuration-change-hook
-                  #'erc--scrolltobottom-at-prompt-minibuffer-active nil t)
-        (add-hook 'pre-command-hook
-                  #'erc--scrolltobottom-on-pre-command 60 t)
-        (add-hook 'post-command-hook
-                  #'erc--scrolltobottom-on-post-command 60 t))
+            (setq-local read-minibuffer-restore-windows nil)
+            (unless (eq erc-scrolltobottom-all 'relaxed)
+              (add-hook 'window-configuration-change-hook
+                        #'erc--scrolltobottom-at-prompt-minibuffer-active 50 t)
+              (add-hook 'post-command-hook
+                        #'erc--scrolltobottom-on-post-command 50 t)))
+        (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
+    (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)
+    (remove-hook 'post-command-hook #'erc--scrolltobottom-on-post-command t)
     (remove-hook 'window-configuration-change-hook
                  #'erc--scrolltobottom-at-prompt-minibuffer-active t)
-    (remove-hook 'pre-command-hook
-                 #'erc--scrolltobottom-on-pre-command t)
-    (remove-hook 'post-command-hook
-                 #'erc--scrolltobottom-on-post-command t)
-    (remove-hook 'pre-command-hook
-                 #'erc--scrolltobottom-on-pre-command-relaxed t)
-    (remove-hook 'post-command-hook
-                 #'erc--scrolltobottom-on-post-command-relaxed t)
-    (kill-local-variable 'erc--scrolltobottom-relaxed-commands)
+    (kill-local-variable 'read-minibuffer-restore-windows)
     (kill-local-variable 'erc--scrolltobottom-window-info)))
 
 (defun erc--scrolltobottom-on-pre-insert (_)
-  "Remember the `window-start' before inserting a message."
+  "Remember `window-start' before inserting a message."
   (setq erc--scrolltobottom-window-info
         (mapcar (lambda (w)
                   (list w
                         (window-start w)
                         (and-let*
-                            ((erc-scrolltobottom-relaxed)
+                            (((eq erc-scrolltobottom-all 'relaxed))
                              (c (count-screen-lines (window-start w)
                                                     (point-max) nil w)))
                           (if (= ?\n (char-before (point-max))) (1+ c) c))))
diff --git a/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
index 7d256bf711b..68ea0b1b070 100644
--- a/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
+++ b/test/lisp/erc/erc-scenarios-scrolltobottom-relaxed.el
@@ -1,4 +1,4 @@
-;;; erc-scenarios-scrolltobottom-relaxed.el --- erc-scrolltobottom-relaxed -*- lexical-binding: t -*-
+;;; erc-scenarios-scrolltobottom-relaxed.el --- erc-scrolltobottom-all relaxed -*- lexical-binding: t -*-
 
 ;; Copyright (C) 2023 Free Software Foundation, Inc.
 
@@ -40,8 +40,7 @@ erc-scenarios-scrolltobottom--relaxed
        (dumb-server (erc-d-run "localhost" t 'help))
        (port (process-contact dumb-server :service))
        (erc-modules `(scrolltobottom fill-wrap ,@erc-modules))
-       (erc-scrolltobottom-all t)
-       (erc-scrolltobottom-relaxed t)
+       (erc-scrolltobottom-all 'relaxed)
        (erc-server-flood-penalty 0.1)
        (expect (erc-d-t-make-expecter))
        lower upper)
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
       [not found] ` <87o7h5euo8.fsf@neverwas.me>
@ 2023-10-14  0:29   ` J.P.
       [not found]   ` <871qdy9hbz.fsf@neverwas.me>
  1 sibling, 0 replies; 11+ messages in thread
From: J.P. @ 2023-10-14  0:29 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

I've added these changes as

  https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=328a9856

This bug has already been closed. Thanks.





^ permalink raw reply	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
       [not found]   ` <871qdy9hbz.fsf@neverwas.me>
@ 2023-10-25  2:15     ` J.P.
       [not found]     ` <87r0lja1lw.fsf@neverwas.me>
  1 sibling, 0 replies; 11+ messages in thread
From: J.P. @ 2023-10-25  2:15 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 879 bytes --]

"J.P." <jp@neverwas.me> writes:

> I've added these changes as
>
>   https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=328a9856
>
> This bug has already been closed. Thanks.

But wait, there's more! (There's always more.)

It seems I went a little overboard in reigning in the frequency of
scroll attempts. This has been walked back slightly in the attached
changes.

There's also a subtle quirk that's come to light involving `point-max'
and (recenter -1) where Emacs calculates point as having drifted off
screen. We could try tackling this with something kludgey, like
decrementing point temporarily during `recenter' attempts. But that
fails if the current input ends in a newline. I've instead decided to
address this by taking a page from other Emacs libraries and setting
`scroll-step' locally to 1 if a user hasn't customized
`scroll-conservatively'. See attached.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Be-slightly-more-aggressive-in-erc-scrolltobotto.patch --]
[-- Type: text/x-patch, Size: 3861 bytes --]

From 3e538317bade00780bb54a31aafd58689ee9eb42 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 24 Oct 2023 18:18:50 -0700
Subject: [PATCH] [5.6] Be slightly more aggressive in erc-scrolltobottom-all

* lisp/erc/erc-goodies.el (erc--scrolltobottom-on-post-command):
Redo obsolete doc string.
(erc--scrolltobottom-at-prompt-minibuffer-active,
erc--scrolltobottom-on-win-conf-change): Rename former to latter to
better reflect actual role.  Remove conditional guard so this always
runs.
(erc--scrolltobottom-setup): Set `scroll-step' locally when a user
hasn't customized `scroll-conservatively'.  Update hook member names.
(Bug#64855)
---
 lisp/erc/erc-goodies.el | 26 ++++++++++----------------
 1 file changed, 10 insertions(+), 16 deletions(-)

diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index 9d70c644429..e854d063cdd 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -119,28 +119,20 @@ erc--scrolltobottom-post-ignore-commands
   "Commands to skip instead of force-scroll on `post-command-hook'.")
 
 (defun erc--scrolltobottom-on-post-command ()
-  "Restore window start or scroll to prompt and recenter.
-When `erc--scrolltobottom-window-info' is non-nil and its first
-item is associated with the selected window, restore start of
-window so long as prompt hasn't moved.  Expect buffer to be
-unnarrowed."
+  "Scroll selected window unless `this-command' is exempted."
   (when (eq (selected-window) (get-buffer-window))
     (unless (memq this-command erc--scrolltobottom-post-ignore-commands)
-      (erc--scrolltobottom-confirm))
-    (setq erc--scrolltobottom-window-info nil)))
+      (setq erc--scrolltobottom-window-info nil)
+      (erc--scrolltobottom-confirm))))
 
 ;; It may be desirable to also restore the relative line position of
 ;; window point after changing dimensions.  Perhaps stashing the
 ;; previous ratio of window line to body height and later recentering
 ;; proportionally would achieve this.
-(defun erc--scrolltobottom-at-prompt-minibuffer-active ()
+(defun erc--scrolltobottom-on-win-conf-change ()
   "Scroll window to bottom when at prompt and using the minibuffer."
-  ;; This is redundant or ineffective in the selected window if at
-  ;; prompt or if only one window exists.
-  (unless (or (input-pending-p)
-              (and (minibuffer-window-active-p (minibuffer-window))
-                   (eq (old-selected-window) (minibuffer-window))))
-    (erc--scrolltobottom-confirm)))
+  (setq erc--scrolltobottom-window-info nil)
+  (erc--scrolltobottom-confirm))
 
 (defun erc--scrolltobottom-all (&rest _)
   "Maybe put prompt on last line in all windows displaying current buffer.
@@ -176,16 +168,18 @@ erc--scrolltobottom-setup
       (if erc-scrolltobottom-all
           (progn
             (setq-local read-minibuffer-restore-windows nil)
+            (when (zerop scroll-conservatively)
+              (setq-local scroll-step 1))
             (unless (eq erc-scrolltobottom-all 'relaxed)
               (add-hook 'window-configuration-change-hook
-                        #'erc--scrolltobottom-at-prompt-minibuffer-active 50 t)
+                        #'erc--scrolltobottom-on-win-conf-change 50 t)
               (add-hook 'post-command-hook
                         #'erc--scrolltobottom-on-post-command 50 t)))
         (add-hook 'post-command-hook #'erc-scroll-to-bottom nil t))
     (remove-hook 'post-command-hook #'erc-scroll-to-bottom t)
     (remove-hook 'post-command-hook #'erc--scrolltobottom-on-post-command t)
     (remove-hook 'window-configuration-change-hook
-                 #'erc--scrolltobottom-at-prompt-minibuffer-active t)
+                 #'erc--scrolltobottom-on-win-conf-change t)
     (kill-local-variable 'read-minibuffer-restore-windows)
     (kill-local-variable 'erc--scrolltobottom-window-info)))
 
-- 
2.41.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic
       [not found]     ` <87r0lja1lw.fsf@neverwas.me>
@ 2023-10-30 13:46       ` J.P.
  0 siblings, 0 replies; 11+ messages in thread
From: J.P. @ 2023-10-30 13:46 UTC (permalink / raw)
  To: 64855; +Cc: emacs-erc

"J.P." <jp@neverwas.me> writes:

> It seems I went a little overboard in reigning in the frequency of
> scroll attempts. This has been walked back slightly in the attached
> changes.
>
> There's also a subtle quirk that's come to light involving `point-max'
> and (recenter -1) where Emacs calculates point as having drifted off
> screen. We could try tackling this with something kludgey, like
> decrementing point temporarily during `recenter' attempts. But that
> fails if the current input ends in a newline. I've instead decided to
> address this by taking a page from other Emacs libraries and setting
> `scroll-step' locally to 1 if a user hasn't customized
> `scroll-conservatively'. See attached.

This followup was installed as

  e9205323e15 * Be slightly more aggressive with erc-scrolltobottom-all

The bug is already closed.





^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2023-10-30 13:46 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-07-25 13:40 bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic J.P.
2023-07-26 13:27 ` J.P.
2023-08-09 15:00 ` J.P.
2023-08-18 13:50 ` J.P.
2023-08-24 14:11 ` J.P.
     [not found] ` <87il948r8x.fsf@neverwas.me>
2023-09-13 14:05   ` J.P.
     [not found]   ` <871qf2183j.fsf@neverwas.me>
2023-09-19 13:38     ` J.P.
2023-10-11  2:53 ` J.P.
     [not found] ` <87o7h5euo8.fsf@neverwas.me>
2023-10-14  0:29   ` J.P.
     [not found]   ` <871qdy9hbz.fsf@neverwas.me>
2023-10-25  2:15     ` J.P.
     [not found]     ` <87r0lja1lw.fsf@neverwas.me>
2023-10-30 13:46       ` J.P.

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).