From 069be911064d0e64f1efc13348dbb878afaf347f Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 5 Sep 2025 17:12:28 +0300 Subject: [PATCH] feat(pack): use `nvim_echo{kind=progress}` to report progress #35625 Problem: Progress reports use plain `nvim_echo()` with manually constructed messages and populate history on every call. Solution: Use `nvim_echo()` with newly added `kind=progress` which (at least for now) is meant to be a unified interface for showing progress report. Also save in history only first and last progress report messages. --- runtime/lua/vim/pack.lua | 39 ++++++-------------- test/functional/plugin/pack_spec.lua | 54 ++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 1614cc0c83..ae999e95e7 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -368,35 +368,16 @@ local function trigger_event(p, event_name, kind) api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data }) end ---- @param title string +--- @param action string --- @return fun(kind: 'begin'|'report'|'end', percent: integer, fmt: string, ...:any): nil -local function new_progress_report(title) - -- TODO(echasnovski): currently print directly in command line because - -- there is no robust built-in way of showing progress: - -- - `vim.ui.progress()` is planned and is a good candidate to use here. - -- - Use `'$/progress'` implementation in 'vim.pack._lsp' if there is - -- a working built-in '$/progress' handler. Something like this: - -- ```lua - -- local progress_token_count = 0 - -- function M.new_progress_report(title) - -- progress_token_count = progress_token_count + 1 - -- return vim.schedule_wrap(function(kind, msg, percent) - -- local value = { kind = kind, message = msg, percentage = percent } - -- dispatchers.notification( - -- '$/progress', - -- { token = progress_token_count, value = value } - -- ) - -- end - -- end - -- ``` - -- Any of these choices is better as users can tweak how progress is shown. +local function new_progress_report(action) + local progress = { kind = 'progress', title = 'vim.pack' } return vim.schedule_wrap(function(kind, percent, fmt, ...) - local progress = kind == 'end' and 'done' or ('%3d%%'):format(percent) - local details = (' %s %s'):format(title, fmt:format(...)) - local chunks = { { 'vim.pack', 'ModeMsg' }, { ': ' }, { progress, 'WarningMsg' }, { details } } - -- TODO: need to add support for progress-messages api - api.nvim_echo(chunks, true, {}) + progress.status = kind == 'end' and 'success' or 'running' + progress.percent = percent + local msg = ('%s %s'):format(action, fmt:format(...)) + progress.id = api.nvim_echo({ { msg } }, kind ~= 'report', progress) -- Force redraw to show installation progress during startup vim.cmd.redraw({ bang = true }) end) @@ -408,9 +389,9 @@ local copcall = package.loaded.jit and pcall or require('coxpcall').pcall --- Execute function in parallel for each non-errored plugin in the list --- @param plug_list vim.pack.Plug[] --- @param f async fun(p: vim.pack.Plug) ---- @param progress_title string -local function run_list(plug_list, f, progress_title) - local report_progress = new_progress_report(progress_title) +--- @param progress_action string +local function run_list(plug_list, f, progress_action) + local report_progress = new_progress_report(progress_action) -- Construct array of functions to execute in parallel local n_finished = 0 diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index 4e88c03c42..ca285a5706 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -243,32 +243,55 @@ local function find_in_log(log, event, kind, repo_name, version) return res end -local function validate_progress_report(title, step_names) - -- NOTE: Assumes that message history contains only progress report messages - local messages = vim.split(n.exec_capture('messages'), '\n') - local n_steps = #step_names - eq(n_steps + 2, #messages) +local function track_nvim_echo() + exec_lua(function() + _G.echo_log = {} + local nvim_echo_orig = vim.api.nvim_echo + ---@diagnostic disable-next-line: duplicate-set-field + vim.api.nvim_echo = function(...) + table.insert(_G.echo_log, vim.deepcopy({ ... })) + return nvim_echo_orig(...) + end + end) +end - local init_msg = ('vim.pack: 0%% %s (0/%d)'):format(title, n_steps) - eq(init_msg, messages[1]) +local function validate_progress_report(action, step_names) + -- NOTE: Assume that `nvim_echo` mocked log has only progress report messages + local echo_log = exec_lua('return _G.echo_log') ---@type table[] + local n_steps = #step_names + eq(n_steps + 2, #echo_log) + + local progress = { kind = 'progress', title = 'vim.pack', status = 'running', percent = 0 } + local init_step = { { { ('%s (0/%d)'):format(action, n_steps) } }, true, progress } + eq(init_step, echo_log[1]) local steps_seen = {} --- @type table for i = 1, n_steps do - local percent = math.floor(100 * i / n_steps) - local msg = ('vim.pack: %3d%% %s (%d/%d)'):format(percent, title, i, n_steps) + local echo_args = echo_log[i + 1] + -- NOTE: There is no guaranteed order (as it is async), so check that some - -- expected step name is used + -- expected step name is used in the message + local msg = ('%s (%d/%d)'):format(action, i, n_steps) local pattern = '^' .. vim.pesc(msg) .. ' %- (%S+)$' - local step = messages[i + 1]:match(pattern) + local step = echo_args[1][1][1]:match(pattern) ---@type string eq(true, vim.tbl_contains(step_names, step)) steps_seen[step] = true + + -- Should not add intermediate progress report to history + eq(echo_args[2], false) + + -- Should update a single message by its id (computed after first call) + progress.id = progress.id or echo_args[3].id ---@type integer + progress.percent = math.floor(100 * i / n_steps) + eq(echo_args[3], progress) end -- Should report all steps eq(n_steps, vim.tbl_count(steps_seen)) - local final_msg = ('vim.pack: done %s (%d/%d)'):format(title, n_steps, n_steps) - eq(final_msg, messages[n_steps + 2]) + progress.percent, progress.status = 100, 'success' + local final_step = { { { ('%s (%d/%d)'):format(action, n_steps, n_steps) } }, true, progress } + eq(final_step, echo_log[n_steps + 2]) end local function is_jit() @@ -431,6 +454,7 @@ describe('vim.pack', function() end) it('shows progress report during installation', function() + track_nvim_echo() exec_lua(function() vim.pack.add({ repos_src.basic, repos_src.defbranch }) end) @@ -1080,6 +1104,7 @@ describe('vim.pack', function() end) it('shows progress report', function() + track_nvim_echo() exec_lua(function() vim.pack.add({ repos_src.fetch, repos_src.defbranch }) vim.pack.update() @@ -1087,7 +1112,7 @@ describe('vim.pack', function() -- During initial download validate_progress_report('Downloading updates', { 'fetch', 'defbranch' }) - n.exec('messages clear') + exec_lua('_G.echo_log = {}') -- During application (only for plugins that have updates) n.exec('write') @@ -1095,6 +1120,7 @@ describe('vim.pack', function() -- During force update n.clear() + track_nvim_echo() repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"') git_add_commit('Commit to be added 3', 'fetch')