Skip to content

Commit 9b7fdea

Browse files
feature: Improve table & code conceal behavior
## Details Use the current conceal level + metadata to determine how to render language headers and table cells. For code blocks we simply check if the langauge element is likely concealed and if it is not only render an icon isntead of an icon + the language name. For table cells things are a little more complicated, but boils down to using `inline` extmarks to pad the table cells such that they take up as much space as if nothing were concealed. This is done through a new table cell style 'padded' which is also the new default. Since this relies on inline extmarks it only works with neovim >= 0.10.0, but will not break for earlier versions, it just will not perform the padding operation. Since cells can be any inline code the extmarks we add which shift text also need to be accounted for in addition to the conceals. This is easy enough for now since we do not use inline marks much, only for links really. But it is a point of fragility and means that table rendering needs to know about the functionality of everything. Still this is much better than the obtrusive overlay, which users can continue to use if they prefer.
1 parent a416b61 commit 9b7fdea

18 files changed

+535
-162
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ require('render-markdown').setup({
152152
153153
(image) @image
154154
]],
155+
-- Query to be able to identify links in nodes
156+
inline_link_query = '[(inline_link) (image)] @link',
155157
-- The level of logs to write to file: vim.fn.stdpath('state') .. '/render-markdown.log'
156158
-- Only intended to be used for plugin development / debugging
157159
log_level = 'error',
@@ -275,8 +277,9 @@ require('render-markdown').setup({
275277
style = 'full',
276278
-- Determines how individual cells of a table are rendered:
277279
-- overlay: writes completely over the table, removing conceal behavior and highlights
278-
-- raw: replaces only the '|' characters in each row, leaving the cells completely unmodified
279-
cell = 'overlay',
280+
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
281+
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
282+
cell = 'padded',
280283
-- Characters used to replace table border
281284
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
282285
-- stylua: ignore
@@ -290,6 +293,8 @@ require('render-markdown').setup({
290293
head = '@markup.heading',
291294
-- Highlight for everything else, main table rows and the line below
292295
row = 'Normal',
296+
-- Highlight for inline padding used to add back concealed space
297+
filler = 'Conceal',
293298
},
294299
-- Callouts are a special instance of a 'block_quote' that start with a 'shortcut_link'
295300
-- Can specify as many additional values as you like following the pattern from any below, such as 'note'
@@ -505,8 +510,9 @@ require('render-markdown').setup({
505510
style = 'full',
506511
-- Determines how individual cells of a table are rendered:
507512
-- overlay: writes completely over the table, removing conceal behavior and highlights
508-
-- raw: replaces only the '|' characters in each row, leaving the cells completely unmodified
509-
cell = 'overlay',
513+
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
514+
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
515+
cell = 'padded',
510516
-- Characters used to replace table border
511517
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
512518
-- stylua: ignore
@@ -520,6 +526,8 @@ require('render-markdown').setup({
520526
head = '@markup.heading',
521527
-- Highlight for everything else, main table rows and the line below
522528
row = 'Normal',
529+
-- Highlight for inline padding used to add back concealed space
530+
filler = 'Conceal',
523531
},
524532
})
525533
```

demo/box_dash_quote.gif

1.47 KB
Loading

demo/callout.gif

3.58 KB
Loading

demo/heading_code.gif

5.06 KB
Loading

demo/latex.gif

-6.81 KB
Loading

demo/list_table.gif

14.7 KB
Loading

demo/list_table.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717

1818
# Table
1919

20-
| Heading 1 | Heading 2 |
21-
| ------------ | ------------ |
22-
| Row 1 Item 1 | Row 1 Item 2 |
23-
| Row 2 Item 1 | Row 2 Item 2 |
24-
| Row 3 Item 1 | Row 3 Item 2 |
20+
| `Inline Heading` | *Italic Heading* |
21+
| ---------------- | ------------------ |
22+
| Regular Item | **Bold Item** |
23+
| `Item Inline` | [Link Item](/test) |

doc/render-markdown.txt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ Full Default Configuration ~
189189

190190
(image) @image
191191
]],
192+
-- Query to be able to identify links in nodes
193+
inline_link_query = '[(inline_link) (image)] @link',
192194
-- The level of logs to write to file: vim.fn.stdpath('state') .. '/render-markdown.log'
193195
-- Only intended to be used for plugin development / debugging
194196
log_level = 'error',
@@ -312,8 +314,9 @@ Full Default Configuration ~
312314
style = 'full',
313315
-- Determines how individual cells of a table are rendered:
314316
-- overlay: writes completely over the table, removing conceal behavior and highlights
315-
-- raw: replaces only the '|' characters in each row, leaving the cells completely unmodified
316-
cell = 'overlay',
317+
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
318+
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
319+
cell = 'padded',
317320
-- Characters used to replace table border
318321
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
319322
-- stylua: ignore
@@ -327,6 +330,8 @@ Full Default Configuration ~
327330
head = '@markup.heading',
328331
-- Highlight for everything else, main table rows and the line below
329332
row = 'Normal',
333+
-- Highlight for inline padding used to add back concealed space
334+
filler = 'Conceal',
330335
},
331336
-- Callouts are a special instance of a 'block_quote' that start with a 'shortcut_link'
332337
-- Can specify as many additional values as you like following the pattern from any below, such as 'note'
@@ -547,8 +552,9 @@ TABLES *render-markdown-setup-tables*
547552
style = 'full',
548553
-- Determines how individual cells of a table are rendered:
549554
-- overlay: writes completely over the table, removing conceal behavior and highlights
550-
-- raw: replaces only the '|' characters in each row, leaving the cells completely unmodified
551-
cell = 'overlay',
555+
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
556+
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
557+
cell = 'padded',
552558
-- Characters used to replace table border
553559
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
554560
-- stylua: ignore
@@ -562,6 +568,8 @@ TABLES *render-markdown-setup-tables*
562568
head = '@markup.heading',
563569
-- Highlight for everything else, main table rows and the line below
564570
row = 'Normal',
571+
-- Highlight for inline padding used to add back concealed space
572+
filler = 'Conceal',
565573
},
566574
})
567575
<

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ demo-callout:
2727
demo file rows content:
2828
rm -f demo/{{file}}.gif
2929
python demo/record.py \
30-
--cols "55" \
30+
--cols "60" \
3131
--rows {{rows}} \
3232
--file demo/{{file}}.md \
3333
--cast {{file}}.cast \

lua/render-markdown/handler/markdown.lua

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,27 @@ M.render_node = function(namespace, buf, capture, node)
103103
if not util.has_10 then
104104
return
105105
end
106-
-- Fenced code blocks will pick up varying amounts of leading white space depending on
107-
-- the context they are in. This gets lumped into the delimiter node and as a result,
108-
-- after concealing, the extmark will be left shifted. Logic below accounts for this.
109-
local padding = 0
110-
local code_block = ts.parent_in_section(info.node, 'fenced_code_block')
111-
if code_block ~= nil then
112-
padding = str.leading_spaces(ts.info(code_block, buf).text)
106+
107+
local icon_text = icon .. ' '
108+
if ts.concealed(buf, info) > 0 then
109+
-- Fenced code blocks will pick up varying amounts of leading white space depending on
110+
-- the context they are in. This gets lumped into the delimiter node and as a result,
111+
-- after concealing, the extmark will be left shifted. Logic below accounts for this.
112+
local padding = 0
113+
local code_block = ts.parent_in_section(info.node, 'fenced_code_block')
114+
if code_block ~= nil then
115+
padding = str.leading_spaces(ts.info(code_block, buf).text)
116+
end
117+
icon_text = str.pad(icon_text .. info.text, padding)
113118
end
119+
114120
local highlight = { icon_highlight }
115121
if code.style == 'full' then
116122
highlight = { icon_highlight, code.highlight }
117123
end
124+
118125
vim.api.nvim_buf_set_extmark(buf, namespace, info.start_row, info.start_col, {
119-
virt_text = { { str.pad(icon .. ' ' .. info.text, padding), highlight } },
126+
virt_text = { { icon_text, highlight } },
120127
virt_text_pos = 'inline',
121128
})
122129
elseif capture == 'list_marker' then
@@ -220,31 +227,19 @@ M.render_node = function(namespace, buf, capture, node)
220227
return
221228
end
222229

223-
---@param row integer
224-
---@param s string
225-
---@return integer
226-
local function get_row_width(row, s)
227-
local result = vim.fn.strdisplaywidth(s)
228-
if pipe_table.cell == 'raw' then
229-
result = result - ts.concealed(buf, row, s)
230-
end
231-
return result
232-
end
233-
234230
local delim = ts.info(delim_node, buf)
235-
local delim_width = get_row_width(delim.start_row, delim.text)
236-
237231
local lines = vim.api.nvim_buf_get_lines(buf, info.start_row, info.end_row, true)
238-
local start_width = get_row_width(info.start_row, list.first(lines))
239-
local end_width = get_row_width(info.end_row - 1, list.last(lines))
240232

233+
local delim_width = vim.fn.strdisplaywidth(delim.text)
234+
local start_width = vim.fn.strdisplaywidth(list.first(lines))
235+
local end_width = vim.fn.strdisplaywidth(list.last(lines))
241236
if delim_width ~= start_width or start_width ~= end_width then
242237
return
243238
end
244239

245240
local headings = vim.split(delim.text, '|', { plain = true, trimempty = true })
246-
local lengths = vim.tbl_map(function(part)
247-
return border[11]:rep(vim.fn.strdisplaywidth(part))
241+
local lengths = vim.tbl_map(function(cell)
242+
return border[11]:rep(vim.fn.strdisplaywidth(cell))
248243
end, headings)
249244

250245
local line_above = border[1] .. table.concat(lengths, border[2]) .. border[3]
@@ -278,30 +273,6 @@ M.render_node = function(namespace, buf, capture, node)
278273
})
279274
end
280275

281-
---@param row_info render.md.NodeInfo
282-
---@param highlight string
283-
local function render_table_row(row_info, highlight)
284-
if pipe_table.cell == 'overlay' then
285-
vim.api.nvim_buf_set_extmark(buf, namespace, row_info.start_row, row_info.start_col, {
286-
end_row = row_info.end_row,
287-
end_col = row_info.end_col,
288-
virt_text = { { row_info.text:gsub('|', border[10]), highlight } },
289-
virt_text_pos = 'overlay',
290-
})
291-
elseif pipe_table.cell == 'raw' then
292-
for i = 1, #row_info.text do
293-
if row_info.text:sub(i, i) == '|' then
294-
vim.api.nvim_buf_set_extmark(buf, namespace, row_info.start_row, i - 1, {
295-
end_row = row_info.end_row,
296-
end_col = i - 1,
297-
virt_text = { { border[10], highlight } },
298-
virt_text_pos = 'overlay',
299-
})
300-
end
301-
end
302-
end
303-
end
304-
305276
if pipe_table.style == 'full' then
306277
render_table_full()
307278
end
@@ -312,9 +283,9 @@ M.render_node = function(namespace, buf, capture, node)
312283
if row_type == 'pipe_table_delimiter_row' then
313284
render_table_delimiter(row_info)
314285
elseif row_type == 'pipe_table_header' then
315-
render_table_row(row_info, pipe_table.head)
286+
M.render_table_row(namespace, buf, row_info, pipe_table.head)
316287
elseif row_type == 'pipe_table_row' then
317-
render_table_row(row_info, pipe_table.row)
288+
M.render_table_row(namespace, buf, row_info, pipe_table.row)
318289
else
319290
-- Should only get here if markdown introduces more row types, currently unhandled
320291
logger.error('Unhandled markdown row type: ' .. row_type)
@@ -326,4 +297,65 @@ M.render_node = function(namespace, buf, capture, node)
326297
end
327298
end
328299

300+
---@param namespace integer
301+
---@param buf integer
302+
---@param info render.md.NodeInfo
303+
---@param highlight string
304+
M.render_table_row = function(namespace, buf, info, highlight)
305+
---@param text string
306+
---@return integer
307+
local function inline_width(text)
308+
local query = state.inline_link_query
309+
local tree = vim.treesitter.get_string_parser(text, 'markdown_inline')
310+
local result = 0
311+
for id in query:iter_captures(tree:parse()[1]:root(), text) do
312+
if query.captures[id] == 'link' then
313+
result = result + vim.fn.strdisplaywidth(state.config.link.hyperlink)
314+
end
315+
end
316+
return result
317+
end
318+
319+
local pipe_table = state.config.pipe_table
320+
local border = pipe_table.border
321+
322+
if vim.tbl_contains({ 'raw', 'padded' }, pipe_table.cell) then
323+
for cell in info.node:iter_children() do
324+
local cell_info = ts.info(cell, buf)
325+
local cell_type = cell_info.node:type()
326+
if cell_type == '|' then
327+
vim.api.nvim_buf_set_extmark(buf, namespace, cell_info.start_row, cell_info.start_col, {
328+
end_row = cell_info.end_row,
329+
end_col = cell_info.end_col,
330+
virt_text = { { border[10], highlight } },
331+
virt_text_pos = 'overlay',
332+
})
333+
elseif cell_type == 'pipe_table_cell' then
334+
if pipe_table.cell == 'padded' then
335+
-- Requires inline extmarks
336+
if util.has_10 then
337+
local concealed = ts.concealed(buf, cell_info) - inline_width(cell_info.text)
338+
if concealed > 0 then
339+
vim.api.nvim_buf_set_extmark(buf, namespace, cell_info.start_row, cell_info.end_col, {
340+
virt_text = { { str.pad('', concealed), pipe_table.filler } },
341+
virt_text_pos = 'inline',
342+
})
343+
end
344+
end
345+
end
346+
else
347+
-- Should only get here if markdown introduces more cell types, currently unhandled
348+
logger.error('Unhandled markdown cell type: ' .. cell_type)
349+
end
350+
end
351+
elseif pipe_table.cell == 'overlay' then
352+
vim.api.nvim_buf_set_extmark(buf, namespace, info.start_row, info.start_col, {
353+
end_row = info.end_row,
354+
end_col = info.end_col,
355+
virt_text = { { info.text:gsub('|', border[10]), highlight } },
356+
virt_text_pos = 'overlay',
357+
})
358+
end
359+
end
360+
329361
return M

0 commit comments

Comments
 (0)