fix(scroll): use actual scrolling to perform the scroll to properly deal with folds etc. Fixes #236

This commit is contained in:
Folke Lemaitre 2024-12-11 09:54:39 +01:00
parent fefa6fd692
commit 280a09e4ee
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040

View file

@ -9,15 +9,13 @@ M.meta = {
---@alias snacks.scroll.View {topline:number, lnum:number}
---@class snacks.scroll.State
---@field anim? snacks.animate.Animation
---@field win number
---@field buf number
---@field view snacks.scroll.View
---@field current snacks.scroll.View
---@field target snacks.scroll.View
---@field cursor number[]
---@field view vim.fn.winsaveview.ret
---@field current vim.fn.winsaveview.ret
---@field target vim.fn.winsaveview.ret
---@field scrolloff number
---@field mousescroll number
---@field height number
---@field virtualedit? string
---@class snacks.scroll.Config
@ -34,12 +32,14 @@ local defaults = {
debug = false,
}
local SCROLL_UP, SCROLL_DOWN = vim.keycode("<c-e>"), vim.keycode("<c-y>")
local SCROLL_WHEEL_DOWN, SCROLL_WHEEL_UP = vim.keycode("<ScrollWheelDown>"), vim.keycode("<ScrollWheelUp>")
local mouse_scrolling = false
M.enabled = false
local states = {} ---@type table<number, snacks.scroll.State>
local stats = { targets = 0, animating = 0, reset = 0, skipped = 0 }
local stats = { targets = 0, animating = 0, reset = 0, skipped = 0, mousescroll = 0 }
local config = Snacks.config.get("scroll", defaults)
local debug_timer = assert((vim.uv or vim.loop).new_timer())
@ -51,7 +51,6 @@ local function get_state(win)
return
end
local view = vim.api.nvim_win_call(win, vim.fn.winsaveview) ---@type vim.fn.winsaveview.ret
view = { topline = view.topline, lnum = view.lnum } --[[@as snacks.scroll.View]]
if not (states[win] and states[win].buf == buf) then
---@diagnostic disable-next-line: missing-fields
states[win] = {
@ -62,8 +61,6 @@ local function get_state(win)
}
end
states[win].scrolloff = vim.wo[win].scrolloff
states[win].mousescroll = tonumber(vim.o.mousescroll:match("ver:(%d+)")) or 1
states[win].height = vim.api.nvim_win_get_height(win)
states[win].view = view
return states[win]
end
@ -91,6 +88,7 @@ function M.enable()
mouse_scrolling = true
end
end)
-- initialize state for buffers entering windows
vim.api.nvim_create_autocmd("BufWinEnter", {
group = group,
@ -107,7 +105,9 @@ function M.enable()
callback = vim.schedule_wrap(function(ev)
for _, win in ipairs(vim.fn.win_findbuf(ev.buf)) do
if states[win] then
states[win].current.lnum = vim.api.nvim_win_get_cursor(win)[1]
local cursor = vim.api.nvim_win_get_cursor(win)
states[win].current.lnum = cursor[1]
states[win].current.col = cursor[2]
end
end
end),
@ -136,42 +136,26 @@ function M.disable()
vim.api.nvim_del_augroup_by_name("snacks_scroll")
end
--- Update the window state
---@param state snacks.scroll.State
---@param changes? {topline?:number, lnum?:number}
local function update(state, changes)
if not vim.api.nvim_win_is_valid(state.win) then
return
---@param amount number
local function scroll(amount)
if amount ~= 0 then
vim.cmd(("normal! %d%s"):format(math.abs(amount), amount < 0 and SCROLL_UP or SCROLL_DOWN))
end
end
if changes then
state.current = vim.tbl_extend("force", state.current, changes)
---@param from number
---@param to number
local function visible_lines(from, to)
from, to = math.min(from, to), math.max(from, to)
local ret = 0
while from < to do
local fold_end = vim.fn.foldclosedend(from)
ret = ret + (fold_end == -1 and 1 or 0)
from = fold_end == -1 and from + 1 or fold_end + 1
end
return ret
end
local done = state.target.topline == state.current.topline
-- adjust lnum for scrolloff when not at target topline
if done then
state.current.lnum = state.target.lnum
else
state.current.lnum = math.max(
state.current.topline + state.scrolloff,
math.min(state.current.lnum, state.current.topline + state.height - 1 - state.scrolloff)
)
end
if changes then
stats.animating = stats.animating + 1
else
stats.reset = stats.reset + 1
end
-- apply the changes
vim.api.nvim_win_call(state.win, function()
vim.fn.winrestview(state.current)
end)
if done then
vim.api.nvim_win_set_cursor(state.win, state.cursor)
---@param state snacks.scroll.State
---@param value? string
local function virtualedit(state, value)
@ -201,36 +185,77 @@ function M.check(win)
-- if delta is 0, then we're animating.
-- also skip if the difference is less than the mousescroll value,
-- since most terminals support smooth mouse scrolling.
if math.abs(state.view.topline - state.current.topline) <= state.mousescroll then
if state.view.topline == state.current.topline then
stats.skipped = stats.skipped + 1
state.current = vim.deepcopy(state.view)
return
elseif mouse_scrolling then
if state.anim then
state.anim:stop()
state.anim = nil
virtualedit(state) -- restore virtualedit
end
mouse_scrolling = false
stats.mousescroll = stats.mousescroll + 1
state.current = vim.deepcopy(state.view)
return
end
-- new target
stats.targets = stats.targets + 1
state.target = vim.deepcopy(state.view)
state.cursor = vim.api.nvim_win_get_cursor(win)
update(state) -- reset to current state
-- animate topline/lnum to target
for _, field in ipairs({ "topline", "lnum" }) do
Snacks.animate(
state.current[field],
state.target[field],
function(value)
update(state, { [field] = value })
end,
vim.tbl_extend("keep", {
int = true,
id = ("scroll_%s_%d"):format(field, win),
}, config.animate)
)
end
virtualedit(state, "all")
local scrolls = 0
vim.api.nvim_win_call(state.win, function()
-- reset to current state
vim.fn.winrestview(state.current)
state.current = vim.fn.winsaveview()
-- calculate the amount of lines to scroll, taking folds into account
scrolls = visible_lines(state.current.topline, state.target.topline)
scrolls = scrolls * (state.target.topline > state.current.topline and -1 or 1)
end)
local from_lnum = state.current.lnum
local from_virtcol = vim.fn.virtcol({ state.current.lnum, state.current.col }, false, win)
local to_virtcol = vim.fn.virtcol({ state.target.lnum, state.target.col }, false, win)
state.anim = Snacks.animate(0, scrolls, function(value, ctx)
vim.api.nvim_win_call(win, function()
scroll(value - ctx.prev)
if ctx.done then
vim.fn.winrestview(state.target)
state.current = vim.fn.winsaveview()
virtualedit(state) -- restore virtualedit
return
end
local info = vim.fn.getwininfo(state.win)[1]
if state.scrolloff < (info.botline - info.topline) / 2 then
local lnum = math.floor(from_lnum + (state.target.lnum - from_lnum) * value / scrolls + 0.5)
-- adjust for scrolloff
local top = info.topline == 1 and 1 or info.topline + state.scrolloff
local bot = info.botline == info.height and info.height or info.botline - state.scrolloff
lnum = math.max(top, math.min(lnum, bot))
-- only move the cursor when the line is visible
if vim.fn.foldclosed(lnum) == -1 then
local virtcol = math.floor(from_virtcol + (to_virtcol - from_virtcol) * value / scrolls + 0.5)
pcall(vim.api.nvim_win_set_cursor, state.win, { lnum, virtcol })
end
end
local old = state.current
state.current = vim.fn.winsaveview()
-- this should never happen, but just in case
if state.current.topline ~= info.topline then
state.current = old
vim.fn.winrestview(state.current)
end
end)
end, vim.tbl_extend("keep", { int = true, id = ("scroll_%d"):format(win) }, config.animate))
end
---@private