Skip to content

Commit 27cc6ce

Browse files
feat: always indent based on heading level rather than parent sections
## Details This is a follow up to: #164 Following this PR several edge cases were fixed around intation when used with heading borders. Along with edge cases when not starting with an H1 in the document. However there was still a problem when indentation level was reduced which caused additional space to be added to the top border. Rather than patching this I went the direction of unifying the definition of a heading level. With this content underneath a heading is always indented to that level, i.e. an H3 will always have 6 starting spaces. Before this would only happen if an H1 & an H2 were included before, this is no longer the case. To do this logic to calculate the change in level between sections was added. Also H1s become less of a special case, we simply define the default document level as 1 so H1s end up at the same level leading to no change. This on its own would cause problems with upper borders since they would be indented based on the previous section. To fix this the range that a section covers was changed from what treesitter provides. Empty lines above a section belong to the lower section making the indentation more or less just work.
1 parent cb935af commit 27cc6ce

File tree

16 files changed

+321
-128
lines changed

16 files changed

+321
-128
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
[12fdb6f](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/12fdb6f6623cb7e20da75be68858f12e1e578ffd)
1717
- leading spaces in checkbox bullet [#158](https://github.com/MeanderingProgrammer/render-markdown.nvim/issues/158)
1818
[06337f6](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/06337f64367ef1f1115f0a9ba41e49b84a04b1a4)
19+
- heading borders with indentation [#164](https://github.com/MeanderingProgrammer/render-markdown.nvim/pull/164)
20+
21+
### Collaborator Shoutouts
22+
23+
- @lukas-reineke
1924

2025
## 6.3.0 (2024-08-29)
2126

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: 2024 September 11
1+
*render-markdown.txt* For 0.10.0 Last change: 2024 September 12
22

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

lua/render-markdown/core/node_info.lua

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,29 +53,25 @@ function NodeInfo:has_error()
5353
end
5454

5555
---@return integer
56-
function NodeInfo:level()
57-
local level = 0
58-
local parent = self.node:parent()
59-
while parent ~= nil and parent:type() ~= 'document' do
60-
if parent:type() == 'section' then
61-
local child = parent:child(0)
62-
child = child and child:child(0)
63-
local is_first_heading = child and child:type() == 'atx_h1_marker'
64-
if not is_first_heading then
65-
level = level + 1
66-
end
67-
end
68-
parent = parent:parent()
69-
end
70-
return level
56+
function NodeInfo:parent_heading_level()
57+
local parent = self:parent('section')
58+
return parent ~= nil and parent:heading_level() or 1
59+
end
60+
61+
---@return integer
62+
function NodeInfo:heading_level()
63+
assert(self.type == 'section', 'Node must be a section')
64+
local heading = self:child('atx_heading')
65+
local node = heading ~= nil and heading.node:child(0) or nil
66+
-- Counts the number of hashtags in the heading marker
67+
return node ~= nil and #vim.treesitter.get_node_text(node, self.buf) or 1
7168
end
7269

7370
---Walk through parent nodes, count the number of target nodes
7471
---@param target string
7572
---@return integer
7673
function NodeInfo:level_in_section(target)
77-
local level = 0
78-
local parent = self.node:parent()
74+
local level, parent = 0, self.node:parent()
7975
while parent ~= nil and in_section(parent) do
8076
if parent:type() == target then
8177
level = level + 1
@@ -132,18 +128,20 @@ function NodeInfo:for_each_child(callback)
132128
end
133129
end
134130

135-
---@param position 'above'|'below'|'on'
131+
---@param position 'above'|'below'|'first'|'last'
136132
---@return string?
137133
function NodeInfo:line(position)
138-
local start_row = nil
134+
local row = nil
139135
if position == 'above' then
140-
start_row = self.start_row - 1
136+
row = self.start_row - 1
141137
elseif position == 'below' then
142-
start_row = self.end_row + 1
143-
else
144-
start_row = self.start_row
138+
row = self.end_row + 1
139+
elseif position == 'first' then
140+
row = self.start_row
141+
elseif position == 'last' then
142+
row = self.end_row - 1
145143
end
146-
return vim.api.nvim_buf_get_lines(self.buf, start_row, start_row + 1, false)[1]
144+
return row ~= nil and vim.api.nvim_buf_get_lines(self.buf, row, row + 1, false)[1] or nil
147145
end
148146

149147
---@return string[]

lua/render-markdown/handler/markdown.lua

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function Handler.new(buf)
2323
code = require('render-markdown.render.code'),
2424
heading = require('render-markdown.render.heading'),
2525
quote = require('render-markdown.render.quote'),
26+
section = require('render-markdown.render.section'),
2627
table = require('render-markdown.render.table'),
2728
}
2829
return self
@@ -38,8 +39,6 @@ function Handler:parse(root)
3839
if render:setup() then
3940
render:render()
4041
end
41-
elseif capture == 'section' then
42-
self:section(info)
4342
elseif capture == 'dash' then
4443
self:dash(info)
4544
elseif capture == 'list_marker' then
@@ -55,31 +54,6 @@ function Handler:parse(root)
5554
return self.marks:get()
5655
end
5756

58-
---@private
59-
---@param info render.md.NodeInfo
60-
function Handler:section(info)
61-
local indent = self.config.indent
62-
if not indent.enabled then
63-
return
64-
end
65-
66-
-- Do not add any indentation on unknown or first level
67-
local heading = info:child('atx_heading')
68-
if heading == nil or heading:child('atx_h1_marker') ~= nil then
69-
return
70-
end
71-
72-
-- Each level stacks inline marks so we do not need to multiply spaces
73-
-- However skipping a level, i.e. 2 -> 5, will only add one level of spaces
74-
for row = info.start_row, info.end_row - 1 do
75-
self.marks:add(false, row, 0, {
76-
priority = 0,
77-
virt_text = { { str.spaces(indent.per_level), 'Normal' } },
78-
virt_text_pos = 'inline',
79-
})
80-
end
81-
end
82-
8357
---@private
8458
---@param info render.md.NodeInfo
8559
function Handler:dash(info)

lua/render-markdown/handler/markdown_inline.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function Handler:shortcut(info)
6767
return
6868
end
6969

70-
local line = info:line('on')
70+
local line = info:line('first')
7171
if line ~= nil and line:find('[' .. info.text .. ']', 1, true) ~= nil then
7272
self:wiki_link(info)
7373
return

lua/render-markdown/health.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local M = {}
55

66
---@private
77
---@type string
8-
M.version = '6.3.7'
8+
M.version = '6.3.8'
99

1010
function M.check()
1111
vim.health.start('render-markdown.nvim [version]')

lua/render-markdown/render/base.lua

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ end
4141

4242
---@protected
4343
---@param line { [1]: string, [2]: string }[]
44-
---@param level integer?
4544
---@return { [1]: string, [2]: string }[]
46-
function Base:indent_virt_line(line, level)
45+
function Base:indent_virt_line(line)
4746
local indent = self.config.indent
4847
if not indent.enabled then
4948
return line
5049
end
51-
level = level or self.info:level()
50+
local level = self.info:parent_heading_level() - 1
5251
if level <= 0 then
5352
return line
5453
end

lua/render-markdown/render/heading.lua

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,6 @@ function Render:border(width)
175175
{ self.heading.above:rep(width - self.heading.left_pad - prefix), background },
176176
}
177177
if str.width(self.info:line('above')) == 0 and self.info.start_row - 1 ~= self.context.last_heading then
178-
if self.data.level > 1 then
179-
line_above = self:indent_virt_line(line_above, 1)
180-
end
181178
self.marks:add(true, self.info.start_row - 1, 0, {
182179
virt_text = line_above,
183180
virt_text_pos = 'overlay',
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
local Base = require('render-markdown.render.base')
2+
local str = require('render-markdown.core.str')
3+
4+
---@class render.md.render.Section: render.md.Renderer
5+
---@field private indent render.md.Indent
6+
---@field private level_change integer
7+
local Render = setmetatable({}, Base)
8+
Render.__index = Render
9+
10+
---@param marks render.md.Marks
11+
---@param config render.md.buffer.Config
12+
---@param context render.md.Context
13+
---@param info render.md.NodeInfo
14+
---@return render.md.Renderer
15+
function Render:new(marks, config, context, info)
16+
return Base.new(self, marks, config, context, info)
17+
end
18+
19+
---@return boolean
20+
function Render:setup()
21+
self.indent = self.config.indent
22+
if not self.indent.enabled then
23+
return false
24+
end
25+
26+
local current_level = self.info:heading_level()
27+
local parent_level = self.info:parent_heading_level()
28+
self.level_change = current_level - parent_level
29+
30+
-- Nothing to do if there is not a change in level
31+
if self.level_change <= 0 then
32+
return false
33+
end
34+
35+
return true
36+
end
37+
38+
function Render:render()
39+
-- Do include empty line in previous section
40+
local start_offset = str.width(self.info:line('above')) == 0 and 1 or 0
41+
local start_row = math.max(self.info.start_row - start_offset, 0)
42+
43+
-- Do not include empty line at the end of current section
44+
local end_offset = str.width(self.info:line('last')) == 0 and 1 or 0
45+
local end_row = self.info.end_row - 1 - end_offset
46+
47+
-- Each level stacks inline marks so we only need to multiply based on any skipped levels
48+
for row = start_row, end_row do
49+
self.marks:add(false, row, 0, {
50+
priority = 0,
51+
virt_text = { { str.spaces(self.indent.per_level * self.level_change), 'Normal' } },
52+
virt_text_pos = 'inline',
53+
})
54+
end
55+
end
56+
57+
return Render

tests/ad_hoc_spec.lua

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ local function setext_heading(start_row, end_row, level)
1111
local foreground = util.hl(string.format('H%d', level))
1212
local background = util.hl(string.format('H%dBg', level))
1313

14-
local result = {}
15-
1614
---@type render.md.MarkInfo
17-
local icon_mark = {
18-
row = { start_row, end_row + 1 },
19-
col = { 0, 0 },
20-
virt_text = { { icon, { foreground, background } } },
21-
virt_text_pos = 'inline',
15+
local sign_mark = {
16+
row = { start_row },
17+
col = { 0 },
18+
sign_text = '󰫎 ',
19+
sign_hl_group = util.hl('_' .. foreground .. '_' .. util.hl('Sign')),
2220
}
23-
table.insert(result, icon_mark)
24-
21+
local result = { sign_mark }
2522
for row = start_row, end_row do
2623
---@type render.md.MarkInfo
2724
local background_mark = {
@@ -32,23 +29,21 @@ local function setext_heading(start_row, end_row, level)
3229
}
3330
table.insert(result, background_mark)
3431
end
35-
32+
---@type render.md.MarkInfo
33+
local icon_mark = {
34+
row = { start_row, end_row + 1 },
35+
col = { 0, 0 },
36+
virt_text = { { icon, { foreground, background } } },
37+
virt_text_pos = 'inline',
38+
}
39+
table.insert(result, 3, icon_mark)
3640
---@type render.md.MarkInfo
3741
local conceal_mark = {
3842
row = { end_row, end_row },
3943
col = { 0, 3 },
4044
conceal = '',
4145
}
42-
table.insert(result, conceal_mark)
43-
44-
---@type render.md.MarkInfo
45-
local sign_mark = {
46-
row = { start_row },
47-
col = { 0 },
48-
sign_text = '󰫎 ',
49-
sign_hl_group = util.hl('_' .. foreground .. '_' .. util.hl('Sign')),
50-
}
51-
table.insert(result, 3, sign_mark)
46+
table.insert(result, #result, conceal_mark)
5247
return result
5348
end
5449

0 commit comments

Comments
 (0)