commit 8b171852a92595176308a36978baead585e878d3
parent 1e1619de830ec64ec887911b444753486db63476
Author: Shadman <shadmansaleh3@gmail.com>
Date: Wed, 27 Aug 2025 02:48:53 +0600
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`.
Diffstat:
19 files changed, 648 insertions(+), 19 deletions(-)
diff --git a/runtime/doc/api-ui-events.txt b/runtime/doc/api-ui-events.txt
@@ -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
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
@@ -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.
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
@@ -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
diff --git a/runtime/doc/message.txt b/runtime/doc/message.txt
@@ -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:
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -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
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -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|,
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
@@ -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
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
@@ -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
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -2224,6 +2224,7 @@ vim.go.ei = vim.go.eventignore
--- `OptionSet`,
--- `PackChanged`,
--- `PackChangedPre`,
+--- `Progress`,
--- `QuickFixCmdPost`,
--- `QuickFixCmdPre`,
--- `QuitPre`,
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -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)
diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h
@@ -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 {
diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h
@@ -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;
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
@@ -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,
- Error *err)
+/// - 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.
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
@@ -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,
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
@@ -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
diff --git a/src/nvim/message.c b/src/nvim/message.c
@@ -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);
}
}
diff --git a/src/nvim/message_defs.h b/src/nvim/message_defs.h
@@ -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.
diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua
@@ -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)
diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua
@@ -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