From 41975e38170954a4c2c2bde099c93a4f8f7a9169 Mon Sep 17 00:00:00 2001 From: delphinus Date: Mon, 26 Oct 2026 16:43:14 +0900 Subject: [PATCH] fix(preview): do not scroll a float past the end of the document The float preview centers the line under the source cursor. When the whole document fit the window, that still scrolled it: with the cursor near the end, the first lines, the heading included, went out of view and the bottom rows were left empty. The topline is now capped so the last line reaches no higher than the bottom of the window. Every line takes at least one row, so the window stays filled when lines wrap. Co-Authored-By: Claude Opus 5.5 (1M context) --- lua/md-render/preview.lua | 4 +++ tests/preview_scroll_test.lua | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/preview_scroll_test.lua diff --git a/lua/md-render/preview.lua b/lua/md-render/preview.lua index 0c35420..476430e 100644 --- a/lua/md-render/preview.lua +++ b/lua/md-render/preview.lua @@ -521,6 +521,10 @@ function Session:scroll_to_source_line(source_cursor_line) target = math.max(1, math.min(target, buf_lines)) local win_height = vim.api.nvim_win_get_height(self.win) local top = math.max(0, target - 1 - math.floor(win_height / 2)) + -- Centering must not scroll past the end: a document that fits the window + -- would lose its first lines and leave the bottom rows empty. Each line + -- takes at least one row, so this still fills the window when lines wrap. + top = math.min(top, math.max(0, buf_lines - win_height)) vim.api.nvim_win_call(self.win, function() vim.fn.winrestview { topline = top + 1 } end) diff --git a/tests/preview_scroll_test.lua b/tests/preview_scroll_test.lua new file mode 100644 index 0000000..2ef9ca5 --- /dev/null +++ b/tests/preview_scroll_test.lua @@ -0,0 +1,66 @@ +-- Test where the floating preview opens. It centers the line under the +-- source cursor, but must not scroll past the end: a document that fits the +-- window shows from its first line. +-- Run: nvim --headless -u NONE --noplugin -l tests/preview_scroll_test.lua + +package.path = vim.fn.getcwd() .. "/lua/?.lua;" .. vim.fn.getcwd() .. "/lua/?/init.lua;" .. package.path + +local preview = require "md-render.preview" + +local pass_count = 0 +local fail_count = 0 + +local function assert_eq(actual, expected, msg) + if vim.deep_equal(actual, expected) then + pass_count = pass_count + 1 + else + fail_count = fail_count + 1 + print("FAIL: " .. msg) + print(" expected: " .. vim.inspect(expected)) + print(" actual: " .. vim.inspect(actual)) + end +end + +vim.o.lines, vim.o.columns = 40, 120 + +--- Open the float on `lines` with the source cursor on `cursor_line`, and +--- return the float's topline, height and line count. +local function open(lines, cursor_line) + local path = vim.fn.tempname() .. ".md" + vim.fn.writefile(lines, path) + vim.cmd("edit " .. path) + vim.api.nvim_win_set_cursor(0, { cursor_line, 0 }) + preview.show() + local win = vim.api.nvim_get_current_win() + local r = { + topline = vim.fn.line "w0", + height = vim.api.nvim_win_get_height(win), + lines = vim.api.nvim_buf_line_count(0), + } + vim.api.nvim_win_close(win, true) + vim.cmd "bwipeout!" + vim.fn.delete(path) + return r +end + +-- Test 1: a short document starts at its first line, wherever the cursor is +do + local r = open({ "## Heading", "", "One.", "", "Two.", "", "Three.", "", "Four." }, 9) + assert_eq(r.lines <= r.height, true, "the document should fit the float") + assert_eq(r.topline, 1, "a document that fits should not be scrolled") +end + +-- Test 2: a long document still centers the cursor line +do + local lines = {} + for i = 1, 200 do + table.insert(lines, ("Paragraph %d."):format(i)) + table.insert(lines, "") + end + local r = open(lines, 199) + assert_eq(r.topline > 1, true, "the cursor line in the middle of a long document should be scrolled to") + assert_eq(r.topline <= r.lines - r.height + 1, true, "the last line should not leave empty rows below it") +end + +print(string.format("\n%d passed, %d failed", pass_count, fail_count)) +if fail_count > 0 then os.exit(1) end