fix(messages): fast context for for nvim_echo({kind}) callback #39755

Problem:  vim.ui_attach() callback for nvim_echo() call that spoofs an
          internal message kind is executed in fast context.

Solution: Set msg_show callback |api-fast| context dynamically at
          external message callsites, and for internal list_cmd",
          "progress" and "shell*" messages.
This commit is contained in:
luukvbaal
2026-05-12 22:28:54 +02:00
committed by GitHub
parent a977e1077b
commit de67f93aea
10 changed files with 41 additions and 23 deletions

View File

@@ -924,6 +924,7 @@ static void write_msg(String message, bool to_err, bool writeln)
static StringBuilder err_line_buf = KV_INITIAL_VALUE;
StringBuilder *line_buf = to_err ? &err_line_buf : &out_line_buf;
msg_ext_no_fast();
#define PUSH_CHAR(c) \
if (kv_max(*line_buf) == 0) { \
kv_resize(*line_buf, LINE_BUFFER_MIN_SIZE); \

View File

@@ -906,6 +906,7 @@ Union(Integer, String) nvim_echo(ArrayOf(Tuple(String, *HLGroupID)) chunks, Bool
msg_didany = true;
msg_no_more = true;
}
msg_ext_no_fast();
id = msg_multihl(opts->id, hl_msg, kind, history, opts->err, &msg_data, &needs_clear);
if (opts->_truncate) {
msg_no_more = false;

View File

@@ -6163,6 +6163,7 @@ void ex_echo(exarg_T *eap)
if (atstart) {
atstart = false;
msg_ext_set_append(eap->cmdidx == CMD_echon);
msg_ext_no_fast();
msg_ext_set_kind("echo");
// Call msg_start() after eval1(), evaluating the expression
// may cause a message to appear.
@@ -6263,11 +6264,13 @@ void ex_execute(exarg_T *eap)
if (ret != FAIL && ga.ga_data != NULL) {
if (eap->cmdidx == CMD_echomsg) {
msg_ext_no_fast();
msg_ext_set_kind("echomsg");
msg(ga.ga_data, echo_hl_id);
} else if (eap->cmdidx == CMD_echoerr) {
// We don't want to abort following commands, restore did_emsg.
int save_did_emsg = did_emsg;
msg_ext_no_fast();
emsg_multiline(ga.ga_data, "echoerr", HLF_E, true);
if (!force_abort) {
did_emsg = save_did_emsg;

View File

@@ -1219,6 +1219,7 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out
if (addr_count == 0) { // :!
// echo the command
msg_start();
msg_ext_no_fast();
msg_ext_set_kind("shell_cmd");
msg_putchar(':');
msg_putchar('!');

View File

@@ -306,6 +306,7 @@ void nlua_error(lua_State *const lstate, const char *const msg)
fprintf(stderr, msg, (int)len, str);
fprintf(stderr, "\n");
} else {
msg_ext_no_fast();
semsg_multiline("lua_error", msg, (int)len, str);
}
@@ -342,6 +343,7 @@ static void nlua_luv_error_event(void **argv)
luv_err_t type = (luv_err_t)(intptr_t)argv[1];
switch (type) {
case kCallback:
msg_ext_no_fast();
semsg_multiline("lua_error", "Lua callback:\n%s", error);
break;
case kThread:
@@ -1049,6 +1051,7 @@ static void nlua_print_event(void **argv)
HlMessageChunk chunk = { { .data = argv[0], .size = (size_t)(intptr_t)argv[1] - 1 }, 0 };
kv_push(msg, chunk);
bool needs_clear = false;
msg_ext_no_fast();
msg_multihl(NIL, msg, "lua_print", true, false, NULL, &needs_clear);
}

View File

@@ -1111,6 +1111,7 @@ char *msg_progress(char *s, char *id, char *status, int hl_id, bool hist, bool t
};
HlMessage chunks = KV_INITIAL_VALUE;
kv_push(chunks, ((HlMessageChunk){ cstr_as_string(s), hl_id }));
msg_ext_no_fast();
msg_multihl(CSTR_AS_OBJ(id), chunks, "progress", false, false, &data, &clear);
kv_destroy(chunks);
ui_flush();
@@ -1705,6 +1706,10 @@ void msg_ext_set_kind(const char *msg_kind)
// the kind but this is called more consistently at the start of a message
// than msg_start() at this point.
redir_col = msg_ext_append ? redir_col : 0;
if (strcmp("list_cmd", msg_kind) == 0) {
msg_ext_no_fast();
}
}
void msg_ext_set_append(bool append)
@@ -1719,6 +1724,13 @@ void msg_ext_set_trigger(const char *trigger)
msg_ext_trigger = trigger;
}
// Should be executed at all callsites emitting non-internal messages.
void msg_ext_no_fast(void)
{
msg_ext_ui_flush();
msg_ext_fast = false;
}
/// Prepare for outputting characters in the command line.
void msg_start(void)
{
@@ -2366,7 +2378,7 @@ 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)) {
msg_ext_ui_flush(); // ensure messages until now are emitted
msg_ext_no_fast();
ui_call_msg_show(cstr_as_string("empty"), (Array)ARRAY_DICT_INIT, false, false, false,
INTEGER_OBJ(-1), (String)STRING_INIT);
cmdline_was_last_drawn = false;
@@ -3421,6 +3433,7 @@ void msg_ext_ui_flush(void)
msg_ext_overwrite = false;
msg_ext_history = false;
msg_ext_append = false;
msg_ext_fast = true;
msg_ext_kind = NULL;
msg_id_next += (msg_ext_id.data.integer == msg_id_next);
msg_ext_id = INTEGER_OBJ(msg_id_next);

View File

@@ -37,6 +37,8 @@ EXTERN bool msg_ext_skip_flush INIT( = false);
EXTERN bool msg_ext_overwrite INIT( = false);
/// Set to true to avoid setting "verbose" kind for "last set" messages.
EXTERN bool msg_ext_skip_verbose INIT( = false);
/// Set to false for non-internal messages to determine UI callback |api-fast| context.
EXTERN bool msg_ext_fast INIT( = true);
/// allocated grid for messages. Used unless ext_messages is active.
/// See also the description at msg_scroll_flush()

View File

@@ -701,6 +701,7 @@ int os_call_shell(char *cmd, int opts, char *extra_args)
if (!emsg_silent && exitcode != 0 && !(opts & kShellOptSilent)) {
msg_ext_set_kind("shell_ret");
msg_ext_no_fast();
if (!ui_has(kUIMessages)) {
msg_putchar('\n');
}
@@ -1126,6 +1127,7 @@ static void out_data_event(void **argv)
int hl = (int)(intptr_t)argv[2] == STDERR_FILENO ? HLF_SE : HLF_SO;
msg_ext_set_kind((int)(intptr_t)argv[2] == STDERR_FILENO ? "shell_err" : "shell_out");
msg_ext_set_append(true);
msg_ext_no_fast();
msg_multiline(cbuf_as_string((char *)argv[0], (size_t)argv[1]), hl, false, false, &need_clear);
xfree(argv[0]);
ui_flush();

View File

@@ -777,28 +777,6 @@ static void ui_attach_error(uint32_t ns_id, const char *name, const char *msg)
void ui_call_event(char *name, Array args)
{
// Internal messages are considered unsafe and are executed in fast context.
bool fast = strcmp(name, "msg_show") == 0;
const char *not_fast[] = {
"empty",
"echo",
"echomsg",
"echoerr",
"list_cmd",
"lua_error",
"lua_print",
"progress",
"shell_cmd",
"shell_err",
"shell_out",
"shell_ret",
NULL,
};
for (int i = 0; fast && not_fast[i]; i++) {
fast = !strequal(not_fast[i], args.items[0].data.string.data);
}
// Don't impose textlock restrictions upon UI event handlers.
int save_expr_map_lock = expr_map_lock;
int save_textlock = textlock;
@@ -806,6 +784,7 @@ void ui_call_event(char *name, Array args)
textlock = 0;
bool handled = false;
bool fast = msg_ext_fast && strcmp("msg_show", name) == 0;
UIEventCallback *event_cb;
map_foreach(&ui_event_cbs, ui_event_ns_id, event_cb, {
Error err = ERROR_INIT;

View File

@@ -691,6 +691,19 @@ describe('messages2', function()
{1:~ }|*12
foo |
]])
feed('<CR>')
-- Fast context is not determined by message kind #39666
exec_lua(function()
vim.schedule(function()
vim.api.nvim_echo({ { 'bar' } }, false, { kind = 'search_cmd' })
vim.fn.getchar()
end)
end)
screen:expect([[
^ |
{1:~ }|*12
bar |
]])
end)
it('properly formatted carriage return messages', function()