local Dir = { UP = 1, DOWN = 2, } local function smooth_scroll(dir) local scroll_at_boundary, cursor_at_boundary if dir == Dir.DOWN then scroll_at_boundary = function() return vim.fn.line("w$") == vim.api.nvim_buf_line_count(0) end cursor_at_boundary = function() return vim.fn.line(".") == vim.api.nvim_buf_line_count(0) end else scroll_at_boundary = function() return vim.fn.line("w0") == 1 end cursor_at_boundary = function() return vim.fn.line(".") == 1 end end -- bail only when the cursor itself can't move further; at the viewport -- boundary / no-op but we still want j/k to walk the cursor if cursor_at_boundary() then return end local win_height = vim.api.nvim_win_get_height(0) local duration, sleep_duration = 100, 16 -- 60 fps local distance = math.floor(win_height / 2) local steps_number = math.ceil(duration / sleep_duration) -- We want a movement every `sleep_duration` msec local step = distance / steps_number local scroll_keys = dir == Dir.DOWN and "\\j" or "\\k" local cursor_keys = dir == Dir.DOWN and "j" or "k" local prev = 0 for i = 1, steps_number do local cur = math.ceil(step * i) local delta = cur - prev prev = cur vim.defer_fn(function() for _ = 1, delta do if cursor_at_boundary() then return end -- past the viewport boundary / no-op; just walk the cursor local keys = scroll_at_boundary() and cursor_keys or scroll_keys vim.cmd('exec "normal! ' .. keys .. '"') end end, sleep_duration * i) end end vim.api.nvim_create_user_command("ScrollSmoothDown", function() smooth_scroll(Dir.DOWN) end, { desc = "Scroll down smoothly" }) vim.api.nvim_create_user_command("ScrollSmoothUp", function() smooth_scroll(Dir.UP) end, { desc = "Scroll up smoothly" })