Skip to content

feat(table): add cell wrapping using virt lines#617

Open
MaxDillon wants to merge 2 commits intoMeanderingProgrammer:mainfrom
MaxDillon:feat/table-cell-wrapping
Open

feat(table): add cell wrapping using virt lines#617
MaxDillon wants to merge 2 commits intoMeanderingProgrammer:mainfrom
MaxDillon:feat/table-cell-wrapping

Conversation

@MaxDillon
Copy link

@MaxDillon MaxDillon commented Mar 3, 2026

Closes: #616

What it does

  • Adds max_table_width for controlling wrapping behavior for tables. Wrap table when exceeds width setting
  • Updates offset to carry virt_text metadata (necesary for reconstructing hl groups inside tables)
  • compute_layout in Renderer:setup() calculates layout (column widths, row heights)
  • Covers original buffer line with overlay to prevent text overflow sticking out to right of table
  • Adds virtual lines below to display wrapped text
  • Re-render on resize or wrap toggle

Copy link
Owner

@MeanderingProgrammer MeanderingProgrammer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really interesting PR, it's going to take me a bit of time to parse through it.

I appreciate the effort! This could be a truly awesome feature.

end

-- Table already fits; use existing renderer
if total_natural <= text_budget then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't dug in much here but this seems to not handle the case where concealing results in the rows fitting within the available width. At least in the specific case of this repos README:

| Screenshot | Video     |
| ---------- | --------- |
| ![Heading](https://github.com/user-attachments/assets/40655575-b091-4ab8-b830-38f8004d7746) | ![Heading](https://github.com/user-attachments/assets/03f629ea-f6da-4f05-a035-827fd944e8be) |
| ![Table](https://github.com/user-attachments/assets/7d021918-e89c-4b7d-b33a-869390f9a826)   | ![Table](https://github.com/user-attachments/assets/fdbcfbfa-5f9e-49b7-8c19-f7e837979a7a)   |
| ![Quote](https://github.com/user-attachments/assets/822ae62c-bc0f-40a7-b8bb-fb3a885a95f9)   | ![Quote](https://github.com/user-attachments/assets/aa002ac7-b30f-4079-bba9-505160a4ad78)   |
| ![Callout](https://github.com/user-attachments/assets/e468a463-bc8d-420c-bb4c-da1263795092) | ![Callout](https://github.com/user-attachments/assets/d56cc5c7-43cd-4ce7-ad33-6164c2e23875) |
| ![Latex](https://github.com/user-attachments/assets/68f27ff3-49c8-42b5-bb7a-3b89c1e98401)   | ![Latex](https://github.com/user-attachments/assets/41e657a6-bcc2-464d-ab8c-a23bfcb80b0f)   |

When I reduce the window size it renders as:

Image

It gets caught on this line as it thinks the width is enough to hold the content. But that would need to based on the unconcealed width rather than the visible width.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I figured out what is going wrong here and it is a bug. Try this to fix it:

Go into any one of the link names (Heading, Table, Quote, Callout, or Latex) and write a bunch more text in there

100iLatex<esc>

This should cause that cell to have enough rendered text to enable wrapping, and that should resolve the issue.

When we are in wrapping mode, the solution mentioned in #616 works for wrapping even with concealed text, but when there is not enough text, I am falling back on the old wrapping strategy. In this scenario, where the un-rendered content is wrapping but the table hasn't entered into wrapping mode, we get this issue.

For now, test with at least one cell having enough rendered content to wrap, and I can either
A) Always use the wrapping renderer regardless of whether any text is getting wrapped

or

B) Include a check to enable wrap mode if any unrendered row content is wrapping

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fixed by fb51061

Just disabling early exit here. We will still default to nowrap if applicable, it will just use the full range of checks now. Try your old experiment with the README

-- | 0.1–1.0 | fraction of window width, e.g. 0.8 = 80% |
-- | 2+ | absolute character width, e.g. 80 = 80 columns |
-- | < 0 | window width minus N, e.g. -10 = width minus 10 |
max_table_width = 0,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor thing is if this does get merged in we'd definitely enable it by default, probably set the default to 1.0. Unless we feel it's unstable and want to keep stress testing it before switching it on for everyone.

@MaxDillon MaxDillon force-pushed the feat/table-cell-wrapping branch from fa67526 to fb51061 Compare March 4, 2026 00:48
end,
})
-- terminal / GUI resize — re-render all visible attached windows
vim.api.nvim_create_autocmd('VimResized', {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not covered by the WinResized autocmd?

end
end

local total_natural = 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused?

})

-- Lines 1..height-1: overlay buffer wrap continuations, then virt_lines.
if height > 1 then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove, if height <= 1 then the for vl = 1, height - 1 do will be skipped all on its own since the value will be < 1.

-- uses the capped widths (padding is included in delim col width).
if self.layout.wrap then
for i, w in ipairs(self.layout.col_widths) do
self.data.delim.cols[i].width = w + 2 * self.config.padding

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation (along with the way the line is built) will always add additional padding to cells that do not need it. The point of padding is to make sure that each cell has space around it, i.e. |A| -> | A |.

However if the cell already has padding like | A | it can be kept as is. With this logic it gets changed to | A | (with 2 extra spaces).

end

local filler = self.config.filler
local function build_line(visual_line)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently doesn't use the alignment signal, not strictly necessary from the start, but we probably wouldn't enable it by default without that. We would probably only try to align cells that are not wrapped.

---@param end_col integer
---@return render.md.mark.Line
function Render:cell_segments(row, start_col, end_col)
local raw_full = vim.api.nvim_buf_get_text(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the parsed columns so now they store the node associated with them: bd482f9

So you can pass that along to this method and do col.node.text. You'll need to rebase against main, will probably have a few merge conflicts to resolve.

Comment on lines +253 to +256
local buf_line = vim.api.nvim_buf_get_lines(
self.context.buf, row.node.start_row, row.node.start_row + 1, false
)[1] or ''
local raw_screen_lines = math.max(1, math.ceil(str.width(buf_line) / win_width))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local _, line = row.node:line('first', 0)
local raw_screen_lines = math.ceil(str.width(line) / win_width)

Comment on lines +593 to +595
local buf_line = vim.api.nvim_buf_get_lines(
self.context.buf, row.node.start_row, row.node.start_row + 1, false
)[1] or ''

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local _, buf_line = row.node:line('first', 0)
buf_line = buf_line or ''


---@private
---@return render.md.table.Layout
function Render:compute_layout()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently does not handle tables with leading spaces, i.e.

  | Col | Col | Col |
  | --- | --- | --- |
  | Row | Row | Row |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: Cell-wrapping for wide tables

2 participants