aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/treesitter
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua4
-rw-r--r--runtime/lua/vim/treesitter/_query_linter.lua177
-rw-r--r--runtime/lua/vim/treesitter/dev.lua56
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua2
-rw-r--r--runtime/lua/vim/treesitter/language.lua4
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua62
-rw-r--r--runtime/lua/vim/treesitter/query.lua27
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