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