diff options
Diffstat (limited to 'runtime/lua/vim')
| -rw-r--r-- | runtime/lua/vim/lsp.lua | 108 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/callbacks.lua | 28 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 14 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/util.lua | 2 | ||||
| -rw-r--r-- | runtime/lua/vim/shared.lua | 75 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter.lua | 37 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 88 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 98 | 
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 | 
