feat(statusline): option to specify stacking highlight groups #37153

**Problem:** No easy way to stack highlight groups #35806.

**Solution:** Add a way to specify a new statusline chunk with a
highlight group that inherits from previous highlight attributes.

Also applies to tabline, etc.
This commit is contained in:
Riley Bruins
2026-02-15 09:16:51 -08:00
committed by GitHub
parent a1895f024a
commit 4719b94443
10 changed files with 73 additions and 16 deletions

View File

@@ -353,6 +353,8 @@ OPTIONS
• 'busy' sets a buffer "busy" status. Indicated in the default statusline.
• 'pumborder' adds a border to the popup menu.
• |g:clipboard| autodetection only selects tmux when running inside tmux
• 'statusline' allows "stacking" highlight groups (groups inherit from
previous highlight attributes)
PERFORMANCE

View File

@@ -6409,6 +6409,8 @@ A jump table for the options with a short description can be found at |Q_op|.
Thus use %#HLname# for highlight group HLname. The same
highlighting is used, also for the statusline of non-current
windows.
$ - Same as `#`, except the `%$HLname$` group will inherit from
preceding highlight attributes.
* - Set highlight group to User{N}, where {N} is taken from the
minwid field, e.g. %1*. Restore normal highlight with %* or
%0*. The difference between User{N} and StatusLine will be

View File

@@ -6857,6 +6857,8 @@ vim.wo.stc = vim.wo.statuscolumn
--- Thus use %#HLname# for highlight group HLname. The same
--- highlighting is used, also for the statusline of non-current
--- windows.
--- $ - Same as `#`, except the `%$HLname$` group will inherit from
--- preceding highlight attributes.
--- * - Set highlight group to User{N}, where {N} is taken from the
--- minwid field, e.g. %1*. Restore normal highlight with %* or
--- %0*. The difference between User{N} and StatusLine will be

View File

@@ -222,8 +222,8 @@ enum {
STL_PREVIEWFLAG, STL_PREVIEWFLAG_ALT, STL_MODIFIED, STL_MODIFIED_ALT, \
STL_QUICKFIX, STL_PERCENTAGE, STL_ALTPERCENT, STL_ARGLISTSTAT, STL_PAGENUM, \
STL_SHOWCMD, STL_FOLDCOL, STL_SIGNCOL, STL_VIM_EXPR, STL_SEPARATE, \
STL_TRUNCMARK, STL_USER_HL, STL_HIGHLIGHT, STL_TABPAGENR, STL_TABCLOSENR, \
STL_CLICK_FUNC, STL_TABPAGENR, STL_TABCLOSENR, STL_CLICK_FUNC, \
STL_TRUNCMARK, STL_USER_HL, STL_HIGHLIGHT, STL_HIGHLIGHT_COMB, STL_TABPAGENR, \
STL_TABCLOSENR, STL_CLICK_FUNC, STL_TABPAGENR, STL_TABCLOSENR, STL_CLICK_FUNC, \
0, })
// arguments for can_bs()

View File

@@ -8914,6 +8914,8 @@ local options = {
Thus use %#HLname# for highlight group HLname. The same
highlighting is used, also for the statusline of non-current
windows.
$ - Same as `#`, except the `%$HLname$` group will inherit from
preceding highlight attributes.
* - Set highlight group to User{N}, where {N} is taken from the
minwid field, e.g. %1*. Restore normal highlight with %* or
%0*. The difference between User{N} and StatusLine will be

View File

@@ -374,7 +374,12 @@ static void win_redr_custom(win_T *wp, bool draw_winbar, bool draw_ruler, bool u
curattr = attr;
curgroup = (int)group;
} else if (sp->userhl < 0) {
curattr = syn_id2attr(-sp->userhl);
int new_attr = syn_id2attr(-sp->userhl);
if (sp->item == STL_HIGHLIGHT_COMB) {
curattr = hl_combine_attr(curattr, new_attr);
} else {
curattr = new_attr;
}
curgroup = -sp->userhl;
} else {
int *userhl = (wp != NULL && wp != curwin && wp->w_status_height != 0)
@@ -1081,7 +1086,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
// remove group if all items are empty and highlight group
// doesn't change
for (n = stl_groupitems[groupdepth] - 1; n >= 0; n--) {
if (stl_items[n].type == Highlight) {
if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
group_start_userhl = group_end_userhl = stl_items[n].minwid;
break;
}
@@ -1090,7 +1095,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
if (stl_items[n].type == Normal) {
break;
}
if (stl_items[n].type == Highlight) {
if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
group_end_userhl = stl_items[n].minwid;
}
}
@@ -1100,7 +1105,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
group_len = 0;
for (n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
// do not use the highlighting from the removed group
if (stl_items[n].type == Highlight) {
if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
stl_items[n].type = Empty;
}
// adjust the start position of TabPage to the next
@@ -1682,17 +1687,18 @@ stcsign:
}
break;
case STL_HIGHLIGHT_COMB:
case STL_HIGHLIGHT: {
// { The name of the highlight is surrounded by `#`
// { The name of the highlight is surrounded by `#` or `$`
char *t = fmt_p;
while (*fmt_p != '#' && *fmt_p != NUL) {
while (*fmt_p != opt && *fmt_p != NUL) {
fmt_p++;
}
// }
// Create a highlight item based on the name
if (*fmt_p == '#') {
stl_items[curitem].type = Highlight;
if (*fmt_p == opt) {
stl_items[curitem].type = opt == STL_HIGHLIGHT_COMB ? HighlightCombining : Highlight;
stl_items[curitem].start = out_p;
stl_items[curitem].minwid = -syn_name2id_len(t, (size_t)(fmt_p - t));
curitem++;
@@ -2073,12 +2079,14 @@ stcsign:
*hltab = stl_hltab;
stl_hlrec_t *sp = stl_hltab;
for (int l = evalstart; l < itemcnt + evalstart; l++) {
if (stl_items[l].type == Highlight
if (stl_items[l].type == Highlight || stl_items[l].type == HighlightCombining
|| stl_items[l].type == HighlightFold || stl_items[l].type == HighlightSign) {
sp->start = stl_items[l].start;
sp->userhl = stl_items[l].minwid;
unsigned type = stl_items[l].type;
sp->item = type == HighlightSign ? STL_SIGNCOL : type == HighlightFold ? STL_FOLDCOL : 0;
sp->item = type == HighlightSign ? STL_SIGNCOL : type ==
HighlightFold ? STL_FOLDCOL : type ==
HighlightCombining ? STL_HIGHLIGHT_COMB : 0;
sp++;
}
}

View File

@@ -44,6 +44,7 @@ typedef enum {
STL_TRUNCMARK = '<', ///< Truncation mark if line is too long.
STL_USER_HL = '*', ///< Highlight from (User)1..9 or 0.
STL_HIGHLIGHT = '#', ///< Highlight name.
STL_HIGHLIGHT_COMB = '$', ///< Highlight name (combining previous attrs).
STL_TABPAGENR = 'T', ///< Tab page label nr.
STL_TABCLOSENR = 'X', ///< Tab page close nr.
STL_CLICK_FUNC = '@', ///< Click region start.
@@ -88,6 +89,7 @@ struct stl_item {
Group,
Separate,
Highlight,
HighlightCombining,
HighlightSign,
HighlightFold,
TabPage,

View File

@@ -99,6 +99,47 @@ for _, model in ipairs(mousemodels) do
eq('0 1 l', eval('g:testvar'))
end)
it('works with combined highlight attributes', function()
screen:add_extra_attr_ids({
[131] = { reverse = true, bold = true, background = Screen.colors.LightMagenta },
[132] = {
reverse = true,
foreground = Screen.colors.Magenta,
bold = true,
background = Screen.colors.LightMagenta,
},
[133] = { reverse = true, bold = true, foreground = Screen.colors.Magenta1 },
[134] = {
bold = true,
background = Screen.colors.LightMagenta,
reverse = true,
undercurl = true,
special = Screen.colors.Red,
},
[135] = {
bold = true,
background = Screen.colors.LightMagenta,
reverse = true,
undercurl = true,
foreground = Screen.colors.Fuchsia,
special = Screen.colors.Red,
},
})
api.nvim_set_option_value(
'statusline',
'\t%#Pmenu#foo%$SpellBad$bar%$String$baz%#Constant#qux',
{}
)
screen:expect([[
^ |
{1:~ }|*5
{3:^I}{131:foo}{134:bar}{135:baz}{133:qux }|
|
]])
end)
it('works for winbar', function()
api.nvim_set_option_value('winbar', 'Not clicky stuff %0@MyClickFunc@Clicky stuff%T', {})
api.nvim_input_mouse('left', 'press', '', 0, 0, 17)

View File

@@ -336,13 +336,13 @@ let test_values = {
\ 'timeout:-1', 'file:/tmp/file', 'expr:Func()', 'double,33'],
\ ['xxx', '-1', 'timeout:', 'best,double', 'double,fast']],
\ 'splitkeep': [['cursor', 'screen', 'topline'], ['xxx']],
\ 'statusline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
\ 'statusline': [['', 'xxx'], ['%{', '%{%', '%{%}', '%(', '%)']],
"\ 'swapsync': [['', 'sync', 'fsync'], ['xxx']],
\ 'switchbuf': [['', 'useopen', 'usetab', 'split', 'vsplit', 'newtab',
\ 'uselast', 'split,newtab'],
\ ['xxx']],
\ 'tabclose': [['', 'left', 'uselast', 'left,uselast'], ['xxx']],
\ 'tabline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
\ 'tabline': [['', 'xxx'], ['%{', '%{%', '%{%}', '%(', '%)']],
\ 'tagcase': [['followic', 'followscs', 'ignore', 'match', 'smart'],
\ ['', 'xxx', 'smart,match']],
\ 'termencoding': [has('gui_gtk') ? [] : ['', 'utf-8'], ['xxx']],

View File

@@ -866,7 +866,6 @@ func Test_set_option_errors()
call assert_fails('set rulerformat=%15(%%', 'E542:')
" Test for 'statusline' errors
call assert_fails('set statusline=%$', 'E539:')
call assert_fails('set statusline=%{', 'E540:')
call assert_fails('set statusline=%{%', 'E540:')
call assert_fails('set statusline=%{%}', 'E539:')
@@ -874,7 +873,6 @@ func Test_set_option_errors()
call assert_fails('set statusline=%)', 'E542:')
" Test for 'tabline' errors
call assert_fails('set tabline=%$', 'E539:')
call assert_fails('set tabline=%{', 'E540:')
call assert_fails('set tabline=%{%', 'E540:')
call assert_fails('set tabline=%{%}', 'E539:')