From c34d02ac79c7d8f01ccf544f2fe611b497df8269 Mon Sep 17 00:00:00 2001 From: Freddie Haddad <6127369+freddiehaddad@users.noreply.github.com> Date: Mon, 3 Aug 2026 08:32:55 -0700 Subject: [PATCH] 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 866d61e91abaa73561f91ddd8f6e27bd50f9bc72) --- src/nvim/drawline.c | 37 +++++++++------- test/functional/ui/decorations_spec.lua | 57 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 6080eed7f6..929e3f1102 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -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; diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 911969d14c..a1d64e52a6 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -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 = {}