Re: bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic

"J.P." <[email protected]>
Newsgroups gmane.emacs.erc.general
Message-ID <[email protected]>
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.
0000-v2-v3.diff (text/x-patch, 15.8 KB)
From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
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
0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch (text/x-patch, 15.6 KB)
From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.