diff --git a/lua/md-render/preview.lua b/lua/md-render/preview.lua index 476430e..42c5330 100644 --- a/lua/md-render/preview.lua +++ b/lua/md-render/preview.lua @@ -521,12 +521,19 @@ 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 } + -- Wrapped lines can fill the window even when there are fewer buffer lines. + local remaining = vim.api.nvim_win_text_height(self.win, { + start_row = top, + end_row = buf_lines - 1, + max_height = win_height, + }).all + if remaining < win_height then + vim.fn.cursor(buf_lines, 1) + vim.cmd "normal! zb" + else + vim.fn.winrestview { topline = top + 1 } + end end) vim.api.nvim_win_set_cursor(self.win, { target, 0 }) end diff --git a/tests/preview_scroll_test.lua b/tests/preview_scroll_test.lua index 2ef9ca5..9eb2e2d 100644 --- a/tests/preview_scroll_test.lua +++ b/tests/preview_scroll_test.lua @@ -30,14 +30,19 @@ local function open(lines, cursor_line) vim.fn.writefile(lines, path) vim.cmd("edit " .. path) vim.api.nvim_win_set_cursor(0, { cursor_line, 0 }) + local source_win = vim.api.nvim_get_current_win() 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), + rows = vim.api.nvim_win_text_height(win, {}).all, + cursor = vim.api.nvim_win_get_cursor(win)[1], + cursor_row = vim.fn.winline(), } vim.api.nvim_win_close(win, true) + r.source_cursor = vim.api.nvim_win_get_cursor(source_win)[1] vim.cmd "bwipeout!" vim.fn.delete(path) return r @@ -60,7 +65,65 @@ do 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") + + -- Near EOF, earlier text fills the rows that centering would leave empty. + r = open(lines, 397) + assert_eq(r.rows, r.lines, "the EOF fixture should not wrap") + assert_eq(r.cursor, 397, "the EOF adjustment should preserve the mapped cursor") + assert_eq(r.cursor_row + r.lines - r.cursor, r.height, "the last line should fill the float's last screen row") + assert_eq(r.source_cursor, 397, "opening and closing the float should preserve the source cursor") +end + +-- An attached UI verifies native wrapping and the actual following text on screen. +local child = vim.fn.jobstart( + { vim.v.progpath, "--embed", "--headless", "-n", "-u", "NONE", "--noplugin", "-i", "NONE" }, + { rpc = true } +) +local function child_lua(code) + return vim.rpcrequest(child, "nvim_exec_lua", code, {}) end +local ok, err = pcall(function() + vim.rpcrequest(child, "nvim_ui_attach", 120, 40, { rgb = true }) + child_lua [[ + package.path = vim.fn.getcwd() .. "/lua/?.lua;" .. vim.fn.getcwd() .. "/lua/?/init.lua;" .. package.path + require("md-render.text_size").setup { enabled = false } + vim.o.scrolloff, vim.o.laststatus = 0, 2 + local lines = {} + for i = 1, 13 do + lines[#lines + 1] = ("Para%02d "):format(i) .. string.rep("x", 145) + lines[#lines + 1] = "" + end + vim.bo.filetype = "markdown" + vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + vim.api.nvim_win_set_cursor(0, {21, 0}) + _G.source_win = vim.api.nvim_get_current_win() + _G.source_buf = vim.api.nvim_get_current_buf() + _G.source_tick = vim.api.nvim_buf_get_changedtick(source_buf) + require("md-render.preview").show_tab() + ]] + vim.rpcrequest(child, "nvim_command", "redraw!") + child_lua [[ + local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() + local session = require("md-render.preview")._sessions[buf] + assert(vim.api.nvim_win_text_height(win, {}).all > vim.api.nvim_buf_line_count(buf), "fixture must wrap on screen") + assert(vim.api.nvim_win_get_cursor(win)[1] == session:source_to_rendered(21), "mapped cursor changed") + local pos + for line, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do + local col = text:find("Para12", 1, true) + if col then pos = vim.fn.screenpos(win, line, col); break end + end + assert(pos and pos.row > 0, "the paragraph after the cursor is off screen") + local cells = {} + for col = pos.col, pos.col + 5 do cells[#cells + 1] = vim.fn.screenstring(pos.row, col) end + assert(table.concat(cells) == "Para12", "the following paragraph was not drawn") + vim.api.nvim_win_close(win, true) + assert(vim.api.nvim_win_get_buf(source_win) == source_buf, "closing the preview lost the source") + assert(vim.api.nvim_win_get_cursor(source_win)[1] == 21, "source cursor changed") + assert(vim.api.nvim_buf_get_changedtick(source_buf) == source_tick, "source text changed") + ]] +end) +vim.fn.jobstop(child) +assert_eq(ok, true, "wrapped paragraphs stay visible: " .. tostring(err)) print(string.format("\n%d passed, %d failed", pass_count, fail_count)) if fail_count > 0 then os.exit(1) end