fix(decor): 'breakindent'/'showbreak' gap ignores active highlight #41073

Problem:
'breakindent' and 'showbreak' draw their own padding with no
reference to whatever decoration or syntax highlight is currently
active, so it goes unhighlighted even mid-highlight, not just past a
real EOL. Gating this on the decoration's `hl_eol` flag (as an
earlier version of this fix did) missed plain highlights with no
`hl_eol` at all, which have the exact same problem.

Solution:
Snapshot decor_attr into decor_attr_save right before it can be
reset by 'linebreak' filler handling, and pass it into
handle_breakindent()/handle_showbreak_and_filler() to extend into
their padding: it is not a real end of the highlight, just screen
cells with no buffer text. Like the 'linebreak' filler, an
underline/strikethrough/overline is excluded, since it looks like a
broken line drawn over the gap. This also fixes 'breakindent' losing
the highlight right after a real 'linebreak' word-push, since that
reset otherwise leaked into the next row.

(cherry picked from commit 866d61e91a)
This commit is contained in:
Freddie Haddad
2026-08-03 08:32:55 -07:00
committed by github-actions[bot]
parent b1e88d1ed4
commit c34d02ac79
2 changed files with 79 additions and 15 deletions

View File

@@ -786,15 +786,17 @@ static bool attr_has_line_deco(int attr)
return (ae.rgb_ae_attr & mask) != 0 || (ae.cterm_ae_attr & mask) != 0;
}
static void handle_breakindent(win_T *wp, winlinevars_T *wlv)
static void handle_breakindent(win_T *wp, winlinevars_T *wlv, int gap_decor_attr)
{
// draw 'breakindent': indent wrapped text accordingly
// if wlv->need_showbreak is set, breakindent also applies
if (wp->w_p_bri && (wlv->row > wlv->startrow + wlv->filler_lines
|| wlv->need_showbreak)) {
int attr = 0;
// Extend the still-active decoration/syntax highlight (e.g. a full-width code-block background)
// into the indent; it is not ending here, just skipping over screen cells with no buffer text.
int attr = gap_decor_attr;
if (wlv->diff_hlf != (hlf_T)0) {
attr = win_hl_attr(wp, (int)wlv->diff_hlf);
attr = hl_combine_attr(attr, win_hl_attr(wp, (int)wlv->diff_hlf));
}
int num = get_breakindent_win(wp, ml_get_buf(wp->w_buffer, wlv->lnum));
if (wlv->row == wlv->startrow) {
@@ -836,7 +838,7 @@ static void handle_breakindent(win_T *wp, winlinevars_T *wlv)
}
}
static void handle_showbreak_and_filler(win_T *wp, winlinevars_T *wlv)
static void handle_showbreak_and_filler(win_T *wp, winlinevars_T *wlv, int gap_decor_attr)
{
int remaining = wp->w_view_width - wlv->off;
if (wlv->filler_todo > wlv->filler_lines - wlv->n_virt_lines) {
@@ -852,8 +854,10 @@ static void handle_showbreak_and_filler(win_T *wp, winlinevars_T *wlv)
char *const sbr = get_showbreak_value(wp);
if (*sbr != NUL && wlv->need_showbreak) {
// Draw 'showbreak' at the start of each broken line.
// Combine 'showbreak' with 'cursorline', prioritizing 'showbreak'.
int attr = hl_combine_attr(wlv->cul_attr, win_hl_attr(wp, HLF_AT));
// Combine 'showbreak' with 'cursorline' and the still-active decoration/syntax highlight,
// prioritizing 'showbreak'.
int attr = hl_combine_attr(wlv->cul_attr, gap_decor_attr);
attr = hl_combine_attr(attr, win_hl_attr(wp, HLF_AT));
colnr_T vcol_before = wlv->vcol;
draw_col_buf(wp, wlv, sbr, strlen(sbr), attr, NULL, true);
wlv->vcol_sbr = wlv->vcol;
@@ -1118,6 +1122,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b
int search_attr = 0; // attributes desired by 'hlsearch' or ComplMatchIns
int vcol_save_attr = 0; // saved attr for 'cursorcolumn'
int decor_attr = 0; // attributes desired by syntax and extmarks
int decor_attr_save = 0; // decor_attr saved for gap_decor_attr
bool has_syntax = false; // this buffer has syntax highl.
int folded_attr = 0; // attributes for folded line
int eol_hl_off = 0; // 1 if highlighted char after EOL
@@ -1807,13 +1812,15 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b
}
}
// Check if 'breakindent' applies and show it.
// Check if 'breakindent' applies and show it. Like the 'linebreak' filler, attrs must not
// extend into this gap either (see attr_has_line_deco()).
int const gap_decor_attr = attr_has_line_deco(decor_attr_save) ? 0 : decor_attr_save;
if (!wp->w_briopt_sbr) {
handle_breakindent(wp, &wlv);
handle_breakindent(wp, &wlv, gap_decor_attr);
}
handle_showbreak_and_filler(wp, &wlv);
handle_showbreak_and_filler(wp, &wlv, gap_decor_attr);
if (wp->w_briopt_sbr) {
handle_breakindent(wp, &wlv);
handle_breakindent(wp, &wlv, gap_decor_attr);
}
wlv.col = wlv.off;
@@ -2348,6 +2355,8 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b
wlv.char_attr);
}
decor_attr_save = decor_attr; // save current attr
// we don't want linebreak to apply for lines that start with
// leading spaces, followed by long letters (since it would add
// a break at the beginning of a line and this might be unexpected)
@@ -2370,11 +2379,9 @@ 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;
// 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.
// Do not bleed attrs into the filler for the pushed-down word (TABs keep their own
// full-width highlight; see attr_has_line_deco()). search_attr also resets when its 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;

View File

@@ -766,6 +766,63 @@ describe('decorations providers', function()
}
end)
it("'hl_eol' and plain highlights extend into 'linebreak'/'breakindent'/'showbreak' gaps", function()
command('set wrap linebreak breakindent breakindentopt=shift:2')
api.nvim_buf_set_lines(0, 0, -1, false, {
' if vim.g.colors_name == "ferricferric" then',
})
local ns = api.nvim_create_namespace('code_bg')
local id = api.nvim_buf_set_extmark(0, ns, 0, 0, { end_row = 1, hl_group = 'DiffAdd', hl_eol = true })
screen:expect([[
{13:^ if vim.g.colors_name == }|
{13: "ferricferric" then }|
{1:~ }|*5
|
]])
-- Extends across the 'showbreak' character too.
command('set showbreak=>')
screen:add_extra_attr_ids({
[100] = { background = Screen.colors.LightBlue },
[101] = { bold = true, foreground = Screen.colors.Blue1, background = Screen.colors.LightBlue },
})
screen:expect([[
{100:^ if vim.g.colors_name == }|
{100: }{101:>}{100:"ferricferric" then }|
{1:~ }|*5
|
]])
-- No hl_eol here: these gaps have no real text, but the highlight is still active, not
-- ending, so it must show through regardless.
api.nvim_buf_set_extmark(0, ns, 0, 0, { id = id, end_row = 1, hl_group = 'DiffAdd' })
screen:expect([[
{100:^ if vim.g.colors_name == }|
{100: }{101:>}{100:"ferricferric" then} |
{1:~ }|*5
|
]])
end)
it('breakindent keeps the highlight after a real linebreak word-push', function()
-- The word pushed down by 'linebreak' resets decor_attr (it hides real word text, see listlbr_spec.lua), which must not then leak into
-- 'breakindent's own gap on the row the word lands on.
screen:try_resize(30, 5)
screen:add_extra_attr_ids({ [100] = { background = Screen.colors.LightBlue } })
command('set wrap linebreak breakindent')
local line = ' This is a long indented line of plain text that will wrap nicely.'
api.nvim_buf_set_lines(0, 0, -1, false, { line })
local ns = api.nvim_create_namespace('code_bg2')
api.nvim_buf_set_extmark(0, ns, 0, 0, { end_row = 0, end_col = #line, hl_group = 'DiffAdd' })
screen:expect([[
{100:^ This is a long indented }|
{100: line of plain text that }|
{100: will wrap nicely.} |
{1:~ }|
|
]])
end)
it('can create and remove signs when CursorMoved autocommand validates botline #18661', function()
exec_lua([[
local lines = {}