···11+# 🧋 smoothie.nvim
22+33+> a tiny plugin for smoother movement keybinds
44+55+## requirements
66+77+- Neovim `>= 0.12`
88+99+## installation
1010+1111+install with `vim.pack`:
1212+1313+```lua
1414+vim.pack.add({ { src = "https://codeberg.org/comfysage/smoothie.nvim" } })
1515+```
1616+1717+## usage
1818+1919+smoothie currently on supports smoothing for the `ctrl-d` and `ctrl-u` keybinds.
2020+2121+```lua
2222+vim.keymap.set("n", "<c-d>", "<plug>(smoothie-ctrl-d)")
2323+vim.keymap.set("n", "<c-u>", "<plug>(smoothie-ctrl-u)")
2424+```
2525+2626+you can configure the smoothing settings:
2727+2828+```lua
2929+vim.g.smoothie_config = {
3030+ interval = 5,
3131+ maxtime = 100,
3232+}
3333+```
3434+3535+this plugin is inspired by [vim-smoothie](https://github.com/psliwka/vim-smoothie) :)
+115
plugin/smoothie.lua
···11+if vim.g.loaded_smoothie then
22+ return
33+end
44+55+vim.g.loaded_smoothie = true
66+77+local cfg = vim.g.smoothie_config or {}
88+local defaults = {
99+ interval = 5,
1010+ maxtime = 100,
1111+}
1212+1313+local opt = function(default, ...)
1414+ return vim.F.if_nil(vim.tbl_get(cfg, ...), default)
1515+end
1616+local opts = vim.defaulttable(function(key)
1717+ return opt(defaults[key], key)
1818+end)
1919+2020+local active = false
2121+2222+---@param min number
2323+---@param n number
2424+---@param max number
2525+---@return number
2626+local function clamp(min, n, max)
2727+ return math.max(min, math.min(n, max))
2828+end
2929+3030+---@param n number
3131+---@param t number
3232+---@return number
3333+local function coillerp(n, t)
3434+ t = clamp(0, t, 1)
3535+ return (math.log(1 + t) * 1 + math.log(1 + t)) * n
3636+end
3737+3838+local function scroll(vcount)
3939+ local win = vim.api.nvim_get_current_win()
4040+ -- distance to scroll
4141+ local d = vim.api.nvim_get_option_value("scroll", { scope = "local", win = win })
4242+4343+ local m = vcount / math.abs(vcount)
4444+4545+ local function step(start, dst, t)
4646+ local diff = dst.row - start.row
4747+4848+ -- reached dst or overshot
4949+ -- - diff == 0: no more distance to travel
5050+ -- - diff * m: both need to be oriented in the same direction (positive product)
5151+ if diff == 0 or diff * m < 0 then
5252+ return true
5353+ end
5454+5555+ -- travel t of the current diff (ceiled), at least 1
5656+ local s = m * math.max(1, math.ceil(coillerp(math.abs(diff), t)))
5757+ start.row = start.row + s
5858+5959+ vim._with({ win = win }, function()
6060+ vim.fn.setpos(".", { start.buf, start.row + 1, start.col, 0 })
6161+ end)
6262+ end
6363+6464+ -- on resume, returns whether still active
6565+ local repeater = coroutine.wrap(function(...)
6666+ while true do
6767+ if step(...) then
6868+ break
6969+ end
7070+ coroutine.yield(true)
7171+ end
7272+ active = false
7373+ end)
7474+7575+ coroutine.wrap(function()
7676+ if active then
7777+ vim.wait(1000, function()
7878+ return not active
7979+ end)
8080+ end
8181+8282+ active = true
8383+ local buf, lnum, col = unpack(vim.fn.getpos("."))
8484+ local start = vim.pos.cursor(buf, { lnum, col })
8585+ local dst = vim.pos.cursor(buf, { math.max(0, lnum + (vcount * d)), col })
8686+8787+ local now = vim.uv.now()
8888+8989+ while active do
9090+ local rel = vim.uv.now() - now
9191+ local done = -1
9292+ vim.defer_fn(function()
9393+ done = repeater(start, dst, rel / opts.maxtime)
9494+ end, opts.interval)
9595+ if not vim.wait(1000, function()
9696+ return done ~= -1
9797+ end, opts.interval) then
9898+ error("timeout")
9999+ end
100100+ vim.api.nvim__redraw({ win = win, valid = true, cursor = true })
101101+ vim.uv.update_time()
102102+ end
103103+ end)()
104104+end
105105+106106+vim.keymap.set("n", "<plug>(smoothie-ctrl-d)", function()
107107+ vim._with({ win = 0 }, function()
108108+ scroll(vim.v.count1)
109109+ end)
110110+end)
111111+vim.keymap.set("n", "<plug>(smoothie-ctrl-u)", function()
112112+ vim._with({ win = 0 }, function()
113113+ scroll(-vim.v.count1)
114114+ end)
115115+end)