Skip to content

Commit 506e500

Browse files
fix(renderer): use binary search over vcols for wrap continuation borders
Replace the analytical walk (which used rendered/concealed column widths) with a binary search using nvim_win_text_height and vcol parameters. The analytical walk underestimated wrap boundaries because Neovim wraps based on raw text width + inline virt_text, ignoring extmark conceal — so concealed URLs still count towards the wrap width. nvim_win_text_height with start_vcol/end_vcol operates in the same coordinate space Neovim uses for wrapping, making it the correct predicate for locating wrap boundaries. - Use height.all * text_width as upper bound (safe, exceeds the effective wrap width including inline virt_text additions) - Convert vcol to byte position via virtcol2col for extmark anchor - Remove unused __col_widths storage from table items
1 parent d07c87a commit 506e500

1 file changed

Lines changed: 39 additions & 59 deletions

File tree

lua/markview/renderers/markdown.lua

Lines changed: 39 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2488,7 +2488,6 @@ markdown.table = function (buffer, item)
24882488
local right_border, right_hl = get_border("row", 3);
24892489
item.__right_border_vt = { { right_border, right_hl } };
24902490
item.__table_width = utils.virt_len(continuation_vt);
2491-
item.__col_widths = col_widths;
24922491

24932492
--- Register for post_render so __table runs after inline extmarks.
24942493
table.insert(markdown.cache, item);
@@ -2627,8 +2626,6 @@ markdown.__table = function (buffer, item)
26272626

26282627
local range = item.range;
26292628

2630-
local col_widths = item.__col_widths;
2631-
26322629
--- Compute the window's text-area width (excluding sign/number columns).
26332630
local textoff = vim.fn.getwininfo(win)[1].textoff;
26342631
local text_width = vim.api.nvim_win_get_width(win) - textoff;
@@ -2652,67 +2649,50 @@ markdown.__table = function (buffer, item)
26522649
});
26532650
end
26542651

2655-
--- Find wrap-break byte positions analytically by walking
2656-
--- the parsed table structure. The rendered width of each
2657-
--- element is known: separators = 1 col (│), columns =
2658-
--- col_widths[c]. We accumulate display width and record
2659-
--- the byte at the start of each element that crosses a
2660-
--- wrap boundary (multiples of text_width).
2652+
--- Find the first byte on each continuation (wrapped)
2653+
--- screen row via binary search over virtual columns.
26612654
---
2662-
--- Unlike a binary search over virtual columns, this
2663-
--- approach is immune to the coordinate-space mismatch
2664-
--- between virtcol (which ignores extmark conceal) and
2665-
--- nvim_win_text_height (which accounts for it). See
2666-
--- CommonMark §6.7 links inside table cells for a case
2667-
--- where concealed URLs broke the old binary search.
2668-
local parts;
2669-
if row == range.row_start then
2670-
parts = item.header;
2671-
elseif row == range.row_start + 1 then
2672-
parts = item.separator;
2673-
else
2674-
local ri = row - (range.row_start + 2) + 1;
2675-
parts = item.rows[ri];
2676-
end
2677-
2678-
if parts and col_widths then
2679-
local disp = 0; --- cumulative display columns
2680-
local wrap_line = 1; --- next wrap line to place
2681-
local cc = 1; --- column counter
2682-
2683-
for _, part in ipairs(parts) do
2684-
local elem_width;
2685-
if part.class == "separator" then
2686-
elem_width = 1;
2687-
elseif part.class == "column" then
2688-
elem_width = col_widths[cc] or 0;
2689-
cc = cc + 1;
2655+
--- nvim_win_text_height with start_vcol/end_vcol operates
2656+
--- in the same coordinate space Neovim uses for wrapping
2657+
--- (raw text width + inline virt_text, ignoring conceal).
2658+
--- This makes it the correct predicate for locating wrap
2659+
--- boundaries — unlike an analytical walk over rendered
2660+
--- widths, which underestimates when cells contain
2661+
--- concealed URLs that still count towards wrap width.
2662+
---
2663+
--- Upper bound: height.all * text_width is guaranteed to
2664+
--- exceed the effective wrap width (including any inline
2665+
--- virt_text additions that push past strdisplaywidth).
2666+
local lnum = row + 1;
2667+
local hi_bound = height.all * text_width;
2668+
2669+
for w = 1, height.all - 1 do
2670+
local lo, hi = 1, hi_bound;
2671+
2672+
while lo < hi do
2673+
local mid = math.floor((lo + hi) / 2);
2674+
2675+
if vim.api.nvim_win_text_height(win, {
2676+
start_row = row, end_row = row,
2677+
start_vcol = 0, end_vcol = mid,
2678+
}).all <= w then
2679+
lo = mid + 1;
26902680
else
2691-
goto continue;
2692-
end
2693-
2694-
--- Check if this element spans a wrap boundary.
2695-
while wrap_line <= height.all - 1
2696-
and disp + elem_width >= text_width * (wrap_line)
2697-
do
2698-
--- The byte at the start of this element is
2699-
--- guaranteed to be visible (not inside an
2700-
--- extmark-concealed URL). Using it as the
2701-
--- anchor ensures the overlay lands on the
2702-
--- correct screen row.
2703-
local anchor = range.col_start + part.col_start;
2704-
vim.api.nvim_buf_set_extmark(buffer, markdown.ns, row, anchor, {
2705-
undo_restore = false, invalidate = true,
2706-
virt_text = continuation_vt,
2707-
virt_text_win_col = 0,
2708-
hl_mode = "combine",
2709-
});
2710-
wrap_line = wrap_line + 1;
2681+
hi = mid;
27112682
end
2683+
end
27122684

2713-
disp = disp + elem_width;
2685+
--- lo is the first vcol on wrap line w+1.
2686+
--- Convert to a byte column for the extmark anchor.
2687+
local byte_col = vim.fn.virtcol2col(win, lnum, lo);
27142688

2715-
::continue::
2689+
if byte_col >= 1 then
2690+
vim.api.nvim_buf_set_extmark(buffer, markdown.ns, row, byte_col - 1, {
2691+
undo_restore = false, invalidate = true,
2692+
virt_text = continuation_vt,
2693+
virt_text_win_col = 0,
2694+
hl_mode = "combine",
2695+
});
27162696
end
27172697
end
27182698
end

0 commit comments

Comments
 (0)