mirror of
https://github.com/neovim/neovim.git
synced 2025-10-26 12:27:24 +00:00
lua LSP client: initial implementation (#11336)
Mainly configuration and RPC infrastructure can be considered "done". Specific requests and their callbacks will be improved later (and also served by plugins). There are also some TODO:s for the client itself, like incremental updates. Co-authored by at-tjdevries and at-h-michael, with many review/suggestion contributions.
This commit is contained in:
committed by
Björn Linse
parent
db436d5277
commit
00dc12c5d8
1055
runtime/lua/vim/lsp.lua
Normal file
1055
runtime/lua/vim/lsp.lua
Normal file
File diff suppressed because it is too large
Load Diff
296
runtime/lua/vim/lsp/builtin_callbacks.lua
Normal file
296
runtime/lua/vim/lsp/builtin_callbacks.lua
Normal file
@@ -0,0 +1,296 @@
|
||||
--- Implements the following default callbacks:
|
||||
--
|
||||
-- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks))
|
||||
--
|
||||
|
||||
-- textDocument/completion
|
||||
-- textDocument/declaration
|
||||
-- textDocument/definition
|
||||
-- textDocument/hover
|
||||
-- textDocument/implementation
|
||||
-- textDocument/publishDiagnostics
|
||||
-- textDocument/rename
|
||||
-- textDocument/signatureHelp
|
||||
-- textDocument/typeDefinition
|
||||
-- TODO codeLens/resolve
|
||||
-- TODO completionItem/resolve
|
||||
-- TODO documentLink/resolve
|
||||
-- TODO textDocument/codeAction
|
||||
-- TODO textDocument/codeLens
|
||||
-- TODO textDocument/documentHighlight
|
||||
-- TODO textDocument/documentLink
|
||||
-- TODO textDocument/documentSymbol
|
||||
-- TODO textDocument/formatting
|
||||
-- TODO textDocument/onTypeFormatting
|
||||
-- TODO textDocument/rangeFormatting
|
||||
-- TODO textDocument/references
|
||||
-- window/logMessage
|
||||
-- window/showMessage
|
||||
|
||||
local log = require 'vim.lsp.log'
|
||||
local protocol = require 'vim.lsp.protocol'
|
||||
local util = require 'vim.lsp.util'
|
||||
local api = vim.api
|
||||
|
||||
local function split_lines(value)
|
||||
return vim.split(value, '\n', true)
|
||||
end
|
||||
|
||||
local builtin_callbacks = {}
|
||||
|
||||
-- textDocument/completion
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
||||
builtin_callbacks['textDocument/completion'] = function(_, _, result)
|
||||
if not result or vim.tbl_isempty(result) then
|
||||
return
|
||||
end
|
||||
local pos = api.nvim_win_get_cursor(0)
|
||||
local row, col = pos[1], pos[2]
|
||||
local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
|
||||
local line_to_cursor = line:sub(col+1)
|
||||
|
||||
local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor)
|
||||
local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$')
|
||||
local match_start, match_finish = match_result[2], match_result[3]
|
||||
|
||||
vim.fn.complete(col + 1 - (match_finish - match_start), matches)
|
||||
end
|
||||
|
||||
-- textDocument/rename
|
||||
builtin_callbacks['textDocument/rename'] = function(_, _, result)
|
||||
if not result then return end
|
||||
util.workspace_apply_workspace_edit(result)
|
||||
end
|
||||
|
||||
local function uri_to_bufnr(uri)
|
||||
return vim.fn.bufadd((vim.uri_to_fname(uri)))
|
||||
end
|
||||
|
||||
builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result)
|
||||
if not result then return end
|
||||
local uri = result.uri
|
||||
local bufnr = uri_to_bufnr(uri)
|
||||
if not bufnr then
|
||||
api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri))
|
||||
return
|
||||
end
|
||||
util.buf_clear_diagnostics(bufnr)
|
||||
util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
|
||||
util.buf_diagnostics_underline(bufnr, result.diagnostics)
|
||||
util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
|
||||
-- util.buf_loclist(bufnr, result.diagnostics)
|
||||
end
|
||||
|
||||
-- textDocument/hover
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
|
||||
-- @params MarkedString | MarkedString[] | MarkupContent
|
||||
builtin_callbacks['textDocument/hover'] = function(_, _, result)
|
||||
if result == nil or vim.tbl_isempty(result) then
|
||||
return
|
||||
end
|
||||
|
||||
if result.contents ~= nil then
|
||||
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
|
||||
if vim.tbl_isempty(markdown_lines) then
|
||||
markdown_lines = { 'No information available' }
|
||||
end
|
||||
util.open_floating_preview(markdown_lines, 'markdown')
|
||||
end
|
||||
end
|
||||
|
||||
builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result)
|
||||
if result == nil or vim.tbl_isempty(result) then return end
|
||||
-- TODO(ashkan) what to do with multiple locations?
|
||||
result = result[1]
|
||||
local bufnr = uri_to_bufnr(result.uri)
|
||||
assert(bufnr)
|
||||
local start = result.range.start
|
||||
local finish = result.range["end"]
|
||||
util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 })
|
||||
util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) })
|
||||
end
|
||||
|
||||
--- Convert SignatureHelp response to preview contents.
|
||||
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
|
||||
local function signature_help_to_preview_contents(input)
|
||||
if not input.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 = input.activeSignature or 0
|
||||
-- If the activeSignature is not inside the valid range, then clip it.
|
||||
if active_signature >= #input.signatures then
|
||||
active_signature = 0
|
||||
end
|
||||
local signature = input.signatures[active_signature + 1]
|
||||
if not signature then
|
||||
return
|
||||
end
|
||||
vim.list_extend(contents, split_lines(signature.label))
|
||||
if signature.documentation then
|
||||
util.convert_input_to_markdown_lines(signature.documentation, contents)
|
||||
end
|
||||
if input.parameters then
|
||||
local active_parameter = input.activeParameter or 0
|
||||
-- If the activeParameter is not inside the valid range, then clip it.
|
||||
if active_parameter >= #input.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
|
||||
util.convert_input_to_markdown_lines(parameter.documentation, contents)
|
||||
end
|
||||
end
|
||||
end
|
||||
return contents
|
||||
end
|
||||
|
||||
-- textDocument/signatureHelp
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp
|
||||
builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result)
|
||||
if result == nil or vim.tbl_isempty(result) then
|
||||
return
|
||||
end
|
||||
|
||||
-- TODO show empty popup when signatures is empty?
|
||||
if #result.signatures > 0 then
|
||||
local markdown_lines = signature_help_to_preview_contents(result)
|
||||
if vim.tbl_isempty(markdown_lines) then
|
||||
markdown_lines = { 'No signature available' }
|
||||
end
|
||||
util.open_floating_preview(markdown_lines, 'markdown')
|
||||
end
|
||||
end
|
||||
|
||||
local function update_tagstack()
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
local line = vim.fn.line('.')
|
||||
local col = vim.fn.col('.')
|
||||
local tagname = vim.fn.expand('<cWORD>')
|
||||
local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname }
|
||||
local winid = vim.fn.win_getid()
|
||||
local tagstack = vim.fn.gettagstack(winid)
|
||||
|
||||
local action
|
||||
|
||||
if tagstack.length == tagstack.curidx then
|
||||
action = 'r'
|
||||
tagstack.items[tagstack.curidx] = item
|
||||
elseif tagstack.length > tagstack.curidx then
|
||||
action = 'r'
|
||||
if tagstack.curidx > 1 then
|
||||
tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item)
|
||||
else
|
||||
tagstack.items = { item }
|
||||
end
|
||||
else
|
||||
action = 'a'
|
||||
tagstack.items = { item }
|
||||
end
|
||||
|
||||
tagstack.curidx = tagstack.curidx + 1
|
||||
vim.fn.settagstack(winid, tagstack, action)
|
||||
end
|
||||
|
||||
local function handle_location(result)
|
||||
-- We can sometimes get a list of locations, so set the first value as the
|
||||
-- only value we want to handle
|
||||
-- TODO(ashkan) was this correct^? We could use location lists.
|
||||
if result[1] ~= nil then
|
||||
result = result[1]
|
||||
end
|
||||
if result.uri == nil then
|
||||
api.nvim_err_writeln('[LSP] Could not find a valid location')
|
||||
return
|
||||
end
|
||||
local result_file = vim.uri_to_fname(result.uri)
|
||||
local bufnr = vim.fn.bufadd(result_file)
|
||||
update_tagstack()
|
||||
api.nvim_set_current_buf(bufnr)
|
||||
local start = result.range.start
|
||||
api.nvim_win_set_cursor(0, {start.line + 1, start.character})
|
||||
end
|
||||
|
||||
local function location_callback(_, method, result)
|
||||
if result == nil or vim.tbl_isempty(result) then
|
||||
local _ = log.info() and log.info(method, 'No location found')
|
||||
return nil
|
||||
end
|
||||
handle_location(result)
|
||||
return true
|
||||
end
|
||||
|
||||
local location_callbacks = {
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration
|
||||
'textDocument/declaration';
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition
|
||||
'textDocument/definition';
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation
|
||||
'textDocument/implementation';
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition
|
||||
'textDocument/typeDefinition';
|
||||
}
|
||||
|
||||
for _, location_method in ipairs(location_callbacks) do
|
||||
builtin_callbacks[location_method] = location_callback
|
||||
end
|
||||
|
||||
local function log_message(_, _, result, client_id)
|
||||
local message_type = result.type
|
||||
local message = result.message
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local client_name = client and client.name or string.format("id=%d", client_id)
|
||||
if not client then
|
||||
api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name))
|
||||
end
|
||||
if message_type == protocol.MessageType.Error then
|
||||
-- Might want to not use err_writeln,
|
||||
-- but displaying a message with red highlights or something
|
||||
api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message))
|
||||
else
|
||||
local message_type_name = protocol.MessageType[message_type]
|
||||
api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
builtin_callbacks['window/showMessage'] = log_message
|
||||
builtin_callbacks['window/logMessage'] = log_message
|
||||
|
||||
-- Add boilerplate error validation and logging for all of these.
|
||||
for k, fn in pairs(builtin_callbacks) do
|
||||
builtin_callbacks[k] = function(err, method, params, client_id)
|
||||
local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err })
|
||||
if err then
|
||||
error(tostring(err))
|
||||
end
|
||||
return fn(err, method, params, client_id)
|
||||
end
|
||||
end
|
||||
|
||||
return builtin_callbacks
|
||||
-- vim:sw=2 ts=2 et
|
||||
95
runtime/lua/vim/lsp/log.lua
Normal file
95
runtime/lua/vim/lsp/log.lua
Normal file
@@ -0,0 +1,95 @@
|
||||
-- Logger for language client plugin.
|
||||
|
||||
local log = {}
|
||||
|
||||
-- Log level dictionary with reverse lookup as well.
|
||||
--
|
||||
-- Can be used to lookup the number from the name or the name from the number.
|
||||
-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
|
||||
-- Level numbers begin with 'trace' at 0
|
||||
log.levels = {
|
||||
TRACE = 0;
|
||||
DEBUG = 1;
|
||||
INFO = 2;
|
||||
WARN = 3;
|
||||
ERROR = 4;
|
||||
-- FATAL = 4;
|
||||
}
|
||||
|
||||
-- Default log level is warn.
|
||||
local current_log_level = log.levels.WARN
|
||||
local log_date_format = "%FT%H:%M:%SZ%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')
|
||||
|
||||
--- Return the log filename.
|
||||
function log.get_filename()
|
||||
return logfilename
|
||||
end
|
||||
|
||||
vim.fn.mkdir(vim.fn.stdpath('data'), "p")
|
||||
local logfile = assert(io.open(logfilename, "a+"))
|
||||
for level, levelnr in pairs(log.levels) do
|
||||
-- Also export the log level on the root object.
|
||||
log[level] = levelnr
|
||||
-- Set the lowercase name as the main use function.
|
||||
-- If called without arguments, it will check whether the log level is
|
||||
-- greater than or equal to this one. When called with arguments, it will
|
||||
-- log at that level (if applicable, it is checked either way).
|
||||
--
|
||||
-- Recommended usage:
|
||||
-- ```
|
||||
-- local _ = log.warn() and log.warn("123")
|
||||
-- ```
|
||||
--
|
||||
-- This way you can avoid string allocations if the log level isn't high enough.
|
||||
log[level:lower()] = function(...)
|
||||
local argc = select("#", ...)
|
||||
if levelnr < current_log_level then return false end
|
||||
if argc == 0 then return true end
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
|
||||
local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
|
||||
for i = 1, argc do
|
||||
local arg = select(i, ...)
|
||||
if arg == nil then
|
||||
table.insert(parts, "nil")
|
||||
else
|
||||
table.insert(parts, vim.inspect(arg, {newline=''}))
|
||||
end
|
||||
end
|
||||
logfile:write(table.concat(parts, '\t'), "\n")
|
||||
logfile:flush()
|
||||
end
|
||||
end
|
||||
-- Add some space to make it easier to distinguish different neovim runs.
|
||||
logfile:write("\n")
|
||||
end
|
||||
|
||||
-- This is put here on purpose after the loop above so that it doesn't
|
||||
-- interfere with iterating the levels
|
||||
vim.tbl_add_reverse_lookup(log.levels)
|
||||
|
||||
function log.set_level(level)
|
||||
if type(level) == 'string' then
|
||||
current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
|
||||
else
|
||||
assert(type(level) == 'number', "level must be a number or string")
|
||||
assert(log.levels[level], string.format("Invalid log level: %d", level))
|
||||
current_log_level = level
|
||||
end
|
||||
end
|
||||
|
||||
-- Return whether the level is sufficient for logging.
|
||||
-- @param level number log level
|
||||
function log.should_log(level)
|
||||
return level >= current_log_level
|
||||
end
|
||||
|
||||
return log
|
||||
-- vim:sw=2 ts=2 et
|
||||
936
runtime/lua/vim/lsp/protocol.lua
Normal file
936
runtime/lua/vim/lsp/protocol.lua
Normal file
@@ -0,0 +1,936 @@
|
||||
-- Protocol for the Microsoft Language Server Protocol (mslsp)
|
||||
|
||||
local protocol = {}
|
||||
|
||||
local function ifnil(a, b)
|
||||
if a == nil then return b end
|
||||
return a
|
||||
end
|
||||
|
||||
|
||||
--[=[
|
||||
-- Useful for interfacing with:
|
||||
-- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md
|
||||
-- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
|
||||
function transform_schema_comments()
|
||||
nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]]
|
||||
nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]]
|
||||
end
|
||||
function transform_schema_to_table()
|
||||
transform_schema_comments()
|
||||
nvim.command [[silent! '<,'>s/: \S\+//]]
|
||||
nvim.command [[silent! '<,'>s/export const //]]
|
||||
nvim.command [[silent! '<,'>s/export namespace \(\S*\)\s*{/protocol.\1 = {/]]
|
||||
nvim.command [[silent! '<,'>s/namespace \(\S*\)\s*{/protocol.\1 = {/]]
|
||||
end
|
||||
--]=]
|
||||
|
||||
local constants = {
|
||||
DiagnosticSeverity = {
|
||||
-- Reports an error.
|
||||
Error = 1;
|
||||
-- Reports a warning.
|
||||
Warning = 2;
|
||||
-- Reports an information.
|
||||
Information = 3;
|
||||
-- Reports a hint.
|
||||
Hint = 4;
|
||||
};
|
||||
|
||||
MessageType = {
|
||||
-- An error message.
|
||||
Error = 1;
|
||||
-- A warning message.
|
||||
Warning = 2;
|
||||
-- An information message.
|
||||
Info = 3;
|
||||
-- A log message.
|
||||
Log = 4;
|
||||
};
|
||||
|
||||
-- The file event type.
|
||||
FileChangeType = {
|
||||
-- The file got created.
|
||||
Created = 1;
|
||||
-- The file got changed.
|
||||
Changed = 2;
|
||||
-- The file got deleted.
|
||||
Deleted = 3;
|
||||
};
|
||||
|
||||
-- The kind of a completion entry.
|
||||
CompletionItemKind = {
|
||||
Text = 1;
|
||||
Method = 2;
|
||||
Function = 3;
|
||||
Constructor = 4;
|
||||
Field = 5;
|
||||
Variable = 6;
|
||||
Class = 7;
|
||||
Interface = 8;
|
||||
Module = 9;
|
||||
Property = 10;
|
||||
Unit = 11;
|
||||
Value = 12;
|
||||
Enum = 13;
|
||||
Keyword = 14;
|
||||
Snippet = 15;
|
||||
Color = 16;
|
||||
File = 17;
|
||||
Reference = 18;
|
||||
Folder = 19;
|
||||
EnumMember = 20;
|
||||
Constant = 21;
|
||||
Struct = 22;
|
||||
Event = 23;
|
||||
Operator = 24;
|
||||
TypeParameter = 25;
|
||||
};
|
||||
|
||||
-- How a completion was triggered
|
||||
CompletionTriggerKind = {
|
||||
-- Completion was triggered by typing an identifier (24x7 code
|
||||
-- complete), manual invocation (e.g Ctrl+Space) or via API.
|
||||
Invoked = 1;
|
||||
-- Completion was triggered by a trigger character specified by
|
||||
-- the `triggerCharacters` properties of the `CompletionRegistrationOptions`.
|
||||
TriggerCharacter = 2;
|
||||
-- Completion was re-triggered as the current completion list is incomplete.
|
||||
TriggerForIncompleteCompletions = 3;
|
||||
};
|
||||
|
||||
-- A document highlight kind.
|
||||
DocumentHighlightKind = {
|
||||
-- A textual occurrence.
|
||||
Text = 1;
|
||||
-- Read-access of a symbol, like reading a variable.
|
||||
Read = 2;
|
||||
-- Write-access of a symbol, like writing to a variable.
|
||||
Write = 3;
|
||||
};
|
||||
|
||||
-- A symbol kind.
|
||||
SymbolKind = {
|
||||
File = 1;
|
||||
Module = 2;
|
||||
Namespace = 3;
|
||||
Package = 4;
|
||||
Class = 5;
|
||||
Method = 6;
|
||||
Property = 7;
|
||||
Field = 8;
|
||||
Constructor = 9;
|
||||
Enum = 10;
|
||||
Interface = 11;
|
||||
Function = 12;
|
||||
Variable = 13;
|
||||
Constant = 14;
|
||||
String = 15;
|
||||
Number = 16;
|
||||
Boolean = 17;
|
||||
Array = 18;
|
||||
Object = 19;
|
||||
Key = 20;
|
||||
Null = 21;
|
||||
EnumMember = 22;
|
||||
Struct = 23;
|
||||
Event = 24;
|
||||
Operator = 25;
|
||||
TypeParameter = 26;
|
||||
};
|
||||
|
||||
-- Represents reasons why a text document is saved.
|
||||
TextDocumentSaveReason = {
|
||||
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
|
||||
-- or by an API call.
|
||||
Manual = 1;
|
||||
-- Automatic after a delay.
|
||||
AfterDelay = 2;
|
||||
-- When the editor lost focus.
|
||||
FocusOut = 3;
|
||||
};
|
||||
|
||||
ErrorCodes = {
|
||||
-- Defined by JSON RPC
|
||||
ParseError = -32700;
|
||||
InvalidRequest = -32600;
|
||||
MethodNotFound = -32601;
|
||||
InvalidParams = -32602;
|
||||
InternalError = -32603;
|
||||
serverErrorStart = -32099;
|
||||
serverErrorEnd = -32000;
|
||||
ServerNotInitialized = -32002;
|
||||
UnknownErrorCode = -32001;
|
||||
-- Defined by the protocol.
|
||||
RequestCancelled = -32800;
|
||||
ContentModified = -32801;
|
||||
};
|
||||
|
||||
-- Describes the content type that a client supports in various
|
||||
-- result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
|
||||
--
|
||||
-- Please note that `MarkupKinds` must not start with a `$`. This kinds
|
||||
-- are reserved for internal usage.
|
||||
MarkupKind = {
|
||||
-- Plain text is supported as a content format
|
||||
PlainText = 'plaintext';
|
||||
-- Markdown is supported as a content format
|
||||
Markdown = 'markdown';
|
||||
};
|
||||
|
||||
ResourceOperationKind = {
|
||||
-- Supports creating new files and folders.
|
||||
Create = 'create';
|
||||
-- Supports renaming existing files and folders.
|
||||
Rename = 'rename';
|
||||
-- Supports deleting existing files and folders.
|
||||
Delete = 'delete';
|
||||
};
|
||||
|
||||
FailureHandlingKind = {
|
||||
-- Applying the workspace change is simply aborted if one of the changes provided
|
||||
-- fails. All operations executed before the failing operation stay executed.
|
||||
Abort = 'abort';
|
||||
-- All operations are executed transactionally. That means they either all
|
||||
-- succeed or no changes at all are applied to the workspace.
|
||||
Transactional = 'transactional';
|
||||
-- If the workspace edit contains only textual file changes they are executed transactionally.
|
||||
-- If resource changes (create, rename or delete file) are part of the change the failure
|
||||
-- handling strategy is abort.
|
||||
TextOnlyTransactional = 'textOnlyTransactional';
|
||||
-- The client tries to undo the operations already executed. But there is no
|
||||
-- guarantee that this succeeds.
|
||||
Undo = 'undo';
|
||||
};
|
||||
|
||||
-- Known error codes for an `InitializeError`;
|
||||
InitializeError = {
|
||||
-- If the protocol version provided by the client can't be handled by the server.
|
||||
-- @deprecated This initialize error got replaced by client capabilities. There is
|
||||
-- no version handshake in version 3.0x
|
||||
unknownProtocolVersion = 1;
|
||||
};
|
||||
|
||||
-- Defines how the host (editor) should sync document changes to the language server.
|
||||
TextDocumentSyncKind = {
|
||||
-- Documents should not be synced at all.
|
||||
None = 0;
|
||||
-- Documents are synced by always sending the full content
|
||||
-- of the document.
|
||||
Full = 1;
|
||||
-- Documents are synced by sending the full content on open.
|
||||
-- After that only incremental updates to the document are
|
||||
-- send.
|
||||
Incremental = 2;
|
||||
};
|
||||
|
||||
WatchKind = {
|
||||
-- Interested in create events.
|
||||
Create = 1;
|
||||
-- Interested in change events
|
||||
Change = 2;
|
||||
-- Interested in delete events
|
||||
Delete = 4;
|
||||
};
|
||||
|
||||
-- Defines whether the insert text in a completion item should be interpreted as
|
||||
-- plain text or a snippet.
|
||||
InsertTextFormat = {
|
||||
-- The primary text to be inserted is treated as a plain string.
|
||||
PlainText = 1;
|
||||
-- The primary text to be inserted is treated as a snippet.
|
||||
--
|
||||
-- A snippet can define tab stops and placeholders with `$1`, `$2`
|
||||
-- and `${3:foo};`. `$0` defines the final tab stop, it defaults to
|
||||
-- the end of the snippet. Placeholders with equal identifiers are linked,
|
||||
-- that is typing in one will update others too.
|
||||
Snippet = 2;
|
||||
};
|
||||
|
||||
-- A set of predefined code action kinds
|
||||
CodeActionKind = {
|
||||
-- Empty kind.
|
||||
Empty = '';
|
||||
-- Base kind for quickfix actions
|
||||
QuickFix = 'quickfix';
|
||||
-- Base kind for refactoring actions
|
||||
Refactor = 'refactor';
|
||||
-- Base kind for refactoring extraction actions
|
||||
--
|
||||
-- Example extract actions:
|
||||
--
|
||||
-- - Extract method
|
||||
-- - Extract function
|
||||
-- - Extract variable
|
||||
-- - Extract interface from class
|
||||
-- - ...
|
||||
RefactorExtract = 'refactor.extract';
|
||||
-- Base kind for refactoring inline actions
|
||||
--
|
||||
-- Example inline actions:
|
||||
--
|
||||
-- - Inline function
|
||||
-- - Inline variable
|
||||
-- - Inline constant
|
||||
-- - ...
|
||||
RefactorInline = 'refactor.inline';
|
||||
-- Base kind for refactoring rewrite actions
|
||||
--
|
||||
-- Example rewrite actions:
|
||||
--
|
||||
-- - Convert JavaScript function to class
|
||||
-- - Add or remove parameter
|
||||
-- - Encapsulate field
|
||||
-- - Make method static
|
||||
-- - Move method to base class
|
||||
-- - ...
|
||||
RefactorRewrite = 'refactor.rewrite';
|
||||
-- Base kind for source actions
|
||||
--
|
||||
-- Source code actions apply to the entire file.
|
||||
Source = 'source';
|
||||
-- Base kind for an organize imports source action
|
||||
SourceOrganizeImports = 'source.organizeImports';
|
||||
};
|
||||
}
|
||||
|
||||
for k, v in pairs(constants) do
|
||||
vim.tbl_add_reverse_lookup(v)
|
||||
protocol[k] = v
|
||||
end
|
||||
|
||||
--[=[
|
||||
--Text document specific client capabilities.
|
||||
export interface TextDocumentClientCapabilities {
|
||||
synchronization?: {
|
||||
--Whether text document synchronization supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports sending will save notifications.
|
||||
willSave?: boolean;
|
||||
--The client supports sending a will save request and
|
||||
--waits for a response providing text edits which will
|
||||
--be applied to the document before it is saved.
|
||||
willSaveWaitUntil?: boolean;
|
||||
--The client supports did save notifications.
|
||||
didSave?: boolean;
|
||||
}
|
||||
--Capabilities specific to the `textDocument/completion`
|
||||
completion?: {
|
||||
--Whether completion supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports the following `CompletionItem` specific
|
||||
--capabilities.
|
||||
completionItem?: {
|
||||
--The client supports snippets as insert text.
|
||||
--
|
||||
--A snippet can define tab stops and placeholders with `$1`, `$2`
|
||||
--and `${3:foo}`. `$0` defines the final tab stop, it defaults to
|
||||
--the end of the snippet. Placeholders with equal identifiers are linked,
|
||||
--that is typing in one will update others too.
|
||||
snippetSupport?: boolean;
|
||||
--The client supports commit characters on a completion item.
|
||||
commitCharactersSupport?: boolean
|
||||
--The client supports the following content formats for the documentation
|
||||
--property. The order describes the preferred format of the client.
|
||||
documentationFormat?: MarkupKind[];
|
||||
--The client supports the deprecated property on a completion item.
|
||||
deprecatedSupport?: boolean;
|
||||
--The client supports the preselect property on a completion item.
|
||||
preselectSupport?: boolean;
|
||||
}
|
||||
completionItemKind?: {
|
||||
--The completion item kind values the client supports. When this
|
||||
--property exists the client also guarantees that it will
|
||||
--handle values outside its set gracefully and falls back
|
||||
--to a default value when unknown.
|
||||
--
|
||||
--If this property is not present the client only supports
|
||||
--the completion items kinds from `Text` to `Reference` as defined in
|
||||
--the initial version of the protocol.
|
||||
valueSet?: CompletionItemKind[];
|
||||
},
|
||||
--The client supports to send additional context information for a
|
||||
--`textDocument/completion` request.
|
||||
contextSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/hover`
|
||||
hover?: {
|
||||
--Whether hover supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports the follow content formats for the content
|
||||
--property. The order describes the preferred format of the client.
|
||||
contentFormat?: MarkupKind[];
|
||||
};
|
||||
--Capabilities specific to the `textDocument/signatureHelp`
|
||||
signatureHelp?: {
|
||||
--Whether signature help supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports the following `SignatureInformation`
|
||||
--specific properties.
|
||||
signatureInformation?: {
|
||||
--The client supports the follow content formats for the documentation
|
||||
--property. The order describes the preferred format of the client.
|
||||
documentationFormat?: MarkupKind[];
|
||||
--Client capabilities specific to parameter information.
|
||||
parameterInformation?: {
|
||||
--The client supports processing label offsets instead of a
|
||||
--simple label string.
|
||||
--
|
||||
--Since 3.14.0
|
||||
labelOffsetSupport?: boolean;
|
||||
}
|
||||
};
|
||||
};
|
||||
--Capabilities specific to the `textDocument/references`
|
||||
references?: {
|
||||
--Whether references supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/documentHighlight`
|
||||
documentHighlight?: {
|
||||
--Whether document highlight supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/documentSymbol`
|
||||
documentSymbol?: {
|
||||
--Whether document symbol supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--Specific capabilities for the `SymbolKind`.
|
||||
symbolKind?: {
|
||||
--The symbol kind values the client supports. When this
|
||||
--property exists the client also guarantees that it will
|
||||
--handle values outside its set gracefully and falls back
|
||||
--to a default value when unknown.
|
||||
--
|
||||
--If this property is not present the client only supports
|
||||
--the symbol kinds from `File` to `Array` as defined in
|
||||
--the initial version of the protocol.
|
||||
valueSet?: SymbolKind[];
|
||||
}
|
||||
--The client supports hierarchical document symbols.
|
||||
hierarchicalDocumentSymbolSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/formatting`
|
||||
formatting?: {
|
||||
--Whether formatting supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/rangeFormatting`
|
||||
rangeFormatting?: {
|
||||
--Whether range formatting supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/onTypeFormatting`
|
||||
onTypeFormatting?: {
|
||||
--Whether on type formatting supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/declaration`
|
||||
declaration?: {
|
||||
--Whether declaration supports dynamic registration. If this is set to `true`
|
||||
--the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
--return value for the corresponding server capability as well.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports additional metadata in the form of declaration links.
|
||||
--
|
||||
--Since 3.14.0
|
||||
linkSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/definition`.
|
||||
--
|
||||
--Since 3.14.0
|
||||
definition?: {
|
||||
--Whether definition supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports additional metadata in the form of definition links.
|
||||
linkSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/typeDefinition`
|
||||
--
|
||||
--Since 3.6.0
|
||||
typeDefinition?: {
|
||||
--Whether typeDefinition supports dynamic registration. If this is set to `true`
|
||||
--the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
--return value for the corresponding server capability as well.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports additional metadata in the form of definition links.
|
||||
--
|
||||
--Since 3.14.0
|
||||
linkSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/implementation`.
|
||||
--
|
||||
--Since 3.6.0
|
||||
implementation?: {
|
||||
--Whether implementation supports dynamic registration. If this is set to `true`
|
||||
--the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
--return value for the corresponding server capability as well.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports additional metadata in the form of definition links.
|
||||
--
|
||||
--Since 3.14.0
|
||||
linkSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/codeAction`
|
||||
codeAction?: {
|
||||
--Whether code action supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client support code action literals as a valid
|
||||
--response of the `textDocument/codeAction` request.
|
||||
--
|
||||
--Since 3.8.0
|
||||
codeActionLiteralSupport?: {
|
||||
--The code action kind is support with the following value
|
||||
--set.
|
||||
codeActionKind: {
|
||||
--The code action kind values the client supports. When this
|
||||
--property exists the client also guarantees that it will
|
||||
--handle values outside its set gracefully and falls back
|
||||
--to a default value when unknown.
|
||||
valueSet: CodeActionKind[];
|
||||
};
|
||||
};
|
||||
};
|
||||
--Capabilities specific to the `textDocument/codeLens`
|
||||
codeLens?: {
|
||||
--Whether code lens supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/documentLink`
|
||||
documentLink?: {
|
||||
--Whether document link supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `textDocument/documentColor` and the
|
||||
--`textDocument/colorPresentation` request.
|
||||
--
|
||||
--Since 3.6.0
|
||||
colorProvider?: {
|
||||
--Whether colorProvider supports dynamic registration. If this is set to `true`
|
||||
--the client supports the new `(ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
--return value for the corresponding server capability as well.
|
||||
dynamicRegistration?: boolean;
|
||||
}
|
||||
--Capabilities specific to the `textDocument/rename`
|
||||
rename?: {
|
||||
--Whether rename supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--The client supports testing for validity of rename operations
|
||||
--before execution.
|
||||
prepareSupport?: boolean;
|
||||
};
|
||||
--Capabilities specific to `textDocument/publishDiagnostics`.
|
||||
publishDiagnostics?: {
|
||||
--Whether the clients accepts diagnostics with related information.
|
||||
relatedInformation?: boolean;
|
||||
};
|
||||
--Capabilities specific to `textDocument/foldingRange` requests.
|
||||
--
|
||||
--Since 3.10.0
|
||||
foldingRange?: {
|
||||
--Whether implementation supports dynamic registration for folding range providers. If this is set to `true`
|
||||
--the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
--return value for the corresponding server capability as well.
|
||||
dynamicRegistration?: boolean;
|
||||
--The maximum number of folding ranges that the client prefers to receive per document. The value serves as a
|
||||
--hint, servers are free to follow the limit.
|
||||
rangeLimit?: number;
|
||||
--If set, the client signals that it only supports folding complete lines. If set, client will
|
||||
--ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange.
|
||||
lineFoldingOnly?: boolean;
|
||||
};
|
||||
}
|
||||
--]=]
|
||||
|
||||
--[=[
|
||||
--Workspace specific client capabilities.
|
||||
export interface WorkspaceClientCapabilities {
|
||||
--The client supports applying batch edits to the workspace by supporting
|
||||
--the request 'workspace/applyEdit'
|
||||
applyEdit?: boolean;
|
||||
--Capabilities specific to `WorkspaceEdit`s
|
||||
workspaceEdit?: {
|
||||
--The client supports versioned document changes in `WorkspaceEdit`s
|
||||
documentChanges?: boolean;
|
||||
--The resource operations the client supports. Clients should at least
|
||||
--support 'create', 'rename' and 'delete' files and folders.
|
||||
resourceOperations?: ResourceOperationKind[];
|
||||
--The failure handling strategy of a client if applying the workspace edit
|
||||
--fails.
|
||||
failureHandling?: FailureHandlingKind;
|
||||
};
|
||||
--Capabilities specific to the `workspace/didChangeConfiguration` notification.
|
||||
didChangeConfiguration?: {
|
||||
--Did change configuration notification supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `workspace/didChangeWatchedFiles` notification.
|
||||
didChangeWatchedFiles?: {
|
||||
--Did change watched files notification supports dynamic registration. Please note
|
||||
--that the current protocol doesn't support static configuration for file changes
|
||||
--from the server side.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--Capabilities specific to the `workspace/symbol` request.
|
||||
symbol?: {
|
||||
--Symbol request supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
--Specific capabilities for the `SymbolKind` in the `workspace/symbol` request.
|
||||
symbolKind?: {
|
||||
--The symbol kind values the client supports. When this
|
||||
--property exists the client also guarantees that it will
|
||||
--handle values outside its set gracefully and falls back
|
||||
--to a default value when unknown.
|
||||
--
|
||||
--If this property is not present the client only supports
|
||||
--the symbol kinds from `File` to `Array` as defined in
|
||||
--the initial version of the protocol.
|
||||
valueSet?: SymbolKind[];
|
||||
}
|
||||
};
|
||||
--Capabilities specific to the `workspace/executeCommand` request.
|
||||
executeCommand?: {
|
||||
--Execute command supports dynamic registration.
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
--The client has support for workspace folders.
|
||||
--
|
||||
--Since 3.6.0
|
||||
workspaceFolders?: boolean;
|
||||
--The client supports `workspace/configuration` requests.
|
||||
--
|
||||
--Since 3.6.0
|
||||
configuration?: boolean;
|
||||
}
|
||||
--]=]
|
||||
|
||||
function protocol.make_client_capabilities()
|
||||
return {
|
||||
textDocument = {
|
||||
synchronization = {
|
||||
dynamicRegistration = false;
|
||||
|
||||
-- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
|
||||
willSave = false;
|
||||
|
||||
-- TODO(ashkan) Implement textDocument/willSaveWaitUntil
|
||||
willSaveWaitUntil = false;
|
||||
|
||||
-- Send textDocument/didSave after saving (BufWritePost)
|
||||
didSave = true;
|
||||
};
|
||||
completion = {
|
||||
dynamicRegistration = false;
|
||||
completionItem = {
|
||||
|
||||
-- TODO(tjdevries): Is it possible to implement this in plain lua?
|
||||
snippetSupport = false;
|
||||
commitCharactersSupport = false;
|
||||
preselectSupport = false;
|
||||
deprecatedSupport = false;
|
||||
documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
|
||||
};
|
||||
completionItemKind = {
|
||||
valueSet = (function()
|
||||
local res = {}
|
||||
for k in pairs(protocol.CompletionItemKind) do
|
||||
if type(k) == 'number' then table.insert(res, k) end
|
||||
end
|
||||
return res
|
||||
end)();
|
||||
};
|
||||
|
||||
-- TODO(tjdevries): Implement this
|
||||
contextSupport = false;
|
||||
};
|
||||
hover = {
|
||||
dynamicRegistration = false;
|
||||
contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
|
||||
};
|
||||
signatureHelp = {
|
||||
dynamicRegistration = false;
|
||||
signatureInformation = {
|
||||
documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
|
||||
-- parameterInformation = {
|
||||
-- labelOffsetSupport = false;
|
||||
-- };
|
||||
};
|
||||
};
|
||||
references = {
|
||||
dynamicRegistration = false;
|
||||
};
|
||||
documentHighlight = {
|
||||
dynamicRegistration = false
|
||||
};
|
||||
-- documentSymbol = {
|
||||
-- dynamicRegistration = false;
|
||||
-- symbolKind = {
|
||||
-- valueSet = (function()
|
||||
-- local res = {}
|
||||
-- for k in pairs(protocol.SymbolKind) do
|
||||
-- if type(k) == 'string' then table.insert(res, k) end
|
||||
-- end
|
||||
-- return res
|
||||
-- end)();
|
||||
-- };
|
||||
-- hierarchicalDocumentSymbolSupport = false;
|
||||
-- };
|
||||
};
|
||||
workspace = nil;
|
||||
experimental = nil;
|
||||
}
|
||||
end
|
||||
|
||||
function protocol.make_text_document_position_params()
|
||||
local position = vim.api.nvim_win_get_cursor(0)
|
||||
return {
|
||||
textDocument = {
|
||||
uri = vim.uri_from_bufnr()
|
||||
};
|
||||
position = {
|
||||
line = position[1] - 1;
|
||||
character = position[2];
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
--[=[
|
||||
export interface DocumentFilter {
|
||||
--A language id, like `typescript`.
|
||||
language?: string;
|
||||
--A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
|
||||
scheme?: string;
|
||||
--A glob pattern, like `*.{ts,js}`.
|
||||
--
|
||||
--Glob patterns can have the following syntax:
|
||||
--- `*` to match one or more characters in a path segment
|
||||
--- `?` to match on one character in a path segment
|
||||
--- `**` to match any number of path segments, including none
|
||||
--- `{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)
|
||||
--- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
|
||||
--- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
|
||||
pattern?: string;
|
||||
}
|
||||
--]=]
|
||||
|
||||
--[[
|
||||
--Static registration options to be returned in the initialize request.
|
||||
interface StaticRegistrationOptions {
|
||||
--The id used to register the request. The id can be used to deregister
|
||||
--the request again. See also Registration#id.
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface DocumentFilter {
|
||||
--A language id, like `typescript`.
|
||||
language?: string;
|
||||
--A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
|
||||
scheme?: string;
|
||||
--A glob pattern, like `*.{ts,js}`.
|
||||
--
|
||||
--Glob patterns can have the following syntax:
|
||||
--- `*` to match one or more characters in a path segment
|
||||
--- `?` to match on one character in a path segment
|
||||
--- `**` to match any number of path segments, including none
|
||||
--- `{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)
|
||||
--- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
|
||||
--- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
|
||||
pattern?: string;
|
||||
}
|
||||
export type DocumentSelector = DocumentFilter[];
|
||||
export interface TextDocumentRegistrationOptions {
|
||||
--A document selector to identify the scope of the registration. If set to null
|
||||
--the document selector provided on the client side will be used.
|
||||
documentSelector: DocumentSelector | null;
|
||||
}
|
||||
|
||||
--Code Action options.
|
||||
export interface CodeActionOptions {
|
||||
--CodeActionKinds that this server may return.
|
||||
--
|
||||
--The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
|
||||
--may list out every specific kind they provide.
|
||||
codeActionKinds?: CodeActionKind[];
|
||||
}
|
||||
|
||||
interface ServerCapabilities {
|
||||
--Defines how text documents are synced. Is either a detailed structure defining each notification or
|
||||
--for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
|
||||
textDocumentSync?: TextDocumentSyncOptions | number;
|
||||
--The server provides hover support.
|
||||
hoverProvider?: boolean;
|
||||
--The server provides completion support.
|
||||
completionProvider?: CompletionOptions;
|
||||
--The server provides signature help support.
|
||||
signatureHelpProvider?: SignatureHelpOptions;
|
||||
--The server provides goto definition support.
|
||||
definitionProvider?: boolean;
|
||||
--The server provides Goto Type Definition support.
|
||||
--
|
||||
--Since 3.6.0
|
||||
typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
--The server provides Goto Implementation support.
|
||||
--
|
||||
--Since 3.6.0
|
||||
implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
--The server provides find references support.
|
||||
referencesProvider?: boolean;
|
||||
--The server provides document highlight support.
|
||||
documentHighlightProvider?: boolean;
|
||||
--The server provides document symbol support.
|
||||
documentSymbolProvider?: boolean;
|
||||
--The server provides workspace symbol support.
|
||||
workspaceSymbolProvider?: boolean;
|
||||
--The server provides code actions. The `CodeActionOptions` return type is only
|
||||
--valid if the client signals code action literal support via the property
|
||||
--`textDocument.codeAction.codeActionLiteralSupport`.
|
||||
codeActionProvider?: boolean | CodeActionOptions;
|
||||
--The server provides code lens.
|
||||
codeLensProvider?: CodeLensOptions;
|
||||
--The server provides document formatting.
|
||||
documentFormattingProvider?: boolean;
|
||||
--The server provides document range formatting.
|
||||
documentRangeFormattingProvider?: boolean;
|
||||
--The server provides document formatting on typing.
|
||||
documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
|
||||
--The server provides rename support. RenameOptions may only be
|
||||
--specified if the client states that it supports
|
||||
--`prepareSupport` in its initial `initialize` request.
|
||||
renameProvider?: boolean | RenameOptions;
|
||||
--The server provides document link support.
|
||||
documentLinkProvider?: DocumentLinkOptions;
|
||||
--The server provides color provider support.
|
||||
--
|
||||
--Since 3.6.0
|
||||
colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
--The server provides folding provider support.
|
||||
--
|
||||
--Since 3.10.0
|
||||
foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
--The server provides go to declaration support.
|
||||
--
|
||||
--Since 3.14.0
|
||||
declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
--The server provides execute command support.
|
||||
executeCommandProvider?: ExecuteCommandOptions;
|
||||
--Workspace specific server capabilities
|
||||
workspace?: {
|
||||
--The server supports workspace folder.
|
||||
--
|
||||
--Since 3.6.0
|
||||
workspaceFolders?: {
|
||||
* The server has support for workspace folders
|
||||
supported?: boolean;
|
||||
* Whether the server wants to receive workspace folder
|
||||
* change notifications.
|
||||
*
|
||||
* If a strings is provided the string is treated as a ID
|
||||
* under which the notification is registered on the client
|
||||
* side. The ID can be used to unregister for these events
|
||||
* using the `client/unregisterCapability` request.
|
||||
changeNotifications?: string | boolean;
|
||||
}
|
||||
}
|
||||
--Experimental server capabilities.
|
||||
experimental?: any;
|
||||
}
|
||||
--]]
|
||||
function protocol.resolve_capabilities(server_capabilities)
|
||||
local general_properties = {}
|
||||
local text_document_sync_properties
|
||||
do
|
||||
local TextDocumentSyncKind = protocol.TextDocumentSyncKind
|
||||
local textDocumentSync = server_capabilities.textDocumentSync
|
||||
if textDocumentSync == nil then
|
||||
-- Defaults if omitted.
|
||||
text_document_sync_properties = {
|
||||
text_document_open_close = false;
|
||||
text_document_did_change = TextDocumentSyncKind.None;
|
||||
-- text_document_did_change = false;
|
||||
text_document_will_save = false;
|
||||
text_document_will_save_wait_until = false;
|
||||
text_document_save = false;
|
||||
text_document_save_include_text = false;
|
||||
}
|
||||
elseif type(textDocumentSync) == 'number' then
|
||||
-- Backwards compatibility
|
||||
if not TextDocumentSyncKind[textDocumentSync] then
|
||||
return nil, "Invalid server TextDocumentSyncKind for textDocumentSync"
|
||||
end
|
||||
text_document_sync_properties = {
|
||||
text_document_open_close = true;
|
||||
text_document_did_change = textDocumentSync;
|
||||
text_document_will_save = false;
|
||||
text_document_will_save_wait_until = false;
|
||||
text_document_save = false;
|
||||
text_document_save_include_text = false;
|
||||
}
|
||||
elseif type(textDocumentSync) == 'table' then
|
||||
text_document_sync_properties = {
|
||||
text_document_open_close = ifnil(textDocumentSync.openClose, false);
|
||||
text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
|
||||
text_document_will_save = ifnil(textDocumentSync.willSave, false);
|
||||
text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
|
||||
text_document_save = ifnil(textDocumentSync.save, false);
|
||||
text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false);
|
||||
}
|
||||
else
|
||||
return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
|
||||
end
|
||||
end
|
||||
general_properties.hover = server_capabilities.hoverProvider or false
|
||||
general_properties.goto_definition = server_capabilities.definitionProvider or false
|
||||
general_properties.find_references = server_capabilities.referencesProvider or false
|
||||
general_properties.document_highlight = server_capabilities.documentHighlightProvider or false
|
||||
general_properties.document_symbol = server_capabilities.documentSymbolProvider or false
|
||||
general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false
|
||||
general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
|
||||
general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
|
||||
|
||||
if server_capabilities.codeActionProvider == nil then
|
||||
general_properties.code_action = false
|
||||
elseif type(server_capabilities.codeActionProvider) == 'boolean' then
|
||||
general_properties.code_action = server_capabilities.codeActionProvider
|
||||
elseif type(server_capabilities.codeActionProvider) == 'table' then
|
||||
-- TODO(ashkan) support CodeActionKind
|
||||
general_properties.code_action = false
|
||||
else
|
||||
error("The server sent invalid codeActionProvider")
|
||||
end
|
||||
|
||||
if server_capabilities.implementationProvider == nil then
|
||||
general_properties.implementation = false
|
||||
elseif type(server_capabilities.implementationProvider) == 'boolean' then
|
||||
general_properties.implementation = server_capabilities.implementationProvider
|
||||
elseif type(server_capabilities.implementationProvider) == 'table' then
|
||||
-- TODO(ashkan) support more detailed implementation options.
|
||||
general_properties.implementation = false
|
||||
else
|
||||
error("The server sent invalid implementationProvider")
|
||||
end
|
||||
|
||||
local signature_help_properties
|
||||
if server_capabilities.signatureHelpProvider == nil then
|
||||
signature_help_properties = {
|
||||
signature_help = false;
|
||||
signature_help_trigger_characters = {};
|
||||
}
|
||||
elseif type(server_capabilities.signatureHelpProvider) == 'table' then
|
||||
signature_help_properties = {
|
||||
signature_help = true;
|
||||
-- The characters that trigger signature help automatically.
|
||||
signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {};
|
||||
}
|
||||
else
|
||||
error("The server sent invalid signatureHelpProvider")
|
||||
end
|
||||
|
||||
return vim.tbl_extend("error"
|
||||
, text_document_sync_properties
|
||||
, signature_help_properties
|
||||
, general_properties
|
||||
)
|
||||
end
|
||||
|
||||
return protocol
|
||||
-- vim:sw=2 ts=2 et
|
||||
451
runtime/lua/vim/lsp/rpc.lua
Normal file
451
runtime/lua/vim/lsp/rpc.lua
Normal file
@@ -0,0 +1,451 @@
|
||||
local uv = vim.loop
|
||||
local log = require('vim.lsp.log')
|
||||
local protocol = require('vim.lsp.protocol')
|
||||
local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
|
||||
|
||||
-- TODO replace with a better implementation.
|
||||
local function json_encode(data)
|
||||
local status, result = pcall(vim.fn.json_encode, data)
|
||||
if status then
|
||||
return result
|
||||
else
|
||||
return nil, result
|
||||
end
|
||||
end
|
||||
local function json_decode(data)
|
||||
local status, result = pcall(vim.fn.json_decode, data)
|
||||
if status then
|
||||
return result
|
||||
else
|
||||
return nil, result
|
||||
end
|
||||
end
|
||||
|
||||
local function is_dir(filename)
|
||||
local stat = vim.loop.fs_stat(filename)
|
||||
return stat and stat.type == 'directory' or false
|
||||
end
|
||||
|
||||
local NIL = vim.NIL
|
||||
local function convert_NIL(v)
|
||||
if v == NIL then return nil end
|
||||
return v
|
||||
end
|
||||
|
||||
-- If a dictionary is passed in, turn it into a list of string of "k=v"
|
||||
-- Accepts a table which can be composed of k=v strings or map-like
|
||||
-- specification, such as:
|
||||
--
|
||||
-- ```
|
||||
-- {
|
||||
-- "PRODUCTION=false";
|
||||
-- "PATH=/usr/bin/";
|
||||
-- PORT = 123;
|
||||
-- HOST = "0.0.0.0";
|
||||
-- }
|
||||
-- ```
|
||||
--
|
||||
-- Non-string values will be cast with `tostring`
|
||||
local function force_env_list(final_env)
|
||||
if final_env then
|
||||
local env = final_env
|
||||
final_env = {}
|
||||
for k,v in pairs(env) do
|
||||
-- If it's passed in as a dict, then convert to list of "k=v"
|
||||
if type(k) == "string" then
|
||||
table.insert(final_env, k..'='..tostring(v))
|
||||
elseif type(v) == 'string' then
|
||||
table.insert(final_env, v)
|
||||
else
|
||||
-- TODO is this right or should I exception here?
|
||||
-- Try to coerce other values to string.
|
||||
table.insert(final_env, tostring(v))
|
||||
end
|
||||
end
|
||||
return final_env
|
||||
end
|
||||
end
|
||||
|
||||
local function format_message_with_content_length(encoded_message)
|
||||
return table.concat {
|
||||
'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
|
||||
encoded_message;
|
||||
}
|
||||
end
|
||||
|
||||
--- Parse an LSP Message's header
|
||||
-- @param header: The header to parse.
|
||||
local function parse_headers(header)
|
||||
if type(header) ~= 'string' then
|
||||
return nil
|
||||
end
|
||||
local headers = {}
|
||||
for line in vim.gsplit(header, '\r\n', true) do
|
||||
if line == '' then
|
||||
break
|
||||
end
|
||||
local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
|
||||
if key then
|
||||
key = key:lower():gsub('%-', '_')
|
||||
headers[key] = value
|
||||
else
|
||||
local _ = log.error() and log.error("invalid header line %q", line)
|
||||
error(string.format("invalid header line %q", line))
|
||||
end
|
||||
end
|
||||
headers.content_length = tonumber(headers.content_length)
|
||||
or error(string.format("Content-Length not found in headers. %q", header))
|
||||
return headers
|
||||
end
|
||||
|
||||
-- This is the start of any possible header patterns. The gsub converts it to a
|
||||
-- case insensitive pattern.
|
||||
local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
|
||||
|
||||
local function request_parser_loop()
|
||||
local buffer = ''
|
||||
while true do
|
||||
-- A message can only be complete if it has a double CRLF and also the full
|
||||
-- payload, so first let's check for the CRLFs
|
||||
local start, finish = buffer:find('\r\n\r\n', 1, true)
|
||||
-- Start parsing the headers
|
||||
if start then
|
||||
-- This is a workaround for servers sending initial garbage before
|
||||
-- sending headers, such as if a bash script sends stdout. It assumes
|
||||
-- that we know all of the headers ahead of time. At this moment, the
|
||||
-- only valid headers start with "Content-*", so that's the thing we will
|
||||
-- be searching for.
|
||||
-- TODO(ashkan) I'd like to remove this, but it seems permanent :(
|
||||
local buffer_start = buffer:find(header_start_pattern)
|
||||
local headers = parse_headers(buffer:sub(buffer_start, start-1))
|
||||
buffer = buffer:sub(finish+1)
|
||||
local content_length = headers.content_length
|
||||
-- Keep waiting for data until we have enough.
|
||||
while #buffer < content_length do
|
||||
buffer = buffer..(coroutine.yield()
|
||||
or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
|
||||
end
|
||||
local body = buffer:sub(1, content_length)
|
||||
buffer = buffer:sub(content_length + 1)
|
||||
-- Yield our data.
|
||||
buffer = buffer..(coroutine.yield(headers, body)
|
||||
or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
|
||||
else
|
||||
-- Get more data since we don't have enough.
|
||||
buffer = buffer..(coroutine.yield()
|
||||
or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local client_errors = vim.tbl_add_reverse_lookup {
|
||||
INVALID_SERVER_MESSAGE = 1;
|
||||
INVALID_SERVER_JSON = 2;
|
||||
NO_RESULT_CALLBACK_FOUND = 3;
|
||||
READ_ERROR = 4;
|
||||
NOTIFICATION_HANDLER_ERROR = 5;
|
||||
SERVER_REQUEST_HANDLER_ERROR = 6;
|
||||
SERVER_RESULT_CALLBACK_ERROR = 7;
|
||||
}
|
||||
|
||||
local function format_rpc_error(err)
|
||||
validate {
|
||||
err = { err, 't' };
|
||||
}
|
||||
local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid")
|
||||
local message_parts = {"RPC", code_name}
|
||||
if err.message then
|
||||
table.insert(message_parts, "message = ")
|
||||
table.insert(message_parts, string.format("%q", err.message))
|
||||
end
|
||||
if err.data then
|
||||
table.insert(message_parts, "data = ")
|
||||
table.insert(message_parts, vim.inspect(err.data))
|
||||
end
|
||||
return table.concat(message_parts, ' ')
|
||||
end
|
||||
|
||||
local function rpc_response_error(code, message, data)
|
||||
-- TODO should this error or just pick a sane error (like InternalError)?
|
||||
local code_name = assert(protocol.ErrorCodes[code], 'Invalid rpc error code')
|
||||
return setmetatable({
|
||||
code = code;
|
||||
message = message or code_name;
|
||||
data = data;
|
||||
}, {
|
||||
__tostring = format_rpc_error;
|
||||
})
|
||||
end
|
||||
|
||||
local default_handlers = {}
|
||||
function default_handlers.notification(method, params)
|
||||
local _ = log.debug() and log.debug('notification', method, params)
|
||||
end
|
||||
function default_handlers.server_request(method, params)
|
||||
local _ = log.debug() and log.debug('server_request', method, params)
|
||||
return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
|
||||
end
|
||||
function default_handlers.on_exit(code, signal)
|
||||
local _ = log.info() and log.info("client exit", { code = code, signal = signal })
|
||||
end
|
||||
function default_handlers.on_error(code, err)
|
||||
local _ = log.error() and log.error('client_error:', client_errors[code], err)
|
||||
end
|
||||
|
||||
--- Create and start an RPC client.
|
||||
-- @param cmd [
|
||||
local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_params)
|
||||
local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
|
||||
validate {
|
||||
cmd = { cmd, 's' };
|
||||
cmd_args = { cmd_args, 't' };
|
||||
handlers = { handlers, 't', true };
|
||||
}
|
||||
|
||||
if not (vim.fn.executable(cmd) == 1) then
|
||||
error(string.format("The given command %q is not executable.", cmd))
|
||||
end
|
||||
if handlers then
|
||||
local user_handlers = handlers
|
||||
handlers = {}
|
||||
for handle_name, default_handler in pairs(default_handlers) do
|
||||
local user_handler = user_handlers[handle_name]
|
||||
if user_handler then
|
||||
if type(user_handler) ~= 'function' then
|
||||
error(string.format("handler.%s must be a function", handle_name))
|
||||
end
|
||||
-- server_request is wrapped elsewhere.
|
||||
if not (handle_name == 'server_request'
|
||||
or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
|
||||
then
|
||||
user_handler = schedule_wrap(user_handler)
|
||||
end
|
||||
handlers[handle_name] = user_handler
|
||||
else
|
||||
handlers[handle_name] = default_handler
|
||||
end
|
||||
end
|
||||
else
|
||||
handlers = default_handlers
|
||||
end
|
||||
|
||||
local stdin = uv.new_pipe(false)
|
||||
local stdout = uv.new_pipe(false)
|
||||
local stderr = uv.new_pipe(false)
|
||||
|
||||
local message_index = 0
|
||||
local message_callbacks = {}
|
||||
|
||||
local handle, pid
|
||||
do
|
||||
local function onexit(code, signal)
|
||||
stdin:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
handle:close()
|
||||
-- Make sure that message_callbacks can be gc'd.
|
||||
message_callbacks = nil
|
||||
handlers.on_exit(code, signal)
|
||||
end
|
||||
local spawn_params = {
|
||||
args = cmd_args;
|
||||
stdio = {stdin, stdout, stderr};
|
||||
}
|
||||
if extra_spawn_params then
|
||||
spawn_params.cwd = extra_spawn_params.cwd
|
||||
if spawn_params.cwd then
|
||||
assert(is_dir(spawn_params.cwd), "cwd must be a directory")
|
||||
end
|
||||
spawn_params.env = force_env_list(extra_spawn_params.env)
|
||||
end
|
||||
handle, pid = uv.spawn(cmd, spawn_params, onexit)
|
||||
end
|
||||
|
||||
local function encode_and_send(payload)
|
||||
local _ = log.debug() and log.debug("rpc.send.payload", payload)
|
||||
if handle:is_closing() then return false end
|
||||
-- TODO(ashkan) remove this once we have a Lua json_encode
|
||||
schedule(function()
|
||||
local encoded = assert(json_encode(payload))
|
||||
stdin:write(format_message_with_content_length(encoded))
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
local function send_notification(method, params)
|
||||
local _ = log.debug() and log.debug("rpc.notify", method, params)
|
||||
return encode_and_send {
|
||||
jsonrpc = "2.0";
|
||||
method = method;
|
||||
params = params;
|
||||
}
|
||||
end
|
||||
|
||||
local function send_response(request_id, err, result)
|
||||
return encode_and_send {
|
||||
id = request_id;
|
||||
jsonrpc = "2.0";
|
||||
error = err;
|
||||
result = result;
|
||||
}
|
||||
end
|
||||
|
||||
local function send_request(method, params, callback)
|
||||
validate {
|
||||
callback = { callback, 'f' };
|
||||
}
|
||||
message_index = message_index + 1
|
||||
local message_id = message_index
|
||||
local result = encode_and_send {
|
||||
id = message_id;
|
||||
jsonrpc = "2.0";
|
||||
method = method;
|
||||
params = params;
|
||||
}
|
||||
if result then
|
||||
message_callbacks[message_id] = schedule_wrap(callback)
|
||||
return result, message_id
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
stderr:read_start(function(_err, chunk)
|
||||
if chunk then
|
||||
local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
|
||||
end
|
||||
end)
|
||||
|
||||
local function on_error(errkind, ...)
|
||||
assert(client_errors[errkind])
|
||||
-- TODO what to do if this fails?
|
||||
pcall(handlers.on_error, errkind, ...)
|
||||
end
|
||||
local function pcall_handler(errkind, status, head, ...)
|
||||
if not status then
|
||||
on_error(errkind, head, ...)
|
||||
return status, head
|
||||
end
|
||||
return status, head, ...
|
||||
end
|
||||
local function try_call(errkind, fn, ...)
|
||||
return pcall_handler(errkind, pcall(fn, ...))
|
||||
end
|
||||
|
||||
-- TODO periodically check message_callbacks for old requests past a certain
|
||||
-- time and log them. This would require storing the timestamp. I could call
|
||||
-- them with an error then, perhaps.
|
||||
|
||||
local function handle_body(body)
|
||||
local decoded, err = json_decode(body)
|
||||
if not decoded then
|
||||
on_error(client_errors.INVALID_SERVER_JSON, err)
|
||||
end
|
||||
local _ = log.debug() and log.debug("decoded", decoded)
|
||||
|
||||
if type(decoded.method) == 'string' and decoded.id then
|
||||
-- Server Request
|
||||
decoded.params = convert_NIL(decoded.params)
|
||||
-- Schedule here so that the users functions don't trigger an error and
|
||||
-- we can still use the result.
|
||||
schedule(function()
|
||||
local status, result
|
||||
status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
|
||||
handlers.server_request, decoded.method, decoded.params)
|
||||
local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
|
||||
if status then
|
||||
if not (result or err) then
|
||||
-- TODO this can be a problem if `null` is sent for result. needs vim.NIL
|
||||
error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
|
||||
end
|
||||
if err then
|
||||
assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
|
||||
local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
|
||||
err.message = err.message or code_name
|
||||
end
|
||||
else
|
||||
-- On an exception, result will contain the error message.
|
||||
err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
|
||||
result = nil
|
||||
end
|
||||
send_response(decoded.id, err, result)
|
||||
end)
|
||||
-- This works because we are expecting vim.NIL here
|
||||
elseif decoded.id and (decoded.result or decoded.error) then
|
||||
-- Server Result
|
||||
decoded.error = convert_NIL(decoded.error)
|
||||
decoded.result = convert_NIL(decoded.result)
|
||||
|
||||
-- We sent a number, so we expect a number.
|
||||
local result_id = tonumber(decoded.id)
|
||||
local callback = message_callbacks[result_id]
|
||||
if callback then
|
||||
message_callbacks[result_id] = nil
|
||||
validate {
|
||||
callback = { callback, 'f' };
|
||||
}
|
||||
if decoded.error then
|
||||
decoded.error = setmetatable(decoded.error, {
|
||||
__tostring = format_rpc_error;
|
||||
})
|
||||
end
|
||||
try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
|
||||
callback, decoded.error, decoded.result)
|
||||
else
|
||||
on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
|
||||
local _ = log.error() and log.error("No callback found for server response id "..result_id)
|
||||
end
|
||||
elseif type(decoded.method) == 'string' then
|
||||
-- Notification
|
||||
decoded.params = convert_NIL(decoded.params)
|
||||
try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
|
||||
handlers.notification, decoded.method, decoded.params)
|
||||
else
|
||||
-- Invalid server message
|
||||
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
|
||||
end
|
||||
end
|
||||
-- TODO(ashkan) remove this once we have a Lua json_decode
|
||||
handle_body = schedule_wrap(handle_body)
|
||||
|
||||
local request_parser = coroutine.wrap(request_parser_loop)
|
||||
request_parser()
|
||||
stdout:read_start(function(err, chunk)
|
||||
if err then
|
||||
-- TODO better handling. Can these be intermittent errors?
|
||||
on_error(client_errors.READ_ERROR, err)
|
||||
return
|
||||
end
|
||||
-- This should signal that we are done reading from the client.
|
||||
if not chunk then return end
|
||||
-- Flush anything in the parser by looping until we don't get a result
|
||||
-- anymore.
|
||||
while true do
|
||||
local headers, body = request_parser(chunk)
|
||||
-- If we successfully parsed, then handle the response.
|
||||
if headers then
|
||||
handle_body(body)
|
||||
-- Set chunk to empty so that we can call request_parser to get
|
||||
-- anything existing in the parser to flush.
|
||||
chunk = ''
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return {
|
||||
pid = pid;
|
||||
handle = handle;
|
||||
request = send_request;
|
||||
notify = send_notification;
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
start = create_and_start_client;
|
||||
rpc_response_error = rpc_response_error;
|
||||
format_rpc_error = format_rpc_error;
|
||||
client_errors = client_errors;
|
||||
}
|
||||
-- vim:sw=2 ts=2 et
|
||||
557
runtime/lua/vim/lsp/util.lua
Normal file
557
runtime/lua/vim/lsp/util.lua
Normal file
@@ -0,0 +1,557 @@
|
||||
local protocol = require 'vim.lsp.protocol'
|
||||
local validate = vim.validate
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
|
||||
local split = vim.split
|
||||
local function split_lines(value)
|
||||
return split(value, '\n', true)
|
||||
end
|
||||
|
||||
local list_extend = vim.list_extend
|
||||
|
||||
--- Find the longest shared prefix between prefix and word.
|
||||
-- e.g. remove_prefix("123tes", "testing") == "ting"
|
||||
local function remove_prefix(prefix, word)
|
||||
local max_prefix_length = math.min(#prefix, #word)
|
||||
local prefix_length = 0
|
||||
for i = 1, max_prefix_length do
|
||||
local current_line_suffix = prefix:sub(-i)
|
||||
local word_prefix = word:sub(1, i)
|
||||
if current_line_suffix == word_prefix then
|
||||
prefix_length = i
|
||||
end
|
||||
end
|
||||
return word:sub(prefix_length + 1)
|
||||
end
|
||||
|
||||
local function resolve_bufnr(bufnr)
|
||||
if bufnr == nil or bufnr == 0 then
|
||||
return api.nvim_get_current_buf()
|
||||
end
|
||||
return bufnr
|
||||
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
|
||||
|
||||
--- Apply the TextEdit response.
|
||||
-- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification
|
||||
function M.text_document_apply_text_edit(text_edit, bufnr)
|
||||
bufnr = resolve_bufnr(bufnr)
|
||||
local range = text_edit.range
|
||||
local start = range.start
|
||||
local finish = range['end']
|
||||
local new_lines = split_lines(text_edit.newText)
|
||||
if start.character == 0 and finish.character == 0 then
|
||||
api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines)
|
||||
return
|
||||
end
|
||||
api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0')
|
||||
error('apply_text_edit currently only supports character ranges starting at 0')
|
||||
return
|
||||
-- TODO test and finish this support for character ranges.
|
||||
-- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false)
|
||||
-- local suffix = lines[#lines]:sub(finish.character+2)
|
||||
-- local prefix = lines[1]:sub(start.character+2)
|
||||
-- new_lines[#new_lines] = new_lines[#new_lines]..suffix
|
||||
-- new_lines[1] = prefix..new_lines[1]
|
||||
-- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines)
|
||||
end
|
||||
|
||||
-- textDocument/completion response returns one of CompletionItem[], CompletionList or null.
|
||||
-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
||||
function M.extract_completion_items(result)
|
||||
if type(result) == 'table' and result.items then
|
||||
return result.items
|
||||
elseif result ~= nil then
|
||||
return result
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
--- Apply the TextDocumentEdit response.
|
||||
-- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification
|
||||
function M.text_document_apply_text_document_edit(text_document_edit, bufnr)
|
||||
-- local text_document = text_document_edit.textDocument
|
||||
-- TODO use text_document_version?
|
||||
-- local text_document_version = text_document.version
|
||||
|
||||
-- TODO technically, you could do this without doing multiple buf_get/set
|
||||
-- by getting the full region (smallest line and largest line) and doing
|
||||
-- the edits on the buffer, and then applying the buffer at the end.
|
||||
-- I'm not sure if that's better.
|
||||
for _, text_edit in ipairs(text_document_edit.edits) do
|
||||
M.text_document_apply_text_edit(text_edit, bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_current_line_to_cursor()
|
||||
local pos = api.nvim_win_get_cursor(0)
|
||||
local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1])
|
||||
return line:sub(pos[2]+1)
|
||||
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, line_prefix)
|
||||
local items = M.extract_completion_items(result)
|
||||
if vim.tbl_isempty(items) then
|
||||
return {}
|
||||
end
|
||||
-- Only initialize if we have some items.
|
||||
if not line_prefix then
|
||||
line_prefix = M.get_current_line_to_cursor()
|
||||
end
|
||||
|
||||
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 = completion_item.insertText or completion_item.label
|
||||
|
||||
-- Ref: `:h complete-items`
|
||||
table.insert(matches, {
|
||||
word = remove_prefix(line_prefix, word),
|
||||
abbr = completion_item.label,
|
||||
kind = protocol.CompletionItemKind[completion_item.kind] or '',
|
||||
menu = completion_item.detail or '',
|
||||
info = info,
|
||||
icase = 1,
|
||||
dup = 0,
|
||||
empty = 1,
|
||||
})
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
|
||||
function M.workspace_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.text_document_apply_text_document_edit(change)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if workspace_edit.changes == nil or #workspace_edit.changes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for uri, changes in pairs(workspace_edit.changes) do
|
||||
local fname = vim.uri_to_fname(uri)
|
||||
-- TODO improve this approach. Try to edit open buffers without switching.
|
||||
-- Not sure how to handle files which aren't open. This is deprecated
|
||||
-- anyway, so I guess it could be left as is.
|
||||
api.nvim_command('edit '..fname)
|
||||
for _, change in ipairs(changes) do
|
||||
M.text_document_apply_text_edit(change)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines
|
||||
-- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover
|
||||
-- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others.
|
||||
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 then
|
||||
return {}
|
||||
end
|
||||
return contents
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if vim.fn.winline() <= height then
|
||||
anchor = anchor..'N'
|
||||
row = 1
|
||||
else
|
||||
anchor = anchor..'S'
|
||||
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
|
||||
|
||||
function M.open_floating_preview(contents, filetype, opts)
|
||||
validate {
|
||||
contents = { contents, 't' };
|
||||
filetype = { filetype, 's', true };
|
||||
opts = { opts, 't', true };
|
||||
}
|
||||
|
||||
-- Trim empty lines from the end.
|
||||
for i = #contents, 1, -1 do
|
||||
if #contents[i] == 0 then
|
||||
table.remove(contents)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local width = 0
|
||||
local height = #contents
|
||||
for i, line in ipairs(contents) do
|
||||
-- Clean up the input and add left pad.
|
||||
line = " "..line:gsub("\r", "")
|
||||
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
|
||||
local line_width = vim.fn.strdisplaywidth(line)
|
||||
width = math.max(line_width, width)
|
||||
contents[i] = line
|
||||
end
|
||||
-- Add right padding of 1 each.
|
||||
width = width + 1
|
||||
|
||||
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_command("autocmd CursorMoved <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
|
||||
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?
|
||||
api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2])
|
||||
else
|
||||
api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1)
|
||||
for line = start[1] + 1, finish[1] - 1 do
|
||||
api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1)
|
||||
end
|
||||
api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2])
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local all_buffer_diagnostics = {}
|
||||
|
||||
local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
|
||||
|
||||
local default_severity_highlight = {
|
||||
[protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
|
||||
[protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
|
||||
[protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
|
||||
[protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
|
||||
}
|
||||
|
||||
local underline_highlight_name = "LspDiagnosticsUnderline"
|
||||
api.nvim_command(string.format("highlight %s gui=underline cterm=underline", underline_highlight_name))
|
||||
|
||||
local function find_color_rgb(color)
|
||||
local rgb_hex = api.nvim_get_color_by_name(color)
|
||||
validate { color = {color, function() return rgb_hex ~= -1 end, "valid color name"} }
|
||||
return rgb_hex
|
||||
end
|
||||
|
||||
--- Determine whether to use black or white text
|
||||
-- Ref: https://stackoverflow.com/a/1855903/837964
|
||||
-- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
|
||||
local function color_is_bright(r, g, b)
|
||||
-- Counting the perceptive luminance - human eye favors green color
|
||||
local luminance = (0.299*r + 0.587*g + 0.114*b)/255
|
||||
if luminance > 0.5 then
|
||||
return true -- Bright colors, black font
|
||||
else
|
||||
return false -- Dark colors, white font
|
||||
end
|
||||
end
|
||||
|
||||
local severity_highlights = {}
|
||||
|
||||
function M.set_severity_highlights(highlights)
|
||||
validate {highlights = {highlights, 't'}}
|
||||
for severity, default_color in pairs(default_severity_highlight) do
|
||||
local severity_name = protocol.DiagnosticSeverity[severity]
|
||||
local highlight_name = "LspDiagnostics"..severity_name
|
||||
local hi_info = highlights[severity] or default_color
|
||||
-- Try to fill in the foreground color with a sane default.
|
||||
if not hi_info.guifg and hi_info.guibg then
|
||||
-- TODO(ashkan) move this out when bitop is guaranteed to be included.
|
||||
local bit = require 'bit'
|
||||
local band, rshift = bit.band, bit.rshift
|
||||
local rgb = find_color_rgb(hi_info.guibg)
|
||||
local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
|
||||
hi_info.guifg = is_bright and "Black" or "White"
|
||||
end
|
||||
if not hi_info.ctermfg and hi_info.ctermbg then
|
||||
-- TODO(ashkan) move this out when bitop is guaranteed to be included.
|
||||
local bit = require 'bit'
|
||||
local band, rshift = bit.band, bit.rshift
|
||||
local rgb = find_color_rgb(hi_info.ctermbg)
|
||||
local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
|
||||
hi_info.ctermfg = is_bright and "Black" or "White"
|
||||
end
|
||||
local cmd_parts = {"highlight", highlight_name}
|
||||
for k, v in pairs(hi_info) do
|
||||
table.insert(cmd_parts, k.."="..v)
|
||||
end
|
||||
api.nvim_command(table.concat(cmd_parts, ' '))
|
||||
severity_highlights[severity] = highlight_name
|
||||
end
|
||||
end
|
||||
|
||||
function M.buf_clear_diagnostics(bufnr)
|
||||
validate { bufnr = {bufnr, 'n', true} }
|
||||
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
|
||||
api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
|
||||
end
|
||||
|
||||
-- Initialize with the defaults.
|
||||
M.set_severity_highlights(default_severity_highlight)
|
||||
|
||||
function M.get_severity_highlight_name(severity)
|
||||
return severity_highlights[severity]
|
||||
end
|
||||
|
||||
function M.show_line_diagnostics()
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
local line = api.nvim_win_get_cursor(0)[1] - 1
|
||||
-- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
|
||||
-- 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]
|
||||
if not buffer_diagnostics then return end
|
||||
local line_diagnostics = buffer_diagnostics[line]
|
||||
if not line_diagnostics then return end
|
||||
|
||||
for i, diagnostic in ipairs(line_diagnostics) do
|
||||
-- for i, mark in ipairs(marks) do
|
||||
-- local mark_id = mark[1]
|
||||
-- local diagnostic = buffer_diagnostics[mark_id]
|
||||
|
||||
-- TODO(ashkan) make format configurable?
|
||||
local prefix = string.format("%d. ", i)
|
||||
local hiname = severity_highlights[diagnostic.severity]
|
||||
local message_lines = split_lines(diagnostic.message)
|
||||
table.insert(lines, prefix..message_lines[1])
|
||||
table.insert(highlights, {#prefix + 1, hiname})
|
||||
for j = 2, #message_lines do
|
||||
table.insert(lines, message_lines[j])
|
||||
table.insert(highlights, {0, hiname})
|
||||
end
|
||||
end
|
||||
local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
|
||||
for i, hi in ipairs(highlights) do
|
||||
local prefixlen, hiname = unpack(hi)
|
||||
-- Start highlight after the prefix
|
||||
api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
|
||||
end
|
||||
return popup_bufnr, winnr
|
||||
end
|
||||
|
||||
function M.buf_diagnostics_save_positions(bufnr, diagnostics)
|
||||
validate {
|
||||
bufnr = {bufnr, 'n', true};
|
||||
diagnostics = {diagnostics, 't', true};
|
||||
}
|
||||
if not diagnostics then return end
|
||||
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
|
||||
|
||||
if not all_buffer_diagnostics[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
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
function M.buf_diagnostics_underline(bufnr, diagnostics)
|
||||
for _, diagnostic in ipairs(diagnostics) do
|
||||
local start = diagnostic.range.start
|
||||
local finish = diagnostic.range["end"]
|
||||
|
||||
-- TODO care about encoding here since this is in byte index?
|
||||
highlight_range(bufnr, diagnostic_ns, underline_highlight_name,
|
||||
{start.line, start.character},
|
||||
{finish.line, finish.character}
|
||||
)
|
||||
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)
|
||||
end
|
||||
buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
|
||||
if not buffer_line_diagnostics then
|
||||
return
|
||||
end
|
||||
for line, line_diags in pairs(buffer_line_diagnostics) do
|
||||
local virt_texts = {}
|
||||
for i = 1, #line_diags - 1 do
|
||||
table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]})
|
||||
end
|
||||
local last = line_diags[#line_diags]
|
||||
-- TODO(ashkan) use first line instead of subbing 2 spaces?
|
||||
table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
|
||||
api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.buf_loclist(bufnr, locations)
|
||||
local targetwin
|
||||
for _, winnr in ipairs(api.nvim_list_wins()) do
|
||||
local winbuf = api.nvim_win_get_buf(winnr)
|
||||
if winbuf == bufnr then
|
||||
targetwin = winnr
|
||||
break
|
||||
end
|
||||
end
|
||||
if not targetwin then return end
|
||||
|
||||
local items = {}
|
||||
local path = api.nvim_buf_get_name(bufnr)
|
||||
for _, d in ipairs(locations) do
|
||||
-- TODO: URL parsing here?
|
||||
local start = d.range.start
|
||||
table.insert(items, {
|
||||
filename = path,
|
||||
lnum = start.line + 1,
|
||||
col = start.character + 1,
|
||||
text = d.message,
|
||||
})
|
||||
end
|
||||
vim.fn.setloclist(targetwin, items, ' ', 'Language Server')
|
||||
end
|
||||
|
||||
return M
|
||||
-- vim:sw=2 ts=2 et
|
||||
@@ -98,6 +98,38 @@ function vim.split(s,sep,plain)
|
||||
return t
|
||||
end
|
||||
|
||||
--- Return a list of all keys used in a table.
|
||||
--- However, the order of the return table of keys is not guaranteed.
|
||||
---
|
||||
--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
|
||||
---
|
||||
--@param t Table
|
||||
--@returns list of keys
|
||||
function vim.tbl_keys(t)
|
||||
assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
|
||||
|
||||
local keys = {}
|
||||
for k, _ in pairs(t) do
|
||||
table.insert(keys, k)
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
--- Return a list of all values used in a table.
|
||||
--- However, the order of the return table of values is not guaranteed.
|
||||
---
|
||||
--@param t Table
|
||||
--@returns list of values
|
||||
function vim.tbl_values(t)
|
||||
assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
|
||||
|
||||
local values = {}
|
||||
for _, v in pairs(t) do
|
||||
table.insert(values, v)
|
||||
end
|
||||
return values
|
||||
end
|
||||
|
||||
--- Checks if a list-like (vector) table contains `value`.
|
||||
---
|
||||
--@param t Table to check
|
||||
@@ -114,6 +146,16 @@ function vim.tbl_contains(t, value)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Returns true if the table is empty, and contains no indexed or keyed values.
|
||||
--
|
||||
--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
|
||||
--
|
||||
--@param t Table to check
|
||||
function vim.tbl_isempty(t)
|
||||
assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
--- Merges two or more map-like tables.
|
||||
---
|
||||
--@see |extend()|
|
||||
@@ -145,13 +187,69 @@ function vim.tbl_extend(behavior, ...)
|
||||
return ret
|
||||
end
|
||||
|
||||
--- Deep compare values for equality
|
||||
function vim.deep_equal(a, b)
|
||||
if a == b then return true end
|
||||
if type(a) ~= type(b) then return false end
|
||||
if type(a) == 'table' then
|
||||
-- TODO improve this algorithm's performance.
|
||||
for k, v in pairs(a) do
|
||||
if not vim.deep_equal(v, b[k]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for k, v in pairs(b) do
|
||||
if not vim.deep_equal(v, a[k]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Add the reverse lookup values to an existing table.
|
||||
--- For example:
|
||||
--- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`
|
||||
--
|
||||
--Do note that it *modifies* the input.
|
||||
--@param o table The table to add the reverse to.
|
||||
function vim.tbl_add_reverse_lookup(o)
|
||||
local keys = vim.tbl_keys(o)
|
||||
for _, k in ipairs(keys) do
|
||||
local v = o[k]
|
||||
if o[v] then
|
||||
error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k)))
|
||||
end
|
||||
o[v] = k
|
||||
end
|
||||
return o
|
||||
end
|
||||
|
||||
--- Extends a list-like table with the values of another list-like table.
|
||||
---
|
||||
--NOTE: This *mutates* dst!
|
||||
--@see |extend()|
|
||||
---
|
||||
--@param dst The list which will be modified and appended to.
|
||||
--@param src The list from which values will be inserted.
|
||||
function vim.list_extend(dst, src)
|
||||
assert(type(dst) == 'table', "dst must be a table")
|
||||
assert(type(src) == 'table', "src must be a table")
|
||||
for _, v in ipairs(src) do
|
||||
table.insert(dst, v)
|
||||
end
|
||||
return dst
|
||||
end
|
||||
|
||||
--- Creates a copy of a list-like table such that any nested tables are
|
||||
--- "unrolled" and appended to the result.
|
||||
---
|
||||
--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
|
||||
---
|
||||
--@param t List-like table
|
||||
--@returns Flattened copy of the given list-like table.
|
||||
function vim.tbl_flatten(t)
|
||||
-- From https://github.com/premake/premake-core/blob/master/src/base/table.lua
|
||||
local result = {}
|
||||
local function _tbl_flatten(_t)
|
||||
local n = #_t
|
||||
@@ -168,6 +266,32 @@ function vim.tbl_flatten(t)
|
||||
return result
|
||||
end
|
||||
|
||||
-- Determine whether a Lua table can be treated as an array.
|
||||
---
|
||||
--@params Table
|
||||
--@returns true: A non-empty array, false: A non-empty table, nil: An empty table
|
||||
function vim.tbl_islist(t)
|
||||
if type(t) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
|
||||
local count = 0
|
||||
|
||||
for k, _ in pairs(t) do
|
||||
if type(k) == "number" then
|
||||
count = count + 1
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if count > 0 then
|
||||
return true
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
|
||||
---
|
||||
--@see https://www.lua.org/pil/20.2.html
|
||||
@@ -279,3 +403,4 @@ function vim.is_callable(f)
|
||||
end
|
||||
|
||||
return vim
|
||||
-- vim:sw=2 ts=2 et
|
||||
|
||||
89
runtime/lua/vim/uri.lua
Normal file
89
runtime/lua/vim/uri.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
--- TODO: This is implemented only for files now.
|
||||
-- https://tools.ietf.org/html/rfc3986
|
||||
-- https://tools.ietf.org/html/rfc2732
|
||||
-- https://tools.ietf.org/html/rfc2396
|
||||
|
||||
|
||||
local uri_decode
|
||||
do
|
||||
local schar = string.char
|
||||
local function hex_to_char(hex)
|
||||
return schar(tonumber(hex, 16))
|
||||
end
|
||||
uri_decode = function(str)
|
||||
return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char)
|
||||
end
|
||||
end
|
||||
|
||||
local uri_encode
|
||||
do
|
||||
local PATTERNS = {
|
||||
--- RFC 2396
|
||||
-- https://tools.ietf.org/html/rfc2396#section-2.2
|
||||
rfc2396 = "^A-Za-z0-9%-_.!~*'()";
|
||||
--- RFC 2732
|
||||
-- https://tools.ietf.org/html/rfc2732
|
||||
rfc2732 = "^A-Za-z0-9%-_.!~*'()[]";
|
||||
--- RFC 3986
|
||||
-- https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/";
|
||||
}
|
||||
local sbyte, tohex = string.byte
|
||||
if jit then
|
||||
tohex = require'bit'.tohex
|
||||
else
|
||||
tohex = function(b) return string.format("%02x", b) end
|
||||
end
|
||||
local function percent_encode_char(char)
|
||||
return "%"..tohex(sbyte(char), 2)
|
||||
end
|
||||
uri_encode = function(text, rfc)
|
||||
if not text then return end
|
||||
local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
|
||||
return text:gsub("(["..pattern.."])", percent_encode_char)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function is_windows_file_uri(uri)
|
||||
return uri:match('^file:///[a-zA-Z]:') ~= nil
|
||||
end
|
||||
|
||||
local function uri_from_fname(path)
|
||||
local volume_path, fname = path:match("^([a-zA-Z]:)(.*)")
|
||||
local is_windows = volume_path ~= nil
|
||||
if is_windows then
|
||||
path = volume_path..uri_encode(fname:gsub("\\", "/"))
|
||||
else
|
||||
path = uri_encode(path)
|
||||
end
|
||||
local uri_parts = {"file://"}
|
||||
if is_windows then
|
||||
table.insert(uri_parts, "/")
|
||||
end
|
||||
table.insert(uri_parts, path)
|
||||
return table.concat(uri_parts)
|
||||
end
|
||||
|
||||
local function uri_from_bufnr(bufnr)
|
||||
return uri_from_fname(vim.api.nvim_buf_get_name(bufnr))
|
||||
end
|
||||
|
||||
local function uri_to_fname(uri)
|
||||
-- TODO improve this.
|
||||
if is_windows_file_uri(uri) then
|
||||
uri = uri:gsub('^file:///', '')
|
||||
uri = uri:gsub('/', '\\')
|
||||
else
|
||||
uri = uri:gsub('^file://', '')
|
||||
end
|
||||
|
||||
return uri_decode(uri)
|
||||
end
|
||||
|
||||
return {
|
||||
uri_from_fname = uri_from_fname,
|
||||
uri_from_bufnr = uri_from_bufnr,
|
||||
uri_to_fname = uri_to_fname,
|
||||
}
|
||||
-- vim:sw=2 ts=2 et
|
||||
Reference in New Issue
Block a user