Skip to content

Commit 62c7191

Browse files
fix(inline): remove hl_mode combine from link extmarks to prevent ghost underlines
Remove hl_mode="combine" from the concealing extmarks in link_hyperlink and link_image. When a link URL is concealed, the virtual text highlight (e.g. underline) bled across every concealed byte, producing ghost underlines on phantom screen rows created by soft-wrap of the hidden text. Add inline conceal torture section to stress test.
1 parent 506e500 commit 62c7191

2 files changed

Lines changed: 28 additions & 4 deletions

File tree

lua/markview/renderers/markdown_inline.lua

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,11 @@ inline.link_hyperlink = function (buffer, item)
615615
hl_group = utils.set_hl(config.hl)
616616
});
617617

618+
--- NOTE: hl_mode must NOT be "combine" here. This extmark conceals
619+
--- the URL portion `](https://…)` which can span hundreds of bytes.
620+
--- With "combine" the virt_text highlight (e.g. underline) bleeds
621+
--- across every concealed byte, producing ghost underlines on the
622+
--- phantom screen rows created by soft-wrap of the hidden text.
618623
vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], {
619624
undo_restore = false, invalidate = true,
620625
end_row = range.row_end,
@@ -626,8 +631,6 @@ inline.link_hyperlink = function (buffer, item)
626631
{ config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) },
627632
{ config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }
628633
},
629-
630-
hl_mode = "combine"
631634
});
632635

633636
if r_label[1] == r_label[3] then
@@ -734,6 +737,8 @@ inline.link_image = function (buffer, item)
734737
hl_group = utils.set_hl(config.hl)
735738
});
736739

740+
--- NOTE: hl_mode must NOT be "combine" here — same reason as link_hyperlink.
741+
--- See the comment there for full explanation.
737742
vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], {
738743
undo_restore = false, invalidate = true,
739744
end_row = range.row_end,
@@ -745,8 +750,6 @@ inline.link_image = function (buffer, item)
745750
{ config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) },
746751
{ config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }
747752
},
748-
749-
hl_mode = "combine"
750753
});
751754

752755
if r_label[1] == r_label[3] then

test/stress.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ Here's a stress test for your markdown renderer:
1212
| `inline code` | ✅ Done | [ref](https://spec.commonmark.org/0.31.2/#code-spans-backtick-strings-and-their-matching-rules-for-inline-code) |
1313
| Nested lists | 🔧 WIP | [deep](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#nested-lists-ordered-and-unordered-mixing-indentation-levels) |
1414

15+
### Inline Conceal Torture
16+
17+
| Kind | Example | With long URL |
18+
|------|---------|---------------|
19+
| Hyperlink | [short](https://example.com) | [Neovim API reference](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()-nvim_buf_del_extmark()-nvim_buf_get_extmarks()-and-related-extmark-functions) |
20+
| Image | ![icon](https://example.com/icon.svg) | ![screenshot of the full treesitter playground](https://raw.githubusercontent.com/nvim-treesitter/playground/master/assets/screenshot-with-custom-queries-and-hl-groups.png) |
21+
| URI autolink | <https://example.com> | <https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz#very-long-anchor> |
22+
| Email autolink | <user@example.com> | <very-long-username-for-testing-purposes@extremely-long-subdomain.mail.example.co.uk> |
23+
| Inline code | `short` | `vim.api.nvim_buf_set_extmark(buffer, ns, row, col, opts)` |
24+
| Highlight | ==marked== | ==this is a rather long highlighted span that should test wrapping== |
25+
| Entity | &amp; and &lt; | &amp; &lt; &gt; &rarr; &larr; &hearts; &infin; &mdash; |
26+
| Escaped | \* not bold \* | \* \[ \] \( \) \` \~ \\ \# \! |
27+
| Emoji | :rocket: launch | :tada: :sparkles: :rocket: :fire: :bug: :memo: :bulb: :wrench: |
28+
| Footnote | see [^1] | see [^long-descriptive-footnote-name-that-tests-width] |
29+
| Bold + link | **[bold link](https://example.com)** | **[bold link with long URL](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis-combined-with-links-and-images)** |
30+
| Code + link | `code` then [link](https://a.co) | `vim.api.nvim_buf_set_extmark()` then [docs](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()-full-details) |
31+
| Multi-conceal | **bold** `code` *italic* [lnk](https://x.co) | **bold** `code` *ital* ==hl== [lnk](https://neovim.io/doc/user/api.html#multi-conceal-stress-test-row) :rocket: |
32+
33+
[^1]: A short footnote.
34+
[^long-descriptive-footnote-name-that-tests-width]: This footnote has a very long reference label to test how concealment handles it in table cells.
35+
1536
### Alignment Torture
1637

1738
| Left | Center | Right | Mixed |

0 commit comments

Comments
 (0)