Skip to content

Conversation

@gpanders
Copy link
Member

@gpanders gpanders commented Jan 20, 2024

Fixes: #11871

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 they are included in the DecorHighlightInline and DecorSignHighlight structures. 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.

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.


TODO:

  • Fix treesitter/highlight_spec.lua test
  • Update news.txt

@github-actions github-actions bot added tui termcodes, terminfo, termcap treesitter ui labels Jan 20, 2024
@gpanders gpanders force-pushed the extmark-uri-osc8 branch 2 times, most recently from 9d1678c to 4866f46 Compare January 20, 2024 19:18
@gpanders
Copy link
Member Author

Demo:

Screen.Recording.2024-01-19.at.9.43.09.PM.mov

DecorPriority priority;
int hl_id;
schar_T conceal_char;
String url;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As these need allocation anyway, there is no point in having them in DecorHighlightInline (as you note, these be converted to ext anyway so it is just waste of space here). better to use a code path more like line_hl_id etc to construct them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a90c842

Not sure how best to handle ephemeral extmarks with URLs now. When they are part of DecorHighlightInline they get converted in decor_sh_from_inline. I explicitly am adding a new DecorSignHighlight when sign.url.data is non null for ephemeral extmarks, which works, but maybe there is a better way.

Comment on lines 490 to 491
/// @param url The URL to associate with the highlight attribute. An empty string indicates the
/// end of a URL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be how it works in the TUI but in the highlight layer it is better to just say an empty string indicates the lack of an URL.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is out of date now and reflects a previous implementation, thanks for catching it. Will update the comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hlattrs.cterm_ae_attr = mask;
}

if (HAS_KEY_X(dict, url)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this actually used anywhere? in the API layer the PR guards against this, but this breaks the memoization allocation pattern used above and for future usages it is better to leave a missing implementation to fill in, than a broken one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used when called from ui_client_dict2hlattrs. Commenting this out makes the new test in tui_spec fail.

We could move these lines from dict2hlattrs into ui_client_dict2hlattrs so that it only affects the TUI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ColinKennedy
Copy link
Contributor

ColinKennedy commented Jan 21, 2024

@gpanders will this new feature be able to parse and process RFC 3986 URIs? Specifically, what I mean is that a URI may point to a website URL or to some other resource like a file on-disk, or something else. Will users be able to customize the "do it" functionality of these URIs?

Currently I'm able to use gf to "go to" a URI such as thing:/FOO(bar)/FIZZ(buzz)?version=10 by overriding 'includeexpr and 'isfname' to recognize these URI sequences. And the "do it" in this case is to open a file on disk or download a byte payload from a website. All vimscript + Python APIs. I was guessing that this PR relies on tree-sitter to do the parsing (so no 'isfname' needed in this case) but does this PR allow the user to customize what happens when a URL is found like with 'includeexpr'?

@gpanders
Copy link
Member Author

Will users be able to customize the "do it" functionality of these URIs?

No, all this does is write a control sequence to your terminal emulator. See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda. The terminal controls what happens when you click on the link, not Neovim.


if (attrs.url.data != NULL) {
StringBuilder sb = KV_INITIAL_VALUE;
kv_printf(sb, "\x1b]8;;%s\x1b\\", attrs.url.data);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should include an id here. attr_id maybe?

Suggested change
kv_printf(sb, "\x1b]8;;%s\x1b\\", attrs.url.data);
kv_printf(sb, "\x1b]8;id=%d;%s\x1b\\", attr_id, attrs.url.data);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky. attr_id will make the terminal highlight all URLs with the same attr_id, which we probably don't want. We don't really have a way to uniquely identify each individual hyperlink right now. We could use something like grid position where the URL starts, but don't have a way to easily get that information at the moment.

For now I'll omit the id, and we can solve this if/when it becomes a problem.

Comment on lines 734 to 739
if (tui->url.data != NULL) {
out(tui, S_LEN("\x1b]8;;\x1b\\"));
}

if (attrs.url.data != NULL) {
StringBuilder sb = KV_INITIAL_VALUE;
kv_printf(sb, "\x1b]8;;%s\x1b\\", attrs.url.data);
out(tui, sb.items, sb.size);
kv_destroy(sb);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are starting a new URL the old URL (if it exists) does not need to be terminated. It works just like any other SGR sequence (e.g. you don't need to "terminate" one color before starting another).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gpanders
Copy link
Member Author

The treesitter changes are non-trivial and will require further discussion so I'm moving those out of this PR.

@clason clason added the ci:s390x Enable CI for s390x label Jan 23, 2024
@zeertzjq
Copy link
Member

This should documented in the docs of hl_attr_define

@gpanders
Copy link
Member Author

This should documented in the docs of hl_attr_define

d80ac48

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.
Reduces size to 32 bytes per HlAttr
@gpanders gpanders merged commit 6ea6b3f into neovim:master Jan 24, 2024
@gpanders gpanders deleted the extmark-uri-osc8 branch January 24, 2024 22:36
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))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to add a kSHIsURL flag instead of checking sh.url? (Sorry for the late review was waiting if bfredl had something to say about it TBH.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it but decided against it because it's duplicating information, so no need to waste a flag. "Non null URL" is the flag, in a sense.

I am not opposed to it though if there are other reasons to use a flag.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than these checks being more readable if it just used flags I don't think so. But it's so only checked a few times and sh.hl_id is checked directly as well so probably doesn't matter much :)

glepnir pushed a commit to glepnir/neovim that referenced this pull request Mar 31, 2024
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.
@Batkow

This comment was marked as off-topic.

@clason

This comment was marked as off-topic.

@Batkow

This comment was marked as off-topic.

@clason
Copy link
Member

clason commented May 3, 2024

No, but you actually need to set the extmark property on the url you want to click; this is not automatic. Please ask usage questions in Chat (or in a Discussion).

@neovim neovim locked as resolved and limited conversation to collaborators May 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ci:s390x Enable CI for s390x tui termcodes, terminfo, termcap ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI: support for hyperlinks (OSC-8)

7 participants