feat(api): nvim_echo can emit Progress messages/events #34846

Problem:
Nvim does not have a core concept for indicating "progress" of
long-running tasks. The LspProgress event is specific to LSP.

Solution:
- `nvim_echo` can emit `kind="progress"` messages.
  - Emits a `Progress` event.
  - Includes new fields (id, status, percent) in the `msg_show` ui-event.
  - The UI is expected to overwrite any message having the same id.
- Messages have a globally unique ID.
  - `nvim_echo` returns the message ID.
- `nvim_echo(… {id=…})` updates existing messages.

Example:

    local grp = vim.api.nvim_create_augroup("Msg", {clear = true})
    vim.api.nvim_create_autocmd('Progress', {
      pattern={"term"},
      group = grp,
      callback = function(ev)
        print(string.format('event fired: %s', vim.inspect(ev))..'\n')
      end
    })

    -- require('vim._extui').enable({enable=true, msg={target='msg', timeout=1000}})
    vim.api.nvim_echo({{'searching'}}, true, {kind='progress',  percent=80, status='running', title="terminal(ripgrep)"})
    local id = vim.api.nvim_echo({{'searching'}}, true, {kind='progress', status='running', percent=10, title="terminal(ripgrep)"})
    vim.api.nvim_echo({}, true, {id = id, kind='progress', percent=20, status = 'running', title='find tests'})
    vim.api.nvim_echo({}, true, {id = id, kind='progress', status='running', percent=70})
    vim.api.nvim_echo({{'complete'}}, true, {id = id, kind='progress', status='success', percent=100, title="find tests"})

Followups:
- Integrate with 'statusline' by listening to the Progress autocmd event.
- Integrate progress ui-event with `vim._extui`.
This commit is contained in:
Shadman
2025-08-27 02:48:53 +06:00
committed by GitHub
parent 1e1619de83
commit 8b171852a9
19 changed files with 648 additions and 19 deletions

View File

@@ -825,7 +825,7 @@ will be set to zero, but can be changed and used for the replacing cmdline or
message window. Cmdline state is emitted as |ui-cmdline| events, which the UI
must handle.
["msg_show", kind, content, replace_last, history, append] ~
["msg_show", kind, content, replace_last, history, append, msg_id, progress] ~
Display a message to the user.
kind
@@ -845,6 +845,7 @@ must handle.
"list_cmd" List output for various commands (|:ls|, |:set|, …)
"lua_error" Error in |:lua| code
"lua_print" |print()| from |:lua| code
"progress" Progress message emitted by |nvim_echo()|
"rpc_error" Error response from |rpcrequest()|
"quickfix" Quickfix navigation message
"search_cmd" Entered search command
@@ -881,6 +882,22 @@ must handle.
True if the message should be appeneded to the previous message,
rather than started on a new line. Is set for |:echon|.
msg_id
Unique identifier for the message. It can either be an integer or
string. When message of same id appears it should replace the older message.
progress
Progress-message properties:
• title: Title string of the progress message.
• status: Status of the progress message. Can contain one of
the following values
• success: The progress item completed successfully
• running: The progress is ongoing
• failed: The progress item failed
• cancel: The progressing process should be canceled.
• percent: How much progress is done on the progress
message
["msg_clear"] ~
Clear all messages currently displayed by "msg_show", emitted after
clearing the screen (messages sent by other "msg_" events below should

View File

@@ -662,6 +662,7 @@ nvim_echo({chunks}, {history}, {opts}) *nvim_echo()*
(optional) name or ID `hl_group`.
• {history} (`boolean`) if true, add to |message-history|.
• {opts} (`vim.api.keyset.echo_opts`) Optional parameters.
• id: message id for updating existing message.
• err: Treat the message like `:echoerr`. Sets `hl_group`
to |hl-ErrorMsg| by default.
• kind: Set the |ui-messages| kind with which this message
@@ -669,6 +670,22 @@ nvim_echo({chunks}, {history}, {opts}) *nvim_echo()*
• verbose: Message is controlled by the 'verbose' option.
Nvim invoked with `-V3log` will write the message to the
"log" file instead of standard output.
• title: The title for |progress-message|.
• status: Current status of the |progress-message|. Can be
one of the following values
• success: The progress item completed successfully
• running: The progress is ongoing
• failed: The progress item failed
• cancel: The progressing process should be canceled.
note: Cancel needs to be handled by progress initiator
by listening for the `Progress` event
• percent: How much progress is done on the progress
message
• data: dictionary containing additional information
Return: ~
(`integer|string`) Message id.
• -1 means nvim_echo didn't show a message
nvim_eval_statusline({str}, {opts}) *nvim_eval_statusline()*
Evaluates statusline string.

View File

@@ -787,6 +787,31 @@ ModeChanged After changing the mode. The pattern is
:au ModeChanged [vV\x16]*:* let &l:rnu = mode() =~# '^[vV\x16]'
:au ModeChanged *:[vV\x16]* let &l:rnu = mode() =~# '^[vV\x16]'
:au WinEnter,WinLeave * let &l:rnu = mode() =~# '^[vV\x16]'
Progress *Progress*
After a progress message is created or updated via
`nvim_echo`. The pattern is matched against
title of the message. The |event-data| contains:
id: id of the message
text: text of the message
title: title of the progress message
status: status of the progress message
percent: how much progress has been
made for this progress item
Usage example:
>
vim.api.nvim_create_autocmd('Progress', {
pattern={"term"},
callback = function(ev)
print(string.format('event fired: %s', vim.inspect(ev)))
end
})
local id = vim.api.nvim_echo({{'searching...'}}, true,
{kind='progress', status='running', percent=10, title="term"})
vim.api.nvim_echo({{'searching'}}, true,
{id = id, kind='progress', status='running', percent=50, title="term"})
vim.api.nvim_echo({{'done'}}, true,
{id = id, kind='progress', status='success', percent=100, title="term"})
< *OptionSet*
OptionSet After setting an option (except during
|startup|). The |autocmd-pattern| is matched

View File

@@ -845,4 +845,29 @@ The |g<| command can be used to see the last page of previous command output.
This is especially useful if you accidentally typed <Space> at the hit-enter
prompt.
==============================================================================
4. PROGRESS MESSAGE *progress-message*
Nvim can emit progress-message, which are a special kind of |ui-messages|
used to report the state of long-running tasks.
Progress messages are created or updated using |nvim_echo()| with `kind='progress'`
and the related options. Each message has a unique `msg_id`. A subsequent
message with the same `msg_id` replaces the older one.
Events: ~
• msg_show |ui-messages| event is fired for ext-ui upon creation/update of a
progress-message
• Updating or creating a progress message also triggers the |Progress| autocommand.
Example: >
local id = vim.api.nvim_echo({{'searching...'}}, true,
{kind='progress', status='running', percent=10, title="term"})
vim.api.nvim_echo({{'searching'}}, true,
{id=id, kind='progress', status='running', percent=50, title="term"})
vim.api.nvim_echo({{'done'}}, true,
{id=id, kind='progress', status='success', percent=100, title="term"})
<
See also: |nvim_echo()| |ui-messages| |Progress|
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@@ -139,9 +139,10 @@ API
actually trusted.
• Added |vim.lsp.is_enabled()| to check if a given LSP config has been enabled
by |vim.lsp.enable()|.
• |nvim_echo()| can set the |ui-messages| kind with which to emit the message.
• |nvim_ui_send()| writes arbitrary data to a UI's stdout. Use this to write
escape sequences to the terminal when Nvim is running in the |TUI|.
• |nvim_echo()| can set the |ui-messages| kind with which to emit the message.
• |nvim_echo()| can create |Progress| messages
BUILD
@@ -189,6 +190,8 @@ EVENTS
• |CmdlineLeavePre| triggered before preparing to leave the command line.
• New `append` paremeter for |ui-messages| `msg_show` event.
• New `msg_id` and `progress` paremeter for |ui-messages| `msg_show` event.
• Creating or updating a progress message with |nvim_echo()| triggers a |Progress| event.
HIGHLIGHTS

View File

@@ -2545,6 +2545,7 @@ A jump table for the options with a short description can be found at |Q_op|.
|OptionSet|,
|PackChanged|,
|PackChangedPre|,
|Progress|,
|QuickFixCmdPost|,
|QuickFixCmdPre|,
|QuitPre|,

View File

@@ -1100,10 +1100,25 @@ function vim.api.nvim_del_var(name) end
--- the (optional) name or ID `hl_group`.
--- @param history boolean if true, add to `message-history`.
--- @param opts vim.api.keyset.echo_opts Optional parameters.
--- - id: message id for updating existing message.
--- - err: Treat the message like `:echoerr`. Sets `hl_group` to `hl-ErrorMsg` by default.
--- - kind: Set the `ui-messages` kind with which this message will be emitted.
--- - verbose: Message is controlled by the 'verbose' option. Nvim invoked with `-V3log`
--- will write the message to the "log" file instead of standard output.
--- - title: The title for `progress-message`.
--- - status: Current status of the `progress-message`. Can be
--- one of the following values
--- - success: The progress item completed successfully
--- - running: The progress is ongoing
--- - failed: The progress item failed
--- - cancel: The progressing process should be canceled.
--- note: Cancel needs to be handled by progress
--- initiator by listening for the `Progress` event
--- - percent: How much progress is done on the progress
--- message
--- - data: dictionary containing additional information
--- @return integer|string # Message id.
--- - -1 means nvim_echo didn't show a message
function vim.api.nvim_echo(chunks, history, opts) end
--- @deprecated

View File

@@ -165,6 +165,7 @@ error('Cannot require a meta file')
--- |'OptionSet'
--- |'PackChanged'
--- |'PackChangedPre'
--- |'Progress'
--- |'QuickFixCmdPost'
--- |'QuickFixCmdPre'
--- |'QuitPre'
@@ -233,6 +234,11 @@ error('Cannot require a meta file')
--- @field err? boolean
--- @field verbose? boolean
--- @field kind? string
--- @field id? integer|string
--- @field title? string
--- @field status? string
--- @field percent? integer
--- @field data? table<string,any>
--- @class vim.api.keyset.empty

View File

@@ -2224,6 +2224,7 @@ vim.go.ei = vim.go.eventignore
--- `OptionSet`,
--- `PackChanged`,
--- `PackChangedPre`,
--- `Progress`,
--- `QuickFixCmdPost`,
--- `QuickFixCmdPre`,
--- `QuitPre`,

View File

@@ -395,7 +395,8 @@ local function new_progress_report(title)
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 } }
vim.api.nvim_echo(chunks, true, { kind = 'progress' })
-- TODO: need to add support for progress-messages api
api.nvim_echo(chunks, true, {})
-- Force redraw to show installation progress during startup
vim.cmd.redraw({ bang = true })
end)

View File

@@ -336,6 +336,11 @@ typedef struct {
Boolean err;
Boolean verbose;
String kind;
Union(Integer, String) id;
String title;
String status;
Integer percent;
DictOf(Object) data;
} Dict(echo_opts);
typedef struct {

View File

@@ -164,7 +164,8 @@ void wildmenu_select(Integer selected)
void wildmenu_hide(void)
FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY;
void msg_show(String kind, Array content, Boolean replace_last, Boolean history, Boolean append)
void msg_show(String kind, Array content, Boolean replace_last, Boolean history, Boolean append,
Object id, Dict progress)
FUNC_API_SINCE(6) FUNC_API_FAST FUNC_API_REMOTE_ONLY;
void msg_clear(void)
FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY;

View File

@@ -758,14 +758,31 @@ void nvim_set_vvar(String name, Object value, Error *err)
/// the (optional) name or ID `hl_group`.
/// @param history if true, add to |message-history|.
/// @param opts Optional parameters.
/// - id: message id for updating existing message.
/// - err: Treat the message like `:echoerr`. Sets `hl_group` to |hl-ErrorMsg| by default.
/// - kind: Set the |ui-messages| kind with which this message will be emitted.
/// - verbose: Message is controlled by the 'verbose' option. Nvim invoked with `-V3log`
/// will write the message to the "log" file instead of standard output.
void nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Boolean history, Dict(echo_opts) *opts,
/// - title: The title for |progress-message|.
/// - status: Current status of the |progress-message|. Can be
/// one of the following values
/// - success: The progress item completed successfully
/// - running: The progress is ongoing
/// - failed: The progress item failed
/// - cancel: The progressing process should be canceled.
/// note: Cancel needs to be handled by progress
/// initiator by listening for the `Progress` event
/// - percent: How much progress is done on the progress
/// message
/// - data: dictionary containing additional information
/// @return Message id.
/// - -1 means nvim_echo didn't show a message
Union(Integer, String) nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Boolean history,
Dict(echo_opts) *opts,
Error *err)
FUNC_API_SINCE(7)
{
MsgID id = INTEGER_OBJ(-1);
HlMessage hl_msg = parse_hl_msg(chunks, opts->err, err);
if (ERROR_SET(err)) {
goto error;
@@ -778,20 +795,52 @@ void nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Boolean history, Dict(
kind = opts->err ? "echoerr" : history ? "echomsg" : "echo";
}
msg_multihl(hl_msg, kind, history, opts->err);
bool is_progress = strequal(kind, "progress");
VALIDATE(is_progress
|| (opts->status.size == 0 && opts->title.size == 0 && opts->percent == 0
&& opts->data.size == 0),
"%s",
"title, status, percent and data fields can only be used with progress messages",
{
goto error;
});
VALIDATE_EXP((!is_progress || strequal(opts->status.data, "success")
|| strequal(opts->status.data, "failed")
|| strequal(opts->status.data, "running")
|| strequal(opts->status.data, "cancel")),
"status", "success|failed|running|cancel", opts->status.data, {
goto error;
});
VALIDATE_RANGE(!is_progress || (opts->percent >= 0 && opts->percent <= 100),
"percent", {
goto error;
});
MessageData msg_data = { .title = opts->title, .status = opts->status,
.percent = opts->percent, .data = opts->data };
id = msg_multihl(opts->id, hl_msg, kind, history, opts->err, &msg_data);
if (opts->verbose) {
verbose_leave();
verbose_stop(); // flush now
}
if (is_progress) {
do_autocmd_progress(id, hl_msg, &msg_data);
}
if (history) {
// history takes ownership
return;
return id;
}
error:
hl_msg_free(hl_msg);
return id;
}
/// Gets the current list of buffers.

View File

@@ -89,6 +89,7 @@ return {
QuitPre = false, -- before :quit
PackChangedPre = false, -- before trying to change state of `vim.pack` plugin
PackChanged = false, -- after changing state of `vim.pack` plugin
Progress = false, -- after showing/updating a progress message
RecordingEnter = true, -- when starting to record a macro
RecordingLeave = true, -- just before a macro stops recording
RemoteReply = false, -- upon string reception from a remote vim
@@ -162,6 +163,7 @@ return {
LspTokenUpdate = true,
PackChangedPre = true,
PackChanged = true,
Progress = true,
RecordingEnter = true,
RecordingLeave = true,
Signal = true,

View File

@@ -966,7 +966,7 @@ static void nlua_print_event(void **argv)
HlMessage msg = KV_INITIAL_VALUE;
HlMessageChunk chunk = { { .data = argv[0], .size = (size_t)(intptr_t)argv[1] - 1 }, 0 };
kv_push(msg, chunk);
msg_multihl(msg, "lua_print", true, false);
msg_multihl(INTEGER_OBJ(0), msg, "lua_print", true, false, NULL);
}
/// Print as a Vim message

View File

@@ -15,6 +15,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/buffer_defs.h"
#include "nvim/channel.h"
#include "nvim/charset.h"
@@ -50,6 +51,7 @@
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/message.h"
#include "nvim/message_defs.h"
#include "nvim/mouse.h"
#include "nvim/ops.h"
#include "nvim/option.h"
@@ -149,6 +151,8 @@ bool keep_msg_more = false; // keep_msg was set by msgmore()
// Extended msg state, currently used for external UIs with ext_messages
static const char *msg_ext_kind = NULL;
static MsgID msg_ext_id = { .type = kObjectTypeInteger, .data.integer = 0 };
static DictOf(Object) msg_ext_progress = ARRAY_DICT_INIT;
static Array *msg_ext_chunks = NULL;
static garray_T msg_ext_last_chunk = GA_INIT(sizeof(char), 40);
static sattr_T msg_ext_last_attr = -1;
@@ -158,6 +162,8 @@ static bool msg_ext_history = false; ///< message was added to history
static int msg_grid_pos_at_flush = 0;
static int64_t msg_id_next = 1; ///< message id to be allocated to next message
static void ui_ext_msg_set_pos(int row, bool scrolled)
{
char buf[MAX_SCHAR_SIZE];
@@ -293,7 +299,8 @@ static bool is_multihl = false;
/// @param kind Message kind (can be NULL to avoid setting kind)
/// @param history Whether to add message to history
/// @param err Whether to print message as an error
void msg_multihl(HlMessage hl_msg, const char *kind, bool history, bool err)
MsgID msg_multihl(MsgID id, HlMessage hl_msg, const char *kind, bool history, bool err,
MessageData *msg_data)
{
no_wait_return++;
msg_start();
@@ -305,6 +312,17 @@ void msg_multihl(HlMessage hl_msg, const char *kind, bool history, bool err)
}
is_multihl = true;
msg_ext_skip_flush = true;
// provide a new id if not given
if (id.type == kObjectTypeNil) {
id = INTEGER_OBJ(msg_id_next++);
} else if (id.type == kObjectTypeInteger) {
id = id.data.integer > 0 ? id : INTEGER_OBJ(msg_id_next++);
if (msg_id_next < id.data.integer) {
msg_id_next = id.data.integer + 1;
}
}
for (uint32_t i = 0; i < kv_size(hl_msg); i++) {
HlMessageChunk chunk = kv_A(hl_msg, i);
if (err) {
@@ -315,12 +333,14 @@ void msg_multihl(HlMessage hl_msg, const char *kind, bool history, bool err)
assert(!ui_has(kUIMessages) || kind == NULL || msg_ext_kind == kind);
}
if (history && kv_size(hl_msg)) {
msg_hist_add_multihl(hl_msg, false);
msg_hist_add_multihl(id, hl_msg, false, msg_data);
}
msg_ext_skip_flush = false;
is_multihl = false;
no_wait_return--;
msg_end();
return id;
}
/// @param keep set keep_msg if it doesn't scroll
@@ -1018,12 +1038,35 @@ static void msg_hist_add(const char *s, int len, int hl_id)
HlMessage msg = KV_INITIAL_VALUE;
kv_push(msg, ((HlMessageChunk){ text, hl_id }));
msg_hist_add_multihl(msg, false);
msg_hist_add_multihl(INTEGER_OBJ(0), msg, false, NULL);
}
static bool do_clear_hist_temp = true;
static void msg_hist_add_multihl(HlMessage msg, bool temp)
void do_autocmd_progress(MsgID msg_id, HlMessage msg, MessageData *msg_data)
{
MAXSIZE_TEMP_DICT(data, 7);
ArrayOf(String) messages = ARRAY_DICT_INIT;
for (size_t i = 0; i < msg.size; i++) {
ADD(messages, STRING_OBJ(msg.items[i].text));
}
PUT_C(data, "id", OBJECT_OBJ(msg_id));
PUT_C(data, "text", ARRAY_OBJ(messages));
if (msg_data != NULL) {
PUT_C(data, "percent", INTEGER_OBJ(msg_data->percent));
PUT_C(data, "status", STRING_OBJ(msg_data->status));
PUT_C(data, "title", STRING_OBJ(msg_data->title));
PUT_C(data, "data", DICT_OBJ(msg_data->data));
}
apply_autocmds_group(EVENT_PROGRESS, msg_data ? msg_data->title.data : "", NULL, true,
AUGROUP_ALL, NULL,
NULL, &DICT_OBJ(data));
kv_destroy(messages);
}
static void msg_hist_add_multihl(MsgID msg_id, HlMessage msg, bool temp, MessageData *msg_data)
{
if (do_clear_hist_temp) {
msg_hist_clear_temp();
@@ -1061,6 +1104,20 @@ static void msg_hist_add_multihl(HlMessage msg, bool temp)
msg_hist_len += !temp;
msg_hist_last = entry;
msg_ext_history = true;
msg_ext_id = msg_id;
if (strequal(msg_ext_kind, "progress") && msg_data != NULL && ui_has(kUIMessages)) {
kv_resize(msg_ext_progress, 3);
if (msg_data->title.size != 0) {
PUT_C(msg_ext_progress, "title", STRING_OBJ(msg_data->title));
}
if (msg_data->status.size != 0) {
PUT_C(msg_ext_progress, "status", STRING_OBJ(msg_data->status));
}
if (msg_data->percent >= 0) {
PUT_C(msg_ext_progress, "percent", INTEGER_OBJ(msg_data->percent));
}
}
msg_hist_clear(msg_hist_max);
}
@@ -1205,7 +1262,7 @@ void ex_messages(exarg_T *eap)
}
if (redirecting() || !ui_has(kUIMessages)) {
msg_silent += ui_has(kUIMessages);
msg_multihl(p->msg, p->kind, false, false);
msg_multihl(INTEGER_OBJ(0), p->msg, p->kind, false, false, NULL);
msg_silent -= ui_has(kUIMessages);
}
}
@@ -2152,7 +2209,9 @@ void msg_puts_len(const char *const str, const ptrdiff_t len, int hl_id, bool hi
// Don't print anything when using ":silent cmd" or empty message.
if (msg_silent != 0 || *str == NUL) {
if (*str == NUL && ui_has(kUIMessages)) {
ui_call_msg_show(cstr_as_string("empty"), (Array)ARRAY_DICT_INIT, false, false, false);
ui_call_msg_show(cstr_as_string("empty"), (Array)ARRAY_DICT_INIT, false, false, false,
INTEGER_OBJ(-1),
(Dict)ARRAY_DICT_INIT);
}
return;
}
@@ -3178,8 +3237,10 @@ void msg_ext_ui_flush(void)
msg_ext_emit_chunk();
if (msg_ext_chunks->size > 0) {
Array *tofree = msg_ext_init_chunks();
ui_call_msg_show(cstr_as_string(msg_ext_kind), *tofree, msg_ext_overwrite, msg_ext_history,
msg_ext_append);
msg_ext_append, msg_ext_id, msg_ext_progress);
// clear info after emiting message.
if (msg_ext_history) {
api_free_array(*tofree);
} else {
@@ -3191,13 +3252,15 @@ void msg_ext_ui_flush(void)
xfree(chunk);
}
xfree(tofree->items);
msg_hist_add_multihl(msg, true);
msg_hist_add_multihl(INTEGER_OBJ(0), msg, true, NULL);
}
xfree(tofree);
msg_ext_overwrite = false;
msg_ext_history = false;
msg_ext_append = false;
msg_ext_kind = NULL;
msg_ext_id = INTEGER_OBJ(0);
kv_destroy(msg_ext_progress);
}
}

View File

@@ -10,7 +10,14 @@ typedef struct {
} HlMessageChunk;
typedef kvec_t(HlMessageChunk) HlMessage;
#define MsgID Union(Integer, String)
typedef struct msg_data {
Integer percent; ///< Progress percentage
String title; ///< Title for progress message
String status; ///< Status for progress message
DictOf(String, Object) data; ///< Extra info for 'echo' messages
} MessageData;
/// Message history for `:messages`
typedef struct msg_hist {
struct msg_hist *next; ///< Next message.

View File

@@ -3138,3 +3138,385 @@ it('pager works in headless mode with UI attached', function()
-- More --^ |
]])
end)
describe('progress-message', function()
local screen
local function setup_autocmd(pattern)
exec_lua(function()
local grp = vim.api.nvim_create_augroup('ProgressListener', { clear = true })
vim.api.nvim_create_autocmd('Progress', {
pattern = pattern,
group = grp,
callback = function(ev)
_G.progress_autocmd_result = ev.data
end,
})
end)
end
local function assert_progress_autocmd(expected, context)
local progress_autocmd_result = exec_lua(function()
return _G.progress_autocmd_result
end)
eq(expected, progress_autocmd_result, context)
exec_lua(function()
_G.progress_autocmd_result = nil
end)
end
local function setup_screen(with_ext_msg)
if with_ext_msg then
screen = Screen.new(25, 5, { ext_messages = true })
screen:add_extra_attr_ids {
[100] = { undercurl = true, special = Screen.colors.Red },
[101] = { foreground = Screen.colors.Magenta1, bold = true },
}
else
screen = Screen.new(40, 5)
end
end
before_each(function()
clear()
setup_screen(true)
setup_autocmd()
end)
it('can be sent by nvim_echo', function()
local id = api.nvim_echo(
{ { 'test-message' } },
true,
{ kind = 'progress', title = 'testsuit', percent = 10, status = 'running' }
)
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
messages = {
{
content = { { 'test-message' } },
progress = {
percent = 10,
status = 'running',
title = 'testsuit',
},
history = true,
id = 1,
kind = 'progress',
},
},
})
assert_progress_autocmd({
text = { 'test-message' },
percent = 10,
status = 'running',
title = 'testsuit',
id = 1,
data = {},
}, 'progress autocmd receives progress messages')
-- can update progress messages
api.nvim_echo(
{ { 'test-message-updated' } },
true,
{ id = id, kind = 'progress', title = 'TestSuit', percent = 50, status = 'running' }
)
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
messages = {
{
content = { { 'test-message-updated' } },
progress = {
percent = 50,
status = 'running',
title = 'TestSuit',
},
history = true,
id = 1,
kind = 'progress',
},
},
})
assert_progress_autocmd({
text = { 'test-message-updated' },
percent = 50,
status = 'running',
title = 'TestSuit',
id = 1,
data = {},
}, 'Progress autocmd receives progress update')
-- progress event can filter by title
setup_autocmd('Special Title')
api.nvim_echo(
{ { 'test-message-updated' } },
true,
{ id = id, kind = 'progress', percent = 80, status = 'running' }
)
assert_progress_autocmd(nil, 'No progress message with Special Title yet')
api.nvim_echo(
{ { 'test-message-updated' } },
true,
{ id = id, kind = 'progress', title = 'Special Title', percent = 100, status = 'success' }
)
assert_progress_autocmd({
text = { 'test-message-updated' },
percent = 100,
status = 'success',
title = 'Special Title',
id = 1,
data = {},
}, 'Progress autocmd receives progress update')
end)
it('user-defined data in `data` field', function()
api.nvim_echo({ { 'test-message' } }, true, {
kind = 'progress',
title = 'TestSuit',
percent = 10,
status = 'running',
data = { test_attribute = 1 },
})
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
messages = {
{
content = { { 'test-message' } },
history = true,
id = 1,
kind = 'progress',
progress = {
percent = 10,
status = 'running',
title = 'TestSuit',
},
},
},
})
assert_progress_autocmd({
text = { 'test-message' },
percent = 10,
status = 'running',
title = 'TestSuit',
id = 1,
data = { test_attribute = 1 },
}, 'Progress autocmd receives progress messages')
end)
it('validates', function()
-- throws error if title, status, percent, data is used in non progress message
eq(
'title, status, percent and data fields can only be used with progress messages',
t.pcall_err(api.nvim_echo, { { 'test-message' } }, false, { title = 'TestSuit' })
)
eq(
'title, status, percent and data fields can only be used with progress messages',
t.pcall_err(api.nvim_echo, { { 'test-message' } }, false, { status = 'running' })
)
eq(
'title, status, percent and data fields can only be used with progress messages',
t.pcall_err(api.nvim_echo, { { 'test-message' } }, false, { percent = 10 })
)
eq(
'title, status, percent and data fields can only be used with progress messages',
t.pcall_err(api.nvim_echo, { { 'test-message' } }, false, { data = { tag = 'test' } })
)
-- throws error if anything other then running/success/failed/cancel is used in status
eq(
"Invalid 'status': expected success|failed|running|cancel, got live",
t.pcall_err(
api.nvim_echo,
{ { 'test-message' } },
false,
{ kind = 'progress', status = 'live' }
)
)
-- throws error if parcent is not in 0-100
eq(
"Invalid 'percent': out of range",
t.pcall_err(
api.nvim_echo,
{ { 'test-message' } },
false,
{ kind = 'progress', status = 'running', percent = -1 }
)
)
eq(
"Invalid 'percent': out of range",
t.pcall_err(
api.nvim_echo,
{ { 'test-message' } },
false,
{ kind = 'progress', status = 'running', percent = 101 }
)
)
-- throws error if data is not a dictionary
eq(
"Invalid 'data': expected Dict, got String",
t.pcall_err(
api.nvim_echo,
{ { 'test-message' } },
false,
{ kind = 'progress', title = 'TestSuit', percent = 10, status = 'running', data = 'test' }
)
)
end)
it('gets placed in history', function()
local id = api.nvim_echo(
{ { 'test-message 10' } },
true,
{ kind = 'progress', title = 'TestSuit', percent = 10, status = 'running' }
)
eq('test-message 10', exec_capture('messages'))
api.nvim_echo(
{ { 'test-message 20' } },
true,
{ id = id, kind = 'progress', title = 'TestSuit', percent = 20, status = 'running' }
)
eq('test-message 10\ntest-message 20', exec_capture('messages'))
api.nvim_echo({ { 'middle msg' } }, true, {})
eq('test-message 10\ntest-message 20\nmiddle msg', exec_capture('messages'))
api.nvim_echo(
{ { 'test-message 30' } },
true,
{ id = id, kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq('test-message 10\ntest-message 20\nmiddle msg\ntest-message 30', exec_capture('messages'))
api.nvim_echo(
{ { 'test-message 50' } },
true,
{ id = id, kind = 'progress', title = 'TestSuit', percent = 50, status = 'running' }
)
eq(
'test-message 10\ntest-message 20\nmiddle msg\ntest-message 30\ntest-message 50',
exec_capture('messages')
)
end)
it('sets msg-id correctly', function()
local id1 = api.nvim_echo(
{ { 'test-message 10' } },
true,
{ kind = 'progress', title = 'TestSuit', percent = 10, status = 'running' }
)
eq(1, id1)
local id2 = api.nvim_echo(
{ { 'test-message 20' } },
true,
{ kind = 'progress', title = 'TestSuit', percent = 20, status = 'running' }
)
eq(2, id2)
local id3 = api.nvim_echo({ { 'normal message' } }, true, {})
eq(3, id3)
local id4 = api.nvim_echo({ { 'without history' } }, false, {})
eq(4, id4)
local id5 = api.nvim_echo(
{ { 'test-message 30' } },
true,
{ id = 10, kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq(10, id5)
-- updating progress message does not create new msg-id
local id5_update = api.nvim_echo(
{ { 'test-message 40' } },
true,
{ id = id5, kind = 'progress', title = 'TestSuit', percent = 40, status = 'running' }
)
eq(id5, id5_update)
local id6 = api.nvim_echo(
{ { 'test-message 30' } },
true,
{ kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq(11, id6)
local id7 = api.nvim_echo(
{ { 'supports str-id' } },
true,
{ id = 'str-id', kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq('str-id', id7)
local id8 = api.nvim_echo(
{ { 'test-message 30' } },
true,
{ kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq(12, id8)
end)
it('supports string ids', function()
-- string id works
local id = api.nvim_echo(
{ { 'supports str-id' } },
true,
{ id = 'str-id', kind = 'progress', title = 'TestSuit', percent = 30, status = 'running' }
)
eq('str-id', id)
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
messages = {
{
content = { { 'supports str-id' } },
history = true,
id = 'str-id',
kind = 'progress',
progress = {
percent = 30,
status = 'running',
title = 'TestSuit',
},
},
},
})
local id_update = api.nvim_echo(
{ { 'supports str-id updated' } },
true,
{ id = id, kind = 'progress', title = 'testsuit', percent = 40, status = 'running' }
)
eq(id, id_update)
assert_progress_autocmd({
text = { 'supports str-id updated' },
percent = 40,
status = 'running',
title = 'testsuit',
id = 'str-id',
data = {},
})
end)
end)

View File

@@ -1397,12 +1397,19 @@ function Screen:_handle_wildmenu_hide()
self.wildmenu_items, self.wildmenu_pos = nil, nil
end
function Screen:_handle_msg_show(kind, chunks, replace_last, history, append)
function Screen:_handle_msg_show(kind, chunks, replace_last, history, append, id, progress)
local pos = #self.messages
if not replace_last or pos == 0 then
pos = pos + 1
end
self.messages[pos] = { kind = kind, content = chunks, history = history, append = append }
self.messages[pos] = {
kind = kind,
content = chunks,
history = history,
append = append,
id = id,
progress = progress,
}
end
function Screen:_handle_msg_clear()
@@ -1533,6 +1540,8 @@ function Screen:_extstate_repr(attr_state)
content = self:_chunks_repr(entry.content, attr_state),
history = entry.history or nil,
append = entry.append or nil,
id = entry.kind == 'progress' and entry.id or nil,
progress = entry.kind == 'progress' and entry.progress or nil,
}
end