├── demo.gif ├── plugin └── smoothie.vim ├── LICENSE ├── README.md └── autoload └── smoothie.vim /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/vim-smoothie/master/demo.gif -------------------------------------------------------------------------------- /plugin/smoothie.vim: -------------------------------------------------------------------------------- 1 | nnoremap (SmoothieDownwards) :call smoothie#downwards() 2 | nnoremap (SmoothieUpwards) :call smoothie#upwards() 3 | nnoremap (SmoothieForwards) :call smoothie#forwards() 4 | nnoremap (SmoothieBackwards) :call smoothie#backwards() 5 | 6 | if !get(g:, 'smoothie_no_default_mappings', v:false) 7 | silent! nmap (SmoothieDownwards) 8 | silent! nmap (SmoothieUpwards) 9 | silent! nmap (SmoothieForwards) 10 | silent! nmap (SmoothieForwards) 11 | silent! nmap (SmoothieForwards) 12 | silent! nmap (SmoothieBackwards) 13 | silent! nmap (SmoothieBackwards) 14 | silent! nmap (SmoothieBackwards) 15 | endif 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Piotr Śliwka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vim-smoothie: Smooth scrolling for Vim done right🥤 2 | =================================================== 3 | 4 | This (neo)vim plugin makes scrolling nice and _smooth_. Find yourself 5 | completely lost every time you press `Ctrl-D` or `Ctrl-F`? You might want to 6 | give _vim-smoothie_ a try! 7 | 8 | ![scrolling demo](demo.gif) 9 | 10 | Installation 11 | ------------ 12 | 13 | You will need reasonably new Vim or Neovim with timers support. Vim 8+ or 14 | Neovim 0.3+ should do the trick. 15 | 16 | Install the plugin using your favorite plugin manager, for example [vim-plug]: 17 | ``` 18 | Plug 'psliwka/vim-smoothie' 19 | ``` 20 | 21 | Customization 22 | ------------- 23 | 24 | _vim-smoothie_ aims for sane defaults, and should work out-of-the-box for most 25 | users. In some cases, however, you might want to customize its behavior, by 26 | adjusting one or more of the following variables in your `vimrc`: 27 | 28 | * `g:smoothie_no_default_mappings`: If true, will prevent the plugin from 29 | overriding default scrolling keys (`Ctrl-D` and friends). You are then 30 | supposed to bind keys you like by yourself. See `plugin/smoothie.vim` to 31 | discover available mappings. 32 | 33 | Alternatives, a.k.a. why create yet another plugin 34 | -------------------------------------------------- 35 | 36 | There are many other Vim plugins attempting to resolve the same problem. The 37 | most intresting one is [sexy_scroller.vim], which covers way more movement 38 | commands than vim-smoothie will ever do. Unfortunately, it also suffers from 39 | frequent visual artifacts, such as erratic screen jumps and animation 40 | jittering, impairing visual orientation and breaking the user experience. Many 41 | of these bugs are nearly impossible to fix due to the plugin's internal design. 42 | Hence, vim-smoothie was born, focusing on stable, bug-free, _smooth_ 43 | experience, at a cost of smaller feature set. 44 | 45 | The table below summarizes key differences between vim-smoothie and three other 46 | popular smooth scrolling plugins I've used in the past: [sexy_scroller.vim], 47 | [comfortable-motion.vim], and [vim-smooth-scroll]. 48 | 49 | | | vim-smoothie | [sexy_scroller.vim] | [comfortable-motion.vim] | [vim-smooth-scroll] | 50 | |---|:---:|:---:|:---:|:---:| 51 | | Supported commands | `^D` `^U` `^F` `^B` | A lot❤️ | `^D` `^U` `^F` `^B` | `^D` `^U` `^F` `^B` | 52 | | Erratic screen jumps and jittering now and then | Nope | A lot💔 | Nope | Nope | 53 | | Scrolling distance is proportional to window height | ✅ | ✅ | ❌ | ✅ | 54 | | Easing out (soft-stop) | ✅ | ✅ | ✅ | ❌ | 55 | | Supports setting `[count]` before movement (f.ex. `3^F` to scroll down 3 pages) | ✅ | ✅ | ❌ | ❌ | 56 | | Respects `scroll` and `startofline` options | ✅ | ✅ | ❌ | ❌ | 57 | | `^D` and `^U` behave correctly near buffer ends, just moving the cursor instead of scrolling the window | ✅ | ✅ | ❌ | ❌ | 58 | | Pun in name | ✅ | ✅ | ❌ | ❌ | 59 | 60 | Known issues/incompatibilities 61 | ------------------------------ 62 | 63 | vim-smoothie strives to remain fully compatible with native commands it 64 | replaces. That is, every command should still behave exactly as described in 65 | `:help scroll.txt`. There are still some deviations from the origial behavior, 66 | which hopefully will be addressed in the future: 67 | 68 | * `^D`, `^U`, `^F`, `^B` should beep when they can't move any further. 69 | * `^F` and `^B` should respect the `window` option. 70 | * Native commands may move in a smarter way over wrapped/folded lines. 71 | 72 | 73 | Credits 74 | ------- 75 | 76 | Created by [Piotr Śliwka](https://github.com/psliwka). 77 | 78 | Many thanks to authors of [vim-smooth-scroll], [comfortable-motion.vim], and 79 | [sexy_scroller.vim] for inspiration! 80 | 81 | License 82 | ------- 83 | 84 | [MIT](LICENSE) 85 | 86 | [vim-plug]: https://github.com/junegunn/vim-plug 87 | [vim-smooth-scroll]: https://github.com/terryma/vim-smooth-scroll 88 | [comfortable-motion.vim]: https://github.com/yuttie/comfortable-motion.vim 89 | [sexy_scroller.vim]: https://github.com/joeytwiddle/sexy_scroller.vim 90 | -------------------------------------------------------------------------------- /autoload/smoothie.vim: -------------------------------------------------------------------------------- 1 | if !exists('g:smoothie_update_interval') 2 | "" 3 | " Time (in milliseconds) between subseqent screen/cursor postion updates. 4 | " Lower value produces smoother animation. Might be useful to increase it 5 | " when running Vim over low-bandwidth/high-latency connections. 6 | let g:smoothie_update_interval = 20 7 | endif 8 | 9 | if !exists('g:smoothie_base_speed') 10 | "" 11 | " Base scrolling speed (in lines per second), to be taken into account by 12 | " the velocity calculation algorithm. Can be decreased to achieve slower 13 | " (and easier to follow) animation. 14 | let g:smoothie_base_speed = 10 15 | endif 16 | 17 | if !exists('g:smoothie_break_on_reverse') 18 | "" 19 | " Stop immediately if we're moving and the user requested moving in opposite 20 | " direction. It's mostly useful at very low scrolling speeds, hence 21 | " disabled by default. 22 | let g:smoothie_break_on_reverse = 0 23 | endif 24 | 25 | "" 26 | " Execute {command}, but saving 'scroll' value before, and restoring it 27 | " afterwards. Useful for some commands (such as ^D or ^U), which overwrite 28 | " 'scroll' permanently if used with a [count]. 29 | function s:execute_preserving_scroll(command) 30 | let l:saved_scroll = &scroll 31 | execute a:command 32 | let &scroll = l:saved_scroll 33 | endfunction 34 | 35 | "" 36 | " Scroll the window up by one line, or move the cursor up if the window is 37 | " already at the top. Return 1 if cannot move any higher. 38 | function s:step_up() 39 | if line('.') > 1 40 | call s:execute_preserving_scroll("normal! 1\") 41 | return 0 42 | else 43 | return 1 44 | endif 45 | endfunction 46 | 47 | "" 48 | " Scroll the window down by one line, or move the cursor down if the window is 49 | " already at the bottom. Return 1 if cannot move any lower. 50 | function s:step_down() 51 | if line('.') < line('$') 52 | call s:execute_preserving_scroll("normal! 1\") 53 | return 0 54 | else 55 | return 1 56 | endif 57 | endfunction 58 | 59 | "" 60 | " Perform as many steps up or down to move {lines} lines from the starting 61 | " position (negative {lines} value means to go up). Return 1 if hit either 62 | " top or bottom, and cannot move further. 63 | function s:step_many(lines) 64 | let l:remaining_lines = a:lines 65 | while 1 66 | if l:remaining_lines < 0 67 | if s:step_up() 68 | return 1 69 | endif 70 | let l:remaining_lines += 1 71 | elseif l:remaining_lines > 0 72 | if s:step_down() 73 | return 1 74 | endif 75 | let l:remaining_lines -= 1 76 | else 77 | return 0 78 | endif 79 | endwhile 80 | endfunction 81 | 82 | "" 83 | " A Number indicating how many lines do we need yet to move down (or up, if 84 | " it's negative), to achieve what the user wants. 85 | let s:target_displacement = 0 86 | 87 | "" 88 | " A Float between -1.0 and 1.0 keeping our position between integral lines, 89 | " used to make the animation smoother. 90 | let s:subline_position = 0.0 91 | 92 | "" 93 | " Start the animation timer if not already running. Should be called when 94 | " updating the target, when there's a chance we're not already moving. 95 | function s:start_moving() 96 | if !exists('s:timer_id') 97 | let s:timer_id = timer_start(g:smoothie_update_interval, function("s:movement_tick"), {'repeat': -1}) 98 | endif 99 | endfunction 100 | 101 | "" 102 | " Stop any movement immediately, and disable the animation timer to conserve 103 | " power. 104 | function s:stop_moving() 105 | let s:target_displacement = 0 106 | let s:subline_position = 0.0 107 | if exists('s:timer_id') 108 | call timer_stop(s:timer_id) 109 | unlet s:timer_id 110 | endif 111 | endfunction 112 | 113 | "" 114 | " Calculate optimal movement velocity (in lines per second, negative value 115 | " means to move upwards) for the next animation frame. 116 | " 117 | " TODO: current algorithm is rather crude, would be good to research better 118 | " alternatives. 119 | function s:compute_velocity() 120 | return g:smoothie_base_speed * (s:target_displacement + s:subline_position) 121 | endfunction 122 | 123 | "" 124 | " Execute single animation frame. Called periodically by a timer. Accepts a 125 | " throwaway parameter: the timer ID. 126 | function s:movement_tick(_) 127 | if s:target_displacement == 0 128 | call s:stop_moving() 129 | return 130 | endif 131 | 132 | let l:subline_step_size = s:subline_position + (g:smoothie_update_interval/1000.0 * s:compute_velocity()) 133 | let l:step_size = float2nr(trunc(l:subline_step_size)) 134 | 135 | if abs(l:step_size) > abs(s:target_displacement) 136 | " clamp step size to prevent overshooting the target 137 | let l:step_size = s:target_displacement 138 | end 139 | 140 | if s:step_many(l:step_size) 141 | " we've collided with either buffer end 142 | call s:stop_moving() 143 | else 144 | let s:target_displacement -= l:step_size 145 | let s:subline_position = l:subline_step_size - l:step_size 146 | endif 147 | 148 | if l:step_size 149 | " Usually Vim handles redraws well on its own, but without explicit redraw 150 | " I've encountered some sporadic display artifacts. TODO: debug further. 151 | redraw 152 | endif 153 | endfunction 154 | 155 | "" 156 | " Set a new target where we should move to (in lines, relative to our current 157 | " position). If we're already moving, try to do the smart thing, taking into 158 | " account our progress in reaching the target set previously. 159 | function s:update_target(lines) 160 | if g:smoothie_break_on_reverse && s:target_displacement * a:lines < 0 161 | call s:stop_moving() 162 | else 163 | let s:target_displacement += a:lines 164 | call s:start_moving() 165 | endif 166 | endfunction 167 | 168 | "" 169 | " Helper function to set 'scroll' to [count], similarly to what native ^U and 170 | " ^D commands do. 171 | function s:count_to_scroll() 172 | if v:count 173 | let &scroll=v:count 174 | end 175 | endfunction 176 | 177 | "" 178 | " Smooth equivalent to ^D. 179 | function smoothie#downwards() 180 | call s:count_to_scroll() 181 | call s:update_target(&scroll) 182 | endfunction 183 | 184 | "" 185 | " Smooth equivalent to ^U. 186 | function smoothie#upwards() 187 | call s:count_to_scroll() 188 | call s:update_target(-&scroll) 189 | endfunction 190 | 191 | "" 192 | " Smooth equivalent to ^F. 193 | function smoothie#forwards() 194 | call s:update_target(winheight(0) * v:count1) 195 | endfunction 196 | 197 | "" 198 | " Smooth equivalent to ^B. 199 | function smoothie#backwards() 200 | call s:update_target(-winheight(0) * v:count1) 201 | endfunction 202 | --------------------------------------------------------------------------------