From eeee4bd4fc4117c994011d087b46e6e5f9a74379 Mon Sep 17 00:00:00 2001 From: Peter Cardenas <16930781+PeterCardenas@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:42:44 -0700 Subject: [PATCH] feat(treesitter/extmark): support removing a conceal highlight #35087 Problem: Cannot remove a `@conceal` highlight when defined in highlights.scm. Solution: Support a `@noconceal` highlight that works similarly to `@nospell` where it overrides the conceal set on the range to remove it. Additionally, can set the conceal metadata field to false for the same behavior. --- runtime/doc/api.txt | 13 ++++--- runtime/doc/news.txt | 2 + runtime/doc/treesitter.txt | 5 +++ runtime/lua/vim/_meta/api.gen.lua | 13 ++++--- runtime/lua/vim/_meta/api_keysets.gen.lua | 2 +- runtime/lua/vim/_meta/api_keysets_extra.lua | 2 +- runtime/lua/vim/treesitter/highlighter.lua | 23 +++++++++-- runtime/lua/vim/treesitter/query.lua | 2 +- .../pack/dist/opt/nvim.tohtml/lua/tohtml.lua | 3 +- src/nvim/api/extmark.c | 38 +++++++++++++------ src/nvim/api/keysets_defs.h | 2 +- src/nvim/decoration.c | 12 ++++-- src/nvim/decoration_defs.h | 1 + test/functional/treesitter/highlight_spec.lua | 31 +++++++++++++++ test/functional/ui/decorations_spec.lua | 2 +- 15 files changed, 116 insertions(+), 35 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 604dddebdd..60ef807a8a 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3324,11 +3324,14 @@ nvim_buf_set_extmark({buf}, {ns_id}, {line}, {col}, {opts}) • cursorline_hl_group: highlight group used for the sign column text when the cursor is on the same line as the mark and 'cursorline' is enabled. - • conceal: string which should be either empty or a single - character. Enable concealing similar to |:syn-conceal|. - When a character is supplied it is used as |:syn-cchar|. - "hl_group" is used as highlight for the cchar if provided, - otherwise it defaults to |hl-Conceal|. + • conceal: either a boolean or a string. + • when a string is given, it should be either empty or a + single character. Enable concealing similar to + |:syn-conceal|. When a character is supplied it is used + as |:syn-cchar|. "hl_group" is used as highlight for the + cchar if provided, otherwise it defaults to |hl-Conceal|. + • when a boolean is given, true is equivalent to "" and + false overrides any existing conceal to remove it. • conceal_lines: string which should be empty. When provided, lines in the range are not drawn at all (according to 'conceallevel'); the next unconcealed line is drawn diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index ac7c08693a..8cc3eeba56 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -197,6 +197,8 @@ TERMINAL TREESITTER • |v_]N| |v_[N| expand selection to sibling treesitter node. +• |treesitter-highlight-conceal| can be removed by adding a `@noconceal` + capture. TUI diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index e2b19b2f9b..170cd4dc75 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -522,6 +522,11 @@ character will be used: >query ; identifiers will be concealed with 'f'. ((identifier) @conceal (#set! conceal "foo")) < +To remove a conceal highlight, you can use the `@noconceal` capture to +override any existing conceal highlight set for the range. >query + + ((identifier) @noconceal) +< *treesitter-highlight-priority* Treesitter uses |nvim_buf_set_extmark()| to set highlights with a default diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 990ee81925..42e962ad64 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -702,11 +702,14 @@ function vim.api.nvim_buf_line_count(buf) end --- - cursorline_hl_group: highlight group used for the sign --- column text when the cursor is on the same line as the --- mark and 'cursorline' is enabled. ---- - conceal: string which should be either empty or a single ---- character. Enable concealing similar to `:syn-conceal`. ---- When a character is supplied it is used as `:syn-cchar`. ---- "hl_group" is used as highlight for the cchar if provided, ---- otherwise it defaults to `hl-Conceal`. +--- - conceal: either a boolean or a string. +--- - when a string is given, it should be either empty or a single +--- character. Enable concealing similar to `:syn-conceal`. +--- When a character is supplied it is used as `:syn-cchar`. +--- "hl_group" is used as highlight for the cchar if provided, +--- otherwise it defaults to `hl-Conceal`. +--- - when a boolean is given, true is equivalent to "" and false +--- overrides any existing conceal to remove it. --- - conceal_lines: string which should be empty. When --- provided, lines in the range are not drawn at all --- (according to 'conceallevel'); the next unconcealed line diff --git a/runtime/lua/vim/_meta/api_keysets.gen.lua b/runtime/lua/vim/_meta/api_keysets.gen.lua index 2d3122719a..6317b81700 100644 --- a/runtime/lua/vim/_meta/api_keysets.gen.lua +++ b/runtime/lua/vim/_meta/api_keysets.gen.lua @@ -437,7 +437,7 @@ error('Cannot require a meta file') --- @field number_hl_group? integer|string --- @field line_hl_group? integer|string --- @field cursorline_hl_group? integer|string ---- @field conceal? string +--- @field conceal? string|boolean --- @field conceal_lines? string --- @field spell? boolean --- @field ui_watched? boolean diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua index cff0e5077e..3224c33b76 100644 --- a/runtime/lua/vim/_meta/api_keysets_extra.lua +++ b/runtime/lua/vim/_meta/api_keysets_extra.lua @@ -22,7 +22,7 @@ error('Cannot require a meta file') --- @field hl_group? string --- @field hl_eol? boolean --- ---- @field conceal? string +--- @field conceal? string|false --- @field spell? boolean --- @field ui_watched? boolean --- @field url? string diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 097ad0ea9a..8f0457a8ed 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -427,14 +427,29 @@ local function on_range_impl( local spell, spell_pri_offset = get_spell(capture_name) + local is_noconceal = capture_name == 'noconceal' + -- The "conceal" attribute can be set at the pattern level or on a particular capture + local conceal_attr = (metadata.conceal ~= nil and metadata.conceal) + or (metadata[capture] and metadata[capture].conceal) + local conceal ---@type boolean|string? + if is_noconceal then + conceal = false + else + conceal = conceal_attr + if conceal_attr == false then + is_noconceal = true + end + end + is_noconceal = is_noconceal or conceal_attr == false + local conceal_pri_offset = is_noconceal and 1 or 0 + -- The "priority" attribute can be set at the pattern level or on a particular capture local priority = ( vim._tointeger(metadata.priority or metadata[capture] and metadata[capture].priority) or vim.hl.priorities.treesitter - ) + spell_pri_offset - - -- The "conceal" attribute can be set at the pattern level or on a particular capture - local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal + ) + + spell_pri_offset + + conceal_pri_offset local url = get_url(match, buf, capture, metadata) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3e0196779b..337764c0c2 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -609,7 +609,7 @@ predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?'] ---@class vim.treesitter.query.TSMetadata ---@field range? Range ---@field offset? Range4 ----@field conceal? string +---@field conceal? string|boolean ---@field bo.commentstring? string ---@field [integer]? vim.treesitter.query.TSMetadata ---@field [string]? integer|string diff --git a/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua b/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua index 035199f674..767997809e 100644 --- a/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua +++ b/runtime/pack/dist/opt/nvim.tohtml/lua/tohtml.lua @@ -480,7 +480,8 @@ local function styletable_treesitter(state) if c ~= nil then local hlid = register_hl(state, '@' .. c .. '.' .. tree:lang()) if metadata.conceal and state.opt.conceallevel ~= 0 then - styletable_insert_conceal(state, srow + 1, scol + 1, erow + 1, ecol + 1, metadata.conceal) + local conceal = metadata.conceal == true and '' or metadata.conceal --[[@as string]] + styletable_insert_conceal(state, srow + 1, scol + 1, erow + 1, ecol + 1, conceal) end styletable_insert_range(state, srow + 1, scol + 1, erow + 1, ecol + 1, hlid) end diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 99095abb4d..45285b1b9f 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -507,11 +507,14 @@ ArrayOf(DictAs(get_extmark_item)) nvim_buf_get_extmarks(Buffer buf, Integer ns_i /// - cursorline_hl_group: highlight group used for the sign /// column text when the cursor is on the same line as the /// mark and 'cursorline' is enabled. -/// - conceal: string which should be either empty or a single -/// character. Enable concealing similar to |:syn-conceal|. -/// When a character is supplied it is used as |:syn-cchar|. -/// "hl_group" is used as highlight for the cchar if provided, -/// otherwise it defaults to |hl-Conceal|. +/// - conceal: either a boolean or a string. +/// - when a string is given, it should be either empty or a single +/// character. Enable concealing similar to |:syn-conceal|. +/// When a character is supplied it is used as |:syn-cchar|. +/// "hl_group" is used as highlight for the cchar if provided, +/// otherwise it defaults to |hl-Conceal|. +/// - when a boolean is given, true is equivalent to "" and false +/// overrides any existing conceal to remove it. /// - conceal_lines: string which should be empty. When /// provided, lines in the range are not drawn at all /// (according to 'conceallevel'); the next unconcealed line @@ -633,14 +636,25 @@ Integer nvim_buf_set_extmark(Buffer buf, Integer ns_id, Integer line, Integer co } if (HAS_KEY(opts, set_extmark, conceal)) { - hl.flags |= kSHConceal; + VALIDATE_EXP((opts->conceal.type == kObjectTypeBoolean + || opts->conceal.type == kObjectTypeString), + "conceal", "String or Boolean", api_typename(opts->conceal.type), { + goto error; + }); + has_hl = true; - if (opts->conceal.size > 0) { - int ch; - hl.conceal_char = utfc_ptr2schar(opts->conceal.data, &ch); - VALIDATE(hl.conceal_char && vim_isprintc(ch), "%s", "conceal char has to be printable", { - goto error; - }); + if (opts->conceal.type == kObjectTypeBoolean) { + hl.flags |= opts->conceal.data.boolean ? kSHConceal : kSHConcealOff; + } else { + String conceal_str = opts->conceal.data.string; + hl.flags |= kSHConceal; + if (conceal_str.size > 0) { + int ch; + hl.conceal_char = utfc_ptr2schar(conceal_str.data, &ch); + VALIDATE(hl.conceal_char && vim_isprintc(ch), "%s", "conceal char must be printable", { + goto error; + }); + } } } diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 3722a5e4c4..eb2f809ab2 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -55,7 +55,7 @@ typedef struct { HLGroupID number_hl_group; HLGroupID line_hl_group; HLGroupID cursorline_hl_group; - String conceal; + Union(String, Boolean) conceal; String conceal_lines; Boolean spell; Boolean ui_watched; diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index eda711e074..f0cf24fa28 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -119,7 +119,7 @@ void decor_redraw(buf_T *buf, int row1, int row2, int col1, DecorInline decor) void decor_redraw_sh(buf_T *buf, int row1, int row2, DecorSignHighlight sh) { if (sh.hl_id || (sh.url != NULL) - || (sh.flags & (kSHIsSign | kSHSpellOn | kSHSpellOff | kSHConceal))) { + || (sh.flags & (kSHIsSign | kSHSpellOn | kSHSpellOff | kSHConceal | kSHConcealOff))) { if (row2 >= row1) { redraw_buf_range_later(buf, row1 + 1, row2 + 1); } @@ -644,7 +644,7 @@ void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end }; if (sh->hl_id || (sh->url != NULL) - || (sh->flags & (kSHConceal | kSHSpellOn | kSHSpellOff))) { + || (sh->flags & (kSHConcealOff | kSHConceal | kSHSpellOn | kSHSpellOff))) { if (sh->hl_id) { range.attr_id = syn_id2attr(sh->hl_id); } @@ -800,6 +800,10 @@ next_mark: } } + if (r->kind == kDecorKindHighlight && (r->data.sh.flags & kSHConcealOff)) { + conceal = 0; + } + if (r->kind == kDecorKindHighlight) { if (r->data.sh.flags & kSHSpellOn) { spell = kTrue; @@ -1213,7 +1217,9 @@ void decor_to_dict_legacy(Dict *dict, DecorInline decor, bool hl_name, Arena *ar priority = sh_hl.priority; } - if (sh_hl.flags & kSHConceal) { + if (sh_hl.flags & kSHConcealOff) { + PUT_C(*dict, "conceal", BOOLEAN_OBJ(false)); + } else if (sh_hl.flags & kSHConceal) { char buf[MAX_SCHAR_SIZE]; schar_get(buf, sh_hl.text[0]); PUT_C(*dict, "conceal", CSTR_TO_ARENA_OBJ(arena, buf)); diff --git a/src/nvim/decoration_defs.h b/src/nvim/decoration_defs.h index c392880818..56e8eb634a 100644 --- a/src/nvim/decoration_defs.h +++ b/src/nvim/decoration_defs.h @@ -56,6 +56,7 @@ enum { kSHSpellOff = 32, kSHConceal = 64, kSHConcealLines = 128, + kSHConcealOff = 256, }; typedef struct { diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua index 87b7c272be..8733e17856 100644 --- a/test/functional/treesitter/highlight_spec.lua +++ b/test/functional/treesitter/highlight_spec.lua @@ -864,6 +864,37 @@ describe('treesitter highlighting (C)', function() }) end) + it('supports @noconceal', function() + insert([[ + int foo = bar; + ]]) + + exec_lua(function() + vim.opt.cole = 2 + local parser = vim.treesitter.get_parser(0, 'c') + vim.treesitter.highlighter.new(parser, { + queries = { + c = [[ + ((identifier) @conceal + (#set! conceal "X")) + + ((identifier) @noconceal + (#eq? @noconceal "bar")) + ]], + }, + }) + end) + + screen:expect({ + grid = [[ + int {14:X} = bar; | + ^ | + {1:~ }|*15 + | + ]], + }) + end) + it('@foo.bar groups has the correct fallback behavior', function() local get_hl = function(name) return api.nvim_get_hl_by_name(name, 1).foreground diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 911969d14c..95c9421595 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -2175,7 +2175,7 @@ describe('extmark decorations', function() command('set conceallevel=1') screen:expect_unchanged() - eq('conceal char has to be printable', pcall_err(api.nvim_buf_set_extmark, 0, ns, 0, 0, { end_col = 0, end_row = 2, conceal = '\255' })) + eq('conceal char must be printable', pcall_err(api.nvim_buf_set_extmark, 0, ns, 0, 0, { end_col = 0, end_row = 2, conceal = '\255' })) end) it('conceal with composed conceal char', function()