Skip to content

Commit 4f786e0

Browse files
committed
Throttle infoview updates on cursor movement
Rapid cursor movement (e.g. holding j/k) was sending an LSP request for every CursorMoved event. Now the first event fires immediately but subsequent ones during a 50ms cooldown are collapsed, with only the latest position flushed once the cooldown expires.
1 parent d0e633c commit 4f786e0

5 files changed

Lines changed: 126 additions & 1 deletion

File tree

lua/lean/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
---@class lean.infoview.Config
4848
---@field mappings? { [string]: ElementEvent }
4949
---@field orientation? "auto"|"vertical"|"horizontal" what orientation to use for opened infoviews
50+
---@field update_cooldown? integer milliseconds to throttle cursor-move updates (default 50, 0 to disable)
5051
---@field view_options? InfoviewViewOptions
5152
---@field severity_markers? table<lsp.DiagnosticSeverity, string> characters to use for denoting diagnostic severity
5253

lua/lean/infoview.lua

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
local Buffer = require 'std.nvim.buffer'
99
local Window = require 'std.nvim.window'
1010
local async = require 'std.async'
11+
local throttle = require 'std.throttle'
1112
---Convert a buffer position to a human-readable (1, 1)-indexed string.
1213
---Takes the workspace into account in order to return a relative path.
1314
---@param buffer Buffer
@@ -98,6 +99,7 @@ local options = {
9899

99100
autoopen = true,
100101
autopause = false,
102+
update_cooldown = 50,
101103
indicators = 'auto',
102104
show_processing = true,
103105
show_no_info_message = false,
@@ -203,6 +205,9 @@ function Infoview:new(obj)
203205
}, self)
204206
new_infoview.info = Info:new { infoview = new_infoview }
205207
new_infoview.info:render()
208+
new_infoview.__throttled_pin_update = throttle(options.update_cooldown, function(pin)
209+
pin:update()
210+
end)
206211
return new_infoview
207212
end
208213

@@ -720,7 +725,19 @@ function Infoview:__update()
720725
end
721726
info:update_last_window()
722727
local cursor = vim.api.nvim_win_get_cursor(0)
723-
info:move_pin(Buffer:current(), { cursor[1] - 1, cursor[2] })
728+
local buffer = Buffer:current()
729+
local pos = { cursor[1] - 1, cursor[2] }
730+
731+
-- Update the diff pin first, while the extmark is still at the old
732+
-- position (it reads the extmark to get the "previous" location).
733+
if info.__auto_diff_pin then
734+
info:__update_auto_diff_pin(buffer, pos)
735+
end
736+
-- Move the extmark immediately so the pin indicator stays responsive,
737+
-- but throttle the LSP request + render so rapid cursor movement
738+
-- doesn't flood the server.
739+
info.pin:__update_extmark(buffer, pos)
740+
self.__throttled_pin_update(info.pin)
724741
end
725742

726743
---Directly mark that the infoview has died. What a shame.

lua/std/throttle.lua

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---@mod std.throttle Throttle
2+
---@brief [[
3+
--- Leading-edge throttle with trailing flush.
4+
---@brief ]]
5+
6+
--- Create a throttled function that fires immediately on the first call,
7+
--- then suppresses further calls during an `ms` millisecond cooldown.
8+
--- If calls were suppressed, the latest one fires after the cooldown expires.
9+
---@param ms integer cooldown in milliseconds
10+
---@param fn function the function to throttle
11+
---@return function throttled the throttled wrapper
12+
local function throttle(ms, fn)
13+
if ms == 0 then
14+
return fn
15+
end
16+
17+
local timer = vim.uv.new_timer()
18+
local pending = nil
19+
20+
return function(...)
21+
if not timer:is_active() then
22+
fn(...)
23+
else
24+
pending = { ... }
25+
end
26+
timer:start(
27+
ms,
28+
0,
29+
vim.schedule_wrap(function()
30+
if pending then
31+
local args = pending
32+
pending = nil
33+
fn(unpack(args))
34+
end
35+
end)
36+
)
37+
end
38+
end
39+
40+
return throttle

scripts/minimal_init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ vim.g.lean_config = {
3232
print('error: ' .. lines)
3333
end,
3434
},
35+
infoview = { update_cooldown = 0 },
3536
mappings = true,
3637
}
3738

spec/std/throttle_spec.lua

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
local throttle = require 'std.throttle'
2+
3+
describe('throttle', function()
4+
it('fires immediately on the first call', function()
5+
local calls = {}
6+
local fn = throttle(50, function(x)
7+
table.insert(calls, x)
8+
end)
9+
10+
fn 'a'
11+
assert.are.same({ 'a' }, calls)
12+
end)
13+
14+
it('suppresses calls during the cooldown', function()
15+
local calls = {}
16+
local fn = throttle(50, function(x)
17+
table.insert(calls, x)
18+
end)
19+
20+
fn 'a'
21+
fn 'b'
22+
fn 'c'
23+
assert.are.same({ 'a' }, calls)
24+
end)
25+
26+
it('flushes the latest suppressed call after the cooldown', function()
27+
local calls = {}
28+
local fn = throttle(10, function(x)
29+
table.insert(calls, x)
30+
end)
31+
32+
fn 'a'
33+
fn 'b'
34+
fn 'c'
35+
assert.are.same({ 'a' }, calls)
36+
37+
vim.wait(50, function()
38+
return #calls > 1
39+
end)
40+
assert.are.same({ 'a', 'c' }, calls)
41+
end)
42+
43+
it('fires immediately again after the cooldown expires with no pending call', function()
44+
local calls = {}
45+
local fn = throttle(10, function(x)
46+
table.insert(calls, x)
47+
end)
48+
49+
fn 'a'
50+
vim.wait(50, function() end)
51+
fn 'b'
52+
assert.are.same({ 'a', 'b' }, calls)
53+
end)
54+
55+
it('passes through directly when cooldown is 0', function()
56+
local calls = {}
57+
local fn = throttle(0, function(x)
58+
table.insert(calls, x)
59+
end)
60+
61+
fn 'a'
62+
fn 'b'
63+
fn 'c'
64+
assert.are.same({ 'a', 'b', 'c' }, calls)
65+
end)
66+
end)

0 commit comments

Comments
 (0)