From cdd1537ecc002ca8593eb473f754e1cecb70f515 Mon Sep 17 00:00:00 2001 From: Immanuel Haffner Date: Fri, 20 Mar 2026 11:52:01 +0100 Subject: [PATCH 1/2] 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. --- lua/markview/renderers/markdown_inline.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lua/markview/renderers/markdown_inline.lua b/lua/markview/renderers/markdown_inline.lua index 4355494b..eb4d53bb 100644 --- a/lua/markview/renderers/markdown_inline.lua +++ b/lua/markview/renderers/markdown_inline.lua @@ -615,6 +615,11 @@ inline.link_hyperlink = function (buffer, item) hl_group = utils.set_hl(config.hl) }); + --- NOTE: hl_mode must NOT be "combine" here. This extmark conceals + --- the URL portion `](https://…)` which can span hundreds of bytes. + --- With "combine" the virt_text highlight (e.g. underline) bleeds + --- across every concealed byte, producing ghost underlines on the + --- phantom screen rows created by soft-wrap of the hidden text. vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], { undo_restore = false, invalidate = true, end_row = range.row_end, @@ -626,8 +631,6 @@ inline.link_hyperlink = function (buffer, item) { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) }, { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) } }, - - hl_mode = "combine" }); if r_label[1] == r_label[3] then @@ -734,6 +737,8 @@ inline.link_image = function (buffer, item) hl_group = utils.set_hl(config.hl) }); + --- NOTE: hl_mode must NOT be "combine" here — same reason as link_hyperlink. + --- See the comment there for full explanation. vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], { undo_restore = false, invalidate = true, end_row = range.row_end, @@ -745,8 +750,6 @@ inline.link_image = function (buffer, item) { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) }, { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) } }, - - hl_mode = "combine" }); if r_label[1] == r_label[3] then From 4df5fa76706d890c004f498e8d43e9c896f0792c Mon Sep 17 00:00:00 2001 From: Immanuel Haffner Date: Fri, 3 Jul 2026 15:15:42 +0000 Subject: [PATCH 2/2] test: add inline conceal + soft-wrap fixture for ghost underlines Repro for the hl_mode="combine" ghost-underline fix. Long concealed link/image URLs that soft-wrap create phantom screen rows; with hl_mode="combine" on the conceal extmark, the link highlight bleeds across every concealed byte and paints those rows. Open in a narrow window (the file carries a 'vim: set wrap linebreak' modeline) and confirm no stray underline/highlight appears on wrapped rows below the link labels. Covers long hyperlinks, images, links in narrow table cells, and several concealed links on one wrapping line. --- test/inline_conceal_wrap.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/inline_conceal_wrap.md diff --git a/test/inline_conceal_wrap.md b/test/inline_conceal_wrap.md new file mode 100644 index 00000000..f0f7cdf2 --- /dev/null +++ b/test/inline_conceal_wrap.md @@ -0,0 +1,29 @@ +; Concealed link/image URLs and soft-wrap — ghost underlines +The concealing extmark for a link/image hides the `](https://…)` portion, which +can span hundreds of bytes. When that hidden text is long enough to soft-wrap, +Neovim creates phantom screen rows for it. With `hl_mode = "combine"` on the +conceal extmark, the virtual-text highlight (e.g. the link underline) bleeds +across every concealed byte and paints those phantom rows, producing "ghost" +underlines/highlights on otherwise-empty wrapped rows. +To reproduce, open this file with `:setlocal wrap` in a NARROW window (e.g. +40–60 columns) so the long URLs would wrap if they weren't concealed. With the +fix, no stray underline/highlight should appear below the visible link labels; +the concealed URL must not leave a colored trail on wrapped rows. +### Long hyperlink URLs (primary repro) +- [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) +- [CommonMark emphasis rules](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis-combined-with-links-and-images) +- [short link for control](https://example.com) +### Long image URLs +- ![treesitter playground screenshot](https://raw.githubusercontent.com/nvim-treesitter/playground/master/assets/screenshot-with-custom-queries-and-hl-groups.png) +- ![short image control](https://example.com/icon.svg) +### Links inside table cells (conceal + wrap in narrow columns) +| Kind | With long URL | +|-------------|--------------------------------------------------------------------------------------------------------| +| Hyperlink | [Neovim API reference](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()-full-details) | +| Image | ![screenshot](https://raw.githubusercontent.com/nvim-treesitter/playground/master/assets/screenshot.png) | +| Bold + link | **[bold link with long URL](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis)** | +| Short (ctl) | [ref](https://a.co) | +### Multiple concealed links on one wrapping line +This paragraph has [first link](https://neovim.io/doc/user/api.html#first-very-long-anchor-for-wrapping) and [second link](https://neovim.io/doc/user/lua.html#second-very-long-anchor-for-wrapping) and [third link](https://neovim.io/doc/user/options.html#third-very-long-anchor) all on one line so their concealed URLs force soft-wrap. + +