diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 8f4ba5a9a9..e437611c77 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -800,6 +800,16 @@ static void draw_statuscol(win_T *wp, winlinevars_T *wlv, int col_rows, statusco draw_col_fill(wlv, schar_from_ascii(' '), stcp->width - width, cur_attr); } +/// Whether "attr" draws an underline, undercurl, strikethrough, or overline: a line tied to text +/// glyphs that looks broken drawn over blank filler cells, unlike a plain background/reverse-video +/// highlight. +static bool attr_has_line_deco(int attr) +{ + HlAttrs ae = syn_attr2entry(attr); + int32_t const mask = HL_UNDERLINE_MASK | HL_STRIKETHROUGH | HL_OVERLINE; + return (ae.rgb_ae_attr & mask) != 0 || (ae.cterm_ae_attr & mask) != 0; +} + static void handle_breakindent(win_T *wp, winlinevars_T *wlv) { // draw 'breakindent': indent wrapped text accordingly @@ -2403,11 +2413,21 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b wlv.n_extra = win_charsize(cstype, wlv.vcol, p, utf_ptr2CharInfo(p).value, &csarg).width - 1; - if (on_last_col && mb_c != TAB) { - // Do not continue search/match highlighting over the - // line break, but for TABs the highlighting should - // include the complete width of the character - search_attr = 0; + // Do not bleed an underline/strikethrough/overline into the filler for the pushed-down + // word (TABs keep their own full-width highlight): drawn over blank cells, they look like + // a broken line. A plain background/reverse-video highlight does not have this problem, + // so leave it be. search_attr also resets when the search/match's own span ends exactly + // here (on_last_col), matching its pre-existing semantics. + if (mb_c != TAB) { + if (on_last_col || attr_has_line_deco(search_attr)) { + search_attr = 0; + } + if (attr_has_line_deco(decor_attr)) { + decor_attr = 0; + } + if (attr_has_line_deco(area_attr)) { + area_attr = 0; + } } if (mb_c == TAB && wlv.n_extra + wlv.col > view_width) { diff --git a/test/functional/legacy/listlbr_spec.lua b/test/functional/legacy/listlbr_spec.lua index c5dedd54cf..a4dfad6cf7 100644 --- a/test/functional/legacy/listlbr_spec.lua +++ b/test/functional/legacy/listlbr_spec.lua @@ -6,7 +6,7 @@ local Screen = require('test.functional.ui.screen') local describe, it, before_each = t.describe, t.it, t.before_each local feed, insert, source = n.feed, n.insert, n.source -local clear, feed_command, expect = n.clear, n.feed_command, n.expect +local clear, command, feed_command, expect = n.clear, n.command, n.feed_command, n.expect describe('listlbr', function() before_each(clear) @@ -274,4 +274,81 @@ describe('listlbr', function() {9:E490: No fold found} | ]]) end) + + it('does not bleed match/extmark highlight into the filler before a wrapped word', function() + local screen = Screen.new(60, 3) + screen:add_extra_attr_ids({ [100] = { underline = true } }) + source([[ + set wrap linebreak + call setline(1, '[license-commit]: https://github.com/neovim/neovim/commit/b17d9691a24099c9210289f16afb1a498a89d803') + highlight TestUL gui=underline cterm=underline + ]]) + + -- Highlight extends past the break point: must not bleed into the filler before the + -- pushed-down word, whether applied via matchadd() (search_attr) or an extmark (decor_attr). + local expected = [[ + ^[license-commit]: {100:https://github.com/neovim/neovim/commit/} | + {100:b17d9691a24099c9210289f16afb1a498a89d803} | + | + ]] + + command([[call matchadd('TestUL', 'https://.*$')]]) + screen:expect(expected) + + -- Confirm the highlight actually toggles off before re-checking via the extmark path below, + -- so the second `expect(expected)` proves that path redraws correctly instead of coasting on + -- leftover state from matchadd(). + command('call clearmatches()') + screen:expect([[ + ^[license-commit]: https://github.com/neovim/neovim/commit/ | + b17d9691a24099c9210289f16afb1a498a89d803 | + | + ]]) + + command([[call nvim_buf_add_highlight(0, -1, 'TestUL', 0, 18, -1)]]) + screen:expect(expected) + end) + + it('extends a to-end-of-line syntax highlight through the filler', function() + local screen = Screen.new(30, 5) + screen:add_extra_attr_ids({ [100] = { reverse = true } }) + source([[ + set wrap linebreak + call setline(1, 'section.heading:' . repeat(' ', 40) . 'END') + syntax match TestBar /^.*$/ + highlight TestBar gui=reverse cterm=reverse + ]]) + + -- Unlike the tests above: the filler here is real trailing padding covered by the same 'to end + -- of line' match, so it must keep the highlight instead of going blank. + screen:expect([[ + {100:^section. }| + {100:heading: }| + {100: END} | + {1:~ }| + | + ]]) + end) + + it('extends a background highlight into the filler for the last word on a line', function() + local screen = Screen.new(20, 5) + screen:add_extra_attr_ids({ [100] = { background = Screen.colors.Red } }) + source([[ + set wrap linebreak + call setline(1, 'word1.word2verylongwordthatpushesdown') + highlight TestBg guibg=Red ctermbg=Red + ]]) + + -- The pushed-down word is the last thing on the line, so nothing absorbs the width overflow: a + -- background highlight must still extend into the filler, since a solid color looks fine over + -- blank cells (unlike underline/strikethrough, see tests above). + command([[call nvim_buf_add_highlight(0, -1, 'TestBg', 0, 0, -1)]]) + screen:expect([[ + {100:^word1. }| + {100:word2verylongwordtha}| + {100:tpushesdown} | + {1:~ }| + | + ]]) + end) end)