feat(code_blocks): add block_on_wrap to control block style under wrap#520
feat(code_blocks): add block_on_wrap to control block style under wrap#520ImmanuelHaffner wants to merge 1 commit into
block_on_wrap to control block style under wrap#520Conversation
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.
efb3668 to
97ac2a6
Compare
block_on_wrap to keep block style under wrapblock_on_wrap to control block style under wrap
|
Updated with the |
| ---@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. |
There was a problem hiding this comment.
I don't really understand why we need literal strings as possible field values. Lua doesn't have enums, no point in trying to replicate it.
It's better to do it like older plugins using integer values instead.
Also fields with multiple possible values should span multiple lines,
---@field block_on_wrap?
---| 0 Default
---| 1 Enabled
---| 2 AutomaticThere was a problem hiding this comment.
---@field label_direction? "left" | "right"is the precedent that justified this
There was a problem hiding this comment.
Did you actually look at the code or are you just copy pasting from your agent?
1 == "on", 0 == "off", that's used by other programs. People can understand that. I can't say 0 == "left", 1 == "right" since nobody uses that.
Using arbitrary strings as values tends to not stick in the long term. I would much rather not deal with spelling mistakes from end users or issues asking why only 2 values are getting checked etc.
| local delim_extra = math.max(#item.text[1], #item.text[#item.text]); | ||
|
|
||
| return (item.range.col_start + width + delim_extra) > usable; | ||
| else |
There was a problem hiding this comment.
This should be an else if as this is now "off" | "adaptive" | String instead of the original type.
There was a problem hiding this comment.
else if what condition? This is the fall through case of neither on nor adaptive
There was a problem hiding this comment.
Since we are explicitly supporting 3 values, we should only match them. The project already has enough dead code that I can't refactor simply cause someone might be using the exception as the default behavior, I would much rather not have more code added to that pile.
| if mode == "on" then | ||
| return false; | ||
| elseif mode == "adaptive" then | ||
| local wininfo = vim.fn.getwininfo(win)[1]; |
There was a problem hiding this comment.
getwininfo() is too performance intensive. It is not suitable for large buffers.
There was a problem hiding this comment.
First, it's in the adaptive mode and only opt-in.
Second, i use markview daily in 10k+ LOC buffers and it works well.
That said. what alternative do you suggest?
There was a problem hiding this comment.
There is no alternatives(at the moment) which is why this option wasn't provided. You can't correctly get a line's final width without having performance cost.
Even if you are using getwininfo() once every code block you are still constantly calling the API. And it tends to not work on text outside of the visible window area, so it leaves edge cases.
Also, LOC isn't what you should be measuring, the complexity & depth of the AST is what you should be looking at when doing performance optimizations.
|
I am still not getting the incentive to use this option. |
ImmanuelHaffner
left a comment
There was a problem hiding this comment.
The purpose of this PR is to get the visually appealing block-style render of code blocks that you already implemented for nowrap inside a wrap buffer.
While on enforcement leads to visual glitches if the code block is too wide for the window, adaptive solves this by adaptively switching over to simple for code blocks that are too wide. No visual glitches.
| local delim_extra = math.max(#item.text[1], #item.text[#item.text]); | ||
|
|
||
| return (item.range.col_start + width + delim_extra) > usable; | ||
| else |
There was a problem hiding this comment.
else if what condition? This is the fall through case of neither on nor adaptive
| ---@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. |
There was a problem hiding this comment.
---@field label_direction? "left" | "right"is the precedent that justified this
| if mode == "on" then | ||
| return false; | ||
| elseif mode == "adaptive" then | ||
| local wininfo = vim.fn.getwininfo(win)[1]; |
There was a problem hiding this comment.
First, it's in the adaptive mode and only opt-in.
Second, i use markview daily in 10k+ LOC buffers and it works well.
That said. what alternative do you suggest?
|
@ImmanuelHaffner You do realize that the right border of the code block bleeds onto the next line when using Unless there's a way to fix that this option will only work sometimes. People have different window sizes, what works on your setup might not work on other's setup. |
|
Per-call latency for Measured over 1M invocations.
Where do you get this from? The p99 is 25µs, that's unnoticeable for any human. |
|
Related to
I have created #521 |
I cannot repro that. I am resizing the window and witness the code block rendering switching adaptively from simple to block and back as i resize in 1 column at a time steps. I also tested with changing |
Yeah, I got it confused with a separate issue. |
|
The style = function (buf)
if vim.o.wrap then
return "simple";
end
local win = require("markview.utils").buf_getwin(buf);
return vim.wo[win].wrap == true and "simple" or "block";
end,This should be much better than adding a new option as users can choose to add whatever logic they want instead of some fixed rules. So, I am closing this PR in favor of it. |
Yes, this is because it's hard to determine accurately when the window width would have been sufficient. This is a conservative fallback to
Cannot repro, renders fine over all window widths and heights and different sign columns.
Well, ok, but that's rather orthogonal to the adaptive solution that I am building. |
There a few reasons actually. One of the things that I learned from open source is knowing when to say no. Not everything needs to be a part of the project. At the end of the day, I have to maintain the project, not the ones who submit the PR. So, I have to pick what I can or can't maintain in the long run. If I accept every single PR, soon the codebase will become alien to me. Which means that it would become hard for me to trace & fix bugs. The The The The Every rejected PR has some reason behind it. And I am not sure how a discussion would've lead to a different outcome. Also, AI PRs are a bit of a red flag cause unless someone re-checks I have no way to verify how good/correct it is. AI can find breaking bugs, what it can't do is figure out if a certain piece of code is causing issues with another piece of code. And whenever it makes some changes, it tends to also add unnecessary changes with them. All of that makes it harder for me to read through the entire thing. |

The
blockrenderer pads each line toblock_widthwithinlinevirtual text. Withwrapenabled that trailing padding wraps onto the next screen line, breaking the block, so the renderer currently falls back to thesimplestyle (which highlights the whole screen-line, i.e. full window width).This PR adds a
code_blocks.block_on_wrapoption that controls what happens underwrap. It is an enum with three values:"off"(default) — always fall back to thesimplestyle. Behaviour is unchanged from before."on"— always keep theblockstyle, even underwrap."adaptive"— keep theblockstyle only when the block fits the window; otherwise fall back tosimple.How
"adaptive"worksThe adaptive check compares
range.col_start + block_widthagainst the window's usable text width (win widthminusgetwininfo().textoff, which already accounts for the sign, number and fold columns). Sinceblock_widthincludes themin_width/padding floor, padding-driven overflow triggers the fallback too.It also accounts for a subtle wrap issue: the fence delimiter rows (
```lang/```) are concealed but their raw columns are still counted by Neovim's wrap engine, on top of theblock_widthpadding. The check adds the widest delimiter's width so those rows don't wrap unnoticed. This biases slightly towardssimple(the safe, glitch-free direction) rather than risking a wrapped block.Performance
The per-line width scan is wrapped in a lazy, memoized
compute_widths()closure in both renderers, so it runs at most once per block and is reused byrender_block()instead of being recomputed. The cheap disqualifiers (wrapoff,"on"/"off",simplestyle,tab) short-circuit before any width computation, so the scan only runs in the"adaptive"-under-wrapcase, andgetwininfois called only there.Changes
code_blocks.block_on_wrap(enum, default"off").markdown.code_block) and indented (markdown.indented_code_block) renderers honour it.doc/markview.nvim-markdown.txt) and type annotations (lua/markview/types/renderers/markdown.lua).Notes
"off", so existing setups are unaffected.tab-containing code blocks still fall back tosimpleas before.This started as a very opinionated change, which is why it stays behind an opt-in option. With
"adaptive"it now does the block-vs-simple switch automatically based on whether the block would wrap, so the default"off"stays conservative while"adaptive"gives a consistent, glitch-free experience. Please LMK if this needs improvement or what's blocking. Cheers