aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/treesitter
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/treesitter')
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua210
-rw-r--r--runtime/lua/vim/treesitter/language.lua37
-rw-r--r--runtime/lua/vim/treesitter/query.lua335
3 files changed, 582 insertions, 0 deletions
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
new file mode 100644
index 0000000000..decde08019
--- /dev/null
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -0,0 +1,210 @@
+local a = vim.api
+
+-- support reload for quick experimentation
+local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {}
+TSHighlighter.__index = TSHighlighter
+
+TSHighlighter.active = TSHighlighter.active or {}
+
+local ns = a.nvim_create_namespace("treesitter/highlighter")
+
+-- These are conventions defined by nvim-treesitter, though it
+-- needs to be user extensible also.
+TSHighlighter.hl_map = {
+ ["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(parser, query)
+ local self = setmetatable({}, TSHighlighter)
+
+ self.parser = parser
+ parser:register_cbs {
+ on_changedtree = function(...) self:on_changedtree(...) end
+ }
+
+ self:set_query(query)
+ self.edit_count = 0
+ self.redraw_count = 0
+ self.line_count = {}
+ self.root = self.parser:parse():root()
+ a.nvim_buf_set_option(self.buf, "syntax", "")
+
+ -- TODO(bfredl): can has multiple highlighters per buffer????
+ if not TSHighlighter.active[parser.bufnr] then
+ TSHighlighter.active[parser.bufnr] = {}
+ end
+
+ TSHighlighter.active[parser.bufnr][parser.lang] = self
+
+ -- Tricky: if syntax hasn't been enabled, we need to reload color scheme
+ -- 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.
+ if vim.g.syntax_on ~= 1 then
+ vim.api.nvim_command("runtime! syntax/synload.vim")
+ end
+ return self
+end
+
+local function is_highlight_name(capture_name)
+ local firstc = string.sub(capture_name, 1, 1)
+ return firstc ~= string.lower(firstc)
+end
+
+function TSHighlighter: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]
+ 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
+ end
+end
+
+function TSHighlighter:on_changedtree(changes)
+ for _, ch in ipairs(changes or {}) do
+ a.nvim__buf_redraw_range(self.buf, ch[1], ch[3]+1)
+ end
+end
+
+function TSHighlighter:set_query(query)
+ if type(query) == "string" then
+ query = vim.treesitter.parse_query(self.parser.lang, query)
+ end
+
+ self.query = query
+
+ self.hl_cache = setmetatable({}, {
+ __index = function(table, capture)
+ local hl = self:get_hl_from_capture(capture)
+ rawset(table, capture, hl)
+
+ return hl
+ end
+ })
+
+ a.nvim__buf_redraw_range(self.parser.bufnr, 0, a.nvim_buf_line_count(self.parser.bufnr))
+end
+
+local function iter_active_tshl(buf, fn)
+ for _, hl in pairs(TSHighlighter.active[buf] or {}) do
+ fn(hl)
+ end
+end
+
+local function on_line_impl(self, buf, line)
+ if self.root == nil then
+ return -- parser bought the farm already
+ end
+
+ if self.iter == nil then
+ self.iter = self.query:iter_captures(self.root,buf,line,self.botline)
+ end
+ while line >= self.nextrow do
+ local capture, node = self.iter()
+ if capture == nil then
+ break
+ end
+ local start_row, start_col, end_row, end_col = node:range()
+ local hl = self.hl_cache[capture]
+ if hl and end_row >= line then
+ a.nvim_buf_set_extmark(buf, ns, start_row, start_col,
+ { end_line = end_row, end_col = end_col,
+ hl_group = hl,
+ ephemeral = true
+ })
+ end
+ if start_row > line then
+ self.nextrow = start_row
+ end
+ end
+end
+
+function TSHighlighter._on_line(_, _win, buf, line, highlighter)
+ -- on_line is only called when this is non-nil
+ if highlighter then
+ on_line_impl(highlighter, buf, line)
+ else
+ iter_active_tshl(buf, function(self)
+ on_line_impl(self, buf, line)
+ end)
+ end
+end
+
+function TSHighlighter._on_buf(_, buf)
+ iter_active_tshl(buf, function(self)
+ if self then
+ local tree = self.parser:parse()
+ self.root = (tree and tree:root()) or nil
+ end
+ end)
+end
+
+function TSHighlighter._on_win(_, _win, buf, _topline, botline)
+ iter_active_tshl(buf, function(self)
+ if not self then
+ return false
+ end
+
+ self.iter = nil
+ self.nextrow = 0
+ self.botline = botline
+ self.redraw_count = self.redraw_count + 1
+ return true
+ end)
+ return true
+end
+
+a.nvim_set_decoration_provider(ns, {
+ on_buf = TSHighlighter._on_buf;
+ on_win = TSHighlighter._on_win;
+ on_line = TSHighlighter._on_line;
+})
+
+return TSHighlighter
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..2903c5905c
--- /dev/null
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -0,0 +1,335 @@
+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 = {}
+
+-- Filter the runtime query files, the spec is like regular runtime files but in the new `queries`
+-- directory. They resemble ftplugins, that is that you can override queries by adding things in the
+-- `queries` directory, and extend using the `after/queries` directory.
+local function filter_files(file_list)
+ local main = nil
+ local after = {}
+
+ for _, fname in ipairs(file_list) do
+ -- Only get the name of the directory containing the queries directory
+ if vim.fn.fnamemodify(fname, ":p:h:h:h:t") == "after" then
+ table.insert(after, fname)
+ -- The first one is the one with most priority
+ elseif not main then
+ main = fname
+ end
+ end
+
+ return { main, unpack(after) }
+end
+
+local function runtime_query_path(lang, query_name)
+ return string.format('queries/%s/%s.scm', lang, query_name)
+end
+
+local function filtered_runtime_queries(lang, query_name)
+ return filter_files(a.nvim_get_runtime_file(runtime_query_path(lang, query_name), true) or {})
+end
+
+local function get_query_files(lang, query_name, is_included)
+ local lang_files = filtered_runtime_queries(lang, query_name)
+ local query_files = lang_files
+
+ if #query_files == 0 then return {} end
+
+ local base_langs = {}
+
+ -- Now get the base languages by looking at the first line of every file
+ -- The syntax is the folowing :
+ -- ;+ inherits: ({language},)*{language}
+ --
+ -- {language} ::= {lang} | ({lang})
+ local MODELINE_FORMAT = "^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$"
+
+ for _, file in ipairs(query_files) do
+ local modeline = vim.fn.readfile(file, "", 1)
+
+ if #modeline == 1 then
+ local langlist = modeline[1]:match(MODELINE_FORMAT)
+
+ if langlist then
+ for _, incllang in ipairs(vim.split(langlist, ',', true)) do
+ local is_optional = incllang:match("%(.*%)")
+
+ if is_optional then
+ if not is_included then
+ table.insert(base_langs, incllang:sub(2, #incllang - 1))
+ end
+ else
+ table.insert(base_langs, incllang)
+ end
+ end
+ end
+ end
+ end
+
+ for _, base_lang in ipairs(base_langs) do
+ local base_files = get_query_files(base_lang, query_name, true)
+ vim.list_extend(query_files, base_files)
+ end
+
+ return query_files
+end
+
+local function read_query_files(filenames)
+ local contents = {}
+
+ for _,filename in ipairs(filenames) do
+ vim.list_extend(contents, vim.fn.readfile(filename))
+ end
+
+ return table.concat(contents, '\n')
+end
+
+--- Returns the runtime query {query_name} for {lang}.
+--
+-- @param lang The language to use for the query
+-- @param query_name The name of the query (i.e. "highlights")
+--
+-- @return The corresponding query, parsed.
+function M.get_query(lang, query_name)
+ local query_files = get_query_files(lang, query_name)
+ local query_string = read_query_files(query_files)
+
+ if #query_string > 0 then
+ return M.parse_query(lang, query_string)
+ end
+end
+
+--- 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, 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, source)
+ local start_row, start_col, start_byte = node:start()
+ local end_row, end_col, end_byte = node:end_()
+
+ if type(source) == "number" then
+ if start_row ~= end_row then
+ return nil
+ end
+ local line = a.nvim_buf_get_lines(source, start_row, start_row+1, true)[1]
+ return string.sub(line, start_col+1, end_col)
+ elseif type(source) == "string" then
+ return source:sub(start_byte+1, end_byte)
+ end
+end
+
+-- Predicate handler receive the following arguments
+-- (match, pattern, bufnr, predicate)
+local predicate_handlers = {
+ ["eq?"] = function(match, _, source, predicate)
+ local node = match[predicate[2]]
+ local node_text = M.get_node_text(node, source)
+
+ 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]], source)
+ end
+
+ if node_text ~= str or str == nil then
+ return false
+ end
+
+ return true
+ end,
+
+ ["lua-match?"] = function(match, _, source, 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, source), regex)
+ end,
+
+ ["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(vim.fn.escape(pattern, '\\')))
+ rawset(t, pattern, res)
+ return res
+ end
+ })
+
+ return function(match, _, source, 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(source, start_row, start_col, end_col)
+ end
+ end)(),
+
+ ["contains?"] = function(match, _, source, predicate)
+ local node = match[predicate[2]]
+ local node_text = M.get_node_text(node, source)
+
+ for i=3,#predicate do
+ if string.find(node_text, predicate[i], 1, true) then
+ return true
+ end
+ end
+
+ return false
+ end
+}
+
+-- As we provide lua-match? also expose vim-match?
+predicate_handlers["vim-match?"] = predicate_handlers["match?"]
+
+--- 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
+
+--- Returns the list of currently supported predicates
+function M.list_predicates()
+ return vim.tbl_keys(predicate_handlers)
+end
+
+local function xor(x, y)
+ return (x or y) and not (x and y)
+end
+
+function Query:match_preds(match, pattern, source)
+ local preds = self.info.patterns[pattern]
+
+ for _, pred in pairs(preds or {}) 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.
+ local pred_name
+ local is_not
+ if string.sub(pred[1], 1, 4) == "not-" then
+ pred_name = string.sub(pred[1], 5)
+ is_not = true
+ else
+ pred_name = pred[1]
+ is_not = false
+ end
+
+ local handler = predicate_handlers[pred_name]
+
+ if not handler then
+ a.nvim_err_writeln(string.format("No handler for %s", pred[1]))
+ return false
+ end
+
+ local pred_matches = handler(match, pattern, source, pred)
+
+ if not xor(is_not, pred_matches) 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, source, start, stop)
+ if type(source) == "number" and source == 0 then
+ source = 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, source)
+ 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, source, start, stop)
+ if type(source) == "number" and source == 0 then
+ source = 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, source)
+ if not active then
+ return iter() -- tail call: try next match
+ end
+ end
+ return pattern, match
+ end
+ return iter
+end
+
+return M