Skip to content

Commit f9badfb

Browse files
committedMar 24, 2025
feat: avoid inline text for checkboxes when possible
## Details Request: #378 Previously checkboxes were rendered by concealing the underlying text and inlining the icon as virtual text. This works but would mess with the cursor positions while scrolling due to how neovim handles showing and hiding inline virtual text. To avoid this we now use information about the amount of space available to us, i.e. the width of the text in square brackets compared to the width of the icon we are rendering, to add more clever marks. Only when the icon is larger than the space available do we do the same thing as before and fully conceal + inline. Otherwise we use overlay virtual text to show the icon and conceal any underlying text that extends under the icon (if any). When doing this we also take into account the new `checkbox.right_pad` option (default value 1), and similarly attempt to use the space available for the padding. If the padding extends outside the available space then the remaining amount is added as inline virtual text, requires neovim >= `0.10.0` to work. The default value of 1 is chosen so that the rendered text looks identical in most cases to before, since we now treat the space after the checkbox as space available to us. ### Related Impacts To maintain parity with how checked and unchecked checkboxes are handled by `markdown`, we now require that there is a space following custom states. This allows us to better determine exactly how much space we have to work with. The `checkbox.position` configuration option was removed. We now infer how to display checkboxes best based on the space available to us and the icon we are about to show. Any potential differences in how things are displayed, can likely be changed back by using the new `checkbox.right_pad` option. Most users should not see any differences. Any users of `checkbox.position` should simply remove it from their configuration, keeping it is fine, it just doesn't do anything. Custom checkbox states will now work for versions of neovim older than `0.10.0`, but since the plugin as a whole still requires `0.9.0` at a minimum this only adds a couple supported versions for that specific feature, and there are some limitations to its usage.
1 parent 5cec1bb commit f9badfb

14 files changed

+107
-86
lines changed
 

Diff for: ‎README.md

+5-9
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Plugin to improve viewing Markdown files in Neovim
3232
- Code inline: background
3333
- Horizontal breaks: icon, color, width
3434
- List bullets: icon, color, padding [^1]
35-
- Checkboxes: icon, color, user defined states [^1]
35+
- Checkboxes: icon, color, user defined states
3636
- Block quotes: icon, color, line breaks [^1]
3737
- Callouts: icon, color, user defined values, Github & Obsidian defaults
3838
- Tables: border, color, alignment indicator, auto align cells [^1]
@@ -494,10 +494,8 @@ require('render-markdown').setup({
494494
enabled = true,
495495
-- Additional modes to render checkboxes.
496496
render_modes = false,
497-
-- Determines how icons fill the available space.
498-
-- | inline | underlying text is concealed resulting in a left aligned icon |
499-
-- | overlay | result is left padded with spaces to hide any additional text |
500-
position = 'inline',
497+
-- Padding to add to the right of checkboxes.
498+
right_pad = 1,
501499
unchecked = {
502500
-- Replaces '[ ]' of 'task_list_marker_unchecked'.
503501
icon = '󰄱 ',
@@ -1086,10 +1084,8 @@ require('render-markdown').setup({
10861084
enabled = true,
10871085
-- Additional modes to render checkboxes.
10881086
render_modes = false,
1089-
-- Determines how icons fill the available space.
1090-
-- | inline | underlying text is concealed resulting in a left aligned icon |
1091-
-- | overlay | result is left padded with spaces to hide any additional text |
1092-
position = 'inline',
1087+
-- Padding to add to the right of checkboxes.
1088+
right_pad = 1,
10931089
unchecked = {
10941090
-- Replaces '[ ]' of 'task_list_marker_unchecked'.
10951091
icon = '󰄱 ',

Diff for: ‎benches/readme_spec.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local util = require('benches.util')
44

55
describe('README.md', function()
66
it('default', function()
7-
local base_marks = 114
7+
local base_marks = 113
88
util.less_than(util.setup('README.md'), 60)
99
util.num_marks(base_marks)
1010

Diff for: ‎doc/render-markdown.txt

+5-9
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Plugin to improve viewing Markdown files in Neovim
6262
- Code inline: background
6363
- Horizontal breaks: icon, color, width
6464
- List bullets: icon, color, padding
65-
- Checkboxes: icon, color, user defined states
65+
- Checkboxes: icon, color, user defined states
6666
- Block quotes: icon, color, line breaks
6767
- Callouts: icon, color, user defined values, Github & Obsidian defaults
6868
- Tables: border, color, alignment indicator, auto align cells
@@ -559,10 +559,8 @@ Default Configuration ~
559559
enabled = true,
560560
-- Additional modes to render checkboxes.
561561
render_modes = false,
562-
-- Determines how icons fill the available space.
563-
-- | inline | underlying text is concealed resulting in a left aligned icon |
564-
-- | overlay | result is left padded with spaces to hide any additional text |
565-
position = 'inline',
562+
-- Padding to add to the right of checkboxes.
563+
right_pad = 1,
566564
unchecked = {
567565
-- Replaces '[ ]' of 'task_list_marker_unchecked'.
568566
icon = '󰄱 ',
@@ -1139,10 +1137,8 @@ Checkbox Configuration ~
11391137
enabled = true,
11401138
-- Additional modes to render checkboxes.
11411139
render_modes = false,
1142-
-- Determines how icons fill the available space.
1143-
-- | inline | underlying text is concealed resulting in a left aligned icon |
1144-
-- | overlay | result is left padded with spaces to hide any additional text |
1145-
position = 'inline',
1140+
-- Padding to add to the right of checkboxes.
1141+
right_pad = 1,
11461142
unchecked = {
11471143
-- Replaces '[ ]' of 'task_list_marker_unchecked'.
11481144
icon = '󰄱 ',

Diff for: ‎lua/render-markdown/health.lua

+1-1
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.1.9'
8+
M.version = '8.1.10'
99

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

Diff for: ‎lua/render-markdown/init.lua

+4-7
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,8 @@ local M = {}
163163
---@field highlight? string
164164
---@field scope_highlight? string
165165

166-
---@alias render.md.checkbox.Position 'overlay'|'inline'
167-
168166
---@class (exact) render.md.UserCheckbox: render.md.UserBaseComponent
169-
---@field position? render.md.checkbox.Position
167+
---@field right_pad? integer
170168
---@field unchecked? render.md.UserCheckboxComponent
171169
---@field checked? render.md.UserCheckboxComponent
172170
---@field custom? table<string, render.md.UserCustomCheckbox>
@@ -285,6 +283,7 @@ local M = {}
285283
---| 'sign'
286284

287285
---@alias render.md.config.conceal.Ignore table<render.md.Element, render.md.Modes>
286+
---@alias render.md.mark.Element boolean|render.md.Element
288287

289288
---@class (exact) render.md.UserAntiConceal
290289
---@field enabled? boolean
@@ -656,10 +655,8 @@ M.default_config = {
656655
enabled = true,
657656
-- Additional modes to render checkboxes.
658657
render_modes = false,
659-
-- Determines how icons fill the available space.
660-
-- | inline | underlying text is concealed resulting in a left aligned icon |
661-
-- | overlay | result is left padded with spaces to hide any additional text |
662-
position = 'inline',
658+
-- Padding to add to the right of checkboxes.
659+
right_pad = 1,
663660
unchecked = {
664661
-- Replaces '[ ]' of 'task_list_marker_unchecked'.
665662
icon = '󰄱 ',

Diff for: ‎lua/render-markdown/lib/list.lua

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function Marks:get()
2929
return self.marks
3030
end
3131

32-
---@param element boolean|render.md.Element
32+
---@param element render.md.mark.Element
3333
---@param node render.md.Node
3434
---@param opts render.md.MarkOpts
3535
---@param offset? Range4
@@ -41,7 +41,7 @@ function Marks:add_over(element, node, opts, offset)
4141
return self:add(element, node.start_row + offset[1], node.start_col + offset[2], opts)
4242
end
4343

44-
---@param element boolean|render.md.Element
44+
---@param element render.md.mark.Element
4545
---@param start_row integer
4646
---@param start_col integer
4747
---@param opts render.md.MarkOpts
@@ -71,7 +71,7 @@ function Marks:add(element, start_row, start_col, opts)
7171
end
7272

7373
---@private
74-
---@param element boolean|render.md.Element
74+
---@param element render.md.mark.Element
7575
---@return boolean
7676
function Marks:conceal(element)
7777
if type(element) == 'boolean' then

Diff for: ‎lua/render-markdown/lib/node.lua

+6
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,10 @@ function Node:lines()
191191
return vim.api.nvim_buf_get_lines(self.buf, self.start_row, self.end_row, false)
192192
end
193193

194+
---@return string?
195+
function Node:after()
196+
local row, col = self.end_row, self.end_col
197+
return vim.api.nvim_buf_get_text(self.buf, row, col, row, col + 1, {})[1]
198+
end
199+
194200
return Node

Diff for: ‎lua/render-markdown/render/base.lua

+45-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,51 @@ function Base:sign(enabled, text, highlight)
4646
end
4747

4848
---@protected
49-
---@param element boolean|render.md.Element
49+
---@param icon string
50+
---@param highlight string
51+
---@return boolean
52+
function Base:check_icon(icon, highlight)
53+
local line = self:append({}, icon, highlight)
54+
local space = self.context:width(self.node) + 1 - Str.width(icon)
55+
local right_pad = self.config.checkbox.right_pad
56+
if space < 0 then
57+
-- Not enough space to fit the icon in-place
58+
return self.marks:add_over('check_icon', self.node, {
59+
virt_text = self:append(line, right_pad),
60+
virt_text_pos = 'inline',
61+
conceal = '',
62+
}, { 0, 0, 0, 1 })
63+
else
64+
local fits = math.min(space, right_pad)
65+
self:append(line, fits)
66+
space, right_pad = space - fits, right_pad - fits
67+
local row = self.node.start_row
68+
local start_col, end_col = self.node.start_col, self.node.end_col + 1
69+
self.marks:add('check_icon', row, start_col, {
70+
end_col = end_col - space,
71+
virt_text = line,
72+
virt_text_pos = 'overlay',
73+
})
74+
if space > 0 then
75+
-- Hide extra space after the icon
76+
self.marks:add('check_icon', row, end_col - space, {
77+
end_col = end_col,
78+
conceal = '',
79+
})
80+
end
81+
if right_pad > 0 then
82+
-- Add padding
83+
self.marks:add('check_icon', row, end_col, {
84+
virt_text = self:append({}, right_pad),
85+
virt_text_pos = 'inline',
86+
})
87+
end
88+
return true
89+
end
90+
end
91+
92+
---@protected
93+
---@param element render.md.mark.Element
5094
---@param node render.md.Node?
5195
---@param highlight? string
5296
function Base:scope(element, node, highlight)

Diff for: ‎lua/render-markdown/render/checkbox.lua

+7-22
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,32 @@
11
local Base = require('render-markdown.render.base')
2-
local Str = require('render-markdown.lib.str')
32

43
---@class render.md.render.Checkbox: render.md.Renderer
54
---@field private checkbox render.md.CheckboxComponent
6-
---@field private inline boolean
75
local Render = setmetatable({}, Base)
86
Render.__index = Render
97

108
---@return boolean
119
function Render:setup()
12-
local checkbox = self.config.checkbox
13-
if self.context:skip(checkbox) then
10+
local config = self.config.checkbox
11+
if self.context:skip(config) then
1412
return false
1513
end
1614

17-
local type_mapping = {
18-
task_list_marker_unchecked = checkbox.unchecked,
19-
task_list_marker_checked = checkbox.checked,
15+
local types = {
16+
task_list_marker_unchecked = config.unchecked,
17+
task_list_marker_checked = config.checked,
2018
}
21-
self.checkbox = type_mapping[self.node.type]
19+
self.checkbox = types[self.node.type]
2220
if self.checkbox == nil then
2321
return false
2422
end
2523

26-
self.inline = checkbox.position == 'inline'
27-
2824
return true
2925
end
3026

3127
function Render:render()
32-
self:icon()
28+
self:check_icon(self.checkbox.icon, self.checkbox.highlight)
3329
self:scope('check_scope', self.node:sibling('paragraph'), self.checkbox.scope_highlight)
3430
end
3531

36-
---@private
37-
function Render:icon()
38-
local icon = self.checkbox.icon
39-
local text = self.inline and icon or Str.pad_to(self.node.text, icon) .. icon
40-
self.marks:add_over('check_icon', self.node, {
41-
virt_text = { { text, self.checkbox.highlight } },
42-
virt_text_pos = self.inline and 'inline' or 'overlay',
43-
conceal = self.inline and '' or nil,
44-
})
45-
end
46-
4732
return Render

Diff for: ‎lua/render-markdown/render/list_item.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function Render:hide_marker()
9090
end
9191

9292
---@private
93-
---@param element boolean|render.md.Element
93+
---@param element render.md.mark.Element
9494
---@param highlight string?
9595
function Render:highlight_scope(element, highlight)
9696
self:scope(element, self.node:child('paragraph'), highlight)

Diff for: ‎lua/render-markdown/render/shortcut.lua

+3-11
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,11 @@ end
7575
---@private
7676
---@param checkbox render.md.CustomCheckbox
7777
function Render:checkbox(checkbox)
78-
if self.context:skip(self.config.checkbox) then
78+
local config = self.config.checkbox
79+
if self.context:skip(config) or self.node:after() ~= ' ' then
7980
return
8081
end
81-
82-
local inline = self.config.checkbox.position == 'inline'
83-
local icon, highlight = checkbox.rendered, checkbox.highlight
84-
local text = inline and icon or Str.pad_to(self.node.text, icon) .. icon
85-
local added = self.marks:add_over('check_icon', self.node, {
86-
virt_text = { { text, highlight } },
87-
virt_text_pos = 'inline',
88-
conceal = '',
89-
})
90-
82+
local added = self:check_icon(checkbox.rendered, checkbox.highlight)
9183
if added then
9284
self.context:add_checkbox(self.node.start_row, checkbox)
9385
end

Diff for: ‎lua/render-markdown/state.lua

+1-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ function M.setup(default_config, user_config)
3131
-- Override settings that require neovim >= 0.10.0 and have compatible alternatives
3232
if not util.has_10 then
3333
config.code.position = 'right'
34-
config.checkbox.position = 'overlay'
3534
end
3635
-- Use lazy.nvim file type configuration if available and no user value is specified
3736
if user_config.file_types == nil then
@@ -216,7 +215,7 @@ function M.validate()
216215
end)
217216
:nested('checkbox', function(checkbox)
218217
component_rules(checkbox)
219-
:one_of('position', { 'overlay', 'inline' })
218+
:type('right_pad', 'number')
220219
:nested({ 'unchecked', 'checked' }, function(box)
221220
box:type({ 'icon', 'highlight' }, 'string'):type('scope_highlight', { 'string', 'nil' }):check()
222221
end)

Diff for: ‎lua/render-markdown/types.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
---@field scope_highlight? string
116116

117117
---@class (exact) render.md.Checkbox: render.md.BaseComponent
118-
---@field position render.md.checkbox.Position
118+
---@field right_pad integer
119119
---@field unchecked render.md.CheckboxComponent
120120
---@field checked render.md.CheckboxComponent
121121
---@field custom table<string, render.md.CustomCheckbox>

Diff for: ‎tests/box_dash_quote_spec.lua

+24-18
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,30 @@ describe('box_dash_quote.md', function()
1313
:add(row:get(), row:get(), 0, 1, util.heading.icon(1))
1414
:add(row:get(), row:inc(), 0, 0, util.heading.bg(1))
1515

16-
marks:add(row:inc(), row:get(), 0, 2, util.conceal())
17-
marks:add(row:get(), row:get(), 2, 5, {
18-
virt_text = { { '󰄱 ', 'RmUnchecked' } },
19-
virt_text_pos = 'inline',
20-
conceal = '',
21-
})
22-
marks:add(row:inc(), row:get(), 0, 2, util.conceal())
23-
marks:add(row:get(), row:get(), 2, 5, {
24-
virt_text = { { '󰱒 ', 'RmChecked' } },
25-
virt_text_pos = 'inline',
26-
conceal = '',
27-
})
28-
marks:add(row:inc(), row:get(), 0, 2, util.conceal())
29-
marks:add(row:get(), row:get(), 2, 5, {
30-
virt_text = { { '󰥔 ', 'RmTodo' } },
31-
virt_text_pos = 'inline',
32-
conceal = '',
33-
})
16+
marks
17+
:add(row:inc(), row:get(), 0, 2, util.conceal())
18+
:add(row:get(), row:get(), 2, 5, {
19+
virt_text = { { '󰄱 ', 'RmUnchecked' }, { ' ', 'Normal' } },
20+
virt_text_pos = 'overlay',
21+
})
22+
:add(row:get(), row:get(), 5, 6, util.conceal())
23+
marks
24+
:add(row:inc(), row:get(), 0, 2, util.conceal())
25+
:add(row:get(), row:get(), 2, 5, {
26+
virt_text = { { '󰱒 ', 'RmChecked' }, { ' ', 'Normal' } },
27+
virt_text_pos = 'overlay',
28+
})
29+
:add(row:get(), row:get(), 5, 6, util.conceal())
30+
marks
31+
:add(row:inc(), row:get(), 0, 2, util.conceal())
32+
:add(row:get(), row:get(), 2, 6, {
33+
virt_text = { { '󰥔 ', 'RmTodo' } },
34+
virt_text_pos = 'overlay',
35+
})
36+
:add(row:get(), nil, 6, nil, {
37+
virt_text = { { ' ', 'Normal' } },
38+
virt_text_pos = 'inline',
39+
})
3440
marks:add(row:inc(), row:get(), 0, 2, util.bullet(1))
3541

3642
marks:add(row:inc(2), nil, 0, nil, {

0 commit comments

Comments
 (0)
Please sign in to comment.