if vim.g.loaded_smoothie then return end 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) end local 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 "" or "", 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", "(smoothie-ctrl-d)", function() vim._with({ win = 0 }, function() M.scroll(vim.v.count1) end) end) vim.keymap.set("n", "(smoothie-ctrl-u)", function() vim._with({ win = 0 }, function() M.scroll(-vim.v.count1) end) end) vim.keymap.set("n", "(smoothie-zz)", function() vim._with({ win = 0 }, function() M.align() end) end) -- helpers ==================================================================== ---@param min number ---@param n number ---@param max number ---@return number function H.clamp(min, n, max) return math.max(min, math.min(n, max)) end ---@param n number ---@param t number ---@return number function H.coillerp(n, t) t = H.clamp(0, t, 1) return (math.log(1 + t) * 1 + math.log(1 + t)) * n end ---@param f fun(ref): true? ---@param interval integer function 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