diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
| commit | 1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch) | |
| tree | cd08258054db80bb9a11b1061bb091c70b76926a /runtime/lua/vim/treesitter | |
| parent | eaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff) | |
| parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
| download | rneovim-aucmd_textputpost.tar.gz rneovim-aucmd_textputpost.tar.bz2 rneovim-aucmd_textputpost.zip | |
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
Diffstat (limited to 'runtime/lua/vim/treesitter')
| -rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 456 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_meta.lua | 80 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_query_linter.lua | 249 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_range.lua | 193 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/dev.lua | 645 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/health.lua | 30 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 183 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 144 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 1041 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/playground.lua | 186 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 441 |
11 files changed, 2957 insertions, 691 deletions
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua new file mode 100644 index 0000000000..5c1cc06908 --- /dev/null +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -0,0 +1,456 @@ +local ts = vim.treesitter + +local Range = require('vim.treesitter._range') + +local api = vim.api + +---@class TS.FoldInfo +---@field levels table<integer,string> +---@field levels0 table<integer,integer> +---@field private start_counts table<integer,integer> +---@field private stop_counts table<integer,integer> +local FoldInfo = {} +FoldInfo.__index = FoldInfo + +---@private +function FoldInfo.new() + return setmetatable({ + start_counts = {}, + stop_counts = {}, + levels0 = {}, + levels = {}, + }, FoldInfo) +end + +---@package +---@param srow integer +---@param erow integer +function FoldInfo:invalidate_range(srow, erow) + for i = srow, erow do + self.start_counts[i + 1] = nil + self.stop_counts[i + 1] = nil + self.levels0[i + 1] = nil + self.levels[i + 1] = nil + end +end + +--- Efficiently remove items from middle of a list a list. +--- +--- Calling table.remove() in a loop will re-index the tail of the table on +--- every iteration, instead this function will re-index the table exactly +--- once. +--- +--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 +--- +---@param t any[] +---@param first integer +---@param last integer +local function list_remove(t, first, last) + local n = #t + for i = 0, n - first do + t[first + i] = t[last + 1 + i] + t[last + 1 + i] = nil + end +end + +---@package +---@param srow integer +---@param erow integer +function FoldInfo:remove_range(srow, erow) + list_remove(self.levels, srow + 1, erow) + list_remove(self.levels0, srow + 1, erow) + list_remove(self.start_counts, srow + 1, erow) + list_remove(self.stop_counts, srow + 1, erow) +end + +--- Efficiently insert items into the middle of a list. +--- +--- Calling table.insert() in a loop will re-index the tail of the table on +--- every iteration, instead this function will re-index the table exactly +--- once. +--- +--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 +--- +---@param t any[] +---@param first integer +---@param last integer +---@param v any +local function list_insert(t, first, last, v) + local n = #t + + -- Shift table forward + for i = n - first, 0, -1 do + t[last + 1 + i] = t[first + i] + end + + -- Fill in new values + for i = first, last do + t[i] = v + end +end + +---@package +---@param srow integer +---@param erow integer +function FoldInfo:add_range(srow, erow) + list_insert(self.levels, srow + 1, erow, '-1') + list_insert(self.levels0, srow + 1, erow, -1) + list_insert(self.start_counts, srow + 1, erow, nil) + list_insert(self.stop_counts, srow + 1, erow, nil) +end + +---@package +---@param lnum integer +function FoldInfo:add_start(lnum) + self.start_counts[lnum] = (self.start_counts[lnum] or 0) + 1 +end + +---@package +---@param lnum integer +function FoldInfo:add_stop(lnum) + self.stop_counts[lnum] = (self.stop_counts[lnum] or 0) + 1 +end + +---@package +---@param lnum integer +---@return integer +function FoldInfo:get_start(lnum) + return self.start_counts[lnum] or 0 +end + +---@package +---@param lnum integer +---@return integer +function FoldInfo:get_stop(lnum) + return self.stop_counts[lnum] or 0 +end + +local function trim_level(level) + local max_fold_level = vim.wo.foldnestmax + if level > max_fold_level then + return max_fold_level + end + return level +end + +--- If a parser doesn't have any ranges explicitly set, treesitter will +--- return a range with end_row and end_bytes with a value of UINT32_MAX, +--- so clip end_row to the max buffer line. +--- +--- TODO(lewis6991): Handle this generally +--- +--- @param bufnr integer +--- @param erow integer? +--- @return integer +local function normalise_erow(bufnr, erow) + local max_erow = api.nvim_buf_line_count(bufnr) - 1 + return math.min(erow or max_erow, max_erow) +end + +-- TODO(lewis6991): Setup a decor provider so injections folds can be parsed +-- as the window is redrawn +---@param bufnr integer +---@param info TS.FoldInfo +---@param srow integer? +---@param erow integer? +---@param parse_injections? boolean +local function get_folds_levels(bufnr, info, srow, erow, parse_injections) + srow = srow or 0 + erow = normalise_erow(bufnr, erow) + + info:invalidate_range(srow, erow) + + local prev_start = -1 + local prev_stop = -1 + + local parser = ts.get_parser(bufnr) + + parser:parse(parse_injections and { srow, erow } or nil) + + parser:for_each_tree(function(tree, ltree) + local query = ts.query.get(ltree:lang(), 'folds') + if not query then + return + end + + -- erow in query is end-exclusive + local q_erow = erow and erow + 1 or -1 + + for id, node, metadata in query:iter_captures(tree:root(), bufnr, srow, q_erow) do + if query.captures[id] == 'fold' then + local range = ts.get_range(node, bufnr, metadata[id]) + local start, _, stop, stop_col = Range.unpack4(range) + + if stop_col == 0 then + stop = stop - 1 + end + + local fold_length = stop - start + 1 + + -- Fold only multiline nodes that are not exactly the same as previously met folds + -- Checking against just the previously found fold is sufficient if nodes + -- are returned in preorder or postorder when traversing tree + if + fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop) + then + info:add_start(start + 1) + info:add_stop(stop + 1) + prev_start = start + prev_stop = stop + end + end + end + end) + + local current_level = info.levels0[srow] or 0 + + -- We now have the list of fold opening and closing, fill the gaps and mark where fold start + for lnum = srow + 1, erow + 1 do + local last_trimmed_level = trim_level(current_level) + current_level = current_level + info:get_start(lnum) + info.levels0[lnum] = current_level + + local trimmed_level = trim_level(current_level) + current_level = current_level - info:get_stop(lnum) + + -- Determine if it's the start/end of a fold + -- NB: vim's fold-expr interface does not have a mechanism to indicate that + -- two (or more) folds start at this line, so it cannot distinguish between + -- ( \n ( \n )) \n (( \n ) \n ) + -- versus + -- ( \n ( \n ) \n ( \n ) \n ) + -- If it did have such a mechanism, (trimmed_level - last_trimmed_level) + -- would be the correct number of starts to pass on. + local prefix = '' + if trimmed_level - last_trimmed_level > 0 then + prefix = '>' + end + + info.levels[lnum] = prefix .. tostring(trimmed_level) + end +end + +local M = {} + +---@type table<integer,TS.FoldInfo> +local foldinfos = {} + +local group = api.nvim_create_augroup('treesitter/fold', {}) + +--- Update the folds in the windows that contain the buffer and use expr foldmethod (assuming that +--- the user doesn't use different foldexpr for the same buffer). +--- +--- Nvim usually automatically updates folds when text changes, but it doesn't work here because +--- FoldInfo update is scheduled. So we do it manually. +local function foldupdate(bufnr) + local function do_update() + for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do + api.nvim_win_call(win, function() + if vim.wo.foldmethod == 'expr' then + vim._foldupdate() + end + end) + end + end + + if api.nvim_get_mode().mode == 'i' then + -- foldUpdate() is guarded in insert mode. So update folds on InsertLeave + if #(api.nvim_get_autocmds({ + group = group, + buffer = bufnr, + })) > 0 then + return + end + api.nvim_create_autocmd('InsertLeave', { + group = group, + buffer = bufnr, + once = true, + callback = do_update, + }) + return + end + + do_update() +end + +--- Schedule a function only if bufnr is loaded. +--- We schedule fold level computation for the following reasons: +--- * queries seem to use the old buffer state in on_bytes for some unknown reason; +--- * to avoid textlock; +--- * to avoid infinite recursion: +--- get_folds_levels → parse → _do_callback → on_changedtree → get_folds_levels. +---@param bufnr integer +---@param fn function +local function schedule_if_loaded(bufnr, fn) + vim.schedule(function() + if not api.nvim_buf_is_loaded(bufnr) then + return + end + fn() + end) +end + +---@param bufnr integer +---@param foldinfo TS.FoldInfo +---@param tree_changes Range4[] +local function on_changedtree(bufnr, foldinfo, tree_changes) + schedule_if_loaded(bufnr, function() + for _, change in ipairs(tree_changes) do + local srow, _, erow = Range.unpack4(change) + get_folds_levels(bufnr, foldinfo, srow, erow) + end + if #tree_changes > 0 then + foldupdate(bufnr) + end + end) +end + +---@param bufnr integer +---@param foldinfo TS.FoldInfo +---@param start_row integer +---@param old_row integer +---@param new_row integer +local function on_bytes(bufnr, foldinfo, start_row, old_row, new_row) + local end_row_old = start_row + old_row + local end_row_new = start_row + new_row + + if new_row ~= old_row then + if new_row < old_row then + foldinfo:remove_range(end_row_new, end_row_old) + else + foldinfo:add_range(start_row, end_row_new) + end + schedule_if_loaded(bufnr, function() + get_folds_levels(bufnr, foldinfo, start_row, end_row_new) + foldupdate(bufnr) + end) + 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) + if not parser then + return '0' + end + + if not foldinfos[bufnr] then + foldinfos[bufnr] = FoldInfo.new() + get_folds_levels(bufnr, foldinfos[bufnr]) + + parser:register_cbs({ + on_changedtree = function(tree_changes) + on_changedtree(bufnr, foldinfos[bufnr], tree_changes) + end, + + on_bytes = function(_, _, start_row, _, _, old_row, _, _, new_row, _, _) + on_bytes(bufnr, foldinfos[bufnr], start_row, old_row, new_row) + end, + + on_detach = function() + foldinfos[bufnr] = nil + end, + }) + end + + return foldinfos[bufnr].levels[lnum] or '0' +end + +---@package +---@return { [1]: string, [2]: string[] }[]|string +function M.foldtext() + local foldstart = vim.v.foldstart + local bufnr = api.nvim_get_current_buf() + + ---@type boolean, LanguageTree + local ok, parser = pcall(ts.get_parser, bufnr) + if not ok then + return vim.fn.foldtext() + end + + local query = ts.query.get(parser:lang(), 'highlights') + if not query then + return vim.fn.foldtext() + end + + local tree = parser:parse({ foldstart - 1, foldstart })[1] + + local line = api.nvim_buf_get_lines(bufnr, foldstart - 1, foldstart, false)[1] + if not line then + return vim.fn.foldtext() + end + + ---@type { [1]: string, [2]: string[], range: { [1]: integer, [2]: integer } }[] | { [1]: string, [2]: string[] }[] + local result = {} + + local line_pos = 0 + + for id, node, metadata in query:iter_captures(tree:root(), 0, foldstart - 1, foldstart) do + local name = query.captures[id] + local start_row, start_col, end_row, end_col = node:range() + + local priority = tonumber(metadata.priority or vim.highlight.priorities.treesitter) + + if start_row == foldstart - 1 and end_row == foldstart - 1 then + -- check for characters ignored by treesitter + if start_col > line_pos then + table.insert(result, { + line:sub(line_pos + 1, start_col), + {}, + range = { line_pos, start_col }, + }) + end + line_pos = end_col + + local text = line:sub(start_col + 1, end_col) + table.insert(result, { text, { { '@' .. name, priority } }, range = { start_col, end_col } }) + end + end + + local i = 1 + while i <= #result do + -- find first capture that is not in current range and apply highlights on the way + local j = i + 1 + while + j <= #result + and result[j].range[1] >= result[i].range[1] + and result[j].range[2] <= result[i].range[2] + do + for k, v in ipairs(result[i][2]) do + if not vim.tbl_contains(result[j][2], v) then + table.insert(result[j][2], k, v) + end + end + j = j + 1 + end + + -- remove the parent capture if it is split into children + if j > i + 1 then + table.remove(result, i) + else + -- highlights need to be sorted by priority, on equal prio, the deeper nested capture (earlier + -- in list) should be considered higher prio + if #result[i][2] > 1 then + table.sort(result[i][2], function(a, b) + return a[2] < b[2] + end) + end + + result[i][2] = vim.tbl_map(function(tbl) + return tbl[1] + end, result[i][2]) + result[i] = { result[i][1], result[i][2] } + + i = i + 1 + end + end + + return result +end + +return M diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua new file mode 100644 index 0000000000..80c998b555 --- /dev/null +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -0,0 +1,80 @@ +---@meta + +---@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 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 = {} + +---@param query userdata +---@param captures true +---@param start? integer +---@param end_? integer +---@param opts? table +---@return fun(): integer, TSNode, any +function TSNode:_rawquery(query, captures, start, end_, opts) end + +---@param query userdata +---@param captures false +---@param start? integer +---@param end_? integer +---@param opts? table +---@return fun(): string, any +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: 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)[]) +---@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 +---@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[] + +---@return integer +vim._ts_get_language_version = function() end + +---@return integer +vim._ts_get_minimum_language_version = function() end + +---@param lang string +---@return TSParser +vim._create_ts_parser = function(lang) end diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua new file mode 100644 index 0000000000..87d74789a3 --- /dev/null +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -0,0 +1,249 @@ +local api = vim.api + +local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') + +local M = {} + +--- @class QueryLinterNormalizedOpts +--- @field langs string[] +--- @field clear boolean + +--- @alias vim.treesitter.ParseError {msg: string, range: Range4} + +--- Contains language dependent context for the query linter +--- @class QueryLinterLanguageContext +--- @field lang string? Current `lang` of the targeted parser +--- @field parser_info table? Parser info returned by vim.treesitter.language.inspect +--- @field is_first_lang boolean Whether this is the first language of a linter run checking queries for multiple `langs` + +--- Adds a diagnostic for node in the query buffer +--- @param diagnostics Diagnostic[] +--- @param range Range4 +--- @param lint string +--- @param lang string? +local function add_lint_for_node(diagnostics, range, lint, lang) + local message = lint:gsub('\n', ' ') + diagnostics[#diagnostics + 1] = { + lnum = range[1], + end_lnum = range[3], + col = range[2], + end_col = range[4], + severity = vim.diagnostic.ERROR, + message = message, + source = lang, + } +end + +--- Determines the target language of a query file by its path: <lang>/<query_type>.scm +--- @param buf integer +--- @return string? +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') + end +end + +--- @param buf integer +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil +--- @return QueryLinterNormalizedOpts +local function normalize_opts(buf, opts) + opts = opts or {} + if not opts.langs then + opts.langs = guess_query_lang(buf) + end + + if type(opts.langs) ~= 'table' then + --- @diagnostic disable-next-line:assign-type-mismatch + opts.langs = { opts.langs } + end + + --- @cast opts QueryLinterNormalizedOpts + opts.langs = opts.langs or {} + return opts +end + +local lint_query = [[;; query + (program [(named_node) (list) (grouping)] @toplevel) + (named_node + name: _ @node.named) + (anonymous_node + name: _ @node.anonymous) + (field_definition + name: (identifier) @field) + (predicate + name: (identifier) @predicate.name + type: (predicate_type) @predicate.type) + (ERROR) @error +]] + +--- @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 + +--- @param node TSNode +--- @param buf integer +--- @param lang string +local function hash_parse(node, buf, lang) + return tostring(node:id()) .. tostring(buf) .. tostring(vim.b[buf].changedtick) .. lang +end + +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @return vim.treesitter.ParseError? +local parse = vim.func._memoize(hash_parse, function(node, buf, lang) + local query_text = vim.treesitter.get_node_text(node, buf) + local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query + + if not ok and type(err) == 'string' then + return get_error_entry(err, node) + end +end) + +--- @param buf integer +--- @param match table<integer,TSNode> +--- @param query Query +--- @param lang_context QueryLinterLanguageContext +--- @param diagnostics Diagnostic[] +local function lint_match(buf, match, query, lang_context, diagnostics) + local lang = lang_context.lang + local parser_info = lang_context.parser_info + + for id, node in pairs(match) do + local cap_id = query.captures[id] + + -- perform language-independent checks only for first lang + 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 and cap_id == 'toplevel' then + local err = parse(node, buf, lang) + if err then + add_lint_for_node(diagnostics, err.range, err.msg, lang) + end + end + end +end + +--- @private +--- @param buf integer Buffer to lint +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil Options for linting +function M.lint(buf, opts) + if buf == 0 then + buf = api.nvim_get_current_buf() + end + + local diagnostics = {} + local query = vim.treesitter.query.parse('query', lint_query) + + opts = normalize_opts(buf, opts) + + -- perform at least one iteration even with no langs to perform language independent checks + for i = 1, math.max(1, #opts.langs) do + local lang = opts.langs[i] + + --- @type (table|nil) + local parser_info = vim.F.npcall(vim.treesitter.language.inspect, lang) + + local parser = vim.treesitter.get_parser(buf) + 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) do + local lang_context = { + lang = lang, + parser_info = parser_info, + is_first_lang = i == 1, + } + lint_match(buf, match, query, lang_context, diagnostics) + end + end + end) + end + + vim.diagnostic.set(namespace, buf, diagnostics) +end + +--- @private +--- @param buf integer +function M.clear(buf) + vim.diagnostic.reset(namespace, buf) +end + +--- @private +--- @param findstart integer +--- @param base string +function M.omnifunc(findstart, base) + if findstart == 1 then + local result = + api.nvim_get_current_line():sub(1, api.nvim_win_get_cursor(0)[2]):find('["#%-%w]*$') + return result - 1 + end + + local buf = api.nvim_get_current_buf() + local query_lang = guess_query_lang(buf) + + local ok, parser_info = pcall(vim.treesitter.language.inspect, query_lang) + if not ok then + return -2 + end + + local items = {} + for _, f in pairs(parser_info.fields) do + if f:find(base, 1, true) then + table.insert(items, f .. ':') + end + end + for _, p in pairs(vim.treesitter.query.list_predicates()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + text = '#not-' .. p + found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, p in pairs(vim.treesitter.query.list_directives()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, s in pairs(parser_info.symbols) do + 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 + end + return { words = items, refresh = 'always' } +end + +return M diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua new file mode 100644 index 0000000000..8d727c3c52 --- /dev/null +++ b/runtime/lua/vim/treesitter/_range.lua @@ -0,0 +1,193 @@ +local api = vim.api + +local M = {} + +---@class Range2 +---@field [1] integer start row +---@field [2] integer end row + +---@class Range4 +---@field [1] integer start row +---@field [2] integer start column +---@field [3] integer end row +---@field [4] integer end column + +---@class Range6 +---@field [1] integer start row +---@field [2] integer start column +---@field [3] integer start bytes +---@field [4] integer end row +---@field [5] integer end column +---@field [6] integer end bytes + +---@alias Range Range2|Range4|Range6 + +---@private +---@param a_row integer +---@param a_col integer +---@param b_row integer +---@param b_col integer +---@return integer +--- 1: a > b +--- 0: a == b +--- -1: a < b +local function cmp_pos(a_row, a_col, b_row, b_col) + if a_row == b_row then + if a_col > b_col then + return 1 + elseif a_col < b_col then + return -1 + else + return 0 + end + elseif a_row > b_row then + return 1 + end + + return -1 +end + +M.cmp_pos = { + lt = function(...) + return cmp_pos(...) == -1 + end, + le = function(...) + return cmp_pos(...) ~= 1 + end, + gt = function(...) + return cmp_pos(...) == 1 + end, + ge = function(...) + return cmp_pos(...) ~= -1 + end, + eq = function(...) + return cmp_pos(...) == 0 + end, + ne = function(...) + return cmp_pos(...) ~= 0 + end, +} + +setmetatable(M.cmp_pos, { __call = cmp_pos }) + +---@private +---Check if a variable is a valid range object +---@param r any +---@return boolean +function M.validate(r) + if type(r) ~= 'table' or #r ~= 6 and #r ~= 4 then + return false + end + + for _, e in + ipairs(r --[[@as any[] ]]) + do + if type(e) ~= 'number' then + return false + end + end + + return true +end + +---@private +---@param r1 Range +---@param r2 Range +---@return boolean +function M.intercepts(r1, r2) + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) + + -- r1 is above r2 + if M.cmp_pos.le(erow_1, ecol_1, srow_2, scol_2) then + return false + end + + -- r1 is below r2 + if M.cmp_pos.ge(srow_1, scol_1, erow_2, ecol_2) then + return false + end + + return true +end + +---@private +---@param r Range +---@return integer, integer, integer, integer +function M.unpack4(r) + if #r == 2 then + return r[1], 0, r[2], 0 + end + local off_1 = #r == 6 and 1 or 0 + return r[1], r[2], r[3 + off_1], r[4 + off_1] +end + +---@private +---@param r Range6 +---@return integer, integer, integer, integer, integer, integer +function M.unpack6(r) + return r[1], r[2], r[3], r[4], r[5], r[6] +end + +---@private +---@param r1 Range +---@param r2 Range +---@return boolean whether r1 contains r2 +function M.contains(r1, r2) + local srow_1, scol_1, erow_1, ecol_1 = M.unpack4(r1) + local srow_2, scol_2, erow_2, ecol_2 = M.unpack4(r2) + + -- start doesn't fit + if M.cmp_pos.gt(srow_1, scol_1, srow_2, scol_2) then + return false + end + + -- end doesn't fit + if M.cmp_pos.lt(erow_1, ecol_1, erow_2, ecol_2) then + return false + end + + return true +end + +--- @param source integer|string +--- @param index integer +--- @return integer +local function get_offset(source, index) + if index == 0 then + return 0 + end + + if type(source) == 'number' then + return api.nvim_buf_get_offset(source, index) + end + + local byte = 0 + local next_offset = source:gmatch('()\n') + local line = 1 + while line <= index do + byte = next_offset() --[[@as integer]] + line = line + 1 + end + + return byte +end + +---@private +---@param source integer|string +---@param range Range +---@return Range6 +function M.add_bytes(source, range) + if type(range) == 'table' and #range == 6 then + return range --[[@as Range6]] + end + + local start_row, start_col, end_row, end_col = M.unpack4(range) + -- TODO(vigoux): proper byte computation here, and account for EOL ? + local start_byte = get_offset(source, start_row) + start_col + local end_byte = get_offset(source, end_row) + end_col + + return { start_row, start_col, start_byte, end_row, end_col, end_byte } +end + +return M diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua new file mode 100644 index 0000000000..69ddc9b558 --- /dev/null +++ b/runtime/lua/vim/treesitter/dev.lua @@ -0,0 +1,645 @@ +local api = vim.api + +---@class TSDevModule +local M = {} + +---@class TSTreeView +---@field ns integer API namespace +---@field opts table Options table with the following keys: +--- - anon (boolean): If true, display anonymous nodes +--- - lang (boolean): If true, display the language alongside each node +--- |