Skip to content

Commit 4d046cd

Browse files
feat(perf): Improve speed of table parsing by storing state
## Details When handling a pipe_table there are 2 slower parts. For each cell we compute the amount of space that is concealed & that amount of inline text added back so we can pad things nicely. For the amount of concealed space, this was improved earlier by pre-computing all the concealed nodes at the start of the parse method. However we can further improve this by implementing our own node contains logic. I am unsure why this ends up being faster, but it is quite substantial. We further improve this by storing only the column range for each node in the cache, and not the entire node. For the latter we had yet to do anything. The computation here involved running a treesitter query and essentially computing exactly what we think the inline parser would add to each cell. Instead what we can do is store some metadata when the inline parser runs and use this information to check what was added within a cell. This avoids a lot of parsing for each cell and substantially improves how quickly things run. However we needed to hack the top level for loop to guarantee that we parse markdown trees after all others. It's not the most elegant thing but seems to do the job. Since we are doing a good amount of contains range type logic I'm sure there will be some errors, those will be good to add to unit tests anyway, currently everything passes.
1 parent 80938b2 commit 4d046cd

File tree

12 files changed

+106
-123
lines changed

12 files changed

+106
-123
lines changed

Diff for: README.md

-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,6 @@ require('render-markdown').setup({
168168
169169
[(inline_link) (full_reference_link) (image)] @link
170170
]],
171-
-- Query to be able to identify links in nodes
172-
inline_link_query = '[(inline_link) (full_reference_link) (image)] @link',
173171
-- The level of logs to write to file: vim.fn.stdpath('state') .. '/render-markdown.log'
174172
-- Only intended to be used for plugin development / debugging
175173
log_level = 'error',

Diff for: doc/render-markdown.txt

-2
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,6 @@ Full Default Configuration ~
200200

201201
[(inline_link) (full_reference_link) (image)] @link
202202
]],
203-
-- Query to be able to identify links in nodes
204-
inline_link_query = '[(inline_link) (full_reference_link) (image)] @link',
205203
-- The level of logs to write to file: vim.fn.stdpath('state') .. '/render-markdown.log'
206204
-- Only intended to be used for plugin development / debugging
207205
log_level = 'error',

Diff for: doc/todo.md

-21
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,3 @@
44
alignment info.
55
- Figure out how to display the many configuration options & impact
66
- Potentially change LuaRocks icon dependency to [mini.icons](https://luarocks.org/modules/neorocks/mini.icons)
7-
- Force non-markdown handlers to run first in `ui.lua`. Cache information
8-
at buffer level from inline parser to avoid computing it:
9-
10-
```lua
11-
-- Parse marks
12-
local marks = {}
13-
-- Parse markdown after all other nodes to take advantage of state
14-
local markdown_roots = {}
15-
parser:for_each_tree(function(tree, language_tree)
16-
local language = language_tree:lang()
17-
if language == 'markdown' then
18-
table.insert(markdown_roots, tree:root())
19-
else
20-
vim.list_extend(marks, M.parse_tree(buf, language, tree:root()))
21-
end
22-
end)
23-
for _, root in ipairs(markdown_roots) do
24-
vim.list_extend(marks, M.parse_tree(buf, 'markdown', root))
25-
end
26-
return marks
27-
```

Diff for: lua/render-markdown/handler/markdown.lua

+49-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local icons = require('render-markdown.icons')
44
local list = require('render-markdown.list')
55
local logger = require('render-markdown.logger')
66
local pipe_table_parser = require('render-markdown.parser.pipe_table')
7-
local shared = require('render-markdown.handler.shared')
7+
local request = require('render-markdown.request')
88
local state = require('render-markdown.state')
99
local str = require('render-markdown.str')
1010
local ts = require('render-markdown.ts')
@@ -98,7 +98,7 @@ function M.heading(config, buf, info)
9898
-- Available width is level + 1 - concealed, where level = number of `#` characters, one
9999
-- is added to account for the space after the last `#` but before the heading title,
100100
-- and concealed text is subtracted since that space is not usable
101-
local padding = level + 1 - ts.concealed(buf, info) - str.width(icon)
101+
local padding = level + 1 - M.concealed(buf, info) - str.width(icon)
102102
if heading.position == 'inline' or padding < 0 then
103103
-- Requires inline extmarks to place when there is not enough space available
104104
if util.has_10 then
@@ -205,7 +205,7 @@ function M.code(config, buf, info)
205205

206206
if code.border == 'thin' then
207207
local code_start = ts.child(buf, info, 'fenced_code_block_delimiter', info.start_row)
208-
if #marks == 0 and ts.hidden(buf, code_info) and ts.hidden(buf, code_start) then
208+
if #marks == 0 and M.hidden(buf, code_info) and M.hidden(buf, code_start) then
209209
start_row = start_row + 1
210210
---@type render.md.Mark
211211
local start_mark = {
@@ -220,7 +220,7 @@ function M.code(config, buf, info)
220220
list.add_mark(marks, start_mark)
221221
end
222222
local code_end = ts.child(buf, info, 'fenced_code_block_delimiter', info.end_row - 1)
223-
if ts.hidden(buf, code_end) then
223+
if M.hidden(buf, code_end) then
224224
end_row = end_row - 1
225225
---@type render.md.Mark
226226
local end_mark = {
@@ -317,7 +317,7 @@ function M.language(config, buf, info, code_block)
317317
return marks
318318
end
319319
local icon_text = icon .. ' '
320-
if ts.hidden(buf, info) then
320+
if M.hidden(buf, info) then
321321
-- Code blocks will pick up varying amounts of leading white space depending on the
322322
-- context they are in. This gets lumped into the delimiter node and as a result,
323323
-- after concealing, the extmark will be left shifted. Logic below accounts for this.
@@ -606,7 +606,7 @@ function M.table_row(config, buf, row, highlight)
606606
elseif cell.type == 'pipe_table_cell' then
607607
-- Requires inline extmarks
608608
if pipe_table.cell == 'padded' and util.has_10 then
609-
local offset = M.table_visual_offset(config, buf, cell)
609+
local offset = M.table_visual_offset(buf, cell)
610610
if offset > 0 then
611611
---@type render.md.Mark
612612
local padding_mark = {
@@ -659,7 +659,7 @@ function M.table_full(config, buf, parsed_table)
659659
if pipe_table.cell == 'raw' then
660660
-- For the raw cell style we want the lengths to match after
661661
-- concealing & inlined elements
662-
result = result - M.table_visual_offset(config, buf, info)
662+
result = result - M.table_visual_offset(buf, info)
663663
end
664664
return result
665665
end
@@ -710,18 +710,51 @@ function M.table_full(config, buf, parsed_table)
710710
end
711711

712712
---@private
713-
---@param config render.md.BufferConfig
713+
---@param buf integer
714+
---@param info? render.md.NodeInfo
715+
---@return boolean
716+
function M.hidden(buf, info)
717+
-- Missing nodes are considered hidden
718+
if info == nil then
719+
return true
720+
end
721+
return str.width(info.text) == M.concealed(buf, info)
722+
end
723+
724+
---@private
725+
---@param buf integer
726+
---@param info render.md.NodeInfo
727+
---@return integer
728+
function M.concealed(buf, info)
729+
local ranges = request.concealed(buf, info.start_row)
730+
if #ranges == 0 then
731+
return 0
732+
end
733+
local result = 0
734+
local col = info.start_col
735+
for _, index in ipairs(vim.fn.str2list(info.text)) do
736+
local ch = vim.fn.nr2char(index)
737+
for _, range in ipairs(ranges) do
738+
-- Essentially vim.treesitter.is_in_node_range but only care about column
739+
if col >= range[1] and col + 1 <= range[2] then
740+
result = result + str.width(ch)
741+
end
742+
end
743+
col = col + #ch
744+
end
745+
return result
746+
end
747+
748+
---@private
714749
---@param buf integer
715750
---@param info render.md.NodeInfo
716751
---@return integer
717-
function M.table_visual_offset(config, buf, info)
718-
local result = ts.concealed(buf, info)
719-
local query = state.inline_link_query
720-
local tree = vim.treesitter.get_string_parser(info.text, 'markdown_inline')
721-
for id, node in query:iter_captures(tree:parse()[1]:root(), info.text) do
722-
if query.captures[id] == 'link' then
723-
local link_info = ts.info(node, info.text)
724-
result = result - str.width(shared.link_icon(config, link_info))
752+
function M.table_visual_offset(buf, info)
753+
local result = M.concealed(buf, info)
754+
local icon_ranges = request.inline_links(buf, info.start_row)
755+
for _, icon_range in ipairs(icon_ranges) do
756+
if info.start_col < icon_range[2] and info.end_col > icon_range[1] then
757+
result = result - str.width(icon_range[3])
725758
end
726759
end
727760
return result

Diff for: lua/render-markdown/handler/markdown_inline.lua

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
local component = require('render-markdown.component')
22
local list = require('render-markdown.list')
33
local logger = require('render-markdown.logger')
4-
local shared = require('render-markdown.handler.shared')
4+
local request = require('render-markdown.request')
55
local state = require('render-markdown.state')
66
local str = require('render-markdown.str')
77
local ts = require('render-markdown.ts')
@@ -26,7 +26,7 @@ function M.parse(root, buf)
2626
elseif capture == 'callout' then
2727
list.add_mark(marks, M.callout(config, info))
2828
elseif capture == 'link' then
29-
list.add_mark(marks, M.link(config, info))
29+
list.add_mark(marks, M.link(config, buf, info))
3030
else
3131
logger.unhandled_capture('inline', capture)
3232
end
@@ -111,13 +111,28 @@ end
111111

112112
---@private
113113
---@param config render.md.BufferConfig
114+
---@param buf integer
114115
---@param info render.md.NodeInfo
115116
---@return render.md.Mark?
116-
function M.link(config, info)
117-
local icon = shared.link_icon(config, info)
117+
function M.link(config, buf, info)
118+
local link = config.link
119+
if not link.enabled then
120+
return nil
121+
end
122+
-- Requires inline extmarks
123+
if not util.has_10 then
124+
return nil
125+
end
126+
local icon = nil
127+
if vim.tbl_contains({ 'inline_link', 'full_reference_link' }, info.type) then
128+
icon = link.hyperlink
129+
elseif info.type == 'image' then
130+
icon = link.image
131+
end
118132
if icon == nil then
119133
return nil
120134
end
135+
request.add_inline_link(buf, info, icon)
121136
---@type render.md.Mark
122137
return {
123138
conceal = true,

Diff for: lua/render-markdown/handler/shared.lua

-29
This file was deleted.

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

-3
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ local M = {}
124124
---@field public markdown_query? string
125125
---@field public markdown_quote_query? string
126126
---@field public inline_query? string
127-
---@field public inline_link_query? string
128127
---@field public log_level? 'debug'|'error'
129128
---@field public file_types? string[]
130129
---@field public acknowledge_conflicts? boolean
@@ -183,8 +182,6 @@ M.default_config = {
183182
184183
[(inline_link) (full_reference_link) (image)] @link
185184
]],
186-
-- Query to be able to identify links in nodes
187-
inline_link_query = '[(inline_link) (full_reference_link) (image)] @link',
188185
-- The level of logs to write to file: vim.fn.stdpath('state') .. '/render-markdown.log'
189186
-- Only intended to be used for plugin development / debugging
190187
log_level = 'error',

Diff for: lua/render-markdown/request.lua

+27-11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ local util = require('render-markdown.util')
22

33
---@class render.md.RequestCache
44
local cache = {
5-
---@type table<integer, table<integer, TSNode[]>>
5+
---@type table<integer, table<integer, { [1]: integer, [2]: integer }[]>>
66
conceal = {},
7+
---@type table<integer, table<integer, { [1]: integer, [2]: integer, [3]: string }[]>>
8+
inline_links = {},
79
}
810

911
---@class render.md.Request
@@ -12,6 +14,7 @@ local M = {}
1214
---@param buf integer
1315
function M.reset_buf(buf)
1416
cache.conceal[buf] = {}
17+
cache.inline_links[buf] = {}
1518
end
1619

1720
---@param buf integer
@@ -30,11 +33,11 @@ function M.compute_conceal(buf, parser)
3033
if query ~= nil then
3134
for _, node, metadata in query:iter_captures(tree:root(), buf, 0, -1) do
3235
if metadata.conceal ~= nil then
33-
local row = node:range()
36+
local row, start_col, _, end_col = node:range()
3437
if nodes[row] == nil then
3538
nodes[row] = {}
3639
end
37-
table.insert(nodes[row], node)
40+
table.insert(nodes[row], { start_col, end_col })
3841
end
3942
end
4043
end
@@ -45,15 +48,28 @@ end
4548

4649
---@param buf integer
4750
---@param row integer
48-
---@param col integer
49-
---@return boolean
50-
function M.concealed(buf, row, col)
51-
for _, node in ipairs(cache.conceal[buf][row] or {}) do
52-
if vim.treesitter.is_in_node_range(node, row, col) then
53-
return true
54-
end
51+
---@return { [1]: integer, [2]: integer }[]
52+
function M.concealed(buf, row)
53+
return cache.conceal[buf][row] or {}
54+
end
55+
56+
---@param buf integer
57+
---@param info render.md.NodeInfo
58+
---@param icon string
59+
function M.add_inline_link(buf, info, icon)
60+
local inline_links = cache.inline_links[buf]
61+
local row = info.start_row
62+
if inline_links[row] == nil then
63+
inline_links[row] = {}
5564
end
56-
return false
65+
table.insert(inline_links[row], { info.start_col, info.end_col, icon })
66+
end
67+
68+
---@param buf integer
69+
---@param row integer
70+
---@return { [1]: integer, [2]: integer, [3]: string }[]
71+
function M.inline_links(buf, row)
72+
return cache.inline_links[buf][row] or {}
5773
end
5874

5975
return M

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

-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ local configs = {}
1414
---@field markdown_query vim.treesitter.Query
1515
---@field markdown_quote_query vim.treesitter.Query
1616
---@field inline_query vim.treesitter.Query
17-
---@field inline_link_query vim.treesitter.Query
1817
local M = {}
1918

2019
---@param default_config render.md.Config
@@ -36,7 +35,6 @@ function M.setup(default_config, user_config)
3635
M.markdown_query = vim.treesitter.query.parse('markdown', config.markdown_query)
3736
M.markdown_quote_query = vim.treesitter.query.parse('markdown', config.markdown_quote_query)
3837
M.inline_query = vim.treesitter.query.parse('markdown_inline', config.inline_query)
39-
M.inline_link_query = vim.treesitter.query.parse('markdown_inline', config.inline_link_query)
4038
end)
4139
end
4240

@@ -324,7 +322,6 @@ function M.validate()
324322
markdown_query = { config.markdown_query, 'string' },
325323
markdown_quote_query = { config.markdown_quote_query, 'string' },
326324
inline_query = { config.inline_query, 'string' },
327-
inline_link_query = { config.inline_link_query, 'string' },
328325
log_level = one_of(config.log_level, { 'debug', 'error' }, {}, false),
329326
file_types = string_array(config.file_types, false),
330327
acknowledge_conflicts = { config.acknowledge_conflicts, 'boolean' },

Diff for: lua/render-markdown/ts.lua

-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
local request = require('render-markdown.request')
2-
local str = require('render-markdown.str')
3-
41
---@param node TSNode
52
---@return boolean
63
local function in_section(node)
@@ -95,31 +92,4 @@ function M.child(buf, info, target_type, target_row)
9592
return nil
9693
end
9794

98-
---@param buf integer
99-
---@param info? render.md.NodeInfo
100-
---@return boolean
101-
function M.hidden(buf, info)
102-
-- Missing nodes are considered hidden
103-
if info == nil then
104-
return true
105-
end
106-
return str.width(info.text) == M.concealed(buf, info)
107-
end
108-
109-
---@param buf integer
110-
---@param info render.md.NodeInfo
111-
---@return integer
112-
function M.concealed(buf, info)
113-
local result = 0
114-
local col = info.start_col
115-
for _, index in ipairs(vim.fn.str2list(info.text)) do
116-
local ch = vim.fn.nr2char(index)
117-
if request.concealed(buf, info.start_row, col) then
118-
result = result + str.width(ch)
119-
end
120-
col = col + #ch
121-
end
122-
return result
123-
end
124-
12595
return M

0 commit comments

Comments
 (0)