diff options
| author | Gregory Anders <8965202+gpanders@users.noreply.github.com> | 2024-01-24 16:36:25 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-24 16:36:25 -0600 |
| commit | 6ea6b3fee27d51607ca4a5ace46dbc38a4481bcb (patch) | |
| tree | 5768ae014d3fd33ad8309063b351ab010d046700 /src/nvim/tui | |
| parent | f7bda77f9e8f2617868123457e852110304e70e1 (diff) | |
| download | rneovim-6ea6b3fee27d51607ca4a5ace46dbc38a4481bcb.tar.gz rneovim-6ea6b3fee27d51607ca4a5ace46dbc38a4481bcb.tar.bz2 rneovim-6ea6b3fee27d51607ca4a5ace46dbc38a4481bcb.zip | |
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.
Diffstat (limited to 'src/nvim/tui')
| -rw-r--r-- | src/nvim/tui/tui.c | 74 |
1 files changed, 67 insertions, 7 deletions
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index f9560ce076..35867d6ce3 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -32,6 +32,7 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" +#include "nvim/strings.h" #include "nvim/tui/input.h" #include "nvim/tui/terminfo.h" #include "nvim/tui/tui.h" @@ -139,6 +140,8 @@ struct TUIData { int width; int height; bool rgb; + int url; ///< Index of URL currently being printed, if any + StringBuilder urlbuf; ///< Re-usable buffer for writing OSC 8 control sequences }; static int got_winch = 0; @@ -147,6 +150,8 @@ static bool cursor_style_enabled = false; # include "tui/tui.c.generated.h" #endif +static Set(cstr_t) urls = SET_INIT; + void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb) FUNC_ATTR_NONNULL_ALL { @@ -156,7 +161,9 @@ void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb) tui->stopped = false; tui->seen_error_exit = 0; tui->loop = &main_loop; + tui->url = -1; kv_init(tui->invalid_regions); + kv_init(tui->urlbuf); signal_watcher_init(tui->loop, &tui->winch_handle, tui); // TODO(bfredl): zero hl is empty, send this explicitly? @@ -412,7 +419,7 @@ static void terminfo_start(TUIData *tui) static void terminfo_stop(TUIData *tui) { // Destroy output stuff - tui_mode_change(tui, (String)STRING_INIT, SHAPE_IDX_N); + tui_mode_change(tui, NULL_STRING, SHAPE_IDX_N); tui_mouse_off(tui); unibi_out(tui, unibi_exit_attribute_mode); // Reset cursor to normal before exiting alternate screen. @@ -423,7 +430,7 @@ static void terminfo_stop(TUIData *tui) tui_reset_key_encoding(tui); // May restore old title before exiting alternate screen. - tui_set_title(tui, (String)STRING_INIT); + tui_set_title(tui, NULL_STRING); if (ui_client_exit_status == 0) { ui_client_exit_status = tui->seen_error_exit; } @@ -522,7 +529,15 @@ void tui_free_all_mem(TUIData *tui) { ugrid_free(&tui->grid); kv_destroy(tui->invalid_regions); + + const char *url; + set_foreach(&urls, url, { + xfree((void *)url); + }); + set_destroy(cstr_t, &urls); + kv_destroy(tui->attrs); + kv_destroy(tui->urlbuf); xfree(tui->space_buf); xfree(tui->term); xfree(tui); @@ -550,6 +565,10 @@ static bool attrs_differ(TUIData *tui, int id1, int id2, bool rgb) HlAttrs a1 = kv_A(tui->attrs, (size_t)id1); HlAttrs a2 = kv_A(tui->attrs, (size_t)id2); + if (a1.url != a2.url) { + return true; + } + if (rgb) { return a1.rgb_fg_color != a2.rgb_fg_color || a1.rgb_bg_color != a2.rgb_bg_color @@ -709,6 +728,19 @@ static void update_attrs(TUIData *tui, int attr_id) } } + if (tui->url != attrs.url) { + if (attrs.url >= 0) { + const char *url = urls.keys[attrs.url]; + kv_size(tui->urlbuf) = 0; + kv_printf(tui->urlbuf, "\x1b]8;;%s\x1b\\", url); + out(tui, tui->urlbuf.items, kv_size(tui->urlbuf)); + } else { + out(tui, S_LEN("\x1b]8;;\x1b\\")); + } + + tui->url = attrs.url; + } + tui->default_attr = fg == -1 && bg == -1 && !bold && !italic && !has_any_underline && !reverse && !standout && !strikethrough; @@ -785,6 +817,13 @@ static void cursor_goto(TUIData *tui, int row, int col) if (row == grid->row && col == grid->col) { return; } + + // If an OSC 8 sequence is active terminate it before moving the cursor + if (tui->url >= 0) { + out(tui, S_LEN("\x1b]8;;\x1b\\")); + tui->url = -1; + } + if (0 == row && 0 == col) { unibi_out(tui, unibi_cursor_home); ugrid_goto(grid, row, col); @@ -1281,11 +1320,32 @@ void tui_grid_scroll(TUIData *tui, Integer g, Integer startrow, Integer endrow, } } +/// Add a URL to be used in an OSC 8 hyperlink. +/// +/// @param tui TUIData +/// @param url URL to add +/// @return Index of new URL, or -1 if URL is invalid +int32_t tui_add_url(TUIData *tui, const char *url) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (url == NULL) { + return -1; + } + + MHPutStatus status; + uint32_t k = set_put_idx(cstr_t, &urls, url, &status); + if (status != kMHExisting) { + urls.keys[k] = xstrdup(url); + } + return (int32_t)k; +} + void tui_hl_attr_define(TUIData *tui, Integer id, HlAttrs attrs, HlAttrs cterm_attrs, Array info) { attrs.cterm_ae_attr = cterm_attrs.cterm_ae_attr; attrs.cterm_fg_color = cterm_attrs.cterm_fg_color; attrs.cterm_bg_color = cterm_attrs.cterm_bg_color; + kv_a(tui->attrs, (size_t)id) = attrs; } @@ -1302,11 +1362,11 @@ void tui_visual_bell(TUIData *tui) void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Integer rgb_sp, Integer cterm_fg, Integer cterm_bg) { - tui->clear_attrs.rgb_fg_color = (int)rgb_fg; - tui->clear_attrs.rgb_bg_color = (int)rgb_bg; - tui->clear_attrs.rgb_sp_color = (int)rgb_sp; - tui->clear_attrs.cterm_fg_color = (int)cterm_fg; - tui->clear_attrs.cterm_bg_color = (int)cterm_bg; + tui->clear_attrs.rgb_fg_color = (RgbValue)rgb_fg; + tui->clear_attrs.rgb_bg_color = (RgbValue)rgb_bg; + tui->clear_attrs.rgb_sp_color = (RgbValue)rgb_sp; + tui->clear_attrs.cterm_fg_color = (int16_t)cterm_fg; + tui->clear_attrs.cterm_bg_color = (int16_t)cterm_bg; tui->print_attr_id = -1; invalidate(tui, 0, tui->grid.height, 0, tui->grid.width); |