Skip to content

Commit 595ac4f

Browse files
feat(0.11): process conceal_lines metadata from highlights
## Details This is some of the work needed to better handle the recent conceal_lines change added to neovim: #352 In order to better render code blocks, or at least have the ability to, we first need to figure out which lines are impacted by the change. To do this update the `conceal` module to process this additional metadata and store it. Expose this information again through the `context` module. This change does not include using this information to actually improve the rendering, but having it ready to go is useful, and the code block implementation to get around this is going to be pretty tedious. Some minor refactoring along the way to make these changes a little easier.
1 parent b57d51d commit 595ac4f

File tree

9 files changed

+158
-75
lines changed

9 files changed

+158
-75
lines changed

doc/render-markdown.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2025 February 27
1+
*render-markdown.txt* For 0.10.0 Last change: 2025 February 28
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*

lua/render-markdown/core/conceal.lua

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
local Str = require('render-markdown.lib.str')
2+
local util = require('render-markdown.core.util')
3+
4+
---@class render.md.conceal.Section
5+
---@field start_col integer
6+
---@field end_col integer
7+
---@field width integer
8+
---@field character? string
9+
10+
---@class render.md.conceal.Line
11+
---@field hidden boolean
12+
---@field sections render.md.conceal.Section[]
213

314
---@class render.md.Conceal
415
---@field private buf integer
516
---@field private level integer
617
---@field private computed boolean
7-
---@field private rows table<integer, [integer, integer, integer][]>
18+
---@field private lines table<integer, render.md.conceal.Line>
819
local Conceal = {}
920
Conceal.__index = Conceal
1021

@@ -16,7 +27,7 @@ function Conceal.new(buf, level)
1627
self.buf = buf
1728
self.level = level
1829
self.computed = false
19-
self.rows = {}
30+
self.lines = {}
2031
return self
2132
end
2233

@@ -26,59 +37,83 @@ function Conceal:enabled()
2637
end
2738

2839
---@param row integer
29-
---@param start_col integer
30-
---@param end_col integer
31-
---@param amount integer
32-
---@param character? string
33-
function Conceal:add(row, start_col, end_col, amount, character)
34-
if not self:enabled() or amount == 0 then
40+
---@param entry boolean|render.md.conceal.Section
41+
function Conceal:add(row, entry)
42+
if not self:enabled() then
3543
return
3644
end
37-
if self.rows[row] == nil then
38-
self.rows[row] = {}
45+
if self.lines[row] == nil then
46+
self.lines[row] = { hidden = false, sections = {} }
3947
end
40-
-- If the range is already concealed by another don't add it
41-
for _, range in ipairs(self.rows[row]) do
42-
if range[1] <= start_col and range[2] >= end_col then
48+
local line = self.lines[row]
49+
if type(entry) == 'boolean' then
50+
line.hidden = entry
51+
else
52+
if entry.width <= 0 then
4353
return
4454
end
55+
-- If the section is covered by an existing one don't add it
56+
for _, section in ipairs(line.sections) do
57+
if section.start_col <= entry.start_col and section.end_col >= entry.end_col then
58+
return
59+
end
60+
end
61+
table.insert(line.sections, entry)
4562
end
46-
table.insert(self.rows[row], { start_col, end_col, self:adjust(amount, character) })
4763
end
4864

49-
---@param amount integer
65+
---@param width integer
5066
---@param character? string
5167
---@return integer
52-
function Conceal:adjust(amount, character)
68+
function Conceal:adjust(width, character)
5369
if self.level == 1 then
54-
-- Level 1: each block is replaced with one character
55-
amount = amount - 1
70+
-- each block is replaced with one character
71+
return width - 1
5672
elseif self.level == 2 then
57-
-- Level 2: replacement character width is used
58-
amount = amount - Str.width(character)
73+
-- replacement character width is used
74+
return width - Str.width(character)
75+
else
76+
return width
5977
end
60-
return amount
78+
end
79+
80+
---@param context render.md.Context
81+
---@param node render.md.Node
82+
---@return boolean
83+
function Conceal:hidden(context, node)
84+
-- conceal lines metadata require neovim >= 0.11.0 to function
85+
return util.has_11 and self:line(context, node).hidden
6186
end
6287

6388
---@param context render.md.Context
6489
---@param node render.md.Node
6590
---@return integer
6691
function Conceal:get(context, node)
67-
if not self.computed then
68-
self.computed = true
69-
self:compute(context)
70-
end
71-
7292
local result = 0
73-
local ranges = self.rows[node.start_row] or {}
74-
for _, range in ipairs(ranges) do
75-
if node.start_col < range[2] and node.end_col > range[1] then
76-
result = result + range[3]
93+
for _, section in ipairs(self:line(context, node).sections) do
94+
if node.start_col < section.end_col and node.end_col > section.start_col then
95+
local amount = self:adjust(section.width, section.character)
96+
result = result + amount
7797
end
7898
end
7999
return result
80100
end
81101

102+
---@private
103+
---@param context render.md.Context
104+
---@param node render.md.Node
105+
function Conceal:line(context, node)
106+
if not self.computed then
107+
self:compute(context)
108+
self.computed = true
109+
end
110+
local line = self.lines[node.start_row]
111+
if line == nil then
112+
line = { hidden = false, sections = {} }
113+
end
114+
return line
115+
end
116+
82117
---Cached row level implementation of vim.treesitter.get_captures_at_pos
83118
---@private
84119
---@param context render.md.Context
@@ -116,21 +151,40 @@ function Conceal:compute_tree(context, language, root)
116151
end
117152
context:for_each(function(range)
118153
for id, node, metadata in query:iter_captures(root, self.buf, range.top, range.bottom) do
154+
if metadata.conceal_lines ~= nil then
155+
local node_range = self:node_range(id, node, metadata)
156+
local row = unpack(node_range)
157+
self:add(row, true)
158+
end
119159
if metadata.conceal ~= nil then
120-
local node_range = metadata.range
121-
if node_range == nil and metadata[id] ~= nil then
122-
node_range = metadata[id].range
123-
end
124-
if node_range == nil then
125-
---@diagnostic disable-next-line: missing-fields
126-
node_range = { node:range() }
127-
end
160+
local node_range = self:node_range(id, node, metadata)
128161
local row, start_col, _, end_col = unpack(node_range)
129-
local amount = Str.width(vim.treesitter.get_node_text(node, self.buf))
130-
self:add(row, start_col, end_col, amount, metadata.conceal)
162+
self:add(row, {
163+
start_col = start_col,
164+
end_col = end_col,
165+
width = Str.width(vim.treesitter.get_node_text(node, self.buf)),
166+
character = metadata.conceal,
167+
})
131168
end
132169
end
133170
end)
134171
end
135172

173+
---@private
174+
---@param id integer
175+
---@param node TSNode
176+
---@param metadata vim.treesitter.query.TSMetadata
177+
---@return Range
178+
function Conceal:node_range(id, node, metadata)
179+
local range = metadata.range
180+
if range ~= nil then
181+
return range
182+
end
183+
range = metadata[id] ~= nil and metadata[id].range or nil
184+
if range ~= nil then
185+
return range
186+
end
187+
return { node:range() }
188+
end
189+
136190
return Conceal

lua/render-markdown/core/context.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ function Context:tab_size()
123123
return util.get('buf', self.buf, 'tabstop')
124124
end
125125

126+
---@param node? render.md.Node
127+
---@return boolean
128+
function Context:hidden(node)
129+
if node == nil then
130+
return false
131+
end
132+
return self.conceal:hidden(self, node)
133+
end
134+
126135
---@param node? render.md.Node
127136
---@return integer
128137
function Context:width(node)

lua/render-markdown/core/util.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
local M = {}
33

44
M.has_10 = vim.fn.has('nvim-0.10') == 1
5+
M.has_11 = vim.fn.has('nvim-0.11') == 1
56

67
---@param key 'ft'|'cmd'
78
---@return string[]

lua/render-markdown/health.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local state = require('render-markdown.state')
55
local M = {}
66

77
---@private
8-
M.version = '8.0.13'
8+
M.version = '8.0.14'
99

1010
function M.check()
1111
M.start('version')

lua/render-markdown/lib/list.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ function Marks:update_context(mark)
9999
local row, start_col = mark.start_row, mark.start_col
100100
local end_col = mark.opts.end_col or start_col
101101
if mark.opts.conceal ~= nil then
102-
self.context.conceal:add(row, start_col, end_col, end_col - start_col)
102+
self.context.conceal:add(row, {
103+
start_col = start_col,
104+
end_col = end_col,
105+
width = end_col - start_col,
106+
character = mark.opts.conceal,
107+
})
103108
end
104109
if mark.opts.virt_text_pos == 'inline' then
105110
local amount = 0

lua/render-markdown/render/code.lua

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ function Render:render()
9696
local background = vim.tbl_contains({ 'normal', 'full' }, self.code.style) and not disabled_language
9797

9898
local icon = self:language()
99-
self:border(icon)
99+
local start_row, end_row = self.node.start_row, self.node.end_row - 1
100+
self:border(start_row, self.code.above, not icon and self:hidden(self.data.code_node))
101+
self:border(end_row, self.code.below, true)
100102
if background then
101-
self:background(self.node.start_row + 1, self.node.end_row - 2)
103+
self:background(start_row + 1, end_row - 1)
102104
end
103105
self:left_pad(background)
104106
end
@@ -127,7 +129,7 @@ function Render:language()
127129
self:sign(icon, icon_highlight)
128130
end
129131

130-
local icon_text = icon .. ' '
132+
local text = icon .. ' '
131133
local highlight = { icon_highlight }
132134
if self.code.border ~= 'none' then
133135
table.insert(highlight, self.code.highlight)
@@ -136,25 +138,25 @@ function Render:language()
136138
if self.code.position == 'left' then
137139
if self.code.language_name and self:hidden(node) then
138140
-- Code blocks pick up varying amounts of leading white space depending
139-
-- on the context they are in. This is lumped into the delimiter node and
140-
-- as a result, after concealing, the extmark would be shifted.
141+
-- on the context they are in. This is lumped into the delimiter node
142+
-- and as a result, after concealing, the extmark would be shifted.
141143
local padding = Str.spaces('start', self.node.text) + self.data.language_padding
142-
icon_text = Str.pad(padding) .. icon_text .. node.text
144+
text = Str.pad(padding) .. text .. node.text
143145
end
144146
return self.marks:add_start('code_language', node, {
145-
virt_text = { { icon_text, highlight } },
147+
virt_text = { { text, highlight } },
146148
virt_text_pos = 'inline',
147149
})
148150
elseif self.code.position == 'right' then
149151
if self.code.language_name then
150-
icon_text = icon_text .. node.text
152+
text = text .. node.text
151153
end
152154
local win_col = self.data.max_width - self.data.language_padding
153155
if self.code.width == 'block' then
154-
win_col = win_col - Str.width(icon_text) + self.data.indent
156+
win_col = win_col - Str.width(text) + self.data.indent
155157
end
156158
return self.marks:add('code_language', node.start_row, 0, {
157-
virt_text = { { icon_text, highlight } },
159+
virt_text = { { text, highlight } },
158160
virt_text_win_col = win_col,
159161
})
160162
else
@@ -163,36 +165,45 @@ function Render:language()
163165
end
164166

165167
---@private
166-
---@param icon boolean
167-
function Render:border(icon)
168+
---@param row integer
169+
---@param border string
170+
---@param context_hidden boolean
171+
function Render:border(row, border, context_hidden)
168172
if self.code.border == 'none' then
169173
return
170174
end
171-
172-
---@param row integer
173-
---@param border string
174-
---@param context_hidden boolean
175-
local function add_border(row, border, context_hidden)
176-
local delim_node = self.node:child('fenced_code_block_delimiter', row)
177-
if self.code.border == 'thin' and context_hidden and self:hidden(delim_node) then
178-
local width = self.code.width == 'block' and self.data.max_width or vim.o.columns
179-
self.marks:add('code_border', row, self.data.col, {
180-
virt_text = { { border:rep(width - self.data.col), colors.bg_to_fg(self.code.highlight) } },
181-
virt_text_pos = 'overlay',
182-
})
183-
else
184-
self:background(row, row)
185-
end
175+
local delim_node = self.node:child('fenced_code_block_delimiter', row)
176+
if self.code.border == 'thin' and context_hidden and self:hidden(delim_node) then
177+
local width = self.code.width == 'block' and self.data.max_width or vim.o.columns
178+
local line = { { border:rep(width - self.data.col), colors.bg_to_fg(self.code.highlight) } }
179+
self.marks:add('code_border', row, self.data.col, {
180+
virt_text = line,
181+
virt_text_pos = 'overlay',
182+
})
183+
else
184+
self:background(row, row)
186185
end
187-
188-
add_border(self.node.start_row, self.code.above, not icon and self:hidden(self.data.code_node))
189-
add_border(self.node.end_row - 1, self.code.below, true)
190186
end
191187

192188
---@private
193189
---@param node? render.md.Node
194190
---@return boolean
195191
function Render:hidden(node)
192+
-- TODO(0.11): handle conceal_lines
193+
-- - Use self.context:hidden(node) to determine if a node is hidden
194+
-- - Default highlights remove the fenced code block delimiter lines along with
195+
-- any extmarks we add there.
196+
-- - To account for this we'll need add back the lines, likely using virt_lines.
197+
-- - For top delimiter
198+
-- - Add extmark above the top row with virt_lines_above = false
199+
-- - By doing this we'll add a line just above the fenced code block
200+
-- - We likely need to handle the sign column here as well
201+
-- - For bottom delimiter
202+
-- - Add extmark below the bottom row with virt_lines_above = true
203+
-- - By doing this we'll add a line just below the fenced code block
204+
-- - For both of these we'll need to do something that does anti_conceal via an
205+
-- offset such that the cursor going over the concealed line naturally shows
206+
-- the raw text and the virtual line disappears
196207
return self.context:width(node) == 0
197208
end
198209

lua/render-markdown/render/heading.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ function Render:setup()
3636
if self.context:skip(self.heading) then
3737
return false
3838
end
39+
if self.context:hidden(self.node) then
40+
return false
41+
end
3942

4043
local atx = nil
4144
local marker = nil

lua/render-markdown/render/table.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ function Render:parse_row(row, num_columns)
168168
local columns = {}
169169
for i, cell in ipairs(cells) do
170170
local start_col, end_col = pipes[i].end_col, pipes[i + 1].start_col
171-
-- Account for double width glyphs by replacing cell range with width
171+
-- Account for double width glyphs by replacing cell range with its width
172172
local width = end_col - start_col
173173
width = width - (cell.end_col - cell.start_col) + self.context:width(cell)
174174
if width < 0 then
@@ -315,7 +315,7 @@ function Render:shift(column, side, amount)
315315
virt_text_pos = 'inline',
316316
})
317317
elseif amount < 0 then
318-
amount = self.context.conceal:adjust(amount, nil)
318+
amount = self.context.conceal:adjust(amount, '')
319319
self.marks:add(true, column.row, col + amount, {
320320
priority = 0,
321321
end_col = col,

0 commit comments

Comments
 (0)