patch 9.2.0467: multi-line statusline loses highlighting attributes
Commit:
https://github.com/vim/vim/commit/5ef1eec5c5a351d84ce1434c250aad255da7c4f4
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Sun May 10 18:14:01 2026 +0000
patch 9.2.0467: multi-line statusline loses highlighting attributes
Problem: In a multi-line statusline (and 'tabpanel'), %#XX# / %N*
set on one row do not persist on subsequent rows.
build_stl_str_hl_local() rebuilds stl_items[] from scratch
on every line break ("%@" or "
"), so the highlight is
reset at each row boundary even though within a row it
stays until %* (or another %# / %*).
Solution: Carry the last Highlight item's stl_minwid across line
breaks via a new in/out int* parameter "carry_hl". At the
start of each row, pre-insert a Highlight item from the
carried value so the row begins under the same highlight;
before returning, update the carried value with the row's
final Highlight item. Apply the same carry to the
tabpanel rendering loop (Hirohito Higashi).
related: #19123
closes: #20180
Co-Authored-By: Claude Opus 4.7 (1M context) <
nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/buffer.c b/src/buffer.c
index 44e504c53..43cba3ad8 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -50,7 +50,7 @@ static int build_stl_str_hl_local(stl_mode_T mode, win_T *wp,
char_u *out, size_t outlen, char_u **fmt_arg,
char_u *opt_name, int opt_scope, int fillchar, int maxwidth,
stl_hlrec_T **hltab, stl_hlrec_T **tabtab,
- stl_clickrec_T **clicktab, int *lbreaks);
+ stl_clickrec_T **clicktab, int *lbreaks, int *carry_hl);
#endif
static int append_arg_number(win_T *wp, char_u *buf, size_t buflen, int add_file);
static void free_buffer(buf_T *);
@@ -4393,7 +4393,7 @@ build_stl_str_hl(
{
return build_stl_str_hl_local(STL_MODE_SINGLE, wp, out, outlen, &fmt,
opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
- NULL);
+ NULL, NULL);
}
int
@@ -4408,11 +4408,13 @@ build_stl_str_hl_mline(
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
- stl_clickrec_T **clicktab) // return: click func regions (can be NULL)
+ stl_clickrec_T **clicktab, // return: click func regions (can be NULL)
+ int *carry_hl) // (in/out) %# / %* highlight carried across
+ // line breaks (can be NULL)
{
return build_stl_str_hl_local(STL_MODE_MULTI, wp, out, outlen, fmt,
opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
- NULL);
+ NULL, carry_hl);
}
# ifdef ENABLE_STL_MODE_MULTI_NL
@@ -4428,11 +4430,13 @@ build_stl_str_hl_mline_nl(
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
- stl_clickrec_T **clicktab) // return: click func regions (can be NULL)
+ stl_clickrec_T **clicktab, // return: click func regions (can be NULL)
+ int *carry_hl) // (in/out) %# / %* highlight carried across
+ // line breaks (can be NULL)
{
return build_stl_str_hl_local(STL_MODE_MULTI_NL, wp, out, outlen, fmt,
opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
- NULL);
+ NULL, carry_hl);
}
# endif
@@ -4453,7 +4457,8 @@ get_stl_rendered_height(
++emsg_off;
(void)build_stl_str_hl_local(STL_MODE_GET_RENDERED_HEIGHT,
wp, buf, sizeof(buf), &fmt,
- opt_name, opt_scope, 0, 0, NULL, NULL, NULL, &rendered_height);
+ opt_name, opt_scope, 0, 0, NULL, NULL, NULL, &rendered_height,
+ NULL);
--emsg_off;
return rendered_height;
}
@@ -4489,7 +4494,9 @@ build_stl_str_hl_local(
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
stl_clickrec_T **clicktab, // return: click func regions (can be NULL)
- int *rendered_height) // return: stl rendered height (can be NULL)
+ int *rendered_height, // return: stl rendered height (can be NULL)
+ int *carry_hl) // (in/out) %# / %* highlight carried across
+ // line breaks (can be NULL)
{
linenr_T lnum;
colnr_T len;
@@ -4614,6 +4621,18 @@ build_stl_str_hl_local(
# endif
p = out;
curitem = 0;
+
+ // Pre-insert a Highlight item from carry_hl so that %# / %* set on a
+ // previous multi-line statusline row continues to apply on this row.
+ if (carry_hl != NULL && *carry_hl != 0)
+ {
+ stl_items[curitem].stl_type = Highlight;
+ stl_items[curitem].stl_start = p;
+ stl_items[curitem].stl_minwid = *carry_hl;
+ stl_items[curitem].stl_clickfunc = NULL;
+ curitem++;
+ }
+
prevchar_isflag = TRUE;
prevchar_isitem = FALSE;
for (s = usefmt; *s != NUL; )
@@ -5446,6 +5465,17 @@ find_linebreak:
outputlen = (size_t)(p - out);
itemcnt = curitem;
+ // Remember the most recent %# / %* highlight so the next row of a
+ // multi-line statusline can resume it.
+ if (carry_hl != NULL)
+ {
+ int last_hl = 0;
+ for (l = 0; l < itemcnt; l++)
+ if (stl_items[l].stl_type == Highlight)
+ last_hl = stl_items[l].stl_minwid;
+ *carry_hl = last_hl;
+ }
+
if (mode == STL_MODE_MULTI
# ifdef ENABLE_STL_MODE_MULTI_NL
|| mode == STL_MODE_MULTI_NL
diff --git a/src/proto/
buffer.pro b/src/proto/
buffer.pro
index 13c273d6a..7c2925642 100644
--- a/src/proto/
buffer.pro
+++ b/src/proto/
buffer.pro
@@ -50,8 +50,8 @@ void maketitle(void);
void resettitle(void);
void free_titles(void);
int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
-int build_stl_str_hl_mline(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
-int build_stl_str_hl_mline_nl(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
+int build_stl_str_hl_mline(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab, int *carry_hl);
+int build_stl_str_hl_mline_nl(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab, int *carry_hl);
int get_stl_rendered_height(win_T *wp, char_u *fmt, char_u *opt_name, int opt_scope);
int get_rel_pos(win_T *wp, char_u *buf, int buflen);
char_u *fix_fname(char_u *fname);
diff --git a/src/screen.c b/src/screen.c
index 57b041d40..9c76b8763 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -1479,6 +1479,7 @@ win_redr_custom(
*out_count = 0;
}
+ int carry_hl = 0;
for (int i = 0; i < stlh_cnt; i++)
{
col = col_save;
@@ -1487,7 +1488,8 @@ win_redr_custom(
&stl_tmp,
opt_name, opt_scope,
fillchar, maxwidth, &hltab, &tabtab,
- &clicktab);
+ &clicktab,
+ &carry_hl);
// Make all characters printable.
p = transstr(buf);
diff --git a/src/tabpanel.c b/src/tabpanel.c
index 1e833c7b6..f7889a5e5 100644
--- a/src/tabpanel.c
+++ b/src/tabpanel.c
@@ -688,6 +688,8 @@ do_by_tplmode(
if (usefmt != NULL && *usefmt != NUL)
{
+ int carry_hl = 0;
+
while (*usefmt != NUL)
{
char_u buf[IOSIZE];
@@ -708,7 +710,8 @@ do_by_tplmode(
(args.cwp, buf, sizeof(buf),
&usefmt, opt_name, opt_scope, TPL_FILLCHAR,
args.col_end - args.col_start, &hltab, &tabtab,
- tplmode == TPLMODE_REDRAW ? &clicktab : NULL);
+ tplmode == TPLMODE_REDRAW ? &clicktab : NULL,
+ &carry_hl);
args.prow = &row;
args.pcol = &col;
diff --git a/src/testdir/dumps/Test_multistatusline_carry_hl_01.dump b/src/testdir/dumps/Test_multistatusline_carry_hl_01.dump
new file mode 100644
index 000000000..724d844bd
--- /dev/null
+++ b/src/testdir/dumps/Test_multistatusline_carry_hl_01.dump
@@ -0,0 +1,9 @@
+> +0&#ffffff0@74
+|~+0#4040ff13&| @73
+|L+3#0000000&|1|A| @68|L+0&#ffff4012|1|B
+|L|2| |c|a|r@1|i|e|d| |S|e|a|r|c|h| @57
+|L+3&#ffffff0|3| |r|e|s|e|t| @66
+|L+0#ffff4012#4040ff13|4| |u|s|e|r|2| @66
+|L|5| |c|a|r@1|i|e|d| |u|s|e|r|2| @58
+|L+3#0000000#ffffff0|6| |r|e|s|e|t| @66
+| +0&&@74
diff --git a/src/testdir/dumps/Test_tabpanel_carry_hl_01.dump b/src/testdir/dumps/Test_tabpanel_carry_hl_01.dump
new file mode 100644
index 000000000..87a130f29
--- /dev/null
+++ b/src/testdir/dumps/Test_tabpanel_carry_hl_01.dump
@@ -0,0 +1,9 @@
+|L+2&#ffffff0|1|A| @16> +0&&@39
+|L+0&#ffff4012|1|B| @16|~+0#4040ff13#ffffff0| @38
+|L+0#0000000#ffff4012|2| |c|a|r@1|i|e|d| |S|e|a|r|c|h| @2|~+0#4040ff13#ffffff0| @38
+|L+2#0000000&|3| |r|e|s|e|t| @11|~+0#4040ff13&| @38
+|L+0#ffff4012#4040ff13|4| |u|s|e|r|2| @11|~+0#4040ff13#ffffff0| @38
+|L+0#ffff4012#4040ff13|5| |c|a|r@1|i|e|d| @9|~+0#4040ff13#ffffff0| @38
+|L+2#0000000&|6| |r|e|s|e|t| @11|~+0#4040ff13&| @38
+| +1#0000000&@19|~+0#4040ff13&| @38
+| +1#0000000&@19| +0&&@21|0|,|0|-|1| @8|A|l@1|
diff --git a/src/testdir/test_statuslineopt.vim b/src/testdir/test_statuslineopt.vim
index 6454dbff8..653529e35 100644
--- a/src/testdir/test_statuslineopt.vim
+++ b/src/testdir/test_statuslineopt.vim
@@ -235,6 +235,35 @@ func Test_multistatusline_highlight()
call StopVimInTerminal(buf)
endfunc
+func Test_multistatusline_carry_hl()
+ CheckScreendump
+
+ " %#XX# / %N* set on one row should persist on subsequent rows until %*
+ " (or another %# / %*) changes it.
+ let lines =<< trim END
+ func MyStatusLine()
+ return 'L1A%=%#Search#L1B%@'
+ \ .. 'L2 carried Search%@'
+ \ .. '%*L3 reset%@'
+ \ .. '%2*L4 user2%@'
+ \ .. 'L5 carried user2%@'
+ \ .. '%*L6 reset'
+ endfunc
+
+ hi User2 ctermfg=Yellow ctermbg=Blue
+ set laststatus=2
+ set statuslineopt=maxheight:6
+ set statusline=%!MyStatusLine()
+ END
+ call writefile(lines, 'XTest_multistatusline_carry_hl', 'D')
+
+ let buf = g:RunVimInTerminal('-S XTest_multistatusline_carry_hl', {'rows': 9})
+ call term_sendkeys(buf, "\<C-L>")
+ call VerifyScreenDump(buf, 'Test_multistatusline_carry_hl_01', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
func Test_statuslineopt_default_stl()
CheckScreendump
diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim
index b5bf678a1..3b5fe390f 100644
--- a/src/testdir/test_tabpanel.vim
+++ b/src/testdir/test_tabpanel.vim
@@ -1129,6 +1129,36 @@ func Test_tabpanel_empty()
set tabpanel&
endfunc
+func Test_tabpanel_carry_hl()
+ CheckScreendump
+
+ " %#XX# / %N* set on one row of a tabpanel should persist on subsequent
+ " rows until %* (or another %# / %*) changes it. Both "%@" and "
" are
+ " accepted as line breaks in 'tabpanel'.
+ let lines =<< trim END
+ func MyTabPanel()
+ return "L1A
"
+ \ .. "%#Search#L1B
"
+ \ .. "L2 carried Search
"
+ \ .. "%*L3 reset
"
+ \ .. "%2*L4 user2
"
+ \ .. "L5 carried
"
+ \ .. "%*L6 reset"
+ endfunc
+
+ hi User2 ctermfg=Yellow ctermbg=Blue
+ set showtabpanel=2
+ set tabpanelopt=columns:20
+ set tabpanel=%!MyTabPanel()
+ END
+ call writefile(lines, 'XTest_tabpanel_carry_hl', 'D')
+
+ let buf = RunVimInTerminal('-S XTest_tabpanel_carry_hl', {'rows': 9, 'cols': 60})
+ call VerifyScreenDump(buf, 'Test_tabpanel_carry_hl_01', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
func Test_tabpanel_getinfo_and_scroll()
CheckScreendump
diff --git a/src/version.c b/src/version.c
index 705339b34..a5ba4c796 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 467,
/**/
466,
/**/