Subject: Re: bug#51590: follow-mode is broken with header-line and tab-line

From: Alan Mackenzie <[email-masked]>
Date: Fri, 5 Nov 2021 21:45:32 +0000

Hello, Eli and Juri.

On Thu, Nov 04, 2021 at 20:46:45 +0200, Eli Zaretskii wrote:
> > From: Juri Linkov <[email-masked]>
> > Cc: [email-masked]
> > Date: Thu, 04 Nov 2021 19:29:28 +0200

> > After trial and error, I arrived to the following patch that completely
> > fixes these problems.  However, I can't explain how it works.

> Alan, any comments?

Yes.  There's a bug in posn-at-x-y, in that either the function or the
documentation is wrong.

The doc says, rather unhelpfully, "By default, X and Y are relative to
[the] text area of the selected window.".  What does "[the] text area"
mean?  In other places, it means the same as "the body" of a window,
which is the area of text not including the mode line, header line or
tab-bar line.

In posn-at-x-y, "the text area" _INCLUDES_ THE HEADER LINE.  This can be
verified, in a GUI Emacs with

    M-: (posn-point (posn-at-x-y 0 18))
                                   ^^
This returns the buffer position of the earliest character in the
window.  The "18" means 18 pixels after the top of the header line.
Surely it ought to mean 18 pixels after the top of the first buffer line
in the window, i.e. the second window line.

This is a bug.

It has unfortunate consequences in (the current) follow-calc-win-end:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun follow-calc-win-end (&optional win)
  "Calculate the end position for window WIN.
Return (END-POS END-OF-BUFFER).

Actually, the position returned is the start of the line after
the last fully-visible line in WIN.  END-OF-BUFFER is t when EOB
is fully-visible in WIN.  If WIN is nil, the selected window is
used."
  (let* ((win (or win (selected-window)))
         (edges (window-inside-pixel-edges win))
         (ht (- (nth 3 edges) (nth 1 edges)))                 <======= 1
         (last-line-pos (posn-point (posn-at-x-y 0 (1- ht) win)))) <== 2
    (if (pos-visible-in-window-p last-line-pos win)           <======= 3
        (let ((end (window-end win t)))
          (list end (pos-visible-in-window-p (point-max) win)))
      (list last-line-pos nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

At 1, ht "the height" is the height of the "body" of the window, i.e.
all lines not including the header-line or mode-line.

At 2, posn-point intends to find the bottom pixel in the text part of
the window.  If there is a partial line there, posn-at-x-y will be in
the partial line, otherwise it will be in the last line of the window.
last-line-pos is the buffer position of whichever line that was.

At 3, pos-visible-in-window-p will return nil for the partial line or t
for the full line.  In the nil case, the function result is the partial
line, in the t case, it's (window-end ...).

This is all well and good.  But because of the bug in posn-at-x-y, when
there's a partial line there, at 2 last-line-pos is set one line too
far towards point-max.  Thus, at 3, pos-visible-in-window-p spuriously
returns t.  (window-end) now returns the line after the partial line,
which is wrong.

It would appear this behaviour of posn-at-x-y is also in Emacs 27.2.  It
would probably be catastrophic to fix posn-at-x-y, since so many
programs will have compensated for this bug.  So, we should probably fix
the doc of the function.

But we can fix follow-calc-win-end, by replacing the form at 2 with:

    (last-line-pos (posn-point (+ (posn-at-x-y 0 (1- ht) win)
                                  (window-header-line-height win))))

And I think we (probably me ;-) should go through the elisp manual and
doc strings replacing vague phrases like "the text area of the window"
with explicit descriptions involving "the header line", etc.

Juri, I'm sorry I haven't looked at the first hunk of your patch yet,
the one in follow-scroll-down.

-- 
Alan Mackenzie (Nuremberg, Germany).