summaryrefslogtreecommitdiffstats
path: root/emacs.d/lisp/smooth-scrolling.el
blob: d3321ef8e190c0779f833651f2ef0e3795ced67c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
;; smooth-scrolling.el
;; $Id$
;; Adam Spiers <emacs-ss@adamspiers.org>
;; 
;; Make emacs scroll smoothly, keeping the point away from the top and
;; bottom of the current buffer's window in order to keep lines of
;; context around the point visible as much as possible, whilst
;; avoiding sudden scroll jumps which are visually confusing.
;;
;; This is a nice alternative to all the native scroll-* custom
;; variables, which unfortunately cannot provide this functionality
;; perfectly.  `scroll-margin' comes closest, but has some bugs
;; (e.g. with handling of mouse clicks).  See
;;
;;   http://www.emacswiki.org/cgi-bin/wiki/SmoothScrolling
;;
;; for the gory details.
;;
;;;_* Installation
;;
;; Put somewhere on your `load-path' and include
;;
;;   (require 'smooth-scrolling)
;;
;; in your .emacs initialization file.
;;
;;;_* Notes
;;
;; This only affects the behaviour of the `next-line' and
;; `previous-line' functions, usually bound to the cursor keys and
;; C-n/C-p, and repeated isearches (`isearch-repeat').  Other methods
;; of moving the point will behave as normal according to the standard
;; custom variables.
;;
;; Prefix arguments to `next-line' and `previous-line' are honoured.
;; The minimum number of lines are scrolled in order to keep the
;; point outside the margin.
;;
;; There is one case where moving the point in this fashion may cause
;; a jump: if the point is placed inside one of the margins by another
;; method (e.g. left mouse click, or M-x goto-line) and then moved in
;; the normal way, the advice code will scroll the minimum number of
;; lines in order to keep the point outside the margin.  This jump may
;; cause some slight confusion at first, but hopefully it is justified
;; by the benefit of automatically ensuring `smooth-scroll-margin'
;; lines of context are visible around the point as often as possible.
;;
;;;_* TODO
;;
;; - Maybe add option to avoid scroll jumps when point is within
;;   margin.
;; 
;;;_* Acknowledgements
;; 
;; Thanks to Mark Hulme-Jones and consolers on #emacs for helping
;; debug issues with line-wrapping.
;; 
;;;_* License
;; 
;; Released under the GNU General Public License v2 or later, with
;; all rights assigned to the Free Software Foundation.
;;

;;;_* Code follows
;;;_ + disable `scroll-margin'
(setq scroll-margin 0)
;;;_ + defcustoms
(defcustom smooth-scroll-margin 10
  "Number of lines of visible margin at the top and bottom of a window.
If the point is within these margins, then scrolling will occur
smoothly for `previous-line' at the top of the window, and for
`next-line' at the bottom.

This is very similar in its goal to `scroll-margin'.  However, it
is implemented by activating `smooth-scroll-down' and
`smooth-scroll-up' advise via `defadvice' for `previous-line' and
`next-line' respectively.  As a result it avoids problems
afflicting `scroll-margin', such as a sudden jump and unexpected
highlighting of a region when the mouse is clicked in the margin.

Scrolling only occurs when the point is closer to the window
boundary it is heading for (top or bottom) than the middle of the
window.  This is to intelligently handle the case where the
margins cover the whole buffer (e.g. `smooth-scroll-margin' set
to 5 and `window-height' returning 10 or less).

See also `smooth-scroll-strict-margins'."
  :type  'integer
  :group 'windows)

(defcustom smooth-scroll-strict-margins t
  "If true, the advice code supporting `smooth-scroll-margin'
will use `count-screen-lines' to determine the number of
*visible* lines between the point and the window top/bottom,
rather than `count-lines' which obtains the number of actual
newlines.  This is because there might be extra newlines hidden
by a mode such as folding-mode, outline-mode, org-mode etc., or
fewer due to very long lines being displayed wrapped when
`truncate-lines' is nil.

However, using `count-screen-lines' can supposedly cause
performance issues in buffers with extremely long lines.  Setting
`cache-long-line-scans' may be able to address this;
alternatively you can set this variable to nil so that the advice
code uses `count-lines', and put up with the fact that sometimes
the point will be allowed to stray into the margin."
  :type  'boolean
  :group 'windows)
;;;_ + helper functions
(defun smooth-scroll-lines-from-window-top ()
  "Work out, using the function indicated by
`smooth-scroll-strict-margins', what the current screen line is,
relative to the top of the window.  Counting starts with 1 referring
to the top line in the window."
  (interactive)
  (cond ((= (window-start) (point))
         ;; In this case, count-screen-lines would return 0, so we override.
         1)
        (smooth-scroll-strict-margins
         (count-screen-lines (window-start) (point) 'count-final-newline))
        (t
         (count-lines (window-start) (point)))))

(defun smooth-scroll-lines-from-window-bottom ()
  "Work out, using the function indicated by
`smooth-scroll-strict-margins', how many screen lines there are
between the point and the bottom of the window.  Counting starts
with 1 referring to the bottom line in the window."
  (interactive)
  (if smooth-scroll-strict-margins
      (count-screen-lines (point) (window-end))
    (count-lines (point) (window-end))))
;;;_ + after advice

(defun smooth-scroll-down ()
  "Scroll down smoothly if cursor is within `smooth-scroll-margin'
lines of the top of the window."
  (and
   ;; Only scroll down if there is buffer above the start of the window.
   (> (line-number-at-pos (window-start)) 1)
   (let ((lines-from-window-top
          (smooth-scroll-lines-from-window-top)))
     (and
      ;; Only scroll down if we're within the top margin
      (<= lines-from-window-top smooth-scroll-margin)
      ;; Only scroll down if we're in the top half of the window
      (<= lines-from-window-top
          ;; N.B. `window-height' includes modeline, so if it returned 21,
          ;; that would mean exactly 10 lines in the top half and 10 in
          ;; the bottom.  22 (or any even number) means there's one in the
          ;; middle.  In both cases the following expression will
          ;; yield 10:
          (/ (1- (window-height)) 2))
      (save-excursion
        (scroll-down
              (1+ (- smooth-scroll-margin lines-from-window-top))))))))
                            
(defun smooth-scroll-up ()
  "Scroll up smoothly if cursor is within `smooth-scroll-margin'
lines of the bottom of the window."
  (and
   ;; Only scroll up if there is buffer below the end of the window.
   (< (window-end) (buffer-end 1))
   (let ((lines-from-window-bottom
          (smooth-scroll-lines-from-window-bottom)))
     (and
      ;; Only scroll up if we're within the bottom margin
      (<= lines-from-window-bottom smooth-scroll-margin)
      ;; Only scroll up if we're in the bottom half of the window.
      (<= lines-from-window-bottom
          ;; See above notes on `window-height'.
          (/ (1- (window-height)) 2))
      (save-excursion
        (scroll-up
         (1+ (- smooth-scroll-margin lines-from-window-bottom))))))))

(defadvice previous-line (after smooth-scroll-down
                            (&optional arg try-vscroll)
                            activate)
  (smooth-scroll-down))
(defadvice next-line (after smooth-scroll-up
                            (&optional arg try-vscroll)
                            activate)
  (smooth-scroll-up))

(defadvice isearch-repeat (after isearch-smooth-scroll
                                 (direction)
                                 activate)
  (if (eq direction 'forward)
      (smooth-scroll-up)
    (smooth-scroll-down)))

;;;_ + provide
(provide 'smooth-scrolling)

;;;_* Local emacs variables

;;Local variables:
;;allout-layout: (0 : -1 0)
;;mode: allout
;;End: