diff options
author | bfredl <bjorn.linse@gmail.com> | 2022-08-26 14:43:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-26 14:43:58 +0200 |
commit | 2ecb4076dfa2236f6610449bbcb3f887f1c4f9f1 (patch) | |
tree | 181f1775ca77c2b9ce50083604254f68cacf43e4 | |
parent | 6547f4397fe643f194763306f8fcdfc6f6ce4b24 (diff) | |
parent | b04ef7f6b966c44b12dbc65b17a761ae9313d6c4 (diff) | |
download | rneovim-2ecb4076dfa2236f6610449bbcb3f887f1c4f9f1.tar.gz rneovim-2ecb4076dfa2236f6610449bbcb3f887f1c4f9f1.tar.bz2 rneovim-2ecb4076dfa2236f6610449bbcb3f887f1c4f9f1.zip |
Merge pull request #19931 from bfredl/scopedhl
feat(highlight)!: use scoped @foo.bar.special groups for tree-sitter highlight
-rw-r--r-- | runtime/doc/treesitter.txt | 54 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 74 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 109 | ||||
-rw-r--r-- | src/nvim/cmdexpand.c | 2 | ||||
-rw-r--r-- | src/nvim/highlight_group.c | 80 | ||||
-rw-r--r-- | src/nvim/testdir/test_cmdline.vim | 6 | ||||
-rw-r--r-- | src/nvim/testdir/test_syntax.vim | 12 | ||||
-rw-r--r-- | test/functional/treesitter/highlight_spec.lua | 101 |
8 files changed, 275 insertions, 163 deletions
diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index c80d8f4479..2c6c9e4ed8 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -323,16 +323,34 @@ for a buffer with this code: > local 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 -begin with an upper-case letter like `@WarningMsg` will map directly to this -highlight group, if defined. Also if the predicate begins with upper-case and -contains a dot only the part before the first will be interpreted as the -highlight group. As an example, this warns of a binary expression with two + + *lua-treesitter-highlight-groups* +The capture names, with `@` included, are directly usable as highlight groups. +A fallback system is implemented, so that more specific groups fallback to +more generic ones. For instance, in a language that has separate doc +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|: > - ((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)) < Treesitter Highlighting Priority *lua-treesitter-highlight-priority* @@ -352,6 +370,18 @@ attribute: > ============================================================================== 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 the node's range or unpack a range table @@ -393,6 +423,14 @@ is_ancestor({dest}, {source}) *is_ancestor()* Return: ~ (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()* Determines if a node contains a range diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 82d41070ee..6431162799 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -154,6 +154,28 @@ function M.get_node_range(node_or_range) 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 ---@param node table The node ---@param range table The range @@ -167,4 +189,56 @@ function M.node_contains(node, range) return start_fits and end_fits 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 diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index e27a5fa9c3..1f242b0fdd 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -12,105 +12,18 @@ TSHighlighterQuery.__index = TSHighlighterQuery 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 function TSHighlighterQuery.new(lang, query_string) local self = setmetatable({}, { __index = TSHighlighterQuery }) self.hl_cache = setmetatable({}, { __index = function(table, capture) - local hl, is_vim_highlight = self:_get_hl_from_capture(capture) - if not is_vim_highlight then - hl = _link_default_highlight_once(lang .. hl, hl) + local name = self._query.captures[capture] + local id = 0 + if not vim.startswith(name, '_') then + id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end - local id = a.nvim_get_hl_id_by_name(hl) - rawset(table, capture, id) return id end, @@ -130,20 +43,6 @@ function TSHighlighterQuery:query() return self._query 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 --- ---@param tree The language tree to use for highlighting diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index abf36734b9..c16ed37c9a 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -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_SYNTAX, get_syntax_name, 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_AUGROUP, expand_get_augroup_name, true, false }, { EXPAND_CSCOPE, get_cscope_name, true, true }, diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 3d5c95002a..a63768a8e3 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -79,6 +79,8 @@ typedef struct { int sg_rgb_sp_idx; ///< RGB special color index int sg_blend; ///< blend level (0-100 inclusive), -1 if unset + + int sg_parent; ///< parent of @nested.group } HlGroup; enum { @@ -183,6 +185,54 @@ static const char *highlight_init_both[] = { "default link DiagnosticSignWarn DiagnosticWarn", "default link DiagnosticSignInfo DiagnosticInfo", "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 }; @@ -1375,6 +1425,11 @@ static void highlight_list_one(const int id) 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, sgp->sg_cterm, NULL, "cterm"); 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) 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. @@ -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. if (highlight_ga.ga_data == NULL) { 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_blend = -1; 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); 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; } - 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; } - hl_id = sgp->sg_link; } return hl_id; diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim index 84dc361529..517509004a 100644 --- a/src/nvim/testdir/test_cmdline.vim +++ b/src/nvim/testdir/test_cmdline.vim @@ -244,7 +244,7 @@ func Test_match_completion() return endif 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 feedkeys(":match \<S-Tab>\<Home>\"\<CR>", 'xt') call assert_equal('"match none', getreg(':')) @@ -255,9 +255,7 @@ func Test_highlight_completion() return endif hi Aardig ctermfg=green - call feedkeys(":hi \<Tab>\<Home>\"\<CR>", 'xt') - call assert_equal('"hi Aardig', getreg(':')) - call feedkeys(":hi default \<Tab>\<Home>\"\<CR>", 'xt') + call feedkeys(":hi default A\<Tab>\<Home>\"\<CR>", 'xt') call assert_equal('"hi default Aardig', getreg(':')) call feedkeys(":hi clear Aa\<Tab>\<Home>\"\<CR>", 'xt') call assert_equal('"hi clear Aardig', getreg(':')) diff --git a/src/nvim/testdir/test_syntax.vim b/src/nvim/testdir/test_syntax.vim index af93a37739..85f27dd043 100644 --- a/src/nvim/testdir/test_syntax.vim +++ b/src/nvim/testdir/test_syntax.vim @@ -188,22 +188,22 @@ func Test_syntax_completion() 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. - hi Aap ctermfg=blue + hi @Aap ctermfg=blue call feedkeys(":syn list \<C-A>\<C-B>\"\<CR>", 'tx') - call assert_match('^"syn list Aap Boolean Character ', @:) - hi clear Aap + call assert_match('^"syn list @Aap @boolean @character ', @:) + hi clear @Aap 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 assert_match('^"syn match Boolean Character ', @:) + call assert_match('^"syn match @boolean @character ', @:) endfunc func Test_echohl_completion() call feedkeys(":echohl no\<C-A>\<C-B>\"\<CR>", 'tx') " 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 func Test_syntax_arg_skipped() diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua index 5ec0a8a060..4b0bd1eb50 100644 --- a/test/functional/treesitter/highlight_spec.lua +++ b/test/functional/treesitter/highlight_spec.lua @@ -6,11 +6,14 @@ local insert = helpers.insert local exec_lua = helpers.exec_lua local feed = helpers.feed local pending_c_parser = helpers.pending_c_parser +local command = helpers.command +local meths = helpers.meths +local eq = helpers.eq before_each(clear) local hl_query = [[ - (ERROR) @ErrorMsg + (ERROR) @error "if" @keyword "else" @keyword @@ -23,23 +26,24 @@ local hl_query = [[ "enum" @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 (char_literal) @string (type_identifier) @type - ((type_identifier) @Special (#eq? @Special "LuaRef")) + ((type_identifier) @constant.builtin (#eq? @constant.builtin "LuaRef")) (primitive_type) @type (sized_type_specifier) @type ; Use lua regexes - ((identifier) @Identifier (#contains? @Identifier "lua_")) + ((identifier) @function (#contains? @function "lua_")) ((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 ]] @@ -103,6 +107,7 @@ describe('treesitter highlighting', function() } exec_lua([[ hl_query = ... ]], hl_query) + command [[ hi link @warning WarningMsg ]] end) 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 -- 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=[[ {2:/// Schedule Lua callback on main loop's event queue} | {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 [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) 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 [[ local parser = vim.treesitter.get_parser(0, "c", {}) 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"}}) ]] @@ -670,6 +682,29 @@ describe('treesitter highlighting', function() {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) it("supports conceal attribute", function() @@ -712,32 +747,26 @@ describe('treesitter highlighting', function() ]]} end) - it("hl_map has the correct fallback behavior", function() - exec_lua [[ - local hl_map = vim.treesitter.highlighter.hl_map - hl_map["foo"] = 1 - hl_map["foo.bar"] = 2 - hl_map["foo.bar.baz"] = 3 - - assert(hl_map["foo"] == 1) - assert(hl_map["foo.a.b.c.d"] == 1) - assert(hl_map["foo.bar"] == 2) - assert(hl_map["foo.bar.a.b.c.d"] == 2) - assert(hl_map["foo.bar.baz"] == 3) - assert(hl_map["foo.bar.baz.d"] == 3) - - hl_map["FOO"] = 1 - hl_map["FOO.BAR"] = 2 - 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") - ]] - + it("@foo.bar groups has the correct fallback behavior", function() + local get_hl = function(name) return meths.get_hl_by_name(name,1).foreground end + meths.set_hl(0, "@foo", {fg = 1}) + meths.set_hl(0, "@foo.bar", {fg = 2}) + meths.set_hl(0, "@foo.bar.baz", {fg = 3}) + + eq(1, get_hl"@foo") + eq(1, get_hl"@foo.a.b.c.d") + eq(2, get_hl"@foo.bar") + eq(2, get_hl"@foo.bar.a.b.c.d") + eq(3, get_hl"@foo.bar.baz") + eq(3, get_hl"@foo.bar.baz.d") + + -- lookup is case insensitive + eq(2, get_hl"@FOO.BAR.SPAM") + + 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) |