1 ;;; scroll-restore.el --- restore original position after scrolling -*- lexical-binding:t -*-
3 ;; Copyright (C) 2007,2014 Free Software Foundation, Inc.
5 ;; Time-stamp: "2007-12-05 10:44:11 martin"
6 ;; Author: Martin Rudalics <rudalics@gmx.at>
10 ;; scroll-restore.el is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 3, or (at your option)
15 ;; scroll-restore.el is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;; Scroll Restore mode is a minor mode to restore the position of
26 ;; `point' in a sequence of scrolling commands whenever that position
27 ;; has gone off-screen and becomes visible again. The user option
28 ;; `scroll-restore-commands' specifies the set of commands that may
29 ;; constitute such a sequence.
31 ;; The following additional options are provided:
33 ;; - Recenter the window when restoring the original position, see
34 ;; `scroll-restore-recenter'.
36 ;; - Jump back to the original position before executing a command not
37 ;; in `scroll-restore-commands', see `scroll-restore-jump-back'. The
38 ;; resulting behavior is similar to that provided by a number of word
41 ;; - Change the appearance of the cursor in the selected window to
42 ;; indicate that the original position is off-screen, see
43 ;; `scroll-restore-handle-cursor'.
45 ;; - With `transient-mark-mode' non-nil Emacs highlights the region
46 ;; between `point' and `mark' when the mark is active. If you scroll
47 ;; `point' off-screen, Emacs relocates `point' _and_ the region.
48 ;; Customizing `scroll-restore-handle-region' permits to highlight the
49 ;; original region as long as the original position of `point' is
50 ;; off-screen, and restore the original region whenever the original
51 ;; position of `point' becomes visible again.
56 ;; - Scroll Restore mode does not handle `switch-frame' and
57 ;; `vertical-scroll-bar' events executed within the loops in
58 ;; `mouse-show-mark' and `scroll-bar-drag' (these don't call
59 ;; `post-command-hook' as needed by Scroll Restore mode).
61 ;; - Scroll Restore mode may disregard your customizations of
62 ;; `scroll-margin'. Handling `scroll-margin' on the Elisp level is
63 ;; tedious and might not work correctly.
65 ;; - Scroll Restore mode should handle `make-cursor-line-fully-visible'
66 ;; but there might be problems.
68 ;; - Scroll Restore mode can handle region and cursor only in the
69 ;; selected window. This makes a difference when you have set
70 ;; `highlight-nonselected-windows' to a non-nil value.
72 ;; - Scroll Restore mode has not been tested with emulation modes like
73 ;; `cua-mode' or `pc-selection-mode'. In particular, the former's
74 ;; handling of `cursor-type' and `cursor-color' might be affected by
75 ;; Scroll Restore mode."
77 ;; - Scroll Restore mode might interact badly with `follow-mode'. For
78 ;; example, the latter may deliberately select a window A when the
79 ;; original position of a window B appears in it. This won't restore
80 ;; the appearance of the cursor when Scroll Restore mode handles it.
85 (defgroup scroll-restore nil
86 "Restore original position after scrolling."
90 (defcustom scroll-restore-commands
91 '(handle-select-window handle-switch-frame
93 scroll-bar-toolkit-scroll mwheel-scroll
94 scroll-other-window scroll-other-window-down
95 scroll-bar-scroll-up scroll-bar-scroll-down scroll-bar-drag)
96 "Commands handled by Scroll Restore mode.
97 Scroll Restore mode will try to restore the original position of
98 `point' after executing a sequence of any of these commands."
99 :type '(repeat symbol)
100 :set #'(lambda (symbol value)
101 (when (boundp 'scroll-restore-commands)
102 (dolist (cmd scroll-restore-commands)
103 (put cmd 'scroll-restore nil)))
104 (set-default symbol value)
105 (dolist (cmd scroll-restore-commands)
106 (put cmd 'scroll-restore t)))
107 :group 'scroll-restore)
110 (defcustom scroll-restore-recenter nil
111 "Non-nil means scrolling back recenters the original position.
112 Setting this to a non-nil value can be useful to detect the original
113 position more easily and coherently when scrolling back."
115 :group 'scroll-restore)
118 (defcustom scroll-restore-jump-back nil
119 "Non-nil means jump back to original position after scrolling.
120 When this option is non-nil, Scroll Restore mode resets `point'
121 to the original position when scrolling has moved that position
122 off-screen and a command not in `scroll-restore-commands' shall
123 be executed. The resulting behavior is similar to that of some
124 word processors. You probably want to remove commands like
125 `scroll-up' and `scroll-down' from `scroll-restore-commands' when
126 activating this option.
128 Alternatively you may consider binding the command
129 `scroll-restore-jump-back' to a key of your choice."
131 :set #'(lambda (symbol value)
132 (set-default symbol value)
133 (when (and (boundp 'scroll-restore-mode) scroll-restore-mode)
134 (scroll-restore-restart)))
135 :group 'scroll-restore)
138 (defvar scroll-restore-buffer nil
139 "Buffer for `scroll-restore-cursor-type'.")
141 ;; Note: nil is a valid cursor-type.
142 (defvar scroll-restore-buffer-cursor-type 'invalid
143 "Original cursor-type of `scroll-restore-buffer'.")
145 (defvar scroll-restore-frame nil
146 "Frame for `scroll-restore-cursor-color'.")
148 (defvar scroll-restore-frame-cursor-color nil
149 "Original cursor-color of `scroll-restore-frame'.")
151 (defcustom scroll-restore-handle-cursor nil
152 "Non-nil means Scroll Restore mode may change appearance of cursor.
153 Scroll Restore mode can change the appearance of the cursor in
154 the selected window while the original position is off-screen.
155 Customize `scroll-restore-cursor-type' to change the type of the
156 cursor and `scroll-restore-cursor-color' to change its color."
158 (const :tag "Off" nil)
159 (const :tag "Cursor type" type)
160 (const :tag "Cursor color" color)
161 (const :tag "Type and color" t))
162 :set #'(lambda (symbol value)
163 (set-default symbol value)
164 (when (and (boundp 'scroll-restore-mode) scroll-restore-mode)
165 (scroll-restore-restart)))
166 :group 'scroll-restore)
168 (defcustom scroll-restore-cursor-type 'box
169 "Type of cursor when original position is off-screen.
170 Applied if and only if `scroll-restore-handle-cursor' is either
173 Be careful when another application uses that type. Otherwise,
174 you might get unexpected results when Scroll Restore mode resets
175 the cursor type to its \"original\" value after a sequence of
176 scrolling commands and the application has changed the cursor
179 To guard against unexpected results, Scroll Restore mode does not
180 reset the type of the cursor whenever its value does not equal
181 the value of scroll-restore-cursor-type."
183 (const :tag "No cursor" nil)
184 (const :tag "Filled box" box)
185 (const :tag "Hollow box" hollow)
186 (const :tag "Vertical bar" bar)
187 (const :tag "Horizontal bar" hbar))
188 :set #'(lambda (symbol value)
189 (set-default symbol value)
190 (when (and (boundp 'scroll-restore-mode) scroll-restore-mode)
191 (scroll-restore-restart)))
192 :group 'scroll-restore)
194 (defcustom scroll-restore-cursor-color "DarkCyan"
195 "Background color of cursor when original position is off-screen.
196 Applied if and only if `scroll-restore-handle-cursor' is either
199 Observe that when Emacs changes the color of the cursor, the
200 change applies to all windows on the associated frame.
202 Be careful when another application is allowed to change the
203 cursor-color. Otherwise, you might get unexpected results when
204 Scroll Restore mode resets the cursor color to its \"original\"
205 value and the application has changed the cursor color in
208 To guard against unexpected results Scroll Restore mode does not
209 reset the color of the cursor whenever its value does not equal
210 the value of scroll-restore-cursor-color."
212 :set #'(lambda (symbol value)
213 (set-default symbol value)
214 (when (and (boundp 'scroll-restore-mode) scroll-restore-mode)
215 (scroll-restore-restart)))
216 :group 'scroll-restore)
219 (defvar scroll-restore-region-overlay
220 (let ((overlay (make-overlay (point-min) (point-min))))
221 (overlay-put overlay 'face 'scroll-restore-region)
222 (delete-overlay overlay)
224 "Overlay used for highlighting the region.")
226 (defcustom scroll-restore-handle-region nil
227 "Non-nil means Scroll Restore mode handles the region.
228 This affects the behavior of Emacs in `transient-mark-mode' only.
229 In particular, Emacs will suppress highlighting the region as
230 long as the original position of `point' is off-screen. Rather,
231 Emacs will highlight the original region \(the region before
232 scrolling started\) in `scroll-restore-region' face. Scrolling
233 back to the original position will restore the region to its
236 Note that Scroll Restore mode does not deactivate the mark during
237 scrolling. Hence any operation on the region will not use the
238 original but the _actual_ value of `point'.
240 If you mark the region via `mouse-drag-region', setting this
241 option has no effect since Scroll Restore mode cannot track mouse
244 :set #'(lambda (symbol value)
245 (set-default symbol value)
246 (when (and (boundp 'scroll-restore-mode) scroll-restore-mode)
247 (scroll-restore-restart)))
248 :group 'scroll-restore)
250 (defface scroll-restore-region
251 '((t :inherit region))
252 "Face for Scroll Restore region when `scroll-restore-handle-region' is
254 :group 'scroll-restore)
256 ;; Note: We can't use `point-before-scroll' for our purposes because
257 ;; that variable is buffer-local. We need a variable that recorded
258 ;; `window-point' before a sequence of scroll operations. Also
259 ;; `point-before-scroll' is not handled by mwheel.el and some other
260 ;; commands that do implicit scrolling. hence, the original position is
261 ;; handled, among others, by the following alist.
262 (defvar scroll-restore-alist nil
263 "List of <window, buffer, point> quadruples.
264 `window' is the window affected, `buffer' its buffer. `pos' is
265 the original position of `point' in that window. `off' non-nil
266 means `pos' was off-screen \(didn't appear in `window'\).")
268 (defun scroll-restore-pre-command ()
269 "Scroll Restore's pre-command function."
270 (let ((overlay-buffer (overlay-buffer scroll-restore-region-overlay)))
271 ;; Handle region overlay.
273 ;; Remove `transient-mark-mode' binding in any case.
274 (with-current-buffer overlay-buffer
275 (kill-local-variable 'transient-mark-mode))
276 (delete-overlay scroll-restore-region-overlay)))
277 ;; Handle cursor-type.
278 (when (and scroll-restore-buffer
279 (not (eq scroll-restore-buffer-cursor-type 'invalid))
280 (with-current-buffer scroll-restore-buffer
281 (eq cursor-type scroll-restore-cursor-type)))
282 (with-current-buffer scroll-restore-buffer
283 (setq cursor-type scroll-restore-buffer-cursor-type)
284 (setq scroll-restore-buffer-cursor-type 'invalid)))
285 ;; Handle cursor-color.
286 (when (and scroll-restore-frame scroll-restore-frame-cursor-color
287 (eq (frame-parameter scroll-restore-frame 'cursor-color)
288 scroll-restore-cursor-color))
289 (let ((frame (selected-frame)))
290 (select-frame scroll-restore-frame)
291 (set-cursor-color scroll-restore-frame-cursor-color)
292 (setq scroll-restore-frame-cursor-color nil)
293 (select-frame frame)))
295 (when (and scroll-restore-jump-back
296 (not (get this-command 'scroll-restore)))
297 (let ((entry (assq (selected-window) scroll-restore-alist)))
299 (let ((window (car entry))
300 ;; (buffer (nth 1 entry))
302 (set-window-point window pos)
303 ;; We are on-screen now.
304 (setcdr (nthcdr 2 entry) (list nil))))))
306 (unless (or scroll-restore-jump-back scroll-restore-handle-region
307 scroll-restore-handle-cursor)
308 ;; Should be never reached.
309 (remove-hook 'pre-command-hook 'scroll-restore-pre-command)))
311 (defun scroll-restore-remove (&optional all)
312 "Remove stale entries from `scroll-restore-alist'.
313 Optional argument ALL non-nil means remove them all."
314 (dolist (entry scroll-restore-alist)
315 (let ((window (car entry))
316 (buffer (nth 1 entry))
318 (when (or all (not (window-live-p window))
319 (not (eq (window-buffer window) buffer))
320 (not (markerp pos)) (not (marker-position pos)))
322 (set-marker pos nil))
323 (setq scroll-restore-alist
324 (assq-delete-all window scroll-restore-alist))))))
326 (defun scroll-restore-add ()
327 "Add new entries to `scroll-restore-alist'."
330 (unless (assq window scroll-restore-alist)
331 (let ((buffer (window-buffer window)))
332 (setq scroll-restore-alist
336 (with-current-buffer buffer
337 (copy-marker (window-point window)))
339 scroll-restore-alist)))))
342 (defun scroll-restore-update (how window buffer pos)
343 "Update various things in `scroll-restore-post-command'.
344 HOW must be either on-off, on-on, off-off, off-on, or t. WINDOW
345 and BUFFER are affected window and buffer. POS is the original
347 (when (eq window (selected-window))
348 (with-current-buffer buffer
350 (when scroll-restore-handle-region
351 (if (and transient-mark-mode mark-active
352 (not deactivate-mark)
353 (memq how '(on-off off-off)))
355 (move-overlay scroll-restore-region-overlay
356 (min pos (mark)) (max pos (mark)) buffer)
357 (overlay-put scroll-restore-region-overlay 'window window)
358 ;; Temporarily disable `transient-mark-mode' in this buffer.
359 (set (make-local-variable 'transient-mark-mode) nil))
360 (delete-overlay scroll-restore-region-overlay)))
362 (when (and scroll-restore-handle-cursor
363 (memq how '(on-off off-off))
364 ;; Change cursor iff there was a visible cursor.
366 (when (memq scroll-restore-handle-cursor '(type t))
367 (setq scroll-restore-buffer buffer)
368 (setq scroll-restore-buffer-cursor-type cursor-type)
369 (setq cursor-type scroll-restore-cursor-type))
370 (when (memq scroll-restore-handle-cursor '(color t))
371 (setq scroll-restore-frame (window-frame window))
372 (setq scroll-restore-frame-cursor-color
373 (frame-parameter scroll-restore-frame 'cursor-color))
374 (let ((frame (selected-frame)))
375 (select-frame scroll-restore-frame)
376 (set-cursor-color scroll-restore-cursor-color)
377 (select-frame frame)))))))
379 (defun scroll-restore-post-command ()
380 "Scroll Restore mode post-command function."
381 (scroll-restore-remove)
383 (dolist (entry scroll-restore-alist)
384 (let ((window (car entry))
385 (buffer (nth 1 entry))
388 (if (get this-command 'scroll-restore)
389 ;; A scroll restore command.
391 ;; `pos' was off-screen.
392 (if (pos-visible-in-window-p (marker-position pos) window)
393 ;; `pos' is on-screen now.
395 ;; Move cursor to original position.
396 (set-window-point window pos)
397 ;; Recenter if desired.
398 (when (and scroll-restore-recenter
399 (eq window (selected-window)))
400 (setq recenter (/ (window-height window) 2)))
401 ;; Record on-screen status.
402 (setcdr (nthcdr 2 entry) (list nil))
403 (scroll-restore-update 'off-on window buffer pos))
404 ;; `pos' is still off-screen
405 (scroll-restore-update 'off-off window buffer pos))
406 ;; `pos' was on-screen.
407 (if (pos-visible-in-window-p pos window)
408 ;; `pos' is still on-screen.
410 ;; Occasionally Emacs deliberately changes
411 ;; `window-point' during scrolling even when
412 ;; it's visible. Maybe this is due to
413 ;; `make-cursor-line-fully-visible' maybe due to
414 ;; `scroll-margin' maybe due to something else.
415 ;; We override that behavior here.
416 (unless (= (window-point) pos)
417 (set-window-point window pos))
418 (scroll-restore-update 'on-on window buffer pos))
419 ;; `pos' moved off-screen.
420 ;; Record off-screen state.
421 (setcdr (nthcdr 2 entry) (list t))
422 (scroll-restore-update 'on-off window buffer pos)))
423 ;; Not a scroll-restore command.
424 (let ((window-point (window-point window)))
425 (when (and (eq window (selected-window))
426 (or (/= window-point pos) off))
427 ;; Record position and on-screen status.
430 (list (move-marker pos (window-point window)) nil)))
431 (scroll-restore-update t window buffer pos)))))
433 (when recenter (recenter recenter))))
435 (defun scroll-restore-jump-back ()
436 "Jump back to original position.
437 The orginal position is the value of `window-point' in the
438 selected window before you started scrolling.
440 This command does not push the mark."
442 (let ((entry (assq (selected-window) scroll-restore-alist)))
444 (goto-char (nth 2 entry))
445 (error "No jump-back position available"))))
447 (define-minor-mode scroll-restore-mode
448 "Toggle Scroll Restore mode.
449 With arg, turn Scroll Restore mode on if arg is positive, off
452 In Scroll Restore mode Emacs attempts to restore the original
453 position that existed before executing a sequence of scrolling
454 commands whenever that position becomes visible again. The
455 option `scroll-restore-commands' permits to specify the set of
456 commands that may constitute such a sequence. In addition you
459 - recenter the window when you scroll back to the original
460 position, see the option `scroll-restore-recenter',
462 - aggressively jump back to the original position before
463 executing a command not in `scroll-restore-commands', see
464 `scroll-restore-jump-back',
466 - change the appearance of the cursor in the selected window
467 while the original position is off-screen, see the option
468 `scroll-restore-handle-cursor',
470 - change the appearance of the region in the selected window
471 while the original position is off-screen, see the option
472 `scroll-restore-handle-region'."
474 :group 'scroll-restore
476 :link '(emacs-commentary-link "scroll-restore.el")
477 (if scroll-restore-mode
480 (when (or scroll-restore-jump-back scroll-restore-handle-region
481 scroll-restore-handle-cursor)
482 (add-hook 'pre-command-hook 'scroll-restore-pre-command))
483 (add-hook 'post-command-hook 'scroll-restore-post-command t))
484 (scroll-restore-remove 'all)
485 (remove-hook 'pre-command-hook 'scroll-restore-pre-command)
486 (remove-hook 'post-command-hook 'scroll-restore-post-command)))
488 (defun scroll-restore-restart ()
489 "Restart Scroll Restore mode."
490 (scroll-restore-mode -1)
491 (scroll-restore-mode 1))
493 (provide 'scroll-restore)
494 ;;; scroll-restore.el ends here