diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 206 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua (renamed from runtime/lua/vim/tshighlighter.lua) | 64 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 37 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 210 |
4 files changed, 345 insertions, 172 deletions
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 927456708c..550dee1e3f 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,4 +1,6 @@ local a = vim.api +local query = require'vim.treesitter.query' +local language = require'vim.treesitter.language' -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with @@ -8,6 +10,12 @@ local parsers = {} local Parser = {} Parser.__index = Parser +--- Parses the buffer if needed and returns a tree. +-- +-- Calling this will call the on_changedtree callbacks if the tree has changed. +-- +-- @returns An up to date tree +-- @returns If the tree changed with this call, the changed ranges function Parser:parse() if self.valid then return self.tree @@ -38,48 +46,39 @@ function Parser:_on_lines(bufnr, changed_tick, start_row, old_stop_row, stop_row end end +--- Sets the included ranges for the current parser +-- +-- @param ranges A table of nodes that will be used as the ranges the parser should include. function Parser:set_included_ranges(ranges) self._parser:set_included_ranges(ranges) -- The buffer will need to be parsed again later self.valid = false end -local M = { - parse_query = vim._ts_parse_query, -} +local M = vim.tbl_extend("error", query, language) setmetatable(M, { __index = function (t, k) if k == "TSHighlighter" then - t[k] = require'vim.tshighlighter' + a.nvim_err_writeln("vim.TSHighlighter is deprecated, please use vim.treesitter.highlighter") + t[k] = require'vim.treesitter.highlighter' + return t[k] + elseif k == "highlighter" then + t[k] = require'vim.treesitter.highlighter' return t[k] end end }) -function M.require_language(lang, path) - if vim._ts_has_language(lang) then - return true - end - if path == nil then - local fname = 'parser/' .. lang .. '.*' - local paths = a.nvim_get_runtime_file(fname, false) - if #paths == 0 then - -- TODO(bfredl): help tag? - error("no parser for '"..lang.."' language") - end - path = paths[1] - end - vim._ts_add_language(path, lang) -end - -function M.inspect_language(lang) - M.require_language(lang) - return vim._ts_inspect_language(lang) -end - -function M.create_parser(bufnr, lang, id) - M.require_language(lang) +--- Creates a new parser. +-- +-- It is not recommended to use this, use vim.treesitter.get_parser() instead. +-- +-- @param bufnr The buffer the parser will be tied to +-- @param lang The language of the parser. +-- @param id The id the parser will have +function M._create_parser(bufnr, lang, id) + language.require_language(lang) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end @@ -91,8 +90,8 @@ function M.create_parser(bufnr, lang, id) self.changedtree_cbs = {} self.lines_cbs = {} self:parse() - -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is - -- using it. + -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is + -- using it. local function lines_cb(_, ...) return self:_on_lines(...) end @@ -108,17 +107,31 @@ function M.create_parser(bufnr, lang, id) return self end -function M.get_parser(bufnr, ft, buf_attach_cbs) +--- Gets the parser for this bufnr / ft combination. +-- +-- If needed this will create the parser. +-- Unconditionnally attach the provided callback +-- +-- @param bufnr The buffer the parser should be tied to +-- @param ft The filetype of this parser +-- @param buf_attach_cbs An `nvim_buf_attach`-like table argument with the following keys : +-- `on_lines` : 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. +-- it will only be passed one argument, that is a table of the ranges (as node ranges) that +-- changed. +-- +-- @returns The parser +function M.get_parser(bufnr, lang, buf_attach_cbs) if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end - if ft == nil then - ft = a.nvim_buf_get_option(bufnr, "filetype") + if lang == nil then + lang = a.nvim_buf_get_option(bufnr, "filetype") end - local id = tostring(bufnr)..'_'..ft + local id = tostring(bufnr)..'_'..lang if parsers[id] == nil then - parsers[id] = M.create_parser(bufnr, ft, id) + parsers[id] = M._create_parser(bufnr, lang, id) end if buf_attach_cbs and buf_attach_cbs.on_changedtree then @@ -132,129 +145,4 @@ function M.get_parser(bufnr, ft, buf_attach_cbs) return parsers[id] end --- query: pattern matching on trees --- predicate matching is implemented in lua -local Query = {} -Query.__index = Query - -local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} -local function check_magic(str) - if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then - return str - end - return '\\v'..str -end - -function M.parse_query(lang, query) - M.require_language(lang) - local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\')) - self.info = self.query:inspect() - self.captures = self.info.captures - self.regexes = {} - for id,preds in pairs(self.info.patterns) do - local regexes = {} - for i, pred in ipairs(preds) do - if (pred[1] == "match?" and type(pred[2]) == "number" - and type(pred[3]) == "string") then - regexes[i] = vim.regex(check_magic(pred[3])) - end - end - if next(regexes) then - self.regexes[id] = regexes - end - end - return self -end - -local function get_node_text(node, bufnr) - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return nil - end - local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] - return string.sub(line, start_col+1, end_col) -end - -function Query:match_preds(match, pattern, bufnr) - local preds = self.info.patterns[pattern] - if not preds then - return true - end - local regexes = self.regexes[pattern] - for i, pred in pairs(preds) do - -- Here we only want to return if a predicate DOES NOT match, and - -- continue on the other case. This way unknown predicates will not be considered, - -- which allows some testing and easier user extensibility (#12173). - -- Also, tree-sitter strips the leading # from predicates for us. - if pred[1] == "eq?" then - local node = match[pred[2]] - local node_text = get_node_text(node, bufnr) - - local str - if type(pred[3]) == "string" then - -- (#eq? @aa "foo") - str = pred[3] - else - -- (#eq? @aa @bb) - str = get_node_text(match[pred[3]], bufnr) - end - - if node_text ~= str or str == nil then - return false - end - elseif pred[1] == "match?" then - if not regexes or not regexes[i] then - return false - end - local node = match[pred[2]] - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return false - end - if not regexes[i]:match_line(bufnr, start_row, start_col, end_col) then - return false - end - end - end - return true -end - -function Query:iter_captures(node, bufnr, start, stop) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - local raw_iter = node:_rawquery(self.query,true,start,stop) - local function iter() - local capture, captured_node, match = raw_iter() - if match ~= nil then - local active = self:match_preds(match, match.pattern, bufnr) - match.active = active - if not active then - return iter() -- tail call: try next match - end - end - return capture, captured_node - end - return iter -end - -function Query:iter_matches(node, bufnr, start, stop) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - local raw_iter = node:_rawquery(self.query,false,start,stop) - local function iter() - local pattern, match = raw_iter() - if match ~= nil then - local active = self:match_preds(match, pattern, bufnr) - if not active then - return iter() -- tail call: try next match - end - end - return pattern, match - end - return iter -end - return M diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 6465751ae8..681d2c6324 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -7,20 +7,50 @@ local ts_hs_ns = a.nvim_create_namespace("treesitter_hl") -- These are conventions defined by tree-sitter, though it -- needs to be user extensible also. --- TODO(bfredl): this is very much incomplete, we will need to --- go through a few tree-sitter provided queries and decide --- on translations that makes the most sense. TSHighlighter.hl_map = { - keyword="Keyword", - string="String", - type="Type", - comment="Comment", - constant="Constant", - operator="Operator", - number="Number", - label="Label", - ["function"]="Function", - ["function.special"]="Function", + ["error"] = "Error", + +-- Miscs + ["comment"] = "Comment", + ["punctuation.delimiter"] = "Delimiter", + ["punctuation.bracket"] = "Delimiter", + ["punctuation.special"] = "Delimiter", + +-- Constants + ["constant"] = "Constant", + ["constant.builtin"] = "Special", + ["constant.macro"] = "Define", + ["string"] = "String", + ["string.regex"] = "String", + ["string.escape"] = "SpecialChar", + ["character"] = "Character", + ["number"] = "Number", + ["boolean"] = "Boolean", + ["float"] = "Float", + +-- Functions + ["function"] = "Function", + ["function.special"] = "Function", + ["function.builtin"] = "Special", + ["function.macro"] = "Macro", + ["parameter"] = "Identifier", + ["method"] = "Function", + ["field"] = "Identifier", + ["property"] = "Identifier", + ["constructor"] = "Special", + +-- Keywords + ["conditional"] = "Conditional", + ["repeat"] = "Repeat", + ["label"] = "Label", + ["operator"] = "Operator", + ["keyword"] = "Keyword", + ["exception"] = "Exception", + + ["type"] = "Type", + ["type.builtin"] = "Type", + ["structure"] = "Structure", + ["include"] = "Include", } function TSHighlighter.new(query, bufnr, ft) @@ -75,7 +105,15 @@ end function TSHighlighter:set_query(query) if type(query) == "string" then query = vim.treesitter.parse_query(self.parser.lang, query) + elseif query == nil then + query = vim.treesitter.get_query(self.parser.lang, 'highlights') + + if query == nil then + a.nvim_err_writeln("No highlights.scm query found for " .. self.parser.lang) + query = vim.treesitter.parse_query(self.parser.lang, "") + end end + self.query = query self.hl_cache = setmetatable({}, { diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua new file mode 100644 index 0000000000..a7e36a0b89 --- /dev/null +++ b/runtime/lua/vim/treesitter/language.lua @@ -0,0 +1,37 @@ +local a = vim.api + +local M = {} + +--- Asserts that the provided language is installed, and optionnaly 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 +function M.require_language(lang, path) + if vim._ts_has_language(lang) then + return true + end + if path == nil then + local fname = 'parser/' .. lang .. '.*' + local paths = a.nvim_get_runtime_file(fname, false) + if #paths == 0 then + -- TODO(bfredl): help tag? + error("no parser for '"..lang.."' language, see :help treesitter-parsers") + end + path = paths[1] + end + vim._ts_add_language(path, lang) +end + +--- Inspects the provided language. +-- +-- Inspecting provides some useful informations on the language like node names, ... +-- +-- @param lang The language. +function M.inspect_language(lang) + M.require_language(lang) + return vim._ts_inspect_language(lang) +end + +return M diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua new file mode 100644 index 0000000000..17f61b24f1 --- /dev/null +++ b/runtime/lua/vim/treesitter/query.lua @@ -0,0 +1,210 @@ +local a = vim.api +local language = require'vim.treesitter.language' + +-- query: pattern matching on trees +-- predicate matching is implemented in lua +local Query = {} +Query.__index = Query + +local M = {} + +--- Parses a query. +-- +-- @param language The language +-- @param query A string containing the query (s-expr syntax) +-- +-- @returns The query +function M.parse_query(lang, query) + language.require_language(lang) + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\')) + self.info = self.query:inspect() + self.captures = self.info.captures + return self +end + +-- TODO(vigoux): support multiline nodes too + +--- Gets the text corresponding to a given node +-- @param node the node +-- @param bufnr the buffer from which the node in extracted. +function M.get_node_text(node, bufnr) + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return nil + end + local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] + return string.sub(line, start_col+1, end_col) +end + +-- Predicate handler receive the following arguments +-- (match, pattern, bufnr, predicate) +local predicate_handlers = { + ["eq?"] = function(match, _, bufnr, predicate) + local node = match[predicate[2]] + local node_text = M.get_node_text(node, bufnr) + + local str + if type(predicate[3]) == "string" then + -- (#eq? @aa "foo") + str = predicate[3] + else + -- (#eq? @aa @bb) + str = M.get_node_text(match[predicate[3]], bufnr) + end + + if node_text ~= str or str == nil then + return false + end + + return true + end, + + ["match?"] = function(match, _, bufnr, predicate) + local node = match[predicate[2]] + local regex = predicate[3] + local start_row, _, end_row, _ = node:range() + if start_row ~= end_row then + return false + end + + return string.find(M.get_node_text(node, bufnr), regex) + end, + + ["vim-match?"] = (function() + local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} + local function check_magic(str) + if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then + return str + end + return '\\v'..str + end + + local compiled_vim_regexes = setmetatable({}, { + __index = function(t, pattern) + local res = vim.regex(check_magic(pattern)) + rawset(t, pattern, res) + return res + end + }) + + return function(match, _, bufnr, pred) + local node = match[pred[2]] + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return false + end + + local regex = compiled_vim_regexes[pred[3]] + return regex:match_line(bufnr, start_row, start_col, end_col) + end + end)(), + + ["contains?"] = function(match, _, bufnr, predicate) + local node = match[predicate[2]] + local node_text = M.get_node_text(node, bufnr) + + for i=3,#predicate do + if string.find(node_text, predicate[i], 1, true) then + return true + end + end + + return false + end +} + +--- Adds a new predicates to be used in queries +-- +-- @param name the name of the predicate, without leading # +-- @param handler the handler function to be used +-- signature will be (match, pattern, bufnr, predicate) +function M.add_predicate(name, handler, force) + if predicate_handlers[name] and not force then + a.nvim_err_writeln(string.format("Overriding %s", name)) + end + + predicate_handlers[name] = handler +end + +function Query:match_preds(match, pattern, bufnr) + local preds = self.info.patterns[pattern] + if not preds then + return true + end + for _, pred in pairs(preds) do + -- Here we only want to return if a predicate DOES NOT match, and + -- continue on the other case. This way unknown predicates will not be considered, + -- which allows some testing and easier user extensibility (#12173). + -- Also, tree-sitter strips the leading # from predicates for us. + if string.sub(pred[1], 1, 4) == "not-" then + local pred_name = string.sub(pred[1], 5) + if predicate_handlers[pred_name] and + predicate_handlers[pred_name](match, pattern, bufnr, pred) then + return false + end + + elseif predicate_handlers[pred[1]] and + not predicate_handlers[pred[1]](match, pattern, bufnr, pred) then + return false + end + end + return true +end + +--- Iterates of the captures of self on a given range. +-- +-- @param node The node under witch 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) +-- +-- @returns The matching capture id +-- @returns The captured node +function Query:iter_captures(node, bufnr, start, stop) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query, true, start, stop) + local function iter() + local capture, captured_node, match = raw_iter() + if match ~= nil then + local active = self:match_preds(match, match.pattern, bufnr) + match.active = active + if not active then + return iter() -- tail call: try next match + end + end + return capture, captured_node + end + return iter +end + +--- Iterates of the matches of self on a given range. +-- +-- @param node The node under witch 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) +-- +-- @returns The matching pattern id +-- @returns The matching match +function Query:iter_matches(node, bufnr, start, stop) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query, false, start, stop) + local function iter() + local pattern, match = raw_iter() + if match ~= nil then + local active = self:match_preds(match, pattern, bufnr) + if not active then + return iter() -- tail call: try next match + end + end + return pattern, match + end + return iter +end + +return M |