diff options
Diffstat (limited to 'runtime/lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 56 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 33 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/callbacks.lua | 47 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 26 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 51 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 157 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 65 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 77 | ||||
-rw-r--r-- | runtime/lua/vim/tshighlighter.lua | 28 |
10 files changed, 405 insertions, 136 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index cfa208f21c..afff4d9900 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -101,7 +101,7 @@ local function for_each_buffer_client(bufnr, callback) for client_id in pairs(client_ids) do local client = active_clients[client_id] if client then - callback(client, client_id) + callback(client, client_id, bufnr) end end end @@ -121,13 +121,9 @@ local function validate_encoding(encoding) or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) end -local function validate_command(input) +function lsp._cmd_parts(input) local cmd, cmd_args - if type(input) == 'string' then - -- Use a shell to execute the command if it is a string. - cmd = vim.api.nvim_get_option('shell') - cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input} - elseif vim.tbl_islist(input) then + if vim.tbl_islist(input) then cmd = input[1] cmd_args = {} -- Don't mutate our input. @@ -138,7 +134,7 @@ local function validate_command(input) end end else - error("cmd type must be string or list.") + error("cmd type must be list.") end return cmd, cmd_args end @@ -158,7 +154,7 @@ local function validate_client_config(config) callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; - cmd_env = { config.cmd_env, "f", true }; + cmd_env = { config.cmd_env, "t", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; @@ -166,7 +162,7 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } - local cmd, cmd_args = validate_command(config.cmd) + local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 if config.offset_encoding then offset_encoding = validate_encoding(config.offset_encoding) @@ -452,7 +448,7 @@ function lsp.start_client(config) -- The rootPath of the workspace. Is null if no folder is open. -- -- @deprecated in favour of rootUri. - rootPath = nil; + rootPath = config.root_dir; -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); @@ -524,23 +520,24 @@ function lsp.start_client(config) end --- Checks capabilities before rpc.request-ing. - function client.request(method, params, callback) + function client.request(method, params, callback, bufnr) if not callback then callback = resolve_callback(method) or error("not found: request callback for client "..client.name) end - local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) + 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.document_symbol and method == 'textDocument/documentSymbol') then - callback(unsupported_method(method), method, nil, client_id) + 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) + callback(err, method, result, client_id, bufnr) end) end @@ -617,6 +614,8 @@ do if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return end + + util.buf_versions[bufnr] = changedtick -- Lazy initialize these because clients may not even need them. local incremental_changes = once(function(client) local size_index = encoding_index[client.offset_encoding] @@ -840,8 +839,8 @@ 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) - local request_success, request_id = client.request(method, params, callback) + 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. @@ -897,21 +896,22 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) return request_results end ---- Sends a notification to all servers attached to the buffer. ---- ---@param bufnr (optional, number) Buffer handle, or 0 for current ---@param method (string) LSP method name ---@param params (string) Parameters to send to the server ---- ---@returns nil +--- Send a notification to a server +-- @param bufnr [number] (optional): The number of the buffer +-- @param method [string]: Name of the request method +-- @param params [string]: Arguments to send to the server +-- +-- @returns true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) validate { bufnr = { bufnr, 'n', true }; method = { method, 's' }; } - for_each_buffer_client(bufnr, function(client, _client_id) - client.rpc.notify(method, params) + local resp = false + for_each_buffer_client(bufnr, function(client, _client_id, _resolved_bufnr) + if client.rpc.notify(method, params) then resp = true end end) + return resp end --- Implements 'omnifunc' compatible LSP completion. @@ -949,12 +949,14 @@ function lsp.omnifunc(findstart, base) -- Get the start position of the current keyword local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) + local params = util.make_position_params() local items = {} lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result) if err or not result then return end - local matches = util.text_document_completion_list_to_complete_items(result) + local matches = util.text_document_completion_list_to_complete_items(result, prefix) -- TODO(ashkan): is this the best way to do this? vim.list_extend(items, matches) vim.fn.complete(textMatch+1, items) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index a6a05fb095..fc9e10cb73 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -23,6 +23,10 @@ local function request(method, params, callback) return vim.lsp.buf_request(0, method, params, callback) end +function M.server_ready() + return not not vim.lsp.buf_notify(0, "window/progress", {}) +end + function M.hover() local params = util.make_position_params() request('textDocument/hover', params) @@ -68,8 +72,9 @@ end function M.formatting(options) validate { options = {options, 't', true} } + local sts = vim.bo.softtabstop; options = vim.tbl_extend('keep', options or {}, { - tabSize = vim.bo.tabstop; + tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; insertSpaces = vim.bo.expandtab; }) local params = { @@ -85,8 +90,9 @@ function M.range_formatting(options, start_pos, end_pos) start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; } + local sts = vim.bo.softtabstop; options = vim.tbl_extend('keep', options or {}, { - tabSize = vim.bo.tabstop; + tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; insertSpaces = vim.bo.expandtab; }) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) @@ -132,5 +138,28 @@ function M.references(context) request('textDocument/references', params) end +function M.document_symbol() + local params = { textDocument = util.make_text_document_params() } + request('textDocument/documentSymbol', params) +end + +--- Send request to server to resolve document highlights for the +--- current text document position. This request can be associated +--- to key mapping or to events such as `CursorHold`, eg: +--- +--- <pre> +--- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]] +--- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]] +--- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]] +--- </pre> +function M.document_highlight() + local params = util.make_position_params() + request('textDocument/documentHighlight', params) +end + +function M.clear_references() + util.buf_clear_references() +end + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 794140ee2e..644c12f98c 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -32,12 +32,19 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_save_positions(bufnr, result.diagnostics) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.set_loclist(result.diagnostics) + util.buf_diagnostics_signs(bufnr, result.diagnostics) + vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") end M['textDocument/references'] = function(_, _, result) if not result then return end - util.set_qflist(result) + util.set_qflist(util.locations_to_items(result)) +end + +M['textDocument/documentSymbol'] = function(_, _, result, _, bufnr) + if not result or vim.tbl_isempty(result) then return end + + util.set_qflist(util.symbols_to_items(result, bufnr)) api.nvim_command("copen") api.nvim_command("wincmd p") end @@ -63,8 +70,9 @@ M['textDocument/completion'] = function(_, _, result) local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) local line_to_cursor = line:sub(col+1) local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) - local matches = util.text_document_completion_list_to_complete_items(result) + local matches = util.text_document_completion_list_to_complete_items(result, prefix) vim.fn.complete(textMatch+1, matches) end @@ -95,7 +103,7 @@ local function location_callback(_, method, result) end util.jump_to_location(result[1]) if #result > 1 then - util.set_qflist(result) + util.set_qflist(util.locations_to_items(result)) api.nvim_command("copen") api.nvim_command("wincmd p") end @@ -195,7 +203,33 @@ M['textDocument/peekDefinition'] = function(_, _, result, _) api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) end -local function log_message(_, _, result, client_id) +M['textDocument/documentHighlight'] = function(_, _, result, _) + if not result then return end + local bufnr = api.nvim_get_current_buf() + util.buf_highlight_references(bufnr, result) +end + +M['window/logMessage'] = function(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + log.error(message) + elseif message_type == protocol.MessageType.Warning then + log.warn(message) + elseif message_type == protocol.MessageType.Info then + log.info(message) + else + log.debug(message) + end + return result +end + +M['window/showMessage'] = function(_, _, result, client_id) local message_type = result.type local message = result.message local client = vim.lsp.get_client_by_id(client_id) @@ -212,9 +246,6 @@ local function log_message(_, _, result, client_id) return result end -M['window/showMessage'] = log_message -M['window/logMessage'] = log_message - -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, method, params, client_id) diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 974eaae38c..c0db5e5485 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -13,7 +13,6 @@ log.levels = { INFO = 2; WARN = 3; ERROR = 4; - -- FATAL = 4; } -- Default log level is warn. diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index f64b0b50e7..41e8119c8c 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -663,19 +663,19 @@ function protocol.make_client_capabilities() documentHighlight = { dynamicRegistration = false }; - -- documentSymbol = { - -- dynamicRegistration = false; - -- symbolKind = { - -- valueSet = (function() - -- local res = {} - -- for k in pairs(protocol.SymbolKind) do - -- if type(k) == 'string' then table.insert(res, k) end - -- end - -- return res - -- end)(); - -- }; - -- hierarchicalDocumentSymbolSupport = false; - -- }; + documentSymbol = { + dynamicRegistration = false; + symbolKind = { + valueSet = (function() + local res = {} + for k in pairs(protocol.SymbolKind) do + if type(k) == 'number' then table.insert(res, k) end + end + return res + end)(); + }; + hierarchicalDocumentSymbolSupport = true; + }; }; workspace = nil; experimental = nil; diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e13b05610b..74d73da31f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -33,38 +33,25 @@ local function convert_NIL(v) return v end --- If a dictionary is passed in, turn it into a list of string of "k=v" --- Accepts a table which can be composed of k=v strings or map-like --- specification, such as: --- --- ``` --- { --- "PRODUCTION=false"; --- "PATH=/usr/bin/"; --- PORT = 123; --- HOST = "0.0.0.0"; --- } --- ``` --- --- Non-string values will be cast with `tostring` -local function force_env_list(final_env) - if final_env then - local env = final_env - final_env = {} - for k,v in pairs(env) do - -- If it's passed in as a dict, then convert to list of "k=v" - if type(k) == "string" then - table.insert(final_env, k..'='..tostring(v)) - elseif type(v) == 'string' then - table.insert(final_env, v) - else - -- TODO is this right or should I exception here? - -- Try to coerce other values to string. - table.insert(final_env, tostring(v)) - end - end - return final_env +--- Merges current process env with the given env and returns the result as +--- a list of "k=v" strings. +--- +--- Example: +--- +--- { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", } +--- => { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", } +local function env_merge(env) + if env == nil then + return env + end + -- Merge. + env = vim.tbl_extend('force', vim.fn.environ(), env) + local final_env = {} + for k,v in pairs(env) do + assert(type(k) == 'string', 'env must be a dict') + table.insert(final_env, k..'='..tostring(v)) end + return final_env end local function format_message_with_content_length(encoded_message) @@ -262,7 +249,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para if spawn_params.cwd then assert(is_dir(spawn_params.cwd), "cwd must be a directory") end - spawn_params.env = force_env_list(extra_spawn_params.env) + spawn_params.env = env_merge(extra_spawn_params.env) end handle, pid = uv.spawn(cmd, spawn_params, onexit) end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index df82e2d412..5dd010f2a4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -88,6 +88,9 @@ function M.apply_text_edits(text_edits, bufnr) -- Reverse sort the orders so we can apply them without interfering with -- eachother. Also add i as a sort key to mimic a stable sort. table.sort(cleaned, edit_sort_key) + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1 @@ -132,7 +135,7 @@ function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) -- TODO(ashkan) check this is correct. - if api.nvim_buf_get_changedtick(bufnr) > text_document.version then + if (M.buf_versions[bufnr] or 0) > text_document.version then print("Buffer ", text_document.uri, " newer than edits.") return end @@ -145,15 +148,36 @@ function M.get_current_line_to_cursor() return line:sub(pos[2]+1) end +-- Sort by CompletionItem.sortText +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +local function sort_completion_items(items) + if items[1] and items[1].sortText then + table.sort(items, function(a, b) return a.sortText < b.sortText + end) + end +end + +-- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. +-- So we exclude completion candidates whose prefix does not match. +local function remove_unmatch_completion_items(items, prefix) + return vim.tbl_filter(function(item) + local word = item.insertText or item.label + return vim.startswith(word, prefix) + end, items) +end + --- Getting vim complete-items with incomplete flag. -- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) -- @return { matches = complete-items table, incomplete = boolean } -function M.text_document_completion_list_to_complete_items(result) +function M.text_document_completion_list_to_complete_items(result, prefix) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then return {} end + items = remove_unmatch_completion_items(items, prefix) + sort_completion_items(items) + local matches = {} for _, completion_item in ipairs(items) do @@ -177,7 +201,7 @@ function M.text_document_completion_list_to_complete_items(result) menu = completion_item.detail or '', info = info, icase = 1, - dup = 0, + dup = 1, empty = 1, }) end @@ -245,7 +269,7 @@ function M.convert_input_to_markdown_lines(input, contents) end end end - if contents[1] == '' or contents[1] == nil then + if (contents[1] == '' or contents[1] == nil) and #contents == 1 then return {} end return contents @@ -545,7 +569,8 @@ do local all_buffer_diagnostics = {} local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") - + local reference_ns = api.nvim_create_namespace("vim_lsp_references") + local sign_ns = 'vim_lsp_signs' local underline_highlight_name = "LspDiagnosticsUnderline" vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) for kind, _ in pairs(protocol.DiagnosticSeverity) do @@ -579,6 +604,11 @@ do function M.buf_clear_diagnostics(bufnr) validate { bufnr = {bufnr, 'n', true} } bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + + -- clear sign group + vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) + + -- clear virtual text namespace api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) end @@ -659,7 +689,6 @@ do end end - function M.buf_diagnostics_underline(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do local start = diagnostic.range["start"] @@ -681,6 +710,25 @@ do end end + function M.buf_clear_references(bufnr) + validate { bufnr = {bufnr, 'n', true} } + api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) + end + + function M.buf_highlight_references(bufnr, references) + validate { bufnr = {bufnr, 'n', true} } + for _, reference in ipairs(references) do + local start_pos = {reference["range"]["start"]["line"], reference["range"]["start"]["character"]} + local end_pos = {reference["range"]["end"]["line"], reference["range"]["end"]["character"]} + local document_highlight_kind = { + [protocol.DocumentHighlightKind.Text] = "LspReferenceText"; + [protocol.DocumentHighlightKind.Read] = "LspReferenceRead"; + [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; + } + highlight_range(bufnr, reference_ns, document_highlight_kind[reference["kind"]], start_pos, end_pos) + end + end + function M.buf_diagnostics_virtual_text(bufnr, diagnostics) local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] if not buffer_line_diagnostics then @@ -701,10 +749,38 @@ do api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) end end + function M.buf_diagnostics_count(kind) + local bufnr = vim.api.nvim_get_current_buf() + local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] + if not buffer_line_diagnostics then return end + local count = 0 + for _, line_diags in pairs(buffer_line_diagnostics) do + for _, diag in ipairs(line_diags) do + if protocol.DiagnosticSeverity[kind] == diag.severity then count = count + 1 end + end + end + return count + end + function M.buf_diagnostics_signs(bufnr, diagnostics) + vim.fn.sign_define('LspDiagnosticsErrorSign', {text=vim.g['LspDiagnosticsErrorSign'] or 'E', texthl='LspDiagnosticsError', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsWarningSign', {text=vim.g['LspDiagnosticsWarningSign'] or 'W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsInformationSign', {text=vim.g['LspDiagnosticsInformationSign'] or 'I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsHintSign', {text=vim.g['LspDiagnosticsHintSign'] or 'H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) + + for _, diagnostic in ipairs(diagnostics) do + local diagnostic_severity_map = { + [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; + [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; + [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; + [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; + } + vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) + end + end end local position_sort = sort_by_key(function(v) - return {v.line, v.character} + return {v.start.line, v.start.character} end) -- Returns the items with the byte position calculated correctly and in sorted @@ -721,17 +797,21 @@ function M.locations_to_items(locations) for _, d in ipairs(locations) do local start = d.range.start local fname = assert(vim.uri_to_fname(d.uri)) - table.insert(grouped[fname], start) + table.insert(grouped[fname], {start = start}) end + + local keys = vim.tbl_keys(grouped) table.sort(keys) -- TODO(ashkan) I wish we could do this lazily. for _, fname in ipairs(keys) do local rows = grouped[fname] + table.sort(rows, position_sort) local i = 0 for line in io.lines(fname) do - for _, pos in ipairs(rows) do + for _, temp in ipairs(rows) do + local pos = temp.start local row = pos.line if i == row then local col @@ -754,23 +834,60 @@ function M.locations_to_items(locations) return items end --- locations is Location[] --- Only sets for the current window. -function M.set_loclist(locations) +function M.set_loclist(items) vim.fn.setloclist(0, {}, ' ', { title = 'Language Server'; - items = M.locations_to_items(locations); + items = items; }) end --- locations is Location[] -function M.set_qflist(locations) +function M.set_qflist(items) vim.fn.setqflist({}, ' ', { title = 'Language Server'; - items = M.locations_to_items(locations); + items = items; }) end +--- Convert symbols to quickfix list items +--- +--@symbols DocumentSymbol[] or SymbolInformation[] +function M.symbols_to_items(symbols, bufnr) + local function _symbols_to_items(_symbols, _items, _bufnr) + for _, symbol in ipairs(_symbols) do + if symbol.location then -- SymbolInformation type + local range = symbol.location.range + local kind = protocol.SymbolKind[symbol.kind] + table.insert(_items, { + filename = vim.uri_to_fname(symbol.location.uri), + lnum = range.start.line + 1, + col = range.start.character + 1, + kind = kind, + text = '['..kind..'] '..symbol.name, + }) + elseif symbol.range then -- DocumentSymbole type + local kind = protocol.SymbolKind[symbol.kind] + table.insert(_items, { + -- bufnr = _bufnr, + filename = vim.api.nvim_buf_get_name(_bufnr), + lnum = symbol.range.start.line + 1, + col = symbol.range.start.character + 1, + kind = kind, + text = '['..kind..'] '..symbol.name + }) + if symbol.children then + for _, child in ipairs(symbol) do + for _, v in ipairs(_symbols_to_items(child, _items, _bufnr)) do + vim.list_extend(_items, v) + end + end + end + end + end + return _items + end + return _symbols_to_items(symbols, {}, bufnr) +end + -- Remove empty lines from the beginning and end. function M.trim_empty_lines(lines) local start = 1 @@ -823,11 +940,15 @@ function M.make_position_params() local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] col = str_utfindex(line, col) return { - textDocument = { uri = vim.uri_from_bufnr(0) }; + textDocument = M.make_text_document_params(); position = { line = row; character = col; } } end +function M.make_text_document_params() + return { uri = vim.uri_from_bufnr(0) } +end + -- @param buf buffer handle or 0 for current. -- @param row 0-indexed line -- @param col 0-indexed byte offset in line @@ -840,5 +961,7 @@ function M.character_offset(buf, row, col) return str_utfindex(line, col) end +M.buf_versions = {} + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index a71e9878bb..1bf1c63fd7 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -20,6 +20,11 @@ vim.deepcopy = (function() local deepcopy_funcs = { table = function(orig) local copy = {} + + if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then + copy = vim.empty_dict() + end + for k, v in pairs(orig) do copy[vim.deepcopy(k)] = vim.deepcopy(v) end @@ -130,6 +135,36 @@ function vim.tbl_values(t) return values end +--- Apply a function to all values of a table. +--- +--@param func function or callable table +--@param t table +function vim.tbl_map(func, t) + vim.validate{func={func,'c'},t={t,'t'}} + + local rettab = {} + for k, v in pairs(t) do + rettab[k] = func(v) + end + return rettab +end + +--- Filter a table using a predicate function +--- +--@param func function or callable table +--@param t table +function vim.tbl_filter(func, t) + vim.validate{func={func,'c'},t={t,'t'}} + + local rettab = {} + for _, entry in pairs(t) do + if func(entry) then + table.insert(rettab, entry) + end + end + return rettab +end + --- Checks if a list-like (vector) table contains `value`. --- --@param t Table to check @@ -169,9 +204,19 @@ function vim.tbl_extend(behavior, ...) if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then error('invalid "behavior": '..tostring(behavior)) end + + if select('#', ...) < 2 then + error('wrong number of arguments (given '..tostring(1 + select('#', ...))..', expected at least 3)') + end + local ret = {} + if vim._empty_dict_mt ~= nil and getmetatable(select(1, ...)) == vim._empty_dict_mt then + ret = vim.empty_dict() + end + for i = 1, select('#', ...) do local tbl = select(i, ...) + vim.validate{["after the second argument"] = {tbl,'t'}} if tbl then for k, v in pairs(tbl) do if behavior ~= 'force' and ret[k] ~= nil then @@ -311,6 +356,24 @@ function vim.tbl_islist(t) end end +--- Counts the number of non-nil values in table `t`. +--- +--- <pre> +--- vim.tbl_count({ a=1, b=2 }) => 2 +--- vim.tbl_count({ 1, 2 }) => 2 +--- </pre> +--- +--@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua +--@param Table +--@returns Number that is the number of the value in table +function vim.tbl_count(t) + vim.validate{t={t,'t'}} + + local count = 0 + for _ in pairs(t) do count = count + 1 end + return count +end + --- Trim whitespace (Lua pattern "%s") from both sides of a string. --- --@see https://www.lua.org/pil/20.2.html @@ -321,7 +384,7 @@ function vim.trim(s) return s:match('^%s*(.*%S)') or '' end ---- Escapes magic chars in a Lua pattern string. +--- Escapes magic chars in a Lua pattern. --- --@see https://github.com/rxi/lume --@param s String to escape diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index aa8b8fcdd1..8dacfa11cf 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -31,8 +31,6 @@ function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_ end local M = { - add_language=vim._ts_add_language, - inspect_language=vim._ts_inspect_language, parse_query = vim._ts_parse_query, } @@ -45,12 +43,34 @@ setmetatable(M, { end }) -function M.create_parser(bufnr, ft, id) +function M.require_language(lang, path) + if vim._ts_has_language(lang) then + return true + end + if path == nil then + local fname = 'parser/' .. lang .. '.*' + local paths = a.nvim_get_runtime_file(fname, false) + if #paths == 0 then + -- TODO(bfredl): help tag? + error("no parser for '"..lang.."' language") + end + path = paths[1] + end + vim._ts_add_language(path, lang) +end + +function M.inspect_language(lang) + M.require_language(lang) + return vim._ts_inspect_language(lang) +end + +function M.create_parser(bufnr, lang, id) + M.require_language(lang) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end - local self = setmetatable({bufnr=bufnr, lang=ft, valid=false}, Parser) - self._parser = vim._create_ts_parser(ft) + local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) + self._parser = vim._create_ts_parser(lang) self.change_cbs = {} self:parse() -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is @@ -93,11 +113,33 @@ end local Query = {} Query.__index = Query +local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} +local function check_magic(str) + if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then + return str + end + return '\\v'..str +end + function M.parse_query(lang, query) + M.require_language(lang) local self = setmetatable({}, Query) self.query = vim._ts_parse_query(lang, query) self.info = self.query:inspect() self.captures = self.info.captures + self.regexes = {} + for id,preds in pairs(self.info.patterns) do + local regexes = {} + for i, pred in ipairs(preds) do + if (pred[1] == "match?" and type(pred[2]) == "number" + and type(pred[3]) == "string") then + regexes[i] = vim.regex(check_magic(pred[3])) + end + end + if next(regexes) then + self.regexes[id] = regexes + end + end return self end @@ -110,8 +152,13 @@ local function get_node_text(node, bufnr) return string.sub(line, start_col+1, end_col) end -local function match_preds(match, preds, bufnr) - for _, pred in pairs(preds) do +function Query:match_preds(match, pattern, bufnr) + local preds = self.info.patterns[pattern] + if not preds then + return true + end + local regexes = self.regexes[pattern] + for i, pred in pairs(preds) do if pred[1] == "eq?" then local node = match[pred[2]] local node_text = get_node_text(node, bufnr) @@ -128,6 +175,16 @@ local function match_preds(match, preds, bufnr) if node_text ~= str or str == nil then return false end + elseif pred[1] == "match?" then + if not regexes or not regexes[i] then + return false + end + local node = match[pred[2]] + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return false + end + return regexes[i]:match_line(bufnr, start_row, start_col, end_col) else return false end @@ -143,8 +200,7 @@ function Query:iter_captures(node, bufnr, start, stop) local function iter() local capture, captured_node, match = raw_iter() if match ~= nil then - local preds = self.info.patterns[match.pattern] - local active = match_preds(match, preds, bufnr) + local active = self:match_preds(match, match.pattern, bufnr) match.active = active if not active then return iter() -- tail call: try next match @@ -163,8 +219,7 @@ function Query:iter_matches(node, bufnr, start, stop) local function iter() local pattern, match = raw_iter() if match ~= nil then - local preds = self.info.patterns[pattern] - local active = (not preds) or match_preds(match, preds, bufnr) + local active = self:match_preds(match, pattern, bufnr) if not active then return iter() -- tail call: try next match end diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index 1544ecbf49..1440acf0d0 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -69,6 +69,8 @@ function TSHighlighter:set_query(query) end self.id_map[i] = hl end + + a.nvim__buf_redraw_range(self.buf, 0, a.nvim_buf_line_count(self.buf)) end function TSHighlighter:on_change(changes) @@ -85,7 +87,6 @@ end function TSHighlighter:on_window(_, _win, _buf, _topline, botline) self.iter = nil - self.active_nodes = {} self.nextrow = 0 self.botline = botline self.redraw_count = self.redraw_count + 1 @@ -107,34 +108,13 @@ function TSHighlighter:on_line(_, _win, buf, line) end local start_row, start_col, end_row, end_col = node:range() local hl = self.id_map[capture] - if hl > 0 and active then - if start_row == line and end_row == line then - a.nvim__put_attr(hl, start_col, end_col) - elseif end_row >= line then - -- TODO(bfredl): this is quite messy. Togheter with multiline bufhl we should support - -- luahl generating multiline highlights (and other kinds of annotations) - self.active_nodes[{hl=hl, start_row=start_row, start_col=start_col, end_row=end_row, end_col=end_col}] = true - end + if hl > 0 and active and end_row >= line then + a.nvim__put_attr(hl, start_row, start_col, end_row, end_col) end if start_row > line then self.nextrow = start_row end end - for node,_ in pairs(self.active_nodes) do - if node.start_row <= line and node.end_row >= line then - local start_col, end_col = node.start_col, node.end_col - if node.start_row < line then - start_col = 0 - end - if node.end_row > line then - end_col = 9000 - end - a.nvim__put_attr(node.hl, start_col, end_col) - end - if node.end_row <= line then - self.active_nodes[node] = nil - end - end self.line_count[line] = (self.line_count[line] or 0) + 1 --return tostring(self.line_count[line]) end |