aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/highlight.lua41
-rw-r--r--runtime/lua/vim/lsp/buf.lua16
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua52
-rw-r--r--runtime/lua/vim/lsp/log.lua4
-rw-r--r--runtime/lua/vim/lsp/protocol.lua10
-rw-r--r--runtime/lua/vim/lsp/util.lua106
-rw-r--r--runtime/lua/vim/shared.lua41
-rw-r--r--runtime/lua/vim/uri.lua10
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