Skip to content

Commit e1879e0

Browse files
feat: heading position right
## Details Request: #238 Adds a new option for `heading.position` for value `right`. When set icons will be added to the end of the heading line rather than at the beginning. This will also conceal the atx heading `#` values. To make this easier internally the query for headings has changed from getting markers, i.e. atx_h1_marker, to instead grab the entire heading line, i.e. atx_heading. From there we do some processing on the node to get different parts like the marker. Needed to make some minor adjustments to ranges as part of this change. Inline icons for headings now remove the space between the marker and the heading title, using the space of the icon itself rather than doing the double spaced thing. Users can add an additional space to the icons in the configuration to get back the old behavior. Added an offset to the list module `add_over` method to allow the starts / ends to by slightly modified but still to be based off of a specific node. Used this in a few other places where applicable. Added more unit tests for different heading configuration behaviors.
1 parent c83fc56 commit e1879e0

File tree

15 files changed

+440
-178
lines changed

15 files changed

+440
-178
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,9 @@ require('render-markdown').setup({
212212
-- Turn on / off any sign column related rendering
213213
sign = true,
214214
-- Determines how icons fill the available space:
215-
-- inline: underlying '#'s are concealed resulting in a left aligned icon
216-
-- overlay: result is left padded with spaces to hide any additional '#'
215+
-- right: '#'s are concealed and icon is appended to right side
216+
-- inline: '#'s are concealed and icon is inlined on left side
217+
-- overlay: icon is left padded with spaces and inserted on left hiding any additional '#'
217218
position = 'overlay',
218219
-- Replaces '#+' of 'atx_h._marker'
219220
-- The number of '#' in the heading determines the 'level'
@@ -619,8 +620,9 @@ require('render-markdown').setup({
619620
-- Turn on / off any sign column related rendering
620621
sign = true,
621622
-- Determines how icons fill the available space:
622-
-- inline: underlying '#'s are concealed resulting in a left aligned icon
623-
-- overlay: result is left padded with spaces to hide any additional '#'
623+
-- right: '#'s are concealed and icon is appended to right side
624+
-- inline: '#'s are concealed and icon is inlined on left side
625+
-- overlay: icon is left padded with spaces and inserted on left hiding any additional '#'
624626
position = 'overlay',
625627
-- Replaces '#+' of 'atx_h._marker'
626628
-- The number of '#' in the heading determines the 'level'

doc/render-markdown.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,9 @@ Default Configuration ~
259259
-- Turn on / off any sign column related rendering
260260
sign = true,
261261
-- Determines how icons fill the available space:
262-
-- inline: underlying '#'s are concealed resulting in a left aligned icon
263-
-- overlay: result is left padded with spaces to hide any additional '#'
262+
-- right: '#'s are concealed and icon is appended to right side
263+
-- inline: '#'s are concealed and icon is inlined on left side
264+
-- overlay: icon is left padded with spaces and inserted on left hiding any additional '#'
264265
position = 'overlay',
265266
-- Replaces '#+' of 'atx_h._marker'
266267
-- The number of '#' in the heading determines the 'level'
@@ -664,8 +665,9 @@ Heading Configuration ~
664665
-- Turn on / off any sign column related rendering
665666
sign = true,
666667
-- Determines how icons fill the available space:
667-
-- inline: underlying '#'s are concealed resulting in a left aligned icon
668-
-- overlay: result is left padded with spaces to hide any additional '#'
668+
-- right: '#'s are concealed and icon is appended to right side
669+
-- inline: '#'s are concealed and icon is inlined on left side
670+
-- overlay: icon is left padded with spaces and inserted on left hiding any additional '#'
669671
position = 'overlay',
670672
-- Replaces '#+' of 'atx_h._marker'
671673
-- The number of '#' in the heading determines the 'level'

lua/render-markdown/handler/markdown.lua

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,10 @@ function Handler.new(buf)
2424
[[
2525
(section) @section
2626
27-
(atx_heading [
28-
(atx_h1_marker)
29-
(atx_h2_marker)
30-
(atx_h3_marker)
31-
(atx_h4_marker)
32-
(atx_h5_marker)
33-
(atx_h6_marker)
34-
] @heading)
35-
(setext_heading) @heading
27+
[
28+
(atx_heading)
29+
(setext_heading)
30+
] @heading
3631
3732
(section (paragraph) @paragraph)
3833

lua/render-markdown/health.lua

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

66
---@private
7-
M.version = '7.5.11'
7+
M.version = '7.5.12'
88

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

lua/render-markdown/init.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ local M = {}
156156
---@field public left_margin? number
157157
---@field public min_width? integer
158158

159-
---@alias render.md.heading.Position 'overlay'|'inline'
159+
---@alias render.md.heading.Position 'overlay'|'inline'|'right'
160160
---@alias render.md.heading.Width 'full'|'block'
161161

162162
---@class (exact) render.md.UserHeading
@@ -330,8 +330,9 @@ M.default_config = {
330330
-- Turn on / off any sign column related rendering
331331
sign = true,
332332
-- Determines how icons fill the available space:
333-
-- inline: underlying '#'s are concealed resulting in a left aligned icon
334-
-- overlay: result is left padded with spaces to hide any additional '#'
333+
-- right: '#'s are concealed and icon is appended to right side
334+
-- inline: '#'s are concealed and icon is inlined on left side
335+
-- overlay: icon is left padded with spaces and inserted on left hiding any additional '#'
335336
position = 'overlay',
336337
-- Replaces '#+' of 'atx_h._marker'
337338
-- The number of '#' in the heading determines the 'level'

lua/render-markdown/lib/list.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ end
3232
---@param element boolean|render.md.Element
3333
---@param node render.md.Node
3434
---@param opts vim.api.keyset.set_extmark
35+
---@param offset? Range4
3536
---@return boolean
36-
function Marks:add_over(element, node, opts)
37-
opts.end_row = node.end_row
38-
opts.end_col = node.end_col
39-
return self:add(element, node.start_row, node.start_col, opts)
37+
function Marks:add_over(element, node, opts, offset)
38+
offset = offset or { 0, 0, 0, 0 }
39+
opts.end_row = node.end_row + offset[3]
40+
opts.end_col = node.end_col + offset[4]
41+
return self:add(element, node.start_row + offset[1], node.start_col + offset[2], opts)
4042
end
4143

4244
---@param element boolean|render.md.Element

lua/render-markdown/render/heading.lua

Lines changed: 90 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local colors = require('render-markdown.colors')
66

77
---@class render.md.data.Heading
88
---@field atx boolean
9+
---@field marker render.md.Node
910
---@field level integer
1011
---@field icon? string
1112
---@field sign? string
@@ -17,7 +18,6 @@ local colors = require('render-markdown.colors')
1718
---@field right_pad number
1819
---@field min_width integer
1920
---@field border boolean
20-
---@field end_row integer
2121

2222
---@class render.md.width.Heading
2323
---@field margin integer
@@ -37,15 +37,24 @@ function Render:setup()
3737
return false
3838
end
3939

40-
local atx, level = nil, nil
41-
if self.node.type == 'setext_heading' then
42-
atx, level = false, self.node:child('setext_h1_underline') ~= nil and 1 or 2
40+
local atx = nil
41+
local marker = nil
42+
local level = nil
43+
if self.node.type == 'atx_heading' then
44+
atx = true
45+
marker = assert(self.node:child_at(0), 'atx heading expected child marker')
46+
level = Str.width(marker.text)
47+
elseif self.node.type == 'setext_heading' then
48+
atx = false
49+
marker = assert(self.node:child_at(1), 'ext heading expected child underline')
50+
level = marker.type == 'setext_h1_underline' and 1 or 2
4351
else
44-
atx, level = true, Str.width(self.node.text)
52+
return false
4553
end
4654

4755
self.data = {
4856
atx = atx,
57+
marker = marker,
4958
level = level,
5059
icon = List.cycle(self.heading.icons, level),
5160
sign = List.cycle(self.heading.signs, level),
@@ -57,21 +66,23 @@ function Render:setup()
5766
right_pad = List.clamp(self.heading.right_pad, level) or 0,
5867
min_width = List.clamp(self.heading.min_width, level) or 0,
5968
border = List.clamp(self.heading.border, level) or false,
60-
end_row = self.node.end_row + (atx and 1 or 0),
6169
}
6270

6371
return true
6472
end
6573

6674
function Render:render()
67-
local width = self:width(self:icon())
6875
if self.heading.sign then
6976
self:sign(self.data.sign, self.data.foreground)
7077
end
78+
local width = self:width(self:icon())
7179
self:background(width)
72-
self:border(width)
7380
self:left_pad(width)
74-
self:conceal_underline()
81+
if self.data.atx then
82+
self:border(width)
83+
else
84+
self:conceal_underline()
85+
end
7586
end
7687

7788
---@private
@@ -84,58 +95,78 @@ function Render:icon()
8495
if self.data.background ~= nil then
8596
table.insert(highlight, self.data.background)
8697
end
87-
88-
if not self.data.atx then
98+
if self.data.atx then
99+
local marker = self.data.marker
100+
-- Add 1 to account for space after last `#`
101+
local width = self.context:width(marker) + 1
102+
if icon == nil or #highlight == 0 then
103+
return width
104+
end
105+
if self.heading.position == 'right' then
106+
self.marks:add_over(true, marker, {
107+
conceal = '',
108+
}, { 0, 0, 0, 1 })
109+
self.marks:add_over('head_icon', marker, {
110+
priority = 1000,
111+
virt_text = { { icon, highlight } },
112+
virt_text_pos = 'eol',
113+
})
114+
return 1 + Str.width(icon)
115+
else
116+
local padding = width - Str.width(icon)
117+
if self.heading.position == 'inline' or padding < 0 then
118+
local added = self.marks:add_over(true, marker, {
119+
virt_text = { { icon, highlight } },
120+
virt_text_pos = 'inline',
121+
conceal = '',
122+
}, { 0, 0, 0, 1 })
123+
return added and Str.width(icon) or width
124+
else
125+
self.marks:add_over('head_icon', marker, {
126+
virt_text = { { Str.pad(padding) .. icon, highlight } },
127+
virt_text_pos = 'overlay',
128+
})
129+
return width
130+
end
131+
end
132+
else
133+
local node = self.node
89134
if icon == nil or #highlight == 0 then
90135
return 0
91136
end
92-
local added = true
93-
for row = self.node.start_row, self.data.end_row - 1 do
94-
added = added
95-
and self.marks:add('head_icon', row, self.node.start_col, {
137+
if self.heading.position == 'right' then
138+
self.marks:add_over('head_icon', node, {
139+
priority = 1000,
140+
virt_text = { { icon, highlight } },
141+
virt_text_pos = 'eol',
142+
})
143+
return 1 + Str.width(icon)
144+
else
145+
local added = true
146+
for row = node.start_row, node.end_row - 1 do
147+
local added_row = self.marks:add('head_icon', row, node.start_col, {
96148
end_row = row,
97-
end_col = self.node.end_col,
98-
virt_text = { { row == self.node.start_row and icon or Str.pad(Str.width(icon)), highlight } },
149+
end_col = node.end_col,
150+
virt_text = { { row == node.start_row and icon or Str.pad(Str.width(icon)), highlight } },
99151
virt_text_pos = 'inline',
100152
})
153+
added = added and added_row
154+
end
155+
return added and Str.width(icon) or 0
101156
end
102-
return added and Str.width(icon) or 0
103-
end
104-
105-
-- For atx headings we add 1 to the available width to account for the space after the last `#`
106-
local width = self.context:width(self.node) + 1
107-
if icon == nil or #highlight == 0 then
108-
return width
109-
end
110-
111-
local padding = width - Str.width(icon)
112-
if self.heading.position == 'inline' or padding < 0 then
113-
local added = self.marks:add_over('head_icon', self.node, {
114-
virt_text = { { icon, highlight } },
115-
virt_text_pos = 'inline',
116-
conceal = '',
117-
})
118-
return added and Str.width(icon) + 1 or width
119-
else
120-
self.marks:add_over('head_icon', self.node, {
121-
virt_text = { { Str.pad(padding) .. icon, highlight } },
122-
virt_text_pos = 'overlay',
123-
})
124-
return width
125157
end
126158
end
127159

128160
---@private
129161
---@param icon_width integer
130162
---@return render.md.width.Heading
131163
function Render:width(icon_width)
132-
local text_width = nil
164+
local width = icon_width
133165
if self.data.atx then
134-
text_width = self.context:width(self.node:sibling('inline'))
166+
width = width + self.context:width(self.node:child('inline'))
135167
else
136-
text_width = vim.fn.max(Iter.list.map(self.node:lines(), Str.width))
168+
width = width + vim.fn.max(Iter.list.map(self.node:lines(), Str.width))
137169
end
138-
local width = icon_width + text_width
139170
local left_padding = self.context:resolve_offset(self.data.left_pad, width)
140171
local right_padding = self.context:resolve_offset(self.data.right_pad, width)
141172
width = math.max(left_padding + width + right_padding, self.data.min_width)
@@ -159,7 +190,7 @@ function Render:background(width)
159190
win_col = width.margin + width.content + self:indent(self.data.level)
160191
table.insert(padding, self:padding_text(vim.o.columns * 2))
161192
end
162-
for row = self.node.start_row, self.data.end_row - 1 do
193+
for row = self.node.start_row, self.node.end_row - 1 do
163194
self.marks:add('head_background', row, 0, {
164195
end_row = row + 1,
165196
hl_group = highlight,
@@ -179,8 +210,7 @@ end
179210
---@private
180211
---@param width render.md.width.Heading
181212
function Render:border(width)
182-
-- Only atx headings support borders
183-
if not self.data.border or not self.data.atx then
213+
if not self.data.border then
184214
return
185215
end
186216

@@ -226,13 +256,13 @@ function Render:border(width)
226256

227257
local line_below = line(self.heading.below)
228258
if not virtual and self:empty_line('below') then
229-
self.marks:add('head_border', self.node.end_row + 1, 0, {
259+
self.marks:add('head_border', self.node.end_row, 0, {
230260
virt_text = line_below,
231261
virt_text_pos = 'overlay',
232262
})
233-
self.context.last_heading = self.node.end_row + 1
263+
self.context.last_heading = self.node.end_row
234264
else
235-
self.marks:add(false, self.node.end_row, 0, {
265+
self.marks:add(false, self.node.start_row, 0, {
236266
virt_lines = { self:indent_virt_line(line_below, self.data.level) },
237267
})
238268
end
@@ -256,27 +286,21 @@ function Render:left_pad(width)
256286
if width.padding > 0 then
257287
table.insert(virt_text, self:padding_text(width.padding, self.data.background))
258288
end
259-
if #virt_text > 0 then
260-
for row = self.node.start_row, self.data.end_row - 1 do
261-
self.marks:add(false, row, 0, {
262-
priority = 0,
263-
virt_text = virt_text,
264-
virt_text_pos = 'inline',
265-
})
266-
end
289+
if #virt_text == 0 then
290+
return
291+
end
292+
for row = self.node.start_row, self.node.end_row - 1 do
293+
self.marks:add(false, row, 0, {
294+
priority = 0,
295+
virt_text = virt_text,
296+
virt_text_pos = 'inline',
297+
})
267298
end
268299
end
269300

270301
---@private
271302
function Render:conceal_underline()
272-
if self.data.atx then
273-
return
274-
end
275-
local node = self.node:child(string.format('setext_h%d_underline', self.data.level))
276-
if node == nil then
277-
return
278-
end
279-
self.marks:add_over(true, node, {
303+
self.marks:add_over(true, self.data.marker, {
280304
conceal = '',
281305
})
282306
end

lua/render-markdown/render/list_item.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,9 @@ end
7171

7272
---@private
7373
function Render:hide_marker()
74-
local node = self.data.marker
75-
self.marks:add('check_icon', node.start_row, node.start_col + self.data.spaces, {
76-
end_row = node.end_row,
77-
end_col = node.end_col,
74+
self.marks:add_over('check_icon', self.data.marker, {
7875
conceal = '',
79-
})
76+
}, { 0, self.data.spaces, 0, 0 })
8077
end
8178

8279
---@private

0 commit comments

Comments
 (0)