mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
feat(term): support OSC 8 hyperlinks in :terminal (#30050)
This commit is contained in:
parent
f3677c71f0
commit
4199671047
@ -166,6 +166,8 @@ TERMINAL
|
|||||||
• The terminal buffer now supports reflow (wrapped lines adapt when the buffer
|
• The terminal buffer now supports reflow (wrapped lines adapt when the buffer
|
||||||
is resized horizontally). Note: Lines that are not visible and kept in
|
is resized horizontally). Note: Lines that are not visible and kept in
|
||||||
'scrollback' are not reflown.
|
'scrollback' are not reflown.
|
||||||
|
• The |terminal| now supports OSC 8 escape sequences and will display
|
||||||
|
hyperlinks in supporting host terminals.
|
||||||
|
|
||||||
TREESITTER
|
TREESITTER
|
||||||
|
|
||||||
|
@ -227,11 +227,66 @@ static void schedule_termrequest(Terminal *term, char *payload, size_t payload_l
|
|||||||
term->pending_send);
|
term->pending_send);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int parse_osc8(VTermStringFragment frag, int *attr)
|
||||||
|
FUNC_ATTR_NONNULL_ALL
|
||||||
|
{
|
||||||
|
// Parse the URI from the OSC 8 sequence and add the URL to our URL set.
|
||||||
|
// Skip the ID, we don't use it (for now)
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < frag.len; i++) {
|
||||||
|
if (frag.str[i] == ';') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move past the semicolon
|
||||||
|
i++;
|
||||||
|
|
||||||
|
if (i >= frag.len) {
|
||||||
|
// Invalid OSC sequence
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the terminator
|
||||||
|
const size_t start = i;
|
||||||
|
for (; i < frag.len; i++) {
|
||||||
|
if (frag.str[i] == '\a' || frag.str[i] == '\x1b') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t len = i - start;
|
||||||
|
if (len == 0) {
|
||||||
|
// Empty OSC 8, no URL
|
||||||
|
*attr = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *url = xmemdupz(&frag.str[start], len + 1);
|
||||||
|
url[len] = 0;
|
||||||
|
*attr = hl_add_url(0, url);
|
||||||
|
xfree(url);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static int on_osc(int command, VTermStringFragment frag, void *user)
|
static int on_osc(int command, VTermStringFragment frag, void *user)
|
||||||
{
|
{
|
||||||
|
Terminal *term = user;
|
||||||
|
|
||||||
if (frag.str == NULL) {
|
if (frag.str == NULL) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command == 8) {
|
||||||
|
int attr = 0;
|
||||||
|
if (parse_osc8(frag, &attr)) {
|
||||||
|
VTermState *state = vterm_obtain_state(term->vt);
|
||||||
|
VTermValue value = { .number = attr };
|
||||||
|
vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!has_event(EVENT_TERMREQUEST)) {
|
if (!has_event(EVENT_TERMREQUEST)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -239,7 +294,7 @@ static int on_osc(int command, VTermStringFragment frag, void *user)
|
|||||||
StringBuilder request = KV_INITIAL_VALUE;
|
StringBuilder request = KV_INITIAL_VALUE;
|
||||||
kv_printf(request, "\x1b]%d;", command);
|
kv_printf(request, "\x1b]%d;", command);
|
||||||
kv_concat_len(request, frag.str, frag.len);
|
kv_concat_len(request, frag.str, frag.len);
|
||||||
schedule_termrequest(user, request.items, request.size);
|
schedule_termrequest(term, request.items, request.size);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -992,6 +1047,10 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cell.uri > 0) {
|
||||||
|
attr_id = hl_combine_attr(attr_id, cell.uri);
|
||||||
|
}
|
||||||
|
|
||||||
if (term->cursor.visible && term->cursor.row == row
|
if (term->cursor.visible && term->cursor.row == row
|
||||||
&& term->cursor.col == col) {
|
&& term->cursor.col == col) {
|
||||||
attr_id = hl_combine_attr(attr_id,
|
attr_id = hl_combine_attr(attr_id,
|
||||||
|
@ -182,6 +182,8 @@ INTERNAL void vterm_state_resetpen(VTermState *state)
|
|||||||
|
|
||||||
state->pen.fg = state->default_fg; setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg);
|
state->pen.fg = state->default_fg; setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg);
|
||||||
state->pen.bg = state->default_bg; setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg);
|
state->pen.bg = state->default_bg; setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg);
|
||||||
|
|
||||||
|
state->pen.uri = 0; setpenattr_int(state, VTERM_ATTR_URI, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL void vterm_state_savepen(VTermState *state, int save)
|
INTERNAL void vterm_state_savepen(VTermState *state, int save)
|
||||||
@ -205,6 +207,8 @@ INTERNAL void vterm_state_savepen(VTermState *state, int save)
|
|||||||
|
|
||||||
setpenattr_col( state, VTERM_ATTR_FOREGROUND, state->pen.fg);
|
setpenattr_col( state, VTERM_ATTR_FOREGROUND, state->pen.fg);
|
||||||
setpenattr_col( state, VTERM_ATTR_BACKGROUND, state->pen.bg);
|
setpenattr_col( state, VTERM_ATTR_BACKGROUND, state->pen.bg);
|
||||||
|
|
||||||
|
setpenattr_int( state, VTERM_ATTR_URI, state->pen.uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,9 +604,75 @@ int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue
|
|||||||
val->number = state->pen.baseline;
|
val->number = state->pen.baseline;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
|
case VTERM_ATTR_URI:
|
||||||
|
val->number = state->pen.uri;
|
||||||
|
return 1;
|
||||||
|
|
||||||
case VTERM_N_ATTRS:
|
case VTERM_N_ATTRS:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val)
|
||||||
|
{
|
||||||
|
if (!val) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type != vterm_get_attr_type(attr)) {
|
||||||
|
DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n",
|
||||||
|
attr, vterm_get_attr_type(attr), type);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (attr) {
|
||||||
|
case VTERM_ATTR_BOLD:
|
||||||
|
state->pen.bold = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_UNDERLINE:
|
||||||
|
state->pen.underline = val->number;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_ITALIC:
|
||||||
|
state->pen.italic = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_BLINK:
|
||||||
|
state->pen.blink = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_REVERSE:
|
||||||
|
state->pen.reverse = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_CONCEAL:
|
||||||
|
state->pen.conceal = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_STRIKE:
|
||||||
|
state->pen.strike = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_FONT:
|
||||||
|
state->pen.font = val->number;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_FOREGROUND:
|
||||||
|
state->pen.fg = val->color;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_BACKGROUND:
|
||||||
|
state->pen.bg = val->color;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_SMALL:
|
||||||
|
state->pen.small = val->boolean;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_BASELINE:
|
||||||
|
state->pen.baseline = val->number;
|
||||||
|
break;
|
||||||
|
case VTERM_ATTR_URI:
|
||||||
|
state->pen.uri = val->number;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(state->callbacks && state->callbacks->setpenattr)
|
||||||
|
(*state->callbacks->setpenattr)(attr, val, state->cbdata);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
@ -17,6 +17,9 @@ typedef struct
|
|||||||
/* After the bitfield */
|
/* After the bitfield */
|
||||||
VTermColor fg, bg;
|
VTermColor fg, bg;
|
||||||
|
|
||||||
|
/* Opaque ID that maps to a URI in a set */
|
||||||
|
int uri;
|
||||||
|
|
||||||
unsigned int bold : 1;
|
unsigned int bold : 1;
|
||||||
unsigned int underline : 2;
|
unsigned int underline : 2;
|
||||||
unsigned int italic : 1;
|
unsigned int italic : 1;
|
||||||
@ -444,6 +447,9 @@ static int setpenattr(VTermAttr attr, VTermValue *val, void *user)
|
|||||||
case VTERM_ATTR_BASELINE:
|
case VTERM_ATTR_BASELINE:
|
||||||
screen->pen.baseline = val->number;
|
screen->pen.baseline = val->number;
|
||||||
return 1;
|
return 1;
|
||||||
|
case VTERM_ATTR_URI:
|
||||||
|
screen->pen.uri = val->number;
|
||||||
|
return 1;
|
||||||
|
|
||||||
case VTERM_N_ATTRS:
|
case VTERM_N_ATTRS:
|
||||||
return 0;
|
return 0;
|
||||||
@ -705,6 +711,8 @@ static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new
|
|||||||
dst->pen.fg = src->fg;
|
dst->pen.fg = src->fg;
|
||||||
dst->pen.bg = src->bg;
|
dst->pen.bg = src->bg;
|
||||||
|
|
||||||
|
dst->pen.uri = src->uri;
|
||||||
|
|
||||||
if(src->width == 2 && pos.col < (new_cols-1))
|
if(src->width == 2 && pos.col < (new_cols-1))
|
||||||
(dst + 1)->chars[0] = (uint32_t) -1;
|
(dst + 1)->chars[0] = (uint32_t) -1;
|
||||||
}
|
}
|
||||||
@ -997,6 +1005,8 @@ int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCe
|
|||||||
cell->fg = intcell->pen.fg;
|
cell->fg = intcell->pen.fg;
|
||||||
cell->bg = intcell->pen.bg;
|
cell->bg = intcell->pen.bg;
|
||||||
|
|
||||||
|
cell->uri = intcell->pen.uri;
|
||||||
|
|
||||||
if(pos.col < (screen->cols - 1) &&
|
if(pos.col < (screen->cols - 1) &&
|
||||||
getcell(screen, pos.row, pos.col + 1)->chars[0] == (uint32_t)-1)
|
getcell(screen, pos.row, pos.col + 1)->chars[0] == (uint32_t)-1)
|
||||||
cell->width = 2;
|
cell->width = 2;
|
||||||
@ -1116,9 +1126,11 @@ static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b)
|
|||||||
return 1;
|
return 1;
|
||||||
if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg))
|
if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg))
|
||||||
return 1;
|
return 1;
|
||||||
if((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small))
|
if((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small))
|
||||||
return 1;
|
return 1;
|
||||||
if((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline))
|
if((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline))
|
||||||
|
return 1;
|
||||||
|
if((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -278,6 +278,7 @@ VTermValueType vterm_get_attr_type(VTermAttr attr)
|
|||||||
case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR;
|
case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR;
|
||||||
case VTERM_ATTR_SMALL: return VTERM_VALUETYPE_BOOL;
|
case VTERM_ATTR_SMALL: return VTERM_VALUETYPE_BOOL;
|
||||||
case VTERM_ATTR_BASELINE: return VTERM_VALUETYPE_INT;
|
case VTERM_ATTR_BASELINE: return VTERM_VALUETYPE_INT;
|
||||||
|
case VTERM_ATTR_URI: return VTERM_VALUETYPE_INT;
|
||||||
|
|
||||||
case VTERM_N_ATTRS: return 0;
|
case VTERM_N_ATTRS: return 0;
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,7 @@ typedef enum {
|
|||||||
VTERM_ATTR_BACKGROUND, // color: 40-49 100-107
|
VTERM_ATTR_BACKGROUND, // color: 40-49 100-107
|
||||||
VTERM_ATTR_SMALL, // bool: 73, 74, 75
|
VTERM_ATTR_SMALL, // bool: 73, 74, 75
|
||||||
VTERM_ATTR_BASELINE, // number: 73, 74, 75
|
VTERM_ATTR_BASELINE, // number: 73, 74, 75
|
||||||
|
VTERM_ATTR_URI, // number
|
||||||
|
|
||||||
VTERM_N_ATTRS
|
VTERM_N_ATTRS
|
||||||
} VTermAttr;
|
} VTermAttr;
|
||||||
@ -470,6 +471,7 @@ void vterm_state_set_default_colors(VTermState *state, const VTermColor *default
|
|||||||
void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col);
|
void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col);
|
||||||
void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright);
|
void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright);
|
||||||
int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val);
|
int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val);
|
||||||
|
int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val);
|
||||||
int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val);
|
int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val);
|
||||||
void vterm_state_focus_in(VTermState *state);
|
void vterm_state_focus_in(VTermState *state);
|
||||||
void vterm_state_focus_out(VTermState *state);
|
void vterm_state_focus_out(VTermState *state);
|
||||||
@ -529,6 +531,7 @@ typedef struct {
|
|||||||
char width;
|
char width;
|
||||||
VTermScreenCellAttrs attrs;
|
VTermScreenCellAttrs attrs;
|
||||||
VTermColor fg, bg;
|
VTermColor fg, bg;
|
||||||
|
int uri;
|
||||||
} VTermScreenCell;
|
} VTermScreenCell;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@ -589,8 +592,9 @@ typedef enum {
|
|||||||
VTERM_ATTR_CONCEAL_MASK = 1 << 9,
|
VTERM_ATTR_CONCEAL_MASK = 1 << 9,
|
||||||
VTERM_ATTR_SMALL_MASK = 1 << 10,
|
VTERM_ATTR_SMALL_MASK = 1 << 10,
|
||||||
VTERM_ATTR_BASELINE_MASK = 1 << 11,
|
VTERM_ATTR_BASELINE_MASK = 1 << 11,
|
||||||
|
VTERM_ATTR_URI_MASK = 1 << 12,
|
||||||
|
|
||||||
VTERM_ALL_ATTRS_MASK = (1 << 12) - 1
|
VTERM_ALL_ATTRS_MASK = (1 << 13) - 1
|
||||||
} VTermAttrMask;
|
} VTermAttrMask;
|
||||||
|
|
||||||
int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs);
|
int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs);
|
||||||
|
@ -40,6 +40,7 @@ struct VTermPen
|
|||||||
{
|
{
|
||||||
VTermColor fg;
|
VTermColor fg;
|
||||||
VTermColor bg;
|
VTermColor bg;
|
||||||
|
int uri;
|
||||||
unsigned int bold:1;
|
unsigned int bold:1;
|
||||||
unsigned int underline:2;
|
unsigned int underline:2;
|
||||||
unsigned int italic:1;
|
unsigned int italic:1;
|
||||||
|
@ -380,3 +380,23 @@ describe(':terminal highlight with custom palette', function()
|
|||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe(':terminal', function()
|
||||||
|
before_each(clear)
|
||||||
|
|
||||||
|
it('can display URLs', function()
|
||||||
|
local screen = Screen.new(50, 7)
|
||||||
|
screen:add_extra_attr_ids {
|
||||||
|
[100] = { url = 'https://example.com' },
|
||||||
|
}
|
||||||
|
screen:attach()
|
||||||
|
local chan = api.nvim_open_term(0, {})
|
||||||
|
api.nvim_chan_send(chan, '\027]8;;https://example.com\027\\Example\027]8;;\027\\')
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{100:^Example} |
|
||||||
|
|*6
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
Loading…
Reference in New Issue
Block a user