mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00
1372 lines
46 KiB
Lua
1372 lines
46 KiB
Lua
local protocol = require 'vim.lsp.protocol'
|
|
local vim = vim
|
|
local validate = vim.validate
|
|
local api = vim.api
|
|
local list_extend = vim.list_extend
|
|
local highlight = require 'vim.highlight'
|
|
|
|
local npcall = vim.F.npcall
|
|
local split = vim.split
|
|
|
|
local _warned = {}
|
|
local warn_once = function(message)
|
|
if not _warned[message] then
|
|
vim.api.nvim_err_writeln(message)
|
|
_warned[message] = true
|
|
end
|
|
end
|
|
|
|
local M = {}
|
|
|
|
-- TODO(remove-callbacks)
|
|
M.diagnostics_by_buf = setmetatable({}, {
|
|
__index = function(_, bufnr)
|
|
warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'")
|
|
return vim.lsp.diagnostic.get(bufnr)
|
|
end
|
|
})
|
|
|
|
--@private
|
|
local function split_lines(value)
|
|
return split(value, '\n', true)
|
|
end
|
|
|
|
--- Replaces text in a range with new text.
|
|
---
|
|
--- CAUTION: Changes in-place!
|
|
---
|
|
--@param lines (table) Original list of strings
|
|
--@param A (table) Start position; a 2-tuple of {line, col} numbers
|
|
--@param B (table) End position; a 2-tuple of {line, col} numbers
|
|
--@param new_lines A list of strings to replace the original
|
|
--@returns (table) The modified {lines} object
|
|
function M.set_lines(lines, A, B, new_lines)
|
|
-- 0-indexing to 1-indexing
|
|
local i_0 = A[1] + 1
|
|
-- If it extends past the end, truncate it to the end. This is because the
|
|
-- way the LSP describes the range including the last newline is by
|
|
-- specifying a line number after what we would call the last line.
|
|
local i_n = math.min(B[1] + 1, #lines)
|
|
if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
|
|
error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
|
|
end
|
|
local prefix = ""
|
|
local suffix = lines[i_n]:sub(B[2]+1)
|
|
if A[2] > 0 then
|
|
prefix = lines[i_0]:sub(1, A[2])
|
|
end
|
|
local n = i_n - i_0 + 1
|
|
if n ~= #new_lines then
|
|
for _ = 1, n - #new_lines do table.remove(lines, i_0) end
|
|
for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
|
|
end
|
|
for i = 1, #new_lines do
|
|
lines[i - 1 + i_0] = new_lines[i]
|
|
end
|
|
if #suffix > 0 then
|
|
local i = i_0 + #new_lines - 1
|
|
lines[i] = lines[i]..suffix
|
|
end
|
|
if #prefix > 0 then
|
|
lines[i_0] = prefix..lines[i_0]
|
|
end
|
|
return lines
|
|
end
|
|
|
|
--@private
|
|
local function sort_by_key(fn)
|
|
return function(a,b)
|
|
local ka, kb = fn(a), fn(b)
|
|
assert(#ka == #kb)
|
|
for i = 1, #ka do
|
|
if ka[i] ~= kb[i] then
|
|
return ka[i] < kb[i]
|
|
end
|
|
end
|
|
-- every value must have been equal here, which means it's not less than.
|
|
return false
|
|
end
|
|
end
|
|
--@private
|
|
local edit_sort_key = sort_by_key(function(e)
|
|
return {e.A[1], e.A[2], e.i}
|
|
end)
|
|
|
|
--@private
|
|
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
|
|
--- Returns a zero-indexed column, since set_lines() does the conversion to
|
|
--- 1-indexed
|
|
local function get_line_byte_from_position(bufnr, position)
|
|
-- LSP's line and characters are 0-indexed
|
|
-- Vim's line and columns are 1-indexed
|
|
local col = position.character
|
|
-- When on the first character, we can ignore the difference between byte and
|
|
-- character
|
|
if col > 0 then
|
|
if not api.nvim_buf_is_loaded(bufnr) then
|
|
vim.fn.bufload(bufnr)
|
|
end
|
|
|
|
local line = position.line
|
|
local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
|
|
if #lines > 0 then
|
|
local ok, result = pcall(vim.str_byteindex, lines[1], col)
|
|
|
|
if ok then
|
|
return result
|
|
end
|
|
end
|
|
end
|
|
return col
|
|
end
|
|
|
|
--- Applies a list of text edits to a buffer.
|
|
--@param text_edits (table) list of `TextEdit` objects
|
|
--@param buf_nr (number) Buffer id
|
|
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
|
|
api.nvim_buf_set_option(bufnr, 'buflisted', true)
|
|
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_position(bufnr, e.range.start)
|
|
local end_row = e.range["end"].line
|
|
local end_col = get_line_byte_from_position(bufnr, e.range['end'])
|
|
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 = {start_row; start_col};
|
|
B = {end_row; end_col};
|
|
lines = vim.split(e.newText, '\n', true);
|
|
})
|
|
end
|
|
|
|
-- 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)
|
|
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
|
|
if set_eol and #lines[#lines] ~= 0 then
|
|
table.insert(lines, '')
|
|
end
|
|
|
|
for i = #cleaned, 1, -1 do
|
|
local e = cleaned[i]
|
|
local A = {e.A[1] - start_line, e.A[2]}
|
|
local B = {e.B[1] - start_line, e.B[2]}
|
|
lines = M.set_lines(lines, A, B, e.lines)
|
|
end
|
|
if set_eol and #lines[#lines] == 0 then
|
|
table.remove(lines)
|
|
end
|
|
api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
|
|
end
|
|
|
|
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
|
|
-- local valid_unix_path_characters = "[^/]"
|
|
-- https://github.com/davidm/lua-glob-pattern
|
|
-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
|
|
-- function M.glob_to_regex(glob)
|
|
-- end
|
|
|
|
--- Can be used to extract the completion items from a
|
|
--- `textDocument/completion` request, which may return one of
|
|
--- `CompletionItem[]`, `CompletionList` or null.
|
|
--@param result (table) The result of a `textDocument/completion` request
|
|
--@returns (table) List of completion items
|
|
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
|
function M.extract_completion_items(result)
|
|
if type(result) == 'table' and result.items then
|
|
-- result is a `CompletionList`
|
|
return result.items
|
|
elseif result ~= nil then
|
|
-- result is `CompletionItem[]`
|
|
return result
|
|
else
|
|
-- result is `null`
|
|
return {}
|
|
end
|
|
end
|
|
|
|
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
|
|
-- document.
|
|
---
|
|
--@param text_document_edit (table) a `TextDocumentEdit` object
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
|
|
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 null
|
|
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
|
|
if text_document.version
|
|
and M.buf_versions[bufnr]
|
|
and M.buf_versions[bufnr] > text_document.version then
|
|
print("Buffer ", text_document.uri, " newer than edits.")
|
|
return
|
|
end
|
|
|
|
M.apply_text_edits(text_document_edit.edits, bufnr)
|
|
end
|
|
|
|
--@private
|
|
--- Recursively parses snippets in a completion entry.
|
|
---
|
|
--@param input (string) Snippet text to parse for snippets
|
|
--@param inner (bool) Whether this function is being called recursively
|
|
--@returns 2-tuple of strings: The first is the parsed result, the second is the
|
|
---unparsed rest of the input
|
|
local function parse_snippet_rec(input, inner)
|
|
local res = ""
|
|
|
|
local close, closeend = nil, nil
|
|
if inner then
|
|
close, closeend = input:find("}", 1, true)
|
|
while close ~= nil and input:sub(close-1,close-1) == "\\" do
|
|
close, closeend = input:find("}", closeend+1, true)
|
|
end
|
|
end
|
|
|
|
local didx = input:find('$', 1, true)
|
|
if didx == nil and close == nil then
|
|
return input, ""
|
|
elseif close ~=nil and (didx == nil or close < didx) then
|
|
-- No inner placeholders
|
|
return input:sub(0, close-1), input:sub(closeend+1)
|
|
end
|
|
|
|
res = res .. input:sub(0, didx-1)
|
|
input = input:sub(didx+1)
|
|
|
|
local tabstop, tabstopend = input:find('^%d+')
|
|
local placeholder, placeholderend = input:find('^{%d+:')
|
|
local choice, choiceend = input:find('^{%d+|')
|
|
|
|
if tabstop then
|
|
input = input:sub(tabstopend+1)
|
|
elseif choice then
|
|
input = input:sub(choiceend+1)
|
|
close, closeend = input:find("|}", 1, true)
|
|
|
|
res = res .. input:sub(0, close-1)
|
|
input = input:sub(closeend+1)
|
|
elseif placeholder then
|
|
-- TODO: add support for variables
|
|
input = input:sub(placeholderend+1)
|
|
|
|
-- placeholders and variables are recursive
|
|
while input ~= "" do
|
|
local r, tail = parse_snippet_rec(input, true)
|
|
r = r:gsub("\\}", "}")
|
|
|
|
res = res .. r
|
|
input = tail
|
|
end
|
|
else
|
|
res = res .. "$"
|
|
end
|
|
|
|
return res, input
|
|
end
|
|
|
|
--- Parses snippets in a completion entry.
|
|
---
|
|
--@param input (string) unparsed snippet
|
|
--@returns (string) parsed snippet
|
|
function M.parse_snippet(input)
|
|
local res, _ = parse_snippet_rec(input, false)
|
|
|
|
return res
|
|
end
|
|
|
|
--@private
|
|
--- Sorts by CompletionItem.sortText.
|
|
---
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
|
local function sort_completion_items(items)
|
|
table.sort(items, function(a, b)
|
|
return (a.sortText or a.label) < (b.sortText or b.label)
|
|
end)
|
|
end
|
|
|
|
--@private
|
|
--- Returns text that should be inserted when selecting completion item. The
|
|
--- precedence is as follows: textEdit.newText > insertText > label
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
|
local function get_completion_word(item)
|
|
if item.textEdit ~= nil and item.textEdit.newText ~= nil then
|
|
if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
|
|
return item.textEdit.newText
|
|
else
|
|
return M.parse_snippet(item.textEdit.newText)
|
|
end
|
|
elseif item.insertText ~= nil then
|
|
if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
|
|
return item.insertText
|
|
else
|
|
return M.parse_snippet(item.insertText)
|
|
end
|
|
end
|
|
return item.label
|
|
end
|
|
|
|
--@private
|
|
--- Some language 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.
|
|
---
|
|
--@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
|
|
--@returns (`vim.lsp.protocol.completionItemKind`)
|
|
--@see 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
|
|
|
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
|
--- |complete-items|.
|
|
---
|
|
--@param result The result of a `textDocument/completion` call, e.g. from
|
|
---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
|
|
--- `CompletionList` or `null`
|
|
--@param prefix (string) the prefix to filter the completion items
|
|
--@returns { matches = complete-items table, incomplete = bool }
|
|
--@see |complete-items|
|
|
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
|
|
local info = ' '
|
|
local documentation = completion_item.documentation
|
|
if documentation then
|
|
if type(documentation) == 'string' and documentation ~= '' then
|
|
info = documentation
|
|
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
|
|
info = documentation.value
|
|
-- else
|
|
-- TODO(ashkan) Validation handling here?
|
|
end
|
|
end
|
|
|
|
local word = get_completion_word(completion_item)
|
|
table.insert(matches, {
|
|
word = word,
|
|
abbr = completion_item.label,
|
|
kind = M._get_completion_item_kind_name(completion_item.kind),
|
|
menu = completion_item.detail or '',
|
|
info = info,
|
|
icase = 1,
|
|
dup = 1,
|
|
empty = 1,
|
|
user_data = {
|
|
nvim = {
|
|
lsp = {
|
|
completion_item = completion_item
|
|
}
|
|
}
|
|
},
|
|
})
|
|
end
|
|
|
|
return matches
|
|
end
|
|
|
|
--- Applies a `WorkspaceEdit`.
|
|
---
|
|
--@param workspace_edit (table) `WorkspaceEdit`
|
|
-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
|
|
function M.apply_workspace_edit(workspace_edit)
|
|
if workspace_edit.documentChanges then
|
|
for _, change in ipairs(workspace_edit.documentChanges) do
|
|
if change.kind then
|
|
-- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
|
|
error(string.format("Unsupported change: %q", vim.inspect(change)))
|
|
else
|
|
M.apply_text_document_edit(change)
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
local all_changes = workspace_edit.changes
|
|
if not (all_changes and not vim.tbl_isempty(all_changes)) then
|
|
return
|
|
end
|
|
|
|
for uri, changes in pairs(all_changes) do
|
|
local bufnr = vim.uri_to_bufnr(uri)
|
|
M.apply_text_edits(changes, bufnr)
|
|
end
|
|
end
|
|
|
|
--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
|
|
--- a list of lines containing valid markdown. Useful to populate the hover
|
|
--- window for `textDocument/hover`, for parsing the result of
|
|
--- `textDocument/signatureHelp`, and potentially others.
|
|
---
|
|
--@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
|
|
--@param contents (table, optional, default `{}`) List of strings to extend with converted lines
|
|
--@returns {contents}, extended with lines of converted markdown.
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
|
|
function M.convert_input_to_markdown_lines(input, contents)
|
|
contents = contents or {}
|
|
-- MarkedString variation 1
|
|
if type(input) == 'string' then
|
|
list_extend(contents, split_lines(input))
|
|
else
|
|
assert(type(input) == 'table', "Expected a table for Hover.contents")
|
|
-- MarkupContent
|
|
if input.kind then
|
|
-- The kind can be either plaintext or markdown. However, either way we
|
|
-- will just be rendering markdown, so we handle them both the same way.
|
|
-- TODO these can have escaped/sanitized html codes in markdown. We
|
|
-- should make sure we handle this correctly.
|
|
|
|
-- Some servers send input.value as empty, so let's ignore this :(
|
|
-- assert(type(input.value) == 'string')
|
|
list_extend(contents, split_lines(input.value or ''))
|
|
-- MarkupString variation 2
|
|
elseif input.language then
|
|
-- Some servers send input.value as empty, so let's ignore this :(
|
|
-- assert(type(input.value) == 'string')
|
|
table.insert(contents, "```"..input.language)
|
|
list_extend(contents, split_lines(input.value or ''))
|
|
table.insert(contents, "```")
|
|
-- By deduction, this must be MarkedString[]
|
|
else
|
|
-- Use our existing logic to handle MarkedString
|
|
for _, marked_string in ipairs(input) do
|
|
M.convert_input_to_markdown_lines(marked_string, contents)
|
|
end
|
|
end
|
|
end
|
|
if (contents[1] == '' or contents[1] == nil) and #contents == 1 then
|
|
return {}
|
|
end
|
|
return contents
|
|
end
|
|
|
|
--- Converts `textDocument/SignatureHelp` response to markdown lines.
|
|
---
|
|
--@param signature_help Response of `textDocument/SignatureHelp`
|
|
--@returns list of lines of converted markdown.
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#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.parameters and #signature.parameters > 0 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.parameters then
|
|
active_parameter = 0
|
|
end
|
|
local parameter = signature.parameters[active_parameter + 1]
|
|
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_to_markdown_lines(parameter.documentation, contents)
|
|
end
|
|
end
|
|
end
|
|
return contents
|
|
end
|
|
|
|
--- Creates a table with sensible default options for a floating window. The
|
|
--- table can be passed to |nvim_open_win()|.
|
|
---
|
|
--@param width (number) window width (in character cells)
|
|
--@param height (number) window height (in character cells)
|
|
--@param opts (table, optional)
|
|
--@returns (table) Options
|
|
function M.make_floating_popup_options(width, height, opts)
|
|
validate {
|
|
opts = { opts, 't', true };
|
|
}
|
|
opts = opts or {}
|
|
validate {
|
|
["opts.offset_x"] = { opts.offset_x, 'n', true };
|
|
["opts.offset_y"] = { opts.offset_y, 'n', true };
|
|
}
|
|
|
|
local anchor = ''
|
|
local row, col
|
|
|
|
local lines_above = vim.fn.winline() - 1
|
|
local lines_below = vim.fn.winheight(0) - lines_above
|
|
|
|
if lines_above < lines_below then
|
|
anchor = anchor..'N'
|
|
height = math.min(lines_below, height)
|
|
row = 1
|
|
else
|
|
anchor = anchor..'S'
|
|
height = math.min(lines_above, height)
|
|
row = 0
|
|
end
|
|
|
|
if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
|
|
anchor = anchor..'W'
|
|
col = 0
|
|
else
|
|
anchor = anchor..'E'
|
|
col = 1
|
|
end
|
|
|
|
return {
|
|
anchor = anchor,
|
|
col = col + (opts.offset_x or 0),
|
|
height = height,
|
|
relative = 'cursor',
|
|
row = row + (opts.offset_y or 0),
|
|
style = 'minimal',
|
|
width = width,
|
|
}
|
|
end
|
|
|
|
--- Jumps to a location.
|
|
---
|
|
--@param location (`Location`|`LocationLink`)
|
|
--@returns `true` if the jump succeeded
|
|
function M.jump_to_location(location)
|
|
-- 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'"
|
|
|
|
-- 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)
|
|
api.nvim_buf_set_option(0, 'buflisted', true)
|
|
local range = location.range or location.targetSelectionRange
|
|
local row = range.start.line
|
|
local col = get_line_byte_from_position(0, range.start)
|
|
api.nvim_win_set_cursor(0, {row + 1, col})
|
|
return true
|
|
end
|
|
|
|
--- Previews a location in a floating window
|
|
---
|
|
--- behavior depends on type of location:
|
|
--- - for Location, range is shown (e.g., function definition)
|
|
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
|
|
---
|
|
--@param location a single `Location` or `LocationLink`
|
|
--@returns (bufnr,winnr) buffer and window number of floating window or nil
|
|
function M.preview_location(location)
|
|
-- location may be LocationLink or Location (more useful for the former)
|
|
local uri = location.targetUri or location.uri
|
|
if uri == nil then return end
|
|
local bufnr = vim.uri_to_bufnr(uri)
|
|
if not api.nvim_buf_is_loaded(bufnr) then
|
|
vim.fn.bufload(bufnr)
|
|
end
|
|
local range = location.targetRange or location.range
|
|
local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
|
|
local filetype = api.nvim_buf_get_option(bufnr, 'filetype')
|
|
return M.open_floating_preview(contents, filetype)
|
|
end
|
|
|
|
--@private
|
|
local function find_window_by_var(name, value)
|
|
for _, win in ipairs(api.nvim_list_wins()) do
|
|
if npcall(api.nvim_win_get_var, win, name) == value then
|
|
return win
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Enters/leaves the focusable window associated with the current buffer via the
|
|
--window - variable `unique_name`. If no such window exists, run the function
|
|
--{fn}.
|
|
---
|
|
--@param unique_name (string) Window variable
|
|
--@param fn (function) should return create a new window and return a tuple of
|
|
---({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid
|
|
---buffer id, the newly created window will be the new focus associated with
|
|
---the current buffer via the tag `unique_name`.
|
|
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
|
|
function M.focusable_float(unique_name, fn)
|
|
-- Go back to previous window if we are in a focusable one
|
|
if npcall(api.nvim_win_get_var, 0, unique_name) then
|
|
return api.nvim_command("wincmd p")
|
|
end
|
|
local bufnr = api.nvim_get_current_buf()
|
|
do
|
|
local win = find_window_by_var(unique_name, bufnr)
|
|
if win and api.nvim_win_is_valid(win) and not vim.fn.pumvisible() then
|
|
api.nvim_set_current_win(win)
|
|
api.nvim_command("stopinsert")
|
|
return
|
|
end
|
|
end
|
|
local pbufnr, pwinnr = fn()
|
|
if pbufnr then
|
|
api.nvim_win_set_var(pwinnr, unique_name, bufnr)
|
|
return pbufnr, pwinnr
|
|
end
|
|
end
|
|
|
|
--- Focuses/unfocuses the floating preview window associated with the current
|
|
--- buffer via the window variable `unique_name`. If no such preview window
|
|
--- exists, makes a new one.
|
|
---
|
|
--@param unique_name (string) Window variable
|
|
--@param fn (function) The return values of this function will be passed
|
|
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
|
|
---floating window should be created
|
|
function M.focusable_preview(unique_name, fn)
|
|
return M.focusable_float(unique_name, function()
|
|
return M.open_floating_preview(fn())
|
|
end)
|
|
end
|
|
|
|
--- Trims empty lines from input and pad left and right with spaces
|
|
---
|
|
---@param contents table of lines to trim and pad
|
|
---@param opts dictionary with optional fields
|
|
--- - pad_left number of columns to pad contents at left (default 1)
|
|
--- - pad_right number of columns to pad contents at right (default 1)
|
|
--- - pad_top number of lines to pad contents at top (default 0)
|
|
--- - pad_bottom number of lines to pad contents at bottom (default 0)
|
|
---@return contents table of trimmed and padded lines
|
|
function M._trim_and_pad(contents, opts)
|
|
validate {
|
|
contents = { contents, 't' };
|
|
opts = { opts, 't', true };
|
|
}
|
|
opts = opts or {}
|
|
local left_padding = (" "):rep(opts.pad_left or 1)
|
|
local right_padding = (" "):rep(opts.pad_right or 1)
|
|
contents = M.trim_empty_lines(contents)
|
|
for i, line in ipairs(contents) do
|
|
contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding)
|
|
end
|
|
if opts.pad_top then
|
|
for _ = 1, opts.pad_top do
|
|
table.insert(contents, 1, "")
|
|
end
|
|
end
|
|
if opts.pad_bottom then
|
|
for _ = 1, opts.pad_bottom do
|
|
table.insert(contents, "")
|
|
end
|
|
end
|
|
return contents
|
|
end
|
|
|
|
|
|
|
|
-- TODO: refactor to separate stripping/converting and make use of open_floating_preview
|
|
--
|
|
--- Converts markdown into syntax highlighted regions by stripping the code
|
|
--- blocks and converting them into highlighted code.
|
|
--- This will by default insert a blank line separator after those code block
|
|
--- regions to improve readability.
|
|
--- The result is shown in a floating preview.
|
|
---
|
|
---@param contents table of lines to show in window
|
|
---@param opts dictionary with optional fields
|
|
--- - height of floating window
|
|
--- - width of floating window
|
|
--- - wrap_at character to wrap at for computing height
|
|
--- - max_width maximal width of floating window
|
|
--- - max_height maximal height of floating window
|
|
--- - pad_left number of columns to pad contents at left
|
|
--- - pad_right number of columns to pad contents at right
|
|
--- - pad_top number of lines to pad contents at top
|
|
--- - pad_bottom number of lines to pad contents at bottom
|
|
--- - separator insert separator after code block
|
|
---@returns width,height size of float
|
|
function M.fancy_floating_markdown(contents, opts)
|
|
validate {
|
|
contents = { contents, 't' };
|
|
opts = { opts, 't', true };
|
|
}
|
|
opts = opts or {}
|
|
|
|
local stripped = {}
|
|
local highlights = {}
|
|
do
|
|
local i = 1
|
|
while i <= #contents do
|
|
local line = contents[i]
|
|
-- TODO(ashkan): use a more strict regex for filetype?
|
|
local ft = line:match("^```([a-zA-Z0-9_]*)$")
|
|
-- local ft = line:match("^```(.*)$")
|
|
-- TODO(ashkan): validate the filetype here.
|
|
if ft then
|
|
local start = #stripped
|
|
i = i + 1
|
|
while i <= #contents do
|
|
line = contents[i]
|
|
if line == "```" then
|
|
i = i + 1
|
|
break
|
|
end
|
|
table.insert(stripped, line)
|
|
i = i + 1
|
|
end
|
|
table.insert(highlights, {
|
|
ft = ft;
|
|
start = start + 1;
|
|
finish = #stripped + 1 - 1;
|
|
})
|
|
else
|
|
table.insert(stripped, line)
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
-- Clean up and add padding
|
|
stripped = M._trim_and_pad(stripped, opts)
|
|
|
|
-- Compute size of float needed to show (wrapped) lines
|
|
opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
|
|
local width, height = M._make_floating_popup_size(stripped, opts)
|
|
|
|
-- Insert blank line separator after code block
|
|
local insert_separator = opts.separator
|
|
if insert_separator == nil then insert_separator = true end
|
|
if insert_separator then
|
|
for i, h in ipairs(highlights) do
|
|
h.start = h.start + i - 1
|
|
h.finish = h.finish + i - 1
|
|
if h.finish + 1 <= #stripped then
|
|
table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
|
|
height = height + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Make the floating window.
|
|
local bufnr = api.nvim_create_buf(false, true)
|
|
local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
|
|
api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
|
|
|
-- Switch to the floating window to apply the syntax highlighting.
|
|
-- This is because the syntax command doesn't accept a target.
|
|
local cwin = vim.api.nvim_get_current_win()
|
|
vim.api.nvim_set_current_win(winnr)
|
|
|
|
vim.cmd("ownsyntax markdown")
|
|
local idx = 1
|
|
--@private
|
|
local function apply_syntax_to_region(ft, start, finish)
|
|
if ft == '' then return end
|
|
local name = ft..idx
|
|
idx = idx + 1
|
|
local lang = "@"..ft:upper()
|
|
-- TODO(ashkan): better validation before this.
|
|
if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
|
|
return
|
|
end
|
|
vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang))
|
|
end
|
|
-- Previous highlight region.
|
|
-- TODO(ashkan): this wasn't working for some reason, but I would like to
|
|
-- make sure that regions between code blocks are definitely markdown.
|
|
-- local ph = {start = 0; finish = 1;}
|
|
for _, h in ipairs(highlights) do
|
|
-- apply_syntax_to_region('markdown', ph.finish, h.start)
|
|
apply_syntax_to_region(h.ft, h.start, h.finish)
|
|
-- ph = h
|
|
end
|
|
|
|
vim.api.nvim_set_current_win(cwin)
|
|
return bufnr, winnr
|
|
end
|
|
|
|
--- Creates autocommands to close a preview window when events happen.
|
|
---
|
|
--@param events (table) list of events
|
|
--@param winnr (number) window id of preview window
|
|
--@see |autocmd-events|
|
|
function M.close_preview_autocmd(events, winnr)
|
|
api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
|
|
end
|
|
|
|
--@internal
|
|
--- Computes size of float needed to show contents (with optional wrapping)
|
|
---
|
|
--@param contents table of lines to show in window
|
|
--@param opts dictionary with optional fields
|
|
-- - height of floating window
|
|
-- - width of floating window
|
|
-- - wrap_at character to wrap at for computing height
|
|
-- - max_width maximal width of floating window
|
|
-- - max_height maximal height of floating window
|
|
--@returns width,height size of float
|
|
function M._make_floating_popup_size(contents, opts)
|
|
validate {
|
|
contents = { contents, 't' };
|
|
opts = { opts, 't', true };
|
|
}
|
|
opts = opts or {}
|
|
|
|
local width = opts.width
|
|
local height = opts.height
|
|
local wrap_at = opts.wrap_at
|
|
local max_width = opts.max_width
|
|
local max_height = opts.max_height
|
|
local line_widths = {}
|
|
|
|
if not width then
|
|
width = 0
|
|
for i, line in ipairs(contents) do
|
|
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
|
|
line_widths[i] = vim.fn.strdisplaywidth(line)
|
|
width = math.max(line_widths[i], width)
|
|
end
|
|
end
|
|
if max_width then
|
|
width = math.min(width, max_width)
|
|
wrap_at = math.min(wrap_at or max_width, max_width)
|
|
end
|
|
|
|
if not height then
|
|
height = #contents
|
|
if wrap_at and width >= wrap_at then
|
|
height = 0
|
|
if vim.tbl_isempty(line_widths) then
|
|
for _, line in ipairs(contents) do
|
|
local line_width = vim.fn.strdisplaywidth(line)
|
|
height = height + math.ceil(line_width/wrap_at)
|
|
end
|
|
else
|
|
for i = 1, #contents do
|
|
height = height + math.max(1, math.ceil(line_widths[i]/wrap_at))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if max_height then
|
|
height = math.min(height, max_height)
|
|
end
|
|
|
|
return width, height
|
|
end
|
|
|
|
--- Shows contents in a floating window.
|
|
---
|
|
--@param contents table of lines to show in window
|
|
--@param filetype string of filetype to set for opened buffer
|
|
--@param opts dictionary with optional fields
|
|
-- - height of floating window
|
|
-- - width of floating window
|
|
-- - wrap_at character to wrap at for computing height
|
|
-- - max_width maximal width of floating window
|
|
-- - max_height maximal height of floating window
|
|
-- - pad_left number of columns to pad contents at left
|
|
-- - pad_right number of columns to pad contents at right
|
|
-- - pad_top number of lines to pad contents at top
|
|
-- - pad_bottom number of lines to pad contents at bottom
|
|
--@returns bufnr,winnr buffer and window number of the newly created floating
|
|
---preview window
|
|
function M.open_floating_preview(contents, filetype, opts)
|
|
validate {
|
|
contents = { contents, 't' };
|
|
filetype = { filetype, 's', true };
|
|
opts = { opts, 't', true };
|
|
}
|
|
opts = opts or {}
|
|
|
|
-- Clean up input: trim empty lines from the end, pad
|
|
contents = M._trim_and_pad(contents, opts)
|
|
|
|
-- Compute size of float needed to show (wrapped) lines
|
|
opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
|
|
local width, height = M._make_floating_popup_size(contents, opts)
|
|
|
|
local floating_bufnr = api.nvim_create_buf(false, true)
|
|
if filetype then
|
|
api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
|
|
end
|
|
local float_option = M.make_floating_popup_options(width, height, opts)
|
|
local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
|
|
if filetype == 'markdown' then
|
|
api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
|
|
end
|
|
api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
|
|
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
|
|
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
|
|
M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
|
|
return floating_bufnr, floating_winnr
|
|
end
|
|
|
|
-- TODO(remove-callbacks)
|
|
do
|
|
--@deprecated
|
|
function M.get_severity_highlight_name(severity)
|
|
warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.")
|
|
return vim.lsp.diagnostic._get_severity_highlight_name(severity)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_clear_diagnostics(bufnr, client_id)
|
|
warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear")
|
|
return vim.lsp.diagnostic.clear(bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.get_line_diagnostics()
|
|
warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics")
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local line_nr = api.nvim_win_get_cursor(0)[1] - 1
|
|
|
|
return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.show_line_diagnostics()
|
|
warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics")
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local line_nr = api.nvim_win_get_cursor(0)[1] - 1
|
|
|
|
return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id)
|
|
warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save")
|
|
return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_get_positions(bufnr, client_id)
|
|
warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get")
|
|
return vim.lsp.diagnostic.get(bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_underline(bufnr, diagnostics, client_id)
|
|
warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'")
|
|
return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id)
|
|
warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'")
|
|
return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_signs(bufnr, diagnostics, client_id)
|
|
warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'")
|
|
return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id)
|
|
end
|
|
|
|
--@deprecated
|
|
function M.buf_diagnostics_count(kind, client_id)
|
|
warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'")
|
|
return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind)
|
|
end
|
|
|
|
end
|
|
|
|
do --[[ References ]]
|
|
local reference_ns = api.nvim_create_namespace("vim_lsp_references")
|
|
|
|
--- Removes document highlights from a buffer.
|
|
---
|
|
--@param bufnr buffer id
|
|
function M.buf_clear_references(bufnr)
|
|
validate { bufnr = {bufnr, 'n', true} }
|
|
api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
|
|
end
|
|
|
|
--- Shows a list of document highlights for a certain buffer.
|
|
---
|
|
--@param bufnr buffer id
|
|
--@param references List of `DocumentHighlight` objects to highlight
|
|
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
|
|
end
|
|
end
|
|
|
|
local position_sort = sort_by_key(function(v)
|
|
return {v.start.line, v.start.character}
|
|
end)
|
|
|
|
--- Returns the items with the byte position calculated correctly and in sorted
|
|
--- order, for display in quickfix and location lists.
|
|
---
|
|
--@param locations (table) list of `Location`s or `LocationLink`s
|
|
--@returns (table) list of items
|
|
function M.locations_to_items(locations)
|
|
local items = {}
|
|
local grouped = setmetatable({}, {
|
|
__index = function(t, k)
|
|
local v = {}
|
|
rawset(t, k, v)
|
|
return v
|
|
end;
|
|
})
|
|
for _, d in ipairs(locations) do
|
|
-- locations may be Location or LocationLink
|
|
local uri = d.uri or d.targetUri
|
|
local range = d.range or d.targetSelectionRange
|
|
table.insert(grouped[uri], {start = range.start})
|
|
end
|
|
|
|
|
|
local keys = vim.tbl_keys(grouped)
|
|
table.sort(keys)
|
|
-- TODO(ashkan) I wish we could do this lazily.
|
|
for _, uri in ipairs(keys) do
|
|
local rows = grouped[uri]
|
|
table.sort(rows, position_sort)
|
|
local bufnr = vim.uri_to_bufnr(uri)
|
|
vim.fn.bufload(bufnr)
|
|
local filename = vim.uri_to_fname(uri)
|
|
for _, temp in ipairs(rows) do
|
|
local pos = temp.start
|
|
local row = pos.line
|
|
local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
|
|
local col = M.character_offset(bufnr, row, pos.character)
|
|
table.insert(items, {
|
|
filename = filename,
|
|
lnum = row + 1,
|
|
col = col + 1;
|
|
text = line;
|
|
})
|
|
end
|
|
end
|
|
return items
|
|
end
|
|
|
|
--- Fills current window's location list with given list of items.
|
|
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
|
|
---
|
|
--@param items (table) list of items
|
|
function M.set_loclist(items)
|
|
vim.fn.setloclist(0, {}, ' ', {
|
|
title = 'Language Server';
|
|
items = items;
|
|
})
|
|
end
|
|
|
|
--- Fills quickfix list with given list of items.
|
|
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
|
|
---
|
|
--@param items (table) list of items
|
|
function M.set_qflist(items)
|
|
vim.fn.setqflist({}, ' ', {
|
|
title = 'Language Server';
|
|
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
|
|
|
|
--- Converts symbols to quickfix list items.
|
|
---
|
|
--@param symbols DocumentSymbol[] or SymbolInformation[]
|
|
function M.symbols_to_items(symbols, bufnr)
|
|
--@private
|
|
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
|
|
|
|
--- Removes empty lines from the beginning and end.
|
|
--@param lines (table) list of lines to trim
|
|
--@returns (table) trimmed list of lines
|
|
function M.trim_empty_lines(lines)
|
|
local start = 1
|
|
for i = 1, #lines do
|
|
if #lines[i] > 0 then
|
|
start = i
|
|
break
|
|
end
|
|
end
|
|
local finish = 1
|
|
for i = #lines, 1, -1 do
|
|
if #lines[i] > 0 then
|
|
finish = i
|
|
break
|
|
end
|
|
end
|
|
return vim.list_extend({}, lines, start, finish)
|
|
end
|
|
|
|
--- Accepts markdown lines and tries to reduce them to a filetype if they
|
|
--- comprise just a single code block.
|
|
---
|
|
--- CAUTION: Modifies the input in-place!
|
|
---
|
|
--@param lines (table) list of lines
|
|
--@returns (string) filetype or 'markdown' if it was unchanged.
|
|
function M.try_trim_markdown_code_blocks(lines)
|
|
local language_id = lines[1]:match("^```(.*)")
|
|
if language_id then
|
|
local has_inner_code_fence = false
|
|
for i = 2, (#lines - 1) do
|
|
local line = lines[i]
|
|
if line:sub(1,3) == '```' then
|
|
has_inner_code_fence = true
|
|
break
|
|
end
|
|
end
|
|
-- No inner code fences + starting with code fence = hooray.
|
|
if not has_inner_code_fence then
|
|
table.remove(lines, 1)
|
|
table.remove(lines)
|
|
return language_id
|
|
end
|
|
end
|
|
return 'markdown'
|
|
end
|
|
|
|
local str_utfindex = vim.str_utfindex
|
|
--@private
|
|
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]
|
|
if not line then
|
|
return { line = 0; character = 0; }
|
|
end
|
|
col = str_utfindex(line, col)
|
|
return { line = row; character = col; }
|
|
end
|
|
|
|
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
|
|
---
|
|
--@returns `TextDocumentPositionParams` object
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
|
|
function M.make_position_params()
|
|
return {
|
|
textDocument = M.make_text_document_params();
|
|
position = make_position_param()
|
|
}
|
|
end
|
|
|
|
--- Using the current position in the current buffer, creates an object that
|
|
--- can be used as a building block for several LSP requests, such as
|
|
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
|
|
--- `textDocument/rangeFormatting`.
|
|
---
|
|
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
|
|
---`current_position`, end = `current_position` } }
|
|
function M.make_range_params()
|
|
local position = make_position_param()
|
|
return {
|
|
textDocument = M.make_text_document_params(),
|
|
range = { start = position; ["end"] = position; }
|
|
}
|
|
end
|
|
|
|
--- Using the given range in the current buffer, creates an object that
|
|
--- is similar to |vim.lsp.util.make_range_params()|.
|
|
---
|
|
--@param start_pos ({number, number}, optional) mark-indexed position.
|
|
---Defaults to the start of the last visual selection.
|
|
--@param end_pos ({number, number}, optional) mark-indexed position.
|
|
---Defaults to the end of the last visual selection.
|
|
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
|
|
---`start_position`, end = `end_position` } }
|
|
function M.make_given_range_params(start_pos, end_pos)
|
|
validate {
|
|
start_pos = {start_pos, 't', true};
|
|
end_pos = {end_pos, 't', true};
|
|
}
|
|
local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
|
|
local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
|
|
-- convert to 0-index
|
|
A[1] = A[1] - 1
|
|
B[1] = B[1] - 1
|
|
-- account for encoding.
|
|
if A[2] > 0 then
|
|
A = {A[1], M.character_offset(0, A[1], A[2])}
|
|
end
|
|
if B[2] > 0 then
|
|
B = {B[1], M.character_offset(0, B[1], B[2])}
|
|
end
|
|
return {
|
|
textDocument = M.make_text_document_params(),
|
|
range = {
|
|
start = {line = A[1], character = A[2]},
|
|
['end'] = {line = B[1], character = B[2]}
|
|
}
|
|
}
|
|
end
|
|
|
|
--- Creates a `TextDocumentIdentifier` object for the current buffer.
|
|
---
|
|
--@returns `TextDocumentIdentifier`
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
|
|
function M.make_text_document_params()
|
|
return { uri = vim.uri_from_bufnr(0) }
|
|
end
|
|
|
|
function M.make_workspace_params(added, removed)
|
|
return { event = { added = added; removed = removed; } }
|
|
end
|
|
--- Returns visual width of tabstop.
|
|
---
|
|
--@see |softtabstop|
|
|
--@param bufnr (optional, number): Buffer handle, defaults to current
|
|
--@returns (number) tabstop visual width
|
|
function M.get_effective_tabstop(bufnr)
|
|
validate { bufnr = {bufnr, 'n', true} }
|
|
local bo = bufnr and vim.bo[bufnr] or vim.bo
|
|
local sts = bo.softtabstop
|
|
return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
|
|
end
|
|
|
|
--- Creates a `FormattingOptions` object for the current buffer and cursor position.
|
|
---
|
|
--@param options Table with valid `FormattingOptions` entries
|
|
--@returns `FormattingOptions object
|
|
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
|
|
function M.make_formatting_params(options)
|
|
validate { options = {options, 't', true} }
|
|
options = vim.tbl_extend('keep', options or {}, {
|
|
tabSize = M.get_effective_tabstop();
|
|
insertSpaces = vim.bo.expandtab;
|
|
})
|
|
return {
|
|
textDocument = { uri = vim.uri_from_bufnr(0) };
|
|
options = options;
|
|
}
|
|
end
|
|
|
|
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
|
|
---
|
|
--@param buf buffer id (0 for current)
|
|
--@param row 0-indexed line
|
|
--@param col 0-indexed byte offset in line
|
|
--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
|
|
function M.character_offset(buf, row, col)
|
|
local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
|
|
-- If the col is past the EOL, use the line length.
|
|
if col > #line then
|
|
return str_utfindex(line)
|
|
end
|
|
return str_utfindex(line, col)
|
|
end
|
|
|
|
M._get_line_byte_from_position = get_line_byte_from_position
|
|
M._warn_once = warn_once
|
|
|
|
M.buf_versions = {}
|
|
|
|
return M
|
|
-- vim:sw=2 ts=2 et
|