diff --git a/lua/md-render/preview.lua b/lua/md-render/preview.lua index 215fc39..6818ed0 100644 --- a/lua/md-render/preview.lua +++ b/lua/md-render/preview.lua @@ -29,6 +29,18 @@ local function usable_win_width(win) return math.max(1, total - textoff) end +--- The `max_width` to render at for a window. ContentBuilder wraps the text +--- at `max_width` and then prepends `indent`, so the indent has to come off +--- the window width; otherwise lines run up to its width past the edge and +--- 'wrap' folds their last characters onto a row of their own. +---@param win integer +---@param indent? string defaults to ContentBuilder's `" "` +---@return integer +local function content_width(win, indent) + local indent_w = vim.api.nvim_strwidth(indent or " ") + return math.max(1, math.min(usable_win_width(win) - indent_w, DEFAULT_MAX_WIDTH)) +end + --- Parse simple YAML frontmatter lines into key-value pairs ---@param fm_lines string[] ---@return {key: string, value: string}[] @@ -503,7 +515,7 @@ end function Session:bind_window(win) self.win = win if not self._explicit_max_width then - local win_width = math.min(usable_win_width(win), DEFAULT_MAX_WIDTH) + local win_width = content_width(win, self.opts.indent) if win_width ~= (self.opts.max_width or DEFAULT_MAX_WIDTH) then self.opts.max_width = win_width self:rebuild() @@ -1752,7 +1764,7 @@ local function install_win_resize_handler(session) local win = render_wins[1] if not vim.api.nvim_win_is_valid(win) then return end - local win_width = math.min(usable_win_width(win), DEFAULT_MAX_WIDTH) + local win_width = content_width(win, session.opts.indent) if win_width == (session.opts.max_width or DEFAULT_MAX_WIDTH) then return end session.opts.max_width = win_width diff --git a/tests/content_width_test.lua b/tests/content_width_test.lua new file mode 100644 index 0000000..2a31d11 --- /dev/null +++ b/tests/content_width_test.lua @@ -0,0 +1,97 @@ +-- Test that rendered lines fit the window they are shown in. ContentBuilder +-- wraps at `max_width` and prepends the indent afterwards, so a window-sized +-- `max_width` let lines run past the edge by the indent's width, and 'wrap' +-- put their last characters on a screen row of their own at column 0. +-- Run: nvim --headless -u NONE --noplugin -l tests/content_width_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 + +local function test(name, fn) + local ok, err = pcall(fn) + if not ok then + fail_count = fail_count + 1 + print("ERROR: " .. name .. ": " .. tostring(err)) + end +end + +-- Mixed CJK and ASCII so that the wrap points land at odd and even columns. +local DOC = { + "# Title", + "", + "- [x] " .. string.rep("あいうえお abc ", 8), + " - " .. string.rep("かきくけこ `x = 1` ", 8), + "", + string.rep("さしすせそ def ", 10), +} + +local function setup_md_buffer() + vim.cmd "silent! only" + local buf = vim.api.nvim_create_buf(false, false) + vim.bo[buf].filetype = "markdown" + vim.api.nvim_buf_set_name(buf, "/tmp/md-render-content-width-test-" .. buf .. ".md") + vim.api.nvim_buf_set_lines(buf, 0, -1, false, DOC) + vim.api.nvim_win_set_buf(0, buf) + return buf +end + +--- Lines of the window's buffer wider than its text area. +local function overflowing(win) + local width = vim.api.nvim_win_get_width(win) - vim.fn.getwininfo(win)[1].textoff + local out = {} + for _, line in ipairs(vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(win), 0, -1, false)) do + if vim.api.nvim_strwidth(line) > width then table.insert(out, line) end + end + return out +end + +local function render_win() + for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if vim.bo[vim.api.nvim_win_get_buf(w)].filetype == "md-render" then return w end + end +end + +for _, columns in ipairs { 40, 51, 60, 73 } do + test("toggle at " .. columns .. " columns", function() + vim.o.columns = columns + local source = setup_md_buffer() + preview.toggle() + assert_eq(overflowing(vim.api.nvim_get_current_win()), {}, "toggle lines fit at " .. columns .. " columns") + preview.toggle() + pcall(vim.api.nvim_buf_delete, source, { force = true }) + end) + + test("split at " .. columns .. " columns", function() + vim.o.columns = columns + local source = setup_md_buffer() + preview.split { mods = { vertical = false } } + assert_eq(overflowing(render_win()), {}, "split lines fit at " .. columns .. " columns") + pcall(vim.api.nvim_buf_delete, source, { force = true }) + end) + + test("pager at " .. columns .. " columns", function() + vim.o.columns = columns + local source = setup_md_buffer() + preview.show_pager() + assert_eq(overflowing(render_win()), {}, "pager lines fit at " .. columns .. " columns") + pcall(vim.api.nvim_buf_delete, source, { force = true }) + end) +end + +print(string.format("content_width_test: %d passed, %d failed", pass_count, fail_count)) +if fail_count > 0 then os.exit(1) end