From 57cd9dd9f7c159693c0be405bc8c70a1ea9ac111 Mon Sep 17 00:00:00 2001 From: Immanuel Haffner Date: Wed, 8 Jul 2026 08:29:49 +0000 Subject: [PATCH] perf(wrap): hoist window metrics out of the per-row fine_wrap loop wrap.render() called fine_wrap() once per wrapped row, and each call ran vim.fn.getwininfo(win) plus vim.wo[win] reads to derive textoff/width. Those values are window-invariant across a single render pass, so this allocated a fresh getwininfo dict for every wrapped line (N calls, N allocations, extra GC pressure), on a path that fires on every scroll, edit and streamed chat token. Extract the metric resolution (including the #434 signcolumn fixup) into wrap.win_width(win), compute it once in wrap.render(), and pass the width into fine_wrap(). The whole row loop now runs inside a single nvim_win_call instead of one per row. Measured on an example stress test markdown file: getwininfo calls per render pass drop 38 -> 1 with identical extmark output. --- lua/markview/wrap.lua | 60 ++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/lua/markview/wrap.lua b/lua/markview/wrap.lua index f086cd43..d68130c7 100644 --- a/lua/markview/wrap.lua +++ b/lua/markview/wrap.lua @@ -168,14 +168,16 @@ wrap.sign_width = function (buffer, ns) end --[[ -Based on the answer from @zeertzjq in neovim/neovim#35964. +Resolves a window's text-area metrics once, so callers can hoist this out of +per-row loops. Returns the usable text `width` (window width minus the columns +reserved by the sign, number and fold columns). + +Must be called inside `win` (e.g. via `nvim_win_call`) because the underlying +functions are window-dependent. ]] ----@param buffer integer ---@param win integer ----@param row integer ----@param ns integer ----@param indent [ string, string? ][] -wrap.fine_wrap = function (buffer, win, row, ns, indent) +---@return integer width +wrap.win_width = function (win) ---|fS local wininfo = vim.fn.getwininfo(win)[1]; @@ -201,13 +203,28 @@ wrap.fine_wrap = function (buffer, win, row, ns, indent) textoff = vim.g.markview_sign_width or 2; end - local win_width = wininfo.width - textoff; + return wininfo.width - textoff; + + ---|fE +end + +--[[ +Based on the answer from @zeertzjq in neovim/neovim#35964. +]] +---@param buffer integer +---@param win integer +---@param row integer +---@param ns integer +---@param indent [ string, string? ][] +---@param win_width integer Precomputed usable text width (see `wrap.win_width`). +wrap.fine_wrap = function (buffer, win, row, ns, indent, win_width) + ---|fS + local end_vcol = vim.fn.virtcol({ row + 1, "$" }) - 1; if end_vcol <= win_width then return; end - local reallen = vim.fn.strdisplaywidth( vim.api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] or "" ); @@ -257,21 +274,22 @@ wrap.render = function (buffer, ns) if not win then return; end - - local function render_line (row, indent) - -- NOTE: The `functions` used inside `fine_wrap` are **window-dependent**. - -- This means we need to call them inside `win`. Otherwise it will run it on whatever window that is currently focused. - vim.api.nvim_win_call(win, function () - wrap.fine_wrap(buffer, win, row, ns, indent); - end); - end - - for row, indent in pairs(wrap.cache[buffer] or {}) do - render_line(row, indent); - end + -- NOTE: The `functions` used inside `fine_wrap` (and `wrap.win_width`) are + -- **window-dependent**. They must run inside `win`, otherwise they operate + -- on whatever window is currently focused. + -- + -- The window's text-area width is invariant across all rows of a single + -- render pass, so it is resolved once here rather than per row (which used + -- to allocate a fresh `getwininfo` dict for every wrapped line). + vim.api.nvim_win_call(win, function () + local win_width = wrap.win_width(win); + + for row, indent in pairs(wrap.cache[buffer] or {}) do + wrap.fine_wrap(buffer, win, row, ns, indent, win_width); + end + end); wrap.cache[buffer] = {}; - ---|fE end