Files
neovim/src/nvim/ui_client.c
Gregory Anders 6ea6b3fee2 feat(ui): add support for OSC 8 hyperlinks (#27109)
Extmarks can contain URLs which can then be drawn in any supporting UI.
In the TUI, for example, URLs are "drawn" by emitting the OSC 8 control
sequence to the TTY. On terminals which support the OSC 8 sequence this
will create clickable hyperlinks.

URLs are treated as inline highlights in the decoration subsystem, so
are included in the `DecorSignHighlight` structure. However, unlike
other inline highlights they use allocated memory which must be freed,
so they set the `ext` flag in `DecorInline` so that their lifetimes are
managed along with other allocated memory like virtual text.

The decoration subsystem then adds the URLs as a new highlight
attribute. The highlight subsystem maintains a set of unique URLs to
avoid duplicating allocations for the same string. To attach a URL to an
existing highlight attribute we call `hl_add_url` which finds the URL in
the set (allocating and adding it if it does not exist) and sets the
`url` highlight attribute to the index of the URL in the set (using an
index helps keep the size of the `HlAttrs` struct small).

This has the potential to lead to an increase in highlight attributes
if a URL is used over a range that contains many different highlight
attributes, because now each existing attribute must be combined with
the URL. In practice, however, URLs typically span a range containing a
single highlight (e.g. link text in Markdown), so this is likely just a
pathological edge case.

When a new highlight attribute is defined with a URL it is copied to all
attached UIs with the `hl_attr_define` UI event. The TUI manages its own
set of URLs (just like the highlight subsystem) to minimize allocations.
The TUI keeps track of which URL is "active" for the cell it is
printing. If no URL is active and a cell containing a URL is printed,
the opening OSC 8 sequence is emitted and that URL becomes the actively
tracked URL. If the cursor is moved while in the middle of a URL span,
we emit the terminating OSC sequence to prevent the hyperlink from
spanning multiple lines.

This does not support nested hyperlinks, but that is a rare (and,
frankly, bizarre) use case. If a valid use case for nested hyperlinks
ever presents itself we can address that issue then.
2024-01-24 16:36:25 -06:00

239 lines
6.5 KiB
C

/// Nvim's own UI client, which attaches to a child or remote Nvim server.
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include "nvim/api/keysets_defs.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/dispatch.h"
#include "nvim/api/private/helpers.h"
#include "nvim/channel.h"
#include "nvim/channel_defs.h"
#include "nvim/eval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/event/multiqueue.h"
#include "nvim/globals.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/log.h"
#include "nvim/main.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/msgpack_rpc/channel.h"
#include "nvim/msgpack_rpc/channel_defs.h"
#include "nvim/os/os_defs.h"
#include "nvim/tui/tui.h"
#include "nvim/tui/tui_defs.h"
#include "nvim/ui.h"
#include "nvim/ui_client.h"
#include "nvim/ui_defs.h"
#ifdef MSWIN
# include "nvim/os/os_win_console.h"
#endif
static TUIData *tui = NULL;
static bool ui_client_is_remote = false;
// uncrustify:off
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ui_client.c.generated.h"
# include "ui_events_client.generated.h"
#endif
// uncrustify:on
uint64_t ui_client_start_server(int argc, char **argv)
{
varnumber_T exit_status;
char **args = xmalloc(((size_t)(2 + argc)) * sizeof(char *));
int args_idx = 0;
args[args_idx++] = xstrdup(argv[0]);
args[args_idx++] = xstrdup("--embed");
for (int i = 1; i < argc; i++) {
args[args_idx++] = xstrdup(argv[i]);
}
args[args_idx++] = NULL;
CallbackReader on_err = CALLBACK_READER_INIT;
on_err.fwd_err = true;
Channel *channel = channel_job_start(args, get_vim_var_str(VV_PROGPATH),
CALLBACK_READER_INIT, on_err, CALLBACK_NONE,
false, true, true, false, kChannelStdinPipe,
NULL, 0, 0, NULL, &exit_status);
// If stdin is not a pty, it is forwarded to the client.
// Replace stdin in the TUI process with the tty fd.
if (ui_client_forward_stdin) {
close(0);
#ifdef MSWIN
os_open_conin_fd();
#else
dup(stderr_isatty ? STDERR_FILENO : STDOUT_FILENO);
#endif
}
return channel->id;
}
void ui_client_attach(int width, int height, char *term, bool rgb)
{
MAXSIZE_TEMP_ARRAY(args, 3);
ADD_C(args, INTEGER_OBJ(width));
ADD_C(args, INTEGER_OBJ(height));
MAXSIZE_TEMP_DICT(opts, 9);
PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb));
PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true));
PUT_C(opts, "ext_termcolors", BOOLEAN_OBJ(true));
if (term) {
PUT_C(opts, "term_name", CSTR_AS_OBJ(term));
}
PUT_C(opts, "term_colors", INTEGER_OBJ(t_colors));
if (!ui_client_is_remote) {
PUT_C(opts, "stdin_tty", BOOLEAN_OBJ(stdin_isatty));
PUT_C(opts, "stdout_tty", BOOLEAN_OBJ(stdout_isatty));
if (ui_client_forward_stdin) {
PUT_C(opts, "stdin_fd", INTEGER_OBJ(UI_CLIENT_STDIN_FD));
ui_client_forward_stdin = false; // stdin shouldn't be forwarded again #22292
}
}
ADD_C(args, DICTIONARY_OBJ(opts));
rpc_send_event(ui_client_channel_id, "nvim_ui_attach", args);
ui_client_attached = true;
}
void ui_client_detach(void)
{
rpc_send_event(ui_client_channel_id, "nvim_ui_detach", (Array)ARRAY_DICT_INIT);
ui_client_attached = false;
}
void ui_client_run(bool remote_ui)
FUNC_ATTR_NORETURN
{
ui_client_is_remote = remote_ui;
int width, height;
char *term;
bool rgb;
tui_start(&tui, &width, &height, &term, &rgb);
ui_client_attach(width, height, term, rgb);
// os_exit() will be invoked when the client channel detaches
while (true) {
LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1);
}
}
void ui_client_stop(void)
{
if (!tui_is_stopped(tui)) {
tui_stop(tui);
}
}
void ui_client_set_size(int width, int height)
{
// The currently known size will be sent when attaching
if (ui_client_attached) {
MAXSIZE_TEMP_ARRAY(args, 2);
ADD_C(args, INTEGER_OBJ((int)width));
ADD_C(args, INTEGER_OBJ((int)height));
rpc_send_event(ui_client_channel_id, "nvim_ui_try_resize", args);
}
}
UIClientHandler ui_client_get_redraw_handler(const char *name, size_t name_len, Error *error)
{
int hash = ui_client_handler_hash(name, name_len);
if (hash < 0) {
return (UIClientHandler){ NULL, NULL };
}
return event_handlers[hash];
}
/// Placeholder for _sync_ requests with 'redraw' method name
///
/// async 'redraw' events, which are expected when nvim acts as a ui client.
/// get handled in msgpack_rpc/unpacker.c and directly dispatched to handlers
/// of specific ui events, like ui_client_event_grid_resize and so on.
Object handle_ui_client_redraw(uint64_t channel_id, Array args, Arena *arena, Error *error)
{
api_set_error(error, kErrorTypeValidation, "'redraw' cannot be sent as a request");
return NIL;
}
static HlAttrs ui_client_dict2hlattrs(Dictionary d, bool rgb)
{
Error err = ERROR_INIT;
Dict(highlight) dict = { 0 };
if (!api_dict_to_keydict(&dict, KeyDict_highlight_get_field, d, &err)) {
// TODO(bfredl): log "err"
return HLATTRS_INIT;
}
HlAttrs attrs = dict2hlattrs(&dict, rgb, NULL, &err);
if (HAS_KEY(&dict, highlight, url)) {
attrs.url = tui_add_url(tui, dict.url.data);
}
return attrs;
}
void ui_client_event_grid_resize(Array args)
{
if (args.size < 3
|| args.items[0].type != kObjectTypeInteger
|| args.items[1].type != kObjectTypeInteger
|| args.items[2].type != kObjectTypeInteger) {
ELOG("Error handling ui event 'grid_resize'");
return;
}
Integer grid = args.items[0].data.integer;
Integer width = args.items[1].data.integer;
Integer height = args.items[2].data.integer;
tui_grid_resize(tui, grid, width, height);
if (grid_line_buf_size < (size_t)width) {
xfree(grid_line_buf_char);
xfree(grid_line_buf_attr);
grid_line_buf_size = (size_t)width;
grid_line_buf_char = xmalloc(grid_line_buf_size * sizeof(schar_T));
grid_line_buf_attr = xmalloc(grid_line_buf_size * sizeof(sattr_T));
}
}
void ui_client_event_grid_line(Array args)
FUNC_ATTR_NORETURN
{
abort(); // unreachable
}
void ui_client_event_raw_line(GridLineEvent *g)
{
int grid = g->args[0];
int row = g->args[1];
int startcol = g->args[2];
Integer endcol = startcol + g->coloff;
Integer clearcol = endcol + g->clear_width;
LineFlags lineflags = g->wrap ? kLineFlagWrap : 0;
tui_raw_line(tui, grid, row, startcol, endcol, clearcol, g->cur_attr, lineflags,
(const schar_T *)grid_line_buf_char, grid_line_buf_attr);
}
#ifdef EXITFREE
void ui_client_free_all_mem(void)
{
tui_free_all_mem(tui);
xfree(grid_line_buf_char);
xfree(grid_line_buf_attr);
}
#endif