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 | |
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')
-rw-r--r-- | src/nvim/api/extmark.c | 23 | ||||
-rw-r--r-- | src/nvim/api/keysets_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/api/ui.c | 8 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 6 | ||||
-rw-r--r-- | src/nvim/decoration.c | 27 | ||||
-rw-r--r-- | src/nvim/decoration_defs.h | 4 | ||||
-rw-r--r-- | src/nvim/highlight.c | 65 | ||||
-rw-r--r-- | src/nvim/highlight_defs.h | 8 | ||||
-rw-r--r-- | src/nvim/highlight_group.c | 4 | ||||
-rw-r--r-- | src/nvim/map_defs.h | 3 | ||||
-rw-r--r-- | src/nvim/terminal.c | 5 | ||||
-rw-r--r-- | src/nvim/tui/tui.c | 74 | ||||
-rw-r--r-- | src/nvim/ui_client.c | 9 |
13 files changed, 209 insertions, 29 deletions
diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 1f0e867162..27a4b7854f 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -481,6 +481,8 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// by a UI. When set, the UI will receive win_extmark events. /// Note: the mark is positioned by virt_text attributes. Can be /// used together with virt_text. +/// - url: A URL to associate with this extmark. In the TUI, the OSC 8 control +/// sequence is used to generate a clickable hyperlink to this URL. /// /// @param[out] err Error details, if any /// @return Id of the created/updated extmark @@ -494,6 +496,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer DecorSignHighlight sign = DECOR_SIGN_HIGHLIGHT_INIT; DecorVirtText virt_text = DECOR_VIRT_TEXT_INIT; DecorVirtText virt_lines = DECOR_VIRT_LINES_INIT; + char *url = NULL; bool has_hl = false; buf_T *buf = find_buffer_by_handle(buffer, err); @@ -678,6 +681,10 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer has_hl = true; } + if (HAS_KEY(opts, set_extmark, url)) { + url = string_to_cstr(opts->url); + } + if (opts->ui_watched) { hl.flags |= kSHUIWatched; if (virt_text.pos == kVPosOverlay) { @@ -747,6 +754,11 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer if (kv_size(virt_lines.data.virt_lines)) { decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true); } + if (url != NULL) { + DecorSignHighlight sh = DECOR_SIGN_HIGHLIGHT_INIT; + sh.url = url; + decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, 0, 0); + } if (has_hl) { DecorSignHighlight sh = decor_sh_from_inline(hl); decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id); @@ -772,7 +784,14 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer } uint32_t decor_indexed = DECOR_ID_INVALID; + if (url != NULL) { + DecorSignHighlight sh = DECOR_SIGN_HIGHLIGHT_INIT; + sh.url = url; + sh.next = decor_indexed; + decor_indexed = decor_put_sh(sh); + } if (sign.flags & kSHIsSign) { + sign.next = decor_indexed; decor_indexed = decor_put_sh(sign); if (sign.text[0]) { decor_flags |= MT_FLAG_DECOR_SIGNTEXT; @@ -814,6 +833,10 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer error: clear_virttext(&virt_text.data.virt_text); clear_virtlines(&virt_lines.data.virt_lines); + if (url != NULL) { + xfree(url); + } + return 0; } diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index b2f0039eb9..811f60f4d6 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -54,6 +54,7 @@ typedef struct { Boolean spell; Boolean ui_watched; Boolean undo_restore; + String url; } Dict(set_extmark); typedef struct { @@ -183,6 +184,7 @@ typedef struct { Boolean fg_indexed; Boolean bg_indexed; Boolean force; + String url; } Dict(highlight); typedef struct { diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index b42c274411..f955b315a8 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -784,6 +784,14 @@ void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs, HlAttrs cte MAXSIZE_TEMP_DICT(cterm, HLATTRS_DICT_SIZE); hlattrs2dict(&rgb, NULL, rgb_attrs, true, false); hlattrs2dict(&cterm, NULL, rgb_attrs, false, false); + + // URLs are not added in hlattrs2dict since they are used only by UIs and not by the highlight + // system. So we add them here. + if (rgb_attrs.url >= 0) { + const char *url = hl_get_url((uint32_t)rgb_attrs.url); + PUT_C(rgb, "url", STRING_OBJ(cstr_as_string((char *)url))); + } + ADD_C(args, DICTIONARY_OBJ(rgb)); ADD_C(args, DICTIONARY_OBJ(cterm)); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index e5a5cc059f..eea9b54a5c 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -176,6 +176,12 @@ void nvim_set_hl(Integer ns_id, String name, Dict(highlight) *val, Error *err) }); int link_id = -1; + // Setting URLs directly through highlight attributes is not supported + if (HAS_KEY(val, highlight, url)) { + api_free_string(val->url); + val->url = NULL_STRING; + } + HlAttrs attrs = dict2hlattrs(val, true, &link_id, err); if (!ERROR_SET(err)) { ns_hl_def((NS)ns_id, hl_id, attrs, link_id, val); diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index d3517c077f..7f1946ba05 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -118,7 +118,7 @@ void decor_redraw(buf_T *buf, int row1, int row2, DecorInline decor) void decor_redraw_sh(buf_T *buf, int row1, int row2, DecorSignHighlight sh) { - if (sh.hl_id || (sh.flags & (kSHIsSign|kSHSpellOn|kSHSpellOff))) { + if (sh.hl_id || (sh.url != NULL) || (sh.flags & (kSHIsSign|kSHSpellOn|kSHSpellOff))) { if (row2 >= row1) { redraw_buf_range_later(buf, row1 + 1, row2 + 1); } @@ -253,7 +253,7 @@ void decor_free(DecorInline decor) } } -void decor_free_inner(DecorVirtText *vt, uint32_t first_idx) +static void decor_free_inner(DecorVirtText *vt, uint32_t first_idx) { while (vt) { if (vt->flags & kVTIsLines) { @@ -273,6 +273,9 @@ void decor_free_inner(DecorVirtText *vt, uint32_t first_idx) xfree(sh->sign_name); } sh->flags = 0; + if (sh->url != NULL) { + XFREE_CLEAR(sh->url); + } if (sh->next == DECOR_ID_INVALID) { sh->next = decor_freelist; decor_freelist = first_idx; @@ -509,7 +512,8 @@ void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end .draw_col = -10, }; - if (sh->hl_id || (sh->flags & (kSHConceal | kSHSpellOn | kSHSpellOff))) { + if (sh->hl_id || (sh->url != NULL) + || (sh->flags & (kSHConceal | kSHSpellOn | kSHSpellOff))) { if (sh->hl_id) { range.attr_id = syn_id2attr(sh->hl_id); } @@ -627,15 +631,22 @@ next_mark: spell = kFalse; } } + if (active && item.data.sh.url != NULL) { + attr = hl_add_url(attr, item.data.sh.url); + } if (item.start_row == state->row && item.start_col <= col && decor_virt_pos(&item) && item.draw_col == -10) { decor_init_draw_col(win_col, hidden, &item); } if (keep) { kv_A(state->active, j++) = item; - } else if (item.owned && item.kind == kDecorKindVirtText) { - clear_virttext(&item.data.vt->data.virt_text); - xfree(item.data.vt); + } else if (item.owned) { + if (item.kind == kDecorKindVirtText) { + clear_virttext(&item.data.vt->data.virt_text); + xfree(item.data.vt); + } else if (item.kind == kDecorKindHighlight) { + xfree((void *)item.data.sh.url); + } } } kv_size(state->active) = j; @@ -962,6 +973,10 @@ void decor_to_dict_legacy(Dictionary *dict, DecorInline decor, bool hl_name) PUT(*dict, "ui_watched", BOOLEAN_OBJ(true)); } + if (sh_hl.url != NULL) { + PUT(*dict, "url", STRING_OBJ(cstr_to_string(sh_hl.url))); + } + if (virt_text) { if (virt_text->hl_mode) { PUT(*dict, "hl_mode", CSTR_TO_OBJ(hl_mode_str[virt_text->hl_mode])); diff --git a/src/nvim/decoration_defs.h b/src/nvim/decoration_defs.h index 93bc202b94..8d0075b169 100644 --- a/src/nvim/decoration_defs.h +++ b/src/nvim/decoration_defs.h @@ -3,6 +3,7 @@ #include <stdint.h> #include "klib/kvec.h" +#include "nvim/api/private/defs.h" #include "nvim/types_defs.h" #define DECOR_ID_INVALID UINT32_MAX @@ -68,10 +69,11 @@ typedef struct { int line_hl_id; int cursorline_hl_id; uint32_t next; + const char *url; } DecorSignHighlight; #define DECOR_SIGN_HIGHLIGHT_INIT { 0, DECOR_PRIORITY_BASE, 0, { 0, 0 }, NULL, 0, 0, 0, 0, \ - DECOR_ID_INVALID } + DECOR_ID_INVALID, NULL } enum { kVTIsLines = 1, diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index bd599d686f..47a87b90c3 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -42,6 +42,7 @@ static Set(HlEntry) attr_entries = SET_INIT; static Map(int, int) combine_attr_entries = MAP_INIT; static Map(int, int) blend_attr_entries = MAP_INIT; static Map(int, int) blendthrough_attr_entries = MAP_INIT; +static Set(cstr_t) urls = SET_INIT; #define attr_entry(i) attr_entries.keys[i] @@ -475,6 +476,7 @@ int hl_get_underline(void) .rgb_bg_color = -1, .rgb_sp_color = -1, .hl_blend = -1, + .url = -1, }, .kind = kHlUI, .id1 = 0, @@ -482,6 +484,43 @@ int hl_get_underline(void) }); } +/// Augment an existing attribute with the beginning or end of a URL hyperlink. +/// +/// @param attr Existing attribute to combine with +/// @param url The URL to associate with the highlight attribute +/// @return Combined attribute +int hl_add_url(int attr, const char *url) +{ + HlAttrs attrs = HLATTRS_INIT; + + MHPutStatus status; + uint32_t k = set_put_idx(cstr_t, &urls, url, &status); + if (status != kMHExisting) { + urls.keys[k] = xstrdup(url); + } + + attrs.url = (int32_t)k; + + int new = get_attr_entry((HlEntry){ + .attr = attrs, + .kind = kHlUI, + .id1 = 0, + .id2 = 0, + }); + + return hl_combine_attr(attr, new); +} + +/// Get a URL by its index. +/// +/// @param index URL index +/// @return URL +const char *hl_get_url(uint32_t index) +{ + assert(urls.keys); + return urls.keys[index]; +} + /// Get attribute code for forwarded :terminal highlights. int hl_get_term_attr(HlAttrs *aep) { @@ -492,12 +531,18 @@ int hl_get_term_attr(HlAttrs *aep) /// Clear all highlight tables. void clear_hl_tables(bool reinit) { + const char *url = NULL; + set_foreach(&urls, url, { + xfree((void *)url); + }); + if (reinit) { set_clear(HlEntry, &attr_entries); highlight_init(); map_clear(int, &combine_attr_entries); map_clear(int, &blend_attr_entries); map_clear(int, &blendthrough_attr_entries); + set_clear(cstr_t, &urls); memset(highlight_attr_last, -1, sizeof(highlight_attr_last)); highlight_attr_set_all(); highlight_changed(); @@ -508,6 +553,7 @@ void clear_hl_tables(bool reinit) map_destroy(int, &blend_attr_entries); map_destroy(int, &blendthrough_attr_entries); map_destroy(ColorKey, &ns_hls); + set_destroy(cstr_t, &urls); } } @@ -599,6 +645,11 @@ int hl_combine_attr(int char_attr, int prim_attr) new_en.hl_blend = prim_aep.hl_blend; } + if ((new_en.url == -1) && (prim_aep.url >= 0)) { + // Combined attributes borrow the string from the primary attribute + new_en.url = prim_aep.url; + } + id = get_attr_entry((HlEntry){ .attr = new_en, .kind = kHlCombine, .id1 = char_attr, .id2 = prim_attr }); if (id > 0) { @@ -680,8 +731,8 @@ int hl_blend_attrs(int back_attr, int front_attr, bool *through) } cattrs.cterm_bg_color = fattrs.cterm_bg_color; - cattrs.cterm_fg_color = cterm_blend(ratio, battrs.cterm_fg_color, - fattrs.cterm_bg_color); + cattrs.cterm_fg_color = (int16_t)cterm_blend(ratio, battrs.cterm_fg_color, + fattrs.cterm_bg_color); cattrs.rgb_ae_attr &= ~(HL_FG_INDEXED | HL_BG_INDEXED); } else { cattrs = fattrs; @@ -729,7 +780,7 @@ static int rgb_blend(int ratio, int rgb1, int rgb2) return (mr << 16) + (mg << 8) + mb; } -static int cterm_blend(int ratio, int c1, int c2) +static int cterm_blend(int ratio, int16_t c1, int16_t c2) { // 1. Convert cterm color numbers to RGB. // 2. Blend the RGB colors. @@ -1085,12 +1136,12 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e hlattrs.rgb_fg_color = fg; hlattrs.rgb_sp_color = sp; hlattrs.hl_blend = blend; - hlattrs.cterm_bg_color = ctermbg == -1 ? 0 : ctermbg + 1; - hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : ctermfg + 1; + hlattrs.cterm_bg_color = ctermbg == -1 ? 0 : (int16_t)(ctermbg + 1); + hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : (int16_t)(ctermfg + 1); hlattrs.cterm_ae_attr = cterm_mask; } else { - hlattrs.cterm_bg_color = bg == -1 ? 0 : bg + 1; - hlattrs.cterm_fg_color = fg == -1 ? 0 : fg + 1; + hlattrs.cterm_bg_color = bg == -1 ? 0 : (int16_t)(bg + 1); + hlattrs.cterm_fg_color = fg == -1 ? 0 : (int16_t)(fg + 1); hlattrs.cterm_ae_attr = mask; } diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h index 7a10e16391..25ab9dc2d9 100644 --- a/src/nvim/highlight_defs.h +++ b/src/nvim/highlight_defs.h @@ -3,6 +3,8 @@ #include <stdbool.h> #include <stdint.h> +#include "nvim/api/private/defs.h" + typedef int32_t RgbValue; /// Highlighting attribute bits. @@ -36,8 +38,9 @@ typedef enum { typedef struct { int16_t rgb_ae_attr, cterm_ae_attr; ///< HlAttrFlags RgbValue rgb_fg_color, rgb_bg_color, rgb_sp_color; - int cterm_fg_color, cterm_bg_color; - int hl_blend; + int16_t cterm_fg_color, cterm_bg_color; + int32_t hl_blend; + int32_t url; } HlAttrs; #define HLATTRS_INIT (HlAttrs) { \ @@ -49,6 +52,7 @@ typedef struct { .cterm_fg_color = 0, \ .cterm_bg_color = 0, \ .hl_blend = -1, \ + .url = -1, \ } /// Values for index in highlight_attr[]. diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 14c44b0249..ef8181a4cb 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -1912,8 +1912,8 @@ static void set_hl_attr(int idx) HlGroup *sgp = hl_table + idx; at_en.cterm_ae_attr = (int16_t)sgp->sg_cterm; - at_en.cterm_fg_color = sgp->sg_cterm_fg; - at_en.cterm_bg_color = sgp->sg_cterm_bg; + at_en.cterm_fg_color = (int16_t)sgp->sg_cterm_fg; + at_en.cterm_bg_color = (int16_t)sgp->sg_cterm_bg; at_en.rgb_ae_attr = (int16_t)sgp->sg_gui; // FIXME(tarruda): The "unset value" for rgb is -1, but since hlgroup is // initialized with 0(by garray functions), check for sg_rgb_{f,b}g_name diff --git a/src/nvim/map_defs.h b/src/nvim/map_defs.h index f3c4e4ea95..836b1447c2 100644 --- a/src/nvim/map_defs.h +++ b/src/nvim/map_defs.h @@ -33,7 +33,8 @@ static inline bool equal_String(String a, String b) if (a.size != b.size) { return false; } - return memcmp(a.data, b.data, a.size) == 0; + + return (a.size == 0) || (memcmp(a.data, b.data, a.size) == 0); } #define Set(type) Set_##type diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 8bee5071af..00acbfb602 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -913,8 +913,8 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te bool fg_indexed = VTERM_COLOR_IS_INDEXED(&cell.fg); bool bg_indexed = VTERM_COLOR_IS_INDEXED(&cell.bg); - int vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0); - int vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0); + int16_t vt_fg_idx = ((!fg_default && fg_indexed) ? cell.fg.indexed.idx + 1 : 0); + int16_t vt_bg_idx = ((!bg_default && bg_indexed) ? cell.bg.indexed.idx + 1 : 0); bool fg_set = vt_fg_idx && vt_fg_idx <= 16 && term->color_set[vt_fg_idx - 1]; bool bg_set = vt_bg_idx && vt_bg_idx <= 16 && term->color_set[vt_bg_idx - 1]; @@ -939,6 +939,7 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te .rgb_bg_color = vt_bg, .rgb_sp_color = -1, .hl_blend = -1, + .url = -1, }); } 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); diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c index 055e234d67..d4dec7db83 100644 --- a/src/nvim/ui_client.c +++ b/src/nvim/ui_client.c @@ -175,7 +175,14 @@ static HlAttrs ui_client_dict2hlattrs(Dictionary d, bool rgb) // TODO(bfredl): log "err" return HLATTRS_INIT; } - return dict2hlattrs(&dict, rgb, NULL, &err); + + 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) |