Skip to content

Commit da3e146

Browse files
feat: combine highlights for links
## Details Request: #413 Link icons are added using inline virtual text. Since we do not set an `hl_mode` property the default value of `replace` is used. This is fine in most context but in headings for instance, where we highlight the background of the line the icons will continue to have their own default background so will stick out. The fix for this is to specify `hl_mode = combine` when we add link icons, which results in the background getting picked up from the background mark. Minor other changes: - Use method name `padding` consistently in renderers rather than different ones like `side_padding`, `left_pad`, etc. - Add an enum for positions in node - Raise an error when an invalid position is provided to `Node:line`, since this is only used internally it would mean there's definitely a bug somewhere that we'd otherwise swallow, include the row number in the output
1 parent 6910fe1 commit da3e146

File tree

16 files changed

+161
-157
lines changed

16 files changed

+161
-157
lines changed

doc/custom-handlers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Each handler must conform to the following interface:
2727
---@field opts render.md.mark.Opts
2828

2929
---@class render.md.mark.Opts: vim.api.keyset.set_extmark
30+
---@field hl_mode? 'replace'|'combine'|'blend'
3031
---@field virt_text? render.md.mark.Line
3132
---@field virt_text_pos? 'eol'|'inline'|'overlay'
3233
---@field virt_lines? render.md.mark.Line[]

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.11.0 Last change: 2025 April 24
1+
*render-markdown.txt* For 0.11.0 Last change: 2025 April 25
22

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

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.3.11'
8+
M.version = '8.3.12'
99

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

lua/render-markdown/lib/marks.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local log = require('render-markdown.core.log')
1010
---@field opts render.md.mark.Opts
1111

1212
---@class render.md.mark.Opts: vim.api.keyset.set_extmark
13+
---@field hl_mode? 'replace'|'combine'|'blend'
1314
---@field virt_text? render.md.mark.Line
1415
---@field virt_text_pos? 'eol'|'inline'|'overlay'
1516
---@field virt_lines? render.md.mark.Line[]

lua/render-markdown/lib/node.lua

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
local Iter = require('render-markdown.lib.iter')
22
local Str = require('render-markdown.lib.str')
33

4+
---@enum (key) render.md.node.Position
5+
local Position = {
6+
above = 'above',
7+
first = 'first',
8+
below = 'below',
9+
last = 'last',
10+
}
11+
412
---@class render.md.Node
513
---@field private buf integer
614
---@field private node TSNode
@@ -194,25 +202,24 @@ function Node:after()
194202
return vim.api.nvim_buf_get_text(self.buf, row, col, row, col + 1, {})[1]
195203
end
196204

197-
---@param position 'above'|'first'|'below'|'last'
205+
---@param position render.md.node.Position
198206
---@param by integer
199-
---@return string?
207+
---@return integer, string?
200208
function Node:line(position, by)
201209
local row = nil
202210
local single = self.start_row == self.end_row
203-
if position == 'above' then
211+
if position == Position.above then
204212
row = self.start_row - by
205-
elseif position == 'first' then
213+
elseif position == Position.first then
206214
row = self.start_row + by
207-
elseif position == 'below' then
215+
elseif position == Position.below then
208216
row = self.end_row - (single and 0 or 1) + by
209-
elseif position == 'last' then
217+
elseif position == Position.last then
210218
row = self.end_row - (single and 0 or 1) - by
219+
else
220+
error('invalid position: ' .. position)
211221
end
212-
if row == nil then
213-
return nil
214-
end
215-
return vim.api.nvim_buf_get_lines(self.buf, row, row + 1, false)[1]
222+
return row, vim.api.nvim_buf_get_lines(self.buf, row, row + 1, false)[1]
216223
end
217224

218225
---@return integer[]

lua/render-markdown/render/base.lua

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,9 @@ function Base:scope(element, node, highlight)
102102
end
103103

104104
---@protected
105-
---@param icon string
106-
---@param highlight string
107105
---@param destination string
108-
---@return string, string
109-
function Base:dest(icon, highlight, destination)
106+
---@param icon render.md.mark.Text
107+
function Base:link_icon(destination, icon)
110108
local options = Iter.table.filter(self.config.link.custom, function(custom)
111109
if custom.kind == 'suffix' then
112110
return vim.endswith(destination, custom.pattern)
@@ -117,8 +115,11 @@ function Base:dest(icon, highlight, destination)
117115
Iter.list.sort(options, function(custom)
118116
return custom.priority or Str.width(custom.pattern)
119117
end)
120-
local result = options[#options] or {}
121-
return result.icon or icon, result.highlight or highlight
118+
local result = options[#options]
119+
if result ~= nil then
120+
icon[1] = result.icon
121+
icon[2] = result.highlight or icon[2]
122+
end
122123
end
123124

124125
---@protected

lua/render-markdown/render/bullet.lua

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,28 @@ local Str = require('render-markdown.lib.str')
44

55
---@class render.md.bullet.Data
66
---@field marker render.md.Node
7-
---@field icons render.md.bullet.Text
87
---@field spaces integer
98
---@field checkbox? render.md.checkbox.custom.Config
109

1110
---@class render.md.render.Bullet: render.md.Render
12-
---@field private info render.md.bullet.Config
1311
---@field private data render.md.bullet.Data
1412
local Render = setmetatable({}, Base)
1513
Render.__index = Render
1614

1715
---@return boolean
1816
function Render:setup()
19-
self.info = self.config.bullet
20-
2117
local marker = self.node:child_at(0)
2218
if marker == nil then
2319
return false
2420
end
25-
26-
local ordered_types = { 'list_marker_dot', 'list_marker_parenthesis' }
27-
local ordered = vim.tbl_contains(ordered_types, marker.type)
2821
self.data = {
2922
marker = marker,
30-
icons = ordered and self.info.ordered_icons or self.info.icons,
3123
-- List markers from tree-sitter should have leading spaces removed, however there are edge
3224
-- cases in the parser: https://github.com/tree-sitter-grammars/tree-sitter-markdown/issues/127
3325
-- As a result we account for leading spaces here, can remove if this gets fixed upstream
3426
spaces = Str.spaces('start', marker.text),
3527
checkbox = self.context:get_checkbox(self.node.start_row),
3628
}
37-
3829
return true
3930
end
4031

@@ -47,23 +38,30 @@ function Render:render()
4738
self:highlight_scope('check_scope', scope_highlight)
4839
end
4940
else
50-
if self.context:skip(self.info) then
41+
local info = self.config.bullet
42+
if self.context:skip(info) then
5143
return
5244
end
45+
46+
local ordered_types = { 'list_marker_dot', 'list_marker_parenthesis' }
47+
local ordered = vim.tbl_contains(ordered_types, self.data.marker.type)
48+
local icons = ordered and info.ordered_icons or info.icons
49+
5350
local level, root = self.node:level_in_section('list')
5451
---@type render.md.bullet.Context
5552
local ctx = {
5653
level = level,
5754
index = self.node:sibling_count('list_item'),
5855
value = self.data.marker.text,
5956
}
60-
local icon = self:get_text(ctx, self.data.icons)
61-
local highlight = self:get_text(ctx, self.info.highlight)
62-
local scope_highlight = self:get_text(ctx, self.info.scope_highlight)
63-
local left_pad = self:get_int(ctx, self.info.left_pad)
64-
local right_pad = self:get_int(ctx, self.info.right_pad)
65-
self:add_icon(icon, highlight)
66-
self:add_padding(left_pad, right_pad, root)
57+
58+
local icon = self:get_text(icons, ctx)
59+
local highlight = self:get_text(info.highlight, ctx)
60+
local scope_highlight = self:get_text(info.scope_highlight, ctx)
61+
local left_pad = self:get_int(info.left_pad, ctx)
62+
local right_pad = self:get_int(info.right_pad, ctx)
63+
self:icon(icon, highlight)
64+
self:padding(root, left_pad, right_pad)
6765
self:highlight_scope(true, scope_highlight)
6866
end
6967
end
@@ -74,16 +72,9 @@ function Render:has_checkbox()
7472
if self.context:skip(self.config.checkbox) then
7573
return false
7674
end
77-
if self.data.checkbox ~= nil then
78-
return true
79-
end
80-
if self.data.marker:sibling('task_list_marker_unchecked') ~= nil then
81-
return true
82-
end
83-
if self.data.marker:sibling('task_list_marker_checked') ~= nil then
84-
return true
85-
end
86-
return false
75+
return self.data.checkbox ~= nil
76+
or self.data.marker:sibling('task_list_marker_unchecked') ~= nil
77+
or self.data.marker:sibling('task_list_marker_checked') ~= nil
8778
end
8879

8980
---@private
@@ -100,10 +91,10 @@ function Render:highlight_scope(element, highlight)
10091
end
10192

10293
---@private
103-
---@param ctx render.md.bullet.Context
10494
---@param values render.md.bullet.Text
95+
---@param ctx render.md.bullet.Context
10596
---@return string?
106-
function Render:get_text(ctx, values)
97+
function Render:get_text(values, ctx)
10798
if type(values) == 'function' then
10899
return values(ctx)
109100
elseif type(values) == 'string' then
@@ -119,10 +110,10 @@ function Render:get_text(ctx, values)
119110
end
120111

121112
---@private
122-
---@param ctx render.md.bullet.Context
123113
---@param value render.md.bullet.Int
114+
---@param ctx render.md.bullet.Context
124115
---@return integer
125-
function Render:get_int(ctx, value)
116+
function Render:get_int(value, ctx)
126117
if type(value) == 'function' then
127118
return value(ctx)
128119
else
@@ -133,7 +124,7 @@ end
133124
---@private
134125
---@param icon string?
135126
---@param highlight string?
136-
function Render:add_icon(icon, highlight)
127+
function Render:icon(icon, highlight)
137128
if icon == nil or highlight == nil then
138129
return
139130
end
@@ -147,56 +138,52 @@ function Render:add_icon(icon, highlight)
147138
end
148139

149140
---@private
141+
---@param root? render.md.Node
150142
---@param left_pad integer
151143
---@param right_pad integer
152-
---@param root? render.md.Node
153-
function Render:add_padding(left_pad, right_pad, root)
144+
function Render:padding(root, left_pad, right_pad)
154145
if left_pad <= 0 and right_pad <= 0 then
155146
return
156147
end
157148
local start_row, end_row = self.node.start_row, self:end_row(root)
149+
local left_line = self:append({}, left_pad)
150+
local right_line = self:append({}, right_pad)
158151
for row = start_row, end_row - 1 do
159152
local left = root ~= nil and root.start_col or self.node.start_col
160153
local right = row == start_row and self.data.marker.end_col - 1 or left
161-
self:side_padding(row, left, left_pad)
162-
self:side_padding(row, right, right_pad)
154+
if #left_line > 0 then
155+
self.marks:add(false, row, left, {
156+
priority = 0,
157+
virt_text = left_line,
158+
virt_text_pos = 'inline',
159+
})
160+
end
161+
if #right_line > 0 then
162+
self.marks:add(false, row, right, {
163+
priority = 0,
164+
virt_text = right_line,
165+
virt_text_pos = 'inline',
166+
})
167+
end
163168
end
164169
end
165170

166171
---@private
167172
---@param root? render.md.Node
168173
---@return integer
169174
function Render:end_row(root)
170-
local next_list = self.node:child('list')
171-
if next_list ~= nil then
172-
return next_list.start_row
175+
local sub_list = self.node:child('list')
176+
if sub_list ~= nil then
177+
return sub_list.start_row
178+
elseif root == nil then
179+
return self.node.end_row
173180
else
181+
-- on the last item of the root list ignore the last line if empty
174182
local row = self.node.end_row
175-
-- On the last item of the root list ignore the last line if it is empty
176-
if
177-
root ~= nil
178-
and root.end_row == row
179-
and Str.width(root:line('last', 0)) == 0
180-
then
181-
return row - 1
182-
else
183-
return row
184-
end
185-
end
186-
end
187-
188-
---@private
189-
---@param row integer
190-
---@param col integer
191-
---@param amount integer
192-
function Render:side_padding(row, col, amount)
193-
local line = self:append({}, amount)
194-
if #line > 0 then
195-
self.marks:add(false, row, col, {
196-
priority = 0,
197-
virt_text = line,
198-
virt_text_pos = 'inline',
199-
})
183+
local _, line = root:line('last', 0)
184+
local ignore_last = root.end_row == row and Str.width(line) == 0
185+
local offset = ignore_last and 1 or 0
186+
return row - offset
200187
end
201188
end
202189

lua/render-markdown/render/code.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function Render:render()
9494
if background then
9595
self:background(start_row + 1, end_row - 1, self.info.highlight)
9696
end
97-
self:left_pad(background)
97+
self:padding(background)
9898
end
9999

100100
---@private
@@ -233,7 +233,7 @@ end
233233

234234
---@private
235235
---@param background boolean
236-
function Render:left_pad(background)
236+
function Render:padding(background)
237237
local col = self.node.start_col
238238
local start_row, end_row = self.node.start_row, self.node.end_row - 1
239239
local empty, widths = {}, col == 0 and {} or self.node:widths()

lua/render-markdown/render/code_inline.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ end
2121
function Render:render()
2222
local highlight = self.info.highlight_inline
2323
self.marks:over('code_background', self.node, { hl_group = highlight })
24-
self:side_padding(highlight, true)
25-
self:side_padding(highlight, false)
24+
self:padding(highlight, true)
25+
self:padding(highlight, false)
2626
end
2727

2828
---@private
2929
---@param highlight string
3030
---@param left boolean
31-
function Render:side_padding(highlight, left)
31+
function Render:padding(highlight, left)
3232
local line, icon_highlight = {}, colors.bg_to_fg(highlight)
3333
if left then
3434
self:append(line, self.info.inline_left, icon_highlight)

0 commit comments

Comments
 (0)