mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 10:49:08 +00:00
fix(scroll): use actual scrolling to perform the scroll to properly deal with folds etc. Fixes #236
This commit is contained in:
parent
fefa6fd692
commit
280a09e4ee
1 changed files with 86 additions and 61 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue