From 97ac2a6013bfa07ee36d2e4f373171c2bbf3e6d7 Mon Sep 17 00:00:00 2001 From: Immanuel Haffner Date: Tue, 7 Jul 2026 14:12:45 +0000 Subject: [PATCH] feat(code_blocks): add block_on_wrap to control block style under wrap The block renderer pads each line to block_width with inline virtual text. With wrap enabled that trailing padding wraps onto the next screen line, breaking the block, so the renderer falls back to the simple style. Add a code_blocks.block_on_wrap option (default "off") to control this: - "off" under wrap, always fall back to the simple style. - "on" always keep the block style, even under wrap. - "adaptive" under wrap, keep block only when it fits the window; otherwise fall back to simple. The adaptive check compares range.col_start + block_width against the window's usable text width (win width minus getwininfo().textoff, which already accounts for the sign, number and fold columns). Since block_width includes the min_width/padding floor, padding-driven overflow triggers the fallback too. The per-line width scan is wrapped in a lazy, memoized compute_widths() closure in both the fenced and indented renderers, so it runs at most once per block and is reused by render_block() instead of recomputed. Cheap disqualifiers short-circuit before any width computation, so the scan only runs in the adaptive-under-wrap case. Applies to both fenced and indented code blocks, and updates the docs and type annotations accordingly. --- doc/markview.nvim-markdown.txt | 8 +- lua/markview/config/markdown.lua | 2 + lua/markview/renderers/markdown.lua | 202 +++++++++++++++++----- lua/markview/types/renderers/markdown.lua | 10 ++ 4 files changed, 174 insertions(+), 48 deletions(-) diff --git a/doc/markview.nvim-markdown.txt b/doc/markview.nvim-markdown.txt index 5de71d2d..3c740c16 100644 --- a/doc/markview.nvim-markdown.txt +++ b/doc/markview.nvim-markdown.txt @@ -297,6 +297,7 @@ Demo {img:2} min_width = 60, pad_amount = 2, pad_char = " ", + block_on_wrap = "off", default = { block_hl = "MarkviewCode", @@ -339,6 +340,7 @@ code_blocks: block ~ ---@field min_width? integer Minimum width of the code block. ---@field pad_amount? integer Number of `pad_char`s to add on the left & right side of the code block. ---@field pad_char? string Character used as padding. + ---@field block_on_wrap? "off" | "on" | "adaptive" Behaviour of the `block` style under `wrap`. `"off"`(default) uses `simple`; `"on"` always keeps `block`; `"adaptive"` keeps `block` only when it fits the window. --- ---@field sign? boolean Enables signs for the code block? ---@field sign_hl? string Highlight group for the sign. @@ -352,8 +354,10 @@ code_blocks: block ~ Creates a block around the code block. ▋  Important -▋ This is disabled if `wrap` is enabled. Or if you used `tab` inside the code -▋ block. +▋ When `wrap` is enabled this falls back to the `simple` style, unless +▋ `block_on_wrap` is set to `"on"` (always block) or `"adaptive"` (block only +▋ when it fits the window). It is also disabled if you used `tab` inside the +▋ code block. code_blocks: simple ~ diff --git a/lua/markview/config/markdown.lua b/lua/markview/config/markdown.lua index 84e490b7..29cc5cca 100644 --- a/lua/markview/config/markdown.lua +++ b/lua/markview/config/markdown.lua @@ -215,6 +215,8 @@ return { pad_amount = 2, pad_char = " ", + block_on_wrap = "off", + default = { block_hl = "MarkviewCode", pad_hl = "MarkviewCode" diff --git a/lua/markview/renderers/markdown.lua b/lua/markview/renderers/markdown.lua index 4925edf6..2a5e6612 100644 --- a/lua/markview/renderers/markdown.lua +++ b/lua/markview/renderers/markdown.lua @@ -371,6 +371,42 @@ markdown.code_block = function (buffer, item) return line_conf; end + --- Block width & per-line widths, computed lazily and memoized so the + --- `adaptive` overflow check and `render_block()` share a single pass. + ---@type integer? + local block_width; + ---@type integer[]? + local line_widths; + + --- Computes `block_width` and `line_widths` once, on first use. + ---@return integer block_width + ---@return integer[] line_widths + local function compute_widths () + if block_width ~= nil and line_widths ~= nil then + return block_width, line_widths; + end + + ---@cast config markview.config.markdown.code_blocks.block + local pad_amount = config.pad_amount or 0; + block_width = config.min_width or 60; + line_widths = {}; + + for l, line in ipairs(item.text) do + local final = require("markview.renderers.common.visual_text").get_visual_text(item.language, line); + + if l ~= 1 and l ~= #item.text then + local w = vim.fn.strdisplaywidth(final); + table.insert(line_widths, w); + + if w > (block_width - (2 * pad_amount)) then + block_width = w + (2 * pad_amount); + end + end + end + + return block_width, line_widths; + end + --[[ *Basic* rendering of `code blocks`. ]] local function render_simple() ---|fS @@ -456,24 +492,10 @@ markdown.code_block = function (buffer, item) ---|fS "chunk: Calculate various widths" local pad_amount = config.pad_amount or 0; - local block_width = config.min_width or 60; - local pad_char = config.pad_char or " "; - ---@type integer[] Visual width of lines. - local line_widths = {}; - - for l, line in ipairs(item.text) do - local final = require("markview.renderers.common.visual_text").get_visual_text(item.language, line); - - if l ~= 1 and l ~= #item.text then - table.insert(line_widths, vim.fn.strdisplaywidth(final)); - - if vim.fn.strdisplaywidth(final) > (block_width - (2 * pad_amount)) then - block_width = vim.fn.strdisplaywidth(final) + (2 * pad_amount); - end - end - end + ---@type integer, integer[] + local block_width, line_widths = compute_widths(); local label_width = utils.virt_len({ label }); ---|fE @@ -705,7 +727,47 @@ markdown.code_block = function (buffer, item) ---|fE end - if not win or config.style == "simple" or item.uses_tab or ( vim.o.wrap == true or vim.wo[win].wrap == true ) then + -- Decide between the `block` and `simple` renderers. + -- + -- The `block` renderer pads each line with `inline` virtual text to reach + -- `block_width`. With `wrap` enabled that trailing padding wraps onto the + -- next screen line, breaking the block. `block_on_wrap` controls what happens + -- under `wrap`: + -- "off" (default) always fall back to `render_simple()`. + -- "on" always keep the `block` style. + -- "adaptive" keep `block` only when it fits the window; else `simple`. + local function wrap_forces_simple () + if vim.o.wrap ~= true and vim.wo[win].wrap ~= true then + -- `wrap` is off: the block never wraps, always safe. + return false; + end + + local mode = config.block_on_wrap or "off"; + + if mode == "on" then + return false; + elseif mode == "adaptive" then + -- Fall back to `simple` only if the rendered block would overflow the + -- window's text area (and thus wrap). `textoff` already accounts for + -- the sign, number and fold columns. + local wininfo = vim.fn.getwininfo(win)[1]; + local usable = wininfo.width - wininfo.textoff; + local width = compute_widths(); + + -- The fence delimiter rows (```` ```lang ```` / ```` ``` ````) are + -- concealed but still counted by the wrap engine, so their raw byte + -- width is added *on top of* the `block_width` padding. Account for + -- the widest delimiter so those rows do not wrap unnoticed. + local delim_extra = math.max(#item.text[1], #item.text[#item.text]); + + return (item.range.col_start + width + delim_extra) > usable; + else + -- "off" (or any unknown value): always fall back under `wrap`. + return true; + end + end + + if not win or config.style == "simple" or item.uses_tab or wrap_forces_simple() then render_simple(); elseif config.style == "block" then render_block() @@ -747,6 +809,55 @@ markdown.indented_code_block = function (buffer, item) local decorations = filetypes.get("indented"); local label = { string.format(" %s%s ", decorations.icon, decorations.name), config.label_hl or decorations.icon_hl }; + -- Widths, computed lazily and memoized so the `adaptive` overflow check and + -- `render_block()` share a single pass. + ---@type integer? + local block_width; + ---@type integer[]? + local line_widths; + ---@type string[]? + local text; + ---@type integer? + local pad_width; + ---@type integer? + local label_width; + + --- Computes the block/line widths once, on first use. + ---@return integer block_width + local function compute_widths () + if block_width ~= nil then + return block_width; + end + + text = vim.api.nvim_buf_get_lines(buffer, range.row_start, range.row_end, false); + + local pad_amount = config.pad_amount --[[@as integer]] or 0; + local pad_char = config.pad_char --[[@as string]] or " "; + block_width = config.min_width --[[@as integer]] or 60; + label_width = utils.virt_len({ label }); + pad_width = vim.fn.strdisplaywidth(string.rep(pad_char, pad_amount)); + line_widths = {}; + + for l, _ in ipairs(item.text) do + local final = string.sub(text[l], range.space_end); + local w = vim.fn.strdisplaywidth(final); + + table.insert(line_widths, w); + + if l == 1 then + if (pad_amount + w + label_width) > block_width then + block_width = pad_amount + w + label_width; + end + else + if (pad_amount + w + pad_amount) > block_width then + block_width = pad_amount + w + pad_amount; + end + end + end + + return block_width; + end + --[[ *Basic* rendering of `code blocks`. ]] local function render_simple() ---|fS @@ -784,37 +895,15 @@ markdown.indented_code_block = function (buffer, item) ---|fS "chunk: Calculate various widths" - local text = vim.api.nvim_buf_get_lines(buffer, range.row_start, range.row_end, false); - local pad_amount = config.pad_amount --[[@as integer]] or 0; - local block_width = config.min_width --[[@as integer]] or 60; - local pad_char = config.pad_char --[[@as string]] or " "; - local label_width = utils.virt_len({ label }); - local pad_width = vim.fn.strdisplaywidth( - string.rep(pad_char, pad_amount) - ); - - ---@type integer[] Visual width of lines. - local line_widths = {}; - - for l, _ in ipairs(item.text) do - local final = string.sub(text[l], range.space_end); - local w = vim.fn.strdisplaywidth(final) - - table.insert(line_widths, w); - - if l == 1 then - if (pad_amount + w + label_width) > block_width then - block_width = pad_amount + w + label_width; - end - else - if (pad_amount + w + pad_amount) > block_width then - block_width = pad_amount + w + pad_amount; - end - end - end + compute_widths(); + ---@cast text string[] + ---@cast line_widths integer[] + ---@cast block_width integer + ---@cast pad_width integer + ---@cast label_width integer ---|fE @@ -897,7 +986,28 @@ markdown.indented_code_block = function (buffer, item) ---@type integer Window containing `buffer`. local win = utils.buf_getwin(buffer); - if not win or config.style == "simple" or ( vim.o.wrap == true or vim.wo[win].wrap == true ) then + -- See `markdown.code_block` for the meaning of `block_on_wrap`. + local function wrap_forces_simple () + if vim.o.wrap ~= true and vim.wo[win].wrap ~= true then + return false; + end + + local mode = config.block_on_wrap or "off"; + + if mode == "on" then + return false; + elseif mode == "adaptive" then + local wininfo = vim.fn.getwininfo(win)[1]; + local usable = wininfo.width - wininfo.textoff; + local width = compute_widths(); + + return (range.col_start + width) > usable; + else + return true; + end + end + + if not win or config.style == "simple" or wrap_forces_simple() then render_simple(); elseif config.style == "block" then render_block() diff --git a/lua/markview/types/renderers/markdown.lua b/lua/markview/types/renderers/markdown.lua index 09145e3e..14ffcf8d 100644 --- a/lua/markview/types/renderers/markdown.lua +++ b/lua/markview/types/renderers/markdown.lua @@ -73,6 +73,15 @@ ---@field [string] markview.config.markdown.code_blocks.opts +--- Controls whether the `block` style is kept when `wrap` is enabled. +--- - `"off"` Fall back to the `simple` style (default). +--- - `"on"` Always keep the `block` style. +--- - `"adaptive"` Keep the `block` style only when the block fits the window; else `simple`. +---@alias markview.config.markdown.code_blocks.block_on_wrap +---| "off" +---| "on" +---| "adaptive" +--- ---@class markview.config.markdown.code_blocks.block --- ---@field enable boolean Enable rendering of code blocks. @@ -86,6 +95,7 @@ ---@field min_width? integer Minimum width of the code block. ---@field pad_amount? integer Number of `pad_char`s to add on the left & right side of the code block. ---@field pad_char? string Character used as padding. +---@field block_on_wrap? markview.config.markdown.code_blocks.block_on_wrap Controls the `block` style under `wrap`. `"off"`(default) falls back to the `simple` style; `"on"` always keeps the `block` style; `"adaptive"` keeps `block` only when it fits the window (else `simple`). --- ---@field sign? boolean Enables signs for the code block? ---@field sign_hl? string Highlight group for the sign.