Skip to content

Feature Request: support scroll-margin>0 #7

@psionic-k

Description

@psionic-k

Frankly can't stand the herky jerk of opening the mini-buffer with packages like vertico, ivy, transient etc. I had used another solution for this, but it relies on scroll margin, and scroll margin really plays badly with pixel precision scrolling. It rubber bands like an idiot.

To kill this menace once and for all, I'm developing a window point solution that will push the window points on certain entry hooks and restore them afterward if they were pushed. Here's where I got so far:

;; Eliminate stupid window movements caused by minibuffer or transient opening
;; and closing.
(defcustom pmx-no-herky-jerk-margin 12
  "Number of lines to protect from incidental scrolling.
A good value is the maximum height of your minibuffer, such as
configured by `ivy-height' and similar variables that configure packages
like `vertico' and `helm'."
  :type 'integer
  :group 'scrolling)

;; You would think we need multiple restore points.  However, there seems to be
;; a behavior where window points in non-selected windows are restored all the
;; time.  This was only apparent after moving them.
(defvar pmx--no-herky-jerk-restore nil
  "Where to restore selected buffer point.
List of BUFFER WINDOW SAFE-MARKER and RESTORE-MARKER.")

;; Counting line height would be more correct.  In general, lines are taller but
;; not shorter than the default, so this is a conservative approximation that
;; treats all lines as the default height.
(defun pmx--no-herky-jerk-enter (&rest _)
  "Adjust window points to prevent implicit scrolling."
  (unless (> (minibuffer-depth) 1)
    (let ((windows (window-at-side-list
                    (window-frame (selected-window))
                    'bottom))
          ;; height of default lines
          (frame-char-height (frame-char-height
                              (window-frame (selected-window)))))
      (while-let ((w (pop windows)))
        (with-current-buffer (window-buffer w)
          (let* ((current-line (line-number-at-pos (window-point w)))
                 (end-line (line-number-at-pos (window-end w)))
                 (window-pixel-height (window-pixel-height w))
                 (window-used-height (cdr (window-text-pixel-size
                                           w (window-start w) (window-end w))))
                 (margin-height (* frame-char-height pmx-no-herky-jerk-margin))
                 (unsafe-height (- window-used-height
                                   (- window-pixel-height margin-height)))
                 (unsafe-lines (+ 2 (ceiling (/ unsafe-height frame-char-height))))
                 (exceeded-lines (- unsafe-lines (- end-line current-line))))
            (when (> exceeded-lines 0)
              ;;  save value for restore
              (let* ((buffer (window-buffer w))
                     (restore-marker (let ((marker (make-marker)))
                                       ;; XXX this may error?
                                       (set-marker marker (window-point w)
                                                   buffer)))
                     (safe-point (progn
                                   (goto-char restore-marker)
                                   ;; XXX goes up too many lines when skipping
                                   ;; wrapped lines
                                   (ignore-error '(beginning-of-buffer
                                                   end-of-buffer)
                                     (previous-line exceeded-lines t))
                                   (end-of-line)
                                   (point))))
                (set-window-point w safe-point)
                (when (eq w (minibuffer-selected-window))
                  (let ((safe-marker (make-marker)))
                    (set-marker safe-marker safe-point buffer)
                    (setq pmx--no-herky-jerk-restore
                          (list buffer w safe-marker restore-marker))))
                (goto-char (marker-position restore-marker))))))))))

(defun pmx--no-herky-jerk-exit ()
  "Restore window points that were rescued from implicit scrolling."
  (when (and pmx--no-herky-jerk-restore
             (= (minibuffer-depth) 1)
             (null (transient-active-prefix)))
    (when-let* ((restore pmx--no-herky-jerk-restore)
                (buffer (pop restore))
                (w (pop restore))
                (safe-marker (pop restore))
                (restore-marker (pop restore)))
      (when (and (window-live-p w)
                 (eq (window-buffer w) buffer)
                 (= (window-point w) (marker-position safe-marker)))
        (goto-char restore-marker)
        (set-window-point w restore-marker))
      (set-marker restore-marker nil)
      (set-marker safe-marker nil)
      (setq pmx--no-herky-jerk-restore nil))))

(add-hook 'minibuffer-setup-hook #'pmx--no-herky-jerk-enter)
(add-hook 'minibuffer-exit-hook #'pmx--no-herky-jerk-exit)

;; Add the same for transient
(with-eval-after-load 'transient
  (advice-add 'transient-setup :before #'pmx--no-herky-jerk-enter)
  (add-hook 'transient-exit-hook #'pmx--no-herky-jerk-exit)
  (setopt transient-hide-during-minibuffer-read t))

edit: added a skip for the minibuffer, side detection, and text-area based logic. Discovered that non-selected windows already restore, which was unexpected. Only the selected window restores.

This alleviates the need for scroll margin. But scroll margin is kind of nice. I hate having to bring lines into context manually after the point goes beyond certain bounds. I figure if I can use window point manipulation to decapitate the beast of pure evil that is minibuffer-induced window scrolling, a smarter scroll margin that works with precision scrolling should be feasible?

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions