2024-07-27 How to make Emacs not scroll from the current position

Emacs – like most or all other editors – scrolls the buffer when the point moves out of the “visible portion” of text. This is of course exactly what you want – 99% of the time. Sometimes, however, I precisely set things up so that a particular set of lines is visible, and I really don’t want to accidentally mess it up (for example with isearch). I went through the Emacs manual to see if there is some mode or something to do that, and it seems that there’s not. (Though there are quite a few ways to customize scrolling, see the relevant chapters of the manual.) Being me, I decided to code it myself.

There are at least two approaches to this uncommon problem: the “correct” one and the “fast” one. (I use quote marks because the latter is not necessarily “incorrect”.) The fast one is to narrow the buffer to the portion visible on screen. It’s a hack, since you might want to use narrowing irrespective of my hypothetical feature, and this approach would make it impossible.

A more correct way would be to automatically move point to the top or bottom of the screen after any command which moves it further up or down. You could add some function to post-command-hook to move point every time it moves past the screen:

(defun move-point-to-screen ()
  "Move point back to the screen after it left it."
  (unless (pos-visible-in-window-p)
    (if (< (point) (window-start))
        (goto-char (window-start))
      (goto-char (1- (window-end))))))

(add-hook 'post-command-hook #'move-point-to-screen)

(Note: remember to (remove-hook 'post-command-hook #'move-point-to-screen) once you no longer want this to be active!)

Unfortunately, this is very far from ideal. For example, it doesn’t stop scrolling commands from, well, scrolling. Disabling scroll commands is fairly easy, though.

(defun inhibit-scroll-commands ()
  "If `this-command' is a scroll command, quit."
  (when (get this-command 'scroll-command)
    (setq this-command #'ignore)))

(add-hook 'pre-command-hook #'inhibit-scroll-commands)

Still, this won’t really work very reliably – for example, isearch-forward and friends will happily move the point as far as they want in the buffer. This is really a wild goose chase – there are potentially many commands which could scroll. After these botched attempts it occurred to me that there is yet another way to achieve what I want. I can define a minor mode to keep the window from scrolling, and when the mode is launched, it stores the current value of (window-start). Then, in post-command-hook, it can restore that value, and set the point to the first or last character visible – depending on two factors. If the point position becomes invisible, it needs to be changed; if that position is before (window-start), it needs to be reset to the window start, and if not, to the last character visible in the window. This is necessary since Emacs display routine moves point to the middle line if for any reason it falls outside the visible portion of the buffer.

(defvar-local fix-window-start-position-stored-position nil
  "`(window-start)' when Fix-Window-Start-Position-Mode was turned on.")

(defun fix-window-start-position-restore-position ()
  "Restore `window-start'."
  (when fix-window-start-position-mode
    (set-window-start
       (selected-window)
       fix-window-start-position-stored-position)
    (when (not (pos-visible-in-window-p))
      (if (< (point) fix-window-start-position-stored-position)
          (goto-char (window-start))
        (goto-char (1- (window-end)))))))

(define-minor-mode fix-window-start-position-mode
  "Toggle Fix-Window-Start-Position mode.
In this mode the window is not moved or scrolled."
  :lighter " Fixed"
  (if fix-window-start-position-mode
      (progn
        (setq fix-window-start-position-stored-position
              (window-start))
        (add-hook 'post-command-hook
                  #'fix-window-start-position-restore-position))
    (remove-hook 'post-command-hook
                 #'fix-window-start-position-restore-position)))

This is still not 100% ideal – for example, the screen flickers a bit during isearch – but seems to work just well enough to be satisfying.

CategoryEnglish, CategoryBlog, CategoryEmacs