Something went wrong. Try again.
🧋 a tiny plugin for smoother movement keybinds
Something went wrong. Try again.
5.6 kB · 230 lines
Lua
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231if vim.g.loaded_smoothie then returnend
vim.g.loaded_smoothie = true
local cfg = vim.g.smoothie_config or {}local defaults = { interval = 20, maxtime = 200,}
local opt = function(default, ...) return vim.F.if_nil(vim.tbl_get(cfg, ...), default)endlocal opts = vim.defaulttable(function(key) return opt(defaults[key], key)end)
local M, H = {}, {}Smoothie = vim.defaulttable(function(k) return M[k] or H[k]end)
local active = false
---@param win integer---@param step fun(win, ...): true? returns true if repeater should be stopped---@param init fun(): any[]function M.smooth(win, step, init) if active then vim.wait(1000, function() return not active end) end active = true
local args = init()
H.throttle(function(t) if not active then return t.cancel() end
if step(t.total_ms / opts.maxtime, unpack(args)) then active = false t.cancel() end
vim.api.nvim__redraw({ win = win, valid = true, cursor = true }) end, opts.interval)end
function M.scroll(vcount) local win = vim.api.nvim_get_current_win() -- distance to scroll local d = vim.api.nvim_get_option_value("scroll", { scope = "local", win = win })
local m = vcount / math.abs(vcount)
local function step(delta, start, dst) local diff = dst.row - start.row
-- reached dst or overshot -- - diff == 0: no more distance to travel -- - diff * m: both need to be oriented in the same direction (positive product) if diff == 0 or diff * m < 0 then return true end
-- travel t of the current diff (ceiled), at least 1 local s = m * math.max(1, math.ceil(H.coillerp(math.abs(diff), delta))) start.row = start.row + s
vim._with({ win = win }, function() vim.fn.setpos(".", { start.buf, start.row + 1, start.col, 0 }) end) end
M.smooth(win, step, function() local buf, lnum, col = unpack(vim.fn.getpos(".")) local start = vim.pos.cursor(buf, { lnum, col }) local dst = vim.pos.cursor(buf, { math.max(0, math.ceil(lnum + (vcount * d))), col }) return { start, dst } end)end
function M.align() local win = vim.api.nvim_get_current_win()
local function step(delta, m, start, dst) local diff = dst.row - start.row
-- reached dst or overshot -- - diff == 0: no more distance to travel -- - diff * m: both need to be oriented in the same direction (positive product) if diff == 0 or diff * m < 0 then return true end
-- travel t of the current diff (ceiled), at least 1 local s = math.max(1, math.ceil(H.coillerp(math.abs(diff), delta))) start.row = start.row + (m * s)
vim.schedule(function() vim._with({ win = win }, function() for _ = 1, s do local key = vim.api.nvim_replace_termcodes(m > 0 and "<c-y>" or "<c-e>", true, false, true) vim.api.nvim_feedkeys(key, "n", false) end end) end) end
M.smooth(win, step, function() local buf, lnum, col = unpack(vim.fn.getpos(".")) local start = vim.pos.cursor(buf, { lnum, col })
local m local wfirst = vim.fn.getpos("w0")[2] local wlast = vim.fn.getpos("w$")[2] -- no lines are visible if wlast < wfirst then m = 0 end local wheight = vim.api.nvim_win_get_height(win) local wdst = math.min(wlast, math.floor(wfirst + wheight / 2)) local dst = vim.pos.cursor(buf, { wdst, col }) local diff = dst.row - start.row m = m or diff / math.abs(diff)
return { m, start, dst } end)end
vim.keymap.set("n", "<plug>(smoothie-ctrl-d)", function() vim._with({ win = 0 }, function() M.scroll(vim.v.count1) end)end)vim.keymap.set("n", "<plug>(smoothie-ctrl-u)", function() vim._with({ win = 0 }, function() M.scroll(-vim.v.count1) end)end)vim.keymap.set("n", "<plug>(smoothie-zz)", function() vim._with({ win = 0 }, function() M.align() end)end)
-- helpers ====================================================================
---@param min number---@param n number---@param max number---@return numberfunction H.clamp(min, n, max) return math.max(min, math.min(n, max))end
---@param n number---@param t number---@return numberfunction H.coillerp(n, t) t = H.clamp(0, t, 1) return (math.log(1 + t) * 1 + math.log(1 + t)) * nend
---@param f fun(ref): true?---@param interval integerfunction H.throttle(f, interval) vim.validate("f", f, "function") vim.validate("interval", interval, "number")
local repeater = vim.uv.new_timer() if not repeater then return end
local now = vim.uv.now() local timer = { now = now, --- last tick tick = now, ntick = nil, total_ms = 0, cancel = function() if repeater ~= nil and not repeater:is_closing() then repeater:stop() repeater:close() return end end, }
local ready = true repeater:start( 0, interval, vim.schedule_wrap(function() vim.uv.update_time()
local ntick = vim.uv.now() vim.wait(interval, function() return ready end, 5, true) local wait_time = vim.uv.now() - ntick
--- next tick timer.ntick = ntick -- total time since timer start timer.total_ms = timer.ntick - timer.now -- delta since last tick timer.delta = timer.ntick - timer.tick
ready = false local timeout = math.max(0, interval - wait_time) vim.defer_fn(function() if f(timer) then timer.cancel() end
ready = true end, timeout)
timer.tick = timer.ntick end) )end