feat(locals)!: remove locals module

Problem: `locals.lua` was neither used nor tested, and providing it goes
against the goal of nvim-treesitter not being a required dependency for
other plugins.

Solution: Remove the module and document that the queries are provided
as-is for limited backward compatibility only.
This commit is contained in:
Christian Clason 2025-05-04 10:51:09 +02:00
parent 53dccb3a77
commit 18bb653917
3 changed files with 5 additions and 461 deletions

View file

@ -51,10 +51,10 @@ Each of these `scheme` files contains a _tree-sitter query_ for a given purpose.
Before going any further, we highly suggest that you [read more about tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries). Before going any further, we highly suggest that you [read more about tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries).
Each query has an appropriate name, which is then used by modules to extract data from the syntax tree. Each query has an appropriate name, which is then used by modules to extract data from the syntax tree.
For now these are the types of queries used by `nvim-treesitter`: For now these are the types of queries provided by `nvim-treesitter`:
- `highlights.scm`: used for syntax highlighting, using the `highlight` module. - `highlights.scm`: used for syntax highlighting, using the `highlight` module.
- `locals.scm`: used to extract keyword definitions, scopes, references, etc, using the `locals` module. - `locals.scm`: used to extract keyword definitions, scopes, references, etc. (not used in this plugin).
- `textobjects.scm`: used to define text objects. - `textobjects.scm`: used to define text objects.
- `folds.scm`: used to define folds. - `folds.scm`: used to define folds.
- `injections.scm`: used to define injections. - `injections.scm`: used to define injections.
@ -296,7 +296,8 @@ Locals are used to keep track of definitions and references in local or global
scopes, see [upstream scopes, see [upstream
documentation](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#local-variables). documentation](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#local-variables).
Note that nvim-treesitter uses more specific subcaptures for definitions and Note that nvim-treesitter uses more specific subcaptures for definitions and
**does not use locals for highlighting**. **does not use locals** (for highlighting or any other purpose). These queries
are only provided for (limited) backwards compatibility.
```query ```query
@local.definition ; various definitions @local.definition ; various definitions

View file

@ -118,7 +118,7 @@ Injections are used for multi-language documents, see `:h treesitter-language-in
## Locals ## Locals
These queries can be used to look up definitions and references to identifiers in a given scope. They are used, e.g., by the `nvim-treesitter-refactor` plugin. These queries can be used to look up definitions and references to identifiers in a given scope. They are not used in this plugin and are provided for (limited) backward compatibility.
# Advanced setup # Advanced setup

View file

@ -1,457 +0,0 @@
-- Functions to handle locals
-- Locals are a generalization of definition and scopes
-- it's the way nvim-treesitter uses to "understand" the code
local api = vim.api
local ts = vim.treesitter
local M = {}
local function get_named_children(node)
local nodes = {} ---@type TSNode[]
for i = 0, node:named_child_count() - 1, 1 do
nodes[i + 1] = node:named_child(i)
end
return nodes
end
-- Creates unique id for a node based on text and range
---@param scope TSNode: the scope node of the definition
---@param node_text string: the node text to use
---@return string: a string id
function M.get_definition_id(scope, node_text)
-- Add a valid starting character in case node text doesn't start with a valid one.
return table.concat({ 'k', node_text or '', scope:range() }, '_')
end
-- Gets a table with all the scopes containing a node
-- The order is from most specific to least (bottom up)
---@param node TSNode
---@param bufnr integer
---@return TSNode[]
function M.get_scope_tree(node, bufnr)
local scopes = {} ---@type TSNode[]
for scope in M.iter_scope_tree(node, bufnr) do
table.insert(scopes, scope)
end
return scopes
end
-- Iterates over a nodes scopes moving from the bottom up
---@param node TSNode
---@param bufnr integer
---@return fun(): TSNode?
function M.iter_scope_tree(node, bufnr)
local last_node = node ---@type TSNode?
return function()
if not last_node then
return
end
local scope = M.containing_scope(last_node, bufnr, false) or node:tree():root()
last_node = scope:parent()
return scope
end
end
-- Gets a table of all nodes and their 'kinds' from a locals list
---@param local_def TSLocal[] the local list result
---@return TSLocal[] a list of node entries
function M.get_local_nodes(local_def)
local result = {}
M.recurse_local_nodes(local_def, function(def, _, kind)
table.insert(result, vim.tbl_extend('keep', { kind = kind }, def))
end)
return result
end
-- Recurse locals results until a node is found.
-- The accumulator function is given
-- * The table of the node
-- * The node
-- * The full definition match `@local.definition.var.something` -> 'var.something'
-- * The last definition match `@local.definition.var.something` -> 'something'
---@param local_def TSLocal The locals result
---@param accumulator function The accumulator function
---@param full_match? string The full match path to append to
---@param last_match? string The last match
function M.recurse_local_nodes(local_def, accumulator, full_match, last_match)
if type(local_def) ~= 'table' then
return
end
if local_def.node then
accumulator(local_def, local_def.node, full_match, last_match)
else
for match_key, def in
pairs(local_def --[[@as {[string]: TSLocal}]])
do
M.recurse_local_nodes(
def,
accumulator,
full_match and (full_match .. '.' .. match_key) or match_key,
match_key
)
end
end
end
---Memoize a function using hash_fn to hash the arguments.
---@generic F: function
---@param fn F
---@param hash_fn fun(...): any
---@return F
local function memoize(fn, hash_fn)
local cache = setmetatable({}, { __mode = 'kv' }) ---@type table<any,any[]>
return function(...)
local key = hash_fn(...)
if cache[key] == nil then
local v = { fn(...) } ---@type any[]
for k, value in pairs(v) do
if value == nil then
value[k] = vim.NIL ---@type table
end
end
cache[key] = v
end
local v = cache[key]
for k, value in ipairs(v) do
if value == vim.NIL then
value[k] = nil ---@type table
end
end
return unpack(v)
end
end
---@param bufnr integer: the buffer
---@return TSNode|nil root: root node of the buffer
local function get_root(bufnr)
local parser = ts.get_parser(bufnr)
if not parser then
return
end
parser:parse()
return parser:trees()[1]:root()
end
---@param bufnr integer: the buffer
---@return vim.treesitter.Query|nil query: `locals` query
---@return TSNode|nil root: root node of the bufferocal function get_query(bufnr)
local function get_query(bufnr)
local root = get_root(bufnr)
local ft = vim.bo[bufnr].filetype
local lang = ts.language.get_lang(ft) or ft
local query = (ts.query.get(lang, 'locals'))
return query, root
end
---@alias TSScope "parent"|"local"|"global"
---@class TSLocal
---@field kind string
---@field node TSNode
---@field scope TSScope
-- Return all locals for the buffer
--
-- memoized by buffer tick
--
---@param bufnr integer buffer
---@return TSLocal[] definitions
---@return TSLocal[] references
---@return TSNode[] scopes
M.get = memoize(function(bufnr)
local query, root = get_query(bufnr)
if not query or not root then
return {}, {}, {}
end
local definitions = {}
local scopes = {}
local references = {}
for id, node, metadata in query:iter_captures(root, bufnr) do
local kind = query.captures[id]
local scope = 'local' ---@type string
for k, v in
pairs(metadata --[[@as {[integer|string]: string}]])
do
if type(k) == 'string' and vim.endswith(k, 'local.scope') then
scope = v
end
end
if node and vim.startswith(kind, 'local.definition') then
table.insert(definitions, { kind = kind, node = node, scope = scope })
end
if node and kind == 'local.scope' then
table.insert(scopes, node)
end
if node and kind == 'local.reference' then
table.insert(references, { kind = kind, node = node, scope = scope })
end
end
return definitions, references, scopes
end, function(bufnr)
local root = get_root(bufnr)
if not root then
return tostring(bufnr)
end
return tostring(root:id())
end)
-- Get a single dimension table to look definition nodes.
-- Keys are generated by using the range of the containing scope and the text of the definition node.
-- This makes looking up a definition for a given scope a simple key lookup.
--
-- This is memoized by buffer tick. If the function is called in succession
-- without the buffer tick changing, then the previous result will be used
-- since the syntax tree hasn't changed.
--
-- Usage lookups require finding the definition of the node, so `find_definition`
-- is called very frequently, which is why this lookup must be fast as possible.
--
---@param bufnr integer: the buffer
---@return TSLocal[] result: a table for looking up definitions
M.get_definitions_lookup_table = memoize(function(bufnr)
local definitions, _, _ = M.get(bufnr)
if not definitions then
return {}
end
local result = {} ---@type TSLocal[]
for _, definition in ipairs(definitions) do
for _, node_entry in ipairs(M.get_local_nodes(definition)) do
local scopes = M.get_definition_scopes(node_entry.node, bufnr, node_entry.scope)
-- Always use the highest valid scope
local scope = scopes[#scopes]
local node_text = ts.get_node_text(node_entry.node, bufnr)
local id = M.get_definition_id(scope, node_text)
result[id] = node_entry
end
end
return result
end, function(bufnr)
local root = get_root(bufnr)
if not root then
return tostring(bufnr)
end
return tostring(root:id())
end)
-- Gets all the scopes of a definition based on the scope type
-- Scope types can be
--
-- "parent": Uses the parent of the containing scope, basically, skipping a scope
-- "global": Uses the top most scope
-- "local": Uses the containing scope of the definition. This is the default
--
---@param node TSNode: the definition node
---@param bufnr integer: the buffer
---@param scope_type TSScope: the scope type
---@return TSNode[]
function M.get_definition_scopes(node, bufnr, scope_type)
local scopes = {}
local scope_count = 1 ---@type integer?
-- Definition is valid for the containing scope
-- and the containing scope of that scope
if scope_type == 'parent' then
scope_count = 2
-- Definition is valid in all parent scopes
elseif scope_type == 'global' then
scope_count = nil
end
local i = 0
for scope_node in M.iter_scope_tree(node, bufnr) do
table.insert(scopes, scope_node)
i = i + 1
if scope_count and i >= scope_count then
break
end
end
return scopes
end
---@param node TSNode
---@param bufnr integer
---@return TSNode node
---@return TSNode scope
---@return string|nil kind
function M.find_definition(node, bufnr)
local def_lookup = M.get_definitions_lookup_table(bufnr)
local node_text = ts.get_node_text(node, bufnr)
for scope in M.iter_scope_tree(node, bufnr) do
local id = M.get_definition_id(scope, node_text)
if def_lookup[id] then
local entry = def_lookup[id]
return entry.node, scope, entry.kind
end
end
return node, node:tree():root(), nil
end
-- Finds usages of a node in a given scope.
---@param node TSNode the node to find usages for
---@param scope_node TSNode|nil the node to look within
---@param bufnr integer|nil the bufnr to look into
---@return TSNode[]: a list of nodes
function M.find_usages(node, scope_node, bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local node_text = ts.get_node_text(node, bufnr)
if not node_text or #node_text < 1 then
return {}
end
scope_node = scope_node or node:tree():root()
local usages = {}
local query, _ = get_query(bufnr)
if not query then
return {}
end
for id, node_capture in query:iter_captures(scope_node, bufnr) do
local kind = query.captures[id]
if
node_capture
and kind == 'local.reference'
and ts.get_node_text(node_capture, bufnr) == node_text
then
table.insert(usages, node_capture)
end
end
return usages
end
---@param node TSNode
---@param bufnr? integer
---@param allow_scope? boolean
---@return TSNode?
function M.containing_scope(node, bufnr, allow_scope)
bufnr = bufnr or api.nvim_get_current_buf()
allow_scope = allow_scope == nil or allow_scope == true
local _, _, scopes = M.get(bufnr)
if not node or not scopes then
return
end
local iter_node = node ---@type TSNode?
while iter_node ~= nil and not vim.tbl_contains(scopes, iter_node) do
iter_node = iter_node:parent() ---@type TSNode?
end
return iter_node or (allow_scope and node or nil)
end
function M.nested_scope(node, cursor_pos)
local bufnr = api.nvim_get_current_buf()
local _, _, scopes = M.get(bufnr)
if not node or not scopes then
return
end
local row = cursor_pos.row ---@type integer
local col = cursor_pos.col ---@type integer
local scope = M.containing_scope(node)
for _, child in ipairs(get_named_children(scope)) do
local row_, col_ = child:start()
if vim.tbl_contains(scopes, child) and ((row_ + 1 == row and col_ > col) or row_ + 1 > row) then
return child
end
end
end
function M.next_scope(node)
local bufnr = api.nvim_get_current_buf()
local _, _, scopes = M.get(bufnr)
if not node or not scopes then
return
end
local scope = M.containing_scope(node)
if not scope then
return
end
local parent = scope:parent()
if not parent then
return
end
local is_prev = true
for _, child in ipairs(get_named_children(parent)) do
if child == scope then
is_prev = false
elseif not is_prev and vim.tbl_contains(scopes, child) then
return child
end
end
end
---@param node TSNode
---@return TSNode|nil
function M.previous_scope(node)
local bufnr = api.nvim_get_current_buf()
local _, _, scopes = M.get(bufnr)
if not node or not scopes then
return
end
local scope = M.containing_scope(node)
if not scope then
return
end
local parent = scope:parent()
if not parent then
return
end
local is_prev = true
local children = get_named_children(parent)
for i = #children, 1, -1 do
if children[i] == scope then
is_prev = false
elseif not is_prev and vim.tbl_contains(scopes, children[i]) then
return children[i]
end
end
end
return M