diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/highlight.lua | 41 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 16 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/callbacks.lua | 52 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 10 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 106 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 41 | ||||
-rw-r--r-- | runtime/lua/vim/uri.lua | 10 |
8 files changed, 239 insertions, 41 deletions
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua new file mode 100644 index 0000000000..5c98c626a4 --- /dev/null +++ b/runtime/lua/vim/highlight.lua @@ -0,0 +1,41 @@ +local api = vim.api + +local highlight = {} + +--- Highlight the yanked region +-- +--- use from init.vim via +--- au TextYankPost * lua require'vim.highlight'.on_yank() +--- customize highlight group and timeout via +--- au TextYankPost * lua require'vim.highlight'.on_yank("IncSearch", 500) +-- @param higroup highlight group for yanked region +-- @param timeout time in ms before highlight is cleared +-- @param event event structure +function highlight.on_yank(higroup, timeout, event) + event = event or vim.v.event + if event.operator ~= 'y' or event.regtype == '' then return end + higroup = higroup or "IncSearch" + timeout = timeout or 500 + + local bufnr = api.nvim_get_current_buf() + local yank_ns = api.nvim_create_namespace('') + api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) + + local pos1 = vim.fn.getpos("'[") + local pos2 = vim.fn.getpos("']") + + pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} + pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} + + local region = vim.region(bufnr, pos1, pos2, event.regtype, event.inclusive) + for linenr, cols in pairs(region) do + api.nvim_buf_add_highlight(bufnr, yank_ns, higroup, linenr, cols[1], cols[2]) + end + + vim.defer_fn( + function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end, + timeout + ) +end + +return highlight diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0b45951a56..7a819f3c3d 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -161,5 +161,21 @@ function M.clear_references() util.buf_clear_references() end +function M.code_action(context) + validate { context = { context, 't', true } } + context = context or { diagnostics = util.get_line_diagnostics() } + local params = util.make_range_params() + params.context = context + request('textDocument/codeAction', params) +end + +function M.execute_command(command) + validate { + command = { command.command, 's' }, + arguments = { command.arguments, 't', true } + } + request('workspace/executeCommand', command) +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 70d21be8e7..7c51fc2cc2 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -3,6 +3,7 @@ local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' local vim = vim local api = vim.api +local buf = require 'vim.lsp.buf' local M = {} @@ -11,13 +12,56 @@ local function err_message(...) api.nvim_command("redraw") end +M['workspace/executeCommand'] = function(err, _) + if err then + error("Could not execute code action: "..err.message) + end +end + +M['textDocument/codeAction'] = function(_, _, actions) + if actions == nil or vim.tbl_isempty(actions) then + print("No code actions available") + return + end + + local option_strings = {"Code Actions:"} + for i, action in ipairs(actions) do + local title = action.title:gsub('\r\n', '\\r\\n') + title = title:gsub('\n', '\\n') + table.insert(option_strings, string.format("%d. %s", i, title)) + end + + local choice = vim.fn.inputlist(option_strings) + if choice < 1 or choice > #actions then + return + end + local action_chosen = actions[choice] + -- textDocument/codeAction can return either Command[] or CodeAction[]. + -- If it is a CodeAction, it can have either an edit, a command or both. + -- Edits should be executed first + if action_chosen.edit or type(action_chosen.command) == "table" then + if action_chosen.edit then + util.apply_workspace_edit(action_chosen.edit) + end + if type(action_chosen.command) == "table" then + buf.execute_command(action_chosen.command) + end + else + buf.execute_command(action_chosen) + end +end + M['workspace/applyEdit'] = function(_, _, workspace_edit) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? if workspace_edit.label then print("Workspace edit", workspace_edit.label) end - util.apply_workspace_edit(workspace_edit.edit) + local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) + return { + applied = status; + failureReason = result; + } end M['textDocument/publishDiagnostics'] = function(_, _, result) @@ -198,12 +242,12 @@ end -- 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) - local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + M[k] = function(err, method, params, client_id, bufnr) + log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr }) if err then error(tostring(err)) end - return fn(err, method, params, client_id) + return fn(err, method, params, client_id, bufnr) end end diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index c0db5e5485..696ce43a59 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -17,14 +17,14 @@ log.levels = { -- Default log level is warn. local current_log_level = log.levels.WARN -local log_date_format = "%FT%H:%M:%SZ%z" +local log_date_format = "%FT%H:%M:%S%z" do local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" local function path_join(...) return table.concat(vim.tbl_flatten{...}, path_sep) end - local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log') + local logfilename = path_join(vim.fn.stdpath('data'), 'lsp.log') --- Return the log filename. function log.get_filename() diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 76817e3a4a..877d11411b 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -620,6 +620,15 @@ function protocol.make_client_capabilities() -- Send textDocument/didSave after saving (BufWritePost) didSave = true; }; + codeAction = { + dynamicRegistration = false; + + codeActionLiteralSupport = { + codeActionKind = { + valueSet = {}; + }; + }; + }; completion = { dynamicRegistration = false; completionItem = { @@ -703,6 +712,7 @@ function protocol.make_client_capabilities() }; hierarchicalWorkspaceSymbolSupport = true; }; + applyEdit = true; }; experimental = nil; } diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 494eebf9ea..5c6d183ac1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -94,18 +94,45 @@ local edit_sort_key = sort_by_key(function(e) return {e.A[1], e.A[2], e.i} end) +local function get_line_byte_from_line_character(bufnr, lnum, cnum) + -- Skip check when the byte and character position is the same + if cnum > 0 then + local lines = api.nvim_buf_get_lines(bufnr, lnum, lnum+1, false) + + if #lines > 0 then + return vim.str_byteindex(lines[1], cnum) + end + end + + return cnum +end + function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end local start_line, finish_line = math.huge, -1 local cleaned = {} for i, e in ipairs(text_edits) do + -- adjust start and end column for UTF-16 encoding of non-ASCII characters + local start_row = e.range.start.line + local start_col = get_line_byte_from_line_character( + bufnr, + start_row, + e.range.start.character) + local end_row = e.range["end"].line + local end_col = get_line_byte_from_line_character( + bufnr, + end_row, + e.range["end"].character) start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) -- TODO(ashkan) sanity check ranges for overlap. table.insert(cleaned, { i = i; - A = {e.range.start.line; e.range.start.character}; - B = {e.range["end"].line; e.range["end"].character}; + A = {start_row; start_col}; + B = {end_row; end_col}; lines = vim.split(e.newText, '\n', true); }) end @@ -113,9 +140,6 @@ 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 @@ -159,10 +183,12 @@ end 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) - -- `VersionedTextDocumentIdentifier`s version may be nil https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if text_document.version ~= vim.NIL and M.buf_versions[bufnr] > text_document.version then - print("Buffer ", text_document.uri, " newer than edits.") - return + if text_document.version then + -- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier + if text_document.version ~= vim.NIL and M.buf_versions[bufnr] ~= nil and M.buf_versions[bufnr] > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return + end end M.apply_text_edits(text_document_edit.edits, bufnr) end @@ -203,6 +229,13 @@ local function remove_unmatch_completion_items(items, prefix) end, items) end +-- Acording to LSP spec, if the client set "completionItemKind.valueSet", +-- the client must handle it properly even if it receives a value outside the specification. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +function M._get_completion_item_kind_name(completion_item_kind) + return protocol.CompletionItemKind[completion_item_kind] or "Unknown" +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 } @@ -234,7 +267,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) table.insert(matches, { word = word, abbr = completion_item.label, - kind = protocol.CompletionItemKind[completion_item.kind] or '', + kind = M._get_completion_item_kind_name(completion_item.kind), menu = completion_item.detail or '', info = info, icase = 1, @@ -436,8 +469,9 @@ function M.jump_to_location(location) local items = {{tagname=vim.fn.expand('<cword>'), from=from}} vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') - --- Jump to new location + --- Jump to new location (adjusting for UTF-16 encoding of characters) api.nvim_set_current_buf(bufnr) + api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line local col = range.start.character @@ -701,20 +735,29 @@ do return severity_highlights[severity] end - function M.show_line_diagnostics() + function M.get_line_diagnostics() local bufnr = api.nvim_get_current_buf() - local line = api.nvim_win_get_cursor(0)[1] - 1 + local linenr = api.nvim_win_get_cursor(0)[1] - 1 + + local buffer_diagnostics = M.diagnostics_by_buf[bufnr] + + if not buffer_diagnostics then + return {} + end + + local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics) + return diagnostics_by_line[linenr] or {} + end + + function M.show_line_diagnostics() -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) -- if #marks == 0 then -- return -- end local lines = {"Diagnostics:"} local highlights = {{0, "Bold"}} - - local buffer_diagnostics = M.diagnostics_by_buf[bufnr] - if not buffer_diagnostics then return end - local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics)[line] - if not line_diagnostics then return end + local line_diagnostics = M.get_line_diagnostics() + if vim.tbl_isempty(line_diagnostics) then return end for i, diagnostic in ipairs(line_diagnostics) do -- for i, mark in ipairs(marks) do @@ -934,6 +977,13 @@ function M.set_qflist(items) }) end +-- Acording to LSP spec, if the client set "symbolKind.valueSet", +-- the client must handle it properly even if it receives a value outside the specification. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol +function M._get_symbol_kind_name(symbol_kind) + return protocol.SymbolKind[symbol_kind] or "Unknown" +end + --- Convert symbols to quickfix list items --- --@symbols DocumentSymbol[] or SymbolInformation[] @@ -942,7 +992,7 @@ function M.symbols_to_items(symbols, bufnr) for _, symbol in ipairs(_symbols) do if symbol.location then -- SymbolInformation type local range = symbol.location.range - local kind = protocol.SymbolKind[symbol.kind] + local kind = M._get_symbol_kind_name(symbol.kind) table.insert(_items, { filename = vim.uri_to_fname(symbol.location.uri), lnum = range.start.line + 1, @@ -951,7 +1001,7 @@ function M.symbols_to_items(symbols, bufnr) text = '['..kind..'] '..symbol.name, }) elseif symbol.range then -- DocumentSymbole type - local kind = protocol.SymbolKind[symbol.kind] + local kind = M._get_symbol_kind_name(symbol.kind) table.insert(_items, { -- bufnr = _bufnr, filename = vim.api.nvim_buf_get_name(_bufnr), @@ -1018,14 +1068,26 @@ function M.try_trim_markdown_code_blocks(lines) end local str_utfindex = vim.str_utfindex -function M.make_position_params() +local function make_position_param() local row, col = unpack(api.nvim_win_get_cursor(0)) row = row - 1 local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] col = str_utfindex(line, col) + return { line = row; character = col; } +end + +function M.make_position_params() return { textDocument = M.make_text_document_params(); - position = { line = row; character = col; } + position = make_position_param() + } +end + +function M.make_range_params() + local position = make_position_param() + return { + textDocument = { uri = vim.uri_from_bufnr(0) }, + range = { start = position; ["end"] = position; } } end diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index d18fcfaf95..2135bfc837 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -200,16 +200,7 @@ function vim.tbl_isempty(t) return next(t) == nil end ---- Merges two or more map-like tables. ---- ---@see |extend()| ---- ---@param behavior Decides what to do if a key is found in more than one map: ---- - "error": raise an error ---- - "keep": use value from the leftmost map ---- - "force": use value from the rightmost map ---@param ... Two or more map-like tables. -function vim.tbl_extend(behavior, ...) +local function tbl_extend(behavior, deep_extend, ...) if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then error('invalid "behavior": '..tostring(behavior)) end @@ -228,7 +219,9 @@ function vim.tbl_extend(behavior, ...) 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 + if type(v) == 'table' and deep_extend and not vim.tbl_islist(v) then + ret[k] = tbl_extend(behavior, true, ret[k] or vim.empty_dict(), v) + elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then error('key found in more than one map: '..k) end -- Else behavior is "keep". @@ -241,6 +234,32 @@ function vim.tbl_extend(behavior, ...) return ret end +--- Merges two or more map-like tables. +--- +--@see |extend()| +--- +--@param behavior Decides what to do if a key is found in more than one map: +--- - "error": raise an error +--- - "keep": use value from the leftmost map +--- - "force": use value from the rightmost map +--@param ... Two or more map-like tables. +function vim.tbl_extend(behavior, ...) + return tbl_extend(behavior, false, ...) +end + +--- Merges recursively two or more map-like tables. +--- +--@see |tbl_extend()| +--- +--@param behavior Decides what to do if a key is found in more than one map: +--- - "error": raise an error +--- - "keep": use value from the leftmost map +--- - "force": use value from the rightmost map +--@param ... Two or more map-like tables. +function vim.tbl_deep_extend(behavior, ...) + return tbl_extend(behavior, true, ...) +end + --- Deep compare values for equality function vim.deep_equal(a, b) if a == b then return true end diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index e28cc9e20f..9c3535c676 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -65,9 +65,11 @@ local function uri_from_fname(path) return table.concat(uri_parts) end +local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*)://.*' + local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) - local scheme = fname:match("^([a-z]+)://.*") + local scheme = fname:match(URI_SCHEME_PATTERN) if scheme then return fname else @@ -76,6 +78,10 @@ local function uri_from_bufnr(bufnr) end local function uri_to_fname(uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) + if scheme ~= 'file' then + return uri + end uri = uri_decode(uri) -- TODO improve this. if is_windows_file_uri(uri) then @@ -89,7 +95,7 @@ end -- Return or create a buffer for a uri. local function uri_to_bufnr(uri) - local scheme = assert(uri:match("^([a-z]+)://.*"), 'Uri must contain a scheme: ' .. uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme == 'file' then return vim.fn.bufadd(uri_to_fname(uri)) else |