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.
This commit is contained in:
Peter Cardenas
2026-04-25 08:42:44 -07:00
committed by GitHub
parent b70224e3bd
commit eeee4bd4fc
15 changed files with 116 additions and 35 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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;
});
}
}
}

View File

@@ -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;

View File

@@ -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));

View File

@@ -56,6 +56,7 @@ enum {
kSHSpellOff = 32,
kSHConceal = 64,
kSHConcealLines = 128,
kSHConcealOff = 256,
};
typedef struct {

View File

@@ -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

View File

@@ -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()