diff --git a/runtime/doc/api-ui-events.txt b/runtime/doc/api-ui-events.txt index 5a70b183be..aef9f8613e 100644 --- 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 index 30c7f3c2c0..b1a9d3bd78 100644 --- 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 index 41a7e72eba..0ed77cd466 100644 --- 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 index d763f23bcb..3fb304c789 100644 --- 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 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 index 13774a8da9..933ba5c2ec 100644 --- 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 index e51bf3f46f..e9cf4defd3 100644 --- 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 index 624b611bda..b7132e5c40 100644 --- 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 index 64a8b29fea..452a68eb50 100644 --- 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 --- @class vim.api.keyset.empty diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index d3c7dedf54..d517d6dca0 100644 --- 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 index 279d8a1d66..29ed78bb87 100644 --- 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 index 0378da6b47..80b567e77c 100644 --- 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 index 010c452044..cba9577664 100644 --- 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 index 5f5a93ec54..7c346d46e5 100644 --- 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 index 6fd62771cf..687f07b744 100644 --- 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 index fd262cba48..5c980e0f2b 100644 --- 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 index 1edc1dbb8b..224aee3e62 100644 --- 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 index 6475c08f48..68b6bf829b 100644 --- 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 index ea93bb52a9..32672e43e7 100644 --- 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 index 355a385b78..844a2d78f6 100644 --- 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