aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/lsp.lua108
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua28
-rw-r--r--runtime/lua/vim/lsp/protocol.lua14
-rw-r--r--runtime/lua/vim/lsp/util.lua2
-rw-r--r--runtime/lua/vim/shared.lua75
-rw-r--r--runtime/lua/vim/treesitter.lua37
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua88
-rw-r--r--runtime/lua/vim/treesitter/query.lua98
8 files changed, 322 insertions, 128 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 585528dd5a..fad213212a 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -25,6 +25,27 @@ local lsp = {
-- format_rpc_error = lsp_rpc.format_rpc_error;
}
+-- maps request name to the required resolved_capability in the client.
+lsp._request_name_to_capability = {
+ ['textDocument/hover'] = 'hover';
+ ['textDocument/signatureHelp'] = 'signature_help';
+ ['textDocument/definition'] = 'goto_definition';
+ ['textDocument/implementation'] = 'implementation';
+ ['textDocument/declaration'] = 'declaration';
+ ['textDocument/typeDefinition'] = 'type_definition';
+ ['textDocument/documentSymbol'] = 'document_symbol';
+ ['textDocument/workspaceSymbol'] = 'workspace_symbol';
+ ['textDocument/prepareCallHierarchy'] = 'call_hierarchy';
+ ['textDocument/rename'] = 'rename';
+ ['textDocument/codeAction'] = 'code_action';
+ ['workspace/executeCommand'] = 'execute_command';
+ ['textDocument/references'] = 'find_references';
+ ['textDocument/rangeFormatting'] = 'document_range_formatting';
+ ['textDocument/formatting'] = 'document_formatting';
+ ['textDocument/completion'] = 'completion';
+ ['textDocument/documentHighlight'] = 'document_highlight';
+}
+
-- TODO improve handling of scratch buffers with LSP attached.
--@private
@@ -51,6 +72,16 @@ local function resolve_bufnr(bufnr)
end
--@private
+--- callback called by the client when trying to call a method that's not
+--- supported in any of the servers registered for the current buffer.
+--@param method (string) name of the method
+function lsp._unsupported_method(method)
+ local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method)
+ log.warn(msg)
+ return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
+end
+
+--@private
--- Checks whether a given path is a directory.
---
--@param filename (string) path to check
@@ -397,9 +428,8 @@ end
--@param trace: "off" | "messages" | "verbose" | nil passed directly to the language
--- server in the initialize request. Invalid/empty values will default to "off"
---
---@returns Client id. |vim.lsp.get_client_by_id()| Note: client is only
---- available after it has been initialized, which may happen after a small
---- delay (or never if there is an error). Use `on_init` to do any actions once
+--@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
+--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
function lsp.start_client(config)
local cleaned_config = validate_client_config(config)
@@ -576,6 +606,15 @@ function lsp.start_client(config)
-- These are the cleaned up capabilities we use for dynamically deciding
-- when to send certain events to clients.
client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities)
+ client.supports_method = function(method)
+ local required_capability = lsp._request_name_to_capability[method]
+ -- if we don't know about the method, assume that the client supports it.
+ if not required_capability then
+ return true
+ end
+
+ return client.resolved_capabilities[required_capability]
+ end
if config.on_init then
local status, err = pcall(config.on_init, client, result)
if not status then
@@ -599,19 +638,6 @@ function lsp.start_client(config)
end
--@private
- --- Throws error for a method that is not supported by the current LSP
- --- server.
- ---
- --@param method (string) an LSP method name not supported by the LSP server.
- --@returns (error) a 'MethodNotFound' JSON-RPC error response.
- local function unsupported_method(method)
- local msg = "server doesn't support "..method
- local _ = log.warn() and log.warn(msg)
- err_message(msg)
- return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
- end
-
- --@private
--- Sends a request to the server.
---
--- This is a thin wrapper around {client.rpc.request} with some additional
@@ -638,20 +664,6 @@ function lsp.start_client(config)
or error(string.format("not found: %q request callback for client %q.", method, client.name))
end
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr)
- -- TODO keep these checks or just let it go anyway?
- if (not client.resolved_capabilities.hover and method == 'textDocument/hover')
- or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp')
- or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition')
- or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation')
- or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration')
- or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition')
- or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol')
- or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol')
- or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy')
- then
- callback(unsupported_method(method), method, nil, client_id, bufnr)
- return
- end
return rpc.request(method, params, function(err, result)
callback(err, method, result, client_id, bufnr)
end)
@@ -910,14 +922,14 @@ function lsp.buf_is_attached(bufnr, client_id)
return (all_buffer_active_clients[bufnr] or {})[client_id] == true
end
---- Gets an active client by id, or nil if the id is invalid or the
---- client is not yet initialized.
----
+--- Gets a client by id, or nil if the id is invalid.
+--- The returned client may not yet be fully initialized.
+--
--@param client_id client id number
---
--@returns |vim.lsp.client| object, or nil
function lsp.get_client_by_id(client_id)
- return active_clients[client_id]
+ return active_clients[client_id] or uninitialized_clients[client_id]
end
--- Stops a client(s).
@@ -998,16 +1010,32 @@ function lsp.buf_request(bufnr, method, params, callback)
callback = { callback, 'f', true };
}
local client_request_ids = {}
- for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr)
- local request_success, request_id = client.request(method, params, callback, resolved_bufnr)
- -- This could only fail if the client shut down in the time since we looked
- -- it up and we did the request, which should be rare.
- if request_success then
- client_request_ids[client_id] = request_id
+ local method_supported = false
+ for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr)
+ if client.supports_method(method) then
+ method_supported = true
+ local request_success, request_id = client.request(method, params, callback, resolved_bufnr)
+
+ -- This could only fail if the client shut down in the time since we looked
+ -- it up and we did the request, which should be rare.
+ if request_success then
+ client_request_ids[client_id] = request_id
+ end
end
end)
+ -- if no clients support the given method, call the callback with the proper
+ -- error message.
+ if not method_supported then
+ local unsupported_err = lsp._unsupported_method(method)
+ local cb = callback or lsp.callbacks['method']
+ if cb then
+ cb(unsupported_err, method, bufnr)
+ end
+ return
+ end
+
local function _cancel_all_requests()
for client_id, request_id in pairs(client_request_ids) do
local client = active_clients[client_id]
diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua
index 4e7a8333a0..3270d1d2a9 100644
--- a/runtime/lua/vim/lsp/callbacks.lua
+++ b/runtime/lua/vim/lsp/callbacks.lua
@@ -82,18 +82,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result)
return
end
- -- Unloaded buffers should not handle diagnostics.
- -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
- -- This should trigger another publish of the diagnostics.
- --
- -- In particular, this stops a ton of spam when first starting a server for current
- -- unloaded buffers.
- if not api.nvim_buf_is_loaded(bufnr) then
- return
- end
-
- util.buf_clear_diagnostics(bufnr)
-
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
-- The diagnostic's severity. Can be omitted. If omitted it is up to the
-- client to interpret diagnostics as error, warning, info or hint.
@@ -104,7 +92,23 @@ M['textDocument/publishDiagnostics'] = function(_, _, result)
end
end
+ util.buf_clear_diagnostics(bufnr)
+
+ -- Always save the diagnostics, even if the buf is not loaded.
+ -- Language servers may report compile or build errors via diagnostics
+ -- Users should be able to find these, even if they're in files which
+ -- are not loaded.
util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
+
+ -- Unloaded buffers should not handle diagnostics.
+ -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
+ -- This should trigger another publish of the diagnostics.
+ --
+ -- In particular, this stops a ton of spam when first starting a server for current
+ -- unloaded buffers.
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
util.buf_diagnostics_underline(bufnr, result.diagnostics)
util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
util.buf_diagnostics_signs(bufnr, result.diagnostics)
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 4e926381e0..2773f59b45 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -703,6 +703,10 @@ function protocol.make_client_capabilities()
};
hierarchicalDocumentSymbolSupport = true;
};
+ rename = {
+ dynamicRegistration = false;
+ prepareSupport = true;
+ };
};
workspace = {
symbol = {
@@ -914,6 +918,7 @@ function protocol.resolve_capabilities(server_capabilities)
return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
end
end
+ general_properties.completion = server_capabilities.completionProvider ~= nil
general_properties.hover = server_capabilities.hoverProvider or false
general_properties.goto_definition = server_capabilities.definitionProvider or false
general_properties.find_references = server_capabilities.referencesProvider or false
@@ -923,6 +928,15 @@ function protocol.resolve_capabilities(server_capabilities)
general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false
+ general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil
+
+ if server_capabilities.renameProvider == nil then
+ general_properties.rename = false
+ elseif type(server_capabilities.renameProvider) == 'boolean' then
+ general_properties.rename = server_capabilities.renameProvider
+ else
+ general_properties.rename = true
+ end
if server_capabilities.codeActionProvider == nil then
general_properties.code_action = false
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 53f88dea7d..b5f171a985 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -807,7 +807,7 @@ function M.fancy_floating_markdown(contents, opts)
h.start = h.start + i - 1
h.finish = h.finish + i - 1
if h.finish + 1 <= #stripped then
- table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at)))
+ table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
height = height + 1
end
end
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 5c89c63f7b..995c52e8ed 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -477,48 +477,77 @@ end
--- 2. (arg_value, fn, msg)
--- - arg_value: argument value
--- - fn: any function accepting one argument, returns true if and
---- only if the argument is valid
+--- only if the argument is valid. Can optionally return an additional
+--- informative error message as the second returned value.
--- - msg: (optional) error string if validation fails
function vim.validate(opt) end -- luacheck: no unused
-vim.validate = (function()
+
+do
local type_names = {
- t='table', s='string', n='number', b='boolean', f='function', c='callable',
- ['table']='table', ['string']='string', ['number']='number',
- ['boolean']='boolean', ['function']='function', ['callable']='callable',
- ['nil']='nil', ['thread']='thread', ['userdata']='userdata',
+ ['table'] = 'table', t = 'table',
+ ['string'] = 'string', s = 'string',
+ ['number'] = 'number', n = 'number',
+ ['boolean'] = 'boolean', b = 'boolean',
+ ['function'] = 'function', f = 'function',
+ ['callable'] = 'callable', c = 'callable',
+ ['nil'] = 'nil',
+ ['thread'] = 'thread',
+ ['userdata'] = 'userdata',
}
- local function _type_name(t)
- local tname = type_names[t]
- if tname == nil then
- error(string.format('invalid type name: %s', tostring(t)))
- end
- return tname
- end
+
local function _is_type(val, t)
return t == 'callable' and vim.is_callable(val) or type(val) == t
end
- return function(opt)
- assert(type(opt) == 'table', string.format('opt: expected table, got %s', type(opt)))
+ local function is_valid(opt)
+ if type(opt) ~= 'table' then
+ return false, string.format('opt: expected table, got %s', type(opt))
+ end
+
for param_name, spec in pairs(opt) do
- assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec)))
+ if type(spec) ~= 'table' then
+ return false, string.format('opt[%s]: expected table, got %s', param_name, type(spec))
+ end
local val = spec[1] -- Argument value.
local t = spec[2] -- Type name, or callable.
local optional = (true == spec[3])
- if not vim.is_callable(t) then -- Check type name.
- if (not optional or val ~= nil) and not _is_type(val, _type_name(t)) then
- error(string.format("%s: expected %s, got %s", param_name, _type_name(t), type(val)))
+ if type(t) == 'string' then
+ local t_name = type_names[t]
+ if not t_name then
+ return false, string.format('invalid type name: %s', t)
+ end
+
+ if (not optional or val ~= nil) and not _is_type(val, t_name) then
+ return false, string.format("%s: expected %s, got %s", param_name, t_name, type(val))
+ end
+ elseif vim.is_callable(t) then
+ -- Check user-provided validation function.
+ local valid, optional_message = t(val)
+ if not valid then
+ local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val)
+ if optional_message ~= nil then
+ error_message = error_message .. string.format(". Info: %s", optional_message)
+ end
+
+ return false, error_message
end
- elseif not t(val) then -- Check user-provided validation function.
- error(string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val))
+ else
+ return false, string.format("invalid type name: %s", tostring(t))
end
end
- return true
+
+ return true, nil
end
-end)()
+ function vim.validate(opt)
+ local ok, err_msg = is_valid(opt)
+ if not ok then
+ error(debug.traceback(err_msg, 2), 2)
+ end
+ end
+end
--- Returns true if object `f` can be called as a function.
---
--@param f Any object
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 77bbfaa3ad..0de3388356 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -59,6 +59,24 @@ function Parser:_on_bytes(bufnr, changed_tick,
end
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.
+-- it will only be passed one argument, that is a table of the ranges (as node ranges) that
+-- changed.
+function Parser:register_cbs(cbs)
+ if not cbs then return end
+
+ if cbs.on_changedtree then
+ table.insert(self.changedtree_cbs, cbs.on_changedtree)
+ end
+
+ if cbs.on_bytes then
+ table.insert(self.bytes_cbs, cbs.on_bytes)
+ 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.
@@ -68,6 +86,11 @@ function Parser:set_included_ranges(ranges)
self.valid = false
end
+--- Gets the included ranges for the parsers
+function Parser:included_ranges()
+ return self._parser:included_ranges()
+end
+
local M = vim.tbl_extend("error", query, language)
setmetatable(M, {
@@ -127,11 +150,7 @@ end
--
-- @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.
+-- @param buf_attach_cbs See Parser:register_cbs
--
-- @returns The parser
function M.get_parser(bufnr, lang, buf_attach_cbs)
@@ -147,13 +166,7 @@ function M.get_parser(bufnr, lang, buf_attach_cbs)
parsers[id] = M._create_parser(bufnr, lang, id)
end
- if buf_attach_cbs and buf_attach_cbs.on_changedtree then
- table.insert(parsers[id].changedtree_cbs, buf_attach_cbs.on_changedtree)
- end
-
- if buf_attach_cbs and buf_attach_cbs.on_bytes then
- table.insert(parsers[id].bytes_cbs, buf_attach_cbs.on_bytes)
- end
+ parsers[id]:register_cbs(buf_attach_cbs)
return parsers[id]
end
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 0f497fe434..decde08019 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -8,7 +8,7 @@ TSHighlighter.active = TSHighlighter.active or {}
local ns = a.nvim_create_namespace("treesitter/highlighter")
--- These are conventions defined by tree-sitter, though it
+-- These are conventions defined by nvim-treesitter, though it
-- needs to be user extensible also.
TSHighlighter.hl_map = {
["error"] = "Error",
@@ -56,21 +56,14 @@ TSHighlighter.hl_map = {
["include"] = "Include",
}
-function TSHighlighter.new(query, bufnr, ft)
- if bufnr == nil or bufnr == 0 then
- bufnr = a.nvim_get_current_buf()
- end
-
+function TSHighlighter.new(parser, query)
local self = setmetatable({}, TSHighlighter)
- self.parser = vim.treesitter.get_parser(
- bufnr,
- ft,
- {
- on_changedtree = function(...) self:on_changedtree(...) end,
- }
- )
-
- self.buf = self.parser.bufnr
+
+ 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
@@ -79,7 +72,11 @@ function TSHighlighter.new(query, bufnr, ft)
a.nvim_buf_set_option(self.buf, "syntax", "")
-- TODO(bfredl): can has multiple highlighters per buffer????
- TSHighlighter.active[bufnr] = self
+ 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
@@ -119,13 +116,6 @@ 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
@@ -139,12 +129,16 @@ function TSHighlighter:set_query(query)
end
})
- a.nvim__buf_redraw_range(self.buf, 0, a.nvim_buf_line_count(self.buf))
+ a.nvim__buf_redraw_range(self.parser.bufnr, 0, a.nvim_buf_line_count(self.parser.bufnr))
end
-function TSHighlighter._on_line(_, _win, buf, line)
- -- on_line is only called when this is non-nil
- local self = TSHighlighter.active[buf]
+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
@@ -172,24 +166,38 @@ function TSHighlighter._on_line(_, _win, buf, line)
end
end
-function TSHighlighter._on_buf(_, buf)
- local self = TSHighlighter.active[buf]
- if self then
- local tree = self.parser:parse()
- self.root = (tree and tree:root()) or nil
+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)
- local self = TSHighlighter.active[buf]
- if not self then
- return false
- end
+ 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
+ self.iter = nil
+ self.nextrow = 0
+ self.botline = botline
+ self.redraw_count = self.redraw_count + 1
+ return true
+ end)
return true
end
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 494fb59fa7..2903c5905c 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -8,6 +8,104 @@ 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