Merge pull request #19931 from bfredl/scopedhl

feat(highlight)!: use scoped @foo.bar.special groups for tree-sitter highlight
This commit is contained in:
bfredl
2022-08-26 14:43:58 +02:00
committed by GitHub
8 changed files with 272 additions and 160 deletions

View File

@@ -323,16 +323,34 @@ for a buffer with this code: >
local query2 = [[ ... ]] local query2 = [[ ... ]]
highlighter:set_query(query2) highlighter:set_query(query2)
As mentioned above the supported predicate is currently only `eq?`. `match?`
predicates behave like matching always fails. As an addition a capture which *lua-treesitter-highlight-groups*
begin with an upper-case letter like `@WarningMsg` will map directly to this The capture names, with `@` included, are directly usable as highlight groups.
highlight group, if defined. Also if the predicate begins with upper-case and A fallback system is implemented, so that more specific groups fallback to
contains a dot only the part before the first will be interpreted as the more generic ones. For instance, in a language that has separate doc
highlight group. As an example, this warns of a binary expression with two comments, `@comment.doc` could be used. If this group is not defined, the
highlighting for an ordinary `@comment` is used. This way, existing color
schemes already work out of the box, but it is possible to add
more specific variants for queries that make them available.
As an additional rule, captures highlights can always be specialized by
language, by appending the language name after an additional dot. For
instance, to highlight comments differently per language: >
hi @comment.c guifg=Blue
hi @comment.lua @guifg=DarkBlue
hi link @comment.doc.java String
<
It is possible to use custom highlight groups. As an example, if we
define the `@warning` group: >
hi link @warning WarningMsg
<
the following query warns of a binary expression with two
identical identifiers, highlighting both as |hl-WarningMsg|: > identical identifiers, highlighting both as |hl-WarningMsg|: >
((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) ((binary_expression left: (identifier) @warning.left right: (identifier) @warning.right)
(eq? @WarningMsg.left @WarningMsg.right)) (eq? @warning.left @warning.right))
< <
Treesitter Highlighting Priority *lua-treesitter-highlight-priority* Treesitter Highlighting Priority *lua-treesitter-highlight-priority*
@@ -352,6 +370,18 @@ attribute: >
============================================================================== ==============================================================================
Lua module: vim.treesitter *lua-treesitter-core* Lua module: vim.treesitter *lua-treesitter-core*
*get_captures_at_position()*
get_captures_at_position({bufnr}, {row}, {col})
Gets a list of captures for a given cursor position
Parameters: ~
{bufnr} (number) The buffer number
{row} (number) The position row
{col} (number) The position column
Return: ~
(table) A table of captures
get_node_range({node_or_range}) *get_node_range()* get_node_range({node_or_range}) *get_node_range()*
Get the node's range or unpack a range table Get the node's range or unpack a range table
@@ -393,6 +423,14 @@ is_ancestor({dest}, {source}) *is_ancestor()*
Return: ~ Return: ~
(boolean) True if dest is an ancestor of source (boolean) True if dest is an ancestor of source
is_in_node_range({node}, {line}, {col}) *is_in_node_range()*
Determines whether (line, col) position is in node range
Parameters: ~
{node} Node defining the range
{line} A line (0-based)
{col} A column (0-based)
node_contains({node}, {range}) *node_contains()* node_contains({node}, {range}) *node_contains()*
Determines if a node contains a range Determines if a node contains a range

View File

@@ -154,6 +154,28 @@ function M.get_node_range(node_or_range)
end end
end end
---Determines whether (line, col) position is in node range
---
---@param node Node defining the range
---@param line A line (0-based)
---@param col A column (0-based)
function M.is_in_node_range(node, line, col)
local start_line, start_col, end_line, end_col = M.get_node_range(node)
if line >= start_line and line <= end_line then
if line == start_line and line == end_line then
return col >= start_col and col < end_col
elseif line == start_line then
return col >= start_col
elseif line == end_line then
return col < end_col
else
return true
end
else
return false
end
end
---Determines if a node contains a range ---Determines if a node contains a range
---@param node table The node ---@param node table The node
---@param range table The range ---@param range table The range
@@ -167,4 +189,56 @@ function M.node_contains(node, range)
return start_fits and end_fits return start_fits and end_fits
end end
---Gets a list of captures for a given cursor position
---@param bufnr number The buffer number
---@param row number The position row
---@param col number The position column
---
---@returns (table) A table of captures
function M.get_captures_at_position(bufnr, row, col)
if bufnr == 0 then
bufnr = a.nvim_get_current_buf()
end
local buf_highlighter = M.highlighter.active[bufnr]
if not buf_highlighter then
return {}
end
local matches = {}
buf_highlighter.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
end
local root = tstree:root()
local root_start_row, _, root_end_row, _ = root:range()
-- Only worry about trees within the line range
if root_start_row > row or root_end_row < row then
return
end
local q = buf_highlighter:get_query(tree:lang())
-- Some injected languages may not have highlight queries.
if not q:query() then
return
end
local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
for capture, node, metadata in iter do
if M.is_in_node_range(node, row, col) then
local c = q._query.captures[capture] -- name of the capture in the query
if c ~= nil then
table.insert(matches, { capture = c, priority = metadata.priority })
end
end
end
end, true)
return matches
end
return M return M

View File

@@ -12,105 +12,18 @@ TSHighlighterQuery.__index = TSHighlighterQuery
local ns = a.nvim_create_namespace('treesitter/highlighter') local ns = a.nvim_create_namespace('treesitter/highlighter')
local _default_highlights = {}
local _link_default_highlight_once = function(from, to)
if not _default_highlights[from] then
_default_highlights[from] = true
a.nvim_set_hl(0, from, { link = to, default = true })
end
return from
end
-- If @definition.special does not exist use @definition instead
local subcapture_fallback = {
__index = function(self, capture)
local rtn
local shortened = capture
while not rtn and shortened do
shortened = shortened:match('(.*)%.')
rtn = shortened and rawget(self, shortened)
end
rawset(self, capture, rtn or '__notfound')
return rtn
end,
}
TSHighlighter.hl_map = setmetatable({
['error'] = 'Error',
['text.underline'] = 'Underlined',
['todo'] = 'Todo',
['debug'] = 'Debug',
-- Miscs
['comment'] = 'Comment',
['punctuation.delimiter'] = 'Delimiter',
['punctuation.bracket'] = 'Delimiter',
['punctuation.special'] = 'Delimiter',
-- Constants
['constant'] = 'Constant',
['constant.builtin'] = 'Special',
['constant.macro'] = 'Define',
['define'] = 'Define',
['macro'] = 'Macro',
['string'] = 'String',
['string.regex'] = 'String',
['string.escape'] = 'SpecialChar',
['character'] = 'Character',
['character.special'] = 'SpecialChar',
['number'] = 'Number',
['boolean'] = 'Boolean',
['float'] = 'Float',
-- Functions
['function'] = 'Function',
['function.special'] = 'Function',
['function.builtin'] = 'Special',
['function.macro'] = 'Macro',
['parameter'] = 'Identifier',
['method'] = 'Function',
['field'] = 'Identifier',
['property'] = 'Identifier',
['constructor'] = 'Special',
-- Keywords
['conditional'] = 'Conditional',
['repeat'] = 'Repeat',
['label'] = 'Label',
['operator'] = 'Operator',
['keyword'] = 'Keyword',
['exception'] = 'Exception',
['type'] = 'Type',
['type.builtin'] = 'Type',
['type.qualifier'] = 'Type',
['type.definition'] = 'Typedef',
['storageclass'] = 'StorageClass',
['structure'] = 'Structure',
['include'] = 'Include',
['preproc'] = 'PreProc',
}, subcapture_fallback)
---@private
local function is_highlight_name(capture_name)
local firstc = string.sub(capture_name, 1, 1)
return firstc ~= string.lower(firstc)
end
---@private ---@private
function TSHighlighterQuery.new(lang, query_string) function TSHighlighterQuery.new(lang, query_string)
local self = setmetatable({}, { __index = TSHighlighterQuery }) local self = setmetatable({}, { __index = TSHighlighterQuery })
self.hl_cache = setmetatable({}, { self.hl_cache = setmetatable({}, {
__index = function(table, capture) __index = function(table, capture)
local hl, is_vim_highlight = self:_get_hl_from_capture(capture) local name = self._query.captures[capture]
if not is_vim_highlight then local id = 0
hl = _link_default_highlight_once(lang .. hl, hl) if not vim.startswith(name, '_') then
id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang)
end end
local id = a.nvim_get_hl_id_by_name(hl)
rawset(table, capture, id) rawset(table, capture, id)
return id return id
end, end,
@@ -130,20 +43,6 @@ function TSHighlighterQuery:query()
return self._query return self._query
end end
---@private
--- Get the hl from capture.
--- Returns a tuple { highlight_name: string, is_builtin: bool }
function TSHighlighterQuery:_get_hl_from_capture(capture)
local name = self._query.captures[capture]
if is_highlight_name(name) then
-- From "Normal.left" only keep "Normal"
return vim.split(name, '.', true)[1], true
else
return TSHighlighter.hl_map[name] or 0, false
end
end
--- Creates a new highlighter using @param tree --- Creates a new highlighter using @param tree
--- ---
---@param tree The language tree to use for highlighting ---@param tree The language tree to use for highlighting

View File

@@ -2059,7 +2059,7 @@ static int ExpandOther(expand_T *xp, regmatch_T *rmp, int *num_file, char ***fil
{ EXPAND_MENUNAMES, get_menu_names, false, true }, { EXPAND_MENUNAMES, get_menu_names, false, true },
{ EXPAND_SYNTAX, get_syntax_name, true, true }, { EXPAND_SYNTAX, get_syntax_name, true, true },
{ EXPAND_SYNTIME, get_syntime_arg, true, true }, { EXPAND_SYNTIME, get_syntime_arg, true, true },
{ EXPAND_HIGHLIGHT, (ExpandFunc)get_highlight_name, true, true }, { EXPAND_HIGHLIGHT, (ExpandFunc)get_highlight_name, true, false },
{ EXPAND_EVENTS, expand_get_event_name, true, false }, { EXPAND_EVENTS, expand_get_event_name, true, false },
{ EXPAND_AUGROUP, expand_get_augroup_name, true, false }, { EXPAND_AUGROUP, expand_get_augroup_name, true, false },
{ EXPAND_CSCOPE, get_cscope_name, true, true }, { EXPAND_CSCOPE, get_cscope_name, true, true },

View File

@@ -79,6 +79,8 @@ typedef struct {
int sg_rgb_sp_idx; ///< RGB special color index int sg_rgb_sp_idx; ///< RGB special color index
int sg_blend; ///< blend level (0-100 inclusive), -1 if unset int sg_blend; ///< blend level (0-100 inclusive), -1 if unset
int sg_parent; ///< parent of @nested.group
} HlGroup; } HlGroup;
enum { enum {
@@ -183,6 +185,54 @@ static const char *highlight_init_both[] = {
"default link DiagnosticSignWarn DiagnosticWarn", "default link DiagnosticSignWarn DiagnosticWarn",
"default link DiagnosticSignInfo DiagnosticInfo", "default link DiagnosticSignInfo DiagnosticInfo",
"default link DiagnosticSignHint DiagnosticHint", "default link DiagnosticSignHint DiagnosticHint",
"default link @error Error",
"default link @text.underline Underlined",
"default link @todo Todo",
"default link @debug Debug",
// Miscs
"default link @comment Comment",
"default link @punctuation Delimiter",
// Constants
"default link @constant Constant",
"default link @constant.builtin Special",
"default link @constant.macro Define",
"default link @define Define",
"default link @macro Macro",
"default link @string String",
"default link @string.escape SpecialChar",
"default link @character Character",
"default link @character.special SpecialChar",
"default link @number Number",
"default link @boolean Boolean",
"default link @float Float",
// Functions
"default link @function Function",
"default link @function.builtin Special",
"default link @function.macro Macro",
"default link @parameter Identifier",
"default link @method Function",
"default link @field Identifier",
"default link @property Identifier",
"default link @constructor Special",
// Keywords
"default link @conditional Conditional",
"default link @repeat Repeat",
"default link @label Label",
"default link @operator Operator",
"default link @keyword Keyword",
"default link @exception Exception",
"default link @type Type",
"default link @type.definition Typedef",
"default link @storageclass StorageClass",
"default link @structure Structure",
"default link @include Include",
"default link @preproc PreProc",
NULL NULL
}; };
@@ -1375,6 +1425,11 @@ static void highlight_list_one(const int id)
return; return;
} }
// don't list specialized groups if a parent is used instead
if (sgp->sg_parent && sgp->sg_cleared) {
return;
}
didh = highlight_list_arg(id, didh, LIST_ATTR, didh = highlight_list_arg(id, didh, LIST_ATTR,
sgp->sg_cterm, NULL, "cterm"); sgp->sg_cterm, NULL, "cterm");
didh = highlight_list_arg(id, didh, LIST_INT, didh = highlight_list_arg(id, didh, LIST_INT,
@@ -1661,7 +1716,12 @@ static void set_hl_attr(int idx)
int syn_name2id(const char *name) int syn_name2id(const char *name)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
return syn_name2id_len(name, STRLEN(name)); if (name[0] == '@') {
// if we look up @aaa.bbb, we have to consider @aaa as well
return syn_check_group(name, strlen(name));
} else {
return syn_name2id_len(name, STRLEN(name));
}
} }
/// Lookup a highlight group name and return its ID. /// Lookup a highlight group name and return its ID.
@@ -1758,6 +1818,14 @@ static int syn_add_group(const char *name, size_t len)
} }
} }
int scoped_parent = 0;
if (len > 1 && name[0] == '@') {
char *delim = xmemrchr(name, '.', len);
if (delim) {
scoped_parent = syn_check_group(name, (size_t)(delim - name));
}
}
// First call for this growarray: init growing array. // First call for this growarray: init growing array.
if (highlight_ga.ga_data == NULL) { if (highlight_ga.ga_data == NULL) {
highlight_ga.ga_itemsize = sizeof(HlGroup); highlight_ga.ga_itemsize = sizeof(HlGroup);
@@ -1783,6 +1851,9 @@ static int syn_add_group(const char *name, size_t len)
hlgp->sg_rgb_sp_idx = kColorIdxNone; hlgp->sg_rgb_sp_idx = kColorIdxNone;
hlgp->sg_blend = -1; hlgp->sg_blend = -1;
hlgp->sg_name_u = arena_memdupz(&highlight_arena, name, len); hlgp->sg_name_u = arena_memdupz(&highlight_arena, name, len);
hlgp->sg_parent = scoped_parent;
// will get set to false by caller if settings are added
hlgp->sg_cleared = true;
vim_strup((char_u *)hlgp->sg_name_u); vim_strup((char_u *)hlgp->sg_name_u);
int id = highlight_ga.ga_len; // ID is index plus one int id = highlight_ga.ga_len; // ID is index plus one
@@ -1844,10 +1915,13 @@ int syn_ns_get_final_id(int *ns_id, int hl_id)
continue; continue;
} }
if (sgp->sg_link == 0 || sgp->sg_link > highlight_ga.ga_len) { if (sgp->sg_link > 0 && sgp->sg_link <= highlight_ga.ga_len) {
hl_id = sgp->sg_link;
} else if (sgp->sg_cleared && sgp->sg_parent > 0) {
hl_id = sgp->sg_parent;
} else {
break; break;
} }
hl_id = sgp->sg_link;
} }
return hl_id; return hl_id;

View File

@@ -244,7 +244,7 @@ func Test_match_completion()
return return
endif endif
hi Aardig ctermfg=green hi Aardig ctermfg=green
call feedkeys(":match \<Tab>\<Home>\"\<CR>", 'xt') call feedkeys(":match A\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"match Aardig', getreg(':')) call assert_equal('"match Aardig', getreg(':'))
call feedkeys(":match \<S-Tab>\<Home>\"\<CR>", 'xt') call feedkeys(":match \<S-Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"match none', getreg(':')) call assert_equal('"match none', getreg(':'))
@@ -255,9 +255,7 @@ func Test_highlight_completion()
return return
endif endif
hi Aardig ctermfg=green hi Aardig ctermfg=green
call feedkeys(":hi \<Tab>\<Home>\"\<CR>", 'xt') call feedkeys(":hi default A\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"hi Aardig', getreg(':'))
call feedkeys(":hi default \<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"hi default Aardig', getreg(':')) call assert_equal('"hi default Aardig', getreg(':'))
call feedkeys(":hi clear Aa\<Tab>\<Home>\"\<CR>", 'xt') call feedkeys(":hi clear Aa\<Tab>\<Home>\"\<CR>", 'xt')
call assert_equal('"hi clear Aardig', getreg(':')) call assert_equal('"hi clear Aardig', getreg(':'))

View File

@@ -188,22 +188,22 @@ func Test_syntax_completion()
call assert_equal('"syn sync ccomment clear fromstart linebreaks= linecont lines= match maxlines= minlines= region', @:) call assert_equal('"syn sync ccomment clear fromstart linebreaks= linecont lines= match maxlines= minlines= region', @:)
" Check that clearing "Aap" avoids it showing up before Boolean. " Check that clearing "Aap" avoids it showing up before Boolean.
hi Aap ctermfg=blue hi @Aap ctermfg=blue
call feedkeys(":syn list \<C-A>\<C-B>\"\<CR>", 'tx') call feedkeys(":syn list \<C-A>\<C-B>\"\<CR>", 'tx')
call assert_match('^"syn list Aap Boolean Character ', @:) call assert_match('^"syn list @Aap @boolean @character ', @:)
hi clear Aap hi clear @Aap
call feedkeys(":syn list \<C-A>\<C-B>\"\<CR>", 'tx') call feedkeys(":syn list \<C-A>\<C-B>\"\<CR>", 'tx')
call assert_match('^"syn list Boolean Character ', @:) call assert_match('^"syn list @boolean @character ', @:)
call feedkeys(":syn match \<C-A>\<C-B>\"\<CR>", 'tx') call feedkeys(":syn match \<C-A>\<C-B>\"\<CR>", 'tx')
call assert_match('^"syn match Boolean Character ', @:) call assert_match('^"syn match @boolean @character ', @:)
endfunc endfunc
func Test_echohl_completion() func Test_echohl_completion()
call feedkeys(":echohl no\<C-A>\<C-B>\"\<CR>", 'tx') call feedkeys(":echohl no\<C-A>\<C-B>\"\<CR>", 'tx')
" call assert_equal('"echohl NonText Normal none', @:) " call assert_equal('"echohl NonText Normal none', @:)
call assert_equal('"echohl NonText Normal NormalFloat NormalNC none', @:) call assert_equal('"echohl NonText Normal NormalFloat none', @:)
endfunc endfunc
func Test_syntax_arg_skipped() func Test_syntax_arg_skipped()

View File

@@ -6,11 +6,14 @@ local insert = helpers.insert
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local feed = helpers.feed local feed = helpers.feed
local pending_c_parser = helpers.pending_c_parser local pending_c_parser = helpers.pending_c_parser
local command = helpers.command
local meths = helpers.meths
local eq = helpers.eq
before_each(clear) before_each(clear)
local hl_query = [[ local hl_query = [[
(ERROR) @ErrorMsg (ERROR) @error
"if" @keyword "if" @keyword
"else" @keyword "else" @keyword
@@ -23,23 +26,24 @@ local hl_query = [[
"enum" @type "enum" @type
"extern" @type "extern" @type
(string_literal) @string.nonexistent-specializer-for-string.should-fallback-to-string ; nonexistent specializer for string should fallback to string
(string_literal) @string.nonexistent_specializer
(number_literal) @number (number_literal) @number
(char_literal) @string (char_literal) @string
(type_identifier) @type (type_identifier) @type
((type_identifier) @Special (#eq? @Special "LuaRef")) ((type_identifier) @constant.builtin (#eq? @constant.builtin "LuaRef"))
(primitive_type) @type (primitive_type) @type
(sized_type_specifier) @type (sized_type_specifier) @type
; Use lua regexes ; Use lua regexes
((identifier) @Identifier (#contains? @Identifier "lua_")) ((identifier) @function (#contains? @function "lua_"))
((identifier) @Constant (#lua-match? @Constant "^[A-Z_]+$")) ((identifier) @Constant (#lua-match? @Constant "^[A-Z_]+$"))
((identifier) @Normal (#vim-match? @Constant "^lstate$")) ((identifier) @Normal (#vim-match? @Normal "^lstate$"))
((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (#eq? @WarningMsg.left @WarningMsg.right)) ((binary_expression left: (identifier) @warning.left right: (identifier) @warning.right) (#eq? @warning.left @warning.right))
(comment) @comment (comment) @comment
]] ]]
@@ -103,6 +107,7 @@ describe('treesitter highlighting', function()
} }
exec_lua([[ hl_query = ... ]], hl_query) exec_lua([[ hl_query = ... ]], hl_query)
command [[ hi link @warning WarningMsg ]]
end) end)
it('is updated with edits', function() it('is updated with edits', function()
@@ -547,7 +552,7 @@ describe('treesitter highlighting', function()
-- This will change ONLY the literal strings to look like comments -- This will change ONLY the literal strings to look like comments
-- The only literal string is the "vim.schedule: expected function" in this test. -- The only literal string is the "vim.schedule: expected function" in this test.
exec_lua [[vim.cmd("highlight link cString comment")]] exec_lua [[vim.cmd("highlight link @string.nonexistent_specializer comment")]]
screen:expect{grid=[[ screen:expect{grid=[[
{2:/// Schedule Lua callback on main loop's event queue} | {2:/// Schedule Lua callback on main loop's event queue} |
{3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) | {3:static} {3:int} {11:nlua_schedule}({3:lua_State} *{3:const} lstate) |
@@ -612,6 +617,11 @@ describe('treesitter highlighting', function()
-- bold will not be overwritten at the moment -- bold will not be overwritten at the moment
[12] = {background = Screen.colors.Red, bold = true, foreground = Screen.colors.Grey100}; [12] = {background = Screen.colors.Red, bold = true, foreground = Screen.colors.Grey100};
}} }}
eq({
{capture='Error', priority='101'};
{capture='type'};
}, exec_lua [[ return vim.treesitter.get_captures_at_position(0, 0, 2) ]])
end) end)
it("allows to use captures with dots (don't use fallback when specialization of foo exists)", function() it("allows to use captures with dots (don't use fallback when specialization of foo exists)", function()
@@ -642,11 +652,13 @@ describe('treesitter highlighting', function()
| |
]]} ]]}
command [[
hi link @foo.bar Type
hi link @foo String
]]
exec_lua [[ exec_lua [[
local parser = vim.treesitter.get_parser(0, "c", {}) local parser = vim.treesitter.get_parser(0, "c", {})
local highlighter = vim.treesitter.highlighter local highlighter = vim.treesitter.highlighter
highlighter.hl_map['foo.bar'] = 'Type'
highlighter.hl_map['foo'] = 'String'
test_hl = highlighter.new(parser, {queries = {c = "(primitive_type) @foo.bar (string_literal) @foo"}}) test_hl = highlighter.new(parser, {queries = {c = "(primitive_type) @foo.bar (string_literal) @foo"}})
]] ]]
@@ -670,6 +682,29 @@ describe('treesitter highlighting', function()
{1:~ }| {1:~ }|
| |
]]} ]]}
-- clearing specialization reactivates fallback
command [[ hi clear @foo.bar ]]
screen:expect{grid=[[
{5:char}* x = {5:"Will somebody ever read this?"}; |
^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
end) end)
it("supports conceal attribute", function() it("supports conceal attribute", function()
@@ -712,32 +747,26 @@ describe('treesitter highlighting', function()
]]} ]]}
end) end)
it("hl_map has the correct fallback behavior", function() it("@foo.bar groups has the correct fallback behavior", function()
exec_lua [[ local get_hl = function(name) return meths.get_hl_by_name(name,1).foreground end
local hl_map = vim.treesitter.highlighter.hl_map meths.set_hl(0, "@foo", {fg = 1})
hl_map["foo"] = 1 meths.set_hl(0, "@foo.bar", {fg = 2})
hl_map["foo.bar"] = 2 meths.set_hl(0, "@foo.bar.baz", {fg = 3})
hl_map["foo.bar.baz"] = 3
assert(hl_map["foo"] == 1) eq(1, get_hl"@foo")
assert(hl_map["foo.a.b.c.d"] == 1) eq(1, get_hl"@foo.a.b.c.d")
assert(hl_map["foo.bar"] == 2) eq(2, get_hl"@foo.bar")
assert(hl_map["foo.bar.a.b.c.d"] == 2) eq(2, get_hl"@foo.bar.a.b.c.d")
assert(hl_map["foo.bar.baz"] == 3) eq(3, get_hl"@foo.bar.baz")
assert(hl_map["foo.bar.baz.d"] == 3) eq(3, get_hl"@foo.bar.baz.d")
hl_map["FOO"] = 1 -- lookup is case insensitive
hl_map["FOO.BAR"] = 2 eq(2, get_hl"@FOO.BAR.SPAM")
assert(hl_map["FOO.BAR.BAZ"] == 2)
hl_map["foo.missing.exists"] = 3
assert(hl_map["foo.missing"] == 1)
assert(hl_map["foo.missing.exists"] == 3)
assert(hl_map["foo.missing.exists.bar"] == 3)
assert(hl_map["total.nonsense.but.a.lot.of.dots"] == nil)
-- It will not perform a second look up of this variable but return a sentinel value
assert(hl_map["total.nonsense.but.a.lot.of.dots"] == "__notfound")
]]
meths.set_hl(0, "@foo.missing.exists", {fg = 3})
eq(1, get_hl"@foo.missing")
eq(3, get_hl"@foo.missing.exists")
eq(3, get_hl"@foo.missing.exists.bar")
eq(nil, get_hl"@total.nonsense.but.a.lot.of.dots")
end) end)
end) end)