patch 9.2.0467: multi-line statusline loses highlighting attributes

Commit: 
https://github.com/vim/vim/commit/5ef1eec5c5a351d84ce1434c250aad255da7c4f4
Author: Hirohito Higashi <[email protected]>
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) <[email protected]>
    Signed-off-by: Hirohito Higashi <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wM8uj-00FlO5-7L%40256bit.org.

Raspunde prin e-mail lui