feat(progress): status api, 'statusline' integration #35428

Problem:
Default statusline doesn't show progress status.

Solution:
- Provide `vim.ui.progress_status()`.
- Include it in the default 'statusline'.

How it works:
Status text summarizes "running" progress messages.
 - If none: returns empty string
 - If one running item: "title:  percent%"
 - If multiple running items: "Progress: N items avg-percent%"
This commit is contained in:
Shadman
2026-03-20 17:18:20 +06:00
committed by GitHub
parent f9b2189b28
commit 24684f90ea
6 changed files with 150 additions and 1 deletions

View File

@@ -5031,6 +5031,13 @@ vim.ui.open({path}, {opt}) *vim.ui.open()*
See also: ~
• |vim.system()|
vim.ui.progress_status() *vim.ui.progress_status()*
Gets the status of currently running progress messages, in a format
convenient for inclusion in 'statusline'.
Return: ~
(`string`) formatted text of progress status for statusline
vim.ui.select({items}, {opts}, {on_choice}) *vim.ui.select()*
Prompts the user to pick from a list of items, allowing arbitrary
(potentially asynchronous) work until `on_choice`.

View File

@@ -466,6 +466,8 @@ UI
• Cursor shape indicates when it is behind an unfocused floating window.
• Improved LSP signature help rendering.
• Multigrid UIs can call nvim_input_mouse with grid 0 to let Nvim decide the grid.
• |vim.ui.progress_status()| returns a formatted string of currently
running |progress-message|.
VIMSCRIPT

View File

@@ -6994,7 +6994,7 @@ vim.wo.stc = vim.wo.statuscolumn
---
---
--- @type string
vim.o.statusline = "%<%f %h%w%m%r %{% v:lua.require('vim._core.util').term_exitcode() %}%=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% luaeval('(package.loaded[''vim.diagnostic''] and next(vim.diagnostic.count()) and vim.diagnostic.status() .. '' '') or '''' ') %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}"
vim.o.statusline = "%<%f %h%w%m%r %{% v:lua.require('vim._core.util').term_exitcode() %}%=%{% luaeval('(package.loaded[''vim.ui''] and vim.ui.progress_status()) or '''' ')%}%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% luaeval('(package.loaded[''vim.diagnostic''] and next(vim.diagnostic.count()) and vim.diagnostic.status() .. '' '') or '''' ') %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}"
vim.o.stl = vim.o.statusline
vim.wo.statusline = vim.o.statusline
vim.wo.stl = vim.wo.statusline

View File

@@ -306,4 +306,91 @@ function M._get_urls()
return urls
end
do
---@class ProgressMessage
---@field id? number|string ID of the progress message
---@field title? string Title of the progress message
---@field status string Status: "running" | "success" | "failed" | "cancel"
---@field percent? integer Percent complete (0100)
---@private
--- Cache of active progress messages, keyed by msg_id
--- TODO(justinmk): visibility of "stale" (never-finished) Progress. https://github.com/neovim/neovim/pull/35428#discussion_r2942696157
---@type table<integer, ProgressMessage>
local progress = {}
-- store progress events
local progress_group, progress_autocmd = nil, nil
---Initialize progress event listeners
local function progress_init()
progress_group = vim.api.nvim_create_augroup('nvim.ui.progress_status', { clear = true })
progress_autocmd = vim.api.nvim_create_autocmd('Progress', {
group = progress_group,
desc = 'Tracks progress messages for vim.ui.progress_status()',
---@param ev {data: {id: integer, title: string, status: string, percent: integer}}
callback = function(ev)
if not ev.data or not ev.data.id then
return
end
progress[ev.data.id] = {
id = ev.data.id,
title = ev.data.title,
status = ev.data.status,
percent = ev.data.percent or 0,
}
-- Clear finished items
if
ev.data.status == 'success'
or ev.data.percent == 100
or ev.data.status == 'failed'
or ev.data.status == 'cancel'
then
progress[ev.data.id] = nil
end
end,
})
end
---Return statusline text summarizing progress messages.
--- - If none: returns empty string
--- - If one running item: "title: 42%"
--- - If multiple running items: "Progress: N items AVG%"
---@param running ProgressMessage[]
---@return string
local function progress_status_fmt(running)
local count = #running
if count == 0 then
return '' -- nothing to show
elseif count == 1 then
local progress_item = running[1]
if progress_item.title == nil then
return string.format('%d%%%% ', progress_item.percent or 0)
end
return string.format('%s: %d%%%% ', progress_item.title, progress_item.percent or 0)
else
local sum = 0 ---@type integer
for _, progress_item in ipairs(running) do
sum = sum + (progress_item.percent or 0)
end
local avg = math.floor(sum / count)
return string.format('Progress: %d items %d%%%% ', count, avg)
end
end
--- Gets the status of currently running progress messages, in a format
--- convenient for inclusion in 'statusline'.
---@return string formatted text of progress status for statusline
function M.progress_status()
-- Create progress event listener on first call
if progress_autocmd == nil then
progress_init()
end
local running = vim.tbl_values(progress)
return progress_status_fmt(running) or ''
end
end
return M

View File

@@ -8807,6 +8807,7 @@ local options = {
'%f %h%w%m%r ',
"%{% v:lua.require('vim._core.util').term_exitcode() %}",
'%=',
"%{% luaeval('(package.loaded[''vim.ui''] and vim.ui.progress_status()) or '''' ')%}",
"%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}",
"%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}",
"%{% &busy > 0 ? '◐ ' : '' %}",

View File

@@ -944,6 +944,7 @@ describe('default statusline', function()
screen = Screen.new(60, 16)
screen:add_extra_attr_ids {
[100] = { foreground = Screen.colors.Magenta1, bold = true },
[131] = { foreground = Screen.colors.NvimDarkGreen },
}
command('set laststatus=2')
command('set ruler')
@@ -964,6 +965,7 @@ describe('default statusline', function()
'%f %h%w%m%r ',
"%{% v:lua.require('vim._core.util').term_exitcode() %}",
'%=',
"%{% luaeval('(package.loaded[''vim.ui''] and vim.ui.progress_status()) or '''' ')%}",
"%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}",
"%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}",
"%{% &busy > 0 ? '◐ ' : '' %}",
@@ -1040,6 +1042,56 @@ describe('default statusline', function()
screen:expect({ any = '%[Exit: 9%]' })
expect_exitcode(9)
end)
it('shows and updates progress status', function()
exec_lua("vim.o.statusline = ''")
local function get_progress()
return exec_lua(function()
local stl_str = vim.ui.progress_status()
return vim.api.nvim_eval_statusline(stl_str, {}).str
end)
end
eq('', get_progress())
---@type integer|string
local id1 = api.nvim_echo(
{ { 'searching...' } },
true,
{ kind = 'progress', title = 'test', status = 'running', percent = 10 }
)
eq('test: 10% ', get_progress())
api.nvim_echo(
{ { 'searching' } },
true,
{ id = id1, kind = 'progress', percent = 50, status = 'running', title = 'terminal(ripgrep)' }
)
eq('terminal(ripgrep): 50% ', get_progress())
api.nvim_echo(
{ { 'searching...' } },
true,
{ kind = 'progress', title = 'second-item', status = 'running', percent = 20 }
)
eq('Progress: 2 items 35% ', get_progress())
api.nvim_echo({ { 'searching' } }, true, {
id = id1,
kind = 'progress',
percent = 100,
status = 'success',
title = 'terminal(ripgrep)',
})
eq('second-item: 20% ', get_progress())
exec('redrawstatus')
screen:expect([[
^ |
{1:~ }|*13
{3:[No Name] second-item: 20% 0,0-1 All}|
{131:terminal(ripgrep)}: {19:100% }searching |
]])
end)
end)
describe("'statusline' in floatwin", function()