diff options
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r-- | runtime/lua/vim/treesitter/health.lua | 34 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 39 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 130 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 86 |
5 files changed, 213 insertions, 80 deletions
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua new file mode 100644 index 0000000000..dd0b11a6c7 --- /dev/null +++ b/runtime/lua/vim/treesitter/health.lua @@ -0,0 +1,34 @@ +local M = {} +local ts = vim.treesitter + +function M.list_parsers() + return vim.api.nvim_get_runtime_file('parser/*', true) +end + +function M.check_health() + local report_info = vim.fn['health#report_info'] + local report_ok = vim.fn['health#report_ok'] + local report_error = vim.fn['health#report_error'] + local parsers = M.list_parsers() + + report_info(string.format("Runtime ABI version : %d", ts.language_version)) + + for _, parser in pairs(parsers) do + local parsername = vim.fn.fnamemodify(parser, ":t:r") + + local is_loadable, ret = pcall(ts.language.require_language, parsername) + + if not is_loadable then + report_error(string.format("Impossible to load parser for %s: %s", parsername, ret)) + elseif ret then + local lang = ts.language.inspect_language(parsername) + report_ok(string.format("Loaded parser for %s: ABI version %d", + parsername, lang._abi_version)) + else + report_error(string.format("Unable to load parser for %s", parsername)) + end + end +end + +return M + diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 275e960e28..fe7e1052c9 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -12,6 +12,16 @@ TSHighlighterQuery.__index = TSHighlighterQuery local ns = a.nvim_create_namespace("treesitter/highlighter") +local _default_highlights = {} +local _link_default_highlight_once = function(from, to) + if not _default_highlights[from] then + _default_highlights[from] = true + vim.cmd(string.format("highlight default link %s %s", from, to)) + end + + return from +end + -- These are conventions defined by nvim-treesitter, though it -- needs to be user extensible also. TSHighlighter.hl_map = { @@ -70,9 +80,12 @@ function TSHighlighterQuery.new(lang, query_string) self.hl_cache = setmetatable({}, { __index = function(table, capture) - local hl = self:get_hl_from_capture(capture) - rawset(table, capture, hl) + local hl, is_vim_highlight = self:_get_hl_from_capture(capture) + if not is_vim_highlight then + hl = _link_default_highlight_once(lang .. hl, hl) + end + rawset(table, capture, hl) return hl end }) @@ -90,16 +103,16 @@ function TSHighlighterQuery:query() return self._query end -function TSHighlighterQuery:get_hl_from_capture(capture) +--- Get the hl from capture. +--- Returns a tuple { highlight_name: string, is_builtin: bool } +function TSHighlighterQuery:_get_hl_from_capture(capture) local name = self._query.captures[capture] if is_highlight_name(name) then -- From "Normal.left" only keep "Normal" - return vim.split(name, '.', true)[1] + return vim.split(name, '.', true)[1], true else - -- Default to false to avoid recomputing - local hl = TSHighlighter.hl_map[name] - return hl and a.nvim_get_hl_id_by_name(hl) or 0 + return TSHighlighter.hl_map[name] or name, false end end @@ -113,8 +126,9 @@ function TSHighlighter.new(tree, opts) opts = opts or {} self.tree = tree tree:register_cbs { - on_changedtree = function(...) self:on_changedtree(...) end, - on_bytes = function(...) self:on_bytes(...) end + on_changedtree = function(...) self:on_changedtree(...) end; + on_bytes = function(...) self:on_bytes(...) end; + on_detach = function(...) self:on_detach(...) end; } self.bufnr = tree:source() @@ -176,6 +190,10 @@ function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end +function TSHighlighter:on_detach() + self:destroy() +end + function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1) @@ -203,6 +221,9 @@ local function on_line_impl(self, buf, line) local state = self:get_highlight_state(tstree) local highlighter_query = self:get_query(tree:lang()) + -- Some injected languages may not have highlight queries. + if not highlighter_query:query() then return end + if state.iter == nil then state.iter = highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) end diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index d60cd2d0c7..eed28e0e41 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -2,12 +2,12 @@ local a = vim.api local M = {} ---- Asserts that the provided language is installed, and optionnaly provide a path for the parser +--- Asserts that the provided language is installed, and optionally provide a path for the parser -- -- Parsers are searched in the `parser` runtime directory. -- -- @param lang The language the parser should parse --- @param path Optionnal path the parser is located at +-- @param path Optional path the parser is located at -- @param silent Don't throw an error if language not found function M.require_language(lang, path, silent) if vim._ts_has_language(lang) then diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index c864fe5878..2f5aeb0710 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -6,35 +6,41 @@ local LanguageTree = {} LanguageTree.__index = LanguageTree -- Represents a single treesitter parser for a language. --- The language can contain child languages with in it's range, +-- The language can contain child languages with in its range, -- hence the tree. -- -- @param source Can be a bufnr or a string of text to parse -- @param lang The language this tree represents -- @param opts Options table --- @param opts.queries A table of language to injection query strings --- This is useful for overridding the built in runtime file --- searching for the injection language query per language. +-- @param opts.injections A table of language to injection query strings. +-- This is useful for overriding the built-in runtime file +-- searching for the injection language query per language. function LanguageTree.new(source, lang, opts) language.require_language(lang) opts = opts or {} - local custom_queries = opts.queries or {} + if opts.queries then + a.nvim_err_writeln("'queries' is no longer supported. Use 'injections' now") + opts.injections = opts.queries + end + + local injections = opts.injections or {} local self = setmetatable({ - _source=source, - _lang=lang, + _source = source, + _lang = lang, _children = {}, _regions = {}, _trees = {}, _opts = opts, - _injection_query = custom_queries[lang] - and query.parse_query(lang, custom_queries[lang]) + _injection_query = injections[lang] + and query.parse_query(lang, injections[lang]) or query.get_query(lang, "injections"), _valid = false, _parser = vim._create_ts_parser(lang), _callbacks = { changedtree = {}, bytes = {}, + detach = {}, child_added = {}, child_removed = {} }, @@ -44,12 +50,17 @@ function LanguageTree.new(source, lang, opts) return self end --- Invalidates this parser and all it's children -function LanguageTree:invalidate() +-- Invalidates this parser and all its children +function LanguageTree:invalidate(reload) self._valid = false + -- buffer was reloaded, reparse all trees + if reload then + self._trees = {} + end + for _, child in ipairs(self._children) do - child:invalidate() + child:invalidate(reload) end end @@ -97,7 +108,7 @@ function LanguageTree:parse() self._trees = {} -- If there are no ranges, set to an empty list - -- so the included ranges in the parser ar cleared. + -- so the included ranges in the parser are cleared. if self._regions and #self._regions > 0 then for i, ranges in ipairs(self._regions) do local old_tree = old_trees[i] @@ -214,7 +225,7 @@ function LanguageTree:remove_child(lang) end end --- Destroys this language tree and all it's children. +-- Destroys this language tree and all its children. -- Any cleanup logic should be performed here. -- Note, this DOES NOT remove this tree from a parent. -- `remove_child` must be called on the parent to remove it. @@ -241,19 +252,19 @@ end -- -- Note, this call invalidates the tree and requires it to be parsed again. -- --- @param regions A list of regions this tree should manange and parse. +-- @param regions A list of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) - -- Transform the tables from 4 element long to 6 element long (with byte offset) - for _, region in ipairs(regions) do - for i, range in ipairs(region) do - if type(range) == "table" and #range == 4 then - -- TODO(vigoux): I don't think string parsers are useful for now - if type(self._source) == "number" then + -- TODO(vigoux): I don't think string parsers are useful for now + if type(self._source) == "number" then + -- Transform the tables from 4 element long to 6 element long (with byte offset) + for _, region in ipairs(regions) do + for i, range in ipairs(region) do + if type(range) == "table" and #range == 4 then local start_row, start_col, end_row, end_col = unpack(range) -- Easy case, this is a buffer parser -- TODO(vigoux): proper byte computation here, and account for EOL ? - local start_byte = a.nvim_buf_get_offset(self.bufnr, start_row) + start_col - local end_byte = a.nvim_buf_get_offset(self.bufnr, end_row) + end_col + local start_byte = a.nvim_buf_get_offset(self._source, start_row) + start_col + local end_byte = a.nvim_buf_get_offset(self._source, end_row) + end_col region[i] = { start_row, start_col, start_byte, end_row, end_col, end_byte } end @@ -291,33 +302,50 @@ function LanguageTree:_get_injections() for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line+1) do local lang = nil - local injection_node = nil - local combined = false + local ranges = {} + local combined = metadata.combined + + -- Directives can configure how injections are captured as well as actual node captures. + -- This allows more advanced processing for determining ranges and language resolution. + if metadata.content then + local content = metadata.content + + -- Allow for captured nodes to be used + if type(content) == "number" then + content = {match[content]} + end + + if content then + vim.list_extend(ranges, content) + end + end + + if metadata.language then + lang = metadata.language + end -- You can specify the content and language together -- using a tag with the language, for example -- @javascript for id, node in pairs(match) do - local data = metadata[id] local name = self._injection_query.captures[id] - local offset_range = data and data.offset -- Lang should override any other language tag - if name == "language" then + if name == "language" and not lang then lang = query.get_node_text(node, self._source) elseif name == "combined" then combined = true - elseif name == "content" then - injection_node = offset_range or node + elseif name == "content" and #ranges == 0 then + table.insert(ranges, node) -- Ignore any tags that start with "_" -- Allows for other tags to be used in matches elseif string.sub(name, 1, 1) ~= "_" then - if lang == nil then + if not lang then lang = name end - if not injection_node then - injection_node = offset_range or node + if #ranges == 0 then + table.insert(ranges, node) end end end @@ -331,21 +359,21 @@ function LanguageTree:_get_injections() injections[tree_index][lang] = {} end - -- Key by pattern so we can either combine each node to parse in the same - -- context or treat each node independently. + -- Key this by pattern. If combined is set to true all captures of this pattern + -- will be parsed by treesitter as the same "source". + -- If combined is false, each "region" will be parsed as a single source. if not injections[tree_index][lang][pattern] then - injections[tree_index][lang][pattern] = { combined = combined, nodes = {} } + injections[tree_index][lang][pattern] = { combined = combined, regions = {} } end - table.insert(injections[tree_index][lang][pattern].nodes, injection_node) + table.insert(injections[tree_index][lang][pattern].regions, ranges) end end local result = {} -- Generate a map by lang of node lists. - -- Each list is a set of ranges that should be parsed - -- together. + -- Each list is a set of ranges that should be parsed together. for _, lang_map in ipairs(injections) do for lang, patterns in pairs(lang_map) do if not result[lang] then @@ -354,10 +382,10 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then - table.insert(result[lang], entry.nodes) + table.insert(result[lang], vim.tbl_flatten(entry.regions)) else - for _, node in ipairs(entry.nodes) do - table.insert(result[lang], {node}) + for _, ranges in ipairs(entry.regions) do + table.insert(result[lang], ranges) end end end @@ -397,14 +425,24 @@ function LanguageTree:_on_bytes(bufnr, changed_tick, new_row, new_col, new_byte) end +function LanguageTree:_on_reload() + self:invalidate(true) +end + + +function LanguageTree:_on_detach(...) + self:invalidate(true) + self:_do_callback('detach', ...) +end + --- Registers callbacks for the parser -- @param cbs An `nvim_buf_attach`-like table argument with the following keys : -- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. --- `on_changedtree` : a callback that will be called everytime the tree has syntactical changes. +-- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. -- it will only be passed one argument, that is a table of the ranges (as node ranges) that -- changed. -- `on_child_added` : emitted when a child is added to the tree. --- `on_child_removed` : emitted when a child is remvoed from the tree. +-- `on_child_removed` : emitted when a child is removed from the tree. function LanguageTree:register_cbs(cbs) if not cbs then return end @@ -416,6 +454,10 @@ function LanguageTree:register_cbs(cbs) table.insert(self._callbacks.bytes, cbs.on_bytes) end + if cbs.on_detach then + table.insert(self._callbacks.detach, cbs.on_detach) + end + if cbs.on_child_added then table.insert(self._callbacks.child_added, cbs.on_child_added) end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index e49f54681d..ed5146be44 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -8,10 +8,33 @@ Query.__index = Query local M = {} +local function dedupe_files(files) + local result = {} + local seen = {} + + for _, path in ipairs(files) do + if not seen[path] then + table.insert(result, path) + seen[path] = true + end + end + + return result +end + +local function safe_read(filename, read_quantifier) + local file, err = io.open(filename, 'r') + if not file then + error(err) + end + local content = file:read(read_quantifier) + io.close(file) + return content +end function M.get_query_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) - local lang_files = a.nvim_get_runtime_file(query_path, true) + local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) if #lang_files == 0 then return {} end @@ -25,7 +48,7 @@ function M.get_query_files(lang, query_name, is_included) local MODELINE_FORMAT = "^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$" for _, file in ipairs(lang_files) do - local modeline = io.open(file, 'r'):read('*l') + local modeline = safe_read(file, '*l') if modeline then local langlist = modeline:match(MODELINE_FORMAT) @@ -60,21 +83,31 @@ local function read_query_files(filenames) local contents = {} for _,filename in ipairs(filenames) do - table.insert(contents, io.open(filename, 'r'):read('*a')) + table.insert(contents, safe_read(filename, '*a')) end return table.concat(contents, '') end -local match_metatable = { - __index = function(tbl, key) - rawset(tbl, key, {}) - return tbl[key] - end -} +--- The explicitly set queries from |vim.treesitter.query.set_query()| +local explicit_queries = setmetatable({}, { + __index = function(t, k) + local lang_queries = {} + rawset(t, k, lang_queries) -local function new_match_metadata() - return setmetatable({}, match_metatable) + return lang_queries + end, +}) + +--- Sets the runtime query {query_name} for {lang} +--- +--- This allows users to override any runtime files and/or configuration +--- set by plugins. +---@param lang string: The language to use for the query +---@param query_name string: The name of the query (i.e. "highlights") +---@param text string: The query text (unparsed). +function M.set_query(lang, query_name, text) + explicit_queries[lang][query_name] = M.parse_query(lang, text) end --- Returns the runtime query {query_name} for {lang}. @@ -84,6 +117,10 @@ end -- -- @return The corresponding query, parsed. function M.get_query(lang, query_name) + if explicit_queries[lang][query_name] then + return explicit_queries[lang][query_name] + end + local query_files = M.get_query_files(lang, query_name) local query_string = read_query_files(query_files) @@ -111,7 +148,7 @@ end --- Gets the text corresponding to a given node -- @param node the node --- @param bufnr the buffer from which the node in extracted. +-- @param bufnr the buffer from which the node is extracted. function M.get_node_text(node, source) local start_row, start_col, start_byte = node:start() local end_row, end_col, end_byte = node:end_() @@ -211,14 +248,14 @@ predicate_handlers["vim-match?"] = predicate_handlers["match?"] -- Directives store metadata or perform side effects against a match. -- Directives should always end with a `!`. -- Directive handler receive the following arguments --- (match, pattern, bufnr, predicate) +-- (match, pattern, bufnr, predicate, metadata) local directive_handlers = { ["set!"] = function(_, _, _, pred, metadata) if #pred == 4 then - -- (set! @capture "key" "value") + -- (#set! @capture "key" "value") metadata[pred[2]][pred[3]] = pred[4] else - -- (set! "key" "value") + -- (#set! "key" "value") metadata[pred[2]] = pred[3] end end, @@ -231,7 +268,6 @@ local directive_handlers = { local start_col_offset = pred[4] or 0 local end_row_offset = pred[5] or 0 local end_col_offset = pred[6] or 0 - local key = pred[7] or "offset" range[1] = range[1] + start_row_offset range[2] = range[2] + start_col_offset @@ -240,12 +276,12 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if range[1] < range[3] or (range[1] == range[3] and range[2] <= range[4]) then - metadata[pred[2]][key] = range + metadata.content = {range} end end } ---- Adds a new predicates to be used in queries +--- Adds a new predicate to be used in queries -- -- @param name the name of the predicate, without leading # -- @param handler the handler function to be used @@ -355,10 +391,10 @@ end --- Iterates of the captures of self on a given range. -- --- @param node The node under witch the search will occur +-- @param node The node under which the search will occur -- @param buffer The source buffer to search -- @param start The starting line of the search --- @param stop The stoping line of the search (end-exclusive) +-- @param stop The stopping line of the search (end-exclusive) -- -- @returns The matching capture id -- @returns The captured node @@ -372,7 +408,7 @@ function Query:iter_captures(node, source, start, stop) local raw_iter = node:_rawquery(self.query, true, start, stop) local function iter() local capture, captured_node, match = raw_iter() - local metadata = new_match_metadata() + local metadata = {} if match ~= nil then local active = self:match_preds(match, match.pattern, source) @@ -388,12 +424,12 @@ function Query:iter_captures(node, source, start, stop) return iter end ---- Iterates of the matches of self on a given range. +--- Iterates the matches of self on a given range. -- --- @param node The node under witch the search will occur +-- @param node The node under which the search will occur -- @param buffer The source buffer to search -- @param start The starting line of the search --- @param stop The stoping line of the search (end-exclusive) +-- @param stop The stopping line of the search (end-exclusive) -- -- @returns The matching pattern id -- @returns The matching match @@ -407,7 +443,7 @@ function Query:iter_matches(node, source, start, stop) local raw_iter = node:_rawquery(self.query, false, start, stop) local function iter() local pattern, match = raw_iter() - local metadata = new_match_metadata() + local metadata = {} if match ~= nil then local active = self:match_preds(match, pattern, source) |