feat(api): add err field to nvim_echo() opts

Problem:  We want to deprecate `nvim_err_write(ln)()` but there is no
          obvious replacement (from Lua). Meanwhile we already have
          `nvim_echo()` with an `opts` argument.
Solution: Add `err` argument to `nvim_echo()` that directly maps to
          `:echoerr`.
This commit is contained in:
Luuk van Baal
2025-01-07 14:20:45 +01:00
parent b67fcd0488
commit ead5683ff9
13 changed files with 92 additions and 27 deletions

View File

@ -662,6 +662,8 @@ nvim_echo({chunks}, {history}, {opts}) *nvim_echo()*
`hl_group` element can be omitted for no highlight. `hl_group` element can be omitted for no highlight.
• {history} if true, add to |message-history|. • {history} if true, add to |message-history|.
• {opts} Optional parameters. • {opts} Optional parameters.
• err: Treat the message like |:echoerr|. Omitted `hlgroup`
uses |hl-ErrorMsg| instead.
• verbose: Message is printed as a result of 'verbose' • verbose: Message is printed as a result of 'verbose'
option. If Nvim was invoked with -V3log_file, the message option. If Nvim was invoked with -V3log_file, the message
will be redirected to the log_file and suppressed from will be redirected to the log_file and suppressed from

View File

@ -178,6 +178,8 @@ API
• Improved API "meta" docstrings and :help documentation. • Improved API "meta" docstrings and :help documentation.
• |nvim__ns_set()| can set properties for a namespace • |nvim__ns_set()| can set properties for a namespace
• |nvim_echo()| `err` field to print error messages and `chunks` accepts
highlight group IDs.
DEFAULTS DEFAULTS

View File

@ -1104,6 +1104,8 @@ function vim.api.nvim_del_var(name) end
--- `hl_group` element can be omitted for no highlight. --- `hl_group` element can be omitted for no highlight.
--- @param history boolean if true, add to `message-history`. --- @param history boolean if true, add to `message-history`.
--- @param opts vim.api.keyset.echo_opts Optional parameters. --- @param opts vim.api.keyset.echo_opts Optional parameters.
--- - err: Treat the message like `:echoerr`. Omitted `hlgroup`
--- uses `hl-ErrorMsg` instead.
--- - verbose: Message is printed as a result of 'verbose' option. --- - verbose: Message is printed as a result of 'verbose' option.
--- If Nvim was invoked with -V3log_file, the message will be --- If Nvim was invoked with -V3log_file, the message will be
--- redirected to the log_file and suppressed from direct output. --- redirected to the log_file and suppressed from direct output.

View File

@ -88,6 +88,7 @@ error('Cannot require a meta file')
--- @field pattern? string|string[] --- @field pattern? string|string[]
--- @class vim.api.keyset.echo_opts --- @class vim.api.keyset.echo_opts
--- @field err? boolean
--- @field verbose? boolean --- @field verbose? boolean
--- @class vim.api.keyset.empty --- @class vim.api.keyset.empty

View File

@ -325,6 +325,7 @@ typedef struct {
} Dict(cmd_opts); } Dict(cmd_opts);
typedef struct { typedef struct {
Boolean err;
Boolean verbose; Boolean verbose;
} Dict(echo_opts); } Dict(echo_opts);

View File

@ -776,7 +776,7 @@ char *api_typename(ObjectType t)
UNREACHABLE; UNREACHABLE;
} }
HlMessage parse_hl_msg(Array chunks, Error *err) HlMessage parse_hl_msg(Array chunks, bool is_err, Error *err)
{ {
HlMessage hl_msg = KV_INITIAL_VALUE; HlMessage hl_msg = KV_INITIAL_VALUE;
for (size_t i = 0; i < chunks.size; i++) { for (size_t i = 0; i < chunks.size; i++) {
@ -791,7 +791,7 @@ HlMessage parse_hl_msg(Array chunks, Error *err)
String str = copy_string(chunk.items[0].data.string, NULL); String str = copy_string(chunk.items[0].data.string, NULL);
int hl_id = 0; int hl_id = is_err ? HLF_E : 0;
if (chunk.size == 2) { if (chunk.size == 2) {
hl_id = object_to_hl_id(chunk.items[1], "text highlight", err); hl_id = object_to_hl_id(chunk.items[1], "text highlight", err);
} }

View File

@ -775,13 +775,15 @@ void nvim_set_vvar(String name, Object value, Error *err)
/// `hl_group` element can be omitted for no highlight. /// `hl_group` element can be omitted for no highlight.
/// @param history if true, add to |message-history|. /// @param history if true, add to |message-history|.
/// @param opts Optional parameters. /// @param opts Optional parameters.
/// - err: Treat the message like |:echoerr|. Omitted `hlgroup`
/// uses |hl-ErrorMsg| instead.
/// - verbose: Message is printed as a result of 'verbose' option. /// - verbose: Message is printed as a result of 'verbose' option.
/// If Nvim was invoked with -V3log_file, the message will be /// If Nvim was invoked with -V3log_file, the message will be
/// redirected to the log_file and suppressed from direct output. /// redirected to the log_file and suppressed from direct output.
void nvim_echo(Array chunks, Boolean history, Dict(echo_opts) *opts, Error *err) void nvim_echo(Array chunks, Boolean history, Dict(echo_opts) *opts, Error *err)
FUNC_API_SINCE(7) FUNC_API_SINCE(7)
{ {
HlMessage hl_msg = parse_hl_msg(chunks, err); HlMessage hl_msg = parse_hl_msg(chunks, opts->err, err);
if (ERROR_SET(err)) { if (ERROR_SET(err)) {
goto error; goto error;
} }
@ -790,7 +792,7 @@ void nvim_echo(Array chunks, Boolean history, Dict(echo_opts) *opts, Error *err)
verbose_enter(); verbose_enter();
} }
msg_multihl(hl_msg, history ? "echomsg" : "echo", history); msg_multihl(hl_msg, opts->err ? "echoerr" : history ? "echomsg" : "echo", history, opts->err);
if (opts->verbose) { if (opts->verbose) {
verbose_leave(); verbose_leave();

View File

@ -7968,8 +7968,7 @@ void ex_execute(exarg_T *eap)
} else if (eap->cmdidx == CMD_echoerr) { } else if (eap->cmdidx == CMD_echoerr) {
// We don't want to abort following commands, restore did_emsg. // We don't want to abort following commands, restore did_emsg.
int save_did_emsg = did_emsg; int save_did_emsg = did_emsg;
msg_ext_set_kind("echoerr"); emsg_multiline(ga.ga_data, "echoerr", HLF_E, true);
emsg_multiline(ga.ga_data, true);
if (!force_abort) { if (!force_abort) {
did_emsg = save_did_emsg; did_emsg = save_did_emsg;
} }

View File

@ -982,7 +982,7 @@ void handle_did_throw(void)
if (messages != NULL) { if (messages != NULL) {
do { do {
msglist_T *next = messages->next; msglist_T *next = messages->next;
emsg_multiline(messages->msg, messages->multiline); emsg_multiline(messages->msg, "emsg", HLF_E, messages->multiline);
xfree(messages->msg); xfree(messages->msg);
xfree(messages->sfile); xfree(messages->sfile);
xfree(messages); xfree(messages);

View File

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

View File

@ -293,20 +293,31 @@ void msg_multiline(String str, int hl_id, bool check_int, bool hist, bool *need_
} }
} }
void msg_multihl(HlMessage hl_msg, const char *kind, bool history) // Avoid starting a new message for each chunk and adding message to history in msg_keep().
static bool is_multihl = false;
void msg_multihl(HlMessage hl_msg, const char *kind, bool history, bool err)
{ {
no_wait_return++; no_wait_return++;
msg_start(); msg_start();
msg_clr_eos(); msg_clr_eos();
bool need_clear = false; bool need_clear = false;
msg_ext_history = history;
msg_ext_set_kind(kind); msg_ext_set_kind(kind);
is_multihl = true;
for (uint32_t i = 0; i < kv_size(hl_msg); i++) { for (uint32_t i = 0; i < kv_size(hl_msg); i++) {
HlMessageChunk chunk = kv_A(hl_msg, i); HlMessageChunk chunk = kv_A(hl_msg, i);
msg_multiline(chunk.text, chunk.hl_id, true, false, &need_clear); if (err) {
emsg_multiline(chunk.text.data, kind, chunk.hl_id, true);
} else {
msg_multiline(chunk.text, chunk.hl_id, true, false, &need_clear);
}
assert(msg_ext_kind == kind);
} }
if (history && kv_size(hl_msg)) { if (history && kv_size(hl_msg)) {
add_msg_hist_multihl(NULL, 0, 0, true, hl_msg); add_msg_hist_multihl(NULL, 0, 0, true, hl_msg);
} }
is_multihl = false;
no_wait_return--; no_wait_return--;
msg_end(); msg_end();
} }
@ -342,18 +353,19 @@ bool msg_keep(const char *s, int hl_id, bool keep, bool multiline)
} }
entered++; entered++;
// Add message to history (unless it's a repeated kept message or a // Add message to history (unless it's a truncated, repeated kept or multihl message).
// truncated message) if ((s != keep_msg
if (s != keep_msg || (*s != '<'
|| (*s != '<' && last_msg_hist != NULL
&& last_msg_hist != NULL && last_msg_hist->msg != NULL
&& last_msg_hist->msg != NULL && strcmp(s, last_msg_hist->msg) != 0)) && !is_multihl) {
&& strcmp(s, last_msg_hist->msg) != 0)) {
add_msg_hist(s, -1, hl_id, multiline); add_msg_hist(s, -1, hl_id, multiline);
} }
if (!is_multihl) {
msg_start();
}
// Truncate the message if needed. // Truncate the message if needed.
msg_start();
char *buf = msg_strtrunc(s, false); char *buf = msg_strtrunc(s, false);
if (buf != NULL) { if (buf != NULL) {
s = buf; s = buf;
@ -368,7 +380,10 @@ bool msg_keep(const char *s, int hl_id, bool keep, bool multiline)
if (need_clear) { if (need_clear) {
msg_clr_eos(); msg_clr_eos();
} }
bool retval = msg_end(); bool retval = true;
if (!is_multihl) {
retval = msg_end();
}
if (keep && retval && vim_strsize(s) < (Rows - cmdline_row - 1) * Columns + sc_col) { if (keep && retval && vim_strsize(s) < (Rows - cmdline_row - 1) * Columns + sc_col) {
set_keep_msg(s, 0); set_keep_msg(s, 0);
@ -618,6 +633,9 @@ void msg_source(int hl_id)
msg_scroll = true; // this will take more than one line msg_scroll = true; // this will take more than one line
msg(p, hl_id); msg(p, hl_id);
xfree(p); xfree(p);
if (is_multihl) {
msg_start(); // avoided in msg_keep() but need the "msg_didout" newline here
}
} }
p = get_emsg_lnum(); p = get_emsg_lnum();
if (p != NULL) { if (p != NULL) {
@ -652,7 +670,7 @@ int emsg_not_now(void)
return false; return false;
} }
bool emsg_multiline(const char *s, bool multiline) bool emsg_multiline(const char *s, const char *kind, int hl_id, bool multiline)
{ {
bool ignore = false; bool ignore = false;
@ -750,14 +768,13 @@ bool emsg_multiline(const char *s, bool multiline)
} }
emsg_on_display = true; // remember there is an error message emsg_on_display = true; // remember there is an error message
int hl_id = HLF_E; // set highlight mode for error messages
if (msg_scrolled != 0) { if (msg_scrolled != 0) {
need_wait_return = true; // needed in case emsg() is called after need_wait_return = true; // needed in case emsg() is called after
} // wait_return() has reset need_wait_return } // wait_return() has reset need_wait_return
// and a redraw is expected because // and a redraw is expected because
// msg_scrolled is non-zero // msg_scrolled is non-zero
if (msg_ext_kind == NULL) { if (msg_ext_kind == NULL) {
msg_ext_set_kind("emsg"); msg_ext_set_kind(kind);
} }
// Display name and line number for the source of the error. // Display name and line number for the source of the error.
@ -765,7 +782,7 @@ bool emsg_multiline(const char *s, bool multiline)
msg_source(hl_id); msg_source(hl_id);
if (msg_ext_kind == NULL) { if (msg_ext_kind == NULL) {
msg_ext_set_kind("emsg"); msg_ext_set_kind(kind);
} }
// Display the error message itself. // Display the error message itself.
@ -781,7 +798,7 @@ bool emsg_multiline(const char *s, bool multiline)
/// @return true if wait_return() not called /// @return true if wait_return() not called
bool emsg(const char *s) bool emsg(const char *s)
{ {
return emsg_multiline(s, false); return emsg_multiline(s, "emsg", HLF_E, false);
} }
void emsg_invreg(int name) void emsg_invreg(int name)
@ -821,7 +838,7 @@ bool semsg_multiline(const char *const fmt, ...)
vim_vsnprintf(errbuf, sizeof(errbuf), fmt, ap); vim_vsnprintf(errbuf, sizeof(errbuf), fmt, ap);
va_end(ap); va_end(ap);
ret = emsg_multiline(errbuf, true); ret = emsg_multiline(errbuf, "emsg", HLF_E, true);
return ret; return ret;
} }
@ -905,7 +922,7 @@ void msg_schedule_semsg(const char *const fmt, ...)
static void msg_semsg_multiline_event(void **argv) static void msg_semsg_multiline_event(void **argv)
{ {
char *s = argv[0]; char *s = argv[0];
emsg_multiline(s, true); emsg_multiline(s, "emsg", HLF_E, true);
xfree(s); xfree(s);
} }
@ -1196,7 +1213,7 @@ void ex_messages(exarg_T *eap)
msg_hist_off = true; msg_hist_off = true;
for (; p != NULL && !got_int; p = p->next) { for (; p != NULL && !got_int; p = p->next) {
if (kv_size(p->multihl)) { if (kv_size(p->multihl)) {
msg_multihl(p->multihl, p->kind, false); msg_multihl(p->multihl, p->kind, false, false);
} else if (p->msg != NULL) { } else if (p->msg != NULL) {
msg_keep(p->msg, p->hl_id, false, p->multiline); msg_keep(p->msg, p->hl_id, false, p->multiline);
} }

View File

@ -3680,6 +3680,30 @@ describe('API', function()
async_meths.nvim_echo({ { 'msg\nmsg' }, { 'msg' } }, false, {}) async_meths.nvim_echo({ { 'msg\nmsg' }, { 'msg' } }, false, {})
eq('', exec_capture('messages')) eq('', exec_capture('messages'))
end) end)
it('can print error message', function()
async_meths.nvim_echo({ { 'Error\nMessage' } }, false, { err = true })
screen:expect([[
|
{1:~ }|*3
{3: }|
{9:Error} |
{9:Message} |
{6:Press ENTER or type command to continue}^ |
]])
feed(':messages<CR>')
screen:expect([[
^ |
{1:~ }|*6
|
]])
async_meths.nvim_echo({ { 'Error' }, { 'Message', 'Special' } }, false, { err = true })
screen:expect([[
^ |
{1:~ }|*6
{9:Error}{16:Message} |
]])
end)
end) end)
describe('nvim_open_term', function() describe('nvim_open_term', function()

View File

@ -315,6 +315,21 @@ describe('ui/ext_messages', function()
}) })
feed('<Esc>') feed('<Esc>')
command('set showmode') command('set showmode')
-- kind=echoerr for nvim_echo() err
feed(':call nvim_echo([["Error"], ["Message", "Special"]], 1, #{ err:1 })<CR>')
screen:expect({
cmdline = { {
abort = false,
} },
messages = {
{
content = { { 'Error', 9, 6 }, { 'Message', 16, 99 } },
history = true,
kind = 'echoerr',
},
},
})
end) end)
it(':echoerr', function() it(':echoerr', function()