aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/highlight.c
diff options
context:
space:
mode:
authorGregory Anders <8965202+gpanders@users.noreply.github.com>2024-01-24 16:36:25 -0600
committerGitHub <noreply@github.com>2024-01-24 16:36:25 -0600
commit6ea6b3fee27d51607ca4a5ace46dbc38a4481bcb (patch)
tree5768ae014d3fd33ad8309063b351ab010d046700 /src/nvim/highlight.c
parentf7bda77f9e8f2617868123457e852110304e70e1 (diff)
downloadrneovim-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/highlight.c')
-rw-r--r--src/nvim/highlight.c65
1 files changed, 58 insertions, 7 deletions
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;
}