馃 a tiny plugin for smoother movement keybinds
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 230 lines 5.6 kB view raw
1if vim.g.loaded_smoothie then 2 return 3end 4 5vim.g.loaded_smoothie = true 6 7local cfg = vim.g.smoothie_config or {} 8local defaults = { 9 interval = 20, 10 maxtime = 200, 11} 12 13local opt = function(default, ...) 14 return vim.F.if_nil(vim.tbl_get(cfg, ...), default) 15end 16local opts = vim.defaulttable(function(key) 17 return opt(defaults[key], key) 18end) 19 20local M, H = {}, {} 21Smoothie = vim.defaulttable(function(k) 22 return M[k] or H[k] 23end) 24 25local active = false 26 27---@param win integer 28---@param step fun(win, ...): true? returns true if repeater should be stopped 29---@param init fun(): any[] 30function M.smooth(win, step, init) 31 if active then 32 vim.wait(1000, function() 33 return not active 34 end) 35 end 36 active = true 37 38 local args = init() 39 40 H.throttle(function(t) 41 if not active then 42 return t.cancel() 43 end 44 45 if step(t.total_ms / opts.maxtime, unpack(args)) then 46 active = false 47 t.cancel() 48 end 49 50 vim.api.nvim__redraw({ win = win, valid = true, cursor = true }) 51 end, opts.interval) 52end 53 54function M.scroll(vcount) 55 local win = vim.api.nvim_get_current_win() 56 -- distance to scroll 57 local d = vim.api.nvim_get_option_value("scroll", { scope = "local", win = win }) 58 59 local m = vcount / math.abs(vcount) 60 61 local function step(delta, start, dst) 62 local diff = dst.row - start.row 63 64 -- reached dst or overshot 65 -- - diff == 0: no more distance to travel 66 -- - diff * m: both need to be oriented in the same direction (positive product) 67 if diff == 0 or diff * m < 0 then 68 return true 69 end 70 71 -- travel t of the current diff (ceiled), at least 1 72 local s = m * math.max(1, math.ceil(H.coillerp(math.abs(diff), delta))) 73 start.row = start.row + s 74 75 vim._with({ win = win }, function() 76 vim.fn.setpos(".", { start.buf, start.row + 1, start.col, 0 }) 77 end) 78 end 79 80 M.smooth(win, step, function() 81 local buf, lnum, col = unpack(vim.fn.getpos(".")) 82 local start = vim.pos.cursor(buf, { lnum, col }) 83 local dst = vim.pos.cursor(buf, { math.max(0, math.ceil(lnum + (vcount * d))), col }) 84 return { start, dst } 85 end) 86end 87 88function M.align() 89 local win = vim.api.nvim_get_current_win() 90 91 local function step(delta, m, start, dst) 92 local diff = dst.row - start.row 93 94 -- reached dst or overshot 95 -- - diff == 0: no more distance to travel 96 -- - diff * m: both need to be oriented in the same direction (positive product) 97 if diff == 0 or diff * m < 0 then 98 return true 99 end 100 101 -- travel t of the current diff (ceiled), at least 1 102 local s = math.max(1, math.ceil(H.coillerp(math.abs(diff), delta))) 103 start.row = start.row + (m * s) 104 105 vim.schedule(function() 106 vim._with({ win = win }, function() 107 for _ = 1, s do 108 local key = vim.api.nvim_replace_termcodes(m > 0 and "<c-y>" or "<c-e>", true, false, true) 109 vim.api.nvim_feedkeys(key, "n", false) 110 end 111 end) 112 end) 113 end 114 115 M.smooth(win, step, function() 116 local buf, lnum, col = unpack(vim.fn.getpos(".")) 117 local start = vim.pos.cursor(buf, { lnum, col }) 118 119 local m 120 local wfirst = vim.fn.getpos("w0")[2] 121 local wlast = vim.fn.getpos("w$")[2] 122 -- no lines are visible 123 if wlast < wfirst then 124 m = 0 125 end 126 local wheight = vim.api.nvim_win_get_height(win) 127 local wdst = math.min(wlast, math.floor(wfirst + wheight / 2)) 128 local dst = vim.pos.cursor(buf, { wdst, col }) 129 local diff = dst.row - start.row 130 m = m or diff / math.abs(diff) 131 132 return { m, start, dst } 133 end) 134end 135 136vim.keymap.set("n", "<plug>(smoothie-ctrl-d)", function() 137 vim._with({ win = 0 }, function() 138 M.scroll(vim.v.count1) 139 end) 140end) 141vim.keymap.set("n", "<plug>(smoothie-ctrl-u)", function() 142 vim._with({ win = 0 }, function() 143 M.scroll(-vim.v.count1) 144 end) 145end) 146vim.keymap.set("n", "<plug>(smoothie-zz)", function() 147 vim._with({ win = 0 }, function() 148 M.align() 149 end) 150end) 151 152-- helpers ==================================================================== 153 154---@param min number 155---@param n number 156---@param max number 157---@return number 158function H.clamp(min, n, max) 159 return math.max(min, math.min(n, max)) 160end 161 162---@param n number 163---@param t number 164---@return number 165function H.coillerp(n, t) 166 t = H.clamp(0, t, 1) 167 return (math.log(1 + t) * 1 + math.log(1 + t)) * n 168end 169 170---@param f fun(ref): true? 171---@param interval integer 172function H.throttle(f, interval) 173 vim.validate("f", f, "function") 174 vim.validate("interval", interval, "number") 175 176 local repeater = vim.uv.new_timer() 177 if not repeater then 178 return 179 end 180 181 local now = vim.uv.now() 182 local timer = { 183 now = now, 184 --- last tick 185 tick = now, 186 ntick = nil, 187 total_ms = 0, 188 cancel = function() 189 if repeater ~= nil and not repeater:is_closing() then 190 repeater:stop() 191 repeater:close() 192 return 193 end 194 end, 195 } 196 197 local ready = true 198 repeater:start( 199 0, 200 interval, 201 vim.schedule_wrap(function() 202 vim.uv.update_time() 203 204 local ntick = vim.uv.now() 205 vim.wait(interval, function() 206 return ready 207 end, 5, true) 208 local wait_time = vim.uv.now() - ntick 209 210 --- next tick 211 timer.ntick = ntick 212 -- total time since timer start 213 timer.total_ms = timer.ntick - timer.now 214 -- delta since last tick 215 timer.delta = timer.ntick - timer.tick 216 217 ready = false 218 local timeout = math.max(0, interval - wait_time) 219 vim.defer_fn(function() 220 if f(timer) then 221 timer.cancel() 222 end 223 224 ready = true 225 end, timeout) 226 227 timer.tick = timer.ntick 228 end) 229 ) 230end