diff options
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r-- | runtime/lua/vim/treesitter/_meta.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_query_linter.lua | 177 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/dev.lua | 57 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 63 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 108 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 51 |
7 files changed, 222 insertions, 241 deletions
diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 9a94f12c16..d01b7be3b0 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -50,7 +50,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) ---@class TSParser ----@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: true): TSTree, Range6[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: false|nil): TSTree, Range4[] ---@field reset fun(self: TSParser) ---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: (Range6|TSNode)[]) diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index 3dd0177a81..abf0bf345d 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,8 +1,6 @@ local api = vim.api local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') --- those node names exist for every language -local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } local M = {} @@ -10,11 +8,13 @@ local M = {} --- @field langs string[] --- @field clear boolean +--- @alias vim.treesitter.ParseError {msg: string, range: Range4} + --- @private --- Caches parse results for queries for each language. --- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the ---- error message of the parse ---- @type table<string,table<string,string|true>> +--- message and range of the parse error. +--- @type table<string,table<string,vim.treesitter.ParseError|true>> local parse_cache = {} --- Contains language dependent context for the query linter @@ -26,20 +26,16 @@ local parse_cache = {} --- @private --- Adds a diagnostic for node in the query buffer --- @param diagnostics Diagnostic[] ---- @param node TSNode ---- @param buf integer +--- @param range Range4 --- @param lint string --- @param lang string? -local function add_lint_for_node(diagnostics, node, buf, lint, lang) - local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') - --- @type string - local message = lint .. ': ' .. node_text - local error_range = { node:range() } +local function add_lint_for_node(diagnostics, range, lint, lang) + local message = lint:gsub('\n', ' ') diagnostics[#diagnostics + 1] = { - lnum = error_range[1], - end_lnum = error_range[3], - col = error_range[2], - end_col = error_range[4], + lnum = range[1], + end_lnum = range[3], + col = range[2], + end_col = range[4], severity = vim.diagnostic.ERROR, message = message, source = lang, @@ -92,6 +88,31 @@ local lint_query = [[;; query ]] --- @private +--- @param err string +--- @param node TSNode +--- @return vim.treesitter.ParseError +local function get_error_entry(err, node) + local start_line, start_col = node:range() + local line_offset, col_offset, msg = err:gmatch('.-:%d+: Query error at (%d+):(%d+)%. ([^:]+)')() ---@type string, string, string + start_line, start_col = + start_line + tonumber(line_offset) - 1, start_col + tonumber(col_offset) - 1 + local end_line, end_col = start_line, start_col + if msg:match('^Invalid syntax') or msg:match('^Impossible') then + -- Use the length of the underlined node + local underlined = vim.split(err, '\n')[2] + end_col = end_col + #underlined + elseif msg:match('^Invalid') then + -- Use the length of the problematic type/capture/field + end_col = end_col + #msg:match('"([^"]+)"') + end + + return { + msg = msg, + range = { start_line, start_col, end_line, end_col }, + } +end + +--- @private --- @param node TSNode --- @param buf integer --- @param lang string @@ -106,104 +127,19 @@ local function check_toplevel(node, buf, lang, diagnostics) local lang_cache = parse_cache[lang] if lang_cache[query_text] == nil then - local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) + local cache_val, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - if not ok and type(err) == 'string' then - err = err:match('.-:%d+: (.+)') + if not cache_val and type(err) == 'string' then + cache_val = get_error_entry(err, node) end - lang_cache[query_text] = ok or err + lang_cache[query_text] = cache_val end local cache_entry = lang_cache[query_text] - if type(cache_entry) == 'string' then - add_lint_for_node(diagnostics, node, buf, cache_entry, lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info table ---- @param diagnostics Diagnostic[] -local function check_field(node, buf, lang, parser_info, diagnostics) - local field_name = vim.treesitter.get_node_text(node, buf) - if not vim.tbl_contains(parser_info.fields, field_name) then - add_lint_for_node(diagnostics, node, buf, 'Invalid field', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info (table) ---- @param diagnostics Diagnostic[] -local function check_node(node, buf, lang, parser_info, diagnostics) - local node_type = vim.treesitter.get_node_text(node, buf) - local is_named = node_type:sub(1, 1) ~= '"' - - if not is_named then - node_type = node_type:gsub('"(.*)".*$', '%1'):gsub('\\(.)', '%1') - end - - local found = vim.tbl_contains(BUILT_IN_NODE_NAMES, node_type) - or vim.tbl_contains(parser_info.symbols, function(s) - return vim.deep_equal(s, { node_type, is_named }) - end, { predicate = true }) - - if not found then - add_lint_for_node(diagnostics, node, buf, 'Invalid node type', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param is_predicate boolean ---- @return string -local function get_predicate_name(node, buf, is_predicate) - local name = vim.treesitter.get_node_text(node, buf) - if is_predicate then - if vim.startswith(name, 'not-') then - --- @type string - name = name:sub(string.len('not-') + 1) - end - return name .. '?' - end - return name .. '!' -end - ---- @private ---- @param predicate_node TSNode ---- @param predicate_type_node TSNode ---- @param buf integer ---- @param lang string? ---- @param diagnostics Diagnostic[] -local function check_predicate(predicate_node, predicate_type_node, buf, lang, diagnostics) - local type_string = vim.treesitter.get_node_text(predicate_type_node, buf) - - -- Quirk of the query grammar that directives are also predicates! - if type_string == '?' then - if - not vim.tbl_contains( - vim.treesitter.query.list_predicates(), - get_predicate_name(predicate_node, buf, true) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown predicate', lang) - end - elseif type_string == '!' then - if - not vim.tbl_contains( - vim.treesitter.query.list_directives(), - get_predicate_name(predicate_node, buf, false) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown directive', lang) - end + if type(cache_entry) ~= 'boolean' then + add_lint_for_node(diagnostics, cache_entry.range, cache_entry.msg, lang) end end @@ -214,8 +150,6 @@ end --- @param lang_context QueryLinterLanguageContext --- @param diagnostics Diagnostic[] local function lint_match(buf, match, query, lang_context, diagnostics) - local predicate --- @type TSNode - local predicate_type --- @type TSNode local lang = lang_context.lang local parser_info = lang_context.parser_info @@ -223,31 +157,16 @@ local function lint_match(buf, match, query, lang_context, diagnostics) local cap_id = query.captures[id] -- perform language-independent checks only for first lang - if lang_context.is_first_lang then - if cap_id == 'error' then - add_lint_for_node(diagnostics, node, buf, 'Syntax error') - elseif cap_id == 'predicate.name' then - predicate = node - elseif cap_id == 'predicate.type' then - predicate_type = node - end + if lang_context.is_first_lang and cap_id == 'error' then + local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') + add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text) end -- other checks rely on Neovim parser introspection - if lang and parser_info then - if cap_id == 'toplevel' then - check_toplevel(node, buf, lang, diagnostics) - elseif cap_id == 'field' then - check_field(node, buf, lang, parser_info, diagnostics) - elseif cap_id == 'node.named' or cap_id == 'node.anonymous' then - check_node(node, buf, lang, parser_info, diagnostics) - end + if lang and parser_info and cap_id == 'toplevel' then + check_toplevel(node, buf, lang, diagnostics) end end - - if predicate and predicate_type then - check_predicate(predicate, predicate_type, buf, lang, diagnostics) - end end --- @private @@ -339,7 +258,7 @@ function M.omnifunc(findstart, base) end end for _, s in pairs(parser_info.symbols) do - local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' + local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' ---@type string if text:find(base, 1, true) then table.insert(items, text) end diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index e7af259d28..7f24ba8590 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -351,11 +351,11 @@ function M.inspect_tree(opts) end, }) api.nvim_buf_set_keymap(b, 'n', 'o', '', { - desc = 'Toggle query previewer', + desc = 'Toggle query editor', callback = function() - local preview_w = vim.b[buf].dev_preview - if not preview_w or not close_win(preview_w) then - M.preview_query() + local edit_w = vim.b[buf].dev_edit + if not edit_w or not close_win(edit_w) then + M.edit_query() end end, }) @@ -464,16 +464,16 @@ function M.inspect_tree(opts) }) end -local preview_ns = api.nvim_create_namespace('treesitter/dev-preview') +local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') ---@param query_win integer ---@param base_win integer -local function update_preview_highlights(query_win, base_win) +---@param lang string +local function update_editor_highlights(query_win, base_win, lang) local base_buf = api.nvim_win_get_buf(base_win) local query_buf = api.nvim_win_get_buf(query_win) - local parser = vim.treesitter.get_parser(base_buf) - local lang = parser:lang() - api.nvim_buf_clear_namespace(base_buf, preview_ns, 0, -1) + local parser = vim.treesitter.get_parser(base_buf, lang) + api.nvim_buf_clear_namespace(base_buf, edit_ns, 0, -1) local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n') local ok_query, query = pcall(vim.treesitter.query.parse, lang, query_content) @@ -493,7 +493,7 @@ local function update_preview_highlights(query_win, base_win) local capture_name = query.captures[id] if capture_name == cursor_word then local lnum, col, end_lnum, end_col = node:range() - api.nvim_buf_set_extmark(base_buf, preview_ns, lnum, col, { + api.nvim_buf_set_extmark(base_buf, edit_ns, lnum, col, { end_row = end_lnum, end_col = end_col, hl_group = 'Visual', @@ -506,17 +506,18 @@ local function update_preview_highlights(query_win, base_win) end --- @private -function M.preview_query() +--- @param lang? string language to open the query editor for. +function M.edit_query(lang) local buf = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - -- Close any existing previewer window - if vim.b[buf].dev_preview then - close_win(vim.b[buf].dev_preview) + -- Close any existing editor window + if vim.b[buf].dev_edit then + close_win(vim.b[buf].dev_edit) end local cmd = '60vnew' - -- If the inspector is open, place the previewer above it. + -- If the inspector is open, place the editor above it. local base_win = vim.b[buf].dev_base ---@type integer? local base_buf = base_win and api.nvim_win_get_buf(base_win) local inspect_win = base_buf and vim.b[base_buf].dev_inspect @@ -528,29 +529,29 @@ function M.preview_query() end vim.cmd(cmd) - local ok, parser = pcall(vim.treesitter.get_parser, buf) + local ok, parser = pcall(vim.treesitter.get_parser, buf, lang) if not ok then return nil, 'No parser available for the given buffer' end - local lang = parser:lang() + lang = parser:lang() local query_win = api.nvim_get_current_win() local query_buf = api.nvim_win_get_buf(query_win) - vim.b[buf].dev_preview = query_win + vim.b[buf].dev_edit = query_win vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc' set_dev_properties(query_win, query_buf) -- Note that omnifunc guesses the language based on the containing folder, -- so we add the parser's language to the buffer's name so that omnifunc -- can infer the language later. - api.nvim_buf_set_name(query_buf, string.format('%s/query_previewer.scm', lang)) + api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang)) - local group = api.nvim_create_augroup('treesitter/dev-preview', {}) + local group = api.nvim_create_augroup('treesitter/dev-edit', {}) api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { group = group, buffer = query_buf, - desc = 'Update query previewer diagnostics when the query changes', + desc = 'Update query editor diagnostics when the query changes', callback = function() vim.treesitter.query.lint(query_buf, { langs = lang, clear = false }) end, @@ -558,37 +559,37 @@ function M.preview_query() api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave', 'CursorMoved', 'BufEnter' }, { group = group, buffer = query_buf, - desc = 'Update query previewer highlights when the cursor moves', + desc = 'Update query editor highlights when the cursor moves', callback = function() if api.nvim_win_is_valid(win) then - update_preview_highlights(query_win, win) + update_editor_highlights(query_win, win, lang) end end, }) api.nvim_create_autocmd('BufLeave', { group = group, buffer = query_buf, - desc = 'Clear the query previewer highlights when leaving the previewer', + desc = 'Clear highlights when leaving the query editor', callback = function() - api.nvim_buf_clear_namespace(buf, preview_ns, 0, -1) + api.nvim_buf_clear_namespace(buf, edit_ns, 0, -1) end, }) api.nvim_create_autocmd('BufLeave', { group = group, buffer = buf, - desc = 'Clear the query previewer highlights when leaving the source buffer', + desc = 'Clear the query editor highlights when leaving the source buffer', callback = function() if not api.nvim_buf_is_loaded(query_buf) then return true end - api.nvim_buf_clear_namespace(query_buf, preview_ns, 0, -1) + api.nvim_buf_clear_namespace(query_buf, edit_ns, 0, -1) end, }) api.nvim_create_autocmd('BufHidden', { group = group, buffer = buf, - desc = 'Close the previewer window when the source buffer is hidden', + desc = 'Close the editor window when the source buffer is hidden', once = true, callback = function() close_win(query_win) diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8d4d6a9337..496193c6ed 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -2,7 +2,7 @@ local api = vim.api local query = vim.treesitter.query local Range = require('vim.treesitter._range') ----@alias TSHlIter fun(): integer, TSNode, TSMetadata +---@alias TSHlIter fun(end_line: integer|nil): integer, TSNode, TSMetadata ---@class TSHighlightState ---@field next_row integer @@ -241,40 +241,43 @@ local function on_line_impl(self, buf, line, is_spell_nav) end while line >= state.next_row do - local capture, node, metadata = state.iter() + local capture, node, metadata = state.iter(line) - if capture == nil then - break + local range = { root_end_row + 1, 0, root_end_row + 1, 0 } + if node then + range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) end - - local range = vim.treesitter.get_range(node, buf, metadata[capture]) local start_row, start_col, end_row, end_col = Range.unpack4(range) - local hl = highlighter_query.hl_cache[capture] - - local capture_name = highlighter_query:query().captures[capture] - local spell = nil ---@type boolean? - if capture_name == 'spell' then - spell = true - elseif capture_name == 'nospell' then - spell = false - end - -- Give nospell a higher priority so it always overrides spell captures. - local spell_pri_offset = capture_name == 'nospell' and 1 or 0 - - if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then - local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) - + spell_pri_offset - api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { - end_line = end_row, - end_col = end_col, - hl_group = hl, - ephemeral = true, - priority = priority, - conceal = metadata.conceal, - spell = spell, - }) + if capture then + local hl = highlighter_query.hl_cache[capture] + + local capture_name = highlighter_query:query().captures[capture] + local spell = nil ---@type boolean? + if capture_name == 'spell' then + spell = true + elseif capture_name == 'nospell' then + spell = false + end + + -- Give nospell a higher priority so it always overrides spell captures. + local spell_pri_offset = capture_name == 'nospell' and 1 or 0 + + if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then + local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter) + + spell_pri_offset + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { + end_line = end_row, + end_col = end_col, + hl_group = hl, + ephemeral = true, + priority = priority, + conceal = metadata.conceal, + spell = spell, + }) + end end + if start_row > line then state.next_row = start_row end diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 9695e2c41c..15bf666a1e 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -82,9 +82,8 @@ function M.add(lang, opts) filetype = { filetype, { 'string', 'table' }, true }, }) - M.register(lang, filetype) - if vim._ts_has_language(lang) then + M.register(lang, filetype) return end @@ -102,6 +101,7 @@ function M.add(lang, opts) end vim._ts_add_language(path, lang, symbol_name) + M.register(lang, filetype) end --- @param x string|string[] diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 6037b17b20..f931291ed7 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -7,9 +7,9 @@ --- --- To create a LanguageTree (parser object) for a given buffer and language, use: --- ---- <pre>lua ---- local parser = vim.treesitter.get_parser(bufnr, lang) ---- </pre> +--- ```lua +--- local parser = vim.treesitter.get_parser(bufnr, lang) +--- ``` --- --- (where `bufnr=0` means current buffer). `lang` defaults to 'filetype'. --- Note: currently the parser is retained for the lifetime of a buffer but this may change; @@ -17,9 +17,9 @@ --- --- Whenever you need to access the current syntax tree, parse the buffer: --- ---- <pre>lua ---- local tree = parser:parse({ start_row, end_row }) ---- </pre> +--- ```lua +--- local tree = parser:parse({ start_row, end_row }) +--- ``` --- --- This returns a table of immutable |treesitter-tree| objects representing the current state of --- the buffer. When the plugin wants to access the state after a (possible) edit it must call @@ -78,13 +78,14 @@ local TSCallbackNames = { ---@field private _opts table Options ---@field private _parser TSParser Parser for language ---@field private _has_regions boolean ----@field private _regions Range6[][]? +---@field private _regions table<integer, Range6[]>? ---List of regions this tree should manage and parse. If nil then regions are ---taken from _trees. This is mostly a short-lived cache for included_regions() ---@field private _lang string Language name ---@field private _parent_lang? string Parent language name ---@field private _source (integer|string) Buffer or string to parse ----@field private _trees TSTree[] Reference to parsed tree (one for each language) +---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language). +---Each key is the index of region, which is synced with _regions and _valid. ---@field private _valid boolean|table<integer,boolean> If the parsed tree is valid ---@field private _logger? fun(logtype: string, msg: string) ---@field private _logfile? file* @@ -211,7 +212,7 @@ function LanguageTree:_log(...) end local info = debug.getinfo(2, 'nl') - local nregions = #self:included_regions() + local nregions = vim.tbl_count(self:included_regions()) local prefix = string.format('%s:%d: (#regions=%d) ', info.name or '???', info.currentline or 0, nregions) @@ -244,8 +245,13 @@ function LanguageTree:invalidate(reload) end end ---- Returns all trees this language tree contains. +--- Returns all trees of the regions parsed by this parser. --- Does not include child languages. +--- The result is list-like if +--- * this LanguageTree is the root, in which case the result is empty or a singleton list; or +--- * the root LanguageTree is fully parsed. +--- +---@return table<integer, TSTree> function LanguageTree:trees() return self._trees end @@ -255,16 +261,15 @@ function LanguageTree:lang() return self._lang end ---- Determines whether this tree is valid. ---- If the tree is invalid, call `parse()`. ---- This will return the updated tree. ----@param exclude_children boolean|nil +--- Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest +--- state of the source. If invalid, user should call |LanguageTree:parse()|. +---@param exclude_children boolean|nil whether to ignore the validity of children (default `false`) ---@return boolean function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then - for i = 1, #self:included_regions() do + for i, _ in pairs(self:included_regions()) do if not valid[i] then return false end @@ -328,7 +333,7 @@ end --- @private --- @param range boolean|Range? ---- @return integer[] changes +--- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time function LanguageTree:_parse_regions(range) @@ -370,7 +375,7 @@ function LanguageTree:_add_injections() local seen_langs = {} ---@type table<string,boolean> local query_time, injections_by_lang = tcall(self._get_injections, self) - for lang, injection_ranges in pairs(injections_by_lang) do + for lang, injection_regions in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) -- Child language trees should just be ignored if not found, since @@ -383,7 +388,7 @@ function LanguageTree:_add_injections() child = self:add_child(lang) end - child:set_included_regions(injection_ranges) + child:set_included_regions(injection_regions) seen_langs[lang] = true end end @@ -408,14 +413,14 @@ end --- Set to `true` to run a complete parse of the source (Note: Can be slow!) --- Set to `false|nil` to only parse regions with empty ranges (typically --- only the root tree without injections). ---- @return TSTree[] +--- @return table<integer, TSTree> function LanguageTree:parse(range) if self:is_valid() then self:_log('valid') return self._trees end - local changes --- @type Range6? + local changes --- @type Range6[]? -- Collect some stats local no_regions_parsed = 0 @@ -451,11 +456,14 @@ function LanguageTree:parse(range) return self._trees end +---@deprecated Misleading name. Use `LanguageTree:children()` (non-recursive) instead, +--- add recursion yourself if needed. --- Invokes the callback for each |LanguageTree| and its children recursively --- ---@param fn fun(tree: LanguageTree, lang: string) ---@param include_self boolean|nil Whether to include the invoking tree in the results function LanguageTree:for_each_child(fn, include_self) + vim.deprecate('LanguageTree:for_each_child()', 'LanguageTree:children()', '0.11') if include_self then fn(self, self._lang) end @@ -562,7 +570,7 @@ function LanguageTree:_iter_regions(fn) local all_valid = true - for i, region in ipairs(self:included_regions()) do + for i, region in pairs(self:included_regions()) do if was_valid or self._valid[i] then self._valid[i] = fn(i, region) if not self._valid[i] then @@ -598,7 +606,7 @@ end --- nodes, which is useful for templating languages like ERB and EJS. --- ---@private ----@param new_regions Range6[][] List of regions this tree should manage and parse. +---@param new_regions (Range4|Range6|TSNode)[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(new_regions) self._has_regions = true @@ -606,16 +614,20 @@ function LanguageTree:set_included_regions(new_regions) for _, region in ipairs(new_regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then - region[i] = Range.add_bytes(self._source, range) + region[i] = Range.add_bytes(self._source, range --[[@as Range4]]) elseif type(range) == 'userdata' then region[i] = { range:range(true) } end end end + -- included_regions is not guaranteed to be list-like, but this is still sound, i.e. if + -- new_regions is different from included_regions, then outdated regions in included_regions are + -- invalidated. For example, if included_regions = new_regions ++ hole ++ outdated_regions, then + -- outdated_regions is invalidated by _iter_regions in else branch. if #self:included_regions() ~= #new_regions then -- TODO(lewis6991): inefficient; invalidate trees incrementally - for _, t in ipairs(self._trees) do + for _, t in pairs(self._trees) do self:_do_callback('changedtree', t:included_ranges(true), t) end self._trees = {} @@ -629,20 +641,22 @@ function LanguageTree:set_included_regions(new_regions) self._regions = new_regions end ----Gets the set of included regions ----@return Range6[][] +---Gets the set of included regions managed by this LanguageTree. This can be different from the +---regions set by injection query, because a partial |LanguageTree:parse()| drops the regions +---outside the requested range. +---@return table<integer, Range6[]> function LanguageTree:included_regions() if self._regions then return self._regions end - if not self._has_regions or #self._trees == 0 then + if not self._has_regions or next(self._trees) == nil then -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} return { {} } end local regions = {} ---@type Range6[][] - for i, _ in ipairs(self._trees) do + for i, _ in pairs(self._trees) do regions[i] = self._trees[i]:included_ranges(true) end @@ -731,7 +745,8 @@ local has_parser = function(lang) or #vim.api.nvim_get_runtime_file('parser/' .. lang .. '.*', false) > 0 end ---- Return parser name for language (if exists) or filetype (if registered and exists) +--- Return parser name for language (if exists) or filetype (if registered and exists). +--- Also attempts with the input lower-cased. --- ---@param alias string language or filetype name ---@return string? # resolved parser name @@ -740,10 +755,19 @@ local function resolve_lang(alias) return alias end + if has_parser(alias:lower()) then + return alias:lower() + end + local lang = vim.treesitter.language.get_lang(alias) if lang and has_parser(lang) then return lang end + + lang = vim.treesitter.language.get_lang(alias:lower()) + if lang and has_parser(lang) then + return lang + end end ---@private @@ -755,9 +779,10 @@ end function LanguageTree:_get_injection(match, metadata) local ranges = {} ---@type Range6[] local combined = metadata['injection.combined'] ~= nil + local injection_lang = metadata['injection.language'] --[[@as string?]] local lang = metadata['injection.self'] ~= nil and self:lang() or metadata['injection.parent'] ~= nil and self._parent_lang - or metadata['injection.language'] --[[@as string?]] + or (injection_lang and resolve_lang(injection_lang)) local include_children = metadata['injection.include-children'] ~= nil for id, node in pairs(match) do @@ -765,7 +790,7 @@ function LanguageTree:_get_injection(match, metadata) -- Lang should override any other language tag if name == 'injection.language' then local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] }) - lang = resolve_lang(text) or resolve_lang(text:lower()) + lang = resolve_lang(text) elseif name == 'injection.content' then ranges = get_node_ranges(node, self._source, metadata[id], include_children) end @@ -774,7 +799,20 @@ function LanguageTree:_get_injection(match, metadata) return lang, combined, ranges end ---- Gets language injection points by language. +--- Can't use vim.tbl_flatten since a range is just a table. +---@param regions Range6[][] +---@return Range6[] +local function combine_regions(regions) + local result = {} ---@type Range6[] + for _, region in ipairs(regions) do + for _, range in ipairs(region) do + result[#result + 1] = range + end + end + return result +end + +--- Gets language injection regions by language. --- --- This is where most of the injection processing occurs. --- @@ -819,11 +857,7 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then - ---@diagnostic disable-next-line:no-unknown - local regions = vim.tbl_map(function(e) - return vim.tbl_flatten(e) - end, entry.regions) - table.insert(result[lang], regions) + table.insert(result[lang], combine_regions(entry.regions)) else for _, ranges in pairs(entry.regions) do table.insert(result[lang], ranges) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3093657313..44ed37d64e 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -514,7 +514,10 @@ local directive_handlers = { -- Example: (#trim! @fold) -- TODO(clason): generalize to arbitrary whitespace removal ['trim!'] = function(match, _, bufnr, pred, metadata) - local node = match[pred[2]] + local capture_id = pred[2] + assert(type(capture_id) == 'number') + + local node = match[capture_id] if not node then return end @@ -526,9 +529,9 @@ local directive_handlers = { return end - while true do + while end_row >= start_row do -- As we only care when end_col == 0, always inspect one line above end_row. - local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] + local end_line = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] if end_line ~= '' then break @@ -539,7 +542,8 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if start_row < end_row or (start_row == end_row and start_col <= end_col) then - metadata.range = { start_row, start_col, end_row, end_col } + metadata[capture_id] = metadata[capture_id] or {} + metadata[capture_id].range = { start_row, start_col, end_row, end_col } end end, } @@ -692,7 +696,8 @@ end --- The iterator returns three values: a numeric id identifying the capture, --- the captured node, and metadata from any directives processing the match. --- The following example shows how to get captures by name: ---- <pre>lua +--- +--- ```lua --- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do --- local name = query.captures[id] -- name of the capture in the query --- -- typically useful info about the node: @@ -700,14 +705,15 @@ end --- local row1, col1, row2, col2 = node:range() -- range of the capture --- -- ... use the info here ... --- end ---- </pre> +--- ``` --- ---@param node TSNode under which the search will occur ---@param source (integer|string) Source buffer or string to extract text from ---@param start integer Starting line for the search ---@param stop integer Stopping line for the search (end-exclusive) --- ----@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata +---@return (fun(end_line: integer|nil): integer, TSNode, TSMetadata): +--- capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) if type(source) == 'number' and source == 0 then source = api.nvim_get_current_buf() @@ -716,7 +722,7 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, true, start, stop) - local function iter() + local function iter(end_line) local capture, captured_node, match = raw_iter() local metadata = {} @@ -724,7 +730,10 @@ function Query:iter_captures(node, source, start, stop) local active = self:match_preds(match, match.pattern, source) match.active = active if not active then - return iter() -- tail call: try next match + if end_line and captured_node:range() > end_line then + return nil, captured_node, nil + end + return iter(end_line) -- tail call: try next match end self:apply_directives(match, match.pattern, source, metadata) @@ -743,7 +752,8 @@ end --- If the query has more than one pattern, the capture table might be sparse --- and e.g. `pairs()` method should be used over `ipairs`. --- Here is an example iterating over all captures in every match: ---- <pre>lua +--- +--- ```lua --- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do --- for id, node in pairs(match) do --- local name = query.captures[id] @@ -754,7 +764,7 @@ end --- -- ... use the info here ... --- end --- end ---- </pre> +--- ``` --- ---@param node TSNode under which the search will occur ---@param source (integer|string) Source buffer or string to search @@ -824,11 +834,24 @@ end --- Omnifunc for completing node names and predicates in treesitter queries. --- --- Use via ---- <pre>lua ---- vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' ---- </pre> +--- +--- ```lua +--- vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' +--- ``` +--- function M.omnifunc(findstart, base) return require('vim.treesitter._query_linter').omnifunc(findstart, base) end +--- Open a window for live editing of a treesitter query. +--- +--- Can also be shown with `:EditQuery`. *:EditQuery* +--- +--- Note that the editor opens a scratch buffer, and so queries aren't persisted on disk. +--- +--- @param lang? string language to open the query editor for. If omitted, inferred from the current buffer's filetype. +function M.edit(lang) + require('vim.treesitter.dev').edit_query(lang) +end + return M |