From 030b422d1e9517ed1b1c70fd8002b74881c80650 Mon Sep 17 00:00:00 2001 From: bfredl Date: Wed, 24 Aug 2022 23:48:52 +0200 Subject: feat(treesitter)!: use @foo.bar style highlight groups This removes the support for defining links via vim.treesitter.highlighter.hl_map (never documented, but plugins did anyway), or the uppercase-only `@FooGroup.Bar` to `FooGroup` rule. The fallback is now strictly `@foo.bar.lang` to `@foo.bar` to `@foo`, and casing is irrelevant (as it already was outside of treesitter) For compatibility, define default links to builting syntax groups as defined by pre-existing color schemes --- runtime/doc/treesitter.txt | 34 ++++++--- runtime/lua/vim/treesitter/highlighter.lua | 109 ++--------------------------- 2 files changed, 30 insertions(+), 113 deletions(-) (limited to 'runtime') diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index c80d8f4479..99780b0f29 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* 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 -- cgit From 064ecb9ec581ec68f1376cde5ad7f960419b0324 Mon Sep 17 00:00:00 2001 From: Quentin Rasmont Date: Thu, 2 Jun 2022 20:07:08 +0200 Subject: feat(treesitter): upstream get_hl_groups_at_position() Util from the nvim-treesitter project. --- runtime/lua/vim/treesitter.lua | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) (limited to 'runtime') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 82d41070ee..8ba4525003 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -2,6 +2,7 @@ local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local LanguageTree = require('vim.treesitter.languagetree') +local highlighter = require('vim.treesitter.highlighter') -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with @@ -167,4 +168,60 @@ function M.node_contains(node, range) return start_fits and end_fits end +---Gets a list of highlight group 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 highlight groups +function M.get_hl_groups_at_position(bufnr, row, col) + local buf_highlighter = 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 + local hl = q.hl_cache[capture] + + if hl and 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 + local general_hl, is_vim_hl = q:_get_hl_from_capture(capture) + local local_hl = not is_vim_hl and (tree:lang() .. general_hl) + table.insert( + matches, + { capture = c, specific = local_hl, general = general_hl, priority = metadata.priority } + ) + end + end + end + end, true) + return matches +end + return M -- cgit From b04ef7f6b966c44b12dbc65b17a761ae9313d6c4 Mon Sep 17 00:00:00 2001 From: bfredl Date: Thu, 25 Aug 2022 21:41:52 +0200 Subject: fix(treesitter): make it get_captures_at_position --- runtime/doc/treesitter.txt | 20 +++++++++++++++++++ runtime/lua/vim/treesitter.lua | 45 +++++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 14 deletions(-) (limited to 'runtime') diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 99780b0f29..2c6c9e4ed8 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -370,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 @@ -411,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 8ba4525003..6431162799 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -2,7 +2,6 @@ local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local LanguageTree = require('vim.treesitter.languagetree') -local highlighter = require('vim.treesitter.highlighter') -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with @@ -155,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 @@ -168,14 +189,17 @@ function M.node_contains(node, range) return start_fits and end_fits end ----Gets a list of highlight group for a given cursor position +---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 highlight groups -function M.get_hl_groups_at_position(bufnr, row, col) - local buf_highlighter = highlighter.active[bufnr] +---@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 {} @@ -206,17 +230,10 @@ function M.get_hl_groups_at_position(bufnr, row, col) local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1) for capture, node, metadata in iter do - local hl = q.hl_cache[capture] - - if hl and M.is_in_node_range(node, row, col) then + 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 - local general_hl, is_vim_hl = q:_get_hl_from_capture(capture) - local local_hl = not is_vim_hl and (tree:lang() .. general_hl) - table.insert( - matches, - { capture = c, specific = local_hl, general = general_hl, priority = metadata.priority } - ) + table.insert(matches, { capture = c, priority = metadata.priority }) end end end -- cgit