Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements, issue fixes #21

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Piotr Śliwka
Copyright (c) 2019 Piotr Śliwka, Subhaditya Nath

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
vim-smoothie: Smooth scrolling for Vim done right🥤
vim-smoothie: Smooth scrolling for Vim done right :cup_with_straw:
===================================================

This (neo)vim plugin makes scrolling nice and _smooth_. Find yourself
Expand Down Expand Up @@ -30,6 +30,23 @@ adjusting one or more of the following variables in your `vimrc`:
supposed to bind keys you like by yourself. See `plugin/smoothie.vim` to
discover available mappings.

* `g:smoothie_disabled`: Disable vim-smoothie. Useful for extremely slow
connections.

* `g:smoothie_base_speed`: Controls the speed. Set to `10` by default. To
go slower, reduce the value. To go faster, increase the value.

* `g:smoothie_update_interval`: Time interval (in milliseconds) between
successive screen updates. Use a smaller value to get smoother
animations, or use a bigger value if you are on a slow connection.

* `g:smoothie_break_on_reverse`: If true, then scrolling will stop
immediately when a command is given to scroll opposite to the current
direction.

* `g:smoothie_bell_enabled`: If true, then a bell will be rang if the
scroll commmand being issued is invalid (i.e. we have reached boundary)

Alternatives, a.k.a. why create yet another plugin
--------------------------------------------------

Expand Down
175 changes: 173 additions & 2 deletions autoload/smoothie.vim
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
if exists('g:loaded_smoothie')
finish
endif
let g:loaded_smoothie = 1
subnut marked this conversation as resolved.
Show resolved Hide resolved

if !has('float') || !has('timers')
let g:smoothie_disabled = 1
echohl WarningMsg
echom 'vim-smoothie needs +timers and +float'
echohl None
finish
endif

""
" This variable is used to inform the s:step_*() functions about whether the
" current movement is a cursor movement or a scroll movement. Used for
" motions like gg and G
let s:cursor_movement = v:false

""
" This variable is needed to let the s:step_down() function know whether to
" continue scrolling after reaching EOL (as in ^F) or not (^B, ^D, ^U, etc.)
"
" NOTE: This variable "MUST" be set to v:false in "every" function that
" invokes motion (except smoothie#forwards, where it must be set to v:true)
let s:ctrl_f_invoked = v:false

if !exists('g:smoothie_disabled')
subnut marked this conversation as resolved.
Show resolved Hide resolved
""
" Disable vim-smoothie. Useful for very slow connections.
let g:smoothie_disabled = 0
endif

if !exists('g:smoothie_update_interval')
""
" Time (in milliseconds) between subseqent screen/cursor postion updates.
Expand All @@ -22,21 +55,40 @@ if !exists('g:smoothie_break_on_reverse')
let g:smoothie_break_on_reverse = 0
endif

if !exists('g:smoothie_bell_enabled')
""
" Enable beeping when either end of the buffer has been reached, and we
" cannot proceed any further. Default value is taken from &belloff
let g:smoothie_bell_enabled = !(&belloff =~# 'all\|error')
endif

""
" Execute {command}, but saving 'scroll' value before, and restoring it
" afterwards. Useful for some commands (such as ^D or ^U), which overwrite
" 'scroll' permanently if used with a [count].
function s:execute_preserving_scroll(command)
let l:saved_scroll = &scroll
let l:saved_scrolloff = 0
if &scrolloff
let l:saved_scrolloff = &scrolloff
let &scrolloff = 0
endif
execute a:command
let &scroll = l:saved_scroll
if l:saved_scrolloff
let &scrolloff = l:saved_scrolloff
endif
endfunction

""
" Scroll the window up by one line, or move the cursor up if the window is
" already at the top. Return 1 if cannot move any higher.
function s:step_up()
if line('.') > 1
if s:cursor_movement
exe 'normal! k'
return 0
endif
call s:execute_preserving_scroll("normal! 1\<C-U>")
return 0
else
Expand All @@ -48,9 +100,33 @@ endfunction
" Scroll the window down by one line, or move the cursor down if the window is
" already at the bottom. Return 1 if cannot move any lower.
function s:step_down()

if line('.') < line('$')
if s:cursor_movement
exe 'normal! j'
return 0
endif
" NOTE: the three lines of code following this comment block
" have been implemented as a temporary workaround for a vim issue
subnut marked this conversation as resolved.
Show resolved Hide resolved
" regarding Ctrl-D and folds.
" See: psliwka/vim-smoothie#20
if foldclosedend('.') != -1
call cursor(foldclosedend('.'), col('.'))
endif
call s:execute_preserving_scroll("normal! 1\<C-D>")
if s:ctrl_f_invoked && (winheight(0) - winline()) >= (line('$') - line('.'))
" ^F is pressed, and the last line of the buffer is visible
" scroll window to keep cursor position fixed
call s:execute_preserving_scroll("normal! \<C-E>")
endif
return 0

elseif s:ctrl_f_invoked && winline() > 1
" cursor is already on last line of buffer, but not on last line of window
" ^F can scroll more
call s:execute_preserving_scroll("normal! \<C-E>")
return 0

else
return 1
endif
Expand All @@ -62,6 +138,7 @@ endfunction
" top or bottom, and cannot move further.
function s:step_many(lines)
let l:remaining_lines = a:lines
let s:moved_once = v:false " we haven't moved yet
while 1
if l:remaining_lines < 0
if s:step_up()
Expand All @@ -76,6 +153,7 @@ function s:step_many(lines)
else
return 0
endif
let s:moved_once = v:true " current command caused us to move atleast once
endwhile
endfunction

Expand All @@ -94,7 +172,7 @@ let s:subline_position = 0.0
" updating the target, when there's a chance we're not already moving.
function s:start_moving()
if !exists('s:timer_id')
let s:timer_id = timer_start(g:smoothie_update_interval, function("s:movement_tick"), {'repeat': -1})
let s:timer_id = timer_start(g:smoothie_update_interval, function('s:movement_tick'), {'repeat': -1})
endif
endfunction

Expand All @@ -117,7 +195,11 @@ endfunction
" TODO: current algorithm is rather crude, would be good to research better
" alternatives.
function s:compute_velocity()
return g:smoothie_base_speed * (s:target_displacement + s:subline_position)
" return g:smoothie_base_speed * (s:target_displacement + s:subline_position)
let l:foo = g:smoothie_base_speed * (s:target_displacement + s:subline_position)
let l:abs = abs(l:foo)
let l:sgn = l:foo / l:abs
return pow(l:abs,0.9)*l:sgn
endfunction

""
Expand All @@ -140,6 +222,16 @@ function s:movement_tick(_)
if s:step_many(l:step_size)
" we've collided with either buffer end
call s:stop_moving()
if !s:moved_once
" i.e. we haven't moved at all. The command was invalid. BELL!
if g:smoothie_bell_enabled
let l:belloff = &belloff
set belloff=
exe "normal \<Esc>"
let &belloff = l:belloff
endif
unlet s:moved_once
endif
else
let s:target_displacement -= l:step_size
let s:subline_position = l:subline_step_size - l:step_size
Expand All @@ -160,6 +252,12 @@ function s:update_target(lines)
if g:smoothie_break_on_reverse && s:target_displacement * a:lines < 0
call s:stop_moving()
else
" Cursor movements are very delicate. Since the displacement for cursor
" movements is calulated from the "current" line, so immediately stop
" moving, otherwise we will end up at the wrong line.
if s:cursor_movement
call s:stop_moving()
endif
let s:target_displacement += a:lines
call s:start_moving()
endif
Expand All @@ -177,25 +275,98 @@ endfunction
""
" Smooth equivalent to ^D.
function smoothie#downwards()
if g:smoothie_disabled
exe "normal! \<C-d>"
return
endif
let s:ctrl_f_invoked = v:false
call s:count_to_scroll()
call s:update_target(&scroll)
endfunction

""
" Smooth equivalent to ^U.
function smoothie#upwards()
if g:smoothie_disabled
exe "normal! \<C-u>"
return
endif
let s:ctrl_f_invoked = v:false
call s:count_to_scroll()
call s:update_target(-&scroll)
endfunction

""
" Smooth equivalent to ^F.
function smoothie#forwards()
if g:smoothie_disabled
exe "normal! \<C-f>"
return
endif
let s:ctrl_f_invoked = v:true
call s:update_target(winheight(0) * v:count1)
endfunction

""
" Smooth equivalent to ^B.
function smoothie#backwards()
if g:smoothie_disabled
exe "normal! \<C-b>"
return
endif
let s:ctrl_f_invoked = v:false
call s:update_target(-winheight(0) * v:count1)
endfunction

""
" Smoothie equivalent to gg.
function smoothie#gg()
let s:cursor_movement = v:true
let s:ctrl_f_invoked = v:false
if g:smoothie_disabled || mode(1) =~# 'o' && mode(1) =~? 'no'
" If in operator pending mode, disable vim-smoothie and force the movement
" to be line-wise, because gg was originally linewise.
" Uses the normal non-smooth version of gg.
exe 'normal! ' . (mode(1) ==# 'no' ? 'V' : '') . v:count . 'gg'
return
endif
" gg behaves like a jump-command so, append current position to the jumplist
" but before that, set the target, because v:count and v:count1 will be lost
let l:target = v:count1
execute "normal! m'"
call s:update_target(l:target - line('.'))
" suspend further commands till the destination is reached
" see point (3) of https://github.com/psliwka/vim-smoothie/issues/1#issuecomment-560158642
while line('.') != l:target
exe 'sleep ' . g:smoothie_update_interval . ' m'
endwhile
let s:cursor_movement = v:false " reset s:cursor_movement to false
if &startofline " :help 'startofline'
call cursor(line('.'), match(getline('.'),'\S')+1)
endif
endfunction

""
" Smoothie equivalent to G.
function smoothie#G()
" absolutely similar to smoothie#gg()
" refer to that for explanation of steps
let s:cursor_movement = v:true
let s:ctrl_f_invoked = v:false
if g:smoothie_disabled || mode(1) =~# 'o' && mode(1) =~? 'no'
exe 'normal! ' . (mode(1) ==# 'no' ? 'V' : '') . v:count . 'G'
return
endif
let l:target = (v:count ? v:count : line('$'))
execute "normal! m'"
call s:update_target(l:target - line('.'))
while line('.') != l:target
exe 'sleep ' . g:smoothie_update_interval . ' m'
endwhile
let s:cursor_movement = v:false
if &startofline
call cursor(line('.'), match(getline('.'),'\S')+1)
endif
endfunction

" vim: et ts=2
50 changes: 37 additions & 13 deletions plugin/smoothie.vim
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
nnoremap <silent> <Plug>(SmoothieDownwards) :<C-U>call smoothie#downwards() <CR>
nnoremap <silent> <Plug>(SmoothieUpwards) :<C-U>call smoothie#upwards() <CR>
nnoremap <silent> <Plug>(SmoothieForwards) :<C-U>call smoothie#forwards() <CR>
nnoremap <silent> <Plug>(SmoothieBackwards) :<C-U>call smoothie#backwards() <CR>
if has('nvim') || has('patch-8.2.1978')
subnut marked this conversation as resolved.
Show resolved Hide resolved
noremap <silent> <Plug>(SmoothieDownwards) <cmd>call smoothie#downwards() <CR>
noremap <silent> <Plug>(SmoothieUpwards) <cmd>call smoothie#upwards() <CR>
noremap <silent> <Plug>(SmoothieForwards) <cmd>call smoothie#forwards() <CR>
noremap <silent> <Plug>(SmoothieBackwards) <cmd>call smoothie#backwards() <CR>
noremap <silent> <Plug>(Smoothie_gg) <cmd>call smoothie#gg() <CR>
noremap <silent> <Plug>(Smoothie_G) <cmd>call smoothie#G() <CR>

if !get(g:, 'smoothie_no_default_mappings', v:false)
silent! nmap <unique> <C-D> <Plug>(SmoothieDownwards)
silent! nmap <unique> <C-U> <Plug>(SmoothieUpwards)
silent! nmap <unique> <C-F> <Plug>(SmoothieForwards)
silent! nmap <unique> <S-Down> <Plug>(SmoothieForwards)
silent! nmap <unique> <PageDown> <Plug>(SmoothieForwards)
silent! nmap <unique> <C-B> <Plug>(SmoothieBackwards)
silent! nmap <unique> <S-Up> <Plug>(SmoothieBackwards)
silent! nmap <unique> <PageUp> <Plug>(SmoothieBackwards)
if !get(g:, 'smoothie_no_default_mappings', v:false)
silent! map <unique> <C-D> <Plug>(SmoothieDownwards)
silent! map <unique> <C-U> <Plug>(SmoothieUpwards)
silent! map <unique> <C-F> <Plug>(SmoothieForwards)
silent! map <unique> <S-Down> <Plug>(SmoothieForwards)
silent! map <unique> <PageDown> <Plug>(SmoothieForwards)
silent! map <unique> <C-B> <Plug>(SmoothieBackwards)
silent! map <unique> <S-Up> <Plug>(SmoothieBackwards)
silent! map <unique> <PageUp> <Plug>(SmoothieBackwards)
endif
else
nnoremap <silent> <Plug>(SmoothieDownwards) :<C-U>call smoothie#downwards() <CR>
nnoremap <silent> <Plug>(SmoothieUpwards) :<C-U>call smoothie#upwards() <CR>
nnoremap <silent> <Plug>(SmoothieForwards) :<C-U>call smoothie#forwards() <CR>
nnoremap <silent> <Plug>(SmoothieBackwards) :<C-U>call smoothie#backwards() <CR>
nnoremap <silent> <Plug>(Smoothie_gg) :<C-U>call smoothie#gg() <CR>
nnoremap <silent> <Plug>(Smoothie_G) :<C-U>call smoothie#G() <CR>

if !get(g:, 'smoothie_no_default_mappings', v:false)
silent! nmap <unique> <C-D> <Plug>(SmoothieDownwards)
silent! nmap <unique> <C-U> <Plug>(SmoothieUpwards)
silent! nmap <unique> <C-F> <Plug>(SmoothieForwards)
silent! nmap <unique> <S-Down> <Plug>(SmoothieForwards)
silent! nmap <unique> <PageDown> <Plug>(SmoothieForwards)
silent! nmap <unique> <C-B> <Plug>(SmoothieBackwards)
silent! nmap <unique> <S-Up> <Plug>(SmoothieBackwards)
silent! nmap <unique> <PageUp> <Plug>(SmoothieBackwards)
endif
endif

" vim: et ts=2