Skip to content

Commit 2be2299

Browse files
authored
perf: cache diagnostics, fire mode events less often (#115)
* ref: separate updating the diagnostics and applying virtual text When checking some lags I had, `tiny-inline-diagnostic` seemed to be the main culprit, and more precisely the number of call to `vim.diagnostic.get`. This introduces a cache for the diagnostics to avoid that call, separating updating the data and displaying it. * feat: prevents triggering USER_EVENT on every mode change The mode can change *a lot* in a short period of time (several times in a second when doing a surround with mini.ai for instance). The issue is that we were calling `enable` each time the mode changed, which triggered the `USER_EVENT` events and in particular the `apply_virtual_texts` function. This led to a lot of unnecessary computation and redraw of the diagnostics, in turn making Neovim laggy, especially in the presence of many diagnostics. We instead only fire the event if we were disabled. If the text or the position of the cursor has changed, the other events (`DiagnosticChanged`, `CursorMoved`, ..) should take care of updating the diagnostics and the virtual text. * fix: prevent diag source to be appended multiple times to message * fix: don't overwrite the diagnostics from other sources When using multiple sources, the diagnostics coming from `DiagnosticChanged` can be multiple and disjoint if coming from different sources. We thus need to overwrite only the diagnostics of the incoming source. * ref: filter diags at write time rather than read time The assumption is that writing happens less often than reading and because of that we chose to keep a single array to read with all the diagnostics instead of one array per buffer and per source and having to create a new concatenated table each time we read them. To get the best of both world we could: - Have some kind of nested iterators that goes through each source and array. This would be problematic though if we need to sort the data between the two sources. - Have a separate cache for the concatenated version of the diagnostics that's populated at read time and then re-used and cleared at write time. * fix: diagnostics not cleared when they should When we get an empty array in the `DiagnosticChanged` events, it means that one of the namespace has been cleared. The problem is that we don't (and can't) know which one it was without calling `vim.diagnostic.get` to get all the diagnostics.
1 parent 220cba7 commit 2be2299

File tree

2 files changed

+74
-22
lines changed

2 files changed

+74
-22
lines changed

lua/tiny-inline-diagnostic/chunk.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,12 @@ function M.get_chunks(opts, diags_on_line, diag_index, diag_line, cursor_line, b
263263
show_source = true
264264
end
265265

266+
local diag_message = diag.message
266267
if show_source and diag.source then
267-
diag.message = diag.message .. " (" .. diag.source .. ")"
268+
diag_message = diag_message .. " (" .. diag.source .. ")"
268269
end
269270

270-
local chunks = { diag.message }
271+
local chunks = { diag_message }
271272
local severities = vim.tbl_map(function(d)
272273
return d.severity
273274
end, diags_on_line)
@@ -280,7 +281,6 @@ function M.get_chunks(opts, diags_on_line, diag_index, diag_line, cursor_line, b
280281
end
281282
end
282283

283-
local diag_message = diag.message
284284

285285
if opts.options.format and diag_message then
286286
diag_message = opts.options.format(diag)

lua/tiny-inline-diagnostic/diagnostic.lua

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@ M.enabled = true
1515
M.user_toggle_state = true
1616
local attached_buffers = {}
1717

18+
---Buffer number => diagnostics
19+
---@type table<number, any>
20+
local diagnostics_cache = {}
21+
1822
---@class DiagnosticPosition
1923
---@field line number
2024
---@field col number
2125

2226
local function enable()
23-
M.enabled = true
24-
vim.api.nvim_exec_autocmds("User", { pattern = USER_EVENT })
27+
-- Prevents calling `enable` even if it's not needed
28+
if not M.enabled then
29+
M.enabled = true
30+
vim.api.nvim_exec_autocmds("User", { pattern = USER_EVENT })
31+
end
2532
end
2633

2734
local function disable()
28-
M.enabled = false
29-
vim.api.nvim_exec_autocmds("User", { pattern = USER_EVENT })
35+
if M.enabled then
36+
M.enabled = false
37+
vim.api.nvim_exec_autocmds("User", { pattern = USER_EVENT })
38+
end
3039
end
3140

3241
-- Diagnostic filtering functions
@@ -87,22 +96,16 @@ end
8796
---@param diagnostics table
8897
---@return table
8998
local function filter_diagnostics(opts, event, diagnostics)
90-
local filtered = filter_by_severity(opts, diagnostics)
91-
92-
table.sort(filtered, function(a, b)
93-
return a.severity < b.severity
94-
end)
95-
9699
if not opts.options.multilines.enabled then
97-
return M.filter_diags_under_cursor(opts, event.buf, filtered)
100+
return M.filter_diags_under_cursor(opts, event.buf, diagnostics)
98101
end
99102

100103
if opts.options.multilines.always_show then
101-
return filtered
104+
return diagnostics
102105
end
103106

104-
local under_cursor = M.filter_diags_under_cursor(opts, event.buf, filtered)
105-
return not vim.tbl_isempty(under_cursor) and under_cursor or filtered
107+
local under_cursor = M.filter_diags_under_cursor(opts, event.buf, diagnostics)
108+
return not vim.tbl_isempty(under_cursor) and under_cursor or diagnostics
106109
end
107110

108111
---@param diagnostics table
@@ -122,6 +125,53 @@ local function get_visible_diagnostics(diagnostics)
122125
return visible_diags
123126
end
124127

128+
---@param opts DiagnosticConfig
129+
---@param bufnr number
130+
---@param diagnostics table
131+
local function update_diagnostics_cache(opts, bufnr, diagnostics)
132+
if vim.tbl_isempty(diagnostics) then
133+
-- The event doesn't contain the associated namespace of the diagnostics,
134+
-- meaning we can't know which namespace was cleared. We thus have to get
135+
-- the diagnostics through normal means.
136+
local diags = vim.diagnostic.get(bufnr)
137+
table.sort(diags, function(a, b)
138+
return a.severity < b.severity
139+
end)
140+
diagnostics_cache[bufnr] = diags
141+
return
142+
end
143+
144+
local diag_buf = diagnostics_cache[bufnr] or {}
145+
146+
-- Do the upfront work of filtering and sorting
147+
diagnostics = filter_by_severity(opts, diagnostics)
148+
149+
-- Find the sources of the incoming diagnostics.
150+
-- It's almost always a single source, but you never know.
151+
local sources = {}
152+
for _, diag in ipairs(diagnostics) do
153+
if not vim.tbl_contains(sources, diag.source) then
154+
table.insert(sources, diag.source)
155+
end
156+
end
157+
158+
-- Clear the diagnostics that are from the incoming source
159+
diag_buf = vim.tbl_filter(function(diag)
160+
return not vim.tbl_contains(sources, diag.source)
161+
end, diag_buf)
162+
163+
-- Insert and sort the results
164+
for _, diag in pairs(diagnostics) do
165+
table.insert(diag_buf, diag)
166+
end
167+
168+
table.sort(diag_buf, function(a, b)
169+
return a.severity < b.severity
170+
end)
171+
172+
diagnostics_cache[bufnr] = diag_buf
173+
end
174+
125175
---@param opts DiagnosticConfig
126176
---@param event table
127177
local function apply_virtual_texts(opts, event)
@@ -139,9 +189,9 @@ local function apply_virtual_texts(opts, event)
139189
return
140190
end
141191

142-
-- Get and validate diagnostics
143-
local ok, diagnostics = pcall(vim.diagnostic.get, event.buf)
144-
if not ok or vim.tbl_isempty(diagnostics) then
192+
-- Get diagnostics and clear them if needed
193+
local diagnostics = diagnostics_cache[event.buf] or {}
194+
if vim.tbl_isempty(diagnostics) then
145195
extmarks.clear(event.buf)
146196
return
147197
end
@@ -214,6 +264,7 @@ end
214264
local function detach_buffer(buf)
215265
timers.close(buf)
216266
attached_buffers[buf] = nil
267+
diagnostics_cache[buf] = nil
217268
end
218269

219270
---@param autocmd_ns number
@@ -274,8 +325,9 @@ local function setup_buffer_autocmds(autocmd_ns, opts, event, throttled_apply)
274325
-- Setup diagnostic change events
275326
vim.api.nvim_create_autocmd("DiagnosticChanged", {
276327
group = autocmd_ns,
277-
callback = function()
278-
if vim.api.nvim_buf_is_valid(event.buf) then
328+
callback = function(args)
329+
if vim.api.nvim_buf_is_valid(args.buf) then
330+
update_diagnostics_cache(opts, args.buf, args.data.diagnostics)
279331
vim.api.nvim_exec_autocmds("User", { pattern = USER_EVENT })
280332
end
281333
end,

0 commit comments

Comments
 (0)