neovim

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

ui.c (32243B)


      1 #include <assert.h>
      2 #include <inttypes.h>
      3 #include <stdbool.h>
      4 #include <stddef.h>
      5 #include <stdint.h>
      6 #include <stdlib.h>
      7 #include <string.h>
      8 
      9 #include "klib/kvec.h"
     10 #include "nvim/api/private/converter.h"
     11 #include "nvim/api/private/defs.h"
     12 #include "nvim/api/private/helpers.h"
     13 #include "nvim/api/private/validate.h"
     14 #include "nvim/api/ui.h"
     15 #include "nvim/assert_defs.h"
     16 #include "nvim/autocmd.h"
     17 #include "nvim/autocmd_defs.h"
     18 #include "nvim/channel.h"
     19 #include "nvim/channel_defs.h"
     20 #include "nvim/eval/typval.h"
     21 #include "nvim/eval/vars.h"
     22 #include "nvim/event/defs.h"
     23 #include "nvim/event/loop.h"
     24 #include "nvim/event/multiqueue.h"
     25 #include "nvim/event/wstream.h"
     26 #include "nvim/globals.h"
     27 #include "nvim/grid.h"
     28 #include "nvim/highlight.h"
     29 #include "nvim/macros_defs.h"
     30 #include "nvim/main.h"
     31 #include "nvim/map_defs.h"
     32 #include "nvim/mbyte.h"
     33 #include "nvim/memory.h"
     34 #include "nvim/memory_defs.h"
     35 #include "nvim/msgpack_rpc/channel.h"
     36 #include "nvim/msgpack_rpc/channel_defs.h"
     37 #include "nvim/msgpack_rpc/packer.h"
     38 #include "nvim/msgpack_rpc/packer_defs.h"
     39 #include "nvim/option.h"
     40 #include "nvim/types_defs.h"
     41 #include "nvim/ui.h"
     42 
     43 #define BUF_POS(ui) ((size_t)((ui)->packer.ptr - (ui)->packer.startptr))
     44 
     45 #include "api/ui.c.generated.h"
     46 #include "ui_events_remote.generated.h"  // IWYU pragma: export
     47 
     48 // TODO(bfredl): just make UI:s owned by their channels instead
     49 static PMap(uint64_t) connected_uis = MAP_INIT;
     50 
     51 /// Gets the UI attached to the given channel, or sets an error message on `err`.
     52 static RemoteUI *get_ui_or_err(uint64_t chan_id, Error *err)
     53 {
     54  RemoteUI *ui = pmap_get(uint64_t)(&connected_uis, chan_id);
     55  if (ui == NULL && err != NULL) {
     56    api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, chan_id);
     57  }
     58  return ui;
     59 }
     60 
     61 static char *mpack_array_dyn16(char **buf)
     62 {
     63  mpack_w(buf, 0xdc);
     64  char *pos = *buf;
     65  mpack_w2(buf, 0xFFEF);
     66  return pos;
     67 }
     68 
     69 static void mpack_str_small(char **buf, const char *str, size_t len)
     70 {
     71  assert(len < 0x20);
     72  mpack_w(buf, 0xa0 | len);
     73  memcpy(*buf, str, len);
     74  *buf += len;
     75 }
     76 
     77 static void remote_ui_destroy(RemoteUI *ui)
     78  FUNC_ATTR_NONNULL_ALL
     79 {
     80  xfree(ui->packer.startptr);
     81  XFREE_CLEAR(ui->term_name);
     82  xfree(ui);
     83 }
     84 
     85 /// Removes the client on the given channel from the list of UIs.
     86 ///
     87 /// @param err  if non-NULL and there is no UI on the channel, set an error
     88 /// @param send_error_exit  send an "error_exit" event with 0 status first
     89 void remote_ui_disconnect(uint64_t channel_id, Error *err, bool send_error_exit)
     90 {
     91  RemoteUI *ui = get_ui_or_err(channel_id, err);
     92  if (!ui) {
     93    return;
     94  }
     95  if (send_error_exit) {
     96    MAXSIZE_TEMP_ARRAY(args, 1);
     97    ADD_C(args, INTEGER_OBJ(0));
     98    push_call(ui, "error_exit", args);
     99    ui_flush_buf(ui, false);
    100  }
    101  pmap_del(uint64_t)(&connected_uis, channel_id, NULL);
    102  ui_detach_impl(ui, channel_id);
    103  Channel *chan = find_channel(channel_id);
    104  if (chan && chan->rpc.ui == ui) {
    105    chan->rpc.ui = NULL;
    106  }
    107 
    108  remote_ui_destroy(ui);
    109 }
    110 
    111 #ifdef EXITFREE
    112 void remote_ui_free_all_mem(void)
    113 {
    114  RemoteUI *ui;
    115  map_foreach_value(&connected_uis, ui, {
    116    remote_ui_destroy(ui);
    117  });
    118  map_destroy(uint64_t, &connected_uis);
    119 }
    120 #endif
    121 
    122 /// Wait until UI has connected.
    123 ///
    124 /// @param only_stdio UI is expected to connect on stdio.
    125 void remote_ui_wait_for_attach(bool only_stdio)
    126 {
    127  if (only_stdio) {
    128    Channel *channel = find_channel(CHAN_STDIO);
    129    if (!channel) {
    130      // `only_stdio` implies --embed mode, thus stdio channel can be assumed.
    131      abort();
    132    }
    133 
    134    LOOP_PROCESS_EVENTS_UNTIL(&main_loop, channel->events, -1,
    135                              map_has(uint64_t, &connected_uis, CHAN_STDIO));
    136  } else {
    137    LOOP_PROCESS_EVENTS_UNTIL(&main_loop, main_loop.events, -1,
    138                              ui_active());
    139  }
    140 }
    141 
    142 /// Activates UI events on the channel.
    143 ///
    144 /// Entry point of all UI clients.  Allows |--embed| to continue startup.
    145 /// Implies that the client is ready to show the UI.  Adds the client to the
    146 /// list of UIs. |nvim_list_uis()|
    147 ///
    148 /// @note If multiple UI clients are attached, the global screen dimensions
    149 ///       degrade to the smallest client. E.g. if client A requests 80x40 but
    150 ///       client B requests 200x100, the global screen has size 80x40.
    151 ///
    152 /// @param channel_id
    153 /// @param width  Requested screen columns
    154 /// @param height  Requested screen rows
    155 /// @param options  |ui-option| map
    156 /// @param[out] err Error details, if any
    157 void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dict options, Error *err)
    158  FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
    159 {
    160  if (map_has(uint64_t, &connected_uis, channel_id)) {
    161    api_set_error(err, kErrorTypeException,
    162                  "UI already attached to channel: %" PRId64, channel_id);
    163    return;
    164  }
    165  if (!ui_can_attach_more()) {
    166    api_set_error(err, kErrorTypeException, "Maximum UI count reached");
    167    return;
    168  }
    169 
    170  if (width <= 0 || height <= 0) {
    171    api_set_error(err, kErrorTypeValidation, "Expected width > 0 and height > 0");
    172    return;
    173  }
    174  RemoteUI *ui = xcalloc(1, sizeof(RemoteUI));
    175  ui->channel_id = channel_id;
    176  ui->width = (int)width;
    177  ui->height = (int)height;
    178  ui->pum_row = -1.0;
    179  ui->pum_col = -1.0;
    180  ui->rgb = true;
    181  CLEAR_FIELD(ui->ui_ext);
    182 
    183  for (size_t i = 0; i < options.size; i++) {
    184    ui_set_option(ui, true, options.items[i].key, options.items[i].value, err);
    185    if (ERROR_SET(err)) {
    186      xfree(ui);
    187      return;
    188    }
    189  }
    190 
    191  if (ui->ui_ext[kUIHlState] || ui->ui_ext[kUIMultigrid]) {
    192    ui->ui_ext[kUILinegrid] = true;
    193  }
    194 
    195  if (ui->ui_ext[kUIMessages]) {
    196    // This uses attribute indices, so ext_linegrid is needed.
    197    ui->ui_ext[kUILinegrid] = true;
    198    // Cmdline uses the messages area, so it should be externalized too.
    199    ui->ui_ext[kUICmdline] = true;
    200  }
    201 
    202  ui->cur_event = NULL;
    203  ui->hl_id = 0;
    204  ui->client_col = -1;
    205  ui->nevents_pos = NULL;
    206  ui->nevents = 0;
    207  ui->flushed_events = false;
    208  ui->incomplete_event = false;
    209  ui->ncalls_pos = NULL;
    210  ui->ncalls = 0;
    211  ui->ncells_pending = 0;
    212  ui->packer = (PackerBuffer) {
    213    .startptr = NULL,
    214    .ptr = NULL,
    215    .endptr = NULL,
    216    .packer_flush = ui_flush_callback,
    217    .anydata = ui,
    218  };
    219  ui->wildmenu_active = false;
    220 
    221  pmap_put(uint64_t)(&connected_uis, channel_id, ui);
    222  current_ui = channel_id;
    223  ui_attach_impl(ui, channel_id);
    224 
    225  Channel *chan = find_channel(channel_id);
    226  if (chan) {
    227    chan->rpc.ui = ui;
    228  }
    229 
    230  may_trigger_vim_suspend_resume(false);
    231 }
    232 
    233 /// @deprecated
    234 void ui_attach(uint64_t channel_id, Integer width, Integer height, Boolean enable_rgb, Error *err)
    235  FUNC_API_DEPRECATED_SINCE(1)
    236 {
    237  MAXSIZE_TEMP_DICT(opts, 1);
    238  PUT_C(opts, "rgb", BOOLEAN_OBJ(enable_rgb));
    239  nvim_ui_attach(channel_id, width, height, opts, err);
    240 }
    241 
    242 /// Tells the nvim server if focus was gained or lost by the GUI
    243 void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error)
    244  FUNC_API_SINCE(11) FUNC_API_REMOTE_ONLY
    245 {
    246  if (!get_ui_or_err(channel_id, error)) {
    247    return;
    248  }
    249 
    250  if (gained) {
    251    current_ui = channel_id;
    252    may_trigger_vim_suspend_resume(false);
    253  }
    254 
    255  do_autocmd_focusgained((bool)gained);
    256 }
    257 
    258 /// Deactivates UI events on the channel.
    259 ///
    260 /// Removes the client from the list of UIs. |nvim_list_uis()|
    261 ///
    262 /// @param channel_id
    263 /// @param[out] err Error details, if any
    264 void nvim_ui_detach(uint64_t channel_id, Error *err)
    265  FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
    266 {
    267  remote_ui_disconnect(channel_id, err, false);
    268 }
    269 
    270 /// Sends a "restart" UI event to the UI on the given channel.
    271 ///
    272 /// @return  false if there is no UI on the channel, otherwise true
    273 bool remote_ui_restart(uint64_t channel_id, Error *err)
    274 {
    275  RemoteUI *ui = get_ui_or_err(channel_id, err);
    276  if (!ui) {
    277    return false;
    278  }
    279 
    280  MAXSIZE_TEMP_ARRAY(args, 2);
    281 
    282  ADD_C(args, CSTR_AS_OBJ(get_vim_var_str(VV_PROGPATH)));
    283 
    284  Arena arena = ARENA_EMPTY;
    285  const list_T *l = get_vim_var_list(VV_ARGV);
    286  int argc = tv_list_len(l);
    287  assert(argc > 0);
    288  Array argv = arena_array(&arena, (size_t)argc + 1);
    289  TV_LIST_ITER_CONST(l, li, {
    290    const char *arg = tv_get_string(TV_LIST_ITEM_TV(li));
    291    ADD_C(argv, CSTR_AS_OBJ(arg));
    292  });
    293  ADD_C(args, ARRAY_OBJ(argv));
    294 
    295  push_call(ui, "restart", args);
    296  arena_mem_free(arena_finish(&arena));
    297  return true;
    298 }
    299 
    300 // Send a connect UI event to the UI on the given channel
    301 void remote_ui_connect(uint64_t channel_id, char *server_addr, Error *err)
    302 {
    303  RemoteUI *ui = get_ui_or_err(channel_id, err);
    304  if (!ui) {
    305    return;
    306  }
    307 
    308  MAXSIZE_TEMP_ARRAY(args, 1);
    309  ADD_C(args, CSTR_AS_OBJ(server_addr));
    310 
    311  push_call(ui, "connect", args);
    312 }
    313 
    314 // TODO(bfredl): use me to detach a specific ui from the server
    315 void remote_ui_stop(RemoteUI *ui)
    316 {
    317 }
    318 
    319 void nvim_ui_try_resize(uint64_t channel_id, Integer width, Integer height, Error *err)
    320  FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
    321 {
    322  RemoteUI *ui = get_ui_or_err(channel_id, err);
    323  if (!ui) {
    324    return;
    325  }
    326 
    327  if (width <= 0 || height <= 0) {
    328    api_set_error(err, kErrorTypeValidation, "Expected width > 0 and height > 0");
    329    return;
    330  }
    331 
    332  ui->width = (int)width;
    333  ui->height = (int)height;
    334  ui_refresh();
    335 }
    336 
    337 void nvim_ui_set_option(uint64_t channel_id, String name, Object value, Error *error)
    338  FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
    339 {
    340  RemoteUI *ui = get_ui_or_err(channel_id, error);
    341  if (!ui) {
    342    return;
    343  }
    344 
    345  ui_set_option(ui, false, name, value, error);
    346 }
    347 
    348 static void ui_set_option(RemoteUI *ui, bool init, String name, Object value, Error *err)
    349 {
    350  if (strequal(name.data, "override")) {
    351    VALIDATE_T("override", kObjectTypeBoolean, value.type, {
    352      return;
    353    });
    354    ui->override = value.data.boolean;
    355    return;
    356  }
    357 
    358  if (strequal(name.data, "rgb")) {
    359    VALIDATE_T("rgb", kObjectTypeBoolean, value.type, {
    360      return;
    361    });
    362    ui->rgb = value.data.boolean;
    363    // A little drastic, but only takes effect for legacy uis. For linegrid UI
    364    // only changes metadata for nvim_list_uis(), no refresh needed.
    365    if (!init && !ui->ui_ext[kUILinegrid]) {
    366      ui_refresh();
    367    }
    368    return;
    369  }
    370 
    371  if (strequal(name.data, "term_name")) {
    372    VALIDATE_T("term_name", kObjectTypeString, value.type, {
    373      return;
    374    });
    375    set_tty_option("term", string_to_cstr(value.data.string));
    376    ui->term_name = string_to_cstr(value.data.string);
    377    return;
    378  }
    379 
    380  if (strequal(name.data, "term_colors")) {
    381    VALIDATE_T("term_colors", kObjectTypeInteger, value.type, {
    382      return;
    383    });
    384    t_colors = (int)value.data.integer;
    385    ui->term_colors = (int)value.data.integer;
    386    return;
    387  }
    388 
    389  if (strequal(name.data, "stdin_fd")) {
    390    VALIDATE_T("stdin_fd", kObjectTypeInteger, value.type, {
    391      return;
    392    });
    393    VALIDATE_INT((value.data.integer >= 0), "stdin_fd", value.data.integer, {
    394      return;
    395    });
    396    VALIDATE((starting == NO_SCREEN), "%s", "stdin_fd can only be used with first attached UI", {
    397      return;
    398    });
    399 
    400    stdin_fd = (int)value.data.integer;
    401    return;
    402  }
    403 
    404  if (strequal(name.data, "stdin_tty")) {
    405    VALIDATE_T("stdin_tty", kObjectTypeBoolean, value.type, {
    406      return;
    407    });
    408    if (ui->channel_id == CHAN_STDIO) {
    409      stdin_isatty = value.data.boolean;
    410    }
    411    ui->stdin_tty = value.data.boolean;
    412    return;
    413  }
    414 
    415  if (strequal(name.data, "stdout_tty")) {
    416    VALIDATE_T("stdout_tty", kObjectTypeBoolean, value.type, {
    417      return;
    418    });
    419    if (ui->channel_id == CHAN_STDIO) {
    420      stdout_isatty = value.data.boolean;
    421    }
    422    ui->stdout_tty = value.data.boolean;
    423    return;
    424  }
    425 
    426  // LEGACY: Deprecated option, use `ext_cmdline` instead.
    427  bool is_popupmenu = strequal(name.data, "popupmenu_external");
    428 
    429  for (UIExtension i = 0; i < kUIExtCount; i++) {
    430    if (strequal(name.data, ui_ext_names[i])
    431        || (i == kUIPopupmenu && is_popupmenu)) {
    432      VALIDATE_EXP((value.type == kObjectTypeBoolean), name.data, "Boolean",
    433                   api_typename(value.type), {
    434        return;
    435      });
    436      bool boolval = value.data.boolean;
    437      if (!init && i == kUILinegrid && boolval != ui->ui_ext[i]) {
    438        // There shouldn't be a reason for a UI to do this ever
    439        // so explicitly don't support this.
    440        api_set_error(err, kErrorTypeValidation, "ext_linegrid option cannot be changed");
    441      }
    442      ui->ui_ext[i] = boolval;
    443      if (!init) {
    444        ui_set_ext_option(ui, i, boolval);
    445      }
    446      return;
    447    }
    448  }
    449 
    450  api_set_error(err, kErrorTypeValidation, "No such UI option: %s", name.data);
    451 }
    452 
    453 /// Tell Nvim to resize a grid. Triggers a grid_resize event with the requested
    454 /// grid size or the maximum size if it exceeds size limits.
    455 ///
    456 /// On invalid grid handle, fails with error.
    457 ///
    458 /// @param channel_id
    459 /// @param grid    The handle of the grid to be changed.
    460 /// @param width   The new requested width.
    461 /// @param height  The new requested height.
    462 /// @param[out] err Error details, if any
    463 void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, Integer height,
    464                             Error *err)
    465  FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY
    466 {
    467  if (!get_ui_or_err(channel_id, err)) {
    468    return;
    469  }
    470 
    471  if (grid == DEFAULT_GRID_HANDLE) {
    472    nvim_ui_try_resize(channel_id, width, height, err);
    473  } else {
    474    ui_grid_resize((handle_T)grid, (int)width, (int)height, err);
    475  }
    476 }
    477 
    478 /// Tells Nvim the number of elements displaying in the popupmenu, to decide
    479 /// [<PageUp>] and [<PageDown>] movement.
    480 ///
    481 /// @param channel_id
    482 /// @param height  Popupmenu height, must be greater than zero.
    483 /// @param[out] err Error details, if any
    484 void nvim_ui_pum_set_height(uint64_t channel_id, Integer height, Error *err)
    485  FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY
    486 {
    487  RemoteUI *ui = get_ui_or_err(channel_id, err);
    488  if (!ui) {
    489    return;
    490  }
    491 
    492  if (height <= 0) {
    493    api_set_error(err, kErrorTypeValidation, "Expected pum height > 0");
    494    return;
    495  }
    496 
    497  if (!ui->ui_ext[kUIPopupmenu]) {
    498    api_set_error(err, kErrorTypeValidation,
    499                  "It must support the ext_popupmenu option");
    500    return;
    501  }
    502 
    503  ui->pum_nlines = (int)height;
    504 }
    505 
    506 /// Tells Nvim the geometry of the popupmenu, to align floating windows with an
    507 /// external popup menu.
    508 ///
    509 /// Note that this method is not to be confused with |nvim_ui_pum_set_height()|,
    510 /// which sets the number of visible items in the popup menu, while this
    511 /// function sets the bounding box of the popup menu, including visual
    512 /// elements such as borders and sliders. Floats need not use the same font
    513 /// size, nor be anchored to exact grid corners, so one can set floating-point
    514 /// numbers to the popup menu geometry.
    515 ///
    516 /// @param channel_id
    517 /// @param width   Popupmenu width.
    518 /// @param height  Popupmenu height.
    519 /// @param row     Popupmenu row.
    520 /// @param col     Popupmenu height.
    521 /// @param[out] err Error details, if any.
    522 void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Float row, Float col,
    523                            Error *err)
    524  FUNC_API_SINCE(7) FUNC_API_REMOTE_ONLY
    525 {
    526  RemoteUI *ui = get_ui_or_err(channel_id, err);
    527  if (!ui) {
    528    return;
    529  }
    530 
    531  if (!ui->ui_ext[kUIPopupmenu]) {
    532    api_set_error(err, kErrorTypeValidation,
    533                  "UI must support the ext_popupmenu option");
    534    return;
    535  }
    536 
    537  if (width <= 0) {
    538    api_set_error(err, kErrorTypeValidation, "Expected width > 0");
    539    return;
    540  } else if (height <= 0) {
    541    api_set_error(err, kErrorTypeValidation, "Expected height > 0");
    542    return;
    543  }
    544 
    545  ui->pum_row = (double)row;
    546  ui->pum_col = (double)col;
    547  ui->pum_width = (double)width;
    548  ui->pum_height = (double)height;
    549  ui->pum_pos = true;
    550 }
    551 
    552 static void flush_event(RemoteUI *ui)
    553 {
    554  if (ui->cur_event) {
    555    mpack_w2(&ui->ncalls_pos, 1 + ui->ncalls);
    556    ui->cur_event = NULL;
    557    ui->ncalls_pos = NULL;
    558    ui->ncalls = 0;
    559  }
    560 }
    561 
    562 static void ui_alloc_buf(RemoteUI *ui)
    563 {
    564  ui->packer.startptr = alloc_block();
    565  ui->packer.ptr = ui->packer.startptr;
    566  ui->packer.endptr = ui->packer.startptr + UI_BUF_SIZE;
    567 }
    568 
    569 static void prepare_call(RemoteUI *ui, const char *name)
    570 {
    571  if (ui->packer.startptr
    572      && (BUF_POS(ui) > UI_BUF_SIZE - EVENT_BUF_SIZE || ui->ncells_pending >= 500)) {
    573    ui_flush_buf(ui, false);
    574  }
    575 
    576  if (ui->packer.startptr == NULL) {
    577    ui_alloc_buf(ui);
    578  }
    579 
    580  // To optimize data transfer (especially for "grid_line"), we bundle adjacent
    581  // calls to same method together, so only add a new call entry if the last
    582  // method call is different from "name"
    583 
    584  if (!ui->cur_event || !strequal(ui->cur_event, name)) {
    585    char **buf = &ui->packer.ptr;
    586    if (!ui->nevents_pos) {
    587      // [2, "redraw", [...]]
    588      mpack_array(buf, 3);
    589      mpack_uint(buf, 2);
    590      mpack_str_small(buf, S_LEN("redraw"));
    591      ui->nevents_pos = mpack_array_dyn16(buf);
    592      assert(ui->cur_event == NULL);
    593    }
    594    flush_event(ui);
    595    ui->cur_event = name;
    596    ui->ncalls_pos = mpack_array_dyn16(buf);
    597    mpack_str_small(buf, name, strlen(name));
    598    ui->nevents++;
    599    ui->ncalls = 1;
    600  } else {
    601    ui->ncalls++;
    602  }
    603 }
    604 
    605 /// Pushes data into RemoteUI, to be consumed later by remote_ui_flush().
    606 static void push_call(RemoteUI *ui, const char *name, Array args)
    607 {
    608  prepare_call(ui, name);
    609  mpack_object_array(args, &ui->packer);
    610 }
    611 
    612 static void ui_flush_callback(PackerBuffer *packer)
    613 {
    614  RemoteUI *ui = packer->anydata;
    615  ui_flush_buf(ui, true);
    616  ui_alloc_buf(ui);
    617 }
    618 
    619 void remote_ui_grid_clear(RemoteUI *ui, Integer grid)
    620 {
    621  MAXSIZE_TEMP_ARRAY(args, 1);
    622  if (ui->ui_ext[kUILinegrid]) {
    623    ADD_C(args, INTEGER_OBJ(grid));
    624  }
    625  const char *name = ui->ui_ext[kUILinegrid] ? "grid_clear" : "clear";
    626  push_call(ui, name, args);
    627 }
    628 
    629 void remote_ui_grid_resize(RemoteUI *ui, Integer grid, Integer width, Integer height)
    630 {
    631  MAXSIZE_TEMP_ARRAY(args, 3);
    632  if (ui->ui_ext[kUILinegrid]) {
    633    ADD_C(args, INTEGER_OBJ(grid));
    634  } else {
    635    ui->client_col = -1;  // force cursor update
    636  }
    637  ADD_C(args, INTEGER_OBJ(width));
    638  ADD_C(args, INTEGER_OBJ(height));
    639  const char *name = ui->ui_ext[kUILinegrid] ? "grid_resize" : "resize";
    640  push_call(ui, name, args);
    641 }
    642 
    643 void remote_ui_grid_scroll(RemoteUI *ui, Integer grid, Integer top, Integer bot, Integer left,
    644                           Integer right, Integer rows, Integer cols)
    645 {
    646  if (ui->ui_ext[kUILinegrid]) {
    647    MAXSIZE_TEMP_ARRAY(args, 7);
    648    ADD_C(args, INTEGER_OBJ(grid));
    649    ADD_C(args, INTEGER_OBJ(top));
    650    ADD_C(args, INTEGER_OBJ(bot));
    651    ADD_C(args, INTEGER_OBJ(left));
    652    ADD_C(args, INTEGER_OBJ(right));
    653    ADD_C(args, INTEGER_OBJ(rows));
    654    ADD_C(args, INTEGER_OBJ(cols));
    655    push_call(ui, "grid_scroll", args);
    656  } else {
    657    MAXSIZE_TEMP_ARRAY(args, 4);
    658    ADD_C(args, INTEGER_OBJ(top));
    659    ADD_C(args, INTEGER_OBJ(bot - 1));
    660    ADD_C(args, INTEGER_OBJ(left));
    661    ADD_C(args, INTEGER_OBJ(right - 1));
    662    push_call(ui, "set_scroll_region", args);
    663 
    664    kv_size(args) = 0;
    665    ADD_C(args, INTEGER_OBJ(rows));
    666    push_call(ui, "scroll", args);
    667 
    668    // some clients have "clear" being affected by scroll region, so reset it.
    669    kv_size(args) = 0;
    670    ADD_C(args, INTEGER_OBJ(0));
    671    ADD_C(args, INTEGER_OBJ(ui->height - 1));
    672    ADD_C(args, INTEGER_OBJ(0));
    673    ADD_C(args, INTEGER_OBJ(ui->width - 1));
    674    push_call(ui, "set_scroll_region", args);
    675  }
    676 }
    677 
    678 void remote_ui_default_colors_set(RemoteUI *ui, Integer rgb_fg, Integer rgb_bg, Integer rgb_sp,
    679                                  Integer cterm_fg, Integer cterm_bg)
    680 {
    681  if (!ui->ui_ext[kUITermColors]) {
    682    HL_SET_DEFAULT_COLORS(rgb_fg, rgb_bg, rgb_sp);
    683  }
    684  MAXSIZE_TEMP_ARRAY(args, 5);
    685  ADD_C(args, INTEGER_OBJ(rgb_fg));
    686  ADD_C(args, INTEGER_OBJ(rgb_bg));
    687  ADD_C(args, INTEGER_OBJ(rgb_sp));
    688  ADD_C(args, INTEGER_OBJ(cterm_fg));
    689  ADD_C(args, INTEGER_OBJ(cterm_bg));
    690  push_call(ui, "default_colors_set", args);
    691 
    692  // Deprecated
    693  if (!ui->ui_ext[kUILinegrid]) {
    694    kv_size(args) = 0;
    695    ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1));
    696    push_call(ui, "update_fg", args);
    697 
    698    kv_size(args) = 0;
    699    ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1));
    700    push_call(ui, "update_bg", args);
    701 
    702    kv_size(args) = 0;
    703    ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1));
    704    push_call(ui, "update_sp", args);
    705  }
    706 }
    707 
    708 void remote_ui_hl_attr_define(RemoteUI *ui, Integer id, HlAttrs rgb_attrs, HlAttrs cterm_attrs,
    709                              Array info)
    710 {
    711  if (!ui->ui_ext[kUILinegrid]) {
    712    return;
    713  }
    714 
    715  MAXSIZE_TEMP_ARRAY(args, 4);
    716  ADD_C(args, INTEGER_OBJ(id));
    717  MAXSIZE_TEMP_DICT(rgb, HLATTRS_DICT_SIZE);
    718  MAXSIZE_TEMP_DICT(cterm, HLATTRS_DICT_SIZE);
    719  hlattrs2dict(&rgb, NULL, rgb_attrs, true, false);
    720  hlattrs2dict(&cterm, NULL, rgb_attrs, false, false);
    721 
    722  // URLs are not added in hlattrs2dict since they are used only by UIs and not by the highlight
    723  // system. So we add them here.
    724  if (rgb_attrs.url >= 0) {
    725    const char *url = hl_get_url((uint32_t)rgb_attrs.url);
    726    PUT_C(rgb, "url", CSTR_AS_OBJ(url));
    727  }
    728 
    729  ADD_C(args, DICT_OBJ(rgb));
    730  ADD_C(args, DICT_OBJ(cterm));
    731 
    732  if (ui->ui_ext[kUIHlState]) {
    733    ADD_C(args, ARRAY_OBJ(info));
    734  } else {
    735    ADD_C(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT));
    736  }
    737 
    738  push_call(ui, "hl_attr_define", args);
    739 }
    740 
    741 void remote_ui_highlight_set(RemoteUI *ui, int id)
    742 {
    743  if (ui->hl_id == id) {
    744    return;
    745  }
    746 
    747  ui->hl_id = id;
    748  MAXSIZE_TEMP_DICT(dict, HLATTRS_DICT_SIZE);
    749  hlattrs2dict(&dict, NULL, syn_attr2entry(id), ui->rgb, false);
    750  MAXSIZE_TEMP_ARRAY(args, 1);
    751  ADD_C(args, DICT_OBJ(dict));
    752  push_call(ui, "highlight_set", args);
    753 }
    754 
    755 /// "true" cursor used only for input focus
    756 void remote_ui_grid_cursor_goto(RemoteUI *ui, Integer grid, Integer row, Integer col)
    757 {
    758  if (ui->ui_ext[kUILinegrid]) {
    759    MAXSIZE_TEMP_ARRAY(args, 3);
    760    ADD_C(args, INTEGER_OBJ(grid));
    761    ADD_C(args, INTEGER_OBJ(row));
    762    ADD_C(args, INTEGER_OBJ(col));
    763    push_call(ui, "grid_cursor_goto", args);
    764  } else {
    765    ui->cursor_row = row;
    766    ui->cursor_col = col;
    767    remote_ui_cursor_goto(ui, row, col);
    768  }
    769 }
    770 
    771 /// emulated cursor used both for drawing and for input focus
    772 void remote_ui_cursor_goto(RemoteUI *ui, Integer row, Integer col)
    773 {
    774  if (ui->client_row == row && ui->client_col == col) {
    775    return;
    776  }
    777  ui->client_row = row;
    778  ui->client_col = col;
    779  MAXSIZE_TEMP_ARRAY(args, 2);
    780  ADD_C(args, INTEGER_OBJ(row));
    781  ADD_C(args, INTEGER_OBJ(col));
    782  push_call(ui, "cursor_goto", args);
    783 }
    784 
    785 void remote_ui_put(RemoteUI *ui, const char *cell)
    786 {
    787  ui->client_col++;
    788  MAXSIZE_TEMP_ARRAY(args, 1);
    789  ADD_C(args, CSTR_AS_OBJ(cell));
    790  push_call(ui, "put", args);
    791 }
    792 
    793 void remote_ui_raw_line(RemoteUI *ui, Integer grid, Integer row, Integer startcol, Integer endcol,
    794                        Integer clearcol, Integer clearattr, LineFlags flags, const schar_T *chunk,
    795                        const sattr_T *attrs)
    796 {
    797  // If MAX_SCHAR_SIZE is made larger, we need to refactor implementation below
    798  // to not only use FIXSTR (only up to 0x20 bytes)
    799  STATIC_ASSERT(MAX_SCHAR_SIZE - 1 < 0x20, "SCHAR doesn't fit in fixstr");
    800 
    801  if (ui->ui_ext[kUILinegrid]) {
    802    prepare_call(ui, "grid_line");
    803 
    804    char **buf = &ui->packer.ptr;
    805    mpack_array(buf, 5);
    806    mpack_uint(buf, (uint32_t)grid);
    807    mpack_uint(buf, (uint32_t)row);
    808    mpack_uint(buf, (uint32_t)startcol);
    809    char *lenpos = mpack_array_dyn16(buf);
    810 
    811    uint32_t repeat = 0;
    812    size_t ncells = (size_t)(endcol - startcol);
    813    int last_hl = -1;
    814    uint32_t nelem = 0;
    815    bool was_space = false;
    816    for (size_t i = 0; i < ncells; i++) {
    817      repeat++;
    818      if (i == ncells - 1 || attrs[i] != attrs[i + 1] || chunk[i] != chunk[i + 1]) {
    819        if (
    820            // Close to overflowing the redraw buffer. Finish this event, flush,
    821            // and start a new "grid_line" event at the current position.
    822            // For simplicity leave place for the final "clear" element as well,
    823            // hence the factor of 2 in the check.
    824            UI_BUF_SIZE - BUF_POS(ui) < 2 * (1 + 2 + MAX_SCHAR_SIZE + 5 + 5) + 1
    825            // Also if there is a lot of packed cells, pass them off to the UI to
    826            // let it start processing them.
    827            || ui->ncells_pending >= 500) {
    828          // If the last chunk was all spaces, add an empty clearing chunk,
    829          // so it's clear that the last chunk wasn't a clearing chunk.
    830          if (was_space) {
    831            nelem++;
    832            ui->ncells_pending += 1;
    833            mpack_array(buf, 3);
    834            mpack_str_small(buf, S_LEN(" "));
    835            mpack_uint(buf, (uint32_t)clearattr);
    836            mpack_uint(buf, 0);
    837          }
    838          mpack_w2(&lenpos, nelem);
    839          // We only ever set the wrap field on the final "grid_line" event for the line.
    840          mpack_bool(buf, false);
    841          ui_flush_buf(ui, false);
    842 
    843          prepare_call(ui, "grid_line");
    844          mpack_array(buf, 5);
    845          mpack_uint(buf, (uint32_t)grid);
    846          mpack_uint(buf, (uint32_t)row);
    847          mpack_uint(buf, (uint32_t)startcol + (uint32_t)i - repeat + 1);
    848          lenpos = mpack_array_dyn16(buf);
    849          nelem = 0;
    850          last_hl = -1;
    851        }
    852        uint32_t csize = (repeat > 1) ? 3 : ((attrs[i] != last_hl) ? 2 : 1);
    853        nelem++;
    854        mpack_array(buf, csize);
    855        char *size_byte = (*buf)++;
    856        size_t len = schar_get_adv(buf, chunk[i]);
    857        *size_byte = (char)(0xa0 | len);
    858        if (csize >= 2) {
    859          mpack_uint(buf, (uint32_t)attrs[i]);
    860          if (csize >= 3) {
    861            mpack_uint(buf, repeat);
    862          }
    863        }
    864        ui->ncells_pending += MIN(repeat, 2);
    865        last_hl = attrs[i];
    866        repeat = 0;
    867        was_space = chunk[i] == schar_from_ascii(' ');
    868      }
    869    }
    870    // If the last chunk was all spaces, add a clearing chunk even if there are
    871    // no more cells to clear, so there is no ambiguity about what to clear.
    872    if (endcol < clearcol || was_space) {
    873      nelem++;
    874      ui->ncells_pending += 1;
    875      mpack_array(buf, 3);
    876      mpack_str_small(buf, S_LEN(" "));
    877      mpack_uint(buf, (uint32_t)clearattr);
    878      mpack_uint(buf, (uint32_t)(clearcol - endcol));
    879    }
    880    mpack_w2(&lenpos, nelem);
    881    mpack_bool(buf, flags & kLineFlagWrap);
    882  } else {
    883    for (int i = 0; i < endcol - startcol; i++) {
    884      remote_ui_cursor_goto(ui, row, startcol + i);
    885      remote_ui_highlight_set(ui, attrs[i]);
    886      char sc_buf[MAX_SCHAR_SIZE];
    887      schar_get(sc_buf, chunk[i]);
    888      remote_ui_put(ui, sc_buf);
    889      if (utf_ambiguous_width(sc_buf)) {
    890        ui->client_col = -1;  // force cursor update
    891      }
    892    }
    893    if (endcol < clearcol) {
    894      remote_ui_cursor_goto(ui, row, endcol);
    895      remote_ui_highlight_set(ui, (int)clearattr);
    896      // legacy eol_clear was only ever used with cleared attributes
    897      // so be on the safe side
    898      if (clearattr == 0 && clearcol == Columns) {
    899        Array args = ARRAY_DICT_INIT;
    900        push_call(ui, "eol_clear", args);
    901      } else {
    902        for (Integer c = endcol; c < clearcol; c++) {
    903          remote_ui_put(ui, " ");
    904        }
    905      }
    906    }
    907  }
    908 }
    909 
    910 /// Flush the internal packing buffer to the client.
    911 ///
    912 /// This might happen multiple times before the actual ui_flush, if the
    913 /// total redraw size is large!
    914 static void ui_flush_buf(RemoteUI *ui, bool incomplete_event)
    915 {
    916  if (!ui->packer.startptr || !BUF_POS(ui)) {
    917    return;
    918  }
    919  ui->incomplete_event = incomplete_event;
    920 
    921  flush_event(ui);
    922  if (ui->nevents_pos != NULL) {
    923    mpack_w2(&ui->nevents_pos, ui->nevents);
    924    ui->nevents = 0;
    925    ui->nevents_pos = NULL;
    926  }
    927 
    928  WBuffer *buf = wstream_new_buffer(ui->packer.startptr, BUF_POS(ui), 1, free_block);
    929  rpc_write_raw(ui->channel_id, buf);
    930 
    931  ui->packer.startptr = NULL;
    932  ui->packer.ptr = NULL;
    933 
    934  // we have sent events to the client, but possibly not yet the final "flush" event.
    935  ui->flushed_events = true;
    936  ui->ncells_pending = 0;
    937 }
    938 
    939 /// An intentional flush (vsync) when Nvim is finished redrawing the screen
    940 ///
    941 /// Clients can know this happened by a final "flush" event at the end of the
    942 /// "redraw" batch.
    943 void remote_ui_flush(RemoteUI *ui)
    944 {
    945  if (ui->nevents > 0 || ui->flushed_events) {
    946    if (!ui->ui_ext[kUILinegrid]) {
    947      remote_ui_cursor_goto(ui, ui->cursor_row, ui->cursor_col);
    948    }
    949    push_call(ui, "flush", (Array)ARRAY_DICT_INIT);
    950    ui_flush_buf(ui, false);
    951    ui->flushed_events = false;
    952  }
    953 }
    954 
    955 void remote_ui_ui_send(RemoteUI *ui, String content)
    956 {
    957  if (!ui->stdout_tty) {
    958    return;
    959  }
    960 
    961  MAXSIZE_TEMP_ARRAY(args, 1);
    962  ADD_C(args, STRING_OBJ(content));
    963  push_call(ui, "ui_send", args);
    964 }
    965 
    966 void remote_ui_flush_pending_data(RemoteUI *ui)
    967 {
    968  ui_flush_buf(ui, false);
    969 }
    970 
    971 static Array translate_contents(RemoteUI *ui, Array contents, Arena *arena)
    972 {
    973  Array new_contents = arena_array(arena, contents.size);
    974  for (size_t i = 0; i < contents.size; i++) {
    975    Array item = contents.items[i].data.array;
    976    Array new_item = arena_array(arena, 2);
    977    int attr = (int)item.items[0].data.integer;
    978    if (attr) {
    979      Dict rgb_attrs = arena_dict(arena, HLATTRS_DICT_SIZE);
    980      hlattrs2dict(&rgb_attrs, NULL, syn_attr2entry(attr), ui->rgb, false);
    981      ADD_C(new_item, DICT_OBJ(rgb_attrs));
    982    } else {
    983      ADD_C(new_item, DICT_OBJ((Dict)ARRAY_DICT_INIT));
    984    }
    985    ADD_C(new_item, item.items[1]);
    986    ADD_C(new_contents, ARRAY_OBJ(new_item));
    987  }
    988  return new_contents;
    989 }
    990 
    991 static Array translate_firstarg(RemoteUI *ui, Array args, Arena *arena)
    992 {
    993  Array new_args = arena_array(arena, args.size);
    994  Array contents = args.items[0].data.array;
    995 
    996  ADD_C(new_args, ARRAY_OBJ(translate_contents(ui, contents, arena)));
    997  for (size_t i = 1; i < args.size; i++) {
    998    ADD_C(new_args, args.items[i]);
    999  }
   1000  return new_args;
   1001 }
   1002 
   1003 void remote_ui_event(RemoteUI *ui, char *name, Array args)
   1004 {
   1005  Arena arena = ARENA_EMPTY;
   1006  if (!ui->ui_ext[kUILinegrid]) {
   1007    // the representation of highlights in cmdline changed, translate back
   1008    // never consumes args
   1009    if (strequal(name, "cmdline_show")) {
   1010      Array new_args = translate_firstarg(ui, args, &arena);
   1011      push_call(ui, name, new_args);
   1012      goto free_ret;
   1013    } else if (strequal(name, "cmdline_block_show")) {
   1014      Array block = args.items[0].data.array;
   1015      Array new_block = arena_array(&arena, block.size);
   1016      for (size_t i = 0; i < block.size; i++) {
   1017        ADD_C(new_block, ARRAY_OBJ(translate_contents(ui, block.items[i].data.array, &arena)));
   1018      }
   1019      MAXSIZE_TEMP_ARRAY(new_args, 1);
   1020      ADD_C(new_args, ARRAY_OBJ(new_block));
   1021      push_call(ui, name, new_args);
   1022      goto free_ret;
   1023    } else if (strequal(name, "cmdline_block_append")) {
   1024      Array new_args = translate_firstarg(ui, args, &arena);
   1025      push_call(ui, name, new_args);
   1026      goto free_ret;
   1027    }
   1028  }
   1029 
   1030  // Back-compat: translate popupmenu_xx to legacy wildmenu_xx.
   1031  if (ui->ui_ext[kUIWildmenu]) {
   1032    if (strequal(name, "popupmenu_show")) {
   1033      ui->wildmenu_active = (args.items[4].data.integer == -1)
   1034                            || !ui->ui_ext[kUIPopupmenu];
   1035      if (ui->wildmenu_active) {
   1036        Array items = args.items[0].data.array;
   1037        Array new_items = arena_array(&arena, items.size);
   1038        for (size_t i = 0; i < items.size; i++) {
   1039          ADD_C(new_items, items.items[i].data.array.items[0]);
   1040        }
   1041        MAXSIZE_TEMP_ARRAY(new_args, 1);
   1042        ADD_C(new_args, ARRAY_OBJ(new_items));
   1043        push_call(ui, "wildmenu_show", new_args);
   1044        if (args.items[1].data.integer != -1) {
   1045          kv_size(new_args) = 0;
   1046          ADD_C(new_args, args.items[1]);
   1047          push_call(ui, "wildmenu_select", new_args);
   1048        }
   1049        goto free_ret;
   1050      }
   1051    } else if (strequal(name, "popupmenu_select")) {
   1052      if (ui->wildmenu_active) {
   1053        name = "wildmenu_select";
   1054      }
   1055    } else if (strequal(name, "popupmenu_hide")) {
   1056      if (ui->wildmenu_active) {
   1057        name = "wildmenu_hide";
   1058      }
   1059    }
   1060  }
   1061 
   1062  push_call(ui, name, args);
   1063  return;
   1064 
   1065 free_ret:
   1066  arena_mem_free(arena_finish(&arena));
   1067 }
   1068 
   1069 /// Sends arbitrary data to a UI. Use this instead of |nvim_chan_send()| or `io.stdout:write()`, if
   1070 /// you really want to write to the |TUI| host terminal.
   1071 ///
   1072 /// Emits a "ui_send" event to all UIs with the "stdout_tty" |ui-option| set. UIs are expected to
   1073 /// write the received data to a connected TTY if one exists.
   1074 ///
   1075 /// @param channel_id
   1076 /// @param content Content to write to the TTY
   1077 /// @param[out] err Error details, if any
   1078 void nvim_ui_send(uint64_t channel_id, String content, Error *err)
   1079  FUNC_API_SINCE(14)
   1080 {
   1081  ui_call_ui_send(content);
   1082 }