aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/util.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r--runtime/lua/vim/lsp/util.lua403
1 files changed, 318 insertions, 85 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index df82e2d412..099a77099b 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -6,6 +6,31 @@ local list_extend = vim.list_extend
local M = {}
+--- Diagnostics received from the server via `textDocument/publishDiagnostics`
+-- by buffer.
+--
+-- {<bufnr>: {diagnostics}}
+--
+-- This contains only entries for active buffers. Entries for detached buffers
+-- are discarded.
+--
+-- If you override the `textDocument/publishDiagnostic` callback,
+-- this will be empty unless you call `buf_diagnostics_save_positions`.
+--
+--
+-- Diagnostic is:
+--
+-- {
+-- range: Range
+-- message: string
+-- severity?: DiagnosticSeverity
+-- code?: number | string
+-- source?: string
+-- tags?: DiagnosticTag[]
+-- relatedInformation?: DiagnosticRelatedInformation[]
+-- }
+M.diagnostics_by_buf = {}
+
local split = vim.split
local function split_lines(value)
return split(value, '\n', true)
@@ -71,16 +96,28 @@ 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 = e.range.start.character
+ local start_bline = api.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1]
+ start_col = vim.str_byteindex(start_bline, start_col)
+ local end_row = e.range["end"].line
+ local end_col = e.range["end"].character
+ local end_bline = api.nvim_buf_get_lines(bufnr, end_row, end_row+1, true)[1]
+ end_col = vim.str_byteindex(end_bline, end_col)
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
@@ -131,10 +168,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)
- -- TODO(ashkan) check this is correct.
- if api.nvim_buf_get_changedtick(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
@@ -145,15 +184,55 @@ 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
+
+-- Returns text that should be inserted when selecting completion item. The precedence is as follows:
+-- textEdit.newText > insertText > label
+-- 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
+ return item.textEdit.newText
+ elseif item.insertText ~= nil then
+ return item.insertText
+ end
+ return item.label
+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 = get_completion_word(item)
+ return vim.startswith(word, 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 }
-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
@@ -169,16 +248,23 @@ function M.text_document_completion_list_to_complete_items(result)
end
end
- local word = completion_item.insertText or completion_item.label
+ local word = get_completion_word(completion_item)
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,
- dup = 0,
+ dup = 1,
empty = 1,
+ user_data = {
+ nvim = {
+ lsp = {
+ completion_item = completion_item
+ }
+ }
+ },
})
end
@@ -245,12 +331,71 @@ 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
end
+--- Convert SignatureHelp response to markdown lines.
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
+function M.convert_signature_help_to_markdown_lines(signature_help)
+ if not signature_help.signatures then
+ return
+ end
+ --The active signature. If omitted or the value lies outside the range of
+ --`signatures` the value defaults to zero or is ignored if `signatures.length
+ --=== 0`. Whenever possible implementors should make an active decision about
+ --the active signature and shouldn't rely on a default value.
+ local contents = {}
+ local active_signature = signature_help.activeSignature or 0
+ -- If the activeSignature is not inside the valid range, then clip it.
+ if active_signature >= #signature_help.signatures then
+ active_signature = 0
+ end
+ local signature = signature_help.signatures[active_signature + 1]
+ if not signature then
+ return
+ end
+ vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ if signature.documentation then
+ M.convert_input_to_markdown_lines(signature.documentation, contents)
+ end
+ if signature_help.parameters then
+ local active_parameter = signature_help.activeParameter or 0
+ -- If the activeParameter is not inside the valid range, then clip it.
+ if active_parameter >= #signature_help.parameters then
+ active_parameter = 0
+ end
+ local parameter = signature.parameters and signature.parameters[active_parameter]
+ if parameter then
+ --[=[
+ --Represents a parameter of a callable-signature. A parameter can
+ --have a label and a doc-comment.
+ interface ParameterInformation {
+ --The label of this parameter information.
+ --
+ --Either a string or an inclusive start and exclusive end offsets within its containing
+ --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
+ --string representation as `Position` and `Range` does.
+ --
+ --*Note*: a label of type string should be a substring of its containing signature label.
+ --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
+ label: string | [number, number];
+ --The human-readable doc-comment of this parameter. Will be shown
+ --in the UI but can be omitted.
+ documentation?: string | MarkupContent;
+ }
+ --]=]
+ -- TODO highlight parameter
+ if parameter.documentation then
+ M.convert_input_help_to_markdown_lines(parameter.documentation, contents)
+ end
+ end
+ end
+ return contents
+end
+
function M.make_floating_popup_options(width, height, opts)
validate {
opts = { opts, 't', true };
@@ -297,14 +442,24 @@ function M.make_floating_popup_options(width, height, opts)
end
function M.jump_to_location(location)
- if location.uri == nil then return end
- local bufnr = vim.uri_to_bufnr(location.uri)
+ -- location may be Location or LocationLink
+ local uri = location.uri or location.targetUri
+ if uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(uri)
-- Save position in jumplist
vim.cmd "normal! m'"
- -- TODO(ashkan) use tagfunc here to update tagstack.
+
+ -- Push a new item into tagstack
+ local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
+ local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
+ vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
+
+ --- Jump to new location (adjusting for UTF-16 encoding of characters)
api.nvim_set_current_buf(bufnr)
- local row = location.range.start.line
- local col = location.range.start.character
+ 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
local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
col = vim.str_byteindex(line, col)
api.nvim_win_set_cursor(0, {row + 1, col})
@@ -498,36 +653,10 @@ function M.open_floating_preview(contents, filetype, opts)
end
api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
- -- TODO make InsertCharPre disappearing optional?
- api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden"}, floating_winnr)
return floating_bufnr, floating_winnr
end
-local function validate_lsp_position(pos)
- validate { pos = {pos, 't'} }
- validate {
- line = {pos.line, 'n'};
- character = {pos.character, 'n'};
- }
- return true
-end
-
-function M.open_floating_peek_preview(bufnr, start, finish, opts)
- validate {
- bufnr = {bufnr, 'n'};
- start = {start, validate_lsp_position, 'valid start Position'};
- finish = {finish, validate_lsp_position, 'valid finish Position'};
- opts = { opts, 't', true };
- }
- local width = math.max(finish.character - start.character + 1, 1)
- local height = math.max(finish.line - start.line + 1, 1)
- local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
- api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character})
- api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
- return floating_winnr
-end
-
-
local function highlight_range(bufnr, ns, hiname, start, finish)
if start[1] == finish[1] then
-- TODO care about encoding here since this is in byte index?
@@ -542,10 +671,9 @@ local function highlight_range(bufnr, ns, hiname, start, finish)
end
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
@@ -573,12 +701,18 @@ do
table.insert(cmd_parts, k.."="..v)
end
api.nvim_command(table.concat(cmd_parts, ' '))
+ api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name)
severity_highlights[severity] = highlight_name
end
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
@@ -593,13 +727,12 @@ do
-- if #marks == 0 then
-- return
-- end
- -- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
local lines = {"Diagnostics:"}
local highlights = {{0, "Bold"}}
- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ local buffer_diagnostics = M.diagnostics_by_buf[bufnr]
if not buffer_diagnostics then return end
- local line_diagnostics = buffer_diagnostics[line]
+ local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics)[line]
if not line_diagnostics then return end
for i, diagnostic in ipairs(line_diagnostics) do
@@ -610,6 +743,7 @@ do
-- TODO(ashkan) make format configurable?
local prefix = string.format("%d. ", i)
local hiname = severity_highlights[diagnostic.severity]
+ assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
local message_lines = split_lines(diagnostic.message)
table.insert(lines, prefix..message_lines[1])
table.insert(highlights, {#prefix + 1, hiname})
@@ -627,6 +761,8 @@ do
return popup_bufnr, winnr
end
+ --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf
+ --
function M.buf_diagnostics_save_positions(bufnr, diagnostics)
validate {
bufnr = {bufnr, 'n', true};
@@ -635,31 +771,17 @@ do
if not diagnostics then return end
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
- if not all_buffer_diagnostics[bufnr] then
+ if not M.diagnostics_by_buf[bufnr] then
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach(bufnr, false, {
on_detach = function(b)
- all_buffer_diagnostics[b] = nil
+ M.diagnostics_by_buf[b] = nil
end
})
end
- all_buffer_diagnostics[bufnr] = {}
- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
-
- for _, diagnostic in ipairs(diagnostics) do
- local start = diagnostic.range.start
- -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {})
- -- buffer_diagnostics[mark_id] = diagnostic
- local line_diagnostics = buffer_diagnostics[start.line]
- if not line_diagnostics then
- line_diagnostics = {}
- buffer_diagnostics[start.line] = line_diagnostics
- end
- table.insert(line_diagnostics, diagnostic)
- end
+ M.diagnostics_by_buf[bufnr] = diagnostics
end
-
function M.buf_diagnostics_underline(bufnr, diagnostics)
for _, diagnostic in ipairs(diagnostics) do
local start = diagnostic.range["start"]
@@ -681,15 +803,46 @@ do
end
end
- function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
- local buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
- if not buffer_line_diagnostics then
- M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ 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";
+ }
+ local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
+ highlight_range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
end
- buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
- if not buffer_line_diagnostics then
+ end
+
+ function M.diagnostics_group_by_line(diagnostics)
+ if not diagnostics then return end
+ local diagnostics_by_line = {}
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ local line_diagnostics = diagnostics_by_line[start.line]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ diagnostics_by_line[start.line] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ return diagnostics_by_line
+ end
+
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
+ if not diagnostics then
return
end
+ local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics)
for line, line_diags in pairs(buffer_line_diagnostics) do
local virt_texts = {}
for i = 1, #line_diags - 1 do
@@ -701,10 +854,36 @@ 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 diagnostics = M.diagnostics_by_buf[bufnr]
+ if not diagnostics then return end
+ local count = 0
+ for _, diagnostic in pairs(diagnostics) do
+ if protocol.DiagnosticSeverity[kind] == diagnostic.severity then
+ count = count + 1
+ end
+ end
+ return count
+ end
+
+ local diagnostic_severity_map = {
+ [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign";
+ [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign";
+ [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign";
+ [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign";
+ }
+
+ function M.buf_diagnostics_signs(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ 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
@@ -719,19 +898,25 @@ function M.locations_to_items(locations)
end;
})
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)
+ -- locations may be Location or LocationLink
+ local uri = d.uri or d.targetUri
+ local fname = assert(vim.uri_to_fname(uri))
+ local range = d.range or d.targetSelectionRange
+ table.insert(grouped[fname], {start = range.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 +939,65 @@ 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
+-- 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[]
+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 = M._get_symbol_kind_name(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 = M._get_symbol_kind_name(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 _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
+ vim.list_extend(_items, v)
+ 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 +1050,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 +1071,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