Files
neovim/src/nvim/map_defs.h
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

237 lines
6.2 KiB
C

#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "nvim/api/private/defs.h"
#include "nvim/assert_defs.h"
#include "nvim/highlight_defs.h"
#include "nvim/types_defs.h"
#if defined(__NetBSD__)
# undef uint64_t
# define uint64_t uint64_t
#endif
typedef const char *cstr_t;
typedef void *ptr_t;
// when used as a key, String doesn't need to be NUL terminated,
// and can also contain embedded NUL:s as part of the data.
static inline uint32_t hash_String(String s)
{
uint32_t h = 0;
for (size_t i = 0; i < s.size; i++) {
h = (h << 5) - h + (uint8_t)s.data[i];
}
return h;
}
static inline bool equal_String(String a, String b)
{
if (a.size != b.size) {
return false;
}
return (a.size == 0) || (memcmp(a.data, b.data, a.size) == 0);
}
#define Set(type) Set_##type
#define Map(T, U) Map_##T##U
#define PMap(T) Map(T, ptr_t)
static const int value_init_int = 0;
static const ptr_t value_init_ptr_t = NULL;
static const ssize_t value_init_ssize_t = -1;
static const uint32_t value_init_uint32_t = 0;
static const uint64_t value_init_uint64_t = 0;
static const int64_t value_init_int64_t = 0;
static const String value_init_String = STRING_INIT;
static const ColorItem value_init_ColorItem = COLOR_ITEM_INITIALIZER;
// layer 0: type non-specific code
typedef struct {
uint32_t n_buckets;
uint32_t size;
uint32_t n_occupied;
uint32_t upper_bound;
uint32_t n_keys; // this is almost always "size", but keys[] could contain ded items..
uint32_t keys_capacity;
uint32_t *hash;
} MapHash;
#define MAPHASH_INIT { 0, 0, 0, 0, 0, 0, NULL }
#define SET_INIT { MAPHASH_INIT, NULL }
#define MAP_INIT { SET_INIT, NULL }
#define MH_TOMBSTONE UINT32_MAX
#define mh_is_empty(h, i) ((h)->hash[i] == 0)
#define mh_is_del(h, i) ((h)->hash[i] == MH_TOMBSTONE)
#define mh_is_either(h, i) ((uint32_t)((h)->hash[i] + 1U) <= 1U)
typedef enum {
kMHExisting = 0,
kMHNewKeyDidFit,
kMHNewKeyRealloc,
} MHPutStatus;
void mh_clear(MapHash *h);
void mh_realloc(MapHash *h, uint32_t n_min_buckets);
// layer 1: key type specific defs
// This is all need for sets.
#define MH_DECLS(T, K, K_query) \
typedef struct { \
MapHash h; \
K *keys; \
} Set(T); \
\
uint32_t mh_find_bucket_##T(Set(T) *set, K_query key, bool put); \
uint32_t mh_get_##T(Set(T) *set, K_query key); \
void mh_rehash_##T(Set(T) *set); \
uint32_t mh_put_##T(Set(T) *set, K_query key, MHPutStatus *new); \
#define KEY_DECLS(T) \
MH_DECLS(T, T, T) \
uint32_t mh_delete_##T(Set(T) *set, T *key); \
static inline bool set_put_##T(Set(T) *set, T key, T **key_alloc) { \
MHPutStatus status; \
uint32_t k = mh_put_##T(set, key, &status); \
if (key_alloc) { \
*key_alloc = &set->keys[k]; \
} \
return status != kMHExisting; \
} \
static inline T set_del_##T(Set(T) *set, T key) \
{ \
mh_delete_##T(set, &key); \
return key; \
} \
static inline bool set_has_##T(Set(T) *set, T key) { \
return mh_get_##T(set, key) != MH_TOMBSTONE; \
} \
// layer 2: key+value specific defs
// now we finally get Maps
#define MAP_DECLS(T, U) \
typedef struct { \
Set(T) set; \
U *values; \
} Map(T, U); \
static inline U map_get_##T##U(Map(T, U) *map, T key) \
{ \
uint32_t k = mh_get_##T(&map->set, key); \
return k == MH_TOMBSTONE ? value_init_##U : map->values[k]; \
} \
U *map_ref_##T##U(Map(T, U) *map, T key, T **key_alloc); \
U *map_put_ref_##T##U(Map(T, U) *map, T key, T **key_alloc, bool *new_item); \
static inline void map_put_##T##U(Map(T, U) *map, T key, U value) \
{ \
U *val = map_put_ref_##T##U(map, key, NULL, NULL); \
*val = value; \
} \
U map_del_##T##U(Map(T, U) *map, T key, T *key_alloc); \
// NOTE: Keys AND values must be allocated! Map and Set does not make a copy.
#define quasiquote(x, y) x##y
MH_DECLS(glyph, char, String)
KEY_DECLS(int)
KEY_DECLS(cstr_t)
KEY_DECLS(ptr_t)
KEY_DECLS(uint64_t)
KEY_DECLS(int64_t)
KEY_DECLS(uint32_t)
KEY_DECLS(String)
KEY_DECLS(HlEntry)
KEY_DECLS(ColorKey)
MAP_DECLS(int, int)
MAP_DECLS(int, ptr_t)
MAP_DECLS(cstr_t, ptr_t)
MAP_DECLS(cstr_t, int)
MAP_DECLS(ptr_t, ptr_t)
MAP_DECLS(uint32_t, ptr_t)
MAP_DECLS(uint64_t, ptr_t)
MAP_DECLS(uint64_t, ssize_t)
MAP_DECLS(uint64_t, uint64_t)
MAP_DECLS(int64_t, int64_t)
MAP_DECLS(int64_t, ptr_t)
MAP_DECLS(uint32_t, uint32_t)
MAP_DECLS(String, int)
MAP_DECLS(int, String)
MAP_DECLS(ColorKey, ColorItem)
#define set_has(T, set, key) set_has_##T(set, key)
#define set_put(T, set, key) set_put_##T(set, key, NULL)
#define set_put_ref(T, set, key, key_alloc) set_put_##T(set, key, key_alloc)
#define set_put_idx(T, set, key, status) mh_put_##T(set, key, status)
#define set_del(T, set, key) set_del_##T(set, key)
#define set_size(set) ((set)->h.size)
#define set_clear(T, set) mh_clear(&(set)->h)
#define set_destroy(T, set) \
do { \
xfree((set)->keys); \
xfree((set)->h.hash); \
*(set) = (Set(T)) SET_INIT; \
} while (0)
#define map_get(T, U) map_get_##T##U
#define map_has(T, map, key) set_has(T, &(map)->set, key)
#define map_put(T, U) map_put_##T##U
#define map_ref(T, U) map_ref_##T##U
#define map_put_ref(T, U) map_put_ref_##T##U
#define map_del(T, U) map_del_##T##U
#define map_size(map) set_size(&(map)->set)
#define map_clear(T, map) set_clear(T, &(map)->set)
#define map_destroy(T, map) \
do { \
set_destroy(T, &(map)->set); \
XFREE_CLEAR((map)->values); \
} while (0)
#define pmap_get(T) map_get(T, ptr_t)
#define pmap_put(T) map_put(T, ptr_t)
#define pmap_ref(T) map_ref(T, ptr_t)
#define pmap_put_ref(T) map_put_ref(T, ptr_t)
/// @see pmap_del2
#define pmap_del(T) map_del(T, ptr_t)
#define set_foreach(set, key, block) \
{ \
uint32_t __i; \
for (__i = 0; __i < (set)->h.n_keys; __i++) { \
(key) = (set)->keys[__i]; \
block; \
} \
}
#define map_foreach_key(map, key, block) set_foreach(&(map)->set, key, block)
#define map_foreach(map, key, value, code) \
{ \
uint32_t __i; \
for (__i = 0; __i < (map)->set.h.n_keys; __i++) { \
(key) = (map)->set.keys[__i]; \
(value) = (map)->values[__i]; \
code; \
} \
}
#define map_foreach_value(map, value, code) \
{ \
uint32_t __i; \
for (__i = 0; __i < (map)->set.h.n_keys; __i++) { \
(value) = (map)->values[__i]; \
code; \
} \
}
void pmap_del2(PMap(cstr_t) *map, const char *key);