mirror of
https://github.com/neovim/neovim
synced 2025-07-17 01:31:48 +00:00
Followup improvements to LSP (#11430)
Addressing things brought up in https://github.com/neovim/neovim/issues/11389 and beyond. - Bugfix for empty dictionary serialization. - Reduce markdown previews of code. - Refactor method triggers to `lsp.buf.*` methods - Switch to v:lua and get rid of vim interface. - Get rid of filetype config in favor of something from https://github.com/neovim/nvim-lsp - Switch error messages to something which doesn't require `ENTER` because if an LSP goes crazy, it'll block neovim. - Rename `builtin_callbacks` to `default_callbacks` - Resolve callback at time of calling using default_callbacks instead of at client creation - Make hover/signatureHelp preview focusable so you can mess with it. - Add vim.lsp.util.set_qflist and vim.lsp.util.set_loclist and vim.lsp.util.locations_to_items(). - Add apply_textedit and tests which enables a new class of features. - Fix column offsets in character and bytes in vim.lsp.buf to be correct. New methods: - Add textDocument/references under vim.lsp.buf.references() - Finish textDocument/rename - Finish textDocument/rangeFormatting and textDocument/format
This commit is contained in:
@ -1,45 +0,0 @@
|
||||
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
|
@ -24,105 +24,14 @@ 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()|.
|
||||
- Use https://github.com/neovim/nvim-lsp and one of the existing servers there
|
||||
or set up a new one using the `nvim_lsp/skeleton` interface (and contribute
|
||||
it if you find it useful). This uses |vim.lsp.start_client()| under the
|
||||
hood.
|
||||
- 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
|
||||
@ -178,8 +87,8 @@ vim.lsp.start_client({config})
|
||||
`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.
|
||||
more. This will be combined with |lsp-default-callbacks| to resolve
|
||||
the callbacks for a client as a fallback.
|
||||
|
||||
`init_options`
|
||||
A {table} of values to pass in the initialization request as
|
||||
@ -203,6 +112,12 @@ vim.lsp.start_client({config})
|
||||
`vim.lsp.client_errors[code]` can be used to retrieve a human
|
||||
understandable string.
|
||||
|
||||
`before_init(initialize_params, config)`
|
||||
A function which is called *before* the request `initialize` is completed.
|
||||
`initialize_params` contains the parameters we are sending to the server
|
||||
and `config` is the config that was passed to `start_client()` for
|
||||
convenience. You can use this to modify parameters before they are sent.
|
||||
|
||||
`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
|
||||
@ -346,31 +261,34 @@ vim.lsp.rpc_response_error({code}, [{message}], [{data}])
|
||||
the server.
|
||||
|
||||
================================================================================
|
||||
*vim.lsp.builtin_callbacks*
|
||||
*vim.lsp.default_callbacks*
|
||||
|
||||
The |vim.lsp.builtin_callbacks| table contains the default |lsp-callbacks|
|
||||
The |vim.lsp.default_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
|
||||
textDocument/publishDiagnostics
|
||||
window/logMessage
|
||||
window/showMessage
|
||||
|
||||
You can check these via `vim.tbl_keys(vim.lsp.builtin_callbacks)`.
|
||||
You can check these via `vim.tbl_keys(vim.lsp.default_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|.
|
||||
These will be used preferrentially in `vim.lsp.buf` methods when handling
|
||||
requests. They will also be used when responding to server requests and
|
||||
notifications.
|
||||
|
||||
Use cases:
|
||||
- Users can modify this to customize to their preferences.
|
||||
- UI plugins can modify this by assigning to
|
||||
`vim.lsp.default_callbacks[method]` so as to provide more specialized
|
||||
handling, allowing you to leverage the UI capabilities available. UIs should
|
||||
try to be conscientious of any existing changes the user may have set
|
||||
already by checking for existing values.
|
||||
|
||||
Any callbacks passed directly to `request` methods on a server client will
|
||||
have the highest precedence, followed by the `default_callbacks`.
|
||||
|
||||
More information about callbacks can be found in |lsp-callbacks|.
|
||||
|
||||
@ -379,8 +297,8 @@ More information about callbacks can be found in |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|.
|
||||
be set by the {callbacks} parameter for |vim.lsp.start_client| or via the
|
||||
|vim.lsp.default_callbacks|.
|
||||
|
||||
This will be called for:
|
||||
- notifications from the server, where `err` will always be `nil`
|
||||
@ -409,12 +327,10 @@ vim.lsp.protocol
|
||||
vim.lsp.protocol.TextDocumentSyncKind[1] == "Full"
|
||||
|
||||
Utility functions used internally are:
|
||||
`vim.lsp.make_client_capabilities()`
|
||||
`vim.lsp.protocol.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)`
|
||||
`vim.lsp.protocol.resolve_capabilities(server_capabilites)`
|
||||
Creates a normalized object describing capabilities from the server
|
||||
capabilities.
|
||||
|
||||
@ -482,18 +398,16 @@ vim.lsp.buf_notify({bufnr}, {method}, {params})
|
||||
================================================================================
|
||||
*lsp-logging*
|
||||
|
||||
*lsp#set_log_level()*
|
||||
lsp#set_log_level({level})
|
||||
*vim.lsp.set_log_level()*
|
||||
vim.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")`
|
||||
Example: `lua vim.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.
|
||||
|
||||
@ -508,43 +422,43 @@ vim.lsp.log_levels
|
||||
================================================================================
|
||||
*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
|
||||
" Configure for python
|
||||
autocmd Filetype python setl omnifunc=v:lua.vim.lsp.omnifunc
|
||||
|
||||
" Or with on_attach
|
||||
start_client {
|
||||
...
|
||||
on_attach = function(client, bufnr)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
|
||||
end;
|
||||
}
|
||||
|
||||
" This is optional, but you may find it useful
|
||||
autocmd CompleteDone * pclose
|
||||
<
|
||||
================================================================================
|
||||
*lsp-vim-functions*
|
||||
|
||||
To use the functions from vim, it is recommended to use |v:lua| to interface
|
||||
with the Lua functions. No direct vim functions are provided, but the
|
||||
interface is still easy to use from mappings.
|
||||
|
||||
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>
|
||||
autocmd Filetype rust,python,go,c,cpp setl omnifunc=v:lua.vim.lsp.omnifunc
|
||||
nnoremap <silent> ;dc <cmd>lua vim.lsp.buf.declaration()<CR>
|
||||
nnoremap <silent> ;df <cmd>lua vim.lsp.buf.definition()<CR>
|
||||
nnoremap <silent> ;h <cmd>lua vim.lsp.buf.hover()<CR>
|
||||
nnoremap <silent> ;i <cmd>lua vim.lsp.buf.implementation()<CR>
|
||||
nnoremap <silent> ;s <cmd>lua vim.lsp.buf.signature_help()<CR>
|
||||
nnoremap <silent> ;td <cmd>lua vim.lsp.buf.type_definition()<CR>
|
||||
<
|
||||
================================================================================
|
||||
*lsp-advanced-js-example*
|
||||
|
@ -1,9 +1,10 @@
|
||||
local builtin_callbacks = require 'vim.lsp.builtin_callbacks'
|
||||
local default_callbacks = require 'vim.lsp.default_callbacks'
|
||||
local log = require 'vim.lsp.log'
|
||||
local lsp_rpc = require 'vim.lsp.rpc'
|
||||
local protocol = require 'vim.lsp.protocol'
|
||||
local util = require 'vim.lsp.util'
|
||||
|
||||
local vim = vim
|
||||
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option
|
||||
= vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option
|
||||
local uv = vim.loop
|
||||
@ -12,7 +13,8 @@ local validate = vim.validate
|
||||
|
||||
local lsp = {
|
||||
protocol = protocol;
|
||||
builtin_callbacks = builtin_callbacks;
|
||||
default_callbacks = default_callbacks;
|
||||
buf = require'vim.lsp.buf';
|
||||
util = util;
|
||||
-- Allow raw RPC access.
|
||||
rpc = lsp_rpc;
|
||||
@ -25,6 +27,11 @@ local lsp = {
|
||||
|
||||
-- TODO improve handling of scratch buffers with LSP attached.
|
||||
|
||||
local function err_message(...)
|
||||
nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
|
||||
nvim_command("redraw")
|
||||
end
|
||||
|
||||
local function resolve_bufnr(bufnr)
|
||||
validate { bufnr = { bufnr, 'n', true } }
|
||||
if bufnr == nil or bufnr == 0 then
|
||||
@ -92,11 +99,7 @@ local function for_each_buffer_client(bufnr, callback)
|
||||
return
|
||||
end
|
||||
for client_id in pairs(client_ids) do
|
||||
-- This is unlikely to happen. Could only potentially happen in a race
|
||||
-- condition between literally a single statement.
|
||||
-- We could skip this error, but let's error for now.
|
||||
local client = active_clients[client_id]
|
||||
-- or error(string.format("Client %d has already shut down.", client_id))
|
||||
if client then
|
||||
callback(client, client_id)
|
||||
end
|
||||
@ -154,13 +157,13 @@ local function validate_client_config(config)
|
||||
root_dir = { config.root_dir, is_dir, "directory" };
|
||||
callbacks = { config.callbacks, "t", true };
|
||||
capabilities = { config.capabilities, "t", true };
|
||||
-- cmd = { config.cmd, "s", false };
|
||||
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
|
||||
cmd_env = { config.cmd_env, "f", true };
|
||||
name = { config.name, 's', true };
|
||||
on_error = { config.on_error, "f", true };
|
||||
on_exit = { config.on_exit, "f", true };
|
||||
on_init = { config.on_init, "f", true };
|
||||
before_init = { config.before_init, "f", true };
|
||||
offset_encoding = { config.offset_encoding, "s", true };
|
||||
}
|
||||
local cmd, cmd_args = validate_command(config.cmd)
|
||||
@ -261,6 +264,12 @@ end
|
||||
-- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a
|
||||
-- human understandable string.
|
||||
--
|
||||
-- before_init(initialize_params, config): A function which is called *before*
|
||||
-- the request `initialize` is completed. `initialize_params` contains
|
||||
-- the parameters we are sending to the server and `config` is the config that
|
||||
-- was passed to `start_client()` for convenience. You can use this to modify
|
||||
-- parameters before they are sent.
|
||||
--
|
||||
-- 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`
|
||||
@ -290,19 +299,19 @@ function lsp.start_client(config)
|
||||
|
||||
local client_id = next_client_id()
|
||||
|
||||
local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks)
|
||||
-- Copy metatable if it has one.
|
||||
if config.callbacks and config.callbacks.__metatable then
|
||||
setmetatable(callbacks, getmetatable(config.callbacks))
|
||||
end
|
||||
local callbacks = config.callbacks or {}
|
||||
local name = config.name or tostring(client_id)
|
||||
local log_prefix = string.format("LSP[%s]", name)
|
||||
|
||||
local handlers = {}
|
||||
|
||||
local function resolve_callback(method)
|
||||
return callbacks[method] or default_callbacks[method]
|
||||
end
|
||||
|
||||
function handlers.notification(method, params)
|
||||
local _ = log.debug() and log.debug('notification', method, params)
|
||||
local callback = callbacks[method]
|
||||
local callback = resolve_callback(method)
|
||||
if callback then
|
||||
-- Method name is provided here for convenience.
|
||||
callback(nil, method, params, client_id)
|
||||
@ -311,7 +320,7 @@ function lsp.start_client(config)
|
||||
|
||||
function handlers.server_request(method, params)
|
||||
local _ = log.debug() and log.debug('server_request', method, params)
|
||||
local callback = callbacks[method]
|
||||
local callback = resolve_callback(method)
|
||||
if callback then
|
||||
local _ = log.debug() and log.debug("server_request: found callback for", method)
|
||||
return callback(nil, method, params, client_id)
|
||||
@ -322,12 +331,12 @@ function lsp.start_client(config)
|
||||
|
||||
function handlers.on_error(code, err)
|
||||
local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err })
|
||||
nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err)))
|
||||
err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
|
||||
if config.on_error then
|
||||
local status, usererr = pcall(config.on_error, code, err)
|
||||
if not status then
|
||||
local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr })
|
||||
nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr))
|
||||
err_message(log_prefix, ' user on_error failed: ', tostring(usererr))
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -335,9 +344,19 @@ function lsp.start_client(config)
|
||||
function handlers.on_exit(code, signal)
|
||||
active_clients[client_id] = nil
|
||||
uninitialized_clients[client_id] = nil
|
||||
for _, client_ids in pairs(all_buffer_active_clients) do
|
||||
local active_buffers = {}
|
||||
for bufnr, client_ids in pairs(all_buffer_active_clients) do
|
||||
if client_ids[client_id] then
|
||||
table.insert(active_buffers, bufnr)
|
||||
end
|
||||
client_ids[client_id] = nil
|
||||
end
|
||||
-- Buffer level cleanup
|
||||
vim.schedule(function()
|
||||
for _, bufnr in ipairs(active_buffers) do
|
||||
util.buf_clear_diagnostics(bufnr)
|
||||
end
|
||||
end)
|
||||
if config.on_exit then
|
||||
pcall(config.on_exit, code, signal, client_id)
|
||||
end
|
||||
@ -379,7 +398,6 @@ function lsp.start_client(config)
|
||||
-- The rootUri of the workspace. Is null if no folder is open. If both
|
||||
-- `rootPath` and `rootUri` are set `rootUri` wins.
|
||||
rootUri = vim.uri_from_fname(config.root_dir);
|
||||
-- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h"));
|
||||
-- User provided initialization options.
|
||||
initializationOptions = config.init_options;
|
||||
-- The capabilities provided by the client (editor or tool)
|
||||
@ -403,11 +421,15 @@ function lsp.start_client(config)
|
||||
-- }
|
||||
workspaceFolders = nil;
|
||||
}
|
||||
if config.before_init then
|
||||
-- TODO(ashkan) handle errors here.
|
||||
pcall(config.before_init, initialize_params, config)
|
||||
end
|
||||
local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params)
|
||||
rpc.request('initialize', initialize_params, function(init_err, result)
|
||||
assert(not init_err, tostring(init_err))
|
||||
assert(result, "server sent empty result")
|
||||
rpc.notify('initialized', {})
|
||||
rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary})
|
||||
client.initialized = true
|
||||
uninitialized_clients[client_id] = nil
|
||||
client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities")
|
||||
@ -439,14 +461,14 @@ function lsp.start_client(config)
|
||||
local function unsupported_method(method)
|
||||
local msg = "server doesn't support "..method
|
||||
local _ = log.warn() and log.warn(msg)
|
||||
nvim_err_writeln(msg)
|
||||
err_message(msg)
|
||||
return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg)
|
||||
end
|
||||
|
||||
--- Checks capabilities before rpc.request-ing.
|
||||
function client.request(method, params, callback)
|
||||
if not callback then
|
||||
callback = client.callbacks[method]
|
||||
callback = resolve_callback(method)
|
||||
or error(string.format("request callback is empty and no default was found for client %s", client.name))
|
||||
end
|
||||
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback)
|
||||
@ -851,7 +873,7 @@ function lsp.omnifunc(findstart, base)
|
||||
position = {
|
||||
-- 0-indexed for both line and character
|
||||
line = pos[1] - 1,
|
||||
character = pos[2],
|
||||
character = vim.str_utfindex(line, pos[2]),
|
||||
};
|
||||
-- The completion context. This is only available if the client specifies
|
||||
-- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true`
|
||||
@ -876,134 +898,8 @@ function lsp.omnifunc(findstart, base)
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--- FileType based configuration utility
|
||||
---
|
||||
|
||||
local all_filetype_configs = {}
|
||||
|
||||
-- Lookup a filetype config client by its name.
|
||||
function lsp.get_filetype_client_by_name(name)
|
||||
local config = all_filetype_configs[name]
|
||||
if config.client_id then
|
||||
return active_clients[config.client_id]
|
||||
end
|
||||
end
|
||||
|
||||
local function start_filetype_config(config)
|
||||
config.client_id = lsp.start_client(config)
|
||||
nvim_command(string.format(
|
||||
"autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)",
|
||||
table.concat(config.filetypes, ','),
|
||||
config.client_id))
|
||||
return config.client_id
|
||||
end
|
||||
|
||||
-- Easy configuration option for common LSP use-cases.
|
||||
-- This will lazy initialize the client when the filetypes specified are
|
||||
-- encountered and attach to those buffers.
|
||||
--
|
||||
-- The configuration 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 string among all other servers configured with
|
||||
-- |vim.lsp.add_filetype_config|.
|
||||
--
|
||||
-- Differences:
|
||||
-- - root_dir: will default to |getcwd()|
|
||||
--
|
||||
function lsp.add_filetype_config(config)
|
||||
-- Additional defaults.
|
||||
-- Keep a copy of the user's input for debugging reasons.
|
||||
local user_config = config
|
||||
config = tbl_extend("force", {}, user_config)
|
||||
config.root_dir = config.root_dir or uv.cwd()
|
||||
-- Validate config.
|
||||
validate_client_config(config)
|
||||
validate {
|
||||
name = { config.name, 's' };
|
||||
}
|
||||
assert(config.filetype, "config must have 'filetype' key")
|
||||
|
||||
local filetypes
|
||||
if type(config.filetype) == 'string' then
|
||||
filetypes = { config.filetype }
|
||||
elseif type(config.filetype) == 'table' then
|
||||
filetypes = config.filetype
|
||||
assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table")
|
||||
else
|
||||
error("config.filetype must be a string or a list of strings")
|
||||
end
|
||||
|
||||
if all_filetype_configs[config.name] then
|
||||
-- If the client exists, then it is likely that they are doing some kind of
|
||||
-- reload flow, so let's not throw an error here.
|
||||
if all_filetype_configs[config.name].client_id then
|
||||
-- TODO log here? It might be unnecessarily annoying.
|
||||
return
|
||||
end
|
||||
error(string.format('A configuration with the name %q already exists. They must be unique', config.name))
|
||||
end
|
||||
|
||||
all_filetype_configs[config.name] = tbl_extend("keep", config, {
|
||||
client_id = nil;
|
||||
filetypes = filetypes;
|
||||
user_config = user_config;
|
||||
})
|
||||
|
||||
nvim_command(string.format(
|
||||
"autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)",
|
||||
table.concat(filetypes, ','),
|
||||
config.name))
|
||||
end
|
||||
|
||||
-- Create a copy of an existing configuration, and override config with values
|
||||
-- from new_config.
|
||||
-- This is useful if you wish you create multiple LSPs with different root_dirs
|
||||
-- or other use cases.
|
||||
--
|
||||
-- You can specify a new unique name, but if you do not, a unique name will be
|
||||
-- created like `name-dup_count`.
|
||||
--
|
||||
-- existing_name: the name of the existing config to copy.
|
||||
-- new_config: the new configuration options. @see |vim.lsp.start_client()|.
|
||||
-- @returns string the new name.
|
||||
function lsp.copy_filetype_config(existing_name, new_config)
|
||||
local config = all_filetype_configs[existing_name]
|
||||
or error(string.format("Configuration with name %q doesn't exist", existing_name))
|
||||
config = tbl_extend("force", config, new_config or {})
|
||||
config.client_id = nil
|
||||
config.original_config_name = existing_name
|
||||
|
||||
-- If the user didn't rename it, we will.
|
||||
if config.name == existing_name then
|
||||
-- Create a new, unique name.
|
||||
local duplicate_count = 0
|
||||
for _, conf in pairs(all_filetype_configs) do
|
||||
if conf.original_config_name == existing_name then
|
||||
duplicate_count = duplicate_count + 1
|
||||
end
|
||||
end
|
||||
config.name = string.format("%s-%d", existing_name, duplicate_count + 1)
|
||||
end
|
||||
print("New config name:", config.name)
|
||||
lsp.add_filetype_config(config)
|
||||
return config.name
|
||||
end
|
||||
|
||||
-- Autocmd handler to actually start the client when an applicable filetype is
|
||||
-- encountered.
|
||||
function lsp._start_filetype_config_client(name)
|
||||
local config = all_filetype_configs[name]
|
||||
-- If it exists and is running, don't make it again.
|
||||
if config.client_id and active_clients[config.client_id] then
|
||||
-- TODO log here?
|
||||
return
|
||||
end
|
||||
lsp.buf_attach_client(0, start_filetype_config(config))
|
||||
return config.client_id
|
||||
function lsp.client_is_stopped(client_id)
|
||||
return active_clients[client_id] == nil
|
||||
end
|
||||
|
||||
---
|
||||
@ -1030,7 +926,7 @@ end
|
||||
-- Print some debug information about all LSP related things.
|
||||
-- The output of this function should not be relied upon and may change.
|
||||
function lsp.print_debug_info()
|
||||
print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs }))
|
||||
print(vim.inspect({ clients = active_clients }))
|
||||
end
|
||||
|
||||
-- Log level dictionary with reverse lookup as well.
|
||||
|
349
runtime/lua/vim/lsp/buf.lua
Normal file
349
runtime/lua/vim/lsp/buf.lua
Normal file
@ -0,0 +1,349 @@
|
||||
local vim = vim
|
||||
local validate = vim.validate
|
||||
local api = vim.api
|
||||
local vfn = vim.fn
|
||||
local util = require 'vim.lsp.util'
|
||||
local log = require 'vim.lsp.log'
|
||||
local list_extend = vim.list_extend
|
||||
|
||||
local M = {}
|
||||
|
||||
local function ok_or_nil(status, ...)
|
||||
if not status then return end
|
||||
return ...
|
||||
end
|
||||
local function npcall(fn, ...)
|
||||
return ok_or_nil(pcall(fn, ...))
|
||||
end
|
||||
|
||||
local function err_message(...)
|
||||
api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
|
||||
api.nvim_command("redraw")
|
||||
end
|
||||
|
||||
local function find_window_by_var(name, value)
|
||||
for _, win in ipairs(api.nvim_list_wins()) do
|
||||
if npcall(api.nvim_win_get_var, win, name) == value then
|
||||
return win
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function request(method, params, callback)
|
||||
-- TODO(ashkan) enable this.
|
||||
-- callback = vim.lsp.default_callbacks[method] or callback
|
||||
validate {
|
||||
method = {method, 's'};
|
||||
callback = {callback, 'f'};
|
||||
}
|
||||
return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id)
|
||||
local _ = log.debug() and log.debug("vim.lsp.buf", method, client_id, err, result)
|
||||
if err then error(tostring(err)) end
|
||||
return callback(err, method, result, client_id)
|
||||
end)
|
||||
end
|
||||
|
||||
local function focusable_preview(method, params, fn)
|
||||
if npcall(api.nvim_win_get_var, 0, method) then
|
||||
return api.nvim_command("wincmd p")
|
||||
end
|
||||
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
do
|
||||
local win = find_window_by_var(method, bufnr)
|
||||
if win then
|
||||
api.nvim_set_current_win(win)
|
||||
api.nvim_command("stopinsert")
|
||||
return
|
||||
end
|
||||
end
|
||||
return request(method, params, function(_, _, result, _)
|
||||
-- TODO(ashkan) could show error in preview...
|
||||
local lines, filetype, opts = fn(result)
|
||||
if lines then
|
||||
local _, winnr = util.open_floating_preview(lines, filetype, opts)
|
||||
api.nvim_win_set_var(winnr, method, bufnr)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function M.hover()
|
||||
local params = util.make_position_params()
|
||||
focusable_preview('textDocument/hover', params, function(result)
|
||||
if not (result and result.contents) then return end
|
||||
|
||||
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
|
||||
markdown_lines = util.trim_empty_lines(markdown_lines)
|
||||
if vim.tbl_isempty(markdown_lines) then
|
||||
return { 'No information available' }
|
||||
end
|
||||
return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.peek_definition()
|
||||
local params = util.make_position_params()
|
||||
request('textDocument/peekDefinition', params, function(_, _, result, _)
|
||||
if not (result and result[1]) then return end
|
||||
local loc = result[1]
|
||||
local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri))
|
||||
local start = loc.range.start
|
||||
local finish = loc.range["end"]
|
||||
util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 })
|
||||
local headbuf = util.open_floating_preview({"Peek:"}, nil, {
|
||||
offset_y = -(finish.line - start.line);
|
||||
width = finish.character - start.character + 2;
|
||||
})
|
||||
-- TODO(ashkan) change highlight group?
|
||||
api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
local function update_tagstack()
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
local line = vfn.line('.')
|
||||
local col = vfn.col('.')
|
||||
local tagname = vfn.expand('<cWORD>')
|
||||
local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname }
|
||||
local winid = vfn.win_getid()
|
||||
local tagstack = vfn.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
|
||||
vfn.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
|
||||
err_message('[LSP] Could not find a valid location')
|
||||
return
|
||||
end
|
||||
local bufnr = vim.uri_to_bufnr(result.uri)
|
||||
update_tagstack()
|
||||
api.nvim_set_current_buf(bufnr)
|
||||
local row = result.range.start.line
|
||||
local col = result.range.start.character
|
||||
local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
|
||||
col = #line:sub(1, col)
|
||||
api.nvim_win_set_cursor(0, {row + 1, col})
|
||||
return true
|
||||
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
|
||||
return handle_location(result)
|
||||
end
|
||||
|
||||
function M.declaration()
|
||||
local params = util.make_position_params()
|
||||
request('textDocument/declaration', params, location_callback)
|
||||
end
|
||||
|
||||
function M.definition()
|
||||
local params = util.make_position_params()
|
||||
request('textDocument/definition', params, location_callback)
|
||||
end
|
||||
|
||||
function M.type_definition()
|
||||
local params = util.make_position_params()
|
||||
request('textDocument/typeDefinition', params, location_callback)
|
||||
end
|
||||
|
||||
function M.implementation()
|
||||
local params = util.make_position_params()
|
||||
request('textDocument/implementation', params, location_callback)
|
||||
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
|
||||
list_extend(contents, vim.split(signature.label, '\n', true))
|
||||
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
|
||||
|
||||
function M.signature_help()
|
||||
local params = util.make_position_params()
|
||||
focusable_preview('textDocument/signatureHelp', params, function(result)
|
||||
if not (result and result.signatures and result.signatures[1]) then
|
||||
return { 'No signature available' }
|
||||
end
|
||||
|
||||
-- TODO show empty popup when signatures is empty?
|
||||
local lines = signature_help_to_preview_contents(result)
|
||||
lines = util.trim_empty_lines(lines)
|
||||
if vim.tbl_isempty(lines) then
|
||||
return { 'No signature available' }
|
||||
end
|
||||
return lines, util.try_trim_markdown_code_blocks(lines)
|
||||
end)
|
||||
end
|
||||
|
||||
-- TODO(ashkan) ?
|
||||
function M.completion(context)
|
||||
local params = util.make_position_params()
|
||||
params.context = context
|
||||
return request('textDocument/completion', params, function(_, _, result)
|
||||
if vim.tbl_isempty(result or {}) then return end
|
||||
local row, col = unpack(api.nvim_win_get_cursor(0))
|
||||
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)
|
||||
vim.fn.complete(col, matches)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.formatting(options)
|
||||
validate { options = {options, 't', true} }
|
||||
options = vim.tbl_extend('keep', options or {}, {
|
||||
tabSize = api.nvim_buf_get_option(0, 'tabstop');
|
||||
insertSpaces = api.nvim_buf_get_option(0, 'expandtab');
|
||||
})
|
||||
local params = {
|
||||
textDocument = { uri = vim.uri_from_bufnr(0) };
|
||||
options = options;
|
||||
}
|
||||
return request('textDocument/formatting', params, function(_, _, result)
|
||||
if not result then return end
|
||||
util.apply_text_edits(result)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.range_formatting(options, start_pos, end_pos)
|
||||
validate {
|
||||
options = {options, 't', true};
|
||||
start_pos = {start_pos, 't', true};
|
||||
end_pos = {end_pos, 't', true};
|
||||
}
|
||||
options = vim.tbl_extend('keep', options or {}, {
|
||||
tabSize = api.nvim_buf_get_option(0, 'tabstop');
|
||||
insertSpaces = api.nvim_buf_get_option(0, 'expandtab');
|
||||
})
|
||||
local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
|
||||
local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
|
||||
-- convert to 0-index
|
||||
A[1] = A[1] - 1
|
||||
B[1] = B[1] - 1
|
||||
-- account for encoding.
|
||||
if A[2] > 0 then
|
||||
A = {A[1], util.character_offset(0, unpack(A))}
|
||||
end
|
||||
if B[2] > 0 then
|
||||
B = {B[1], util.character_offset(0, unpack(B))}
|
||||
end
|
||||
local params = {
|
||||
textDocument = { uri = vim.uri_from_bufnr(0) };
|
||||
range = {
|
||||
start = { line = A[1]; character = A[2]; };
|
||||
["end"] = { line = B[1]; character = B[2]; };
|
||||
};
|
||||
options = options;
|
||||
}
|
||||
return request('textDocument/rangeFormatting', params, function(_, _, result)
|
||||
if not result then return end
|
||||
util.apply_text_edits(result)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.rename(new_name)
|
||||
-- TODO(ashkan) use prepareRename
|
||||
-- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position.
|
||||
local params = util.make_position_params()
|
||||
new_name = new_name or npcall(vfn.input, "New Name: ")
|
||||
if not (new_name and #new_name > 0) then return end
|
||||
params.newName = new_name
|
||||
request('textDocument/rename', params, function(_, _, result)
|
||||
if not result then return end
|
||||
util.apply_workspace_edit(result)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.references(context)
|
||||
validate { context = { context, 't', true } }
|
||||
local params = util.make_position_params()
|
||||
params.context = context or {
|
||||
includeDeclaration = true;
|
||||
}
|
||||
params[vim.type_idx] = vim.types.dictionary
|
||||
request('textDocument/references', params, function(_, _, result)
|
||||
if not result then return end
|
||||
util.set_qflist(result)
|
||||
vim.api.nvim_command("copen")
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
-- vim:sw=2 ts=2 et
|
@ -1,296 +0,0 @@
|
||||
--- 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
|
69
runtime/lua/vim/lsp/default_callbacks.lua
Normal file
69
runtime/lua/vim/lsp/default_callbacks.lua
Normal file
@ -0,0 +1,69 @@
|
||||
local log = require 'vim.lsp.log'
|
||||
local protocol = require 'vim.lsp.protocol'
|
||||
local util = require 'vim.lsp.util'
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
|
||||
local function err_message(...)
|
||||
api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
|
||||
api.nvim_command("redraw")
|
||||
end
|
||||
|
||||
M['workspace/applyEdit'] = function(_, _, workspace_edit)
|
||||
if not workspace_edit then return end
|
||||
-- TODO(ashkan) Do something more with label?
|
||||
if workspace_edit.label then
|
||||
print("Workspace edit", workspace_edit.label)
|
||||
end
|
||||
util.apply_workspace_edit(workspace_edit.edit)
|
||||
end
|
||||
|
||||
M['textDocument/publishDiagnostics'] = function(_, _, result)
|
||||
if not result then return end
|
||||
local uri = result.uri
|
||||
local bufnr = vim.uri_to_bufnr(uri)
|
||||
if not bufnr then
|
||||
err_message("LSP.publishDiagnostics: Couldn't find buffer for ", 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.set_loclist(result.diagnostics)
|
||||
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
|
||||
err_message("LSP[", client_name, "] client has shut down after sending the message")
|
||||
end
|
||||
if message_type == protocol.MessageType.Error then
|
||||
err_message("LSP[", 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
|
||||
|
||||
M['window/showMessage'] = log_message
|
||||
M['window/logMessage'] = log_message
|
||||
|
||||
-- Add boilerplate error validation and logging for all of these.
|
||||
for k, fn in pairs(M) do
|
||||
M[k] = function(err, method, params, client_id)
|
||||
local _ = log.debug() and log.debug('default_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 M
|
||||
-- vim:sw=2 ts=2 et
|
@ -10,7 +10,6 @@ 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]]
|
||||
@ -681,19 +680,6 @@ function protocol.make_client_capabilities()
|
||||
}
|
||||
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`.
|
||||
|
@ -340,6 +340,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para
|
||||
local decoded, err = json_decode(body)
|
||||
if not decoded then
|
||||
on_error(client_errors.INVALID_SERVER_JSON, err)
|
||||
return
|
||||
end
|
||||
local _ = log.debug() and log.debug("decoded", decoded)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
local protocol = require 'vim.lsp.protocol'
|
||||
local vim = vim
|
||||
local validate = vim.validate
|
||||
local api = vim.api
|
||||
|
||||
@ -26,11 +27,90 @@ local function remove_prefix(prefix, word)
|
||||
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()
|
||||
-- TODO(ashkan) @performance this could do less copying.
|
||||
function M.set_lines(lines, A, B, new_lines)
|
||||
-- 0-indexing to 1-indexing
|
||||
local i_0 = A[1] + 1
|
||||
local i_n = B[1] + 1
|
||||
if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
|
||||
error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
|
||||
end
|
||||
return bufnr
|
||||
local prefix = ""
|
||||
local suffix = lines[i_n]:sub(B[2]+1)
|
||||
if A[2] > 0 then
|
||||
prefix = lines[i_0]:sub(1, A[2])
|
||||
end
|
||||
local n = i_n - i_0 + 1
|
||||
if n ~= #new_lines then
|
||||
for _ = 1, n - #new_lines do table.remove(lines, i_0) end
|
||||
for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
|
||||
end
|
||||
for i = 1, #new_lines do
|
||||
lines[i - 1 + i_0] = new_lines[i]
|
||||
end
|
||||
if #suffix > 0 then
|
||||
local i = i_0 + #new_lines - 1
|
||||
lines[i] = lines[i]..suffix
|
||||
end
|
||||
if #prefix > 0 then
|
||||
lines[i_0] = prefix..lines[i_0]
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
local function sort_by_key(fn)
|
||||
return function(a,b)
|
||||
local ka, kb = fn(a), fn(b)
|
||||
assert(#ka == #kb)
|
||||
for i = 1, #ka do
|
||||
if ka[i] ~= kb[i] then
|
||||
return ka[i] < kb[i]
|
||||
end
|
||||
end
|
||||
-- every value must have been equal here, which means it's not less than.
|
||||
return false
|
||||
end
|
||||
end
|
||||
local edit_sort_key = sort_by_key(function(e)
|
||||
return {e.A[1], e.A[2], e.i}
|
||||
end)
|
||||
|
||||
function M.apply_text_edits(text_edits, bufnr)
|
||||
if not next(text_edits) then return end
|
||||
local start_line, finish_line = math.huge, -1
|
||||
local cleaned = {}
|
||||
for i, e in ipairs(text_edits) do
|
||||
start_line = math.min(e.range.start.line, start_line)
|
||||
finish_line = math.max(e.range["end"].line, finish_line)
|
||||
-- TODO(ashkan) sanity check ranges for overlap.
|
||||
table.insert(cleaned, {
|
||||
i = i;
|
||||
A = {e.range.start.line; e.range.start.character};
|
||||
B = {e.range["end"].line; e.range["end"].character};
|
||||
lines = vim.split(e.newText, '\n', true);
|
||||
})
|
||||
end
|
||||
|
||||
-- Reverse sort the orders so we can apply them without interfering with
|
||||
-- eachother. Also add i as a sort key to mimic a stable sort.
|
||||
table.sort(cleaned, edit_sort_key)
|
||||
local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
|
||||
local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
|
||||
local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) == finish_line + 1
|
||||
if set_eol and #lines[#lines] ~= 0 then
|
||||
table.insert(lines, '')
|
||||
end
|
||||
|
||||
for i = #cleaned, 1, -1 do
|
||||
local e = cleaned[i]
|
||||
local A = {e.A[1] - start_line, e.A[2]}
|
||||
local B = {e.B[1] - start_line, e.B[2]}
|
||||
lines = M.set_lines(lines, A, B, e.lines)
|
||||
end
|
||||
if set_eol and #lines[#lines] == 0 then
|
||||
table.remove(lines)
|
||||
end
|
||||
api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
|
||||
end
|
||||
|
||||
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
|
||||
@ -40,30 +120,6 @@ end
|
||||
-- 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)
|
||||
@ -78,18 +134,15 @@ 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)
|
||||
function M.apply_text_document_edit(text_document_edit)
|
||||
local text_document = text_document_edit.textDocument
|
||||
local bufnr = vim.uri_to_bufnr(text_document.uri)
|
||||
-- TODO(ashkan) check this is correct.
|
||||
if api.nvim_buf_get_changedtick(bufnr) > text_document.version then
|
||||
print("Buffer ", text_document.uri, " newer than edits.")
|
||||
return
|
||||
end
|
||||
M.apply_text_edits(text_document_edit.edits, bufnr)
|
||||
end
|
||||
|
||||
function M.get_current_line_to_cursor()
|
||||
@ -145,32 +198,27 @@ function M.text_document_completion_list_to_complete_items(result, line_prefix)
|
||||
end
|
||||
|
||||
-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
|
||||
function M.workspace_apply_workspace_edit(workspace_edit)
|
||||
function M.apply_workspace_edit(workspace_edit)
|
||||
if workspace_edit.documentChanges then
|
||||
for _, change in ipairs(workspace_edit.documentChanges) do
|
||||
if change.kind then
|
||||
-- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
|
||||
error(string.format("Unsupported change: %q", vim.inspect(change)))
|
||||
else
|
||||
M.text_document_apply_text_document_edit(change)
|
||||
M.apply_text_document_edit(change)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if workspace_edit.changes == nil or #workspace_edit.changes == 0 then
|
||||
local all_changes = workspace_edit.changes
|
||||
if not (all_changes and not vim.tbl_isempty(all_changes)) then
|
||||
return
|
||||
end
|
||||
|
||||
for uri, changes in pairs(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
|
||||
for uri, changes in pairs(all_changes) do
|
||||
local bufnr = vim.uri_to_bufnr(uri)
|
||||
M.apply_text_edits(changes, bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
@ -261,28 +309,26 @@ function M.open_floating_preview(contents, filetype, opts)
|
||||
filetype = { filetype, 's', true };
|
||||
opts = { opts, 't', true };
|
||||
}
|
||||
opts = opts or {}
|
||||
|
||||
-- Trim empty lines from the end.
|
||||
for i = #contents, 1, -1 do
|
||||
if #contents[i] == 0 then
|
||||
table.remove(contents)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
contents = M.trim_empty_lines(contents)
|
||||
|
||||
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
|
||||
local width = opts.width
|
||||
local height = opts.height or #contents
|
||||
if not width then
|
||||
width = 0
|
||||
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
|
||||
end
|
||||
-- Add right padding of 1 each.
|
||||
width = width + 1
|
||||
|
||||
local floating_bufnr = api.nvim_create_buf(false, true)
|
||||
if filetype then
|
||||
@ -295,7 +341,8 @@ function M.open_floating_preview(contents, filetype, opts)
|
||||
end
|
||||
api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
|
||||
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
|
||||
api.nvim_command("autocmd CursorMoved <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
|
||||
-- TODO make InsertCharPre disappearing optional?
|
||||
api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
|
||||
return floating_bufnr, floating_winnr
|
||||
end
|
||||
|
||||
@ -527,30 +574,140 @@ do
|
||||
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
|
||||
local position_sort = sort_by_key(function(v)
|
||||
return {v.line, v.character}
|
||||
end)
|
||||
|
||||
-- Returns the items with the byte position calculated correctly and in sorted
|
||||
-- order.
|
||||
function M.locations_to_items(locations)
|
||||
local items = {}
|
||||
local grouped = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
local v = {}
|
||||
rawset(t, k, v)
|
||||
return v
|
||||
end;
|
||||
})
|
||||
for _, d in ipairs(locations) do
|
||||
local start = d.range.start
|
||||
local fname = assert(vim.uri_to_fname(d.uri))
|
||||
table.insert(grouped[fname], start)
|
||||
end
|
||||
local keys = vim.tbl_keys(grouped)
|
||||
table.sort(keys)
|
||||
-- TODO(ashkan) I wish we could do this lazily.
|
||||
for _, fname in ipairs(keys) do
|
||||
local rows = grouped[fname]
|
||||
table.sort(rows, position_sort)
|
||||
local i = 0
|
||||
for line in io.lines(fname) do
|
||||
for _, pos in ipairs(rows) do
|
||||
local row = pos.line
|
||||
if i == row then
|
||||
local col
|
||||
if pos.character > #line then
|
||||
col = #line
|
||||
else
|
||||
col = vim.str_byteindex(line, pos.character)
|
||||
end
|
||||
table.insert(items, {
|
||||
filename = fname,
|
||||
lnum = row + 1,
|
||||
col = col + 1;
|
||||
})
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
-- locations is Location[]
|
||||
-- Only sets for the current window.
|
||||
function M.set_loclist(locations)
|
||||
vim.fn.setloclist(0, {}, ' ', {
|
||||
title = 'Language Server';
|
||||
items = M.locations_to_items(locations);
|
||||
})
|
||||
end
|
||||
|
||||
-- locations is Location[]
|
||||
function M.set_qflist(locations)
|
||||
vim.fn.setqflist({}, ' ', {
|
||||
title = 'Language Server';
|
||||
items = M.locations_to_items(locations);
|
||||
})
|
||||
end
|
||||
|
||||
-- Remove empty lines from the beginning and end.
|
||||
function M.trim_empty_lines(lines)
|
||||
local start = 1
|
||||
for i = 1, #lines do
|
||||
if #lines[i] > 0 then
|
||||
start = i
|
||||
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,
|
||||
})
|
||||
local finish = 1
|
||||
for i = #lines, 1, -1 do
|
||||
if #lines[i] > 0 then
|
||||
finish = i
|
||||
break
|
||||
end
|
||||
end
|
||||
vim.fn.setloclist(targetwin, items, ' ', 'Language Server')
|
||||
return vim.list_extend({}, lines, start, finish)
|
||||
end
|
||||
|
||||
-- Accepts markdown lines and tries to reduce it to a filetype if it is
|
||||
-- just a single code block.
|
||||
-- Note: This modifies the input.
|
||||
--
|
||||
-- Returns: filetype or 'markdown' if it was unchanged.
|
||||
function M.try_trim_markdown_code_blocks(lines)
|
||||
local language_id = lines[1]:match("^```(.*)")
|
||||
if language_id then
|
||||
local has_inner_code_fence = false
|
||||
for i = 2, (#lines - 1) do
|
||||
local line = lines[i]
|
||||
if line:sub(1,3) == '```' then
|
||||
has_inner_code_fence = true
|
||||
break
|
||||
end
|
||||
end
|
||||
-- No inner code fences + starting with code fence = hooray.
|
||||
if not has_inner_code_fence then
|
||||
table.remove(lines, 1)
|
||||
table.remove(lines)
|
||||
return language_id
|
||||
end
|
||||
end
|
||||
return 'markdown'
|
||||
end
|
||||
|
||||
local str_utfindex = vim.str_utfindex
|
||||
function M.make_position_params()
|
||||
local row, col = unpack(api.nvim_win_get_cursor(0))
|
||||
row = row - 1
|
||||
local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
|
||||
col = str_utfindex(line, col)
|
||||
return {
|
||||
textDocument = { uri = vim.uri_from_bufnr(0) };
|
||||
position = { line = row; character = col; }
|
||||
}
|
||||
end
|
||||
|
||||
-- @param buf buffer handle or 0 for current.
|
||||
-- @param row 0-indexed line
|
||||
-- @param col 0-indexed byte offset in line
|
||||
function M.character_offset(buf, row, col)
|
||||
local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
|
||||
-- If the col is past the EOL, use the line length.
|
||||
if col > #line then
|
||||
return str_utfindex(line)
|
||||
end
|
||||
return str_utfindex(line, col)
|
||||
end
|
||||
|
||||
return M
|
||||
|
@ -226,18 +226,25 @@ function vim.tbl_add_reverse_lookup(o)
|
||||
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)
|
||||
-- Extends a list-like table with the values of another list-like table.
|
||||
--
|
||||
-- NOTE: This *mutates* dst!
|
||||
-- @see |extend()|
|
||||
--
|
||||
-- @param dst list which will be modified and appended to.
|
||||
-- @param src list from which values will be inserted.
|
||||
-- @param start Start index on src. defaults to 1
|
||||
-- @param finish Final index on src. defaults to #src
|
||||
-- @returns dst
|
||||
function vim.list_extend(dst, src, start, finish)
|
||||
vim.validate {
|
||||
dst = {dst, 't'};
|
||||
src = {src, 't'};
|
||||
start = {start, 'n', true};
|
||||
finish = {finish, 'n', true};
|
||||
}
|
||||
for i = start or 1, finish or #src do
|
||||
table.insert(dst, src[i])
|
||||
end
|
||||
return dst
|
||||
end
|
||||
|
@ -77,13 +77,18 @@ local function uri_to_fname(uri)
|
||||
else
|
||||
uri = uri:gsub('^file://', '')
|
||||
end
|
||||
|
||||
return uri_decode(uri)
|
||||
end
|
||||
|
||||
-- Return or create a buffer for a uri.
|
||||
local function uri_to_bufnr(uri)
|
||||
return vim.fn.bufadd((uri_to_fname(uri)))
|
||||
end
|
||||
|
||||
return {
|
||||
uri_from_fname = uri_from_fname,
|
||||
uri_from_bufnr = uri_from_bufnr,
|
||||
uri_to_fname = uri_to_fname,
|
||||
uri_to_bufnr = uri_to_bufnr,
|
||||
}
|
||||
-- vim:sw=2 ts=2 et
|
||||
|
@ -353,10 +353,14 @@ describe('lua stdlib', function()
|
||||
|
||||
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',
|
||||
eq('Error executing lua: .../shared.lua: src: expected table, got nil',
|
||||
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 ]])
|
||||
eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1) ]])
|
||||
eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 2) ]])
|
||||
eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1, -1) ]])
|
||||
eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, -1, 2) ]])
|
||||
end)
|
||||
|
||||
it('vim.tbl_add_reverse_lookup', function()
|
||||
|
76
test/functional/plugin/lsp/util_spec.lua
Normal file
76
test/functional/plugin/lsp/util_spec.lua
Normal file
@ -0,0 +1,76 @@
|
||||
local helpers = require('test.functional.helpers')(after_each)
|
||||
local eq = helpers.eq
|
||||
local exec_lua = helpers.exec_lua
|
||||
local dedent = helpers.dedent
|
||||
local insert = helpers.insert
|
||||
local clear = helpers.clear
|
||||
|
||||
describe('LSP util', function()
|
||||
local test_text = dedent([[
|
||||
First line of text
|
||||
Second line of text
|
||||
Third line of text
|
||||
Fourth line of text]])
|
||||
|
||||
local function reset()
|
||||
clear()
|
||||
insert(test_text)
|
||||
end
|
||||
|
||||
before_each(reset)
|
||||
|
||||
local function make_edit(y_0, x_0, y_1, x_1, text)
|
||||
return {
|
||||
range = {
|
||||
start = { line = y_0, character = x_0 };
|
||||
["end"] = { line = y_1, character = x_1 };
|
||||
};
|
||||
newText = type(text) == 'table' and table.concat(text, '\n') or (text or "");
|
||||
}
|
||||
end
|
||||
|
||||
local function buf_lines(bufnr)
|
||||
return exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr)
|
||||
end
|
||||
|
||||
describe('apply_edits', function()
|
||||
it('should apply simple edits', function()
|
||||
local edits = {
|
||||
make_edit(0, 0, 0, 0, {"123"});
|
||||
make_edit(1, 0, 1, 1, {"2"});
|
||||
make_edit(2, 0, 2, 2, {"3"});
|
||||
}
|
||||
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
|
||||
eq({
|
||||
'123First line of text';
|
||||
'2econd line of text';
|
||||
'3ird line of text';
|
||||
'Fourth line of text';
|
||||
}, buf_lines(1))
|
||||
end)
|
||||
|
||||
it('should apply complex edits', function()
|
||||
local edits = {
|
||||
make_edit(0, 0, 0, 0, {"", "12"});
|
||||
make_edit(0, 0, 0, 0, {"3", "foo"});
|
||||
make_edit(0, 1, 0, 1, {"bar", "123"});
|
||||
make_edit(0, #"First ", 0, #"First line of text", {"guy"});
|
||||
make_edit(1, 0, 1, #'Second', {"baz"});
|
||||
make_edit(2, #'Th', 2, #"Third", {"e next"});
|
||||
make_edit(3, #'', 3, #"Fourth", {"another line of text", "before this"});
|
||||
make_edit(3, #'Fourth', 3, #"Fourth line of text", {"!"});
|
||||
}
|
||||
exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1)
|
||||
eq({
|
||||
'';
|
||||
'123';
|
||||
'fooFbar';
|
||||
'123irst guy';
|
||||
'baz line of text';
|
||||
'The next line of text';
|
||||
'another line of text';
|
||||
'before this!';
|
||||
}, buf_lines(1))
|
||||
end)
|
||||
end)
|
||||
end)
|
Reference in New Issue
Block a user