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:
parent
db436d5277
commit
00dc12c5d8
|
@ -0,0 +1,45 @@
|
|||
function! lsp#add_filetype_config(config) abort
|
||||
call luaeval('vim.lsp.add_filetype_config(_A)', a:config)
|
||||
endfunction
|
||||
|
||||
function! lsp#set_log_level(level) abort
|
||||
call luaeval('vim.lsp.set_log_level(_A)', a:level)
|
||||
endfunction
|
||||
|
||||
function! lsp#get_log_path() abort
|
||||
return luaeval('vim.lsp.get_log_path()')
|
||||
endfunction
|
||||
|
||||
function! lsp#omnifunc(findstart, base) abort
|
||||
return luaeval("vim.lsp.omnifunc(_A[1], _A[2])", [a:findstart, a:base])
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_hover() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/hover', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_declaration() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/declaration', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_definition() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/definition', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_signature_help() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/signatureHelp', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_type_definition() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/typeDefinition', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! lsp#text_document_implementation() abort
|
||||
lua vim.lsp.buf_request(nil, 'textDocument/implementation', vim.lsp.protocol.make_text_document_position_params())
|
||||
return ''
|
||||
endfunction
|
|
@ -0,0 +1,662 @@
|
|||
*lsp.txt* The Language Server Protocol
|
||||
|
||||
NVIM REFERENCE MANUAL
|
||||
|
||||
|
||||
Neovim Language Server Protocol (LSP) API
|
||||
|
||||
Neovim exposes a powerful API that conforms to Microsoft's published Language
|
||||
Server Protocol specification. The documentation can be found here:
|
||||
|
||||
https://microsoft.github.io/language-server-protocol/
|
||||
|
||||
|
||||
================================================================================
|
||||
*lsp-api*
|
||||
|
||||
Neovim exposes a API for the language server protocol. To get the real benefits
|
||||
of this API, a language server must be installed.
|
||||
Many examples can be found here:
|
||||
|
||||
https://microsoft.github.io/language-server-protocol/implementors/servers/
|
||||
|
||||
After installing a language server to your machine, you must let Neovim know
|
||||
how to start and interact with that language server.
|
||||
|
||||
To do so, you can either:
|
||||
- Use the |vim.lsp.add_filetype_config()|, which solves the common use-case of
|
||||
a single server for one or more filetypes. This can also be used from vim
|
||||
via |lsp#add_filetype_config()|.
|
||||
- Or |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. These are the
|
||||
backbone of the LSP API. These are easy to use enough for basic or more
|
||||
complex configurations such as in |lsp-advanced-js-example|.
|
||||
|
||||
================================================================================
|
||||
*lsp-filetype-config*
|
||||
|
||||
These are utilities specific to filetype based configurations.
|
||||
|
||||
*lsp#add_filetype_config()*
|
||||
*vim.lsp.add_filetype_config()*
|
||||
lsp#add_filetype_config({config}) for Vim.
|
||||
vim.lsp.add_filetype_config({config}) for Lua
|
||||
|
||||
These are functions which can be used to create a simple configuration which
|
||||
will start a language server for a list of filetypes based on the |FileType|
|
||||
event.
|
||||
It will lazily start start the server, meaning that it will only start once
|
||||
a matching filetype is encountered.
|
||||
|
||||
The {config} options are the same as |vim.lsp.start_client()|, but
|
||||
with a few additions and distinctions:
|
||||
|
||||
Additional parameters:~
|
||||
`filetype`
|
||||
{string} or {list} of filetypes to attach to.
|
||||
`name`
|
||||
A unique identifying string among all other servers configured with
|
||||
|vim.lsp.add_filetype_config|.
|
||||
|
||||
Differences:~
|
||||
`root_dir`
|
||||
Will default to |getcwd()| instead of being required.
|
||||
|
||||
NOTE: the function options in {config} like {config.on_init} are for Lua
|
||||
callbacks, not Vim callbacks.
|
||||
>
|
||||
" Go example
|
||||
call lsp#add_filetype_config({
|
||||
\ 'filetype': 'go',
|
||||
\ 'name': 'gopls',
|
||||
\ 'cmd': 'gopls'
|
||||
\ })
|
||||
" Python example
|
||||
call lsp#add_filetype_config({
|
||||
\ 'filetype': 'python',
|
||||
\ 'name': 'pyls',
|
||||
\ 'cmd': 'pyls'
|
||||
\ })
|
||||
" Rust example
|
||||
call lsp#add_filetype_config({
|
||||
\ 'filetype': 'rust',
|
||||
\ 'name': 'rls',
|
||||
\ 'cmd': 'rls',
|
||||
\ 'capabilities': {
|
||||
\ 'clippy_preference': 'on',
|
||||
\ 'all_targets': v:false,
|
||||
\ 'build_on_save': v:true,
|
||||
\ 'wait_to_build': 0
|
||||
\ }})
|
||||
<
|
||||
>
|
||||
-- From Lua
|
||||
vim.lsp.add_filetype_config {
|
||||
name = "clangd";
|
||||
filetype = {"c", "cpp"};
|
||||
cmd = "clangd -background-index";
|
||||
capabilities = {
|
||||
offsetEncoding = {"utf-8", "utf-16"};
|
||||
};
|
||||
on_init = vim.schedule_wrap(function(client, result)
|
||||
if result.offsetEncoding then
|
||||
client.offset_encoding = result.offsetEncoding
|
||||
end
|
||||
end)
|
||||
}
|
||||
<
|
||||
*vim.lsp.copy_filetype_config()*
|
||||
vim.lsp.copy_filetype_config({existing_name}, [{override_config}])
|
||||
|
||||
You can use this to copy an existing filetype configuration and change it by
|
||||
specifying {override_config} which will override any properties in the
|
||||
existing configuration. If you don't specify a new unique name with
|
||||
{override_config.name} then it will try to create one and return it.
|
||||
|
||||
Returns:~
|
||||
`name` the new configuration name.
|
||||
|
||||
*vim.lsp.get_filetype_client_by_name()*
|
||||
vim.lsp.get_filetype_client_by_name({name})
|
||||
|
||||
Use this to look up a client by its name created from
|
||||
|vim.lsp.add_filetype_config()|.
|
||||
|
||||
Returns nil if the client is not active or the name is not valid.
|
||||
|
||||
================================================================================
|
||||
*lsp-core-api*
|
||||
These are the core api functions for working with clients. You will mainly be
|
||||
using |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()| for operations
|
||||
and |vim.lsp.get_client_by_id()| to retrieve a client by its id after it has
|
||||
initialized (or {config.on_init}. see below)
|
||||
|
||||
*vim.lsp.start_client()*
|
||||
|
||||
vim.lsp.start_client({config})
|
||||
|
||||
The main function used for starting clients.
|
||||
Start a client and initialize it.
|
||||
|
||||
Its arguments are passed via a configuration object {config}.
|
||||
|
||||
Mandatory parameters:~
|
||||
|
||||
`root_dir`
|
||||
{string} specifying the directory where the LSP server will base
|
||||
as its rootUri on initialization.
|
||||
|
||||
`cmd`
|
||||
{string} or {list} which is the base command to execute for the LSP. A
|
||||
string will be run using |'shell'| and a list will be interpreted as a
|
||||
bare command with arguments passed. This is the same as |jobstart()|.
|
||||
|
||||
Optional parameters:~
|
||||
|
||||
`cmd_cwd`
|
||||
{string} specifying the directory to launch the `cmd` process. This is not
|
||||
related to `root_dir`.
|
||||
By default, |getcwd()| is used.
|
||||
|
||||
`cmd_env`
|
||||
{table} specifying the environment flags to pass to the LSP on spawn.
|
||||
This can be specified using keys like a map or as a list with `k=v` pairs
|
||||
or both. Non-string values are coerced to a string.
|
||||
For example:
|
||||
`{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`
|
||||
|
||||
`capabilities`
|
||||
A {table} which will be used instead of
|
||||
`vim.lsp.protocol.make_client_capabilities()` which contains neovim's
|
||||
default capabilities and passed to the language server on initialization.
|
||||
You'll probably want to use make_client_capabilities() and modify the
|
||||
result.
|
||||
NOTE:
|
||||
To send an empty dictionary, you should use
|
||||
`{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as
|
||||
an array.
|
||||
|
||||
`callbacks`
|
||||
A {table} of whose keys are language server method names and the values
|
||||
are `function(err, method, params, client_id)` See |lsp-callbacks| for
|
||||
more. This will be combined with |lsp-builtin-callbacks| to provide
|
||||
defaults.
|
||||
|
||||
`init_options`
|
||||
A {table} of values to pass in the initialization request as
|
||||
`initializationOptions`. See the `initialize` in the LSP spec.
|
||||
|
||||
`name`
|
||||
A {string} used in log messages. Defaults to {client_id}
|
||||
|
||||
`offset_encoding`
|
||||
One of "utf-8", "utf-16", or "utf-32" which is the encoding that the LSP
|
||||
server expects.
|
||||
The default encoding for Language Server Protocol is UTF-16, but there are
|
||||
language servers which may use other encodings.
|
||||
By default, it is "utf-16" as specified in the LSP specification. The
|
||||
client does not verify this is correct.
|
||||
|
||||
`on_error(code, ...)`
|
||||
A function for handling errors thrown by client operation. {code} is a
|
||||
number describing the error. Other arguments may be passed depending on
|
||||
the error kind. See |vim.lsp.client_errors| for possible errors.
|
||||
`vim.lsp.client_errors[code]` can be used to retrieve a human
|
||||
understandable string.
|
||||
|
||||
`on_init(client, initialize_result)`
|
||||
A function which is called after the request `initialize` is completed.
|
||||
`initialize_result` contains `capabilities` and anything else the server
|
||||
may send. For example, `clangd` sends `initialize_result.offsetEncoding`
|
||||
if `capabilities.offsetEncoding` was sent to it. You can *only* modify the
|
||||
`client.offset_encoding` here before any notifications are sent.
|
||||
|
||||
`on_attach(client, bufnr)`
|
||||
A function which is called after the client is attached to a buffer.
|
||||
|
||||
`on_exit(code, signal, client_id)`
|
||||
A function which is called after the client has exited. code is the exit
|
||||
code of the process, and signal is a number describing the signal used to
|
||||
terminate (if any).
|
||||
|
||||
`trace`
|
||||
"off" | "messages" | "verbose" | nil passed directly to the language
|
||||
server in the initialize request.
|
||||
Invalid/empty values will default to "off"
|
||||
|
||||
Returns:~
|
||||
{client_id}
|
||||
You can use |vim.lsp.get_client_by_id()| to get the actual client object.
|
||||
See |lsp-client| for what the client structure will be.
|
||||
|
||||
NOTE: The client is only available *after* it has been initialized, which
|
||||
may happen after a small delay (or never if there is an error). For this
|
||||
reason, you may want to use `on_init` to do any actions once the client has
|
||||
been initialized.
|
||||
|
||||
*lsp-client*
|
||||
|
||||
The client object has some methods and members related to using the client.
|
||||
|
||||
Methods:~
|
||||
|
||||
`request(method, params, [callback])`
|
||||
Send a request to the server. If callback is not specified, it will use
|
||||
{client.callbacks} to try to find a callback. If one is not found there,
|
||||
then an error will occur.
|
||||
This is a thin wrapper around {client.rpc.request} with some additional
|
||||
checking.
|
||||
Returns a boolean to indicate if the notification was successful. If it
|
||||
is false, then it will always be false (the client has shutdown).
|
||||
If it was successful, then it will return the request id as the second
|
||||
result. You can use this with `notify("$/cancel", { id = request_id })`
|
||||
to cancel the request. This helper is made automatically with
|
||||
|vim.lsp.buf_request()|
|
||||
Returns: status, [client_id]
|
||||
|
||||
`notify(method, params)`
|
||||
This is just {client.rpc.notify}()
|
||||
Returns a boolean to indicate if the notification was successful. If it
|
||||
is false, then it will always be false (the client has shutdown).
|
||||
Returns: status
|
||||
|
||||
`cancel_request(id)`
|
||||
This is just {client.rpc.notify}("$/cancelRequest", { id = id })
|
||||
Returns the same as `notify()`.
|
||||
|
||||
`stop([force])`
|
||||
Stop a client, optionally with force.
|
||||
By default, it will just ask the server to shutdown without force.
|
||||
If you request to stop a client which has previously been requested to
|
||||
shutdown, it will automatically escalate and force shutdown.
|
||||
|
||||
`is_stopped()`
|
||||
Returns true if the client is fully stopped.
|
||||
|
||||
Members: ~
|
||||
`id` (number)
|
||||
The id allocated to the client.
|
||||
|
||||
`name` (string)
|
||||
If a name is specified on creation, that will be used. Otherwise it is
|
||||
just the client id. This is used for logs and messages.
|
||||
|
||||
`offset_encoding` (string)
|
||||
The encoding used for communicating with the server. You can modify this
|
||||
in the `on_init` method before text is sent to the server.
|
||||
|
||||
`callbacks` (table)
|
||||
The callbacks used by the client as described in |lsp-callbacks|.
|
||||
|
||||
`config` (table)
|
||||
A copy of the table that was passed by the user to
|
||||
|vim.lsp.start_client()|.
|
||||
|
||||
`server_capabilities` (table)
|
||||
The response from the server sent on `initialize` describing the
|
||||
server's capabilities.
|
||||
|
||||
`resolved_capabilities` (table)
|
||||
A normalized table of capabilities that we have detected based on the
|
||||
initialize response from the server in `server_capabilities`.
|
||||
|
||||
|
||||
*vim.lsp.buf_attach_client()*
|
||||
vim.lsp.buf_attach_client({bufnr}, {client_id})
|
||||
|
||||
Implements the `textDocument/did*` notifications required to track a buffer
|
||||
for any language server.
|
||||
|
||||
Without calling this, the server won't be notified of changes to a buffer.
|
||||
|
||||
*vim.lsp.get_client_by_id()*
|
||||
vim.lsp.get_client_by_id({client_id})
|
||||
|
||||
Look up an active client by its id, returns nil if it is not yet initialized
|
||||
or is not a valid id. Returns |lsp-client|
|
||||
|
||||
*vim.lsp.stop_client()*
|
||||
vim.lsp.stop_client({client_id}, [{force}])
|
||||
|
||||
Stop a client, optionally with force.
|
||||
By default, it will just ask the server to shutdown without force.
|
||||
If you request to stop a client which has previously been requested to
|
||||
shutdown, it will automatically escalate and force shutdown.
|
||||
|
||||
You can also use `client.stop()` if you have access to the client.
|
||||
|
||||
*vim.lsp.stop_all_clients()*
|
||||
vim.lsp.stop_all_clients([{force}])
|
||||
|
||||
|vim.lsp.stop_client()|, but for all active clients.
|
||||
|
||||
*vim.lsp.get_active_clients()*
|
||||
vim.lsp.get_active_clients()
|
||||
|
||||
Return a list of all of the active clients. See |lsp-client| for a
|
||||
description of what a client looks like.
|
||||
|
||||
*vim.lsp.rpc_response_error()*
|
||||
vim.lsp.rpc_response_error({code}, [{message}], [{data}])
|
||||
|
||||
Helper function to create an RPC response object/table. This is an alias for
|
||||
|vim.lsp.rpc.rpc_response_error|. Code must be an RPC error code as
|
||||
described in `vim.lsp.protocol.ErrorCodes`.
|
||||
|
||||
You can describe an optional {message} string or arbitrary {data} to send to
|
||||
the server.
|
||||
|
||||
================================================================================
|
||||
*vim.lsp.builtin_callbacks*
|
||||
|
||||
The |vim.lsp.builtin_callbacks| table contains the default |lsp-callbacks|
|
||||
that are used when creating a new client. The keys are the LSP method names.
|
||||
|
||||
The following requests and notifications have built-in callbacks defined to
|
||||
handle the response in an idiomatic way.
|
||||
|
||||
textDocument/completion
|
||||
textDocument/declaration
|
||||
textDocument/definition
|
||||
textDocument/hover
|
||||
textDocument/implementation
|
||||
textDocument/rename
|
||||
textDocument/signatureHelp
|
||||
textDocument/typeDefinition
|
||||
window/logMessage
|
||||
window/showMessage
|
||||
|
||||
You can check these via `vim.tbl_keys(vim.lsp.builtin_callbacks)`.
|
||||
|
||||
These will be automatically used and can be overridden by users (either by
|
||||
modifying the |vim.lsp.builtin_callbacks| object or on a per-client basis
|
||||
by passing in a table via the {callbacks} parameter on |vim.lsp.start_client|
|
||||
or |vim.lsp.add_filetype_config|.
|
||||
|
||||
More information about callbacks can be found in |lsp-callbacks|.
|
||||
|
||||
================================================================================
|
||||
*lsp-callbacks*
|
||||
|
||||
Callbacks are functions which are called in a variety of situations by the
|
||||
client. Their signature is `function(err, method, params, client_id)` They can
|
||||
be set by the {callbacks} parameter for |vim.lsp.start_client| and
|
||||
|vim.lsp.add_filetype_config| or via the |vim.lsp.builtin_callbacks|.
|
||||
|
||||
This will be called for:
|
||||
- notifications from the server, where `err` will always be `nil`
|
||||
- requests initiated by the server. The parameter `err` will be `nil` here as
|
||||
well.
|
||||
For these, you can respond by returning two values: `result, err` The
|
||||
err must be in the format of an RPC error, which is
|
||||
`{ code, message, data? }`
|
||||
You can use |vim.lsp.rpc_response_error()| to help with creating this object.
|
||||
- as a callback for requests initiated by the client if the request doesn't
|
||||
explicitly specify a callback (such as in |vim.lsp.buf_request|).
|
||||
|
||||
================================================================================
|
||||
*vim.lsp.protocol*
|
||||
vim.lsp.protocol
|
||||
|
||||
Contains constants as described in the Language Server Protocol
|
||||
specification and helper functions for creating protocol related objects.
|
||||
|
||||
https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
|
||||
|
||||
Useful examples are `vim.lsp.protocol.ErrorCodes`. These objects allow
|
||||
reverse lookup by either the number or string name.
|
||||
|
||||
e.g. vim.lsp.protocol.TextDocumentSyncKind.Full == 1
|
||||
vim.lsp.protocol.TextDocumentSyncKind[1] == "Full"
|
||||
|
||||
Utility functions used internally are:
|
||||
`vim.lsp.make_client_capabilities()`
|
||||
Make a ClientCapabilities object. These are the builtin
|
||||
capabilities.
|
||||
`vim.lsp.make_text_document_position_params()`
|
||||
Make a TextDocumentPositionParams object.
|
||||
`vim.lsp.resolve_capabilities(server_capabilites)`
|
||||
Creates a normalized object describing capabilities from the server
|
||||
capabilities.
|
||||
|
||||
================================================================================
|
||||
*vim.lsp.util*
|
||||
|
||||
TODO: Describe the utils here for handling/applying things from LSP.
|
||||
|
||||
================================================================================
|
||||
*lsp-buf-methods*
|
||||
There are methods which operate on the buffer level for all of the active
|
||||
clients attached to the buffer.
|
||||
|
||||
*vim.lsp.buf_request()*
|
||||
vim.lsp.buf_request({bufnr}, {method}, {params}, [{callback}])
|
||||
Send a async request for all the clients active and attached to the buffer.
|
||||
|
||||
Parameters: ~
|
||||
{bufnr}: The buffer handle or 0 for the current buffer.
|
||||
|
||||
{method}: The LSP method name.
|
||||
|
||||
{params}: The parameters to send.
|
||||
|
||||
{callback}: An optional `function(err, method, params, client_id)` which
|
||||
will be called for this request. If you do not specify it, then it will
|
||||
use the client's callback in {client.callbacks}. See |lsp-callbacks| for
|
||||
more information.
|
||||
|
||||
Returns:~
|
||||
|
||||
A table from client id to the request id for all of the successful
|
||||
requests.
|
||||
|
||||
The second result is a function which can be used to cancel all the
|
||||
requests. You can do this individually with `client.cancel_request()`
|
||||
|
||||
*vim.lsp.buf_request_sync()*
|
||||
vim.lsp.buf_request_sync({bufnr}, {method}, {params}, [{timeout_ms}])
|
||||
Calls |vim.lsp.buf_request()|, but it will wait for the result and block Vim
|
||||
in the process.
|
||||
The parameters are the same as |vim.lsp.buf_request()|, but the return
|
||||
result is different.
|
||||
It will wait maximum of {timeout_ms} which defaults to 100ms.
|
||||
|
||||
Returns:~
|
||||
|
||||
If the timeout is exceeded or a cancel is sent or an error, it will cancel
|
||||
the request and return `nil, err` where `err` is a string that describes
|
||||
the reason why it failed.
|
||||
|
||||
If it is successful, it will return a table from client id to result id.
|
||||
|
||||
*vim.lsp.buf_notify()*
|
||||
vim.lsp.buf_notify({bufnr}, {method}, {params})
|
||||
Send a notification to all servers on the buffer.
|
||||
|
||||
Parameters: ~
|
||||
{bufnr}: The buffer handle or 0 for the current buffer.
|
||||
|
||||
{method}: The LSP method name.
|
||||
|
||||
{params}: The parameters to send.
|
||||
|
||||
================================================================================
|
||||
*lsp-logging*
|
||||
|
||||
*lsp#set_log_level()*
|
||||
lsp#set_log_level({level})
|
||||
You can set the log level for language server client logging.
|
||||
Possible values: "trace", "debug", "info", "warn", "error"
|
||||
|
||||
Default: "warn"
|
||||
|
||||
Example: `call lsp#set_log_level("debug")`
|
||||
|
||||
*lsp#get_log_path()*
|
||||
*vim.lsp.get_log_path()*
|
||||
lsp#get_log_path()
|
||||
vim.lsp.get_log_path()
|
||||
Returns the path that LSP logs are written.
|
||||
|
||||
*vim.lsp.log_levels*
|
||||
vim.lsp.log_levels
|
||||
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
|
||||
|
||||
================================================================================
|
||||
*lsp-omnifunc*
|
||||
*vim.lsp.omnifunc()*
|
||||
*lsp#omnifunc*
|
||||
lsp#omnifunc({findstart}, {base})
|
||||
vim.lsp.omnifunc({findstart}, {base})
|
||||
|
||||
To configure omnifunc, add the following in your init.vim:
|
||||
>
|
||||
set omnifunc=lsp#omnifunc
|
||||
|
||||
" This is optional, but you may find it useful
|
||||
autocmd CompleteDone * pclose
|
||||
<
|
||||
================================================================================
|
||||
*lsp-vim-functions*
|
||||
|
||||
These methods can be used in mappings and are the equivalent of using the
|
||||
request from lua as follows:
|
||||
|
||||
>
|
||||
lua vim.lsp.buf_request(0, "textDocument/hover", vim.lsp.protocol.make_text_document_position_params())
|
||||
<
|
||||
|
||||
lsp#text_document_declaration()
|
||||
lsp#text_document_definition()
|
||||
lsp#text_document_hover()
|
||||
lsp#text_document_implementation()
|
||||
lsp#text_document_signature_help()
|
||||
lsp#text_document_type_definition()
|
||||
|
||||
>
|
||||
" Example config
|
||||
autocmd Filetype rust,python,go,c,cpp setl omnifunc=lsp#omnifunc
|
||||
nnoremap <silent> ;dc :call lsp#text_document_declaration()<CR>
|
||||
nnoremap <silent> ;df :call lsp#text_document_definition()<CR>
|
||||
nnoremap <silent> ;h :call lsp#text_document_hover()<CR>
|
||||
nnoremap <silent> ;i :call lsp#text_document_implementation()<CR>
|
||||
nnoremap <silent> ;s :call lsp#text_document_signature_help()<CR>
|
||||
nnoremap <silent> ;td :call lsp#text_document_type_definition()<CR>
|
||||
<
|
||||
================================================================================
|
||||
*lsp-advanced-js-example*
|
||||
|
||||
For more advanced configurations where just filtering by filetype isn't
|
||||
sufficient, you can use the `vim.lsp.start_client()` and
|
||||
`vim.lsp.buf_attach_client()` commands to easily customize the configuration
|
||||
however you please. For example, if you want to do your own filtering, or
|
||||
start a new LSP client based on the root directory for if you plan to work
|
||||
with multiple projects in a single session. Below is a fully working Lua
|
||||
example which can do exactly that.
|
||||
|
||||
The example will:
|
||||
1. Check for each new buffer whether or not we want to start an LSP client.
|
||||
2. Try to find a root directory by ascending from the buffer's path.
|
||||
3. Create a new LSP for that root directory if one doesn't exist.
|
||||
4. Attach the buffer to the client for that root directory.
|
||||
|
||||
>
|
||||
-- Some path manipulation utilities
|
||||
local function is_dir(filename)
|
||||
local stat = vim.loop.fs_stat(filename)
|
||||
return stat and stat.type == 'directory' or false
|
||||
end
|
||||
|
||||
local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
|
||||
-- Asumes filepath is a file.
|
||||
local function dirname(filepath)
|
||||
local is_changed = false
|
||||
local result = filepath:gsub(path_sep.."([^"..path_sep.."]+)$", function()
|
||||
is_changed = true
|
||||
return ""
|
||||
end)
|
||||
return result, is_changed
|
||||
end
|
||||
|
||||
local function path_join(...)
|
||||
return table.concat(vim.tbl_flatten {...}, path_sep)
|
||||
end
|
||||
|
||||
-- Ascend the buffer's path until we find the rootdir.
|
||||
-- is_root_path is a function which returns bool
|
||||
local function buffer_find_root_dir(bufnr, is_root_path)
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
if vim.fn.filereadable(bufname) == 0 then
|
||||
return nil
|
||||
end
|
||||
local dir = bufname
|
||||
-- Just in case our algo is buggy, don't infinite loop.
|
||||
for _ = 1, 100 do
|
||||
local did_change
|
||||
dir, did_change = dirname(dir)
|
||||
if is_root_path(dir, bufname) then
|
||||
return dir, bufname
|
||||
end
|
||||
-- If we can't ascend further, then stop looking.
|
||||
if not did_change then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A table to store our root_dir to client_id lookup. We want one LSP per
|
||||
-- root directory, and this is how we assert that.
|
||||
local javascript_lsps = {}
|
||||
-- Which filetypes we want to consider.
|
||||
local javascript_filetypes = {
|
||||
["javascript.jsx"] = true;
|
||||
["javascript"] = true;
|
||||
["typescript"] = true;
|
||||
["typescript.jsx"] = true;
|
||||
}
|
||||
|
||||
-- Create a template configuration for a server to start, minus the root_dir
|
||||
-- which we will specify later.
|
||||
local javascript_lsp_config = {
|
||||
name = "javascript";
|
||||
cmd = { path_join(os.getenv("JAVASCRIPT_LANGUAGE_SERVER_DIRECTORY"), "lib", "language-server-stdio.js") };
|
||||
}
|
||||
|
||||
-- This needs to be global so that we can call it from the autocmd.
|
||||
function check_start_javascript_lsp()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
-- Filter which files we are considering.
|
||||
if not javascript_filetypes[vim.api.nvim_buf_get_option(bufnr, 'filetype')] then
|
||||
return
|
||||
end
|
||||
-- Try to find our root directory. We will define this as a directory which contains
|
||||
-- node_modules. Another choice would be to check for `package.json`, or for `.git`.
|
||||
local root_dir = buffer_find_root_dir(bufnr, function(dir)
|
||||
return is_dir(path_join(dir, 'node_modules'))
|
||||
-- return vim.fn.filereadable(path_join(dir, 'package.json')) == 1
|
||||
-- return is_dir(path_join(dir, '.git'))
|
||||
end)
|
||||
-- We couldn't find a root directory, so ignore this file.
|
||||
if not root_dir then return end
|
||||
|
||||
-- Check if we have a client alredy or start and store it.
|
||||
local client_id = javascript_lsps[root_dir]
|
||||
if not client_id then
|
||||
local new_config = vim.tbl_extend("error", javascript_lsp_config, {
|
||||
root_dir = root_dir;
|
||||
})
|
||||
client_id = vim.lsp.start_client(new_config)
|
||||
javascript_lsps[root_dir] = client_id
|
||||
end
|
||||
-- Finally, attach to the buffer to track changes. This will do nothing if we
|
||||
-- are already attached.
|
||||
vim.lsp.buf_attach_client(bufnr, client_id)
|
||||
end
|
||||
|
||||
vim.api.nvim_command [[autocmd BufReadPost * lua check_start_javascript_lsp()]]
|
||||
<
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -256,6 +256,13 @@ local function __index(t, key)
|
|||
-- Expose all `vim.shared` functions on the `vim` module.
|
||||
t[key] = require('vim.shared')[key]
|
||||
return t[key]
|
||||
elseif require('vim.uri')[key] ~= nil then
|
||||
-- Expose all `vim.uri` functions on the `vim` module.
|
||||
t[key] = require('vim.uri')[key]
|
||||
return t[key]
|
||||
elseif key == 'lsp' then
|
||||
t.lsp = require('vim.lsp')
|
||||
return t.lsp
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
local protocol = require 'vim.lsp.protocol'
|
||||
|
||||
-- Internal utility methods.
|
||||
|
||||
-- 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 message_parts(sep, ...)
|
||||
local parts = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local arg = select(i, ...)
|
||||
if arg ~= nil then
|
||||
table.insert(parts, arg)
|
||||
end
|
||||
end
|
||||
return table.concat(parts, sep)
|
||||
end
|
||||
|
||||
-- Assert utility methods
|
||||
|
||||
local function assert_eq(a, b, ...)
|
||||
if not vim.deep_equal(a, b) then
|
||||
error(message_parts(": ",
|
||||
..., "assert_eq failed",
|
||||
string.format("left == %q, right == %q", vim.inspect(a), vim.inspect(b))
|
||||
))
|
||||
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
|
||||
|
||||
-- Server utility methods.
|
||||
|
||||
local function read_message()
|
||||
local line = io.read("*l")
|
||||
local length = line:lower():match("content%-length:%s*(%d+)")
|
||||
return assert(json_decode(io.read(2 + length):sub(2)), "read_message.json_decode")
|
||||
end
|
||||
|
||||
local function send(payload)
|
||||
io.stdout:write(format_message_with_content_length(json_encode(payload)))
|
||||
end
|
||||
|
||||
local function respond(id, err, result)
|
||||
assert(type(id) == 'number', "id must be a number")
|
||||
send { jsonrpc = "2.0"; id = id, error = err, result = result }
|
||||
end
|
||||
|
||||
local function notify(method, params)
|
||||
assert(type(method) == 'string', "method must be a string")
|
||||
send { method = method, params = params or {} }
|
||||
end
|
||||
|
||||
local function expect_notification(method, params, ...)
|
||||
local message = read_message()
|
||||
assert_eq(method, message.method,
|
||||
..., "expect_notification", "method")
|
||||
assert_eq(params, message.params,
|
||||
..., "expect_notification", method, "params")
|
||||
assert_eq({jsonrpc = "2.0"; method=method, params=params}, message,
|
||||
..., "expect_notification", "message")
|
||||
end
|
||||
|
||||
local function expect_request(method, callback, ...)
|
||||
local req = read_message()
|
||||
assert_eq(method, req.method,
|
||||
..., "expect_request", "method")
|
||||
local err, result = callback(req.params)
|
||||
respond(req.id, err, result)
|
||||
end
|
||||
|
||||
io.stderr:setvbuf("no")
|
||||
|
||||
local function skeleton(config)
|
||||
local on_init = assert(config.on_init)
|
||||
local body = assert(config.body)
|
||||
expect_request("initialize", function(params)
|
||||
return nil, on_init(params)
|
||||
end)
|
||||
expect_notification("initialized", {})
|
||||
body()
|
||||
expect_request("shutdown", function()
|
||||
return nil, {}
|
||||
end)
|
||||
expect_notification("exit", nil)
|
||||
end
|
||||
|
||||
-- The actual tests.
|
||||
|
||||
local tests = {}
|
||||
|
||||
function tests.basic_init()
|
||||
skeleton {
|
||||
on_init = function(_params)
|
||||
return { capabilities = {} }
|
||||
end;
|
||||
body = function()
|
||||
notify('test')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_capabilities()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_finish()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open_and_change()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 3;
|
||||
};
|
||||
contentChanges = {
|
||||
{ text = table.concat({"testing"; "boop"}, "\n"); };
|
||||
}
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open_and_change_multi()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 3;
|
||||
};
|
||||
contentChanges = {
|
||||
{ text = table.concat({"testing"; "321"}, "\n"); };
|
||||
}
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 4;
|
||||
};
|
||||
contentChanges = {
|
||||
{ text = table.concat({"testing"; "boop"}, "\n"); };
|
||||
}
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open_and_change_multi_and_close()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Full;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 3;
|
||||
};
|
||||
contentChanges = {
|
||||
{ text = table.concat({"testing"; "321"}, "\n"); };
|
||||
}
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 4;
|
||||
};
|
||||
contentChanges = {
|
||||
{ text = table.concat({"testing"; "boop"}, "\n"); };
|
||||
}
|
||||
})
|
||||
expect_notification('textDocument/didClose', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
};
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open_and_change_incremental()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 3;
|
||||
};
|
||||
contentChanges = {
|
||||
{
|
||||
range = {
|
||||
start = { line = 1; character = 0; };
|
||||
["end"] = { line = 2; character = 0; };
|
||||
};
|
||||
rangeLength = 4;
|
||||
text = "boop\n";
|
||||
};
|
||||
}
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.basic_check_buffer_open_and_change_incremental_editting()
|
||||
skeleton {
|
||||
on_init = function(params)
|
||||
local expected_capabilities = protocol.make_client_capabilities()
|
||||
assert_eq(params.capabilities, expected_capabilities)
|
||||
return {
|
||||
capabilities = {
|
||||
textDocumentSync = protocol.TextDocumentSyncKind.Incremental;
|
||||
}
|
||||
}
|
||||
end;
|
||||
body = function()
|
||||
notify('start')
|
||||
expect_notification('textDocument/didOpen', {
|
||||
textDocument = {
|
||||
languageId = "";
|
||||
text = table.concat({"testing"; "123"}, "\n");
|
||||
uri = "file://";
|
||||
version = 0;
|
||||
};
|
||||
})
|
||||
expect_notification('textDocument/didChange', {
|
||||
textDocument = {
|
||||
uri = "file://";
|
||||
version = 3;
|
||||
};
|
||||
contentChanges = {
|
||||
{
|
||||
range = {
|
||||
start = { line = 0; character = 0; };
|
||||
["end"] = { line = 1; character = 0; };
|
||||
};
|
||||
rangeLength = 4;
|
||||
text = "testing\n\n";
|
||||
};
|
||||
}
|
||||
})
|
||||
expect_notification("finish")
|
||||
notify('finish')
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function tests.invalid_header()
|
||||
io.stdout:write("Content-length: \r\n")
|
||||
end
|
||||
|
||||
-- Tests will be indexed by TEST_NAME
|
||||
|
||||
local kill_timer = vim.loop.new_timer()
|
||||
kill_timer:start(_G.TIMEOUT or 1e3, 0, function()
|
||||
kill_timer:stop()
|
||||
kill_timer:close()
|
||||
io.stderr:write("TIMEOUT")
|
||||
os.exit(100)
|
||||
end)
|
||||
|
||||
local test_name = _G.TEST_NAME -- lualint workaround
|
||||
assert(type(test_name) == 'string', 'TEST_NAME must be specified.')
|
||||
local status, err = pcall(assert(tests[test_name], "Test not found"))
|
||||
kill_timer:stop()
|
||||
kill_timer:close()
|
||||
if not status then
|
||||
io.stderr:write(err)
|
||||
os.exit(1)
|
||||
end
|
||||
os.exit(0)
|
|
@ -0,0 +1,107 @@
|
|||
local helpers = require('test.functional.helpers')(after_each)
|
||||
local clear = helpers.clear
|
||||
local exec_lua = helpers.exec_lua
|
||||
local eq = helpers.eq
|
||||
|
||||
describe('URI methods', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
end)
|
||||
|
||||
describe('file path to uri', function()
|
||||
describe('encode Unix file path', function()
|
||||
it('file path includes only ascii charactors', function()
|
||||
exec_lua("filepath = '/Foo/Bar/Baz.txt'")
|
||||
|
||||
eq('file:///Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
|
||||
it('file path including white space', function()
|
||||
exec_lua("filepath = '/Foo /Bar/Baz.txt'")
|
||||
|
||||
eq('file:///Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
|
||||
it('file path including Unicode charactors', function()
|
||||
exec_lua("filepath = '/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt'")
|
||||
|
||||
-- The URI encoding should be case-insensitive
|
||||
eq('file:///xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('encode Windows filepath', function()
|
||||
it('file path includes only ascii charactors', function()
|
||||
exec_lua([[filepath = 'C:\\Foo\\Bar\\Baz.txt']])
|
||||
|
||||
eq('file:///C:/Foo/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
|
||||
it('file path including white space', function()
|
||||
exec_lua([[filepath = 'C:\\Foo \\Bar\\Baz.txt']])
|
||||
|
||||
eq('file:///C:/Foo%20/Bar/Baz.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
|
||||
it('file path including Unicode charactors', function()
|
||||
exec_lua([[filepath = 'C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt']])
|
||||
|
||||
eq('file:///C:/xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt', exec_lua("return vim.uri_from_fname(filepath)"))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('uri to filepath', function()
|
||||
describe('decode Unix file path', function()
|
||||
it('file path includes only ascii charactors', function()
|
||||
exec_lua("uri = 'file:///Foo/Bar/Baz.txt'")
|
||||
|
||||
eq('/Foo/Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
|
||||
end)
|
||||
|
||||
it('file path including white space', function()
|
||||
exec_lua("uri = 'file:///Foo%20/Bar/Baz.txt'")
|
||||
|
||||
eq('/Foo /Bar/Baz.txt', exec_lua("return vim.uri_to_fname(uri)"))
|
||||
end)
|
||||
|
||||
it('file path including Unicode charactors', function()
|
||||
local test_case = [[
|
||||
local uri = 'file:///xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
|
||||
return vim.uri_to_fname(uri)
|
||||
]]
|
||||
|
||||
eq('/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt', exec_lua(test_case))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('decode Windows filepath', function()
|
||||
it('file path includes only ascii charactors', function()
|
||||
local test_case = [[
|
||||
local uri = 'file:///C:/Foo/Bar/Baz.txt'
|
||||
return vim.uri_to_fname(uri)
|
||||
]]
|
||||
|
||||
eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
|
||||
end)
|
||||
|
||||
it('file path including white space', function()
|
||||
local test_case = [[
|
||||
local uri = 'file:///C:/Foo%20/Bar/Baz.txt'
|
||||
return vim.uri_to_fname(uri)
|
||||
]]
|
||||
|
||||
eq('C:\\Foo \\Bar\\Baz.txt', exec_lua(test_case))
|
||||
end)
|
||||
|
||||
it('file path including Unicode charactors', function()
|
||||
local test_case = [[
|
||||
local uri = 'file:///C:/xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
|
||||
return vim.uri_to_fname(uri)
|
||||
]]
|
||||
|
||||
eq('C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt', exec_lua(test_case))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
|
@ -305,6 +305,78 @@ describe('lua stdlib', function()
|
|||
pcall_err(exec_lua, [[return vim.pesc(2)]]))
|
||||
end)
|
||||
|
||||
it('vim.tbl_keys', function()
|
||||
eq({}, exec_lua("return vim.tbl_keys({})"))
|
||||
for _, v in pairs(exec_lua("return vim.tbl_keys({'a', 'b', 'c'})")) do
|
||||
eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
|
||||
end
|
||||
for _, v in pairs(exec_lua("return vim.tbl_keys({a=1, b=2, c=3})")) do
|
||||
eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
|
||||
end
|
||||
end)
|
||||
|
||||
it('vim.tbl_values', function()
|
||||
eq({}, exec_lua("return vim.tbl_values({})"))
|
||||
for _, v in pairs(exec_lua("return vim.tbl_values({'a', 'b', 'c'})")) do
|
||||
eq(true, exec_lua("return vim.tbl_contains({ 'a', 'b', 'c' }, ...)", v))
|
||||
end
|
||||
for _, v in pairs(exec_lua("return vim.tbl_values({a=1, b=2, c=3})")) do
|
||||
eq(true, exec_lua("return vim.tbl_contains({ 1, 2, 3 }, ...)", v))
|
||||
end
|
||||
end)
|
||||
|
||||
it('vim.tbl_islist', function()
|
||||
eq(NIL, exec_lua("return vim.tbl_islist({})"))
|
||||
eq(true, exec_lua("return vim.tbl_islist({'a', 'b', 'c'})"))
|
||||
eq(false, exec_lua("return vim.tbl_islist({'a', '32', a='hello', b='baz'})"))
|
||||
eq(false, exec_lua("return vim.tbl_islist({1, a='hello', b='baz'})"))
|
||||
eq(false, exec_lua("return vim.tbl_islist({a='hello', b='baz', 1})"))
|
||||
eq(false, exec_lua("return vim.tbl_islist({1, 2, nil, a='hello'})"))
|
||||
end)
|
||||
|
||||
it('vim.tbl_isempty', function()
|
||||
eq(true, exec_lua("return vim.tbl_isempty({})"))
|
||||
eq(false, exec_lua("return vim.tbl_isempty({ 1, 2, 3 })"))
|
||||
eq(false, exec_lua("return vim.tbl_isempty({a=1, b=2, c=3})"))
|
||||
end)
|
||||
|
||||
it('vim.deep_equal', function()
|
||||
eq(true, exec_lua [[ return vim.deep_equal({a=1}, {a=1}) ]])
|
||||
eq(true, exec_lua [[ return vim.deep_equal({a={b=1}}, {a={b=1}}) ]])
|
||||
eq(true, exec_lua [[ return vim.deep_equal({a={b={nil}}}, {a={b={}}}) ]])
|
||||
eq(true, exec_lua [[ return vim.deep_equal({a=1, [5]=5}, {nil,nil,nil,nil,5,a=1}) ]])
|
||||
eq(false, exec_lua [[ return vim.deep_equal(1, {nil,nil,nil,nil,5,a=1}) ]])
|
||||
eq(false, exec_lua [[ return vim.deep_equal(1, 3) ]])
|
||||
eq(false, exec_lua [[ return vim.deep_equal(nil, 3) ]])
|
||||
eq(false, exec_lua [[ return vim.deep_equal({a=1}, {a=2}) ]])
|
||||
end)
|
||||
|
||||
it('vim.list_extend', function()
|
||||
eq({1,2,3}, exec_lua [[ return vim.list_extend({1}, {2,3}) ]])
|
||||
eq('Error executing lua: .../shared.lua: src must be a table',
|
||||
pcall_err(exec_lua, [[ return vim.list_extend({1}, nil) ]]))
|
||||
eq({1,2}, exec_lua [[ return vim.list_extend({1}, {2;a=1}) ]])
|
||||
eq(true, exec_lua [[ local a = {1} return vim.list_extend(a, {2;a=1}) == a ]])
|
||||
end)
|
||||
|
||||
it('vim.tbl_add_reverse_lookup', function()
|
||||
eq(true, exec_lua [[
|
||||
local a = { A = 1 }
|
||||
vim.tbl_add_reverse_lookup(a)
|
||||
return vim.deep_equal(a, { A = 1; [1] = 'A'; })
|
||||
]])
|
||||
-- Throw an error for trying to do it twice (run into an existing key)
|
||||
local code = [[
|
||||
local res = {}
|
||||
local a = { A = 1 }
|
||||
vim.tbl_add_reverse_lookup(a)
|
||||
assert(vim.deep_equal(a, { A = 1; [1] = 'A'; }))
|
||||
vim.tbl_add_reverse_lookup(a)
|
||||
]]
|
||||
matches('Error executing lua: .../shared.lua: The reverse lookup found an existing value for "[1A]" while processing key "[1A]"',
|
||||
pcall_err(exec_lua, code))
|
||||
end)
|
||||
|
||||
it('vim.call, vim.fn', function()
|
||||
eq(true, exec_lua([[return vim.call('sin', 0.0) == 0.0 ]]))
|
||||
eq(true, exec_lua([[return vim.fn.sin(0.0) == 0.0 ]]))
|
||||
|
|
|
@ -0,0 +1,634 @@
|
|||
local helpers = require('test.functional.helpers')(after_each)
|
||||
|
||||
local clear = helpers.clear
|
||||
local exec_lua = helpers.exec_lua
|
||||
local eq = helpers.eq
|
||||
local NIL = helpers.NIL
|
||||
|
||||
-- Use these to get access to a coroutine so that I can run async tests and use
|
||||
-- yield.
|
||||
local run, stop = helpers.run, helpers.stop
|
||||
|
||||
if helpers.pending_win32(pending) then return end
|
||||
|
||||
local is_windows = require'luv'.os_uname().sysname == "Windows"
|
||||
local lsp_test_rpc_server_file = "test/functional/fixtures/lsp-test-rpc-server.lua"
|
||||
if is_windows then
|
||||
lsp_test_rpc_server_file = lsp_test_rpc_server_file:gsub("/", "\\")
|
||||
end
|
||||
|
||||
local function test_rpc_server_setup(test_name, timeout_ms)
|
||||
exec_lua([=[
|
||||
lsp = require('vim.lsp')
|
||||
local test_name, fixture_filename, timeout = ...
|
||||
TEST_RPC_CLIENT_ID = lsp.start_client {
|
||||
cmd = {
|
||||
vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
|
||||
"-c", string.format("lua TEST_NAME = %q", test_name),
|
||||
"-c", string.format("lua TIMEOUT = %d", timeout),
|
||||
"-c", "luafile "..fixture_filename,
|
||||
};
|
||||
callbacks = setmetatable({}, {
|
||||
__index = function(t, method)
|
||||
return function(...)
|
||||
return vim.rpcrequest(1, 'callback', ...)
|
||||
end
|
||||
end;
|
||||
});
|
||||
root_dir = vim.loop.cwd();
|
||||
on_init = function(client, result)
|
||||
TEST_RPC_CLIENT = client
|
||||
vim.rpcrequest(1, "init", result)
|
||||
end;
|
||||
on_exit = function(...)
|
||||
vim.rpcnotify(1, "exit", ...)
|
||||
end;
|
||||
}
|
||||
]=], test_name, lsp_test_rpc_server_file, timeout_ms or 1e3)
|
||||
end
|
||||
|
||||
local function test_rpc_server(config)
|
||||
if config.test_name then
|
||||
clear()
|
||||
test_rpc_server_setup(config.test_name, config.timeout_ms or 1e3)
|
||||
end
|
||||
local client = setmetatable({}, {
|
||||
__index = function(_, name)
|
||||
-- Workaround for not being able to yield() inside __index for Lua 5.1 :(
|
||||
-- Otherwise I would just return the value here.
|
||||
return function(...)
|
||||
return exec_lua([=[
|
||||
local name = ...
|
||||
if type(TEST_RPC_CLIENT[name]) == 'function' then
|
||||
return TEST_RPC_CLIENT[name](select(2, ...))
|
||||
else
|
||||
return TEST_RPC_CLIENT[name]
|
||||
end
|
||||
]=], name, ...)
|
||||
end
|
||||
end;
|
||||
})
|
||||
local code, signal
|
||||
local function on_request(method, args)
|
||||
if method == "init" then
|
||||
if config.on_init then
|
||||
config.on_init(client, unpack(args))
|
||||
end
|
||||
return NIL
|
||||
end
|
||||
if method == 'callback' then
|
||||
if config.on_callback then
|
||||
config.on_callback(unpack(args))
|
||||
end
|
||||
end
|
||||
return NIL
|
||||
end
|
||||
local function on_notify(method, args)
|
||||
if method == 'exit' then
|
||||
code, signal = unpack(args)
|
||||
return stop()
|
||||
end
|
||||
end
|
||||
-- TODO specify timeout?
|
||||
-- run(on_request, on_notify, config.on_setup, 1000)
|
||||
run(on_request, on_notify, config.on_setup)
|
||||
if config.on_exit then
|
||||
config.on_exit(code, signal)
|
||||
end
|
||||
stop()
|
||||
if config.test_name then
|
||||
exec_lua("lsp._vim_exit_handler()")
|
||||
end
|
||||
end
|
||||
|
||||
describe('Language Client API', function()
|
||||
describe('server_name is specified', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
-- Run an instance of nvim on the file which contains our "scripts".
|
||||
-- Pass TEST_NAME to pick the script.
|
||||
local test_name = "basic_init"
|
||||
exec_lua([=[
|
||||
lsp = require('vim.lsp')
|
||||
local test_name, fixture_filename = ...
|
||||
TEST_RPC_CLIENT_ID = lsp.start_client {
|
||||
cmd = {
|
||||
vim.api.nvim_get_vvar("progpath"), '-Es', '-u', 'NONE', '--headless',
|
||||
"-c", string.format("lua TEST_NAME = %q", test_name),
|
||||
"-c", "luafile "..fixture_filename;
|
||||
};
|
||||
root_dir = vim.loop.cwd();
|
||||
}
|
||||
]=], test_name, lsp_test_rpc_server_file)
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
exec_lua("lsp._vim_exit_handler()")
|
||||
-- exec_lua("lsp.stop_all_clients(true)")
|
||||
end)
|
||||
|
||||
describe('start_client and stop_client', function()
|
||||
it('should return true', function()
|
||||
for _ = 1, 20 do
|
||||
helpers.sleep(10)
|
||||
if exec_lua("return #lsp.get_active_clients()") > 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
eq(1, exec_lua("return #lsp.get_active_clients()"))
|
||||
eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
|
||||
eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
|
||||
exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).stop()")
|
||||
eq(false, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID).is_stopped()"))
|
||||
for _ = 1, 20 do
|
||||
helpers.sleep(10)
|
||||
if exec_lua("return #lsp.get_active_clients()") == 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
eq(true, exec_lua("return lsp.get_client_by_id(TEST_RPC_CLIENT_ID) == nil"))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('basic_init test', function()
|
||||
it('should run correctly', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "test", {}, 1};
|
||||
}
|
||||
test_rpc_server {
|
||||
test_name = "basic_init";
|
||||
on_init = function(client, _init_result)
|
||||
-- client is a dummy object which will queue up commands to be run
|
||||
-- once the server initializes. It can't accept lua callbacks or
|
||||
-- other types that may be unserializable for now.
|
||||
client.stop()
|
||||
end;
|
||||
-- If the program timed out, then code will be nil.
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
-- Note that NIL must be used here.
|
||||
-- on_callback(err, method, result, client_id)
|
||||
on_callback = function(...)
|
||||
eq(table.remove(expected_callbacks), {...})
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should fail', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "test", {}, 1};
|
||||
}
|
||||
test_rpc_server {
|
||||
test_name = "basic_init";
|
||||
on_init = function(client)
|
||||
client.notify('test')
|
||||
client.stop()
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(1, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(...)
|
||||
eq(table.remove(expected_callbacks), {...}, "expected callback")
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should succeed with manual shutdown', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "test", {}, 1};
|
||||
}
|
||||
test_rpc_server {
|
||||
test_name = "basic_init";
|
||||
on_init = function(client)
|
||||
eq(0, client.resolved_capabilities().text_document_did_change)
|
||||
client.request('shutdown')
|
||||
client.notify('exit')
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(...)
|
||||
eq(table.remove(expected_callbacks), {...}, "expected callback")
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should verify capabilities sent', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
}
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_capabilities";
|
||||
on_init = function(client)
|
||||
client.stop()
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(...)
|
||||
eq(table.remove(expected_callbacks), {...}, "expected callback")
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should not send didOpen if the buffer closes before init', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_finish";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
eq(1, exec_lua("return TEST_RPC_CLIENT_ID"))
|
||||
eq(true, exec_lua("return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)"))
|
||||
eq(true, exec_lua("return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)"))
|
||||
exec_lua [[
|
||||
vim.api.nvim_command(BUFFER.."bwipeout")
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(full_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
client.notify('finish')
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should check the body sent attaching before init', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(full_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(not lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Shouldn't attach twice")
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should check the body sent attaching after init', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(full_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should check the body and didChange full', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open_and_change";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local full_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(full_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
exec_lua [[
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"boop";
|
||||
})
|
||||
]]
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
-- TODO(askhan) we don't support full for now, so we can disable these tests.
|
||||
pending('should check the body and didChange incremental', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open_and_change_incremental";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
|
||||
eq(sync_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
exec_lua [[
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"boop";
|
||||
})
|
||||
]]
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
-- TODO(askhan) we don't support full for now, so we can disable these tests.
|
||||
pending('should check the body and didChange incremental normal mode editting', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open_and_change_incremental_editting";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Incremental")
|
||||
eq(sync_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
helpers.command("normal! 1Go")
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should check the body and didChange full with 2 changes', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open_and_change_multi";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(sync_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
exec_lua [[
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"321";
|
||||
})
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"boop";
|
||||
})
|
||||
]]
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
it('should check the body and didChange full lifecycle', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "basic_check_buffer_open_and_change_multi_and_close";
|
||||
on_setup = function()
|
||||
exec_lua [[
|
||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
|
||||
"testing";
|
||||
"123";
|
||||
})
|
||||
]]
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
local sync_kind = exec_lua("return require'vim.lsp.protocol'.TextDocumentSyncKind.Full")
|
||||
eq(sync_kind, client.resolved_capabilities().text_document_did_change)
|
||||
eq(true, client.resolved_capabilities().text_document_open_close)
|
||||
exec_lua [[
|
||||
assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
|
||||
]]
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
if method == 'start' then
|
||||
exec_lua [[
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"321";
|
||||
})
|
||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||
"boop";
|
||||
})
|
||||
vim.api.nvim_command(BUFFER.."bwipeout")
|
||||
]]
|
||||
client.notify('finish')
|
||||
end
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
if method == 'finish' then
|
||||
client.stop()
|
||||
end
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe("parsing tests", function()
|
||||
it('should handle invalid content-length correctly', function()
|
||||
local expected_callbacks = {
|
||||
{NIL, "shutdown", {}, 1};
|
||||
{NIL, "finish", {}, 1};
|
||||
{NIL, "start", {}, 1};
|
||||
}
|
||||
local client
|
||||
test_rpc_server {
|
||||
test_name = "invalid_header";
|
||||
on_setup = function()
|
||||
end;
|
||||
on_init = function(_client)
|
||||
client = _client
|
||||
client.stop(true)
|
||||
end;
|
||||
on_exit = function(code, signal)
|
||||
eq(0, code, "exit code") eq(0, signal, "exit signal")
|
||||
end;
|
||||
on_callback = function(err, method, params, client_id)
|
||||
eq(table.remove(expected_callbacks), {err, method, params, client_id}, "expected callback")
|
||||
end;
|
||||
}
|
||||
end)
|
||||
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue