feat(api): nvim_set_hl{update:boolean} #37546

Problem: nvim_set_hl always replaces all attributes.

Solution: Add update field. When true, merge with existing
attributes instead of replacing. Unspecified attributes are preserved.
If highlight group doesn't exist, falls back to reset mode.
This commit is contained in:
glepnir
2026-03-25 18:01:50 +08:00
committed by GitHub
parent 170ff4b244
commit 4d04d0123d
10 changed files with 138 additions and 49 deletions

View File

@@ -1589,6 +1589,8 @@ nvim_set_hl({ns_id}, {name}, {val}) *nvim_set_hl()*
• underdotted: boolean
• underdouble: boolean
• underline: boolean
• update: boolean false by default; true updates only
specified attributes, leaving others unchanged.
nvim_set_hl_ns({ns_id}) *nvim_set_hl_ns()*
Set active namespace for highlights defined with |nvim_set_hl()|. This can

View File

@@ -180,6 +180,7 @@ API
execute code while nvim is blocking for input.
• |vim.secure.trust()| accepts `path` for the `allow` action.
• |nvim_get_chan_info()| includes `exitcode` field for terminal buffers
• |nvim_set_hl()| supports updating specified attributes only.
BUILD

View File

@@ -2253,6 +2253,7 @@ function vim.api.nvim_set_decoration_provider(ns_id, opts) end
--- - underdotted: boolean
--- - underdouble: boolean
--- - underline: boolean
--- - update: boolean false by default; true updates only specified attributes, leaving others unchanged.
function vim.api.nvim_set_hl(ns_id, name, val) end
--- Set active namespace for highlights defined with `nvim_set_hl()`. This can be set for

View File

@@ -329,6 +329,7 @@ error('Cannot require a meta file')
--- @field fg_indexed? boolean
--- @field bg_indexed? boolean
--- @field force? boolean
--- @field update? boolean
--- @field url? string
--- @class vim.api.keyset.highlight_cterm

View File

@@ -206,6 +206,7 @@ typedef struct {
Boolean fg_indexed;
Boolean bg_indexed;
Boolean force;
Boolean update;
String url;
} Dict(highlight);

View File

@@ -170,9 +170,8 @@ DictAs(get_hl_info) nvim_get_hl(Integer ns_id, Dict(get_highlight) *opts, Arena
/// - underdotted: boolean
/// - underdouble: boolean
/// - underline: boolean
/// - update: boolean false by default; true updates only specified attributes, leaving others unchanged.
/// @param[out] err Error details, if any
///
// TODO(bfredl): val should take update vs reset flag
void nvim_set_hl(uint64_t channel_id, Integer ns_id, String name, Dict(highlight) *val, Error *err)
FUNC_API_SINCE(7)
{
@@ -188,7 +187,14 @@ void nvim_set_hl(uint64_t channel_id, Integer ns_id, String name, Dict(highlight
return;
}
HlAttrs attrs = dict2hlattrs(val, true, &link_id, err);
bool update = HAS_KEY(val, highlight, update) && val->update;
HlAttrs *base = NULL;
HlAttrs base_attrs;
if (update && hl_ns_get_attrs((int)ns_id, hl_id, NULL, &base_attrs)) {
base = &base_attrs;
}
HlAttrs attrs = dict2hlattrs(val, true, &link_id, base, err);
if (!ERROR_SET(err)) {
WITH_SCRIPT_CONTEXT(channel_id, {
ns_hl_def((NS)ns_id, hl_id, attrs, link_id, val);

View File

@@ -221,7 +221,7 @@ int ns_get_hl(NS *ns_hl, int hl_id, bool link, bool nodefault)
fallback = false;
Dict(highlight) dict = KEYDICT_INIT;
if (api_dict_to_keydict(&dict, KeyDict_highlight_get_field, ret.data.dict, &err)) {
attrs = dict2hlattrs(&dict, true, &it.link_id, &err);
attrs = dict2hlattrs(&dict, true, &it.link_id, NULL, &err);
fallback = GET_BOOL_OR_TRUE(&dict, highlight, fallback);
tmp = dict.fallback; // or false
if (it.link_id >= 0) {
@@ -291,6 +291,28 @@ bool win_check_ns_hl(win_T *wp)
return hl_check_ns();
}
/// Get highlight attributes for a highlight group
///
/// @param ns_id Namespace ID (0 for global namespace)
/// @param hl_id Highlight group ID (1-based)
/// @param[in] optional If non-NULL, passed to syn_ns_id2attr to track
/// whether the group was explicitly defined in the namespace.
/// @param[out] attrs Pointer to store the attributes
/// @return true if highlight group exists and has valid attributes
bool hl_ns_get_attrs(int ns_id, int hl_id, bool *optional, HlAttrs *attrs)
{
bool opt = optional ? *optional : true;
int syn_attr = syn_ns_id2attr(ns_id, hl_id, &opt);
if (optional) {
*optional = opt;
}
if (syn_attr <= 0) {
return false;
}
*attrs = syn_attr2entry(syn_attr);
return true;
}
/// Get attribute code for a builtin highlight group.
///
/// The final syntax group could be modified by hi-link or 'winhighlight'.
@@ -300,11 +322,7 @@ int hl_get_ui_attr(int ns_id, int idx, int final_id, bool optional)
bool available = false;
if (final_id > 0) {
int syn_attr = syn_ns_id2attr(ns_id, final_id, &optional);
if (syn_attr > 0) {
attrs = syn_attr2entry(syn_attr);
available = true;
}
available = hl_ns_get_attrs(ns_id, final_id, &optional, &attrs);
}
if (HLF_PNI <= idx && idx <= HLF_PST) {
@@ -1005,49 +1023,53 @@ void hlattrs2dict(Dict *hl, Dict *hl_attrs, HlAttrs ae, bool use_rgb, bool short
}
}
HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *err)
HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, HlAttrs *base, Error *err)
{
#define HAS_KEY_X(d, key) HAS_KEY(d, highlight, key)
HlAttrs hlattrs = HLATTRS_INIT;
int32_t fg = -1;
int32_t bg = -1;
int32_t ctermfg = -1;
int32_t ctermbg = -1;
int32_t sp = -1;
int blend = -1;
int32_t mask = 0;
int32_t cterm_mask = 0;
int32_t fg = base ? base->rgb_fg_color : -1;
int32_t bg = base ? base->rgb_bg_color : -1;
int32_t ctermfg = base ? (base->cterm_fg_color == 0 ? -1 : base->cterm_fg_color - 1) : -1;
int32_t ctermbg = base ? (base->cterm_bg_color == 0 ? -1 : base->cterm_bg_color - 1) : -1;
int32_t sp = base ? base->rgb_sp_color : -1;
int blend = base ? base->hl_blend : -1;
int32_t mask = base ? base->rgb_ae_attr : 0;
int32_t cterm_mask = base ? base->cterm_ae_attr : 0;
bool cterm_mask_provided = false;
#define CHECK_FLAG(d, m, name, extra, flag) \
if (d->name##extra) { \
if (flag & HL_UNDERLINE_MASK) { \
m &= ~HL_UNDERLINE_MASK; \
#define CHECK_FLAG_WITH_KEY(d, m, name, extra, flag) \
if (HAS_KEY_X(d, name)) { \
if (d->name##extra) { \
if (flag & HL_UNDERLINE_MASK) { \
m &= ~HL_UNDERLINE_MASK; \
} \
m |= flag; \
} else { \
m &= ~flag; \
} \
m |= flag; \
}
CHECK_FLAG(dict, mask, reverse, , HL_INVERSE);
CHECK_FLAG(dict, mask, bold, , HL_BOLD);
CHECK_FLAG(dict, mask, italic, , HL_ITALIC);
CHECK_FLAG(dict, mask, underline, , HL_UNDERLINE);
CHECK_FLAG(dict, mask, undercurl, , HL_UNDERCURL);
CHECK_FLAG(dict, mask, underdouble, , HL_UNDERDOUBLE);
CHECK_FLAG(dict, mask, underdotted, , HL_UNDERDOTTED);
CHECK_FLAG(dict, mask, underdashed, , HL_UNDERDASHED);
CHECK_FLAG(dict, mask, standout, , HL_STANDOUT);
CHECK_FLAG(dict, mask, strikethrough, , HL_STRIKETHROUGH);
CHECK_FLAG(dict, mask, altfont, , HL_ALTFONT);
CHECK_FLAG(dict, mask, dim, , HL_DIM);
CHECK_FLAG(dict, mask, blink, , HL_BLINK);
CHECK_FLAG(dict, mask, conceal, , HL_CONCEALED);
CHECK_FLAG(dict, mask, overline, , HL_OVERLINE);
CHECK_FLAG_WITH_KEY(dict, mask, reverse, , HL_INVERSE);
CHECK_FLAG_WITH_KEY(dict, mask, bold, , HL_BOLD);
CHECK_FLAG_WITH_KEY(dict, mask, italic, , HL_ITALIC);
CHECK_FLAG_WITH_KEY(dict, mask, underline, , HL_UNDERLINE);
CHECK_FLAG_WITH_KEY(dict, mask, undercurl, , HL_UNDERCURL);
CHECK_FLAG_WITH_KEY(dict, mask, underdouble, , HL_UNDERDOUBLE);
CHECK_FLAG_WITH_KEY(dict, mask, underdotted, , HL_UNDERDOTTED);
CHECK_FLAG_WITH_KEY(dict, mask, underdashed, , HL_UNDERDASHED);
CHECK_FLAG_WITH_KEY(dict, mask, standout, , HL_STANDOUT);
CHECK_FLAG_WITH_KEY(dict, mask, strikethrough, , HL_STRIKETHROUGH);
CHECK_FLAG_WITH_KEY(dict, mask, altfont, , HL_ALTFONT);
CHECK_FLAG_WITH_KEY(dict, mask, dim, , HL_DIM);
CHECK_FLAG_WITH_KEY(dict, mask, blink, , HL_BLINK);
CHECK_FLAG_WITH_KEY(dict, mask, conceal, , HL_CONCEALED);
CHECK_FLAG_WITH_KEY(dict, mask, overline, , HL_OVERLINE);
if (use_rgb) {
CHECK_FLAG(dict, mask, fg_indexed, , HL_FG_INDEXED);
CHECK_FLAG(dict, mask, bg_indexed, , HL_BG_INDEXED);
CHECK_FLAG_WITH_KEY(dict, mask, fg_indexed, , HL_FG_INDEXED);
CHECK_FLAG_WITH_KEY(dict, mask, bg_indexed, , HL_BG_INDEXED);
}
CHECK_FLAG(dict, mask, nocombine, , HL_NOCOMBINE);
CHECK_FLAG(dict, mask, default, _, HL_DEFAULT);
CHECK_FLAG_WITH_KEY(dict, mask, nocombine, , HL_NOCOMBINE);
CHECK_FLAG_WITH_KEY(dict, mask, default, _, HL_DEFAULT);
if (HAS_KEY_X(dict, fg)) {
fg = object_to_color(dict->fg, "fg", use_rgb, err);
@@ -1111,6 +1133,16 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e
}
cterm_mask_provided = true;
cterm_mask = 0;
#define CHECK_FLAG(d, m, name, extra, flag) \
if (d->name##extra) { \
if (flag & HL_UNDERLINE_MASK) { \
m &= ~HL_UNDERLINE_MASK; \
} \
m |= flag; \
}
CHECK_FLAG(cterm, cterm_mask, reverse, , HL_INVERSE);
CHECK_FLAG(cterm, cterm_mask, bold, , HL_BOLD);
CHECK_FLAG(cterm, cterm_mask, italic, , HL_ITALIC);
@@ -1129,6 +1161,7 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e
CHECK_FLAG(cterm, cterm_mask, nocombine, , HL_NOCOMBINE);
}
#undef CHECK_FLAG
#undef CHECK_FLAG_WITH_KEY
if (HAS_KEY_X(dict, ctermfg)) {
ctermfg = object_to_color(dict->ctermfg, "ctermfg", false, err);

View File

@@ -936,6 +936,7 @@ void set_hl_group(int id, HlAttrs attrs, Dict(highlight) *dict, int link_id)
g->sg_link = 0;
}
bool update = HAS_KEY(dict, highlight, update) && dict->update;
g->sg_gui = attrs.rgb_ae_attr &~HL_DEFAULT;
g->sg_rgb_fg = attrs.rgb_fg_color;
@@ -954,20 +955,29 @@ void set_hl_group(int id, HlAttrs attrs, Dict(highlight) *dict, int link_id)
};
for (int j = 0; cattrs[j].dest; j++) {
if (cattrs[j].val < 0) {
if (cattrs[j].name.type != kObjectTypeNil) {
if (cattrs[j].val < 0) {
*cattrs[j].dest = kColorIdxNone;
} else if (cattrs[j].name.type == kObjectTypeString && cattrs[j].name.data.string.size) {
name_to_color(cattrs[j].name.data.string.data, cattrs[j].dest);
} else {
*cattrs[j].dest = kColorIdxHex;
}
} else if (!update) {
*cattrs[j].dest = kColorIdxNone;
} else if (cattrs[j].name.type == kObjectTypeString && cattrs[j].name.data.string.size) {
name_to_color(cattrs[j].name.data.string.data, cattrs[j].dest);
} else {
*cattrs[j].dest = kColorIdxHex;
}
}
g->sg_cterm = attrs.cterm_ae_attr &~HL_DEFAULT;
g->sg_cterm_bg = attrs.cterm_bg_color;
g->sg_cterm_fg = attrs.cterm_fg_color;
g->sg_cterm_bold = g->sg_cterm & HL_BOLD;
g->sg_blend = attrs.hl_blend;
if (attrs.hl_blend != -1) {
g->sg_blend = attrs.hl_blend;
} else if (!update) {
g->sg_blend = -1;
}
g->sg_script_ctx = current_sctx;
g->sg_script_ctx.sc_lnum += SOURCING_LNUM;

View File

@@ -223,7 +223,7 @@ static HlAttrs ui_client_dict2hlattrs(Dict d, bool rgb)
return HLATTRS_INIT;
}
HlAttrs attrs = dict2hlattrs(&dict, rgb, NULL, &err);
HlAttrs attrs = dict2hlattrs(&dict, rgb, NULL, NULL, &err);
if (HAS_KEY(&dict, highlight, url)) {
attrs.url = tui_add_url(tui, dict.url.data);

View File

@@ -257,6 +257,40 @@ describe('API: set highlight', function()
)
assert_alive()
end)
it('update=true sets only specified keys', function()
api.nvim_set_hl(0, 'TestGroup', { fg = '#ff0000', bg = '#0000ff', bold = true })
api.nvim_set_hl(0, 'TestGroup', { bg = '#00ff00', update = true })
local hl = api.nvim_get_hl(0, { name = 'TestGroup' })
eq(16711680, hl.fg)
eq(65280, hl.bg)
eq(true, hl.bold)
api.nvim_set_hl(0, 'TestGroup', { bold = false, update = true })
hl = api.nvim_get_hl(0, { name = 'TestGroup' })
eq(nil, hl.bold)
eq(16711680, hl.fg)
api.nvim_set_hl(0, 'TestGroup', { italic = true })
hl = api.nvim_get_hl(0, { name = 'TestGroup' })
eq(nil, hl.fg)
eq(nil, hl.bg)
eq(true, hl.italic)
local ns = api.nvim_create_namespace('test')
api.nvim_set_hl(ns, 'TestGroup', { fg = '#ff0000', italic = true })
api.nvim_set_hl(ns, 'TestGroup', { fg = '#00ff00', update = true })
hl = api.nvim_get_hl(ns, { name = 'TestGroup' })
eq(65280, hl.fg)
eq(true, hl.italic)
api.nvim_set_hl(0, 'LinkedGroup', { link = 'Normal' })
api.nvim_set_hl(0, 'LinkedGroup', { bold = true, update = true })
hl = api.nvim_get_hl(0, { name = 'LinkedGroup' })
eq(nil, hl.link)
eq(true, hl.bold)
end)
end)
describe('API: get highlight', function()