diff options
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 36 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_meta.lua | 113 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_meta/misc.lua | 78 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_meta/tsnode.lua | 185 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_meta/tstree.lua | 44 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_query_linter.lua | 21 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_range.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/dev.lua | 72 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/health.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 64 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 44 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 43 |
13 files changed, 498 insertions, 224 deletions
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index eecf1ad6b1..7237d2e7d4 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -87,7 +87,7 @@ end ---@param srow integer ---@param erow integer 0-indexed, exclusive function FoldInfo:add_range(srow, erow) - list_insert(self.levels, srow + 1, erow, '=') + list_insert(self.levels, srow + 1, erow, -1) list_insert(self.levels0, srow + 1, erow, -1) end @@ -114,7 +114,7 @@ local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) srow = srow or 0 erow = erow or api.nvim_buf_line_count(bufnr) - local parser = ts.get_parser(bufnr) + local parser = assert(ts.get_parser(bufnr, nil, { error = false })) parser:parse(parse_injections and { srow, erow } or nil) @@ -131,24 +131,18 @@ local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) -- Collect folds starting from srow - 1, because we should first subtract the folds that end at -- srow - 1 from the level of srow - 1 to get accurate level of srow. - for _, match, metadata in - query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow, { all = true }) - do + for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do for id, nodes in pairs(match) do if query.captures[id] == 'fold' then local range = ts.get_range(nodes[1], bufnr, metadata[id]) local start, _, stop, stop_col = Range.unpack4(range) - for i = 2, #nodes, 1 do - local node_range = ts.get_range(nodes[i], bufnr, metadata[id]) - local node_start, _, node_stop, node_stop_col = Range.unpack4(node_range) - if node_start < start then - start = node_start - end - if node_stop > stop then - stop = node_stop - stop_col = node_stop_col - end + if #nodes > 1 then + -- assumes nodes are ordered by range + local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id]) + local _, _, end_stop, end_stop_col = Range.unpack4(end_range) + stop = end_stop + stop_col = end_stop_col end if stop_col == 0 then @@ -268,6 +262,15 @@ end ---@package function FoldInfo:do_foldupdate(bufnr) + -- InsertLeave is not executed when <C-C> is used for exiting the insert mode, leaving + -- do_foldupdate untouched. If another execution of foldupdate consumes foldupdate_range, the + -- InsertLeave do_foldupdate gets nil foldupdate_range. In that case, skip the update. This is + -- correct because the update that consumed the range must have incorporated the range that + -- InsertLeave meant to update. + if not self.foldupdate_range then + return + end + local srow, erow = self.foldupdate_range[1], self.foldupdate_range[2] self.foldupdate_range = nil for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do @@ -383,14 +386,13 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, end end ----@package ---@param lnum integer|nil ---@return string function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - local parser = vim.F.npcall(ts.get_parser, bufnr) + local parser = ts.get_parser(bufnr, nil, { error = false }) if not parser then return '0' end diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua deleted file mode 100644 index 177699a207..0000000000 --- a/runtime/lua/vim/treesitter/_meta.lua +++ /dev/null @@ -1,113 +0,0 @@ ----@meta -error('Cannot require a meta file') - ----@class TSNode: userdata ----@field id fun(self: TSNode): string ----@field tree fun(self: TSNode): TSTree ----@field range fun(self: TSNode, include_bytes: false?): integer, integer, integer, integer ----@field range fun(self: TSNode, include_bytes: true): integer, integer, integer, integer, integer, integer ----@field start fun(self: TSNode): integer, integer, integer ----@field end_ fun(self: TSNode): integer, integer, integer ----@field type fun(self: TSNode): string ----@field symbol fun(self: TSNode): integer ----@field named fun(self: TSNode): boolean ----@field missing fun(self: TSNode): boolean ----@field extra fun(self: TSNode): boolean ----@field child_count fun(self: TSNode): integer ----@field named_child_count fun(self: TSNode): integer ----@field child fun(self: TSNode, index: integer): TSNode? ----@field named_child fun(self: TSNode, index: integer): TSNode? ----@field descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode? ----@field named_descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode? ----@field parent fun(self: TSNode): TSNode? ----@field child_containing_descendant fun(self: TSNode, descendant: TSNode): TSNode? ----@field next_sibling fun(self: TSNode): TSNode? ----@field prev_sibling fun(self: TSNode): TSNode? ----@field next_named_sibling fun(self: TSNode): TSNode? ----@field prev_named_sibling fun(self: TSNode): TSNode? ----@field named_children fun(self: TSNode): TSNode[] ----@field has_changes fun(self: TSNode): boolean ----@field has_error fun(self: TSNode): boolean ----@field sexpr fun(self: TSNode): string ----@field equal fun(self: TSNode, other: TSNode): boolean ----@field iter_children fun(self: TSNode): fun(): TSNode, string ----@field field fun(self: TSNode, name: string): TSNode[] ----@field byte_length fun(self: TSNode): integer -local TSNode = {} - ----@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) - ----@class TSParser: userdata ----@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean): TSTree, (Range4|Range6)[] ----@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)[]) ----@field set_timeout fun(self: TSParser, timeout: integer) ----@field timeout fun(self: TSParser): integer ----@field _set_logger fun(self: TSParser, lex: boolean, parse: boolean, cb: TSLoggerCallback) ----@field _logger fun(self: TSParser): TSLoggerCallback - ----@class TSTree: userdata ----@field root fun(self: TSTree): TSNode ----@field edit fun(self: TSTree, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _: integer, _:integer) ----@field copy fun(self: TSTree): TSTree ----@field included_ranges fun(self: TSTree, include_bytes: true): Range6[] ----@field included_ranges fun(self: TSTree, include_bytes: false): Range4[] - ----@class TSQuery: userdata ----@field inspect fun(self: TSQuery): TSQueryInfo - ----@class (exact) TSQueryInfo ----@field captures string[] ----@field patterns table<integer, (integer|string)[][]> - ---- @param lang string -vim._ts_inspect_language = function(lang) end - ----@return integer -vim._ts_get_language_version = function() end - ---- @param path string ---- @param lang string ---- @param symbol_name? string -vim._ts_add_language = function(path, lang, symbol_name) end - ----@return integer -vim._ts_get_minimum_language_version = function() end - ----@param lang string Language to use for the query ----@param query string Query string in s-expr syntax ----@return TSQuery -vim._ts_parse_query = function(lang, query) end - ----@param lang string ----@return TSParser -vim._create_ts_parser = function(lang) end - ---- @class TSQueryMatch: userdata ---- @field captures fun(self: TSQueryMatch): table<integer,TSNode[]> -local TSQueryMatch = {} - ---- @return integer match_id ---- @return integer pattern_index -function TSQueryMatch:info() end - ---- @class TSQueryCursor: userdata ---- @field remove_match fun(self: TSQueryCursor, id: integer) -local TSQueryCursor = {} - ---- @return integer capture ---- @return TSNode captured_node ---- @return TSQueryMatch match -function TSQueryCursor:next_capture() end - ---- @return TSQueryMatch match -function TSQueryCursor:next_match() end - ---- @param node TSNode ---- @param query TSQuery ---- @param start integer? ---- @param stop integer? ---- @param opts? { max_start_depth?: integer, match_limit?: integer} ---- @return TSQueryCursor -function vim._create_ts_querycursor(node, query, start, stop, opts) end diff --git a/runtime/lua/vim/treesitter/_meta/misc.lua b/runtime/lua/vim/treesitter/_meta/misc.lua new file mode 100644 index 0000000000..33701ef254 --- /dev/null +++ b/runtime/lua/vim/treesitter/_meta/misc.lua @@ -0,0 +1,78 @@ +---@meta +-- luacheck: no unused args +error('Cannot require a meta file') + +---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) + +---@class TSParser: userdata +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean): TSTree, (Range4|Range6)[] +---@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)[]) +---@field set_timeout fun(self: TSParser, timeout: integer) +---@field timeout fun(self: TSParser): integer +---@field _set_logger fun(self: TSParser, lex: boolean, parse: boolean, cb: TSLoggerCallback) +---@field _logger fun(self: TSParser): TSLoggerCallback + +---@class TSQuery: userdata +---@field inspect fun(self: TSQuery): TSQueryInfo + +---@class (exact) TSQueryInfo +---@field captures string[] +---@field patterns table<integer, (integer|string)[][]> + +--- @param lang string +--- @return table +vim._ts_inspect_language = function(lang) end + +---@return integer +vim._ts_get_language_version = function() end + +--- @param path string +--- @param lang string +--- @param symbol_name? string +vim._ts_add_language_from_object = function(path, lang, symbol_name) end + +--- @param path string +--- @param lang string +vim._ts_add_language_from_wasm = function(path, lang) end + +---@return integer +vim._ts_get_minimum_language_version = function() end + +---@param lang string Language to use for the query +---@param query string Query string in s-expr syntax +---@return TSQuery +vim._ts_parse_query = function(lang, query) end + +---@param lang string +---@return TSParser +vim._create_ts_parser = function(lang) end + +--- @class TSQueryMatch: userdata +--- @field captures fun(self: TSQueryMatch): table<integer,TSNode[]> +local TSQueryMatch = {} -- luacheck: no unused + +--- @return integer match_id +--- @return integer pattern_index +function TSQueryMatch:info() end + +--- @class TSQueryCursor: userdata +--- @field remove_match fun(self: TSQueryCursor, id: integer) +local TSQueryCursor = {} -- luacheck: no unused + +--- @return integer capture +--- @return TSNode captured_node +--- @return TSQueryMatch match +function TSQueryCursor:next_capture() end + +--- @return TSQueryMatch match +function TSQueryCursor:next_match() end + +--- @param node TSNode +--- @param query TSQuery +--- @param start integer? +--- @param stop integer? +--- @param opts? { max_start_depth?: integer, match_limit?: integer} +--- @return TSQueryCursor +function vim._create_ts_querycursor(node, query, start, stop, opts) end diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua new file mode 100644 index 0000000000..acc9f8d24e --- /dev/null +++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua @@ -0,0 +1,185 @@ +---@meta +-- luacheck: no unused args +error('Cannot require a meta file') + +--- @brief A "treesitter node" represents one specific element of the parsed contents of a buffer, +--- which can be captured by a |Query| for, e.g., highlighting. It is a |userdata| reference to an +--- object held by the treesitter library. +--- +--- An instance `TSNode` of a treesitter node supports the following methods. + +---@nodoc +---@class TSNode: userdata +---@field named_children fun(self: TSNode): TSNode[] +---@field __has_ancestor fun(self: TSNode, node_types: string[]): boolean +local TSNode = {} -- luacheck: no unused + +--- Get the node's immediate parent. +--- Prefer |TSNode:child_containing_descendant()| +--- for iterating over the node's ancestors. +--- @return TSNode? +function TSNode:parent() end + +--- Get the node's next sibling. +--- @return TSNode? +function TSNode:next_sibling() end + +--- Get the node's previous sibling. +--- @return TSNode? +function TSNode:prev_sibling() end + +--- Get the node's next named sibling. +--- @return TSNode? +function TSNode:next_named_sibling() end + +--- Get the node's previous named sibling. +--- @return TSNode? +function TSNode:prev_named_sibling() end + +--- Iterates over all the direct children of {TSNode}, regardless of whether +--- they are named or not. +--- Returns the child node plus the eventual field name corresponding to this +--- child node. +--- @return fun(): TSNode, string +function TSNode:iter_children() end + +--- Returns a table of the nodes corresponding to the {name} field. +--- @param name string +--- @return TSNode[] +function TSNode:field(name) end + +--- Get the node's number of children. +--- @return integer +function TSNode:child_count() end + +--- Get the node's child at the given {index}, where zero represents the first +--- child. +--- @param index integer +--- @return TSNode? +function TSNode:child(index) end + +--- Get the node's number of named children. +--- @return integer +function TSNode:named_child_count() end + +--- Get the node's named child at the given {index}, where zero represents the +--- first named child. +--- @param index integer +--- @return TSNode? +function TSNode:named_child(index) end + +--- Get the node's child that contains {descendant}. +--- @param descendant TSNode +--- @return TSNode? +function TSNode:child_containing_descendant(descendant) end + +--- Get the node's start position. Return three values: the row, column and +--- total byte count (all zero-based). +--- @return integer, integer, integer +function TSNode:start() end + +--- Get the node's end position. Return three values: the row, column and +--- total byte count (all zero-based). +--- @return integer, integer, integer +function TSNode:end_() end + +--- Get the range of the node. +--- +--- Return four or six values: +--- +--- - start row +--- - start column +--- - start byte (if {include_bytes} is `true`) +--- - end row +--- - end column +--- - end byte (if {include_bytes} is `true`) +--- @param include_bytes boolean? +function TSNode:range(include_bytes) end + +--- @nodoc +--- @param include_bytes false? +--- @return integer, integer, integer, integer +function TSNode:range(include_bytes) end + +--- @nodoc +--- @param include_bytes true +--- @return integer, integer, integer, integer, integer, integer +function TSNode:range(include_bytes) end + +--- Get the node's type as a string. +--- @return string +function TSNode:type() end + +--- Get the node's type as a numerical id. +--- @return integer +function TSNode:symbol() end + +--- Check if the node is named. Named nodes correspond to named rules in the +--- grammar, whereas anonymous nodes correspond to string literals in the +--- grammar. +--- @return boolean +function TSNode:named() end + +--- Check if the node is missing. Missing nodes are inserted by the parser in +--- order to recover from certain kinds of syntax errors. +--- @return boolean +function TSNode:missing() end + +--- Check if the node is extra. Extra nodes represent things like comments, +--- which are not required by the grammar but can appear anywhere. +--- @return boolean +function TSNode:extra() end + +--- Check if a syntax node has been edited. +--- @return boolean +function TSNode:has_changes() end + +--- Check if the node is a syntax error or contains any syntax errors. +--- @return boolean +function TSNode:has_error() end + +--- Get an S-expression representing the node as a string. +--- @return string +function TSNode:sexpr() end + +--- Get a unique identifier for the node inside its own tree. +--- +--- No guarantees are made about this identifier's internal representation, +--- except for being a primitive Lua type with value equality (so not a +--- table). Presently it is a (non-printable) string. +--- +--- Note: The `id` is not guaranteed to be unique for nodes from different +--- trees. +--- @return string +function TSNode:id() end + +--- Get the |TSTree| of the node. +--- @return TSTree +function TSNode:tree() end + +--- Get the smallest node within this node that spans the given range of (row, +--- column) positions +--- @param start_row integer +--- @param start_col integer +--- @param end_row integer +--- @param end_col integer +--- @return TSNode? +function TSNode:descendant_for_range(start_row, start_col, end_row, end_col) end + +--- Get the smallest named node within this node that spans the given range of +--- (row, column) positions +--- @param start_row integer +--- @param start_col integer +--- @param end_row integer +--- @param end_col integer +--- @return TSNode? +function TSNode:named_descendant_for_range(start_row, start_col, end_row, end_col) end + +--- Check if {node} refers to the same node within the same tree. +--- @param node TSNode +--- @return boolean +function TSNode:equal(node) end + +--- Return the number of bytes spanned by this node. +--- @return integer +function TSNode:byte_length() end diff --git a/runtime/lua/vim/treesitter/_meta/tstree.lua b/runtime/lua/vim/treesitter/_meta/tstree.lua new file mode 100644 index 0000000000..24cb60040e --- /dev/null +++ b/runtime/lua/vim/treesitter/_meta/tstree.lua @@ -0,0 +1,44 @@ +---@meta +-- luacheck: no unused args +error('Cannot require a meta file') + +--- @brief A "treesitter tree" represents the parsed contents of a buffer, which can be +--- used to perform further analysis. It is a |userdata| reference to an object +--- held by the treesitter library. +--- +--- An instance `TSTree` of a treesitter tree supports the following methods. + +---@nodoc +---@class TSTree: userdata +local TSTree = {} -- luacheck: no unused + +--- Return the root node of this tree. +---@return TSNode +function TSTree:root() end + +-- stylua: ignore +---@param start_byte integer +---@param end_byte_old integer +---@param end_byte_new integer +---@param start_row integer +---@param start_col integer +---@param end_row_old integer +---@param end_col_old integer +---@param end_row_new integer +---@param end_col_new integer +---@nodoc +function TSTree: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 + +--- Returns a copy of the `TSTree`. +---@return TSTree +function TSTree:copy() end + +---@param include_bytes true +---@return Range6[] +---@nodoc +function TSTree:included_ranges(include_bytes) end + +---@param include_bytes false +---@return Range4[] +---@nodoc +function TSTree:included_ranges(include_bytes) end diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index 12b4cbc7b9..c5e4b86e1e 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -40,7 +40,8 @@ end local function guess_query_lang(buf) local filename = api.nvim_buf_get_name(buf) if filename ~= '' then - return vim.F.npcall(vim.fn.fnamemodify, filename, ':p:h:t') + local resolved_filename = vim.F.npcall(vim.fn.fnamemodify, filename, ':p:h:t') + return resolved_filename and vim.treesitter.language.get_lang(resolved_filename) end end @@ -64,7 +65,7 @@ local function normalize_opts(buf, opts) end local lint_query = [[;; query - (program [(named_node) (list) (grouping)] @toplevel) + (program [(named_node) (anonymous_node) (list) (grouping)] @toplevel) (named_node name: _ @node.named) (anonymous_node @@ -170,17 +171,17 @@ function M.lint(buf, opts) --- @type (table|nil) local parser_info = vim.F.npcall(vim.treesitter.language.inspect, lang) + local lang_context = { + lang = lang, + parser_info = parser_info, + is_first_lang = i == 1, + } - local parser = vim.treesitter.get_parser(buf) + local parser = assert(vim.treesitter.get_parser(buf, nil, { error = false })) parser:parse() parser:for_each_tree(function(tree, ltree) if ltree:lang() == 'query' then - for _, match, _ in query:iter_matches(tree:root(), buf, 0, -1, { all = true }) do - local lang_context = { - lang = lang, - parser_info = parser_info, - is_first_lang = i == 1, - } + for _, match, _ in query:iter_matches(tree:root(), buf, 0, -1) do lint_match(buf, match, query, lang_context, diagnostics) end end @@ -240,7 +241,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([[\]], [[\\]]) .. '"' ---@type string + local text = s[2] and s[1] or string.format('%q', s[1]):gsub('\n', 'n') ---@type string if text:find(base, 1, true) then table.insert(items, text) end diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 8d727c3c52..82ab8517aa 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -3,16 +3,19 @@ local api = vim.api local M = {} ---@class Range2 +---@inlinedoc ---@field [1] integer start row ---@field [2] integer end row ---@class Range4 +---@inlinedoc ---@field [1] integer start row ---@field [2] integer start column ---@field [3] integer end row ---@field [4] integer end column ---@class Range6 +---@inlinedoc ---@field [1] integer start row ---@field [2] integer start column ---@field [3] integer start bytes @@ -150,6 +153,7 @@ function M.contains(r1, r2) return true end +--- @private --- @param source integer|string --- @param index integer --- @return integer diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 5c91f101c0..90c3720b80 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -76,10 +76,14 @@ end --- ---@package function TSTreeView:new(bufnr, lang) - local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang) - if not ok then - local err = parser --[[ @as string ]] - return nil, 'No parser available for the given buffer:\n' .. err + local parser = vim.treesitter.get_parser(bufnr or 0, lang, { error = false }) + if not parser then + return nil, + string.format( + 'Failed to create TSTreeView for buffer %s: no parser for lang "%s"', + bufnr, + lang + ) end -- For each child tree (injected language), find the root of the tree and locate the node within @@ -154,7 +158,8 @@ end ---@param w integer ---@param b integer -local function set_dev_properties(w, b) +---@param opts nil|{ indent?: integer } +local function set_dev_options(w, b, opts) vim.wo[w].scrolloff = 5 vim.wo[w].wrap = false vim.wo[w].foldmethod = 'expr' @@ -165,6 +170,12 @@ local function set_dev_properties(w, b) vim.bo[b].buftype = 'nofile' vim.bo[b].bufhidden = 'wipe' vim.bo[b].filetype = 'query' + vim.bo[b].swapfile = false + + opts = opts or {} + if opts.indent then + vim.bo[b].shiftwidth = opts.indent + end end --- Updates the cursor position in the inspector to match the node under the cursor. @@ -174,7 +185,7 @@ end --- @param source_buf integer --- @param inspect_buf integer --- @param inspect_win integer ---- @param pos? { [1]: integer, [2]: integer } +--- @param pos? [integer, integer] local function set_inspector_cursor(treeview, lang, source_buf, inspect_buf, inspect_win, pos) api.nvim_buf_clear_namespace(inspect_buf, treeview.ns, 0, -1) @@ -183,6 +194,7 @@ local function set_inspector_cursor(treeview, lang, source_buf, inspect_buf, ins lang = lang, pos = pos, ignore_injections = false, + include_anonymous = treeview.opts.anon, }) if not cursor_node then return @@ -220,14 +232,13 @@ function TSTreeView:draw(bufnr) local text ---@type string if item.node:named() then - if item.field then - text = string.format('%s: (%s', item.field, item.node:type()) - else - text = string.format('(%s', item.node:type()) - end + text = string.format('(%s', item.node:type()) else text = string.format('%q', item.node:type()):gsub('\n', 'n') end + if item.field then + text = string.format('%s: %s', item.field, text) + end local next = self:get(i + 1) if not next or next.depth <= item.depth then @@ -325,7 +336,10 @@ function M.inspect_tree(opts) opts = opts or {} + -- source buffer local buf = api.nvim_get_current_buf() + + -- window id for source buffer local win = api.nvim_get_current_win() local treeview = assert(TSTreeView:new(buf, opts.lang)) @@ -334,12 +348,14 @@ function M.inspect_tree(opts) close_win(vim.b[buf].dev_inspect) end + -- window id for tree buffer local w = opts.winid if not w then vim.cmd(opts.command or '60vnew') w = api.nvim_get_current_win() end + -- tree buffer local b = opts.bufnr if b then api.nvim_win_set_buf(w, b) @@ -350,7 +366,7 @@ function M.inspect_tree(opts) vim.b[buf].dev_inspect = w vim.b[b].dev_base = win -- base window handle vim.b[b].disable_query_linter = true - set_dev_properties(w, b) + set_dev_options(w, b, { indent = treeview.opts.indent }) local title --- @type string? local opts_title = opts.title @@ -375,6 +391,12 @@ function M.inspect_tree(opts) callback = function() local row = api.nvim_win_get_cursor(w)[1] local lnum, col = treeview:get(row).node:start() + + -- update source window if original was closed + if not api.nvim_win_is_valid(win) then + win = vim.fn.win_findbuf(buf)[1] + end + api.nvim_set_current_win(win) api.nvim_win_set_cursor(win, { lnum + 1, col }) end, @@ -432,6 +454,7 @@ function M.inspect_tree(opts) return true end + w = api.nvim_get_current_win() api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1) local row = api.nvim_win_get_cursor(w)[1] local lnum, col, end_lnum, end_col = treeview:get(row).node:range() @@ -441,6 +464,11 @@ function M.inspect_tree(opts) hl_group = 'Visual', }) + -- update source window if original was closed + if not api.nvim_win_is_valid(win) then + win = vim.fn.win_findbuf(buf)[1] + end + local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) -- Move the cursor if highlighted range is completely out of view @@ -506,7 +534,10 @@ function M.inspect_tree(opts) buffer = buf, once = true, callback = function() - close_win(w) + -- close all tree windows + for _, window in pairs(vim.fn.win_findbuf(b)) do + close_win(window) + end end, }) end @@ -519,7 +550,7 @@ local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') 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, lang) + local parser = assert(vim.treesitter.get_parser(base_buf, lang, { error = false })) 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') @@ -554,6 +585,8 @@ end --- @private --- @param lang? string language to open the query editor for. +--- @return boolean? `true` on success, `nil` on failure +--- @return string? error message, if applicable function M.edit_query(lang) local buf = api.nvim_get_current_buf() local win = api.nvim_get_current_win() @@ -576,9 +609,10 @@ function M.edit_query(lang) end vim.cmd(cmd) - local ok, parser = pcall(vim.treesitter.get_parser, buf, lang) - if not ok then - return nil, 'No parser available for the given buffer' + local parser = vim.treesitter.get_parser(buf, lang, { error = false }) + if not parser then + return nil, + string.format('Failed to show query editor for buffer %s: no parser for lang "%s"', buf, lang) end lang = parser:lang() @@ -587,7 +621,7 @@ function M.edit_query(lang) 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) + set_dev_options(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 @@ -652,6 +686,8 @@ function M.edit_query(lang) }) vim.cmd('normal! G') vim.cmd.startinsert() + + return true end return M diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index ed3616ef46..637f9ea543 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -28,6 +28,9 @@ function M.check() ) end end + + local can_wasm = vim._ts_add_language_from_wasm ~= nil + health.info(string.format('Can load WASM parsers: %s', tostring(can_wasm))) end return M diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index d2f986b874..a94c408f4e 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -47,7 +47,7 @@ function TSHighlighterQuery:get_hl_from_capture(capture) return self.hl_cache[capture] end ----@package +---@nodoc function TSHighlighterQuery:query() return self._query end @@ -75,7 +75,7 @@ local TSHighlighter = { TSHighlighter.__index = TSHighlighter ----@package +---@nodoc --- --- Creates a highlighter for `tree`. --- @@ -139,11 +139,14 @@ function TSHighlighter.new(tree, opts) -- but use synload.vim rather than syntax.vim to not enable -- syntax FileType autocmds. Later on we should integrate with the -- `:syntax` and `set syntax=...` machinery properly. + -- Still need to ensure that syntaxset augroup exists, so that calling :destroy() + -- immediately afterwards will not error. if vim.g.syntax_on ~= 1 then vim.cmd.runtime({ 'syntax/synload.vim', bang = true }) + vim.api.nvim_create_augroup('syntaxset', { clear = false }) end - api.nvim_buf_call(self.bufnr, function() + vim._with({ buf = self.bufnr }, function() vim.opt_local.spelloptions:append('noplainbuffer') end) @@ -232,7 +235,7 @@ function TSHighlighter:on_changedtree(changes) end --- Gets the query used for @param lang ----@package +---@nodoc ---@param lang string Language used by the highlighter. ---@return vim.treesitter.highlighter.Query function TSHighlighter:get_query(lang) @@ -377,11 +380,15 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) return end + -- Do not affect potentially populated highlight state. Here we just want a temporary + -- empty state so the C code can detect whether the region should be spell checked. + local highlight_states = self._highlight_states self:prepare_highlight_states(srow, erow) for row = srow, erow do on_line_impl(self, buf, row, true) end + self._highlight_states = highlight_states end ---@private diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index d0a74daa6c..9f7807e036 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -7,11 +7,15 @@ local ft_to_lang = { help = 'vimdoc', } ---- Get the filetypes associated with the parser named {lang}. +--- Returns the filetypes for which a parser named {lang} is used. +--- +--- The list includes {lang} itself plus all filetypes registered via +--- |vim.treesitter.language.register()|. +--- --- @param lang string Name of parser --- @return string[] filetypes function M.get_filetypes(lang) - local r = {} ---@type string[] + local r = { lang } ---@type string[] for ft, p in pairs(ft_to_lang) do if p == lang then r[#r + 1] = ft @@ -20,6 +24,12 @@ function M.get_filetypes(lang) return r end +--- Returns the language name to be used when loading a parser for {filetype}. +--- +--- If no language has been explicitly registered via |vim.treesitter.language.register()|, +--- default to {filetype}. For composite filetypes like `html.glimmer`, only the main filetype is +--- returned. +--- --- @param filetype string --- @return string|nil function M.get_lang(filetype) @@ -29,9 +39,9 @@ function M.get_lang(filetype) if ft_to_lang[filetype] then return ft_to_lang[filetype] end - -- support subfiletypes like html.glimmer + -- for subfiletypes like html.glimmer use only "main" filetype filetype = vim.split(filetype, '.', { plain = true })[1] - return ft_to_lang[filetype] + return ft_to_lang[filetype] or filetype end ---@deprecated @@ -52,17 +62,27 @@ function M.require_language(lang, path, silent, symbol_name) return installed end - M.add(lang, opts) - return true + return M.add(lang, opts) +end + +--- Load wasm or native parser (wrapper) +--- todo(clason): move to C +--- +---@param path string Path of parser library +---@param lang string Language name +---@param symbol_name? string Internal symbol name for the language to load (default lang) +---@return boolean? True if parser is loaded +local function loadparser(path, lang, symbol_name) + if vim.endswith(path, '.wasm') then + return vim._ts_add_language_from_wasm and vim._ts_add_language_from_wasm(path, lang) + else + return vim._ts_add_language_from_object(path, lang, symbol_name) + end end ---@class vim.treesitter.language.add.Opts ---@inlinedoc --- ----Default filetype the parser should be associated with. ----(Default: {lang}) ----@field filetype? string|string[] ---- ---Optional path the parser is located at ---@field path? string --- @@ -71,46 +91,52 @@ end --- Load parser with name {lang} --- ---- Parsers are searched in the `parser` runtime directory, or the provided {path} +--- Parsers are searched in the `parser` runtime directory, or the provided {path}. +--- Can be used to check for available parsers before enabling treesitter features, e.g., +--- ```lua +--- if vim.treesitter.language.add('markdown') then +--- vim.treesitter.start(bufnr, 'markdown') +--- end +--- ``` --- ---@param lang string Name of the parser (alphanumerical and `_` only) ---@param opts? vim.treesitter.language.add.Opts Options: +---@return boolean? True if parser is loaded +---@return string? Error if parser cannot be loaded function M.add(lang, opts) opts = opts or {} local path = opts.path - local filetype = opts.filetype or lang local symbol_name = opts.symbol_name vim.validate({ lang = { lang, 'string' }, path = { path, 'string', true }, symbol_name = { symbol_name, 'string', true }, - filetype = { filetype, { 'string', 'table' }, true }, }) -- parser names are assumed to be lowercase (consistent behavior on case-insensitive file systems) lang = lang:lower() if vim._ts_has_language(lang) then - M.register(lang, filetype) - return + return true end if path == nil then + -- allow only safe language names when looking for libraries to load if not (lang and lang:match('[%w_]+') == lang) then - error("'" .. lang .. "' is not a valid language name") + return nil, string.format('Invalid language name "%s"', lang) end local fname = 'parser/' .. lang .. '.*' local paths = api.nvim_get_runtime_file(fname, false) if #paths == 0 then - error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") + return nil, string.format('No parser for language "%s"', lang) end path = paths[1] end - vim._ts_add_language(path, lang, symbol_name) - M.register(lang, filetype) + return loadparser(path, lang, symbol_name) or nil, + string.format('Cannot load parser %s for language "%s"', path, lang) end --- @param x string|string[] diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b0812123b9..fd68c2b910 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -98,9 +98,9 @@ local LanguageTree = {} LanguageTree.__index = LanguageTree ---- @package +--- @nodoc --- ---- |LanguageTree| contains a tree of parsers: the root treesitter parser for {lang} and any +--- LanguageTree contains a tree of parsers: the root treesitter parser for {lang} and any --- "injected" language parsers, which themselves may inject other languages, recursively. --- ---@param source (integer|string) Buffer or text string to parse @@ -108,7 +108,7 @@ LanguageTree.__index = LanguageTree ---@param opts vim.treesitter.LanguageTree.new.Opts? ---@return vim.treesitter.LanguageTree parser object function LanguageTree.new(source, lang, opts) - language.add(lang) + assert(language.add(lang)) opts = opts or {} if source == 0 then @@ -638,6 +638,8 @@ end ---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. +---Each list represents a range in the form of +---{ {start_row}, {start_col}, {start_bytes}, {end_row}, {end_col}, {end_bytes} }. ---@return table<integer, Range6[]> function LanguageTree:included_regions() if self._regions then @@ -732,7 +734,7 @@ local function add_injection(t, tree_index, pattern, lang, combined, ranges) table.insert(t[tree_index][lang][pattern].regions, ranges) end --- TODO(clason): replace by refactored `ts.has_parser` API (without registering) +-- TODO(clason): replace by refactored `ts.has_parser` API (without side effects) --- The result of this function is cached to prevent nvim_get_runtime_file from being --- called too often --- @param lang string parser name @@ -831,13 +833,7 @@ function LanguageTree:_get_injections() local start_line, _, end_line, _ = root_node:range() for pattern, match, metadata in - self._injection_query:iter_matches( - root_node, - self._source, - start_line, - end_line + 1, - { all = true } - ) + self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do local lang, combined, ranges = self:_get_injection(match, metadata) if lang then @@ -951,7 +947,7 @@ function LanguageTree:_edit( end end ----@package +---@nodoc ---@param bufnr integer ---@param changed_tick integer ---@param start_row integer @@ -1023,12 +1019,12 @@ function LanguageTree:_on_bytes( ) end ----@package +---@nodoc function LanguageTree:_on_reload() self:invalidate(true) end ----@package +---@nodoc function LanguageTree:_on_detach(...) self:invalidate(true) self:_do_callback('detach', ...) @@ -1087,7 +1083,7 @@ end --- Determines whether {range} is contained in the |LanguageTree|. --- ----@param range Range4 `{ start_line, start_col, end_line, end_col }` +---@param range Range4 ---@return boolean function LanguageTree:contains(range) for _, tree in pairs(self._trees) do @@ -1108,7 +1104,7 @@ end --- Gets the tree that contains {range}. --- ----@param range Range4 `{ start_line, start_col, end_line, end_col }` +---@param range Range4 ---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts ---@return TSTree? function LanguageTree:tree_for_range(range, opts) @@ -1133,9 +1129,21 @@ function LanguageTree:tree_for_range(range, opts) return nil end +--- Gets the smallest node that contains {range}. +--- +---@param range Range4 +---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts +---@return TSNode? +function LanguageTree:node_for_range(range, opts) + local tree = self:tree_for_range(range, opts) + if tree then + return tree:root():descendant_for_range(unpack(range)) + end +end + --- Gets the smallest named node that contains {range}. --- ----@param range Range4 `{ start_line, start_col, end_line, end_col }` +---@param range Range4 ---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts ---@return TSNode? function LanguageTree:named_node_for_range(range, opts) @@ -1147,7 +1155,7 @@ end --- Gets the appropriate language that contains {range}. --- ----@param range Range4 `{ start_line, start_col, end_line, end_col }` +---@param range Range4 ---@return vim.treesitter.LanguageTree tree Managing {range} function LanguageTree:language_for_range(range) for _, child in pairs(self._children) do diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index ef5c2143a7..4614967799 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -247,8 +247,7 @@ end) --- ---@see [vim.treesitter.query.get()] M.parse = memoize('concat-2', function(lang, query) - language.add(lang) - + assert(language.add(lang)) local ts_query = vim._ts_parse_query(lang, query) return Query.new(lang, ts_query) end) @@ -487,8 +486,8 @@ predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?'] ---@class vim.treesitter.query.TSMetadata ---@field range? Range ---@field conceal? string ----@field [integer] vim.treesitter.query.TSMetadata ----@field [string] integer|string +---@field [integer]? vim.treesitter.query.TSMetadata +---@field [string]? integer|string ---@alias TSDirective fun(match: table<integer,TSNode[]>, _, _, predicate: (string|integer)[], metadata: vim.treesitter.query.TSMetadata) @@ -620,16 +619,16 @@ local directive_handlers = { --- @field force? boolean --- --- Use the correct implementation of the match table where capture IDs map to ---- a list of nodes instead of a single node. Defaults to false (for backward ---- compatibility). This option will eventually become the default and removed. +--- a list of nodes instead of a single node. Defaults to true. This option will +--- be removed in a future release. --- @field all? boolean --- Adds a new predicate to be used in queries --- ---@param name string Name of the predicate, without leading # ----@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table) +---@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: vim.treesitter.query.TSMetadata) --- - see |vim.treesitter.query.add_directive()| for argument meanings ----@param opts vim.treesitter.query.add_predicate.Opts +---@param opts? vim.treesitter.query.add_predicate.Opts function M.add_predicate(name, handler, opts) -- Backward compatibility: old signature had "force" as boolean argument if type(opts) == 'boolean' then @@ -642,7 +641,7 @@ function M.add_predicate(name, handler, opts) error(string.format('Overriding existing predicate %s', name)) end - if opts.all then + if opts.all ~= false then predicate_handlers[name] = handler else --- @param match table<integer, TSNode[]> @@ -667,7 +666,7 @@ end --- metadata table `metadata[capture_id].key = value` --- ---@param name string Name of the directive, without leading # ----@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table) +---@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: vim.treesitter.query.TSMetadata) --- - match: A table mapping capture IDs to a list of captured nodes --- - pattern: the index of the matching pattern in the query file --- - predicate: list of strings containing the full directive being called, e.g. @@ -894,16 +893,10 @@ end --- index of the pattern in the query, a table mapping capture indices to a list --- of nodes, and metadata from any directives processing the match. --- ---- WARNING: Set `all=true` to ensure all matching nodes in a match are ---- returned, otherwise only the last node in a match is returned, breaking captures ---- involving quantifiers such as `(comment)+ @comment`. The default option ---- `all=false` is only provided for backward compatibility and will be removed ---- after Nvim 0.10. ---- --- Example: --- --- ```lua ---- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, 0, -1, { all = true }) do +--- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, 0, -1) do --- for id, nodes in pairs(match) do --- local name = query.captures[id] --- for _, node in ipairs(nodes) do @@ -925,11 +918,11 @@ end --- - max_start_depth (integer) if non-zero, sets the maximum start depth --- for each match. This is used to prevent traversing too deep into a tree. --- - match_limit (integer) Set the maximum number of in-progress matches (Default: 256). ---- - all (boolean) When set, the returned match table maps capture IDs to a list of nodes. ---- Older versions of iter_matches incorrectly mapped capture IDs to a single node, which is ---- incorrect behavior. This option will eventually become the default and removed. +--- - all (boolean) When `false` (default `true`), the returned table maps capture IDs to a single +--- (last) node instead of the full list of matching nodes. This option is only for backward +--- compatibility and will be removed in a future release. --- ----@return (fun(): integer, table<integer, TSNode[]>, table): pattern id, match, metadata +---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata): pattern id, match, metadata function Query:iter_matches(node, source, start, stop, opts) opts = opts or {} opts.match_limit = opts.match_limit or 256 @@ -960,10 +953,10 @@ function Query:iter_matches(node, source, start, stop, opts) local captures = match:captures() - if not opts.all then + if opts.all == false then -- Convert the match table into the old buggy version for backward - -- compatibility. This is slow. Plugin authors, if you're reading this, set the "all" - -- option! + -- compatibility. This is slow, but we only do it when the caller explicitly opted into it by + -- setting `all` to `false`. local old_match = {} ---@type table<integer, TSNode> for k, v in pairs(captures or {}) do old_match[k] = v[#v] @@ -1034,7 +1027,7 @@ end --- --- @param lang? string language to open the query editor for. If omitted, inferred from the current buffer's filetype. function M.edit(lang) - vim.treesitter.dev.edit_query(lang) + assert(vim.treesitter.dev.edit_query(lang)) end return M |