Skip to content

Commit e6d88fe

Browse files
authored
Merge pull request #43 from nikero41/progress-bar-improvements
Improve progress bar
2 parents bc22a03 + 9c378ed commit e6d88fe

2 files changed

Lines changed: 153 additions & 54 deletions

File tree

lua/autocmds.lua

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,7 @@ function Autocmds:setup()
121121
vim.api.nvim_create_autocmd("LspProgress", {
122122
desc = "Show progress bar for LSP progress on terminal",
123123
group = vim.api.nvim_create_augroup("terminal-progress-bar", { clear = true }),
124-
callback = function(args)
125-
local params = args.data and args.data.params or {}
126-
local value = params.value or {}
127-
128-
local progress_bar = require("nikero.progress_bar")
129-
130-
if value.kind == "end" then
131-
progress_bar:clear()
132-
return
133-
elseif value.percentage ~= nil then
134-
progress_bar:progress(value.percentage)
135-
else
136-
progress_bar:loading()
137-
end
138-
end,
124+
callback = function(args) require("nikero.progress_bar"):on_lsp_progress(args) end,
139125
})
140126
end
141127

lua/nikero/progress_bar.lua

Lines changed: 152 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
---@class ProgressBar
2-
---@field loading fun(self: ProgressBar)
3-
---@field error fun(self: ProgressBar, percent: number | nil)
4-
---@field progress fun(self: ProgressBar, percent: number | nil)
5-
---@field clear fun(self: ProgressBar)
2+
---@field tasks table<string, { percentage?: number }>
3+
---@field render_interval integer
4+
---@field clear_delay integer
5+
---@field render_scheduled boolean
6+
---@field render_timer uv.uv_timer_t?
7+
---@field clear_timer uv.uv_timer_t?
8+
---@field last_state integer?
9+
---@field last_percent integer?
610
local ProgressBar = {}
11+
ProgressBar.__index = ProgressBar
712

813
local ESC = "\27"
914

10-
-- OSC ... ST (ST can be ESC \)
15+
---@enum con_emu_state
16+
local con_emu_state = {
17+
clear = 0,
18+
progress = 1,
19+
error = 2,
20+
loading = 3,
21+
paused = 4,
22+
}
23+
1124
---@param payload string
1225
---@return string
1326
local function osc(payload)
1427
local seq = ESC .. "]" .. payload .. ESC .. "\\"
15-
16-
-- Wrap an escape sequence so tmux forwards it to the outer terminal.
17-
-- This requires tmux allow-passthrough on.
18-
if os.getenv("TMUX") ~= nil then
19-
-- Inside the DCS wrapper, tmux expects ESC bytes to be doubled.
28+
if vim.env.TMUX ~= nil then
2029
seq = seq:gsub(ESC, ESC .. ESC)
2130
return ESC .. "Ptmux;" .. seq .. ESC .. "\\"
2231
end
23-
2432
return seq
2533
end
2634

@@ -34,45 +42,150 @@ local function clamp(n, lo, hi)
3442
return n
3543
end
3644

37-
---@enum con_emu_state
38-
local con_emu_state = {
39-
clear = 0,
40-
progress = 1, -- normal
41-
error = 2,
42-
loading = 3, -- indeterminate
43-
paused = 4,
44-
}
45-
4645
---@param state con_emu_state
47-
---@param percent number|nil
46+
---@param percent integer|nil
4847
---@return string
4948
local function progress_osc(state, percent)
50-
if percent ~= nil then
51-
percent = math.floor(clamp(percent, 0, 100))
52-
return osc(("9;4;%d;%d"):format(state, percent))
53-
end
49+
if percent ~= nil then return osc(("9;4;%d;%d"):format(state, percent)) end
5450
return osc(("9;4;%d"):format(state))
5551
end
5652

57-
---@param seq string
58-
local function write_osc(seq)
59-
-- vim.api.nvim_ui_send(seq)
60-
-- NOTE: change this when upgrading to Neovim 0.12
61-
-- selene: allow(incorrect_standard_library_use)
62-
io.stdout:write(seq)
63-
-- selene: allow(incorrect_standard_library_use)
64-
io.stdout:flush()
53+
---@param client_id integer|string
54+
---@param token unknown
55+
---@return string
56+
local function task_key(client_id, token) return tostring(client_id) .. ":" .. tostring(token) end
57+
58+
---@param opts? { render_interval?: integer, clear_delay?: integer }
59+
---@return ProgressBar
60+
function ProgressBar.new(opts)
61+
opts = opts or {}
62+
return setmetatable({
63+
tasks = {},
64+
render_interval = opts.render_interval or 100,
65+
clear_delay = opts.clear_delay or 300,
66+
render_scheduled = false,
67+
render_timer = nil,
68+
clear_timer = nil,
69+
last_state = nil,
70+
last_percent = nil,
71+
}, ProgressBar)
72+
end
73+
74+
---@param state con_emu_state
75+
---@param percent number|nil
76+
---@param force boolean|nil
77+
function ProgressBar:_send(state, percent, force)
78+
if percent ~= nil then percent = math.floor(clamp(percent, 0, 100)) end
79+
if not force and self.last_state == state and self.last_percent == percent then return end
80+
81+
local payload = progress_osc(state, percent)
82+
local ok = pcall(vim.api.nvim_ui_send, payload)
83+
if not ok then return end
84+
85+
self.last_state = state
86+
self.last_percent = percent
87+
end
88+
89+
function ProgressBar:_stop_clear_timer()
90+
if self.clear_timer then self.clear_timer:stop() end
91+
end
92+
93+
function ProgressBar:_schedule_clear()
94+
self:_stop_clear_timer()
95+
if self.clear_timer == nil then self.clear_timer = vim.uv.new_timer() end
96+
self.clear_timer:start(
97+
self.clear_delay,
98+
0,
99+
vim.schedule_wrap(function()
100+
if next(self.tasks) ~= nil then return end
101+
self:_send(con_emu_state.clear, nil, true)
102+
end)
103+
)
104+
end
105+
106+
function ProgressBar:_render_now()
107+
self:_stop_clear_timer()
108+
109+
local count = 0
110+
local percent = nil
111+
for _, task in pairs(self.tasks) do
112+
count = count + 1
113+
if count == 1 then
114+
percent = task.percentage
115+
else
116+
percent = nil
117+
break
118+
end
119+
end
120+
121+
if count == 0 then
122+
self:_schedule_clear()
123+
elseif percent ~= nil then
124+
self:_send(con_emu_state.progress, percent)
125+
else
126+
self:_send(con_emu_state.loading)
127+
end
128+
end
129+
130+
function ProgressBar:_schedule_render()
131+
if self.render_scheduled then return end
132+
133+
self.render_scheduled = true
134+
if self.render_timer == nil then self.render_timer = vim.uv.new_timer() end
135+
self.render_timer:start(
136+
self.render_interval,
137+
0,
138+
vim.schedule_wrap(function()
139+
self.render_scheduled = false
140+
self:_render_now()
141+
end)
142+
)
143+
end
144+
145+
---@param args vim.api.keyset.create_autocmd.callback_args
146+
function ProgressBar:on_lsp_progress(args)
147+
local data = args.data or {}
148+
local params = data.params or {}
149+
local value = params.value or {}
150+
local token = params.token
151+
if token == nil then return end
152+
153+
local key = task_key(data.client_id or 0, token)
154+
if value.kind == "end" then
155+
self.tasks[key] = nil
156+
else
157+
self.tasks[key] = { percentage = value.percentage }
158+
end
159+
160+
self:_schedule_render()
161+
end
162+
163+
function ProgressBar:clear()
164+
self.tasks = {}
165+
self:_send(con_emu_state.clear, nil, true)
166+
end
167+
168+
---@param self ProgressBar
169+
function ProgressBar:destroy()
170+
if self.render_timer then
171+
self.render_timer:stop()
172+
self.render_timer:close()
173+
self.render_timer = nil
174+
end
175+
if self.clear_timer then
176+
self.clear_timer:stop()
177+
self.clear_timer:close()
178+
self.clear_timer = nil
179+
end
180+
self:clear()
65181
end
66182

67-
function ProgressBar:loading() write_osc(progress_osc(con_emu_state.loading)) end
68-
function ProgressBar:error(percent) write_osc(progress_osc(con_emu_state.error, percent)) end
69-
function ProgressBar:progress(percent) write_osc(progress_osc(con_emu_state.progress, percent)) end
70-
function ProgressBar:clear() write_osc(progress_osc(con_emu_state.clear)) end
183+
local progress_bar = ProgressBar.new()
71184

72185
vim.api.nvim_create_autocmd("VimLeavePre", {
73186
desc = "Clear progress bar on exit",
74187
group = vim.api.nvim_create_augroup("clear-progress-bar", { clear = true }),
75-
callback = function() ProgressBar:clear() end,
188+
callback = function() progress_bar:destroy() end,
76189
})
77190

78-
return ProgressBar
191+
return progress_bar

0 commit comments

Comments
 (0)