From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: "J.P." Newsgroups: gmane.emacs.bugs Subject: bug#64855: 30.0.50; ERC 5.6: Make scrolltobottom less erratic Date: Wed, 09 Aug 2023 08:00:07 -0700 Message-ID: <87zg30l0qw.fsf__46593.5794565425$1691593319$gmane$org@neverwas.me> References: <87h6psyurb.fsf@neverwas.me> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="23661"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Cc: emacs-erc@gnu.org To: 64855@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Wed Aug 09 17:01:50 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1qTkh4-0005w4-5M for geb-bug-gnu-emacs@m.gmane-mx.org; Wed, 09 Aug 2023 17:01:50 +0200 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1qTkgd-0002Zo-EI; Wed, 09 Aug 2023 11:01:25 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qTkgJ-0002ZU-7c for bug-gnu-emacs@gnu.org; Wed, 09 Aug 2023 11:01:03 -0400 Original-Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1qTkgI-0006IO-HC for bug-gnu-emacs@gnu.org; Wed, 09 Aug 2023 11:01:02 -0400 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1qTkgI-0002Rx-4a for bug-gnu-emacs@gnu.org; Wed, 09 Aug 2023 11:01:02 -0400 X-Loop: help-debbugs@gnu.org Resent-From: "J.P." Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Wed, 09 Aug 2023 15:01:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 64855 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch Original-Received: via spool by 64855-submit@debbugs.gnu.org id=B64855.16915932189355 (code B ref 64855); Wed, 09 Aug 2023 15:01:02 +0000 Original-Received: (at 64855) by debbugs.gnu.org; 9 Aug 2023 15:00:18 +0000 Original-Received: from localhost ([127.0.0.1]:40332 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qTkfY-0002Qn-S0 for submit@debbugs.gnu.org; Wed, 09 Aug 2023 11:00:18 -0400 Original-Received: from mail-108-mta42.mxroute.com ([136.175.108.42]:42841) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qTkfT-0002QY-Jp for 64855@debbugs.gnu.org; Wed, 09 Aug 2023 11:00:15 -0400 Original-Received: from mail-111-mta2.mxroute.com ([136.175.111.2] filter006.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta42.mxroute.com (ZoneMTA) with ESMTPSA id 189dacffa9900023b6.001 for <64855@debbugs.gnu.org> (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384); Wed, 09 Aug 2023 15:00:10 +0000 X-Zone-Loop: 639962acff67ea37c5938d93fefc9e4a6b5b62a08517 X-Originating-IP: [136.175.111.2] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=neverwas.me ; s=x; h=Content-Type:MIME-Version:Message-ID:Date:References:In-Reply-To: Subject:Cc:To:From:Sender:Reply-To:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=KUI2a9YxndEMJYaasoNPj/eSiQS0hWbvk2aKRI9q+6k=; b=cGef5PmOwiuNhWL9SvVqL/H/I7 /wsXnUNtFEMxFtpCenkmhBJ9fblxedPb1tzSnBY7iqAD2R9cl9QFPnX14+j7aPhkKs6RMTIP5EMr2 r/QKW70FyHWN23/muJ8wkg5IIUziedbxXKpM2KW5SrfD6aRlBr+9H9SYLNdEf1504b+p79oi9mt9Z MlBU2lS+kjiBWuYaItBzsl8HZdft4yzwN28BUi91Wlo/LzRlB2HgVZqBMgmDW4mccWd7byX8Be9Gu CBslXYPtKr6FftnFicN2f+fup8QI9DlH1GkrCN4nSXmhdczIegr6aJjuYIK+xd6YarUJPIPe8c7gh 5qyRUxVg==; In-Reply-To: <87h6psyurb.fsf@neverwas.me> (J. P.'s message of "Tue, 25 Jul 2023 06:40:24 -0700") X-Authenticated-Id: masked@neverwas.me X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:267042 Archived-At: --=-=-= Content-Type: text/plain 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. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0000-v2-v3.diff >From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001 From: "F. Jason Park" 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 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-5.6-Consider-all-windows-in-erc-scrolltobottom-mode.patch >From 81fcee1a241f4efdf0c80fa9ee8a647cfbad794b Mon Sep 17 00:00:00 2001 From: "F. Jason Park" 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 --=-=-=--