diff options
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_query_linter.lua | 177 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/dev.lua | 56 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 62 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 27 |
7 files changed, 140 insertions, 192 deletions
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index d82e04a5a8..8bc08c9c2e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -299,7 +299,9 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) local srow, _, erow = Range.unpack4(change) get_folds_levels(bufnr, foldinfo, srow, erow) end - foldupdate(bufnr) + if #tree_changes > 0 then + foldupdate(bufnr) + end end) end 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 72b6e3db4a..bc54853103 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -101,18 +101,18 @@ function TSTreeView:new(bufnr, lang) -- the root in the child tree to the {injections} table. local root = parser:parse(true)[1]:root() local injections = {} ---@type table<integer,table> - parser:for_each_child(function(child, lang_) - child:for_each_tree(function(tree) + for _, child in pairs(parser:children()) do + child:for_each_tree(function(tree, ltree) local r = tree:root() local node = root:named_descendant_for_range(r:range()) if node then injections[node:id()] = { - lang = lang_, + lang = ltree:lang(), root = r, } end end) - end) + end local nodes = traverse(root, 0, parser:lang(), injections, {}) @@ -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) +local function update_editor_highlights(query_win, base_win) 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) + 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,17 @@ local function update_preview_highlights(query_win, base_win) end --- @private -function M.preview_query() +function M.edit_query() 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 @@ -537,20 +537,20 @@ function M.preview_query() 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 +558,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) 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 56b075b723..8d4d6a9337 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -322,7 +322,7 @@ function TSHighlighter._on_win(_, _win, buf, topline, botline) if not self then return false end - self.tree:parse({ topline, botline }) + self.tree:parse({ topline, botline + 1 }) self:reset_highlight_state() self.redraw_count = self.redraw_count + 1 return true 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 e81778b269..79f36a27fd 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 @@ -444,18 +444,21 @@ function LanguageTree:parse(range) range = range, }) - self:for_each_child(function(child) + for _, child in pairs(self._children) do child:parse(range) - end) + end 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 @@ -897,6 +900,20 @@ function LanguageTree:_edit( end return true end) + + for _, child in pairs(self._children) do + child:_edit( + start_byte, + end_byte_old, + end_byte_new, + start_row, + start_col, + end_row_old, + end_col_old, + end_row_new, + end_col_new + ) + end end ---@package @@ -943,20 +960,17 @@ function LanguageTree:_on_bytes( ) -- Edit trees together BEFORE emitting a bytes callback. - ---@private - self:for_each_child(function(child) - child:_edit( - start_byte, - start_byte + old_byte, - start_byte + new_byte, - start_row, - start_col, - start_row + old_row, - old_end_col, - start_row + new_row, - new_end_col - ) - end, true) + self:_edit( + start_byte, + start_byte + old_byte, + start_byte + new_byte, + start_row, + start_col, + start_row + old_row, + old_end_col, + start_row + new_row, + new_end_col + ) self:_do_callback( 'bytes', @@ -1017,9 +1031,9 @@ function LanguageTree:register_cbs(cbs, recursive) end if recursive then - self:for_each_child(function(child) + for _, child in pairs(self._children) do child:register_cbs(cbs, true) - end) + end end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 3093657313..d7973cc48f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -692,7 +692,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,7 +701,7 @@ 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 @@ -743,7 +744,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 +756,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 +826,22 @@ 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. +function M.edit() + require('vim.treesitter.dev').edit_query() +end + return M |