From de67f93aeaa24165cd344ae2d8854713d2b78a1c Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Tue, 12 May 2026 22:28:54 +0200 Subject: [PATCH] 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. --- src/nvim/api/deprecated.c | 1 + src/nvim/api/vim.c | 1 + src/nvim/eval.c | 3 +++ src/nvim/ex_cmds.c | 1 + src/nvim/lua/executor.c | 3 +++ src/nvim/message.c | 15 ++++++++++++++- src/nvim/message.h | 2 ++ src/nvim/os/shell.c | 2 ++ src/nvim/ui.c | 23 +---------------------- test/functional/ui/messages2_spec.lua | 13 +++++++++++++ 10 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index e503072e02..be078bf626 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -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); \ diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index b64b7bb496..a425c52f9c 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -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; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 6d9e3b2bbc..fc9d6a5d82 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -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; diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 1d9c6aac02..94322f1720 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -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('!'); diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index fda848e51c..199813bf4a 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -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); } diff --git a/src/nvim/message.c b/src/nvim/message.c index 201f639adf..984abf4b87 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -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); diff --git a/src/nvim/message.h b/src/nvim/message.h index 95d5e24d4d..7c0fab83ab 100644 --- a/src/nvim/message.h +++ b/src/nvim/message.h @@ -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() diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index 557726e4f7..056d1d4961 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -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(); diff --git a/src/nvim/ui.c b/src/nvim/ui.c index a3dd887a4d..f63cb715b0 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -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; diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index ed860051fc..48a77d53df 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -691,6 +691,19 @@ describe('messages2', function() {1:~ }|*12 foo | ]]) + feed('') + -- 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()