.
675 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | # History of user-visible changes
2 |
3 | ## [v0.3.1](https://github.com/trevorpogue/topspace/tree/v0.3.1) (2022-08-24)
4 |
5 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.3.0...v0.3.1)
6 |
7 | **Fixed bugs:**
8 |
9 | - Prevent line `1` indicator sometimes displaying too high in `linum-mode` [\#22](https://github.com/trevorpogue/topspace/pull/22) ([trevorpogue](https://github.com/trevorpogue))
10 |
11 | ## [v0.3.0](https://github.com/trevorpogue/topspace/tree/v0.3.0) (2022-05-13)
12 |
13 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.2.1...v0.3.0)
14 |
15 | **Implemented enhancements:**
16 |
17 | - Add `topspace-set-height`, enhance `topspace-center-position` [\#19](https://github.com/trevorpogue/topspace/pull/19) ([trevorpogue](https://github.com/trevorpogue))
18 | - Add `topspace-height` function for use by external packages [\#15](https://github.com/trevorpogue/topspace/pull/15) ([trevorpogue](https://github.com/trevorpogue))
19 | - Add support for `smooth-scrolling` package [\#14](https://github.com/trevorpogue/topspace/pull/14) ([trevorpogue](https://github.com/trevorpogue))
20 |
21 | **Fixed bugs:**
22 |
23 | - Fix unexpected top space height change when echo area height changes [\#18](https://github.com/trevorpogue/topspace/pull/18) ([trevorpogue](https://github.com/trevorpogue))
24 |
25 | ## [v0.2.1](https://github.com/trevorpogue/topspace/tree/v0.2.1) (2022-04-15)
26 |
27 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.2.0...v0.2.1)
28 |
29 | **Fixed bugs:**
30 |
31 | - Prevent "Beginning of buffer" error message when scrolling above top [\#12](https://github.com/trevorpogue/topspace/pull/12) ([trevorpogue](https://github.com/trevorpogue))
32 | - Fix inability to use scrolling commands interactively [\#11](https://github.com/trevorpogue/topspace/pull/11) ([trevorpogue](https://github.com/trevorpogue))
33 |
34 | ## [v0.2.0](https://github.com/trevorpogue/topspace/tree/v0.2.0) (2022-04-12)
35 |
36 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.1.2...v0.2.0)
37 |
38 | **Implemented enhancements:**
39 |
40 | - Put topspace-empty-line-indicator inside left fringe [\#9](https://github.com/trevorpogue/topspace/pull/9) ([trevorpogue](https://github.com/trevorpogue))
41 | - Add topspace-empty-line-indicator defcustom [\#8](https://github.com/trevorpogue/topspace/pull/8) ([trevorpogue](https://github.com/trevorpogue))
42 | - Add `topspace-active`, improve `topspace-autocenter-buffers` [\#4](https://github.com/trevorpogue/topspace/pull/4) ([trevorpogue](https://github.com/trevorpogue))
43 |
44 | **Fixed bugs:**
45 |
46 | - Support buffers with varying line heights [\#10](https://github.com/trevorpogue/topspace/pull/10) ([trevorpogue](https://github.com/trevorpogue))
47 | - Fix bug where topspace-mode doesn't work locally [\#6](https://github.com/trevorpogue/topspace/pull/6) ([trevorpogue](https://github.com/trevorpogue))
48 |
49 | ## [v0.1.2](https://github.com/trevorpogue/topspace/tree/v0.1.2) (2022-03-01)
50 |
51 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.1.1...v0.1.2)
52 |
53 | **Fixed bugs:**
54 | * [#2](https://github.com/trevorpogue/topspace/pull/2): Make `recenter-top-bottom` act correctly when it moves point to bottom and top space is added to get there
55 |
56 | **Other changes:**
57 |
58 | * [2584138](https://github.com/trevorpogue/topspace/commit/25841387a5d0300ea49356b9781c357b84df20bd): Raise topspace-center-position default to a subjectively better position
59 |
60 | ## [v0.1.1](https://github.com/trevorpogue/topspace/tree/v0.1.1) (2022-02-22)
61 |
62 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/v0.1.0...v0.1.1)
63 |
64 | **Fixed bugs:**
65 |
66 | * [4a69b2e](https://github.com/trevorpogue/topspace/commit/4a69b2eb741f8db9d69169a03a6724af0f2ec7ac): Allow recenter and recenter-top-bottom to be called interactively without an error
67 | * [4eb27ab](https://github.com/trevorpogue/topspace/commit/4eb27abaa182e856ba3f3c8e1e84fdd2e1f009af): Prevent top space from all suddenly disappearing when visual-line-mode is enabled and cursor scrolls bellow window-end when top space is present
68 |
69 | ## [v0.1.0](https://github.com/trevorpogue/topspace/tree/v0.1.0) (2022-02-19)
70 |
71 | [Full Changelog](https://github.com/trevorpogue/topspace/compare/79aa4e78d3f5c90fc9db46d597f1680c7900b52a...v0.1.0)
72 |
73 | **Implemented enhancements:**
74 |
75 | * [#1](https://github.com/trevorpogue/topspace/pull/1): Make mode work for any scrolling command by using add-advice with scroll-up, scroll-down, and recenter
76 |
77 |
78 | **Fixed bugs:**
79 |
80 | * [#1](https://github.com/trevorpogue/topspace/pull/1): Stabilize, clean up, and add performance optimizations to code to make it ready for submission to MELPA
81 |
82 | **Other changes:**
83 |
84 | * [e5b65ec](https://github.com/trevorpogue/topspace/commit/e5b65eccf92571163aa1b6bd738be22d8e0ad1a5): Change project name from vertical-center-mode to topspace
85 |
86 |
87 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | TopSpace
2 | Scroll down & recenter top lines in Emacs.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 
16 |
17 |
18 | [ Installation |
19 | Usage |
20 | Customization |
21 | Extra functions |
22 | How it works ]
23 |
24 | TopSpace is an Emacs minor mode that lets you display a buffer's first line in the center of a window instead of just at the top.
25 | This is done by automatically drawing an upper margin/padding above line 1
26 | as you recenter and scroll it down.
27 |
28 | ### Features
29 |
30 | * **Easier on the eyes**: Recenter or scroll down top text to a more comfortable eye level for reading, especially when in full-screen or on a large monitor.
31 | * **Easy to use**: No new keybindings are required, keep using all your previous scrolling & recentering commands, except now you can also scroll down the first line. It also integrates seamlessly with [centered-cursor-mode][1] to keep the cursor centered all the way to the first line.
32 |
33 | # Installation
34 | Run the following command in Emacs
35 |
36 | M-x `package-install` [RET] `topspace` [RET],
37 |
38 | then enable TopSpace locally with
39 |
40 | M-x `topspace-mode`,
41 |
42 | or globally with
43 |
44 | M-x `global-topspace-mode`.
45 |
46 | To enable `topspace-mode` globally on startup, add the following to your Emacs config
47 | ```
48 | (global-topspace-mode 1)
49 | ```
50 | # Usage
51 | ### Just enable and go
52 | No new keybindings are required, keep using all your previous scrolling & recentering commands, except now you can also scroll down the first line.
53 |
54 | # Customization
55 | ```elisp
56 | (defcustom topspace-active #'topspace-default-active
57 | "Determine when `topspace-mode' mode is active / has any effect on buffer.
58 | This is useful in particular when `global-topspace-mode' is enabled but you want
59 | `topspace-mode' to be inactive in certain buffers or in any specific
60 | circumstance. When inactive, `topspace-mode' will still technically be on,
61 | but will be effectively off and have no effect on the buffer.
62 | Note that if `topspace-active' returns non-nil but `topspace-mode' is off,
63 | `topspace-mode' will still be disabled.
64 |
65 | With the default value, topspace will only be inactive in child frames.
66 |
67 | If non-nil, then always be active. If nil, never be active.
68 | If set to a predicate function (function that returns a boolean value),
69 | then be active only when that function returns a non-nil value."
70 | :type '(choice (const :tag "always" t)
71 | (const :tag "never" nil)
72 | (function :tag "predicate function")))
73 |
74 | (defcustom topspace-autocenter-buffers #'topspace-default-autocenter-buffers
75 | "Center small buffers with top space when first opened or window sizes change.
76 | This is done by automatically calling `topspace-recenter-buffer'
77 | and the positioning can be customized with `topspace-center-position'.
78 | Top space will not be added if the number of text lines in the buffer is larger
79 | than or close to the selected window's height, or if `window-start' is greater
80 | than 1.
81 |
82 | With the default value, buffers will not be centered if in a child frame
83 | or if the user has already scrolled or used `recenter' with buffer in the
84 | selected window.
85 |
86 | If non-nil, then always autocenter. If nil, never autocenter.
87 | If set to a predicate function (function that returns a boolean value),
88 | then do auto-centering only when that function returns a non-nil value."
89 | :group 'topspace
90 | :type '(choice (const :tag "always" t)
91 | (const :tag "never" nil)
92 | (function :tag "predicate function")))
93 |
94 | (defcustom topspace-center-position 0.4
95 | "Target position when centering buffers.
96 |
97 | Used in `topspace-recenter-buffer' when called without an argument, or when
98 | opening/resizing buffers if `topspace-autocenter-buffers' returns non-nil.
99 |
100 | Can be set to a floating-point number, integer, or function that returns a
101 | floating-point number or integer.
102 |
103 | If a floating-point number, it represents the position to center buffers as a
104 | ratio of frame height, and can be a value from 0.0 to 1.0 where lower values
105 | center buffers higher up in the screen.
106 |
107 | If a positive or negative integer value, buffers will be centered by putting
108 | their center line at a distance of `topspace-center-position' lines away
109 | from the top of the selected window when positive, or from the bottom
110 | of the selected window when negative.
111 | The distance will be in units of lines with height `default-line-height',
112 | and the value should be less than the height of the window.
113 |
114 | If a function, the same rules above apply to the function's return value."
115 | :group 'topspace
116 | :type '(choice float integer
117 | (function :tag "float or integer function")))
118 |
119 | (defcustom topspace-empty-line-indicator
120 | #'topspace-default-empty-line-indicator
121 | "Text that will appear in each empty topspace line above the top text line.
122 | Can be set to either a constant string or a function that returns a string.
123 |
124 | The conditions in which the indicator string is present are also customizable
125 | by setting `topspace-empty-line-indicator' to a function, where the function
126 | returns \"\" (an empty string) under any conditions in which you don't want
127 | the indicator string to be shown.
128 |
129 | By default it will show the empty-line bitmap in the left fringe
130 | if `indicate-empty-lines' is non-nil, otherwise nothing.
131 | This is done by adding a 'display property to the string (see
132 | `topspace-default-empty-line-indicator' for more details).
133 | The default bitmap is the one that the `empty-line' logical fringe indicator
134 | maps to in `fringe-indicator-alist'.
135 |
136 | You can alternatively show the string text in the body of each top space line by
137 | having `topspace-empty-line-indicator' return a string without the 'display
138 | property added. If you do this you may be interested in also changing the
139 | string's face like so: (propertize indicator-string 'face 'fringe)."
140 | :type '(choice 'string (function :tag "String function")))
141 |
142 | (defcustom topspace-mode-line " T"
143 | "Mode line lighter for Topspace.
144 | The value of this variable is a mode line template as in
145 | `mode-line-format'. See Info Node `(elisp)Mode Line Format' for
146 | more information. Note that it should contain a _single_ mode
147 | line construct only.
148 | Set this variable to nil to disable the mode line completely."
149 | :group 'topspace
150 | :type 'sexp)
151 |
152 | (defvar topspace-keymap (make-sparse-keymap)
153 | "Keymap for Topspace commands.
154 | By default this is left empty for users to set with their own
155 | preferred bindings.")
156 | ```
157 |
158 | # Extra functions
159 |
160 | ```elisp
161 | ;;;###autoload
162 | (defun topspace-height ()
163 | "Return the top space height in lines for current buffer in selected window.
164 | The top space is the empty region in the buffer above the top text line.
165 | The return value is of type float, and is equivalent to
166 | the top space pixel height / `default-line-height'.
167 |
168 | If the height does not exist yet, zero will be returned if
169 | `topspace-autocenter-buffers' returns nil, otherwise a value that centers
170 | the buffer will be returned according to `topspace-center-position'.
171 |
172 | If the stored height is now invalid, it will first be corrected by
173 | `topspace--correct-height' before being returned.
174 | Valid top space line heights are:
175 | - never negative,
176 | - only positive when `window-start' equals 1,
177 | `topspace-active' returns non-nil, and `topspace-mode' is enabled,
178 | - not larger than `topspace--window-height' minus `topspace--context-lines'."
179 | ...
180 |
181 | ;;;###autoload
182 | (defun topspace-set-height (&optional total-lines)
183 | "Set and redraw the top space overlay to have a target height of TOTAL-LINES.
184 | This sets the top space height for the current buffer in the selected window.
185 | Integer or float values are accepted for TOTAL-LINES, and the value is
186 | considered to be in units of `default-line-height'.
187 |
188 | If argument TOTAL-LINES is not provided, the top space height will be set to
189 | the value returned by `topspace-height', which can be useful when redrawing a
190 | previously stored top space height in a window after a new buffer is
191 | displayed in it, or when first setting the height to an initial default value
192 | according to `topspace-height'.
193 |
194 | If TOTAL-LINES is invalid, it will be corrected by `topspace--correct-height'.
195 | Valid top space line heights are:
196 | - never negative,
197 | - only positive when `window-start' equals 1,
198 | `topspace-active' returns non-nil, and `topspace-mode' is enabled,
199 | - not larger than `topspace--window-height' minus `topspace--context-lines'."
200 | (interactive "P")
201 | ...
202 |
203 | ;;;###autoload
204 | (defun topspace-recenter-buffer (&optional position)
205 | "Add enough top space to center small buffers according to POSITION.
206 | POSITION defaults to `topspace-center-position'.
207 | Top space will not be added if the number of text lines in the buffer is larger
208 | than or close to the selected window's height, or if `window-start' is greater
209 | than 1.
210 |
211 | If POSITION is a float, it represents the position to center buffer as a ratio
212 | of frame height, and can be a value from 0.0 to 1.0 where lower values center
213 | the buffer higher up in the screen.
214 |
215 | If POSITION is a positive or negative integer value, buffer will be centered
216 | by putting its center line at a distance of `topspace-center-position' lines
217 | away from the top of the selected window when positive, or from the bottom
218 | of the selected window when negative.
219 | The distance will be in units of lines with height `default-line-height',
220 | and the value should be less than the height of the window.
221 |
222 | Top space will not be added if the number of text lines in the buffer is larger
223 | than or close to the selected window's height, or if `window-start' is greater
224 | than 1.
225 |
226 | Customize `topspace-center-position' to adjust the default centering position.
227 | Customize `topspace-autocenter-buffers' to run this command automatically
228 | after first opening buffers and after window sizes change."
229 | (interactive)
230 | ...
231 |
232 | ;;;###autoload
233 | (defun topspace-default-active ()
234 | "Default function that `topspace-active' is set to.
235 | Return nil if the selected window is in a child-frame."
236 | ...
237 |
238 | ;;;###autoload
239 | (defun topspace-default-autocenter-buffers ()
240 | "Default function that `topspace-autocenter-buffers' is set to.
241 | Return nil if the selected window is in a child-frame or user has scrolled
242 | buffer in selected window."
243 | ...
244 |
245 | ;;;###autoload
246 | (defun topspace-default-empty-line-indicator ()
247 | "Default function that `topspace-empty-line-indicator' is set to.
248 | Put the empty-line bitmap in fringe if `indicate-empty-lines' is non-nil.
249 | This is done by adding a 'display property to the returned string.
250 | The bitmap used is the one that the `empty-line' logical fringe indicator
251 | maps to in `fringe-indicator-alist'."
252 | ...
253 |
254 | ;;;###autoload
255 | (defun topspace-buffer-was-scrolled-p ()
256 | "Return t if current buffer has been scrolled in the selected window before.
257 | This is provided since it is used in `topspace-default-autocenter-buffers'.
258 | Scrolling is only recorded if topspace is active in the buffer at the time of
259 | scrolling."
260 | ...
261 | ```
262 |
263 | # How it works
264 |
265 | The "upper margin" is created by drawing an [overlay](https://www.gnu.org/software/emacs/manual/html_node/elisp/Overlays.html) before
266 | window-start containing newline characters. As you scroll down the
267 | first line, more newline characters are added or removed accordingly.
268 |
269 | No new keybindings are required as topspace automatically works for
270 | any commands or subsequent function calls which use `scroll-up`,
271 | `scroll-down`, or `recenter` as the underlying primitives for
272 | scrolling. This includes all scrolling commands/functions available
273 | in Emacs as far as the author is aware. This is achieved by using
274 | `advice-add` with the `scroll-up`, `scroll-down`, and `recenter`
275 | commands so that custom topspace functions are called before or after
276 | each time any of these other commands are called (interactively or
277 | otherwise).
278 |
279 | Fill out the [satisfaction survey](https://www.supersurvey.com/QRMU65MKE) to help the author know what you would like improved or added.
280 |
281 | [1]: https://github.com/andre-r/centered-cursor-mode.el
282 |
--------------------------------------------------------------------------------
/test/test-helper.el:
--------------------------------------------------------------------------------
1 | ;;; test-helper.el --- Helper for tests -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2022 Trevor Edwin Pogue
4 |
5 | ;; Author: Trevor Edwin Pogue
6 |
7 | ;;; Code:
8 |
9 | (when (require 'undercover nil t)
10 | (setq undercover-force-coverage t)
11 | (undercover "*.el"
12 | ;; (:report-file "coverage/.resultset.json")
13 | ;; (:report-format 'simplecov)
14 | ;; (:report-format 'text)
15 | ))
16 |
17 | (require 'smooth-scrolling)
18 | (require 'linum)
19 | (require 'topspace)
20 |
21 | ;;; test-helper.el ends here
22 |
--------------------------------------------------------------------------------
/test/topspace-test.el:
--------------------------------------------------------------------------------
1 | ;;; test-topspace.el --- Main test file -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2022 Trevor Edwin Pogue
4 |
5 | ;; Author: Trevor Edwin Pogue
6 |
7 | ;;; Code:
8 |
9 | (setq topspace--log-target '(file . "~/topspace/topspace.log"))
10 | (setq topspace--start-time (float-time))
11 | (setq topspace--scroll-down-scale-factor 0)
12 |
13 | (defun topspace--log (message)
14 | "Log MESSAGE."
15 | (when topspace--log-target
16 | (let ((log-line (format "%06d %s\n"
17 | (round (- (* 1000 (float-time))
18 | (* 1000 topspace--start-time)))
19 | message))
20 | (target-type (car topspace--log-target))
21 | (target-name (cdr topspace--log-target)))
22 | (pcase target-type
23 | ('buffer
24 | (with-current-buffer (get-buffer-create target-name)
25 | (goto-char (point-max))
26 | (insert log-line)))
27 | ('file
28 | (let ((save-silently t))
29 | (append-to-file log-line nil target-name)))
30 | (_
31 | (error "Unrecognized log target type: %S" target-type))))))
32 |
33 | (defmacro topspace--cmds (&rest cmds)
34 | "Run CMDS with command hooks."
35 | (let ((result '(progn)))
36 | (dolist (cmd cmds)
37 | (setq result
38 | (append result
39 | `((run-hooks 'pre-command-hook)
40 | (eval ',cmd)
41 | (run-hooks 'post-command-hook)
42 | ))))
43 | result))
44 |
45 | (describe
46 | "topspace"
47 | :var (prev-height)
48 |
49 | (before-all
50 | (setq topspace-center-position 0.42)
51 | (topspace--cmds (set-frame-size (selected-frame) 90 24))
52 | (switch-to-buffer (find-file-noselect "./topspace.el" t))
53 | (global-topspace-mode))
54 |
55 | (before-each (switch-to-buffer "topspace.el"))
56 |
57 | (it "reduces top space height before cursor can move below window-end"
58 | (goto-char 1)
59 | (topspace-set-height 0)
60 | (topspace--cmds
61 | (scroll-down)
62 | (scroll-up)
63 | (scroll-down)
64 | )
65 | (setq prev-height (topspace-height))
66 | (topspace--cmds
67 | (next-line))
68 | (expect (topspace-height) :to-equal (1- prev-height))
69 | (topspace--cmds (next-line 4))
70 | (expect (topspace-height) :to-equal (- prev-height 5))
71 | (topspace--cmds (scroll-down 2)))
72 |
73 | (it "moves cursor up before cursor is scrolled below window-end"
74 | (topspace--cmds (scroll-down-line))
75 | (expect (topspace-height) :to-equal (- prev-height 2))
76 | (topspace--cmds
77 | (scroll-down-line)
78 | (scroll-down-line))
79 | (expect (topspace-height) :to-equal prev-height)
80 | (topspace--cmds (scroll-up-line))
81 | (expect (topspace-height) :to-equal (1- prev-height)))
82 |
83 | (describe
84 | "topspace--after-scroll"
85 | (it "is needed when first scrolling above the top line"
86 | (goto-char 1)
87 | (topspace-set-height 0)
88 | (scroll-up-line)
89 | (scroll-down 2)
90 | (goto-char 1)
91 | (topspace-set-height 0)
92 | (scroll-up-line)
93 | (scroll-down 2)
94 | (expect (round (topspace-height)) :to-equal 1)
95 | ))
96 |
97 | (describe
98 | "topspace--window-configuration-change"
99 |
100 | (it "autocenters buffer when window size changes"
101 | (switch-to-buffer "*scratch*")
102 | (run-hooks 'window-configuration-change-hook)
103 | (expect (round (* (topspace-height) 10)) :to-equal 78)
104 | (topspace--cmds (set-frame-size (selected-frame) 90 22))
105 | (run-hooks 'window-configuration-change-hook)
106 | (expect (round (* (topspace-height) 10)) :to-equal 70)
107 | (topspace--cmds (set-frame-size (selected-frame) 90 24)))
108 |
109 | (it "will redraw topspace even if window height didn't change
110 | in case topspace-autocenter-buffers changed return value"
111 | (spy-on 'topspace-set-height)
112 | (topspace--window-configuration-change)
113 | (expect 'topspace-set-height :to-have-been-called)))
114 |
115 | (describe
116 | "topspace-mode"
117 | (it "can be enabled and disabled locally"
118 | (topspace-mode -1)
119 | (expect topspace-mode :to-equal nil)
120 | (scroll-up-line)
121 | (topspace-set-height 1)
122 | (expect (topspace-height) :to-equal 0.0)
123 | (ignore-errors (scroll-down-line))
124 | (topspace-mode 1)
125 | (expect topspace-mode :to-equal t)
126 | (switch-to-buffer "*scratch*")
127 | (topspace-mode -1)
128 | (topspace-recenter-buffer)
129 | (expect (topspace-height) :to-equal 0.0)
130 | (topspace-mode 1)))
131 |
132 | (describe
133 | "topspace--center-line"
134 | (it "has an optional argument that takes the value `topspace-center-position'
135 | by default"
136 | (expect (topspace--center-line) :to-equal
137 | (topspace--center-line topspace-center-position))))
138 |
139 | (describe
140 | "topspace--increase-height"
141 | (it "increases top space height"
142 | (goto-char 1)
143 | (recenter)
144 | (setq prev-height (topspace-height))
145 | (topspace--increase-height 1)
146 | (expect (topspace-height) :to-equal (1+ prev-height))))
147 |
148 | (describe
149 | "topspace--after-recenter"
150 | (it "adds top space if recentering near top of buffer"
151 | (goto-char 1)
152 | (recenter)
153 | (expect (round (topspace-height)) :to-equal (/ (window-height) 2))
154 | (recenter -1)
155 | (expect (round (topspace-height)) :to-equal (- (window-height) 2))))
156 |
157 | (describe
158 | "topspace--previous-line"
159 | (it "is to be used like previous-line but non-interactively"
160 | (goto-char 1)
161 | (next-line)
162 | (topspace--previous-line)
163 | (expect (line-number-at-pos) :to-equal 1)
164 | (should-error (topspace--previous-line))))
165 |
166 | (describe
167 | "topspace--smooth-scroll-lines-above-point"
168 | (it "allows smooth-scrolling package to work with topspace"
169 | :to-equal (smooth-scroll-lines-above-point)
170 | (progn (goto-char 1)
171 | (topspace-set-height 0)
172 | (goto-line smooth-scroll-margin)
173 | (set-window-start (selected-window) (point))
174 | (scroll-down smooth-scroll-margin)
175 | (setq smooth-scrolling-mode nil)
176 | (call-interactively 'smooth-scrolling-mode))
177 | (previous-line)
178 | (previous-line)
179 | (expect (round (topspace-height)) :to-equal 2)
180 | (setq smooth-scrolling-mode nil)))
181 |
182 | (describe
183 | "topspace-default-empty-line-indicator"
184 | (it "can return a string with an indicator in left-fringe"
185 | (setq indicate-empty-lines t)
186 | (add-to-list 'fringe-indicator-alist '(up . up-arrow))
187 | (let ((bitmap (catch 'tag (dolist (x fringe-indicator-alist)
188 | (when (eq (car x) 'empty-line)
189 | (throw 'tag (cdr x)))))))
190 | (expect (topspace-default-empty-line-indicator) :to-equal
191 | (propertize " " 'display (list `left-fringe bitmap
192 | `fringe))))))
193 | (describe
194 | "topspace--count-lines"
195 | ;; TODO: figure out how to test cask on a graphical emacs frame with display
196 | ;; (it "can count lines if window-absolute-pixel-position returns non-nil"
197 | ;; (expect (display-graphic-p) :to-equal nil)
198 | ;; (make-frame-on-display ":0")
199 | ;; (topspace--log (frame-list))
200 | ;; (sit-for 1)
201 | ;; (with-selected-window
202 | ;; ;; (switch-to-buffer "topspace.el")
203 | ;; (frame-selected-window (car (frames-on-display-list)))
204 | ;; (expect (round (topspace--count-lines (point-min) (point-max)))
205 | ;; :to-equal
206 | ;; (line-number-at-pos (point-max)))))
207 |
208 | (it "can count lines'"
209 | (set-window-start (selected-window) 300)
210 | (expect (round (topspace--count-lines (window-start)
211 | (topspace--window-end)))
212 | :to-equal (count-screen-lines (window-start)
213 | (topspace--window-end)))
214 | (set-window-start (selected-window) 1))
215 |
216 | (it "can count lines if start is larger than end"
217 | (set-window-start (selected-window) 300)
218 | (expect (round (topspace--count-lines 401 201))
219 | :to-equal 4)
220 | (expect (round (topspace--count-lines 201 401))
221 | :to-equal 4)
222 | (set-window-start (selected-window) 1))
223 |
224 | (it "can count lines if window-absolute-pixel-position returns nil"
225 | (expect (round (round (topspace--count-lines 201 401)))
226 | :to-equal
227 | 4)))
228 |
229 | (describe
230 | "topspace--correct-height"
231 | (it "fixes topspace height when larger than max valid value"
232 | (let ((max-height
233 | (- (window-text-height) topspace--context-lines)))
234 | (expect (topspace--correct-height (1+ max-height))
235 | :to-equal max-height))))
236 |
237 | (describe
238 | "topspace-height"
239 | (it "by default returns 0 for new buffer when topspace-autocenter-buffers
240 | returns nil"
241 | (let ((prev-autocenter-val topspace-autocenter-buffers))
242 | (setq topspace--heights '())
243 | (setq topspace-autocenter-buffers nil)
244 | (expect (topspace-height) :to-equal 0.0)
245 | (setq topspace-autocenter-buffers prev-autocenter-val))))
246 |
247 | (describe
248 | "topspace--current-line-plus-topspace"
249 | (it "can accept an arg or no args"
250 | (expect (topspace--current-line-plus-topspace)
251 | :to-equal (topspace--current-line-plus-topspace
252 | (topspace-height)))))
253 |
254 | (describe
255 | "topspace-center-position"
256 | (it "can be a float value or function, in which case
257 | its return value represents the position to center buffers as a ratio of
258 | frame height, and can be a value from 0 to 1 where lower values center
259 | buffers higher up in the screen."
260 | (setq topspace--prev-center-position topspace-center-position)
261 | (setq topspace-center-position 0.5)
262 | (switch-to-buffer "*scratch*")
263 | (topspace-recenter-buffer)
264 | (expect (topspace-height) :to-equal
265 | (- (* (topspace--frame-height)
266 | (topspace--eval-choice topspace-center-position))
267 | (window-top-line)))
268 | (defun topspace--center-position-test () 0.5)
269 | (setq topspace-center-position #'topspace--center-position-test)
270 | (topspace-recenter-buffer)
271 | (expect (topspace-height) :to-equal
272 | (- (* (topspace--frame-height)
273 | (topspace--eval-choice topspace-center-position))
274 | (window-top-line)))
275 | (setq topspace-center-position topspace--prev-center-position))
276 |
277 | (it "can be an integer value or function, in which case:
278 |
279 | If a positive integer value, buffers will be centered putting their center
280 | line at a distance of `topspace-center-position' from the top of the
281 | selected window.
282 |
283 | If a negative integer value, buffers will be centered putting their center
284 | line at line at a distance of `topspace-center-position' away from
285 | the bottom of the selected window.
286 | (ARG should be less than the height of the window.)"
287 | (setq topspace--prev-center-position topspace-center-position)
288 | (setq topspace-center-position 4)
289 | (switch-to-buffer "*scratch*")
290 | ;; (expect (topspace-height) :to-equal (/ (frame-text-lines) 2))
291 | (topspace-recenter-buffer)
292 | (expect (topspace-height) :to-equal 4.0)
293 | (defun topspace--center-position-test () 4)
294 | (setq topspace-center-position #'topspace--center-position-test)
295 | (topspace-recenter-buffer)
296 | (expect (topspace-height) :to-equal 4.0)
297 | (setq topspace-center-position -4)
298 | (topspace-recenter-buffer)
299 | (expect (topspace-height) :to-equal (float (- (window-text-height)
300 | topspace--context-lines)))
301 | (setq topspace-center-position topspace--prev-center-position))
302 | )
303 | )
304 |
305 | ;;; test-topspace.el ends here
306 |
--------------------------------------------------------------------------------
/topspace.el:
--------------------------------------------------------------------------------
1 | ;;; topspace.el --- Recenter line 1 with scrollable upper margin/padding -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2021-2022 Free Software Foundation, Inc.
4 |
5 | ;; Author: Trevor Edwin Pogue
6 | ;; Maintainer: Trevor Edwin Pogue
7 | ;; URL: https://github.com/trevorpogue/topspace
8 | ;; Keywords: convenience, scrolling, center, cursor, margin, padding
9 | ;; Version: 0.3.1
10 | ;; Package-Requires: ((emacs "25.1"))
11 |
12 | ;; This file is part of GNU Emacs.
13 |
14 | ;; GNU Emacs is free software: you can redistribute it and/or modify
15 | ;; it under the terms of the GNU General Public License as published by
16 | ;; the Free Software Foundation, either version 3 of the License, or
17 | ;; (at your option) any later version.
18 |
19 | ;; GNU Emacs is distributed in the hope that it will be useful,
20 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | ;; GNU General Public License for more details.
23 |
24 | ;; You should have received a copy of the GNU General Public License
25 | ;; along with GNU Emacs. If not, see .
26 |
27 | ;;; Commentary:
28 |
29 | ;; TopSpace lets you display a buffer's first line in the center of a window
30 | ;; instead of just at the top.
31 | ;; This is done by automatically drawing an upper margin/padding above line 1
32 | ;; as you recenter and scroll it down.
33 |
34 | ;; See https://github.com/trevorpogue/topspace for a GIF demo & documentation.
35 |
36 | ;; Features:
37 |
38 | ;; - Easier on the eyes: Recenter or scroll down top text to a more
39 | ;; comfortable eye level for reading, especially when in full-screen
40 | ;; or on a large monitor.
41 |
42 | ;; - Easy to use: No new keybindings are required, keep using all
43 | ;; your previous scrolling & recentering commands, except now you
44 | ;; can also scroll down the first line. It also integrates
45 | ;; seamlessly with `centered-cursor-mode' to keep the cursor
46 | ;; centered all the way to the first line.
47 |
48 | ;;; Code:
49 |
50 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
51 | ;;; Private variables
52 |
53 | (defvar-local topspace--heights '()
54 | "Stores top space heights of each window that buffer has been selected in.")
55 |
56 | (defvar-local topspace--buffer-was-scrolled '()
57 | "Stores if user has scrolled buffer in selected window before.
58 | Only recorded if topspace is active in the buffer at the time of scrolling.")
59 |
60 | (defvar-local topspace--previous-window-heights '()
61 | "Stores the window heights of each window that buffer has been selected in.")
62 |
63 | (defvar-local topspace--window-start-before-scroll 2
64 | "Helps to identify if more top space must be drawn after scrolling up.")
65 |
66 | (defvar-local topspace--total-lines-scrolling 0
67 | "Stores the total lines that the user is scrolling until scroll is complete.")
68 |
69 | (defvar-local topspace--pre-command-point 1
70 | "Used for performance improvement by abandoning extra calculations.
71 | In the post command hook, this determines if point moved further than the
72 | window height, in which case there is no point checking if the top space
73 | should be reduced in size or not. It also determines the direction of
74 | movement that the user is moving point in since some `post-command-hook'
75 | operations are only needed when moving downward.")
76 |
77 | (defvar-local topspace--pre-command-window-start 2
78 | "Used for performance improvement by abandoning extra calculations.
79 | In the post command hook, this determines if any top space was present
80 | before the command, otherwise there is no point checking if the top
81 | space should be reduced in size or not.")
82 |
83 | (defvar-local topspace--got-first-window-configuration-change nil
84 | "Displaying top space before the first window config change can cause errors.
85 | This flag signals to wait until then to display top space.")
86 |
87 | (defvar topspace--advice-added nil
88 | "Keeps track of if `advice-add' has been done already.")
89 |
90 | (defvar topspace--scroll-down-scale-factor 1
91 | "For eliminating an error when testing in non-interactive batch mode.
92 | An error occurs in this mode any time `scroll-down' is passed a
93 | non-zero value, which halts tests and makes testing many topspace features
94 | impossible. So this variable is set to zero when testing in this mode.")
95 |
96 | (defvar topspace--context-lines 1
97 | "Determines how many lines away from `window-end' the cursor can get.
98 | This is relevant when scrolling in such a way that the cursor tries to
99 | move past `window-end'.")
100 |
101 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
102 | ;;; Customization
103 |
104 | (defgroup topspace nil
105 | "Scroll down & recenter top lines / get upper margins/padding."
106 | :group 'scrolling
107 | :group 'convenience
108 | :link '(emacs-library-link :tag "Source Lisp File" "topspace.el")
109 | :link '(url-link "https://github.com/trevorpogue/topspace")
110 | :link '(emacs-commentary-link :tag "Commentary" "topspace"))
111 |
112 | (defcustom topspace-active #'topspace-default-active
113 | "Determine when `topspace-mode' mode is active / has any effect on buffer.
114 | This is useful in particular when `global-topspace-mode' is enabled but you want
115 | `topspace-mode' to be inactive in certain buffers or in any specific
116 | circumstance. When inactive, `topspace-mode' will still technically be on,
117 | but will be effectively off and have no effect on the buffer.
118 | Note that if `topspace-active' returns non-nil but `topspace-mode' is off,
119 | `topspace-mode' will still be disabled.
120 |
121 | With the default value, topspace will only be inactive in child frames.
122 |
123 | If non-nil, then always be active. If nil, never be active.
124 | If set to a predicate function (function that returns a boolean value),
125 | then be active only when that function returns a non-nil value."
126 | :type '(choice (const :tag "always" t)
127 | (const :tag "never" nil)
128 | (function :tag "predicate function")))
129 |
130 | (defcustom topspace-autocenter-buffers #'topspace-default-autocenter-buffers
131 | "Center small buffers with top space when first opened or window sizes change.
132 | This is done by automatically calling `topspace-recenter-buffer'
133 | and the positioning can be customized with `topspace-center-position'.
134 | Top space will not be added if the number of text lines in the buffer is larger
135 | than or close to the selected window's height, or if `window-start' is greater
136 | than 1.
137 |
138 | With the default value, buffers will not be centered if in a child frame
139 | or if the user has already scrolled or used `recenter' with buffer in the
140 | selected window.
141 |
142 | If non-nil, then always autocenter. If nil, never autocenter.
143 | If set to a predicate function (function that returns a boolean value),
144 | then do auto-centering only when that function returns a non-nil value."
145 | :group 'topspace
146 | :type '(choice (const :tag "always" t)
147 | (const :tag "never" nil)
148 | (function :tag "predicate function")))
149 |
150 | (defcustom topspace-center-position 0.4
151 | "Target position when centering buffers.
152 |
153 | Used in `topspace-recenter-buffer' when called without an argument, or when
154 | opening/resizing buffers if `topspace-autocenter-buffers' returns non-nil.
155 |
156 | Can be set to a floating-point number, integer, or function that returns a
157 | floating-point number or integer.
158 |
159 | If a floating-point number, it represents the position to center buffers as a
160 | ratio of frame height, and can be a value from 0.0 to 1.0 where lower values
161 | center buffers higher up in the screen.
162 |
163 | If a positive or negative integer value, buffers will be centered by putting
164 | their center line at a distance of `topspace-center-position' lines away
165 | from the top of the selected window when positive, or from the bottom
166 | of the selected window when negative.
167 | The distance will be in units of lines with height `default-line-height',
168 | and the value should be less than the height of the window.
169 |
170 | If a function, the same rules above apply to the function's return value."
171 | :group 'topspace
172 | :type '(choice float integer
173 | (function :tag "floating-point number or integer function")))
174 |
175 | (defcustom topspace-empty-line-indicator
176 | #'topspace-default-empty-line-indicator
177 | "Text that will appear in each empty topspace line above the top text line.
178 | Can be set to either a constant string or a function that returns a string.
179 |
180 | The conditions in which the indicator string is present are also customizable
181 | by setting `topspace-empty-line-indicator' to a function, where the function
182 | returns \"\" (an empty string) under any conditions in which you don't want
183 | the indicator string to be shown.
184 |
185 | By default it will show the empty-line bitmap in the left fringe
186 | if `indicate-empty-lines' is non-nil, otherwise nothing.
187 | This is done by adding a 'display property to the string (see
188 | `topspace-default-empty-line-indicator' for more details).
189 | The default bitmap is the one that the `empty-line' logical fringe indicator
190 | maps to in `fringe-indicator-alist'.
191 |
192 | You can alternatively show the string text in the body of each top space line by
193 | having `topspace-empty-line-indicator' return a string without the 'display
194 | property added. If you do this you may be interested in also changing the
195 | string's face like so: (propertize indicator-string 'face 'fringe)."
196 | :type '(choice 'string (function :tag "String function")))
197 |
198 | (defcustom topspace-mode-line " T"
199 | "Mode line lighter for Topspace.
200 | The value of this variable is a mode line template as in
201 | `mode-line-format'. See Info Node `(elisp)Mode Line Format' for
202 | more information. Note that it should contain a _single_ mode
203 | line construct only.
204 | Set this variable to nil to disable the mode line completely."
205 | :group 'topspace
206 | :type 'sexp)
207 |
208 | (defvar topspace-keymap (make-sparse-keymap)
209 | "Keymap for Topspace commands.
210 | By default this is left empty for users to set with their own
211 | preferred bindings.")
212 |
213 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
214 | ;;; User functions
215 |
216 | ;;;###autoload
217 | (defun topspace-height ()
218 | "Return the top space height in lines for current buffer in selected window.
219 | The top space is the empty region in the buffer above the top text line.
220 | The return value is a floating-point number, and is equivalent to
221 | the top space pixel height / `default-line-height'.
222 |
223 | If the height does not exist yet, zero will be returned if
224 | `topspace-autocenter-buffers' returns nil, otherwise a value that centers
225 | the buffer will be returned according to `topspace-center-position'.
226 |
227 | If the stored height is now invalid, it will first be corrected by
228 | `topspace--correct-height' before being returned.
229 | Valid top space line heights are:
230 | - never negative,
231 | - only positive when `window-start' equals 1,
232 | `topspace-active' returns non-nil, and `topspace-mode' is enabled,
233 | - not larger than `window-text-height' minus `topspace--context-lines'."
234 | (let ((height) (window (selected-window)))
235 | ;; First try returning previously stored top space height
236 | (setq height (alist-get window topspace--heights))
237 | (unless height
238 | ;; If it has never been created before then get the default value
239 | (setq height (if (topspace--eval-choice topspace-autocenter-buffers)
240 | (topspace--height-to-recenter-buffer) 0.0)))
241 | ;; Correct, store, and return the new value
242 | (topspace--set-height height)))
243 |
244 | ;;;###autoload
245 | (defun topspace-set-height (&optional total-lines)
246 | "Set and redraw the top space overlay to have a target height of TOTAL-LINES.
247 | This sets the top space height for the current buffer in the selected window.
248 | Integer or floating-point numbers are accepted for TOTAL-LINES, and the value is
249 | considered to be in units of `default-line-height'.
250 |
251 | If argument TOTAL-LINES is not provided, the top space height will be set to
252 | the value returned by `topspace-height', which can be useful when redrawing a
253 | previously stored top space height in a window after a new buffer is
254 | displayed in it, or when first setting the height to an initial default value
255 | according to `topspace-height'.
256 |
257 | If TOTAL-LINES is invalid, it will be corrected by `topspace--correct-height'.
258 | Valid top space line heights are:
259 | - never negative,
260 | - only positive when `window-start' equals 1,
261 | `topspace-active' returns non-nil, and `topspace-mode' is enabled,
262 | - not larger than `window-text-height' minus `topspace--context-lines'."
263 | (interactive "P")
264 | (let ((old-height) (window (selected-window)))
265 | ;; Get the previous top space height
266 | (unless old-height (setq old-height (topspace-height)))
267 | ;; Get the default value if TOTAL-LINES arg not provided
268 | (unless total-lines (setq total-lines old-height))
269 | ;; Update or correct the stored top space height to new value
270 | (setq total-lines (topspace--correct-height
271 | (topspace--set-height total-lines)))
272 | (when (and (> total-lines 0) (> total-lines old-height))
273 | ;; If top space height is increasing, make sure it doesn't push the
274 | ;; cursor off the screen
275 | (let ((lines-past-max (topspace--total-lines-past-max total-lines)))
276 | (when (> lines-past-max 0)
277 | (topspace--previous-line (ceiling lines-past-max)))))
278 | (let ((topspace (make-overlay 1 1)))
279 | ;; Redraw top space with the new height by drawing a new overlay and
280 | ;; erasing any previously drawn overlays for current buffer in
281 | ;; selected window
282 | (remove-overlays 1 1 'topspace--remove-from-window-tag window)
283 | (overlay-put topspace 'window window)
284 | (overlay-put topspace 'topspace--remove-from-window-tag window)
285 | (overlay-put topspace 'topspace--remove-from-buffer-tag t)
286 | (overlay-put topspace 'before-string (topspace--text total-lines)))
287 | ;; Return the new height
288 | total-lines))
289 |
290 | ;;;###autoload
291 | (defun topspace-recenter-buffer (&optional position)
292 | "Add enough top space to center small buffers according to POSITION.
293 | POSITION defaults to `topspace-center-position'.
294 | Top space will not be added if the number of text lines in the buffer is larger
295 | than or close to the selected window's height, or if `window-start' is greater
296 | than 1.
297 |
298 | If POSITION is a floating-point, it represents the position to center buffer as
299 | a ratio of frame height, and can be a value from 0.0 to 1.0 where lower values
300 | center the buffer higher up in the screen.
301 |
302 | If POSITION is a positive or negative integer value, buffer will be centered
303 | by putting its center line at a distance of `topspace-center-position' lines
304 | away from the top of the selected window when positive, or from the bottom
305 | of the selected window when negative.
306 | The distance will be in units of lines with height `default-line-height',
307 | and the value should be less than the height of the window.
308 |
309 | Top space will not be added if the number of text lines in the buffer is larger
310 | than or close to the selected window's height, or if `window-start' is greater
311 | than 1.
312 |
313 | Customize `topspace-center-position' to adjust the default centering position.
314 | Customize `topspace-autocenter-buffers' to run this command automatically
315 | after first opening buffers and after window sizes change."
316 | (interactive)
317 | (cond
318 | ((not (topspace--enabled)) (topspace-set-height 0.0))
319 | (t (topspace-set-height (topspace--height-to-recenter-buffer position)))))
320 |
321 | ;;;###autoload
322 | (defun topspace-default-active ()
323 | "Default function that `topspace-active' is set to.
324 | Return nil if the selected window is in a child-frame."
325 | (or ;; frame-parent is only provided in Emacs 26.1, so first check
326 | ;; if fhat function exists.
327 | (not (fboundp 'frame-parent))
328 | (not (frame-parent))))
329 |
330 | ;;;###autoload
331 | (defun topspace-default-autocenter-buffers ()
332 | "Default function that `topspace-autocenter-buffers' is set to.
333 | Return nil if the selected window is in a child-frame or user has scrolled
334 | buffer in selected window."
335 | (and (not (topspace-buffer-was-scrolled-p))
336 | (or ;; frame-parent is only provided in Emacs 26.1, so first check
337 | ;; if fhat function exists.
338 | (not (fboundp 'frame-parent))
339 | (not (frame-parent)))))
340 |
341 | ;;;###autoload
342 | (defun topspace-default-empty-line-indicator ()
343 | "Default function that `topspace-empty-line-indicator' is set to.
344 | Put the empty-line bitmap in fringe if `indicate-empty-lines' is non-nil.
345 | This is done by adding a 'display property to the returned string.
346 | The bitmap used is the one that the `empty-line' logical fringe indicator
347 | maps to in `fringe-indicator-alist'."
348 | (if indicate-empty-lines
349 | (let ((bitmap
350 | (catch 'tag
351 | (dolist (x fringe-indicator-alist)
352 | (when (eq (car x) 'empty-line) (throw 'tag (cdr x)))))))
353 | (propertize " " 'display (list `left-fringe bitmap `fringe)))
354 | ""))
355 |
356 | ;;;###autoload
357 | (defun topspace-buffer-was-scrolled-p ()
358 | "Return t if current buffer has been scrolled in the selected window before.
359 | This is provided since it is used in `topspace-default-autocenter-buffers'.
360 | Scrolling is only recorded if topspace is active in the buffer at the time of
361 | scrolling."
362 | (alist-get (selected-window) topspace--buffer-was-scrolled))
363 |
364 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
365 | ;;; Advice for `scroll-up', `scroll-down', and `recenter'
366 |
367 | (defun topspace--scroll (total-lines)
368 | "Run before `scroll-up'/`scroll-down' for updating top space before scrolling.
369 | TOTAL-LINES is used in the same way as in `scroll-down'."
370 | (setf (alist-get (selected-window) topspace--buffer-was-scrolled) t)
371 | (let ((old-topspace-height (topspace-height))
372 | (new-topspace-height))
373 | (setq new-topspace-height (topspace--correct-height
374 | (+ old-topspace-height total-lines)))
375 | (setq topspace--window-start-before-scroll (window-start))
376 | (topspace-set-height new-topspace-height)
377 | (setq total-lines
378 | (- total-lines (- new-topspace-height old-topspace-height)))
379 | (round total-lines)))
380 |
381 | (defun topspace--filter-args-scroll-down (&optional total-lines)
382 | "Run before `scroll-down' for scrolling above the top line.
383 | TOTAL-LINES is used in the same way as in `scroll-down'."
384 | (cond
385 | ((not (topspace--enabled)) (topspace-set-height 0.0) total-lines)
386 | (t
387 | (setq total-lines (car total-lines))
388 | (setq total-lines (or total-lines (- (window-text-height)
389 | next-screen-context-lines)))
390 | (setq topspace--total-lines-scrolling total-lines)
391 | (list (* topspace--scroll-down-scale-factor
392 | (topspace--scroll total-lines))))))
393 |
394 | (defun topspace--filter-args-scroll-up (&optional total-lines)
395 | "Run before `scroll-up' for scrolling above the top line.
396 | TOTAL-LINES is used in the same way as in `scroll-up'."
397 | (cond
398 | ((not (topspace--enabled)) (topspace-set-height 0.0) total-lines)
399 | (t
400 | (setq total-lines (car total-lines))
401 | (setq total-lines (* (or total-lines (- (window-text-height)
402 | next-screen-context-lines)) -1))
403 | (setq topspace--total-lines-scrolling total-lines)
404 | (list (* (topspace--scroll total-lines) -1)))))
405 |
406 | (defun topspace--after-scroll (&optional total-lines)
407 | "Run after `scroll-up'/`scroll-down' for scrolling above the top line.
408 | TOTAL-LINES is used in the same way as in `scroll-down'.
409 | This is needed when scrolling down (moving buffer text lower in the screen)
410 | and no top space was present before scrolling but it should be after scrolling.
411 | The reason this is needed is because `topspace-set-height' only draws the
412 | overlay when `window-start` equals 1, which can only be true after the scroll
413 | command is run in the described case above."
414 | (cond
415 | ((not (topspace--enabled)))
416 | (t
417 | (setq total-lines topspace--total-lines-scrolling)
418 | (when (and (> topspace--window-start-before-scroll 1) (= (window-start) 1))
419 | (let ((lines-already-scrolled (topspace--count-lines
420 | 1 topspace--window-start-before-scroll)))
421 | (setq total-lines (abs total-lines))
422 | (set-window-start (selected-window) 1)
423 | (topspace-set-height (- total-lines lines-already-scrolled)))
424 | (when (and (bound-and-true-p linum-mode) (fboundp 'linum-update-window))
425 | (linum-update-window (selected-window)))))))
426 |
427 | (defun topspace--after-recenter (&optional line-offset redisplay)
428 | "Recenter near the top of buffers by adding top space appropriately.
429 | LINE-OFFSET and REDISPLAY are used in the same way as in `recenter'."
430 | ;; redisplay is unused but needed since this function
431 | ;; must take the same arguments as `recenter'
432 | redisplay ; remove flycheck warning for unused argument (see above)
433 | (cond
434 | ((not (topspace--enabled)))
435 | (t
436 | (setf (alist-get (selected-window) topspace--buffer-was-scrolled) t)
437 | (when (= (window-start) 1)
438 | (setq line-offset (topspace--calculate-recenter-line-offset line-offset))
439 | (topspace-set-height (- line-offset (topspace--count-lines
440 | (window-start)
441 | (point))))))))
442 |
443 | (defun topspace--smooth-scroll-lines-above-point (&rest args)
444 | "Add support for `smooth-scroll-mode', ignore ARGS.
445 | ARGS are needed for compatibility with `advice-add'."
446 | ;; remove flycheck warnings by using R and checking smooth-scroll functions
447 | args
448 | (when (and (fboundp 'smooth-scroll-count-lines)
449 | (fboundp 'smooth-scroll-line-beginning-position))
450 | (+ (topspace-height)
451 | (smooth-scroll-count-lines
452 | (window-start) (smooth-scroll-line-beginning-position)))))
453 |
454 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
455 | ;;; Top space line height calculation
456 |
457 | (defun topspace--set-height (height)
458 | "Set the stored top space line height for the selected window to HEIGHT.
459 | Will only set to HEIGHT if HEIGHT is a valid value as per
460 | `topspace--correct-height'. This only sets the underlying stored value for
461 | top space height, and it does not redraw the top space."
462 | (setq height (topspace--correct-height height))
463 | (setf (alist-get (selected-window) topspace--heights) height)
464 | height)
465 |
466 | (defun topspace--correct-height (height)
467 | "Return HEIGHT if a valid top space line height, else a valid value.
468 | Valid top space line heights are:
469 | - never negative,
470 | - only positive when `window-start' equals 1,
471 | `topspace-active' returns non-nil, and `topspace-mode' is enabled,
472 | - not larger than `window-text-height' minus `topspace--context-lines'."
473 | (let ((max-height (- (window-text-height) topspace--context-lines)))
474 | (setq height (float height))
475 | (when (> (window-start) 1) (setq height 0.0))
476 | (when (< height 0) (setq height 0.0))
477 | (when (> height max-height) (setq height max-height))
478 | (unless (topspace--enabled) (setq height 0.0)))
479 | height)
480 |
481 | (defun topspace--window-end ()
482 | "Return the up-to-date `window-end'."
483 | (or (window-end (selected-window) t)))
484 |
485 | (defun topspace--total-lines-past-max (&optional topspace-height)
486 | "Used when making sure top space height does not push cursor off-screen.
487 | Return how many lines past the bottom of the window the cursor would get pushed
488 | if setting the top space to the target value TOPSPACE-HEIGHT.
489 | Any value above 0 flags that the target TOPSPACE-HEIGHT is too large."
490 | (- (topspace--current-line-plus-topspace topspace-height)
491 | (- (window-text-height) topspace--context-lines)))
492 |
493 | (defun topspace--current-line-plus-topspace (&optional topspace-height)
494 | "Used when making sure top space height does not push cursor off-screen.
495 | Return the current line plus the top space height TOPSPACE-HEIGHT."
496 | (+ (topspace--count-lines (window-start) (point))
497 | (or topspace-height (topspace-height))))
498 |
499 | (defun topspace--calculate-recenter-line-offset (&optional line-offset)
500 | "Convert LINE-OFFSET to a line offset from the top of the window.
501 | It is interpreted in the same way as the first ARG in `recenter'."
502 | (unless line-offset (setq line-offset (/ (float (window-text-height)) 2)))
503 | (when (< line-offset 0)
504 | ;; subtracting 1 below made `recenter-top-bottom' act correctly
505 | ;; when it moves point to bottom and top space is added to get there
506 | (setq line-offset (- (- (window-text-height) line-offset)
507 | topspace--context-lines
508 | 1)))
509 | line-offset)
510 |
511 | (defun topspace--center-line (&optional position)
512 | "Calculate the centering position when using `topspace-recenter-buffer'.
513 | Return how many lines away from the top of the selected window that the
514 | buffer's center line will be moved to based on POSITION, which defaults to
515 | `topspace-center-position'. Note that when POSITION
516 | is a floating-point number, the return value is only valid for windows
517 | starting at the top of the frame, which must be accounted for in the calling
518 | functions."
519 | (setq position (or position (topspace--eval-choice topspace-center-position)))
520 | (if (floatp position)
521 | (* (topspace--frame-height) position)
522 | (topspace--calculate-recenter-line-offset position)))
523 |
524 | (defun topspace--height-to-recenter-buffer (&optional position)
525 | "Return the necessary top space height to center selected window's buffer.
526 | Buffer will be centered according to POSITION, which defaults to
527 | `topspace-center-position'."
528 | (setq position (or position (topspace--eval-choice topspace-center-position)))
529 | (let ((buffer-height (topspace--count-lines
530 | (window-start)
531 | (topspace--window-end)))
532 | (result)
533 | (window-height (window-text-height)))
534 | (setq result (- (topspace--center-line position) (/ buffer-height 2)))
535 | (when (floatp position) (setq result (- result (window-top-line))))
536 | (when (> (+ result buffer-height)
537 | (- window-height topspace--context-lines))
538 | (setq result (- (- window-height buffer-height)
539 | topspace--context-lines)))
540 | result))
541 |
542 | (defun topspace--frame-height ()
543 | "Return the number of lines in the selected frame's text area.
544 | Subtract 3 from `frame-text-lines' to discount echo area and bottom
545 | mode-line in centering."
546 | (- (frame-text-lines) 3))
547 |
548 | (defun topspace--count-pixel-height (start end)
549 | "Return total pixels between points START and END as if they're both visible."
550 | (let ((result 0))
551 | (save-excursion
552 | (goto-char end)
553 | (beginning-of-visual-line)
554 | (setq end (point))
555 | (goto-char start)
556 | (beginning-of-visual-line)
557 | (while (< (point) end)
558 | (setq result (+ result (line-pixel-height)))
559 | (vertical-motion 1)))
560 | result))
561 |
562 | (defun topspace--count-lines-slow (start end)
563 | "Return screen lines between points START and END.
564 | Like `topspace--count-lines' but is a slower backup alternative."
565 | (/ (topspace--count-pixel-height start end) (float (default-line-height))))
566 |
567 | (defun topspace--count-lines (start end)
568 | "Return screen lines between points START and END.
569 | Like `count-screen-lines' except `count-screen-lines' will
570 | return unexpected value when END is in column 0. This fixes that issue.
571 | This function also tries to first count the lines using a potentially faster
572 | technique involving `window-absolute-pixel-position'.
573 | If that doesn't work it uses `topspace--count-lines-slow'."
574 | (let ((old-end) (old-start) (swap)
575 | (line-height (float (default-line-height))))
576 | (when (> start end) (setq swap end) (setq end start) (setq start swap))
577 | (setq old-end end) (setq old-start start)
578 | ;; use faster visual method for counting portion of lines in screen:
579 | (when (< start (topspace--window-end))
580 | (setq end (min end (topspace--window-end))))
581 | (when (> end (window-start))
582 | (setq start (max start (window-start))))
583 | (let ((end-y (window-absolute-pixel-position end))
584 | (start-y (window-absolute-pixel-position start)))
585 | (+
586 | (if (> old-end end) (topspace--count-lines-slow end old-end) 0.0)
587 | (if (< old-start start) (topspace--count-lines-slow old-start start) 0.0)
588 | (condition-case nil
589 | ;; first try counting lines by getting the pixel difference
590 | ;; between end and start and dividing by `default-line-height'
591 | (/ (- (cdr end-y) (cdr start-y)) line-height)
592 | ;; if the pixel method above doesn't work do this slower method
593 | ;; (it won't work if either START or END are not visible in window)
594 | (error (topspace--count-lines-slow start end)))))))
595 |
596 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
597 | ;;; Overlay drawing
598 |
599 | (defun topspace--text (height)
600 | "Return the topspace text that appears in the top overlay with height HEIGHT."
601 | (cond
602 | ((= (round height) 0) "")
603 | ((= (round height) 1)
604 | ;; comment a) You cannot set a string's line-height
605 | ;; to a positive float less than 1. So in this condition,
606 | ;; settle for rounding the top space height up to 1
607 | "\n")
608 | (t
609 | ;; set the text to a series of newline characters with the last line
610 | ;; having a line-height set to a float accounting for the potential
611 | ;; fractional portion of the top space height
612 | (let ((text "")
613 | (indicator-line (topspace--eval-choice
614 | topspace-empty-line-indicator)))
615 | (setq indicator-line (cl-concatenate 'string indicator-line "\n"))
616 | (dotimes (n (1- (floor height)))
617 | n ;; remove flycheck warning
618 | (setq text (cl-concatenate 'string text indicator-line)))
619 | (setq indicator-line
620 | ;; set that last line with a float line-height.
621 | ;; The float will be set to >1 due to comment a) above
622 | (propertize indicator-line 'line-height
623 | (- (1+ height) (floor height))))
624 | (cl-concatenate 'string text indicator-line)))))
625 |
626 | (defun topspace--increase-height (total-lines)
627 | "Increase the top space line height by the target amount of TOTAL-LINES."
628 | (topspace-set-height (+ (topspace-height) total-lines)))
629 |
630 | (defun topspace--decrease-height (total-lines)
631 | "Decrease the top space line height by the target amount of TOTAL-LINES."
632 | (topspace-set-height (- (topspace-height) total-lines)))
633 |
634 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
635 | ;;; Utilities
636 |
637 | (defun topspace--eval-choice (variable-or-function)
638 | "Evaluate VARIABLE-OR-FUNCTION which is either var or func'n of type var.
639 | If it is a variable, return its value, if it is a function,
640 | evaluate the function and return its return value.
641 | VARIABLE-OR-FUNCTION is most likely a user customizable variable of choice
642 | type."
643 | (condition-case nil
644 | (funcall variable-or-function)
645 | (error variable-or-function)))
646 |
647 | (defun topspace--previous-line (&optional arg try-vscroll)
648 | "Functionally identical to `previous-line' but for non-interactive use.
649 | Use TRY-VSCROLL to control whether to vscroll tall
650 | lines: if either `auto-window-vscroll' or TRY-VSCROLL is nil, this
651 | function will not vscroll.
652 | ARG defaults to 1."
653 | (or arg (setq arg 1))
654 | (line-move (- arg) nil nil try-vscroll)
655 | nil)
656 |
657 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
658 | ;;; Hooks
659 |
660 | (defun topspace--window-configuration-change ()
661 | "Update top spaces when window buffers change or windows are resized."
662 | (setq topspace--got-first-window-configuration-change t)
663 | (let ((current-height (window-text-height)) (window (selected-window)))
664 | (let ((previous-height (alist-get window topspace--previous-window-heights
665 | 0.0)))
666 | (if (and (topspace--eval-choice topspace-autocenter-buffers)
667 | (not (= previous-height current-height)))
668 | (topspace-recenter-buffer)
669 | (topspace-set-height))
670 | (setf (alist-get window topspace--previous-window-heights)
671 | current-height))))
672 |
673 | (defun topspace--pre-command ()
674 | "Reduce the amount of code that must execute in `topspace--post-command'."
675 | (setq-local topspace--pre-command-point (point))
676 | (setq-local topspace--pre-command-window-start (window-start)))
677 |
678 | (defun topspace--post-command ()
679 | "Reduce top space height before the cursor can move past `window-end'."
680 | (when (and (= topspace--pre-command-window-start 1)
681 | (> (point) topspace--pre-command-point))
682 | (let ((next-line-point))
683 | (save-excursion
684 | (goto-char topspace--pre-command-point)
685 | (vertical-motion 1)
686 | (beginning-of-visual-line)
687 | (setq next-line-point (point)))
688 | (when (and
689 | ;; These checks are for improving performance by only running
690 | ;; `topspace--count-lines' run by `topspace--total-lines-past-max'
691 | ;; when necessary because `topspace--count-lines' is slow
692 | (>= (point) next-line-point)
693 | (< (- (line-number-at-pos (point))
694 | (line-number-at-pos topspace--pre-command-point))
695 | (window-text-height)))
696 | (let ((topspace-height (topspace-height)) (total-lines-past-max))
697 | (when (> topspace-height 0)
698 | (setq total-lines-past-max (topspace--total-lines-past-max
699 | topspace-height))
700 | (when (> total-lines-past-max 0)
701 | (topspace--decrease-height total-lines-past-max)))))))
702 | (when (and (= (window-start) 1)
703 | topspace--got-first-window-configuration-change)
704 | (topspace-set-height)))
705 |
706 | (defvar topspace--hook-alist
707 | '((window-configuration-change-hook . topspace--window-configuration-change)
708 | (pre-command-hook . topspace--pre-command)
709 | (post-command-hook . topspace--post-command))
710 | "A list of hooks to add/remove in the format (hook-variable . function).")
711 |
712 | (defun topspace--add-hooks ()
713 | "Add hooks defined in `topspace--hook-alist'."
714 | (dolist (hook-func-pair topspace--hook-alist)
715 | (add-hook (car hook-func-pair) (cdr hook-func-pair) -90 t)))
716 |
717 | (defun topspace--remove-hooks ()
718 | "Remove hooks defined in `topspace--hook-alist'."
719 | (dolist (hook-func-pair topspace--hook-alist)
720 | (remove-hook (car hook-func-pair) (cdr hook-func-pair) t)))
721 |
722 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
723 | ;;; Mode definition and setup
724 |
725 | (defun topspace--enable-p ()
726 | "Return non-nil if buffer is allowed to enable `topspace-mode.'.
727 | Topspace will not be enabled for:
728 | - minibuffers
729 | - ephemeral buffers (See Info node `(elisp)Buffer Names')
730 | - if `topspace-mode' is already enabled"
731 | (not (or (minibufferp) (string-prefix-p " " (buffer-name)))))
732 |
733 | (defun topspace--enable ()
734 | "Enable `topspace-mode' and do mode setup."
735 | (when (topspace--enable-p)
736 | (topspace--add-hooks)
737 | (unless topspace--advice-added
738 | (setq topspace--advice-added t)
739 | (advice-add #'scroll-up :filter-args #'topspace--filter-args-scroll-up)
740 | (advice-add #'scroll-down :filter-args
741 | #'topspace--filter-args-scroll-down)
742 | (advice-add #'scroll-up :after #'topspace--after-scroll)
743 | (advice-add #'scroll-down :after #'topspace--after-scroll)
744 | (advice-add #'recenter :after #'topspace--after-recenter)
745 | (when (fboundp 'smooth-scroll-lines-above-point)
746 | (advice-add #'smooth-scroll-lines-above-point
747 | :override #'topspace--smooth-scroll-lines-above-point)))
748 | (dolist (window (get-buffer-window-list))
749 | (with-selected-window window (topspace-set-height)))))
750 |
751 | (defun topspace--disable ()
752 | "Disable `topspace-mode' and do mode cleanup."
753 | (remove-overlays 1 1 'topspace--remove-from-buffer-tag t)
754 | (topspace--remove-hooks))
755 |
756 | ;;;###autoload
757 | (define-minor-mode topspace-mode
758 | "Recenter line 1 with scrollable upper margin/padding.
759 |
760 | TopSpace lets you display a buffer's first line in the center of a window
761 | instead of just at the top.
762 | This is done by automatically drawing an upper margin/padding above line 1
763 | as you recenter and scroll it down.
764 |
765 | See https://github.com/trevorpogue/topspace for a GIF demo & documentation.
766 |
767 | Features:
768 |
769 | - Easier on the eyes: Recenter or scroll down top text to a more
770 | comfortable eye level for reading, especially when in full-screen
771 | or on a large monitor.
772 |
773 | - Easy to use: No new keybindings are required, keep using all
774 | your previous scrolling & recentering commands, except now you
775 | can also scroll above the top lines. It also integrates
776 | seamlessly with `centered-cursor-mode' to keep the cursor
777 | centered all the way to the top line.
778 |
779 | Enabling/disabling:
780 | When called interactively, toggle `topspace-mode'.
781 |
782 | With prefix ARG, enable `topspace-mode' if
783 | ARG is positive, otherwise disable it.
784 |
785 | When called from Lisp, enable `topspace-mode' if
786 | ARG is omitted, nil or positive.
787 |
788 | If ARG is `toggle', toggle `topspace-mode'.
789 | Otherwise behave as if called interactively."
790 | :init-value nil
791 | :ligher topspace-mode-line
792 | :keymap topspace-keymap
793 | :group 'topspace
794 | (if topspace-mode (topspace--enable) (topspace--disable)))
795 |
796 | ;;;###autoload
797 | (define-globalized-minor-mode global-topspace-mode topspace-mode
798 | topspace-mode
799 | :group 'topspace)
800 |
801 | (defun topspace--enabled ()
802 | "Return t only if both `topspace-mode' and `topspace-active' are non-nil."
803 | (and (topspace--eval-choice topspace-active) topspace-mode))
804 |
805 | (provide 'topspace)
806 |
807 | ;;; topspace.el ends here
808 |
--------------------------------------------------------------------------------