Skip to content

Commit 6bb1d43

Browse files
feat(perf): Skip parsing when no text changes and in already parsed range
# Details Issue: #115 After the change to parse only the visible range of a file along with a debounce there was a performance degredation for simple cursor movements. This is because cursor movements no longer had the benefit of using cached marks, so are re-computed and feed into the same debounced method. This can cause laggy behavior. To fix this I have added back the caching behavior from before with some updates to account for the new logic. Like before marks will not be parsed for events that do not update the text. In addition to the simple event check we also need to validate that the range of cached marks accounts for everything in the users viewport, otherwise we still need to parse. This should have the benefits of the snappy behavior from before along with being able to handle essentially arbitrary file sizes. Have increased the default max file size to 10MB since performance impact is minimal.
1 parent 70ae5e9 commit 6bb1d43

File tree

15 files changed

+176
-84
lines changed

15 files changed

+176
-84
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ require('render-markdown').setup({
124124
enabled = true,
125125
-- Maximum file size (in MB) that this plugin will attempt to render
126126
-- Any file larger than this will effectively be ignored
127-
max_file_size = 1.5,
127+
max_file_size = 10.0,
128128
-- Milliseconds that must pass before updating marks, updates occur
129129
-- within the context of the visible window, not the entire buffer
130130
debounce = 100,

benches/medium_spec.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ local util = require('benches.util')
55
describe('medium.md', function()
66
it('default', function()
77
local base_marks = 46
8-
util.between(20, 30, util.setup('temp/medium.md'))
8+
util.less_than(util.setup('temp/medium.md'), 30)
99
util.num_marks(base_marks)
1010

11-
util.between(0, 5, util.move_down(3))
11+
util.less_than(util.move_down(3), 0.5)
1212
util.num_marks(base_marks + 2)
1313

14-
util.between(1, 15, util.insert_mode())
14+
util.less_than(util.insert_mode(), 15)
1515
util.num_marks(base_marks + 2)
1616
end)
1717
end)

benches/medium_table_spec.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ local util = require('benches.util')
55
describe('medium-table.md', function()
66
it('default', function()
77
local base_marks = 468
8-
util.between(80, 105, util.setup('temp/medium-table.md'))
8+
util.less_than(util.setup('temp/medium-table.md'), 105)
99
util.num_marks(base_marks)
1010

11-
util.between(1, 20, util.move_down(1))
11+
util.less_than(util.move_down(1), 0.5)
1212
util.num_marks(base_marks + 2)
1313

14-
util.between(5, 30, util.insert_mode())
14+
util.less_than(util.insert_mode(), 25)
1515
util.num_marks(base_marks + 2)
1616
end)
1717
end)

benches/readme_spec.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ local util = require('benches.util')
55
describe('README.md', function()
66
it('default', function()
77
local base_marks = 55
8-
util.between(30, 50, util.setup('README.md'))
8+
util.less_than(util.setup('README.md'), 50)
99
util.num_marks(base_marks)
1010

11-
util.between(1, 5, util.move_down(1))
11+
util.less_than(util.move_down(1), 0.5)
1212
util.num_marks(base_marks + 2)
1313

14-
util.between(10, 20, util.insert_mode())
14+
util.less_than(util.insert_mode(), 20)
1515
util.num_marks(base_marks + 2)
1616
end)
1717
end)

benches/util.lua

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,10 @@ function M.feed(keys)
5050
vim.api.nvim_feedkeys(escape, 'nx', false)
5151
end
5252

53-
---@param low integer
54-
---@param high integer
5553
---@param actual number
56-
function M.between(low, high, actual)
57-
truthy(actual > low and actual < high, string.format('expected %d < %f < %d', low, actual, high))
54+
---@param max number
55+
function M.less_than(actual, max)
56+
truthy(actual < max, string.format('expected %f < %f', actual, max))
5857
end
5958

6059
---@param expected integer

doc/render-markdown.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2024 August 02
1+
*render-markdown.txt* For 0.10.0 Last change: 2024 August 04
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*
@@ -157,7 +157,7 @@ Full Default Configuration ~
157157
enabled = true,
158158
-- Maximum file size (in MB) that this plugin will attempt to render
159159
-- Any file larger than this will effectively be ignored
160-
max_file_size = 1.5,
160+
max_file_size = 10.0,
161161
-- Milliseconds that must pass before updating marks, updates occur
162162
-- within the context of the visible window, not the entire buffer
163163
debounce = 100,

lua/render-markdown/buffer_state.lua

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
---@field private timer uv_timer_t
44
---@field private running boolean
55
---@field state? 'default'|'rendered'
6+
---@field marks? render.md.Extmark[]
67
local BufferState = {}
78
BufferState.__index = BufferState
89

@@ -14,18 +15,19 @@ function BufferState.new(buf)
1415
self.timer = (vim.uv or vim.loop).new_timer()
1516
self.running = false
1617
self.state = nil
18+
self.marks = nil
1719
return self
1820
end
1921

2022
---@param ms integer
21-
---@param cb fun(buf: integer)
22-
function BufferState:debounce(ms, cb)
23+
---@param callback fun()
24+
function BufferState:debounce(ms, callback)
2325
self.timer:start(ms, 0, function()
2426
self.running = false
2527
end)
2628
if not self.running then
2729
self.running = true
28-
vim.schedule_wrap(cb)(self.buf)
30+
vim.schedule(callback)
2931
end
3032
end
3133

lua/render-markdown/context.lua

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
---@field private win integer
44
---@field private top integer
55
---@field private bottom integer
6-
---@field private conceallevel integer
76
---@field private conceal? table<integer, [integer, integer][]>
87
---@field private links table<integer, [integer, integer, string][]>
98
local Context = {}
@@ -12,15 +11,16 @@ Context.__index = Context
1211
---@param buf integer
1312
---@param win integer
1413
---@param offset integer
14+
---@return render.md.Context
1515
function Context.new(buf, win, offset)
1616
local self = setmetatable({}, Context)
1717
self.buf = buf
1818
self.win = win
1919
local top = vim.api.nvim_win_call(win, vim.fn.winsaveview).topline - 1
2020
local height = vim.api.nvim_win_get_height(win)
21+
local lines = vim.api.nvim_buf_line_count(buf)
2122
self.top = math.max(top - offset, 0)
22-
self.bottom = top + height + offset
23-
self.conceallevel = vim.api.nvim_get_option_value('conceallevel', { scope = 'local', win = win })
23+
self.bottom = math.min(top + height + offset, lines)
2424
self.conceal = nil
2525
self.links = {}
2626
return self
@@ -47,6 +47,12 @@ function Context:get_width()
4747
return vim.api.nvim_win_get_width(self.win)
4848
end
4949

50+
---@param other render.md.Context
51+
---@return boolean
52+
function Context:contains_range(other)
53+
return self.top <= other.top and self.bottom >= other.bottom
54+
end
55+
5056
---@return Range2
5157
function Context:range()
5258
return { self.top, self.bottom }
@@ -82,7 +88,8 @@ end
8288
---@private
8389
---@return table<integer, [integer, integer][]>
8490
function Context:compute_conceal()
85-
if self.conceallevel == 0 then
91+
local conceallevel = vim.api.nvim_get_option_value('conceallevel', { scope = 'local', win = self.win })
92+
if conceallevel == 0 then
8693
return {}
8794
end
8895
local ranges = {}
@@ -137,6 +144,17 @@ function M.reset(buf, win)
137144
cache[buf] = Context.new(buf, win, 10)
138145
end
139146

147+
---@param buf integer
148+
---@param win integer
149+
---@return boolean
150+
function M.contains_range(buf, win)
151+
local context = cache[buf]
152+
if context == nil then
153+
return false
154+
end
155+
return context:contains_range(Context.new(buf, win, 0))
156+
end
157+
140158
---@param buf integer
141159
---@return render.md.Context
142160
function M.get(buf)

lua/render-markdown/extmark.lua

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---@class render.md.Extmark
2+
---@field private namespace integer
3+
---@field private buf integer
4+
---@field private mark render.md.Mark
5+
---@field private id? integer
6+
local Extmark = {}
7+
Extmark.__index = Extmark
8+
9+
---@param namespace integer
10+
---@param buf integer
11+
---@param mark render.md.Mark
12+
---@return render.md.Extmark
13+
function Extmark.new(namespace, buf, mark)
14+
local self = setmetatable({}, Extmark)
15+
self.namespace = namespace
16+
self.buf = buf
17+
self.mark = mark
18+
self.id = nil
19+
return self
20+
end
21+
22+
---@param config render.md.BufferConfig
23+
---@param row? integer
24+
function Extmark:render(config, row)
25+
if self:should_show(config, row) then
26+
self:show()
27+
else
28+
self:hide()
29+
end
30+
end
31+
32+
---@private
33+
function Extmark:show()
34+
if self.id == nil then
35+
self.mark.opts.strict = false
36+
self.id = vim.api.nvim_buf_set_extmark(
37+
self.buf,
38+
self.namespace,
39+
self.mark.start_row,
40+
self.mark.start_col,
41+
self.mark.opts
42+
)
43+
end
44+
end
45+
46+
---@private
47+
function Extmark:hide()
48+
if self.id ~= nil then
49+
vim.api.nvim_buf_del_extmark(self.buf, self.namespace, self.id)
50+
self.id = nil
51+
end
52+
end
53+
54+
---Render marks based on anti-conceal behavior and current row
55+
---@private
56+
---@param config render.md.BufferConfig
57+
---@param row? integer
58+
---@return boolean
59+
function Extmark:should_show(config, row)
60+
-- Anti-conceal is not enabled -> all marks should be shown
61+
if not config.anti_conceal.enabled then
62+
return true
63+
end
64+
-- Row is not known means buffer is not active -> all marks should be shown
65+
if row == nil then
66+
return true
67+
end
68+
-- Mark is not concealable -> mark should always be shown
69+
if not self.mark.conceal then
70+
return true
71+
end
72+
-- Show mark if it is not on the current row
73+
return self.mark.start_row ~= row
74+
end
75+
76+
return Extmark

lua/render-markdown/handler/markdown_inline.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function M.shortcut(config, buf, info)
7171
if checkbox ~= nil then
7272
return M.checkbox(config, info, checkbox)
7373
end
74-
local line = vim.api.nvim_buf_get_lines(buf, info.start_row, info.start_row + 1, true)[1]
74+
local line = vim.api.nvim_buf_get_lines(buf, info.start_row, info.start_row + 1, false)[1]
7575
if line:find('[' .. info.text .. ']', 1, true) ~= nil then
7676
return M.wiki_link(config, info)
7777
end

0 commit comments

Comments
 (0)