diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 5ae83df462..77fb305524 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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`. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 1f8cd421d7..ce7bb8240b 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 3c9534a091..da762aa878 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -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 diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 8b6aad226d..f891ba9754 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -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 (0–100) + ---@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 + 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 diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 5f16787413..32717d4d32 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -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 ? '◐ ' : '' %}", diff --git a/test/functional/ui/statusline_spec.lua b/test/functional/ui/statusline_spec.lua index 73eb156eb4..903765303b 100644 --- a/test/functional/ui/statusline_spec.lua +++ b/test/functional/ui/statusline_spec.lua @@ -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()