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 }