Skip to content

Commit d69f0d8

Browse files
feat: use changed tick to determine whether we should parse instead of event
## Details For the main event based update trigger rather than handling some special events as changes all the builtin events are now handled as non-changes. Instead in these cases we compare the `changedtick` between the last time we computed `extmarks` and now and if it is different (meaning the buffer has been modified) only then do we re-compute `extmarks`. The `ui.update` method still takes a 4th boolean argument, but it is now called `force` as the meaning of it is to force an update even if no change to the buffer has been detected. The user configured `change_events` will result in `force` being set since some events that do not produce buffer changes may still require us to re-compute `extmarks` like an example where `diagnostics` were used to do some of the rendering. We also set `force` on `WinResized` events as those do not change the buffer but may result in a different rendering output since width can impact some `extmarks`. Also for user commands and initial render. What this allows us to do is avoid re-computing marks for things like mode changes that do not modify the buffer. To support this clearing was modified to delete each `extmark` individually rather than clearing the entire namespace for the buffer, we can then re-create them later without doing a whole parse & render cycle which is the most time consuming task this plugin does. Before we needed to always re-compute on mode changes since the user can have made text changes in insert mode, but now this is fully handled by the `changedtick` value. Other changes: - do not hide any marks in `command` mode - add `trace` and `warn` level logs to match neovim levels - make most logs that were `debug` level now `trace` level and update docs - unhandled log level from user results in no logs instead of all - when logging nodes include the length of the text rather than all the text in the node (easier to read), also include the TSNode type
1 parent 048d680 commit d69f0d8

File tree

17 files changed

+122
-115
lines changed

17 files changed

+122
-115
lines changed

benches/medium_spec.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('medium.md', function()
1111
util.less_than(util.move_down(3), 0.5)
1212
util.num_marks(base_marks + 2)
1313

14-
util.less_than(util.insert_mode(), 5)
14+
util.less_than(util.modify(), 2.5)
1515
util.num_marks(base_marks + 2)
1616
end)
1717
end)

benches/medium_table_spec.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('medium-table.md', function()
1111
util.less_than(util.move_down(1), 0.5)
1212
util.num_marks(base_marks + 1)
1313

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

benches/readme_spec.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('README.md', function()
1111
util.less_than(util.move_down(1), 0.5)
1212
util.num_marks(base_marks + 2)
1313

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

benches/util.lua

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ local M = {}
77
---@return number
88
function M.setup(file)
99
return M.time(function()
10-
require('render-markdown').setup({ debounce = 0 })
10+
require('render-markdown').setup({
11+
debounce = 0,
12+
change_events = { 'TextChanged' },
13+
})
1114
vim.cmd('e ' .. file)
1215
end)
1316
end
@@ -16,16 +19,17 @@ end
1619
---@return number
1720
function M.move_down(n)
1821
return M.time(function()
19-
M.feed(('%dj'):format(n))
20-
-- Unsure why, but the CursorMoved event needs to be triggered manually
22+
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
23+
vim.api.nvim_win_set_cursor(0, { row + n, col })
24+
-- CursorMoved event needs to be triggered manually
2125
vim.api.nvim_exec_autocmds('CursorMoved', {})
2226
end)
2327
end
2428

2529
---@return number
26-
function M.insert_mode()
30+
function M.modify()
2731
return M.time(function()
28-
M.feed('i')
32+
vim.api.nvim_exec_autocmds('TextChanged', {})
2933
end)
3034
end
3135

@@ -40,13 +44,6 @@ function M.time(callback)
4044
return (end_time - start_time) / 1e+6
4145
end
4246

43-
---@private
44-
---@param keys string
45-
function M.feed(keys)
46-
local escape = vim.api.nvim_replace_termcodes(keys, true, false, true)
47-
vim.api.nvim_feedkeys(escape, 'nx', false)
48-
end
49-
5047
---@param actual number
5148
---@param max number
5249
function M.less_than(actual, max)

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 NVIM v0.11.1 Last change: 2025 May 29
1+
*render-markdown.txt* For NVIM v0.11.1 Last change: 2025 May 30
22

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

doc/troubleshooting.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ to some default the author prefers. So the settings you think you are using are
3939
necessarily the only ones be used.
4040

4141
Run `:RenderMarkdown config`, which will output only the non-default values being
42-
used, you might be surprised what you find.
42+
used, you might be surprised by what you find.
4343

4444
## Validate Parse Tree
4545

@@ -92,7 +92,7 @@ Run `:InspectTree` which should output the following:
9292
If this is not what you see you likely need to update `nvim-treesitter` and your
9393
treesitter parsers.
9494

95-
## Generate Debug Logs
95+
## Generate Trace Logs
9696

9797
If all else fails hopefully the logs can provide some insight. This plugin
9898
ships with logging, however it only includes errors by default.
@@ -105,11 +105,11 @@ Use the same file from [Validate Parse Tree](#validate-parse-tree).
105105

106106
### 2) Update Log Level
107107

108-
Change plugin configuration to output `debug` logs:
108+
Change plugin configuration to output `trace` logs:
109109

110110
```lua
111111
require('render-markdown').setup({
112-
log_level = 'debug',
112+
log_level = 'trace',
113113
})
114114
```
115115

lua/render-markdown/core/handlers.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,22 @@ end
5353
---@param language string
5454
---@return render.md.Mark[]
5555
function M.tree(context, ctx, language)
56-
log.buf('debug', 'Language', ctx.buf, language)
56+
log.buf('trace', 'Language', ctx.buf, language)
5757
if not context.view:overlaps(ctx.root) then
5858
return {}
5959
end
6060
local marks = {} ---@type render.md.Mark[]
6161
local custom = M.config.custom[language]
6262
if custom then
63-
log.buf('debug', 'Handler', ctx.buf, 'custom')
63+
log.buf('trace', 'Handler', ctx.buf, 'custom')
6464
vim.list_extend(marks, custom.parse(ctx))
6565
if not custom.extends then
6666
return marks
6767
end
6868
end
6969
local builtin = M.builtin[language]
7070
if builtin then
71-
log.buf('debug', 'Handler', ctx.buf, 'builtin')
71+
log.buf('trace', 'Handler', ctx.buf, 'builtin')
7272
vim.list_extend(marks, builtin.parse(ctx))
7373
end
7474
return marks

lua/render-markdown/core/log.lua

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ local Env = require('render-markdown.lib.env')
1010
---@field level render.md.log.Level
1111
---@field runtime boolean
1212

13-
---@enum render.md.log.Level
13+
---@enum (key) render.md.log.Level
1414
local Level = {
15-
debug = 'debug',
16-
info = 'info',
17-
error = 'error',
18-
off = 'off',
15+
trace = 0,
16+
debug = 1,
17+
info = 2,
18+
warn = 3,
19+
error = 4,
20+
off = 5,
1921
}
2022

2123
---@class render.md.Log
@@ -74,9 +76,10 @@ end
7476
---@param capture string
7577
---@param node render.md.Node
7678
function M.node(capture, node)
77-
M.add('debug', 'Node', {
79+
M.add('trace', 'Node', {
7880
capture = capture,
79-
text = node.text,
81+
type = node.type,
82+
length = #node.text,
8083
rows = { node.start_row, node.end_row },
8184
cols = { node.start_col, node.end_col },
8285
})
@@ -138,19 +141,9 @@ end
138141

139142
---@private
140143
---@param level render.md.log.Level
141-
---@return integer
144+
---@return number
142145
function M.level(level)
143-
if level == Level.debug then
144-
return 1
145-
elseif level == Level.info then
146-
return 2
147-
elseif level == Level.error then
148-
return 3
149-
elseif level == Level.off then
150-
return 4
151-
else
152-
return 0
153-
end
146+
return Level[level] or math.huge
154147
end
155148

156149
---@private

lua/render-markdown/core/manager.lua

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,24 @@ function M.attach(buf)
119119
'CmdlineChanged',
120120
'CursorHold',
121121
'CursorMoved',
122+
'DiffUpdated',
123+
'ModeChanged',
124+
'TextChanged',
122125
'WinScrolled',
123126
}
124-
local change_events = { 'DiffUpdated', 'ModeChanged', 'TextChanged' }
125-
vim.list_extend(change_events, M.config.change_events)
126127
if config.resolved:render('i') then
127-
vim.list_extend(events, { 'CursorHoldI', 'CursorMovedI' })
128-
vim.list_extend(change_events, { 'TextChangedI' })
128+
events[#events + 1] = 'CursorHoldI'
129+
events[#events + 1] = 'CursorMovedI'
130+
events[#events + 1] = 'TextChangedI'
129131
end
130-
vim.api.nvim_create_autocmd(vim.list_extend(events, change_events), {
132+
local force = M.config.change_events
133+
for _, event in ipairs(force) do
134+
if not vim.tbl_contains(events, event) then
135+
events[#events + 1] = event
136+
end
137+
end
138+
139+
vim.api.nvim_create_autocmd(events, {
131140
group = M.group,
132141
buffer = buf,
133142
callback = function(args)
@@ -140,7 +149,7 @@ function M.attach(buf)
140149
return
141150
end
142151
local event = args.event
143-
ui.update(buf, win, event, vim.tbl_contains(change_events, event))
152+
ui.update(buf, win, event, vim.tbl_contains(force, event))
144153
end,
145154
})
146155

lua/render-markdown/core/ui.lua

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ M.cache = {}
2626
---@param config render.md.ui.Config
2727
function M.setup(config)
2828
M.config = config
29-
-- reset cache
30-
for buf, decorator in pairs(M.cache) do
31-
M.clear(buf, decorator)
29+
-- clear marks and reset cache
30+
for buf in pairs(M.cache) do
31+
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
3232
end
3333
M.cache = {}
3434
end
@@ -38,7 +38,7 @@ end
3838
function M.get(buf)
3939
local result = M.cache[buf]
4040
if not result then
41-
result = Decorator.new()
41+
result = Decorator.new(buf)
4242
M.cache[buf] = result
4343
end
4444
return result
@@ -48,24 +48,16 @@ end
4848
---@param buf integer
4949
---@param win integer
5050
---@param event string
51-
---@param change boolean
52-
function M.update(buf, win, event, change)
53-
log.buf('info', 'Update', buf, event, ('change %s'):format(change))
54-
M.updater.new(buf, win, change):start()
55-
end
56-
57-
---@private
58-
---@param buf integer
59-
---@param decorator render.md.Decorator
60-
function M.clear(buf, decorator)
61-
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
62-
decorator:clear()
51+
---@param force boolean
52+
function M.update(buf, win, event, force)
53+
log.buf('info', 'Update', buf, event, ('force %s'):format(force))
54+
M.updater.new(buf, win, force):start()
6355
end
6456

6557
---@class render.md.ui.Updater
6658
---@field private buf integer
6759
---@field private win integer
68-
---@field private change boolean
60+
---@field private force boolean
6961
---@field private decorator render.md.Decorator
7062
---@field private config render.md.buf.Config
7163
---@field private mode string
@@ -74,13 +66,13 @@ Updater.__index = Updater
7466

7567
---@param buf integer
7668
---@param win integer
77-
---@param change boolean
69+
---@param force boolean
7870
---@return render.md.ui.Updater
79-
function Updater.new(buf, win, change)
71+
function Updater.new(buf, win, force)
8072
local self = setmetatable({}, Updater)
8173
self.buf = buf
8274
self.win = win
83-
self.change = change
75+
self.force = force
8476
self.decorator = M.get(buf)
8577
self.config = state.get(buf)
8678
return self
@@ -94,7 +86,7 @@ function Updater:start()
9486
return
9587
end
9688
self.decorator:schedule(
97-
self:should_parse(),
89+
self:changed(),
9890
self.config.debounce,
9991
log.runtime('update', function()
10092
self:run()
@@ -104,9 +96,11 @@ end
10496

10597
---@private
10698
---@return boolean
107-
function Updater:should_parse()
108-
-- need to parse on changes or when we have not parsed the visible range yet
109-
return self.change or not Context.contains(self.buf, self.win)
99+
function Updater:changed()
100+
-- force or buffer has changed or we have not handled the visible range yet
101+
return self.force
102+
or self.decorator:changed()
103+
or not Context.contains(self.buf, self.win)
110104
end
111105

112106
---@private
@@ -126,20 +120,28 @@ function Updater:run()
126120
Env.win.set(window, name, value[next_state])
127121
end
128122
end
129-
if render then
123+
if not render then
124+
self:clear()
125+
M.config.on.clear({ buf = self.buf, win = self.win })
126+
else
130127
self:render()
131128
M.config.on.render({ buf = self.buf, win = self.win })
132-
else
133-
M.clear(self.buf, self.decorator)
134-
M.config.on.clear({ buf = self.buf, win = self.win })
129+
end
130+
end
131+
132+
---@private
133+
function Updater:clear()
134+
local extmarks = self.decorator:get()
135+
for _, extmark in ipairs(extmarks) do
136+
extmark:hide(M.ns, self.buf)
135137
end
136138
end
137139

138140
---@private
139141
function Updater:render()
140-
local initial = self.decorator:initial()
141-
if initial or self:should_parse() then
142-
M.clear(self.buf, self.decorator)
142+
if self:changed() then
143+
local initial = self.decorator:initial()
144+
self:clear()
143145
local extmarks = self:get_extmarks()
144146
self.decorator:set(extmarks)
145147
if initial then
@@ -161,8 +163,8 @@ end
161163
---@private
162164
---@return render.md.Extmark[]
163165
function Updater:get_extmarks()
164-
local has_parser, parser = pcall(vim.treesitter.get_parser, self.buf)
165-
if not has_parser or not parser then
166+
local ok, parser = pcall(vim.treesitter.get_parser, self.buf)
167+
if not ok or not parser then
166168
log.buf('error', 'Fail', self.buf, 'no treesitter parser found')
167169
return {}
168170
end
@@ -179,9 +181,10 @@ end
179181
function Updater:hidden()
180182
-- anti-conceal is not enabled -> hide nothing
181183
-- row is not known -> buffer is not active -> hide nothing
184+
-- in command mode -> cursor is not in buffer -> hide nothing
182185
local config = self.config.anti_conceal
183186
local row = Env.row.get(self.buf, self.win)
184-
if not config.enabled or not row then
187+
if not config.enabled or not row or Env.mode.is(self.mode, { 'c' }) then
185188
return nil
186189
end
187190
if Env.mode.is(self.mode, { 'v', 'V', '\22' }) then

0 commit comments

Comments
 (0)