Files
neovim/src/nvim/ui.c
luukvbaal 4369d7d9a7 fix(ui)!: decouple ext_messages from message grid #27963
Problem:  ext_messages is implemented to mimic the message grid
          implementation w.r.t. scrolling messages, clearing scrolled
          messages, hit-enter-prompts and replacing a previous message.
          Meanwhile, an ext_messages UI may not be implemented in a way
          where these events are wanted. Moreover, correctness of these
          events even assuming a "scrolled message" implementation
          depends on fragile "currently visible messages" global state,
          which already isn't correct after a previous message was
          supposed to have been overwritten (because that should not only
          happen when `msg_scroll == false`).

Solution: - No longer attempt to keep track of the currently visible
            messages: remove the `msg_ext(_history)_visible` variables.
            UIs may remove messages pre-emptively (timer based), or never
            show messages that don't fit a certain area in the first place.
          - No longer emit the `msg(_history)_clear` events to clear
            "scrolled" messages. This opens up the `msg_clear` event to
            be emitted when messages should actually be cleared (e.g.
            when the screen is cleared). May also be useful to emit before
            the first message in an event loop cycle as a hint to the UI
            that it is a new batch of messages (vim._extui currently
            schedules an event to determine that).
          - Set `replace_last` explicitly at the few callsites that want
            this to be set to true to replace an incomplete status message.
          - Don't store a "keep" message to be re-emitted.
2025-06-25 08:25:40 -07:00

828 lines
20 KiB
C

#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#include "nvim/api/extmark.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/validate.h"
#include "nvim/api/ui.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/cursor_shape.h"
#include "nvim/drawscreen.h"
#include "nvim/event/multiqueue.h"
#include "nvim/ex_getln.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/log.h"
#include "nvim/lua/executor.h"
#include "nvim/map_defs.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/os/os_defs.h"
#include "nvim/os/time.h"
#include "nvim/state_defs.h"
#include "nvim/strings.h"
#include "nvim/ui.h"
#include "nvim/ui_client.h"
#include "nvim/ui_compositor.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"
typedef struct {
LuaRef cb;
uint8_t errors;
bool ext_widgets[kUIGlobalCount];
} UIEventCallback;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ui.c.generated.h"
#endif
#define MAX_UI_COUNT 16
static RemoteUI *uis[MAX_UI_COUNT];
static bool ui_ext[kUIExtCount] = { 0 };
static size_t ui_count = 0;
static int ui_mode_idx = SHAPE_IDX_N;
static int cursor_row = 0, cursor_col = 0;
static bool pending_cursor_update = false;
static int busy = 0;
static bool pending_mode_info_update = false;
static bool pending_mode_update = false;
static handle_T cursor_grid_handle = DEFAULT_GRID_HANDLE;
static PMap(uint32_t) ui_event_cbs = MAP_INIT;
bool ui_cb_ext[kUIExtCount]; ///< Internalized UI capabilities.
static bool has_mouse = false;
static int pending_has_mouse = -1;
static bool pending_default_colors = false;
#ifdef NVIM_LOG_DEBUG
static size_t uilog_seen = 0;
static const char *uilog_last_event = NULL;
static void ui_log(const char *funname)
{
# ifdef EXITFREE
if (entered_free_all_mem) {
return; // do nothing, we cannot log now
}
# endif
if (uilog_last_event == funname) {
uilog_seen++;
} else {
if (uilog_seen > 0) {
logmsg(LOGLVL_DBG, "UI: ", NULL, -1, true,
"%s (+%zu times...)", uilog_last_event, uilog_seen);
}
logmsg(LOGLVL_DBG, "UI: ", NULL, -1, true, "%s", funname);
uilog_seen = 0;
uilog_last_event = funname;
}
}
#else
# define ui_log(funname)
#endif
// UI_CALL invokes a function on all registered UI instances.
// This is called by code generated by generators/gen_api_ui_events.lua
// C code should use ui_call_{funname} instead.
#define UI_CALL(cond, funname, ...) \
do { \
bool any_call = false; \
for (size_t i = 0; i < ui_count; i++) { \
RemoteUI *ui = uis[i]; \
if ((cond)) { \
remote_ui_##funname(__VA_ARGS__); \
any_call = true; \
} \
} \
if (any_call) { \
ui_log(STR(funname)); \
} \
} while (0)
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ui_events_call.generated.h"
#endif
void ui_init(void)
{
default_grid.handle = 1;
msg_grid_adj.target = &default_grid;
ui_comp_init();
}
#ifdef EXITFREE
void ui_free_all_mem(void)
{
UIEventCallback *event_cb;
map_foreach_value(&ui_event_cbs, event_cb, {
free_ui_event_callback(event_cb);
})
map_destroy(uint32_t, &ui_event_cbs);
multiqueue_free(resize_events);
}
#endif
/// Returns true if any `rgb=true` UI is attached.
bool ui_rgb_attached(void)
{
if (p_tgc) {
return true;
}
for (size_t i = 0; i < ui_count; i++) {
// We do not consider the TUI in this loop because we already checked for 'termguicolors' at the
// beginning of this function. In this loop, we are checking to see if any _other_ UIs which
// support RGB are attached.
bool tui = uis[i]->stdin_tty || uis[i]->stdout_tty;
if (!tui && uis[i]->rgb) {
return true;
}
}
return false;
}
/// Returns true if a GUI is attached.
bool ui_gui_attached(void)
{
for (size_t i = 0; i < ui_count; i++) {
bool tui = uis[i]->stdin_tty || uis[i]->stdout_tty;
if (!tui) {
return true;
}
}
return false;
}
/// Returns true if any UI requested `override=true`.
bool ui_override(void)
{
for (size_t i = 0; i < ui_count; i++) {
if (uis[i]->override) {
return true;
}
}
return false;
}
/// Gets the number of UIs connected to this server.
size_t ui_active(void)
{
return ui_count;
}
void ui_refresh(void)
{
if (ui_client_channel_id) {
abort();
}
int width = INT_MAX;
int height = INT_MAX;
bool ext_widgets[kUIExtCount];
bool inclusive = ui_override();
memset(ext_widgets, !!ui_active(), ARRAY_SIZE(ext_widgets));
for (size_t i = 0; i < ui_count; i++) {
RemoteUI *ui = uis[i];
width = MIN(ui->width, width);
height = MIN(ui->height, height);
for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
}
}
cursor_row = cursor_col = 0;
pending_cursor_update = true;
bool had_message = ui_ext[kUIMessages];
for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
ui_ext[i] = ext_widgets[i] | ui_cb_ext[i];
if (i < kUIGlobalCount) {
ui_call_option_set(cstr_as_string(ui_ext_names[i]), BOOLEAN_OBJ(ui_ext[i]));
}
}
// Reset 'cmdheight' for all tabpages when ext_messages toggles.
if (had_message != ui_ext[kUIMessages]) {
if (ui_refresh_cmdheight) {
set_option_value(kOptCmdheight, NUMBER_OPTVAL(had_message), 0);
FOR_ALL_TABS(tp) {
tp->tp_ch_used = had_message;
}
}
msg_scroll_flush();
}
msg_ui_refresh();
if (!ui_active()) {
return;
}
if (updating_screen) {
ui_schedule_refresh();
return;
}
ui_default_colors_set();
int save_p_lz = p_lz;
p_lz = false; // convince redrawing() to return true ...
screen_resize(width, height);
p_lz = save_p_lz;
ui_mode_info_set();
pending_mode_update = true;
ui_cursor_shape();
pending_has_mouse = -1;
}
int ui_pum_get_height(void)
{
int pum_height = 0;
for (size_t i = 0; i < ui_count; i++) {
int ui_pum_height = uis[i]->pum_nlines;
if (ui_pum_height) {
pum_height =
pum_height != 0 ? MIN(pum_height, ui_pum_height) : ui_pum_height;
}
}
return pum_height;
}
bool ui_pum_get_pos(double *pwidth, double *pheight, double *prow, double *pcol)
{
for (size_t i = 0; i < ui_count; i++) {
if (!uis[i]->pum_pos) {
continue;
}
*pwidth = uis[i]->pum_width;
*pheight = uis[i]->pum_height;
*prow = uis[i]->pum_row;
*pcol = uis[i]->pum_col;
return true;
}
return false;
}
static void ui_refresh_event(void **argv)
{
ui_refresh();
}
void ui_schedule_refresh(void)
{
multiqueue_put(resize_events, ui_refresh_event, NULL);
}
void ui_default_colors_set(void)
{
// Throttle setting of default colors at startup, so it only happens once
// if the user sets the colorscheme in startup.
pending_default_colors = true;
if (starting == 0) {
ui_may_set_default_colors();
}
}
static void ui_may_set_default_colors(void)
{
if (pending_default_colors) {
pending_default_colors = false;
ui_call_default_colors_set(normal_fg, normal_bg, normal_sp,
cterm_normal_fg_color, cterm_normal_bg_color);
}
}
void ui_busy_start(void)
{
if (!(busy++)) {
ui_call_busy_start();
}
}
void ui_busy_stop(void)
{
if (!(--busy)) {
ui_call_busy_stop();
}
}
/// Emit a bell or visualbell as a warning
///
/// val is one of the OptBoFlags values, e.g., kOptBoFlagOperator
void vim_beep(unsigned val)
{
called_vim_beep = true;
if (emsg_silent != 0 || in_assert_fails) {
return;
}
if (!((bo_flags & val) || (bo_flags & kOptBoFlagAll))) {
static int beeps = 0;
static uint64_t start_time = 0;
// Only beep up to three times per half a second,
// otherwise a sequence of beeps would freeze Vim.
if (start_time == 0 || os_hrtime() - start_time > 500000000U) {
beeps = 0;
start_time = os_hrtime();
}
beeps++;
if (beeps <= 3) {
if (p_vb) {
ui_call_visual_bell();
} else {
ui_call_bell();
}
}
}
// When 'debug' contains "beep" produce a message. If we are sourcing
// a script or executing a function give the user a hint where the beep
// comes from.
if (vim_strchr(p_debug, 'e') != NULL) {
msg_source(HLF_W);
msg(_("Beep!"), HLF_W);
}
}
/// Trigger UIEnter for all attached UIs.
/// Used on startup after VimEnter.
void do_autocmd_uienter_all(void)
{
for (size_t i = 0; i < ui_count; i++) {
do_autocmd_uienter(uis[i]->channel_id, true);
}
}
void ui_attach_impl(RemoteUI *ui, uint64_t chanid)
{
if (ui_count == MAX_UI_COUNT) {
abort();
}
if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]
&& !ui_client_channel_id) {
ui_comp_attach(ui);
}
uis[ui_count++] = ui;
ui_refresh_options();
resettitle();
char cwd[MAXPATHL];
size_t cwdlen = sizeof(cwd);
if (uv_cwd(cwd, &cwdlen) == 0) {
ui_call_chdir((String){ .data = cwd, .size = cwdlen });
}
for (UIExtension i = kUIGlobalCount; (int)i < kUIExtCount; i++) {
ui_set_ext_option(ui, i, ui->ui_ext[i]);
}
bool sent = false;
if (ui->ui_ext[kUIHlState]) {
sent = highlight_use_hlstate();
}
if (!sent) {
ui_send_all_hls(ui);
}
ui_refresh();
do_autocmd_uienter(chanid, true);
}
void ui_detach_impl(RemoteUI *ui, uint64_t chanid)
{
size_t shift_index = MAX_UI_COUNT;
// Find the index that will be removed
for (size_t i = 0; i < ui_count; i++) {
if (uis[i] == ui) {
shift_index = i;
break;
}
}
if (shift_index == MAX_UI_COUNT) {
abort();
}
// Shift UIs at "shift_index"
while (shift_index < ui_count - 1) {
uis[shift_index] = uis[shift_index + 1];
shift_index++;
}
if (--ui_count
// During teardown/exit the loop was already destroyed, cannot schedule.
// https://github.com/neovim/neovim/pull/5119#issuecomment-258667046
&& !exiting) {
ui_schedule_refresh();
}
if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]) {
ui_comp_detach(ui);
}
do_autocmd_uienter(chanid, false);
}
void ui_set_ext_option(RemoteUI *ui, UIExtension ext, bool active)
{
if (ext < kUIGlobalCount) {
ui_refresh();
return;
}
if (ui_ext_names[ext][0] != '_' || active) {
remote_ui_option_set(ui, cstr_as_string(ui_ext_names[ext]), BOOLEAN_OBJ(active));
}
if (ext == kUITermColors) {
ui_default_colors_set();
}
}
void ui_line(ScreenGrid *grid, int row, bool invalid_row, int startcol, int endcol, int clearcol,
int clearattr, bool wrap)
{
assert(0 <= row && row < grid->rows);
LineFlags flags = wrap ? kLineFlagWrap : 0;
if (startcol == 0 && invalid_row) {
flags |= kLineFlagInvalid;
}
// set default colors now so that that text won't have to be repainted later
ui_may_set_default_colors();
size_t off = grid->line_offset[row] + (size_t)startcol;
ui_call_raw_line(grid->handle, row, startcol, endcol, clearcol, clearattr,
flags, (const schar_T *)grid->chars + off,
(const sattr_T *)grid->attrs + off);
// 'writedelay': flush & delay each time.
if (p_wd && (rdb_flags & kOptRdbFlagLine)) {
// If 'writedelay' is active, set the cursor to indicate what was drawn.
ui_call_grid_cursor_goto(grid->handle, row,
MIN(clearcol, (int)grid->cols - 1));
ui_call_flush();
uint64_t wd = (uint64_t)llabs(p_wd);
os_sleep(wd);
pending_cursor_update = true; // restore the cursor later
}
}
void ui_cursor_goto(int new_row, int new_col)
{
ui_grid_cursor_goto(DEFAULT_GRID_HANDLE, new_row, new_col);
}
void ui_grid_cursor_goto(handle_T grid_handle, int new_row, int new_col)
{
if (new_row == cursor_row
&& new_col == cursor_col
&& grid_handle == cursor_grid_handle) {
return;
}
cursor_row = new_row;
cursor_col = new_col;
cursor_grid_handle = grid_handle;
pending_cursor_update = true;
}
/// moving the cursor grid will implicitly move the cursor
void ui_check_cursor_grid(handle_T grid_handle)
{
if (cursor_grid_handle == grid_handle) {
pending_cursor_update = true;
}
}
void ui_mode_info_set(void)
{
pending_mode_info_update = true;
}
int ui_current_row(void)
{
return cursor_row;
}
int ui_current_col(void)
{
return cursor_col;
}
void ui_flush(void)
{
assert(!ui_client_channel_id);
if (!ui_active()) {
return;
}
static bool was_busy = false;
cmdline_ui_flush();
if (State != MODE_CMDLINE && curwin->w_floating && curwin->w_config.hide) {
if (!was_busy) {
ui_call_busy_start();
was_busy = true;
}
} else if (was_busy) {
ui_call_busy_stop();
was_busy = false;
}
win_ui_flush(false);
msg_ext_ui_flush();
msg_scroll_flush();
if (pending_cursor_update) {
ui_call_grid_cursor_goto(cursor_grid_handle, cursor_row, cursor_col);
pending_cursor_update = false;
// The cursor move might change the composition order,
// so flush again to update the windows that changed
// TODO(bfredl): refactor the flow of information so that win_ui_flush()
// only is called once. (as order state is exposed, it should be owned
// by nvim core, not the compositor)
win_ui_flush(false);
}
if (pending_mode_info_update) {
Arena arena = ARENA_EMPTY;
Array style = mode_style_array(&arena);
bool enabled = (*p_guicursor != NUL);
ui_call_mode_info_set(enabled, style);
arena_mem_free(arena_finish(&arena));
pending_mode_info_update = false;
}
if (pending_mode_update && !starting) {
char *full_name = shape_table[ui_mode_idx].full_name;
ui_call_mode_change(cstr_as_string(full_name), ui_mode_idx);
pending_mode_update = false;
}
if (pending_has_mouse != has_mouse) {
(has_mouse ? ui_call_mouse_on : ui_call_mouse_off)();
pending_has_mouse = has_mouse;
}
ui_call_flush();
if (p_wd && (rdb_flags & kOptRdbFlagFlush)) {
os_sleep((uint64_t)llabs(p_wd));
}
}
/// Check if 'mouse' is active for the current mode
///
/// TODO(bfredl): precompute the State -> active mapping when 'mouse' changes,
/// then this can be checked directly in ui_flush()
void ui_check_mouse(void)
{
has_mouse = false;
// Be quick when mouse is off.
if (*p_mouse == NUL) {
return;
}
int checkfor = MOUSE_NORMAL; // assume normal mode
if (VIsual_active) {
checkfor = MOUSE_VISUAL;
} else if (State == MODE_HITRETURN || State == MODE_ASKMORE || State == MODE_SETWSIZE) {
checkfor = MOUSE_RETURN;
} else if (State & MODE_INSERT) {
checkfor = MOUSE_INSERT;
} else if (State & MODE_CMDLINE) {
checkfor = MOUSE_COMMAND;
} else if (State == MODE_EXTERNCMD) {
checkfor = ' '; // don't use mouse for ":!cmd"
}
// mouse should be active if at least one of the following is true:
// - "c" is in 'mouse', or
// - 'a' is in 'mouse' and "c" is in MOUSE_A, or
// - the current buffer is a help file and 'h' is in 'mouse' and we are in a
// normal editing mode (not at hit-return message).
for (char *p = p_mouse; *p; p++) {
switch (*p) {
case 'a':
if (vim_strchr(MOUSE_A, checkfor) != NULL) {
has_mouse = true;
return;
}
break;
case MOUSE_HELP:
if (checkfor != MOUSE_RETURN && curbuf->b_help) {
has_mouse = true;
return;
}
break;
default:
if (checkfor == *p) {
has_mouse = true;
return;
}
}
}
}
/// Check if current mode has changed.
///
/// May update the shape of the cursor.
void ui_cursor_shape_no_check_conceal(void)
{
if (!full_screen) {
return;
}
int new_mode_idx = cursor_get_mode_idx();
if (new_mode_idx != ui_mode_idx) {
ui_mode_idx = new_mode_idx;
pending_mode_update = true;
}
}
/// Check if current mode has changed.
///
/// May update the shape of the cursor.
/// With concealing on, may conceal or unconceal the cursor line.
void ui_cursor_shape(void)
{
ui_cursor_shape_no_check_conceal();
conceal_check_cursor_line();
}
/// Returns true if the given UI extension is enabled.
bool ui_has(UIExtension ext)
{
return ui_ext[ext];
}
Array ui_array(Arena *arena)
{
Array all_uis = arena_array(arena, ui_count);
for (size_t i = 0; i < ui_count; i++) {
RemoteUI *ui = uis[i];
Dict info = arena_dict(arena, 10 + kUIExtCount);
PUT_C(info, "width", INTEGER_OBJ(ui->width));
PUT_C(info, "height", INTEGER_OBJ(ui->height));
PUT_C(info, "rgb", BOOLEAN_OBJ(ui->rgb));
PUT_C(info, "override", BOOLEAN_OBJ(ui->override));
// TUI fields. (`stdin_fd` is intentionally omitted.)
PUT_C(info, "term_name", CSTR_AS_OBJ(ui->term_name));
// term_background is deprecated. Populate with an empty string
PUT_C(info, "term_background", STATIC_CSTR_AS_OBJ(""));
PUT_C(info, "term_colors", INTEGER_OBJ(ui->term_colors));
PUT_C(info, "stdin_tty", BOOLEAN_OBJ(ui->stdin_tty));
PUT_C(info, "stdout_tty", BOOLEAN_OBJ(ui->stdout_tty));
for (UIExtension j = 0; j < kUIExtCount; j++) {
if (ui_ext_names[j][0] != '_' || ui->ui_ext[j]) {
PUT_C(info, (char *)ui_ext_names[j], BOOLEAN_OBJ(ui->ui_ext[j]));
}
}
PUT_C(info, "chan", INTEGER_OBJ((Integer)ui->channel_id));
ADD_C(all_uis, DICT_OBJ(info));
}
return all_uis;
}
void ui_grid_resize(handle_T grid_handle, int width, int height, Error *err)
{
if (grid_handle == DEFAULT_GRID_HANDLE) {
screen_resize(width, height);
return;
}
win_T *wp = get_win_by_grid_handle(grid_handle);
VALIDATE_INT((wp != NULL), "window handle", (int64_t)grid_handle, {
return;
});
if (wp->w_floating) {
if (width != wp->w_width || height != wp->w_height) {
wp->w_config.width = width;
wp->w_config.height = height;
win_config_float(wp, wp->w_config);
}
} else {
// non-positive indicates no request
wp->w_height_request = MAX(height, 0);
wp->w_width_request = MAX(width, 0);
win_set_inner_size(wp, true);
}
}
static void ui_attach_error(uint32_t ns_id, const char *name, const char *msg)
{
const char *ns = describe_ns((NS)ns_id, "(UNKNOWN PLUGIN)");
ELOG("Error in \"%s\" UI event handler (ns=%s):\n%s", name, ns, msg);
msg_schedule_semsg_multiline("Error in \"%s\" UI event handler (ns=%s):\n%s", name, ns, msg);
}
void ui_call_event(char *name, bool fast, Array args)
{
bool handled = false;
UIEventCallback *event_cb;
map_foreach(&ui_event_cbs, ui_event_ns_id, event_cb, {
Error err = ERROR_INIT;
uint32_t ns_id = ui_event_ns_id;
Object res = nlua_call_ref_ctx(fast, event_cb->cb, name, args, kRetNilBool, NULL, &err);
ui_event_ns_id = 0;
if (LUARET_TRUTHY(res)) {
handled = true;
}
if (ERROR_SET(&err)) {
ui_attach_error(ns_id, name, err.msg);
ui_remove_cb(ns_id, true);
}
api_clear_error(&err);
})
if (!handled) {
UI_CALL(true, event, ui, name, args);
}
ui_log(name);
}
static void ui_cb_update_ext(void)
{
memset(ui_cb_ext, 0, ARRAY_SIZE(ui_cb_ext));
for (size_t i = 0; i < kUIGlobalCount; i++) {
UIEventCallback *event_cb;
map_foreach_value(&ui_event_cbs, event_cb, {
if (event_cb->ext_widgets[i]) {
ui_cb_ext[i] = true;
break;
}
})
}
}
static void free_ui_event_callback(UIEventCallback *event_cb)
{
api_free_luaref(event_cb->cb);
xfree(event_cb);
}
void ui_add_cb(uint32_t ns_id, LuaRef cb, bool *ext_widgets)
{
UIEventCallback *event_cb = xcalloc(1, sizeof(UIEventCallback));
event_cb->cb = cb;
memcpy(event_cb->ext_widgets, ext_widgets, ARRAY_SIZE(event_cb->ext_widgets));
if (event_cb->ext_widgets[kUIMessages]) {
event_cb->ext_widgets[kUICmdline] = true;
}
ptr_t *item = pmap_put_ref(uint32_t)(&ui_event_cbs, ns_id, NULL, NULL);
if (*item) {
free_ui_event_callback((UIEventCallback *)(*item));
}
*item = event_cb;
ui_cb_update_ext();
ui_refresh();
}
void ui_remove_cb(uint32_t ns_id, bool checkerr)
{
UIEventCallback *item = pmap_get(uint32_t)(&ui_event_cbs, ns_id);
if (item && (!checkerr || ++item->errors > CB_MAX_ERROR)) {
pmap_del(uint32_t)(&ui_event_cbs, ns_id, NULL);
free_ui_event_callback(item);
ui_cb_update_ext();
ui_refresh();
if (checkerr) {
const char *ns = describe_ns((NS)ns_id, "(UNKNOWN PLUGIN)");
msg_schedule_semsg("Excessive errors in vim.ui_attach() callback (ns=%s)", ns);
}
}
}