helix icon indicating copy to clipboard operation
helix copied to clipboard

Support different kinds of underline rendering (updated)

Open pascalkuthe opened this issue 3 years ago • 21 comments

Supersedes #3015 (and closes #2809).

I have rebased the original PR on master and addressed the issues raised in the original PR. The reasons for the disappearing undercurls/underlines was that the orignal PR treated them as modifiers. However, underlines styles behave more like colors than modifiers: There are multiple different underline styles that are incompatible, overwrite each other and are removed with a single escape sequence. Modifiers on the other hand can be added and removed independent of other modifiers.

Tracking underline styles in the modifier set can not work correctly. The fix suggested by @archseer of only emitting NoUnderline if there is no underline left in to does not work for nested highlights. Consider the following example of 3 nested highlights (underline used by syntax highlight, underdotted used by diagnostic, undercurl used by cursor):

The input highlight ranges look like this (which correspond to the modifier sets)

  ~
 ...
______
foobar

The expected output looks like this:

foobar
_.~.__

The corresponding escape sequences look like this:

[_]f[.]o[~]o[.]b[_]ar[nounderline]

From the modifier set it is not possible to build these escape sequences. Because the modifier set only contains which modifiers are present but not the order in which they were added removed (so after the foo it's impossible to determine which underline should be shown).

This example might seem contrived, but I noticed this almost immediately in real world use. To fix the problems in the original PR I renamed the underline style attribute to underline_color and added the underline_style attribute for underline styles. Now underline styles are tracked through the same mechanisms that fg/bg/underline-color are tracked and therefore cases with nested highlights are handled correctly.

To esnure backwards compatibility of themes the underlined modifier will keep working and is equivalent to underline-style="line"

Edit: This updated PR also improves upon the original PR by using a much more robust terminfo parsing library (called termini written by me), because cxterminfo was barely tested and caused crashes. Furthermore a workaround for an issue in crossterm was implemented that caused visual artifacts in terminal emulators that do not support underline colors.

pascalkuthe avatar Oct 01 '22 15:10 pascalkuthe

cxterminfo is panicking for me, on MacOS, iTerm2 v3.4.16

thread 'main' panicked at 'attempt to add with overflow', /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:319:27

Omnikar avatar Oct 02 '22 17:10 Omnikar

That's good to know. I personally don't own a Mac. Could you provide a back trace and the panic message so I can try to debug this?

pascalkuthe avatar Oct 02 '22 17:10 pascalkuthe

Here's the output with RUST_BACKTRACE=1

Panic output
thread 'main' panicked at 'attempt to add with overflow', /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:319:27
stack backtrace:
   0: rust_begin_unwind
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/panicking.rs:142:14
   2: core::panicking::panic
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/panicking.rs:48:5
   3: cxterminfo::terminfo::TermInfo::from_data
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:319:27
   4: cxterminfo::terminfo::TermInfo::from_file
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:240:9
   5: cxterminfo::terminfo::TermInfo::from_name
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:231:24
   6: cxterminfo::terminfo::TermInfo::from_env
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/cxterminfo-0.2.0/src/terminfo.rs:192:13
   7: helix_tui::backend::crossterm::Capabilities::from_env_or_default
             at ./helix-tui/src/backend/crossterm.rs:30:15
   8: helix_tui::backend::crossterm::CrosstermBackend<W>::new
             at ./helix-tui/src/backend/crossterm.rs:55:27
   9: helix_term::compositor::Compositor::new
             at ./helix-term/src/compositor.rs:93:23
  10: helix_term::application::Application::new
             at ./helix-term/src/application.rs:142:30
  11: hx::main_impl::{{closure}}
             at ./helix-term/src/main.rs:143:19
  12: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/future/mod.rs:91:19
  13: tokio::park::thread::CachedParkThread::block_on::{{closure}}
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/park/thread.rs:267:54
  14: tokio::coop::with_budget::{{closure}}
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/coop.rs:102:9
  15: std::thread::local::LocalKey<T>::try_with
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/std/src/thread/local.rs:445:16
  16: std::thread::local::LocalKey<T>::with
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/std/src/thread/local.rs:421:9
  17: tokio::coop::with_budget
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/coop.rs:95:5
  18: tokio::coop::budget
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/coop.rs:72:5
  19: tokio::park::thread::CachedParkThread::block_on
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/park/thread.rs:267:31
  20: tokio::runtime::enter::Enter::block_on
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/runtime/enter.rs:152:13
  21: tokio::runtime::scheduler::multi_thread::MultiThread::block_on
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/runtime/scheduler/multi_thread/mod.rs:79:9
  22: tokio::runtime::Runtime::block_on
             at /Users/redacted/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.1/src/runtime/mod.rs:492:44
  23: hx::main_impl
             at ./helix-term/src/main.rs:147:5
  24: hx::main
             at ./helix-term/src/main.rs:37:21
  25: core::ops::function::FnOnce::call_once
             at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/ops/function.rs:248:5

Omnikar avatar Oct 02 '22 17:10 Omnikar

That looks like an upstream bug :/ Digging into this one will be a pain although it doesn't look specifically MacOS related. I don't have the time to dig into terminfo parsing right now but will try to really dig into this the next few days.

While looking into this, I noticed that the cxterminfo crate (where this bug occurred) is quite well documented (and is only like 500LOC) but has basically no tests or reverse dependencies. Using this crate was decided upon in the previous PR because the alternatives (terminfo) have more dependencies. I assumed due diligence was done and didn't recheck the dependencies but they probably just missed that.

I will probably have to inline that create, really understand how it works and then write a bunch of tests because using a basically untested crate will not do. Marking this as draft until then.

Thanks for pointing my attention to this

pascalkuthe avatar Oct 02 '22 18:10 pascalkuthe

Looking at cxterminfo made very uncomfortable with including it in helix. The code is quite clearly the first (or one of the first) rust projects of the author. For example many std functions are reimplemented in the crate and the code is written in a very panicky manner (tons of slice indexing instead of using the Read trait). In many places a naive but insufficient solutions were implemented (for example looking up the home directory using $HOME instead of using dirs-next). Most importantly the code was basically untested (and without any dependencies on crates.io) and was not very nice to read.

The code is simple enough that I choose to rewrite a small terminfo parsing library myself instead of trying to contribute to cxterminfo. I have published termini to crates.io and updated this PR to use it. Like cxterminfo, termini is focused on providing a low-dependency terminfo libary. It only has a single dependency (dirs-next) and a very small footprint (less than 1k loc). However, termini is written using idiomatic rust to avoid panics and ensure correct behavior. I have heavily fuzzed the implementation, so the parser should basically never panic even for completely nonsensical input. I have added a ton of different terminfo files as test (including detecting undercurl support from kitty and alacritty).

Could you confirm that this is now working for you @Omnikar?

pascalkuthe avatar Oct 06 '22 19:10 pascalkuthe

It's not panicking, but I am getting this weird dimming effect, and no coloured underlines (although I'm not sure whether iTerm2 supports coloured underlines? But that should be detected shouldn't it?): image

Omnikar avatar Oct 06 '22 20:10 Omnikar

I just tested with Kitty and confirmed that it works as expected, so I'm not sure what's going on with iTerm2.

Omnikar avatar Oct 06 '22 20:10 Omnikar

It's not panicking, but I am getting this weird dimming effect, ...

Yes I've also had that in alacritty.

I had to update to alacritty on master to get rid of this issue (as there isn't support for underline coloring in the current stable version yet (https://github.com/alacritty/alacritty/pull/5935 has fixed that though)).

Interesting that the issue is similar in ITerm2.

I'm not sure if it's possible to check, whether the terminal supports underline coloring or not.

Philipp-M avatar Oct 06 '22 21:10 Philipp-M

Hmm this should indeed be detected with terminfo. Perhaphs we are checking the wrong capabilities here. But atleast the capability parsing is working as expected :D

pascalkuthe avatar Oct 06 '22 21:10 pascalkuthe

Ok this was a bit of a ride... We did check the correct terminfo for underline style support. However there is no separate terminfo capability for colored underline support. This is normally not a problem because correct underline color escape sequences are ignored by terminal emulators that don't support them. The standard for underline colors dictates that colons are used as separators (instead of semicolons that are used for normal color escape sequences). The reason for this is to allow emulators that do not support underline colors to easily detect and ignore their escape sequences. crossterm incorrectly uses this semicolon syntax for underline colors. This is a common mixup and therefore editors that do support underline colors also support this syntax. However emulators that do not support underline colors will try to interpret this escape sequence leading leading instead of ignoring it. This leads to the weird visual artifacts you see.

For more details you can take a look at this nvim issue, the following quote is the most relevant here:

For undercurl, the newly invented escape sequence is 4:3 strictly with a colon, as with a semicolon it means single underlined and italic.

For colored underline, the newly invented escape sequence 58:... is meant to follow the pattern of 38 and 48. ITU T.416 § 13.1.8 clearly specifies the colon only as the separator (and the well-known ECMA-48 § 8.3.117 just points to this standard).

Using semicolon instead was/is a frequent misinterpretation of this standard, and is commonly used in the wild – for 38 and 48. More and more emulators are catching up and beginning to support colon, in addition to semicolon. Semicolon is pretty fragile; in case an emulator doesn't recognize a sequence (let's say doesn't recognize the new extension of 58), subsequent numbers are interpreted as other attributes. E.g. if 256-color mode is chosen then the next numeric parameter is 5 which turns on blinking.

To work around this I re implemented the SetUnderline color event from crossterm (just a couple lines of copy paste) with the correct seperators. I have confirmed that this works with alacritty v10. This should probably be fixed upstream at some point but this feature is surprisingly useful and I'd hate to have to wait for a new upstream version to ship this in helix.

Could you test that this now correctly works with iterm @Omnikar?

pascalkuthe avatar Oct 06 '22 22:10 pascalkuthe

Yep! It works fine now. The underlines render as the colour of the text, but they don't cause any unwanted visual artifacts.

Omnikar avatar Oct 07 '22 01:10 Omnikar

Perfect, that's the best we can do for terminals that don't support colored underlines

pascalkuthe avatar Oct 07 '22 09:10 pascalkuthe

Should this PR add underline theming to more of the themes?

Omnikar avatar Oct 07 '22 20:10 Omnikar

Should this PR add underline theming to more of the themes?

I have taught about this but decided against it. Instead I would rater leave that up to a future PR/each theme author individually. Themes can make different choices here on what underlines to use and colors and we would basically need a review from every theme author. Updating all themes would also take forever and would be super tedious to review so I would rather not do that in this PR

pascalkuthe avatar Oct 07 '22 22:10 pascalkuthe

Verified this is still working in wezterm!

The docs in this PR are unclear on if the underline-style and underline-color require the underline modifier to also be present.

EpocSquadron avatar Oct 08 '22 13:10 EpocSquadron

Verified this is still working in wezterm!

The docs in this PR are unclear on if the underline-style and underline-color require the underline modifier to also be present.

Thanks for testing it on wezterm!

The underline modifier is only supported for backwards compatibility with existing themes and not required. It is mapped to underline-style=line when the theme is parsed and does not exist as an actual modifier internally.

I have added a note to the docs to make that more clear

pascalkuthe avatar Oct 08 '22 14:10 pascalkuthe

This appears to also exhibit https://github.com/helix-editor/helix/pull/3015#issuecomment-1239657972: the styled underlines behave oddly when the cursor is moved across them and the cursor uses underline styling

https://user-images.githubusercontent.com/21230295/195217805-7ec79707-d365-4212-abb2-a6908bf7d12f.mp4

Relevant parts of the theme:

"ui.cursor.primary" = { bg = "bg3", modifiers = ["underlined"] }
"diagnostic.error" = { underline-color = "red1", underline-style = "curl" }

This is on Kitty 0.26.2 and wezterm 20220905-102802-7d4b8249

the-mikedavis avatar Oct 11 '22 23:10 the-mikedavis

This appears to also exhibit #3015 (comment): the styled underlines behave oddly when the cursor is moved across them and the cursor uses underline styling underline.mp4

Relevant parts of the theme:

"ui.cursor.primary" = { bg = "bg3", modifiers = ["underlined"] }
"diagnostic.error" = { underline-color = "red1", underline-style = "curl" }

This is on Kitty 0.26.2 and wezterm 20220905-102802-7d4b8249

I tested this case while originally working on the PR and it worked. However since then my work on the terminfo detection introduced a new bug that happened lead to the exact same behavior (although it was completely unrelated) and I did not retest tat case. I have fixed that bug so it should work now.

Part of the bug was (partly) caused by a typo in the terminfo searchpath of termini (my unit tests all use terminfo files inside the repo so that wasn't caught by the tests) so in some cases termini actually didn't parse the terminfo file at all. Could you confirm this still working with iterm @Omnikar, just in case that bug applied on your system aswell?

pascalkuthe avatar Oct 12 '22 09:10 pascalkuthe

Building from the latest, I see that being fixed on Kitty & WezTerm 👍

What do you think about having underline be a toml table?

"diagnostic.error" = { bg = "red0", underline = { color = "red1", style = "curl" } }

the-mikedavis avatar Oct 12 '22 23:10 the-mikedavis

What do you think about having underline be a toml table?

"diagnostic.error" = { bg = "red0", underline = { color = "red1", style = "curl" } }

I think this looks much nicer. I wasn't a big fan at first when I changed the themes because the most common use-case for these new attributes is diagnostics, which looked needlessly nested:

"diagnostic.error" = { underline = { color = "red", style = "curl"} }

However then I remembered that toml allows this:

"diagnostic.error".underline = { color = "red", style = "curl"} 

That looks is much nicer :) I have updated the PR.

On a slightly offtopic note. Wouldn't it be cleaner to alos implement highlight scopes like that so we could avaoid the quting and write:

diagnoistic.error.underline = {color = "red", style = "curl" }

pascalkuthe avatar Oct 13 '22 17:10 pascalkuthe

I think it might make the parsing or merging awkward if theme keys weren't strings

the-mikedavis avatar Oct 14 '22 01:10 the-mikedavis