diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 90 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 6 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 6 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 13 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 9 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 111 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 8 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 30 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 54 |
9 files changed, 257 insertions, 70 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6c5c2c5062..563ffc479e 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -38,11 +38,13 @@ lsp._request_name_to_capability = { ['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'; + ['textDocument/codeLens'] = 'code_lens'; + ['codeLens/resolve'] = 'code_lens_resolve'; ['workspace/executeCommand'] = 'execute_command'; + ['workspace/symbol'] = 'workspace_symbol'; ['textDocument/references'] = 'find_references'; ['textDocument/rangeFormatting'] = 'document_range_formatting'; ['textDocument/formatting'] = 'document_formatting'; @@ -228,6 +230,7 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; flags = { config.flags, "t", true }; + get_language_id = { config.get_language_id, "f", true }; } local cmd, cmd_args = lsp._cmd_parts(config.cmd) @@ -262,8 +265,11 @@ end --@param bufnr (Number) Number of the buffer, or 0 for current --@param client Client object local function text_document_did_open_handler(bufnr, client) - local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, false) - if allow_incremental_sync then + local use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ) + if use_incremental_sync then if not client._cached_buffers then client._cached_buffers = {} end @@ -275,17 +281,31 @@ local function text_document_did_open_handler(bufnr, client) if not vim.api.nvim_buf_is_loaded(bufnr) then return end + local filetype = nvim_buf_get_option(bufnr, 'filetype') + local params = { textDocument = { version = 0; uri = vim.uri_from_bufnr(bufnr); - -- TODO make sure our filetypes are compatible with languageId names. - languageId = nvim_buf_get_option(bufnr, 'filetype'); + languageId = client.config.get_language_id(bufnr, filetype); text = buf_get_full_text(bufnr); } } client.notify('textDocument/didOpen', params) util.buf_versions[bufnr] = params.textDocument.version + + -- Next chance we get, we should re-do the diagnostics + vim.schedule(function() + vim.lsp.handlers["textDocument/publishDiagnostics"]( + nil, + "textDocument/publishDiagnostics", + { + diagnostics = vim.lsp.diagnostic.get(bufnr, client.id), + uri = vim.uri_from_bufnr(bufnr), + }, + client.id + ) + end) end -- FIXME: DOC: Shouldn't need to use a dummy function @@ -407,6 +427,9 @@ end --- --@param name (string, default=client-id) Name in log messages. --- +--@param get_language_id function(bufnr, filetype) -> language ID as string. +--- Defaults to the filetype. +--- --@param offset_encoding (default="utf-16") One of "utf-8", "utf-16", --- or "utf-32" which is the encoding that the LSP server expects. Client does --- not verify this is correct. @@ -445,16 +468,7 @@ end --@param trace: "off" | "messages" | "verbose" | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to "off" --@param flags: A table with flags for the client. The current (experimental) flags are: ---- - allow_incremental_sync (bool, default false): Allow using on_line callbacks for lsp ---- ---- <pre> ---- -- In attach function for the client, you can do: ---- local custom_attach = function(client) ---- if client.config.flags then ---- client.config.flags.allow_incremental_sync = true ---- end ---- end ---- </pre> +--- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits --- --@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be --- fully initialized. Use `on_init` to do any actions once @@ -466,6 +480,11 @@ function lsp.start_client(config) config.flags = config.flags or {} config.settings = config.settings or {} + -- By default, get_language_id just returns the exact filetype it is passed. + -- It is possible to pass in something that will calculate a different filetype, + -- to be sent by the client. + config.get_language_id = config.get_language_id or function(_, filetype) return filetype end + local client_id = next_client_id() local handlers = config.handlers or {} @@ -831,34 +850,30 @@ do end util.buf_versions[bufnr] = changedtick - -- Lazy initialize these because clients may not even need them. - local incremental_changes = once(function(client) + + local incremental_changes = function(client) local lines = nvim_buf_get_lines(bufnr, 0, -1, true) local startline = math.min(firstline + 1, math.min(#client._cached_buffers[bufnr], #lines)) - local endline = math.min(-(#lines - new_lastline), 0) - local incremental_change = vim.lsp.util.compute_diff(client._cached_buffers[bufnr], lines, startline, endline) + local endline = math.min(-(#lines - new_lastline), -1) + local incremental_change = vim.lsp.util.compute_diff( + client._cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or "utf-16") client._cached_buffers[bufnr] = lines return incremental_change - end) + end + local full_changes = once(function() return { text = buf_get_full_text(bufnr); }; end) - local uri = vim.uri_from_bufnr(bufnr) - for_each_buffer_client(bufnr, function(client, _client_id) - local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, false) + local uri = vim.uri_from_bufnr(bufnr) + for_each_buffer_client(bufnr, function(client) + local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, true) local text_document_did_change = client.resolved_capabilities.text_document_did_change local changes if text_document_did_change == protocol.TextDocumentSyncKind.None then return - --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by - -- neovim right now so only send the full content for now. In general, we - -- can assume that servers *will* support both versions anyway, as there - -- is no way to specify the sync capability by the client. - -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both. - --]=] elseif not allow_incremental_sync or text_document_did_change == protocol.TextDocumentSyncKind.Full then changes = full_changes(client) elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then @@ -916,10 +931,25 @@ function lsp.buf_attach_client(bufnr, client_id) all_buffer_active_clients[bufnr] = buffer_client_ids local uri = vim.uri_from_bufnr(bufnr) - nvim_command(string.format("autocmd BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0)", bufnr)) + local buf_did_save_autocommand = [=[ + augroup lsp_c_%d_b_%d_did_save + au! + au BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0) + augroup END + ]=] + vim.api.nvim_exec(string.format(buf_did_save_autocommand, client_id, bufnr, bufnr), false) -- First time, so attach and set up stuff. vim.api.nvim_buf_attach(bufnr, false, { on_lines = text_document_did_change_handler; + on_reload = function() + local params = { textDocument = { uri = uri; } } + for_each_buffer_client(bufnr, function(client, _) + if client.resolved_capabilities.text_document_open_close then + client.notify('textDocument/didClose', params) + end + text_document_did_open_handler(bufnr, client) + end) + end; on_detach = function() local params = { textDocument = { uri = uri; } } for_each_buffer_client(bufnr, function(client, _) diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index a1f24706c0..4e82c46fef 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -270,8 +270,10 @@ local function set_diagnostic_cache(diagnostics, bufnr, client_id) diagnostic.severity = DiagnosticSeverity.Error end -- Account for servers that place diagnostics on terminating newline - local start = diagnostic.range.start - start.line = math.min(start.line, buf_line_count - 1) + if buf_line_count > 0 then + local start = diagnostic.range.start + start.line = math.min(start.line, buf_line_count - 1) + end end diagnostic_cache[bufnr][client_id] = diagnostics diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 0cf80e1443..eacbd90077 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -304,7 +304,7 @@ M['textDocument/typeDefinition'] = location_handler M['textDocument/implementation'] = location_handler --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp -M['textDocument/signatureHelp'] = function(_, method, result) +M['textDocument/signatureHelp'] = function(_, method, result, _, bufnr) -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore if not (result and result.signatures and result.signatures[1]) then @@ -317,9 +317,11 @@ M['textDocument/signatureHelp'] = function(_, method, result) print('No signature help available') return end - util.focusable_preview(method, function() + local syntax = api.nvim_buf_get_option(bufnr, 'syntax') + local p_bufnr, _ = util.focusable_preview(method, function() return lines, util.try_trim_markdown_code_blocks(lines) end) + api.nvim_buf_set_option(p_bufnr, 'syntax', syntax) end --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 388f65c180..7e43eb84de 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -749,6 +749,9 @@ function protocol.make_client_capabilities() }; workspaceFolders = true; applyEdit = true; + workspaceEdit = { + resourceOperations = {'rename', 'create', 'delete',}, + }; }; callHierarchy = { dynamicRegistration = false; @@ -975,6 +978,16 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.rename = true end + if server_capabilities.codeLensProvider == nil then + general_properties.code_lens = false + general_properties.code_lens_resolve = false + elseif type(server_capabilities.codeLensProvider) == 'table' then + general_properties.code_lens = true + general_properties.code_lens_resolve = server_capabilities.codeLensProvider.resolveProvider or false + else + error("The server sent invalid codeLensProvider") + end + if server_capabilities.codeActionProvider == nil then general_properties.code_action = false elseif type(server_capabilities.codeActionProvider) == 'boolean' diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 4e2dd7c8e8..1aa8326514 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -315,8 +315,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) dispatchers = { dispatchers, 't', true }; } - if not (vim.fn.executable(cmd) == 1) then - error(string.format("The given command %q is not executable.", cmd)) + if extra_spawn_params and extra_spawn_params.cwd then + assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory") + elseif not (vim.fn.executable(cmd) == 1) then + error(string.format("The given command %q is not executable.", cmd)) end if dispatchers then local user_dispatchers = dispatchers @@ -370,9 +372,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) } if extra_spawn_params then spawn_params.cwd = extra_spawn_params.cwd - if spawn_params.cwd then - assert(is_dir(spawn_params.cwd), "cwd must be a directory") - end spawn_params.env = env_merge(extra_spawn_params.env) end handle, pid = uv.spawn(cmd, spawn_params, onexit) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 88a5fb468f..ec1131ae1f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -357,8 +357,11 @@ end --- Returns the range table for the difference between old and new lines --@param old_lines table list of lines --@param new_lines table list of lines +--@param start_line_idx int line to begin search for first difference +--@param end_line_idx int line to begin search for last difference +--@param offset_encoding string encoding requested by language server --@returns table start_line_idx and start_col_idx of range -function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx) +function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx, offset_encoding) local start_line, start_char = first_difference(old_lines, new_lines, start_line_idx) local end_line, end_char = last_difference(vim.list_slice(old_lines, start_line, #old_lines), vim.list_slice(new_lines, start_line, #new_lines), start_char, end_line_idx) @@ -373,10 +376,19 @@ function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx) adj_end_char = #old_lines[#old_lines + end_line + 1] + end_char + 1 end + local _ + if offset_encoding == "utf-16" then + _, start_char = vim.str_utfindex(old_lines[start_line], start_char - 1) + _, end_char = vim.str_utfindex(old_lines[#old_lines + end_line + 1], adj_end_char) + else + start_char = start_char - 1 + end_char = adj_end_char + end + local result = { range = { - start = { line = start_line - 1, character = start_char - 1}, - ["end"] = { line = adj_end_line, character = adj_end_char} + start = { line = start_line - 1, character = start_char}, + ["end"] = { line = adj_end_line, character = end_char} }, text = text, rangeLength = length + 1, @@ -518,13 +530,13 @@ end --- precedence is as follows: textEdit.newText > insertText > label --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function get_completion_word(item) - if item.textEdit ~= nil and item.textEdit.newText ~= nil then + if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then return item.textEdit.newText else return M.parse_snippet(item.textEdit.newText) end - elseif item.insertText ~= nil then + elseif item.insertText ~= nil and item.insertText ~= "" then if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then return item.insertText else @@ -612,6 +624,62 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return matches end + +--- Rename old_fname to new_fname +-- +--@param opts (table) +-- overwrite? bool +-- ignoreIfExists? bool +function M.rename(old_fname, new_fname, opts) + opts = opts or {} + local bufnr = vim.fn.bufadd(old_fname) + vim.fn.bufload(bufnr) + local target_exists = vim.loop.fs_stat(new_fname) ~= nil + if target_exists and not opts.overwrite or opts.ignoreIfExists then + vim.notify('Rename target already exists. Skipping rename.') + return + end + local ok, err = os.rename(old_fname, new_fname) + assert(ok, err) + api.nvim_buf_call(bufnr, function() + vim.cmd('saveas! ' .. vim.fn.fnameescape(new_fname)) + end) +end + + +local function create_file(change) + local opts = change.options or {} + -- from spec: Overwrite wins over `ignoreIfExists` + local fname = vim.uri_to_fname(change.uri) + if not opts.ignoreIfExists or opts.overwrite then + local file = io.open(fname, 'w') + file:close() + end + vim.fn.bufadd(fname) +end + + +local function delete_file(change) + local opts = change.options or {} + local fname = vim.uri_to_fname(change.uri) + local stat = vim.loop.fs_stat(fname) + if opts.ignoreIfNotExists and not stat then + return + end + assert(stat, "Cannot delete not existing file or folder " .. fname) + local flags + if stat and stat.type == 'directory' then + flags = opts.recursive and 'rf' or 'd' + else + flags = '' + end + local bufnr = vim.fn.bufadd(fname) + local result = tonumber(vim.fn.delete(fname, flags)) + assert(result == 0, 'Could not delete file: ' .. fname .. ', stat: ' .. vim.inspect(stat)) + api.nvim_buf_delete(bufnr, { force = true }) +end + + --- Applies a `WorkspaceEdit`. --- --@param workspace_edit (table) `WorkspaceEdit` @@ -619,8 +687,17 @@ end function M.apply_workspace_edit(workspace_edit) if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do - if change.kind then - -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile + if change.kind == "rename" then + M.rename( + vim.uri_to_fname(change.oldUri), + vim.uri_to_fname(change.newUri), + change.options + ) + elseif change.kind == 'create' then + create_file(change) + elseif change.kind == 'delete' then + delete_file(change) + elseif change.kind then error(string.format("Unsupported change: %q", vim.inspect(change))) else M.apply_text_document_edit(change, idx) @@ -846,8 +923,8 @@ function M.preview_location(location) end local range = location.targetRange or location.range local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false) - local filetype = api.nvim_buf_get_option(bufnr, 'filetype') - return M.open_floating_preview(contents, filetype) + local syntax = api.nvim_buf_get_option(bufnr, 'syntax') + return M.open_floating_preview(contents, syntax) end --@private @@ -1032,8 +1109,10 @@ function M.fancy_floating_markdown(contents, opts) -- This is because the syntax command doesn't accept a target. local cwin = vim.api.nvim_get_current_win() vim.api.nvim_set_current_win(winnr) + api.nvim_win_set_option(winnr, 'conceallevel', 2) + api.nvim_win_set_option(winnr, 'concealcursor', 'n') - vim.cmd("ownsyntax markdown") + vim.cmd("ownsyntax lsp_markdown") local idx = 1 --@private local function apply_syntax_to_region(ft, start, finish) @@ -1134,7 +1213,7 @@ end --- Shows contents in a floating window. --- --@param contents table of lines to show in window ---@param filetype string of filetype to set for opened buffer +--@param syntax string of syntax to set for opened buffer --@param opts dictionary with optional fields -- - height of floating window -- - width of floating window @@ -1147,10 +1226,10 @@ end -- - pad_bottom number of lines to pad contents at bottom --@returns bufnr,winnr buffer and window number of the newly created floating ---preview window -function M.open_floating_preview(contents, filetype, opts) +function M.open_floating_preview(contents, syntax, opts) validate { contents = { contents, 't' }; - filetype = { filetype, 's', true }; + syntax = { syntax, 's', true }; opts = { opts, 't', true }; } opts = opts or {} @@ -1163,12 +1242,12 @@ function M.open_floating_preview(contents, filetype, opts) local width, height = M._make_floating_popup_size(contents, opts) local floating_bufnr = api.nvim_create_buf(false, true) - if filetype then - api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) + if syntax then + api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax) end local float_option = M.make_floating_popup_options(width, height, opts) local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) - if filetype == 'markdown' then + if syntax == 'markdown' then api.nvim_win_set_option(floating_winnr, 'conceallevel', 2) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 3af66b134c..cac0ab864b 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -10,13 +10,11 @@ local parsers = {} local M = vim.tbl_extend("error", query, language) +M.language_version = vim._ts_get_language_version() + setmetatable(M, { __index = function (t, k) - if k == "TSHighlighter" then - 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 + if k == "highlighter" then t[k] = require'vim.treesitter.highlighter' return t[k] end diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 6b833c6d35..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 @@ -208,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/query.lua b/runtime/lua/vim/treesitter/query.lua index 8b94348994..f40e1d5294 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,7 +83,7 @@ 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, '') @@ -77,6 +100,27 @@ local function new_match_metadata() return setmetatable({}, match_metatable) 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) + + 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}. -- -- @param lang The language to use for the query @@ -84,6 +128,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) |