neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

ui.c (22433B)


      1 #include <assert.h>
      2 #include <limits.h>
      3 #include <stdbool.h>
      4 #include <stdint.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 #include <uv.h>
      8 
      9 #include "nvim/api/extmark.h"
     10 #include "nvim/api/private/helpers.h"
     11 #include "nvim/api/private/validate.h"
     12 #include "nvim/api/ui.h"
     13 #include "nvim/ascii_defs.h"
     14 #include "nvim/autocmd.h"
     15 #include "nvim/buffer.h"
     16 #include "nvim/buffer_defs.h"
     17 #include "nvim/cursor_shape.h"
     18 #include "nvim/drawscreen.h"
     19 #include "nvim/event/multiqueue.h"
     20 #include "nvim/ex_getln.h"
     21 #include "nvim/gettext_defs.h"
     22 #include "nvim/globals.h"
     23 #include "nvim/grid.h"
     24 #include "nvim/highlight.h"
     25 #include "nvim/highlight_defs.h"
     26 #include "nvim/log.h"
     27 #include "nvim/lua/executor.h"
     28 #include "nvim/map_defs.h"
     29 #include "nvim/memory.h"
     30 #include "nvim/memory_defs.h"
     31 #include "nvim/message.h"
     32 #include "nvim/option.h"
     33 #include "nvim/option_defs.h"
     34 #include "nvim/option_vars.h"
     35 #include "nvim/os/os_defs.h"
     36 #include "nvim/os/time.h"
     37 #include "nvim/state_defs.h"
     38 #include "nvim/strings.h"
     39 #include "nvim/ui.h"
     40 #include "nvim/ui_client.h"
     41 #include "nvim/ui_compositor.h"
     42 #include "nvim/window.h"
     43 #include "nvim/winfloat.h"
     44 
     45 typedef struct {
     46  LuaRef cb;
     47  uint8_t errors;
     48  bool ext_widgets[kUIGlobalCount];
     49 } UIEventCallback;
     50 
     51 #include "ui.c.generated.h"
     52 
     53 #define MAX_UI_COUNT 16
     54 
     55 static RemoteUI *uis[MAX_UI_COUNT];
     56 static bool ui_ext[kUIExtCount] = { 0 };
     57 static size_t ui_count = 0;
     58 static int ui_mode_idx = SHAPE_IDX_N;
     59 static int cursor_row = 0, cursor_col = 0;
     60 static bool pending_cursor_update = false;
     61 static int busy = 0;
     62 static bool pending_mode_info_update = false;
     63 static bool pending_mode_update = false;
     64 static handle_T cursor_grid_handle = DEFAULT_GRID_HANDLE;
     65 
     66 static PMap(uint32_t) ui_event_cbs = MAP_INIT;
     67 bool ui_cb_ext[kUIExtCount];  ///< Internalized UI capabilities.
     68 
     69 static bool has_mouse = false;
     70 static int pending_has_mouse = -1;
     71 static bool pending_default_colors = false;
     72 
     73 #ifdef NVIM_LOG_DEBUG
     74 static size_t uilog_seen = 0;
     75 static const char *uilog_last_event = NULL;
     76 
     77 static void ui_log(const char *funname)
     78 {
     79 # ifdef EXITFREE
     80  if (entered_free_all_mem) {
     81    return;  // do nothing, we cannot log now
     82  }
     83 # endif
     84 
     85  if (uilog_last_event == funname) {
     86    uilog_seen++;
     87  } else {
     88    if (uilog_seen > 0) {
     89      logmsg(LOGLVL_DBG, "UI: ", NULL, -1, true,
     90             "%s (+%zu times...)", uilog_last_event, uilog_seen);
     91    }
     92    logmsg(LOGLVL_DBG, "UI: ", NULL, -1, true, "%s", funname);
     93    uilog_seen = 0;
     94    uilog_last_event = funname;
     95  }
     96 }
     97 #else
     98 # define ui_log(funname)
     99 #endif
    100 
    101 // UI_CALL invokes a function on all registered UI instances.
    102 // This is called by code generated by generators/gen_api_ui_events.lua
    103 // C code should use ui_call_{funname} instead.
    104 #define UI_CALL(cond, funname, ...) \
    105  do { \
    106    bool any_call = false; \
    107    for (size_t i = 0; i < ui_count; i++) { \
    108      RemoteUI *ui = uis[i]; \
    109      if ((cond)) { \
    110        remote_ui_##funname(__VA_ARGS__); \
    111        any_call = true; \
    112      } \
    113    } \
    114    if (any_call) { \
    115      ui_log(STR(funname)); \
    116    } \
    117  } while (0)
    118 
    119 #include "ui_events_call.generated.h"
    120 
    121 void ui_init(void)
    122 {
    123  default_grid.handle = 1;
    124  msg_grid_adj.target = &default_grid;
    125  ui_comp_init();
    126 }
    127 
    128 #ifdef EXITFREE
    129 void ui_free_all_mem(void)
    130 {
    131  UIEventCallback *event_cb;
    132  map_foreach_value(&ui_event_cbs, event_cb, {
    133    free_ui_event_callback(event_cb);
    134  })
    135  map_destroy(uint32_t, &ui_event_cbs);
    136 
    137  multiqueue_free(resize_events);
    138 }
    139 #endif
    140 
    141 /// Returns true if any `rgb=true` UI is attached.
    142 bool ui_rgb_attached(void)
    143 {
    144  if (p_tgc) {
    145    return true;
    146  }
    147  for (size_t i = 0; i < ui_count; i++) {
    148    // We do not consider the TUI in this loop because we already checked for 'termguicolors' at the
    149    // beginning of this function. In this loop, we are checking to see if any _other_ UIs which
    150    // support RGB are attached.
    151    bool tui = uis[i]->stdin_tty || uis[i]->stdout_tty;
    152    if (!tui && uis[i]->rgb) {
    153      return true;
    154    }
    155  }
    156  return false;
    157 }
    158 
    159 /// Returns true if a GUI is attached.
    160 bool ui_gui_attached(void)
    161 {
    162  for (size_t i = 0; i < ui_count; i++) {
    163    bool tui = uis[i]->stdin_tty || uis[i]->stdout_tty;
    164    if (!tui) {
    165      return true;
    166    }
    167  }
    168  return false;
    169 }
    170 
    171 /// Returns true if any UI requested `override=true`.
    172 bool ui_override(void)
    173 {
    174  for (size_t i = 0; i < ui_count; i++) {
    175    if (uis[i]->override) {
    176      return true;
    177    }
    178  }
    179  return false;
    180 }
    181 
    182 /// Gets the number of UIs connected to this server.
    183 size_t ui_active(void)
    184 {
    185  return ui_count;
    186 }
    187 
    188 void ui_refresh(void)
    189 {
    190  if (ui_client_channel_id) {
    191    abort();
    192  }
    193 
    194  int width = INT_MAX;
    195  int height = INT_MAX;
    196  bool ext_widgets[kUIExtCount];
    197  bool inclusive = ui_override();
    198  memset(ext_widgets, !!ui_active(), ARRAY_SIZE(ext_widgets));
    199 
    200  for (size_t i = 0; i < ui_count; i++) {
    201    RemoteUI *ui = uis[i];
    202    width = MIN(ui->width, width);
    203    height = MIN(ui->height, height);
    204    for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
    205      ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
    206    }
    207  }
    208 
    209  cursor_row = cursor_col = 0;
    210  pending_cursor_update = true;
    211 
    212  bool had_message = ui_ext[kUIMessages];
    213  for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
    214    ui_ext[i] = ext_widgets[i] | ui_cb_ext[i];
    215    if (i < kUIGlobalCount) {
    216      ui_call_option_set(cstr_as_string(ui_ext_names[i]), BOOLEAN_OBJ(ui_ext[i]));
    217    }
    218  }
    219 
    220  // Reset 'cmdheight' for all tabpages when ext_messages toggles.
    221  if (had_message != ui_ext[kUIMessages]) {
    222    if (ui_refresh_cmdheight) {
    223      set_option_value(kOptCmdheight, NUMBER_OPTVAL(had_message), 0);
    224      FOR_ALL_TABS(tp) {
    225        tp->tp_ch_used = had_message;
    226      }
    227    }
    228    msg_scroll_flush();
    229  }
    230  msg_ui_refresh();
    231 
    232  if (!ui_active()) {
    233    return;
    234  }
    235 
    236  if (updating_screen) {
    237    ui_schedule_refresh();
    238    return;
    239  }
    240 
    241  ui_default_colors_set();
    242 
    243  int save_p_lz = p_lz;
    244  p_lz = false;  // convince redrawing() to return true ...
    245  screen_resize(width, height);
    246  p_lz = save_p_lz;
    247 
    248  ui_mode_info_set();
    249  pending_mode_update = true;
    250  ui_cursor_shape();
    251  pending_has_mouse = -1;
    252 }
    253 
    254 int ui_pum_get_height(void)
    255 {
    256  int pum_height = 0;
    257  for (size_t i = 0; i < ui_count; i++) {
    258    int ui_pum_height = uis[i]->pum_nlines;
    259    if (ui_pum_height) {
    260      pum_height =
    261        pum_height != 0 ? MIN(pum_height, ui_pum_height) : ui_pum_height;
    262    }
    263  }
    264  return pum_height;
    265 }
    266 
    267 bool ui_pum_get_pos(double *pwidth, double *pheight, double *prow, double *pcol)
    268 {
    269  for (size_t i = 0; i < ui_count; i++) {
    270    if (!uis[i]->pum_pos) {
    271      continue;
    272    }
    273    *pwidth = uis[i]->pum_width;
    274    *pheight = uis[i]->pum_height;
    275    *prow = uis[i]->pum_row;
    276    *pcol = uis[i]->pum_col;
    277    return true;
    278  }
    279  return false;
    280 }
    281 
    282 static void ui_refresh_event(void **argv)
    283 {
    284  ui_refresh();
    285 }
    286 
    287 void ui_schedule_refresh(void)
    288 {
    289  multiqueue_put(resize_events, ui_refresh_event, NULL);
    290 }
    291 
    292 void ui_default_colors_set(void)
    293 {
    294  // Throttle setting of default colors at startup, so it only happens once
    295  // if the user sets the colorscheme in startup.
    296  pending_default_colors = true;
    297  if (starting == 0) {
    298    ui_may_set_default_colors();
    299  }
    300 }
    301 
    302 static void ui_may_set_default_colors(void)
    303 {
    304  if (pending_default_colors) {
    305    pending_default_colors = false;
    306    ui_call_default_colors_set(normal_fg, normal_bg, normal_sp,
    307                               cterm_normal_fg_color, cterm_normal_bg_color);
    308  }
    309 }
    310 
    311 void ui_busy_start(void)
    312 {
    313  if (!(busy++)) {
    314    ui_call_busy_start();
    315  }
    316 }
    317 
    318 void ui_busy_stop(void)
    319 {
    320  if (!(--busy)) {
    321    ui_call_busy_stop();
    322  }
    323 }
    324 
    325 /// Emit a bell or visualbell as a warning
    326 ///
    327 /// val is one of the OptBoFlags values, e.g., kOptBoFlagOperator
    328 void vim_beep(unsigned val)
    329 {
    330  called_vim_beep = true;
    331 
    332  if (emsg_silent != 0 || in_assert_fails) {
    333    return;
    334  }
    335 
    336  if (!((bo_flags & val) || (bo_flags & kOptBoFlagAll))) {
    337    static int beeps = 0;
    338    static uint64_t start_time = 0;
    339 
    340    // Only beep up to three times per half a second,
    341    // otherwise a sequence of beeps would freeze Vim.
    342    if (start_time == 0 || os_hrtime() - start_time > 500000000U) {
    343      beeps = 0;
    344      start_time = os_hrtime();
    345    }
    346    beeps++;
    347    if (beeps <= 3) {
    348      if (p_vb) {
    349        ui_call_visual_bell();
    350      } else {
    351        ui_call_bell();
    352      }
    353    }
    354  }
    355 
    356  // When 'debug' contains "beep" produce a message.  If we are sourcing
    357  // a script or executing a function give the user a hint where the beep
    358  // comes from.
    359  if (vim_strchr(p_debug, 'e') != NULL) {
    360    msg_source(HLF_W);
    361    msg(_("Beep!"), HLF_W);
    362  }
    363 }
    364 
    365 /// Trigger UIEnter for all attached UIs.
    366 /// Used on startup after VimEnter.
    367 void do_autocmd_uienter_all(void)
    368 {
    369  for (size_t i = 0; i < ui_count; i++) {
    370    do_autocmd_uienter(uis[i]->channel_id, true);
    371  }
    372 }
    373 
    374 bool ui_can_attach_more(void)
    375 {
    376  return ui_count < MAX_UI_COUNT;
    377 }
    378 
    379 void ui_attach_impl(RemoteUI *ui, uint64_t chanid)
    380 {
    381  if (ui_count >= MAX_UI_COUNT) {
    382    abort();
    383  }
    384  if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]
    385      && !ui_client_channel_id) {
    386    ui_comp_attach(ui);
    387  }
    388 
    389  uis[ui_count++] = ui;
    390  ui_refresh_options();
    391  resettitle();
    392 
    393  char cwd[MAXPATHL];
    394  size_t cwdlen = sizeof(cwd);
    395  if (uv_cwd(cwd, &cwdlen) == 0) {
    396    ui_call_chdir((String){ .data = cwd, .size = cwdlen });
    397  }
    398 
    399  for (UIExtension i = kUIGlobalCount; (int)i < kUIExtCount; i++) {
    400    ui_set_ext_option(ui, i, ui->ui_ext[i]);
    401  }
    402 
    403  bool sent = false;
    404  if (ui->ui_ext[kUIHlState]) {
    405    sent = highlight_use_hlstate();
    406  }
    407  if (!sent) {
    408    ui_send_all_hls(ui);
    409  }
    410  ui_refresh();
    411 
    412  do_autocmd_uienter(chanid, true);
    413 }
    414 
    415 void ui_detach_impl(RemoteUI *ui, uint64_t chanid)
    416 {
    417  if (ui_count > MAX_UI_COUNT) {
    418    abort();
    419  }
    420  size_t shift_index = MAX_UI_COUNT;
    421 
    422  // Find the index that will be removed
    423  for (size_t i = 0; i < ui_count; i++) {
    424    if (uis[i] == ui) {
    425      shift_index = i;
    426      break;
    427    }
    428  }
    429 
    430  if (shift_index >= MAX_UI_COUNT) {
    431    abort();
    432  }
    433 
    434  // Shift UIs at "shift_index"
    435  while (shift_index < ui_count - 1) {
    436    uis[shift_index] = uis[shift_index + 1];
    437    shift_index++;
    438  }
    439 
    440  if (--ui_count
    441      // During teardown/exit the loop was already destroyed, cannot schedule.
    442      // https://github.com/neovim/neovim/pull/5119#issuecomment-258667046
    443      && !exiting) {
    444    ui_schedule_refresh();
    445  }
    446 
    447  if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]) {
    448    ui_comp_detach(ui);
    449  }
    450 
    451  do_autocmd_uienter(chanid, false);
    452 }
    453 
    454 void ui_set_ext_option(RemoteUI *ui, UIExtension ext, bool active)
    455 {
    456  if (ext < kUIGlobalCount) {
    457    ui_refresh();
    458    return;
    459  }
    460  if (ui_ext_names[ext][0] != '_' || active) {
    461    remote_ui_option_set(ui, cstr_as_string(ui_ext_names[ext]), BOOLEAN_OBJ(active));
    462  }
    463  if (ext == kUITermColors) {
    464    ui_default_colors_set();
    465  }
    466 }
    467 
    468 void ui_line(ScreenGrid *grid, int row, bool invalid_row, int startcol, int endcol, int clearcol,
    469             int clearattr, bool wrap)
    470 {
    471  assert(0 <= row && row < grid->rows);
    472  LineFlags flags = wrap ? kLineFlagWrap : 0;
    473  if (startcol == 0 && invalid_row) {
    474    flags |= kLineFlagInvalid;
    475  }
    476 
    477  // set default colors now so that that text won't have to be repainted later
    478  ui_may_set_default_colors();
    479 
    480  size_t off = grid->line_offset[row] + (size_t)startcol;
    481 
    482  ui_call_raw_line(grid->handle, row, startcol, endcol, clearcol, clearattr,
    483                   flags, (const schar_T *)grid->chars + off,
    484                   (const sattr_T *)grid->attrs + off);
    485 
    486  // 'writedelay': flush & delay each time.
    487  if (p_wd && (rdb_flags & kOptRdbFlagLine)) {
    488    // If 'writedelay' is active, set the cursor to indicate what was drawn.
    489    ui_call_grid_cursor_goto(grid->handle, row,
    490                             MIN(clearcol, (int)grid->cols - 1));
    491    ui_call_flush();
    492    uint64_t wd = (uint64_t)llabs(p_wd);
    493    os_sleep(wd);
    494    pending_cursor_update = true;  // restore the cursor later
    495  }
    496 }
    497 
    498 void ui_cursor_goto(int new_row, int new_col)
    499 {
    500  ui_grid_cursor_goto(DEFAULT_GRID_HANDLE, new_row, new_col);
    501 }
    502 
    503 void ui_grid_cursor_goto(handle_T grid_handle, int new_row, int new_col)
    504 {
    505  if (new_row == cursor_row
    506      && new_col == cursor_col
    507      && grid_handle == cursor_grid_handle) {
    508    return;
    509  }
    510 
    511  cursor_row = new_row;
    512  cursor_col = new_col;
    513  cursor_grid_handle = grid_handle;
    514  pending_cursor_update = true;
    515 }
    516 
    517 /// moving the cursor grid will implicitly move the cursor
    518 void ui_check_cursor_grid(handle_T grid_handle)
    519 {
    520  if (cursor_grid_handle == grid_handle) {
    521    pending_cursor_update = true;
    522  }
    523 }
    524 
    525 void ui_mode_info_set(void)
    526 {
    527  pending_mode_info_update = true;
    528 }
    529 
    530 int ui_current_row(void)
    531 {
    532  return cursor_row;
    533 }
    534 
    535 int ui_current_col(void)
    536 {
    537  return cursor_col;
    538 }
    539 
    540 void ui_flush(void)
    541 {
    542  assert(!ui_client_channel_id);
    543  if (!ui_active()) {
    544    return;
    545  }
    546 
    547  static bool was_busy = false;
    548 
    549  if (!(State & MODE_CMDLINE) && curwin->w_floating && curwin->w_config.hide) {
    550    if (!was_busy) {
    551      ui_call_busy_start();
    552      was_busy = true;
    553    }
    554  } else if (was_busy) {
    555    ui_call_busy_stop();
    556    was_busy = false;
    557  }
    558 
    559  win_ui_flush(false);
    560  // Avoid flushing callbacks expected to change text during textlock.
    561  if (textlock == 0 && expr_map_lock == 0) {
    562    cmdline_ui_flush();
    563    msg_ext_ui_flush();
    564  }
    565  msg_scroll_flush();
    566 
    567  if (pending_cursor_update) {
    568    ui_call_grid_cursor_goto(cursor_grid_handle, cursor_row, cursor_col);
    569    pending_cursor_update = false;
    570    // The cursor move might change the composition order,
    571    // so flush again to update the windows that changed
    572    // TODO(bfredl): refactor the flow of information so that win_ui_flush()
    573    // only is called once. (as order state is exposed, it should be owned
    574    // by nvim core, not the compositor)
    575    win_ui_flush(false);
    576  }
    577  if (pending_mode_info_update) {
    578    Arena arena = ARENA_EMPTY;
    579    Array style = mode_style_array(&arena);
    580    bool enabled = (*p_guicursor != NUL);
    581    ui_call_mode_info_set(enabled, style);
    582    arena_mem_free(arena_finish(&arena));
    583    pending_mode_info_update = false;
    584  }
    585 
    586  static bool cursor_was_obscured = false;
    587  bool cursor_obscured = ui_cursor_is_behind_floatwin();
    588  if ((cursor_obscured != cursor_was_obscured || pending_mode_update) && !starting) {
    589    // Show "empty box" (underline style) cursor instead if behind a floatwin.
    590    int idx = cursor_obscured ? SHAPE_IDX_R : ui_mode_idx;
    591    char *full_name = shape_table[idx].full_name;
    592    ui_call_mode_change(cstr_as_string(full_name), idx);
    593    pending_mode_update = false;
    594    cursor_was_obscured = cursor_obscured;
    595  }
    596 
    597  if (pending_has_mouse != has_mouse) {
    598    (has_mouse ? ui_call_mouse_on : ui_call_mouse_off)();
    599    pending_has_mouse = has_mouse;
    600  }
    601  ui_call_flush();
    602 
    603  if (p_wd && (rdb_flags & kOptRdbFlagFlush)) {
    604    os_sleep((uint64_t)llabs(p_wd));
    605  }
    606 }
    607 
    608 /// Check if 'mouse' is active for the current mode
    609 ///
    610 /// TODO(bfredl): precompute the State -> active mapping when 'mouse' changes,
    611 /// then this can be checked directly in ui_flush()
    612 void ui_check_mouse(void)
    613 {
    614  has_mouse = false;
    615  // Be quick when mouse is off.
    616  if (*p_mouse == NUL) {
    617    return;
    618  }
    619 
    620  int checkfor = MOUSE_NORMAL;  // assume normal mode
    621  if (VIsual_active) {
    622    checkfor = MOUSE_VISUAL;
    623  } else if (State == MODE_HITRETURN || State == MODE_ASKMORE || State == MODE_SETWSIZE) {
    624    checkfor = MOUSE_RETURN;
    625  } else if (State & MODE_INSERT) {
    626    checkfor = MOUSE_INSERT;
    627  } else if (State & MODE_CMDLINE) {
    628    checkfor = MOUSE_COMMAND;
    629  } else if (State == MODE_EXTERNCMD) {
    630    checkfor = ' ';  // don't use mouse for ":!cmd"
    631  }
    632 
    633  // mouse should be active if at least one of the following is true:
    634  // - "c" is in 'mouse', or
    635  // - 'a' is in 'mouse' and "c" is in MOUSE_A, or
    636  // - the current buffer is a help file and 'h' is in 'mouse' and we are in a
    637  //   normal editing mode (not at hit-return message).
    638  for (char *p = p_mouse; *p; p++) {
    639    switch (*p) {
    640    case 'a':
    641      if (vim_strchr(MOUSE_A, checkfor) != NULL) {
    642        has_mouse = true;
    643        return;
    644      }
    645      break;
    646    case MOUSE_HELP:
    647      if (checkfor != MOUSE_RETURN && curbuf->b_help) {
    648        has_mouse = true;
    649        return;
    650      }
    651      break;
    652    default:
    653      if (checkfor == *p) {
    654        has_mouse = true;
    655        return;
    656      }
    657    }
    658  }
    659 }
    660 
    661 /// Check if current mode has changed.
    662 ///
    663 /// May update the shape of the cursor.
    664 void ui_cursor_shape_no_check_conceal(void)
    665 {
    666  if (!full_screen) {
    667    return;
    668  }
    669  int new_mode_idx = cursor_get_mode_idx();
    670 
    671  if (new_mode_idx != ui_mode_idx) {
    672    ui_mode_idx = new_mode_idx;
    673    pending_mode_update = true;
    674  }
    675 }
    676 
    677 /// Check if current mode has changed.
    678 ///
    679 /// May update the shape of the cursor.
    680 /// With concealing on, may conceal or unconceal the cursor line.
    681 void ui_cursor_shape(void)
    682 {
    683  ui_cursor_shape_no_check_conceal();
    684  conceal_check_cursor_line();
    685 }
    686 
    687 /// Check if the cursor is behind a floating window (only in compositor mode).
    688 /// @return true if cursor is obscured by a float with higher zindex
    689 static bool ui_cursor_is_behind_floatwin(void)
    690 {
    691  if ((State & MODE_CMDLINE) || !ui_comp_should_draw()) {
    692    return false;
    693  }
    694 
    695  int crow = curwin->w_winrow + curwin->w_winrow_off + curwin->w_wrow;
    696  int ccol = curwin->w_wincol + curwin->w_wincol_off
    697             + (curwin->w_p_rl ? curwin->w_view_width - curwin->w_wcol - 1 : curwin->w_wcol);
    698 
    699  ScreenGrid *top_grid = ui_comp_get_grid_at_coord(crow, ccol);
    700  return top_grid != &curwin->w_grid_alloc && top_grid != &default_grid;
    701 }
    702 
    703 /// Returns true if the given UI extension is enabled.
    704 bool ui_has(UIExtension ext)
    705 {
    706  return ui_ext[ext];
    707 }
    708 
    709 Array ui_array(Arena *arena)
    710 {
    711  Array all_uis = arena_array(arena, ui_count);
    712  for (size_t i = 0; i < ui_count; i++) {
    713    RemoteUI *ui = uis[i];
    714    Dict info = arena_dict(arena, 10 + kUIExtCount);
    715    PUT_C(info, "width", INTEGER_OBJ(ui->width));
    716    PUT_C(info, "height", INTEGER_OBJ(ui->height));
    717    PUT_C(info, "rgb", BOOLEAN_OBJ(ui->rgb));
    718    PUT_C(info, "override", BOOLEAN_OBJ(ui->override));
    719 
    720    // TUI fields. (`stdin_fd` is intentionally omitted.)
    721    PUT_C(info, "term_name", CSTR_AS_OBJ(ui->term_name));
    722 
    723    // term_background is deprecated. Populate with an empty string
    724    PUT_C(info, "term_background", STATIC_CSTR_AS_OBJ(""));
    725 
    726    PUT_C(info, "term_colors", INTEGER_OBJ(ui->term_colors));
    727    PUT_C(info, "stdin_tty", BOOLEAN_OBJ(ui->stdin_tty));
    728    PUT_C(info, "stdout_tty", BOOLEAN_OBJ(ui->stdout_tty));
    729 
    730    for (UIExtension j = 0; j < kUIExtCount; j++) {
    731      if (ui_ext_names[j][0] != '_' || ui->ui_ext[j]) {
    732        PUT_C(info, (char *)ui_ext_names[j], BOOLEAN_OBJ(ui->ui_ext[j]));
    733      }
    734    }
    735    PUT_C(info, "chan", INTEGER_OBJ((Integer)ui->channel_id));
    736 
    737    ADD_C(all_uis, DICT_OBJ(info));
    738  }
    739  return all_uis;
    740 }
    741 
    742 void ui_grid_resize(handle_T grid_handle, int width, int height, Error *err)
    743 {
    744  if (grid_handle == DEFAULT_GRID_HANDLE) {
    745    screen_resize(width, height);
    746    return;
    747  }
    748 
    749  win_T *wp = get_win_by_grid_handle(grid_handle);
    750  VALIDATE_INT((wp != NULL), "window handle", (int64_t)grid_handle, {
    751    return;
    752  });
    753 
    754  if (wp->w_floating) {
    755    if (width != wp->w_width || height != wp->w_height) {
    756      wp->w_config.width = MAX(width, 1);
    757      wp->w_config.height = MAX(height, 1);
    758      win_config_float(wp, wp->w_config);
    759    }
    760  } else {
    761    // non-positive indicates no request
    762    wp->w_height_request = MAX(height, 0);
    763    wp->w_width_request = MAX(width, 0);
    764    win_set_inner_size(wp, true);
    765  }
    766 }
    767 
    768 static void ui_attach_error(uint32_t ns_id, const char *name, const char *msg)
    769 {
    770  const char *ns = describe_ns((NS)ns_id, "(UNKNOWN PLUGIN)");
    771  ELOG("Error in \"%s\" UI event handler (ns=%s):\n%s", name, ns, msg);
    772  msg_schedule_semsg_multiline("Error in \"%s\" UI event handler (ns=%s):\n%s", name, ns, msg);
    773 }
    774 
    775 void ui_call_event(char *name, Array args)
    776 {
    777  // Internal messages are considered unsafe and are executed in fast context.
    778  bool fast = strcmp(name, "msg_show") == 0;
    779  const char *not_fast[] = {
    780    "empty",
    781    "echo",
    782    "echomsg",
    783    "echoerr",
    784    "list_cmd",
    785    "lua_error",
    786    "lua_print",
    787    "progress",
    788    NULL,
    789  };
    790 
    791  for (int i = 0; fast && not_fast[i]; i++) {
    792    fast = !strequal(not_fast[i], args.items[0].data.string.data);
    793  }
    794 
    795  // Don't impose textlock restrictions upon UI event handlers.
    796  int save_expr_map_lock = expr_map_lock;
    797  int save_textlock = textlock;
    798  expr_map_lock = 0;
    799  textlock = 0;
    800 
    801  bool handled = false;
    802  UIEventCallback *event_cb;
    803  map_foreach(&ui_event_cbs, ui_event_ns_id, event_cb, {
    804    Error err = ERROR_INIT;
    805    uint32_t ns_id = ui_event_ns_id;
    806    Object res = nlua_call_ref_ctx(fast, event_cb->cb, name, args, kRetNilBool, NULL, &err);
    807    ui_event_ns_id = 0;
    808    if (LUARET_TRUTHY(res)) {
    809      handled = true;
    810    }
    811    if (ERROR_SET(&err)) {
    812      ui_attach_error(ns_id, name, err.msg);
    813      ui_remove_cb(ns_id, true);
    814    }
    815    api_clear_error(&err);
    816  })
    817  expr_map_lock = save_expr_map_lock;
    818  textlock = save_textlock;
    819 
    820  if (!handled) {
    821    UI_CALL(true, event, ui, name, args);
    822  }
    823 
    824  ui_log(name);
    825 }
    826 
    827 static void ui_cb_update_ext(void)
    828 {
    829  memset(ui_cb_ext, 0, ARRAY_SIZE(ui_cb_ext));
    830 
    831  for (size_t i = 0; i < kUIGlobalCount; i++) {
    832    UIEventCallback *event_cb;
    833 
    834    map_foreach_value(&ui_event_cbs, event_cb, {
    835      if (event_cb->ext_widgets[i]) {
    836        ui_cb_ext[i] = true;
    837        break;
    838      }
    839    })
    840  }
    841 }
    842 
    843 static void free_ui_event_callback(UIEventCallback *event_cb)
    844 {
    845  api_free_luaref(event_cb->cb);
    846  xfree(event_cb);
    847 }
    848 
    849 void ui_add_cb(uint32_t ns_id, LuaRef cb, bool *ext_widgets)
    850 {
    851  UIEventCallback *event_cb = xcalloc(1, sizeof(UIEventCallback));
    852  event_cb->cb = cb;
    853  memcpy(event_cb->ext_widgets, ext_widgets, ARRAY_SIZE(event_cb->ext_widgets));
    854  if (event_cb->ext_widgets[kUIMessages]) {
    855    event_cb->ext_widgets[kUICmdline] = true;
    856  }
    857 
    858  ptr_t *item = pmap_put_ref(uint32_t)(&ui_event_cbs, ns_id, NULL, NULL);
    859  if (*item) {
    860    free_ui_event_callback((UIEventCallback *)(*item));
    861  }
    862  *item = event_cb;
    863 
    864  ui_cb_update_ext();
    865  ui_refresh();
    866 }
    867 
    868 void ui_remove_cb(uint32_t ns_id, bool checkerr)
    869 {
    870  UIEventCallback *item = pmap_get(uint32_t)(&ui_event_cbs, ns_id);
    871  if (item && (!checkerr || ++item->errors > CB_MAX_ERROR)) {
    872    pmap_del(uint32_t)(&ui_event_cbs, ns_id, NULL);
    873    free_ui_event_callback(item);
    874    ui_cb_update_ext();
    875    ui_refresh();
    876    if (checkerr) {
    877      const char *ns = describe_ns((NS)ns_id, "(UNKNOWN PLUGIN)");
    878      msg_schedule_semsg("Excessive errors in vim.ui_attach() callback (ns=%s)", ns);
    879    }
    880  }
    881 }