From 4d04d0123d2571391a00b87f7ee70f987fb7cedd Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 25 Mar 2026 18:01:50 +0800 Subject: [PATCH] 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. --- runtime/doc/api.txt | 2 + runtime/doc/news.txt | 1 + runtime/lua/vim/_meta/api.lua | 1 + runtime/lua/vim/_meta/api_keysets.lua | 1 + src/nvim/api/keysets_defs.h | 1 + src/nvim/api/vim.c | 12 ++- src/nvim/highlight.c | 111 ++++++++++++++++--------- src/nvim/highlight_group.c | 22 +++-- src/nvim/ui_client.c | 2 +- test/functional/api/highlight_spec.lua | 34 ++++++++ 10 files changed, 138 insertions(+), 49 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index c878251a88..0d4db20af4 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 2c5e9a6d7d..8461bafab2 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index ccaa6192aa..380058a287 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -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 diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index 1bbd8073c2..7f4feba87d 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -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 diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 23859049a0..c54c8f712d 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -206,6 +206,7 @@ typedef struct { Boolean fg_indexed; Boolean bg_indexed; Boolean force; + Boolean update; String url; } Dict(highlight); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 8460e62652..2e9bc89204 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -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); diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index 529db6a9c0..2211dc39d0 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -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); diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index b26b485ccd..99d505285e 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -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; diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c index ab4eecec9a..0e3a5b3fff 100644 --- a/src/nvim/ui_client.c +++ b/src/nvim/ui_client.c @@ -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); diff --git a/test/functional/api/highlight_spec.lua b/test/functional/api/highlight_spec.lua index bc7e80377e..2da2df0bd0 100644 --- a/test/functional/api/highlight_spec.lua +++ b/test/functional/api/highlight_spec.lua @@ -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()