3

When using page-down (internally scroll-up-command), sometimes the window ends up in a state where end end-of-file is at the top of the screen.

Is there a way to page-down, clamping window position so it doesn't end up scrolling past the document end?

The emacs buffer displays something like this:

;;; some-file.el ends here


















Note that I'm not asking for a way to prevent the window moving past the document end entirely (although that could be nice), just a way to change the behavior of scroll-up-command

ideasman42
  • 8,375
  • 1
  • 28
  • 105

1 Answers1

1

This can be done using scrolling commands written in elisp.

;; Scrolling that re-centers, keeping the cursor vertically centered.
;; Also clamps window top when scrolling down,
;; so the text doesn't scroll off-screen.

(defun my-scroll-and-clamp--forward-line (n)
  "Wrap `forward-line', supporting Emacs built-in goal column.
Argument N the number of lines, passed to `forward-line'."
  (let ((next-column
         (or goal-column
             (and (memq last-command
                        '(next-line previous-line line-move))
                  (if (consp temporary-goal-column)
                      (car temporary-goal-column)
                    temporary-goal-column)))))
    (unless next-column
      (setq temporary-goal-column (current-column))
      (setq next-column temporary-goal-column))
    (forward-line n)
    (move-to-column next-column))
  ;; Needed so `temporary-goal-column' is respected in the future.
  (setq this-command 'line-move))

(defmacro my-scroll-and-clamp--with-evil-visual-mode-hack (&rest body)
  "Execute BODY with the point not restricted to line limits.

This is needed so the point is not forced to line bounds
even when in evil visual line mode."
  `(let ((mark-found nil))
     (when (and (fboundp 'evil-visual-state-p)
                (funcall 'evil-visual-state-p)
                (fboundp 'evil-visual-type)
                (eq (funcall 'evil-visual-type) 'line)
                (boundp 'evil-visual-point))
       (let ((mark (symbol-value 'evil-visual-point)))
         (when (markerp mark)
           (setq mark-found mark))))
     (unwind-protect
         (progn
           (when mark-found
             (goto-char (marker-position mark-found)))
           ,@body)
       (when mark-found
         (set-marker mark-found (point))))))


;;;###autoload
(defun my-scroll-and-clamp-up-command ()
  (interactive)
  (my-scroll-and-clamp--with-evil-visual-mode-hack
   (let ((height (window-height)))

     ;; Move point.
     (my-scroll-and-clamp--forward-line height)

     ;; Move window.
     (set-window-start
      (selected-window)
      (min
       (save-excursion ;; new point.
         (forward-line (- (/ height 2)))
         (point))
       (save-excursion ;; max point.
         (goto-char (point-max))
         (beginning-of-line)
         (forward-line (- (- height (+ 1 (* 2 scroll-margin)))))
         (point))))))
  (redisplay))

;;;###autoload
(defun my-scroll-and-clamp-down-command ()
  (interactive)
  (my-scroll-and-clamp--with-evil-visual-mode-hack
   (let* ((height (window-height)))

     ;; Move point.
     (my-scroll-and-clamp--forward-line (- height))
     (setq this-command 'line-move)

     ;; Move window.
     (set-window-start
      (selected-window)
      (save-excursion ;; new point.
        (forward-line (- (/ height 2)))
        (point)))))
  (redisplay))

Example shortcut bindings:

(global-set-key (kbd "<next>") 'my-scroll-and-clamp-up-command)
(global-set-key (kbd "<prior>") 'my-scroll-and-clamp-down-command)
ideasman42
  • 8,375
  • 1
  • 28
  • 105
  • This is not triggered when I use mouse wheel, can it be also bind to it? – alper Jul 27 '20 at 11:57
  • Yes you could bind to mouse wheel, you might want to scroll fewer lines in that case though. – ideasman42 Jul 27 '20 at 12:29
  • I binded as `S-down-mouse-1` but seems like it did not take any affect :-( Is it also possible to add N-line margin while scrolling on your code. smooth scrolling have this as `(setq smooth-scroll-margin 5)` @ideasman42 – alper Jul 27 '20 at 13:42
  • 1
    Not sure what your question is exactly, any issue binding is unlikely related to this function. This is a spesific scroll action, you can bind how you like - of course, for example passing `height` as a smaller/fixed value. Note that I wrote a package on melpa that does fast modal scrolling using the mouse, see: `scroll-on-drag` - https://gitlab.com/ideasman42/emacs-scroll-on-drag – ideasman42 Jul 28 '20 at 09:28
  • I am having following warning: `‘next-line’ is for interactive use only; use ‘forward-line’ instead.` should I change `next-line` with `forward-line`? – alper Aug 17 '21 at 14:18
  • Odd, I don't see that here, `next-line` isn't called directly, so it's only checked for comparison. Could you find what function causes this warning? – ideasman42 Aug 18 '21 at 01:16
  • I was having that warning for emacs 27 and 28. Like flycheck gives a warning message for the `next-line`. Please see https://emacs.stackexchange.com/a/59843/18414 // I come up with a function that is originally referencing your solution to ignore emtpy lines on the bottom of the buffer. // For `my-bottom ()` function. – alper Aug 18 '21 at 11:20
  • Using next-line for emacs.stackexchange.com/a/59843/18414 works, updated. – ideasman42 Aug 18 '21 at 13:28