From f370d0d718f760e5a3b19bfd4f28f04d8b1c3260 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Thu, 8 Oct 2026 19:18:14 +1030 Subject: [PATCH 1/3] fix(wrap): Keep the wrap indent checks in byte space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wrapcol`(from `virtcol2col()`) is a **byte index**, but it was compared against `strdisplaywidth()`, a **display width**. And, while `wrap` & `breakindent` are set, both `strdisplaywidth()` & `virtcol()` are *inflated* (`72` spaces measure as `104` cells in a `40` column window). The *"No enough text after indent"* check therefore missed the last wrap of a line & inserted a wrap indent **before the line's last character**, so the indent's border/spaces showed up inside the text or pushed the last character onto its own row, > quoted quoted quote▋ d > quoted quoted▋ The checks now stay in byte space(`#line`) & require more than a single character to be left after the wrap point. --- lua/markview/wrap.lua | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lua/markview/wrap.lua b/lua/markview/wrap.lua index f086cd43..dd44eaef 100644 --- a/lua/markview/wrap.lua +++ b/lua/markview/wrap.lua @@ -208,17 +208,42 @@ wrap.fine_wrap = function (buffer, win, row, ns, indent) return; end - local reallen = vim.fn.strdisplaywidth( - vim.api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] or "" - ); + local line = vim.api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] or ""; + + --[[ + IMPORTANT: `wrapcol`(from `virtcol2col()`) is a **byte index**, while + `strdisplaywidth()` returns a **display width**. + + And, while `wrap` & `breakindent` are set, Neovim's `strdisplaywidth()` & + `virtcol()` are *inflated*(`72` spaces in a `40` column window measure as + `104` cells). Comparing one against the other made the + *"No enough text after indent"* check miss the last wrap of a line, so an + indent was inserted **before the line's last character**, + + ``` + > quoted quoted quote▋ d + ``` + + Both checks below therefore stay in **byte space**(`#line`) & require more + than a single character to be left after the wrap point. + ]] + local line_len = #line; + + --- Index(1-based) of the last byte that isn't just trailing whitespace. + local last_char = line_len; + + while last_char > 0 and line:sub(last_char, last_char):match("%s") do + last_char = last_char - 1; + end + local virtlen = require("markview.utils").virt_len(indent); local wraps = math.ceil(end_vcol / win_width); for w = 1, wraps, 1 do local wrapcol = vim.fn.virtcol2col(win, row + 1, (win_width * w) + 1); - if wrapcol >= end_vcol or wrapcol >= reallen then - -- Out of bounds.Or at end of line. + if wrapcol > line_len or wrapcol >= last_char then + -- Out of bounds. Or only the line's last character is left. break; elseif wrapcol + virtlen >= end_vcol then -- No enough text after indent. From d4ca0ae04c43feed579e99af7c6a743c0d5e450f Mon Sep 17 00:00:00 2001 From: jsun969 Date: Thu, 8 Oct 2026 19:18:21 +1030 Subject: [PATCH 2/3] fix(autocmds): Render again when a window is resized `wrap` indents are placed at the wrap columns of the width they were rendered at. Buffers were only rendered again on cursor movements, text changes & `wrap`/`linebreak` option changes, so resizing a window(e.g. narrowing a split & widening it back) left the indents behind in the middle of a line. `WinResized` & `VimResized` now render the buffers that are on screen again, debounced(`50`ms) the same way cursor movements are. --- lua/markview/autocmds.lua | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lua/markview/autocmds.lua b/lua/markview/autocmds.lua index 6cfe230b..64815ba9 100644 --- a/lua/markview/autocmds.lua +++ b/lua/markview/autocmds.lua @@ -317,6 +317,52 @@ autocmds.optionSet = function (args) ---|fE end +---@diagnostic disable-next-line: undefined-field +autocmds.resize_timer = vim.uv.new_timer(); + +--[[ Window resized. ]] +---@param args vim.api.keyset.create_autocmd.callback_args +autocmds.resize = function (args) + ---|fS + + local state = require("markview.state"); + local actions = require("markview.actions"); + + autocmds.resize_timer:stop(); + + --[[ + `wrap` indents are placed at the wrap columns of the width they were + rendered at, so every buffer on screen needs to be rendered again for the + new width. + + Resizes arrive in bursts(a mouse drag or a per-step terminal resize), so + the renders are *debounced* the same way cursor movements are. + ]] + autocmds.resize_timer:start(50, 0, vim.schedule_wrap(function () + if not state.enabled() then + return; + end + + for _, buffer in ipairs(state.get_enabled_buffers()) do + --[[ + Wrap indents are *window dependent*, so only buffers that are + displayed somewhere need to be rendered again. + ]] + if vim.api.nvim_buf_is_valid(buffer) and vim.fn.win_findbuf(buffer)[1] then + if buffer == state.get_splitview_source() then + actions.splitview_render(); + elseif actions.in_preview_mode() then + actions.render(buffer); + else + actions.clear(buffer); + end + end + end + end)); + + ---|fE +end + ---@diagnostic disable-next-line: undefined-field autocmds.cursor_timer = vim.uv.new_timer(); @@ -507,6 +553,12 @@ autocmds.setup = function () callback = autocmds.optionSet }); + vim.api.nvim_create_autocmd({ + "WinResized", "VimResized" + }, { + callback = autocmds.resize + }); + vim.api.nvim_create_autocmd({ "BufAdd", "BufEnter", "BufWinEnter" From ac38d6b49f6d9eb3d7fb4ba2e3e05f8541521bbf Mon Sep 17 00:00:00 2001 From: jsun969 Date: Thu, 8 Oct 2026 19:18:21 +1030 Subject: [PATCH 3/3] test(wrap): Add tail indent & resize fixtures `test/wrap_tail_indent.md` holds block quotes & list items padded to exact byte lengths(72/73/86/90/88/71) that wrap into the cases `fine_wrap()` used to mangle, plus controls(heading, paragraph, table, code block) that must render unchanged, & the steps to check both fixes. `test/minimal_init.lua` runs the plugin from a checkout, so the fixture can be checked without `lazy.nvim`, ```sh nvim -u test/minimal_init.lua test/wrap_tail_indent.md ``` --- test/minimal_init.lua | 30 ++++++++++++++++++++ test/wrap_tail_indent.md | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 test/minimal_init.lua create mode 100644 test/wrap_tail_indent.md diff --git a/test/minimal_init.lua b/test/minimal_init.lua new file mode 100644 index 00000000..5ace8d0a --- /dev/null +++ b/test/minimal_init.lua @@ -0,0 +1,30 @@ +--[[ + Minimal init for smoke testing this plugin from a checkout(no `lazy.nvim`), + + ```sh + nvim -u test/minimal_init.lua test/wrap_tail_indent.md + ``` + + It only enables the plugin, the wrap related options & the preview/hybrid + mode settings the wrapping issues are reported with. +]] + +local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h"); + +vim.opt.runtimepath:prepend(root); + +vim.opt.number = true; +vim.opt.termguicolors = true; +vim.opt.laststatus = 3; + +-- Text wrapping, as reported. +vim.opt.wrap = true; +vim.opt.linebreak = true; +vim.opt.breakindent = true; + +require("markview").setup({ + preview = { + hybrid_modes = { "n", "no", "c" }, + linewise_hybrid_mode = true, + }, +}); diff --git a/test/wrap_tail_indent.md b/test/wrap_tail_indent.md new file mode 100644 index 00000000..ce085ca8 --- /dev/null +++ b/test/wrap_tail_indent.md @@ -0,0 +1,60 @@ + + +# Wrap indent checks + +## Block quotes + +> case A: 72 bytes, a trailing space. lorem ipsum dolor sit amet consec + +> case B: 72 bytes, no trailing space. lorem ipsum dolor sit amet consec + +> case C: 73 bytes, wraps one character onto the last row. lorem ipsum do + +> case D: 86 bytes, wraps one character onto the last row. lorem ipsum dolor sit ametx + +## List items + +- case E: 86 bytes, list item. lorem ipsum dolor sit amet consectetur adipiscing elit + + - case F: 90 bytes, nested list item. lorem ipsum dolor sit amet consectetur adipiscing + +- [ ] case G: 88 bytes, checkbox item. lorem ipsum dolor sit amet consectetur adipiscing + +## Controls (must render exactly as before) + +# A heading that is long enough to wrap in a narrow window + +Plain paragraph, wrapped by Neovim itself, no indent is added here: lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + +| column a | column b | column c | +|---|---|---| +| lorem ipsum dolor sit am | x | y | + +```lua +local x = 1 -- lorem ipsum dolor sit amet consectetur a +``` + +--- + +> case H: 71 bytes, no trailing space. lorem ipsum dolor sit amet conse