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:
Ashkan Kiani
2019-11-24 04:44:50 -08:00
committed by GitHub
13 changed files with 880 additions and 757 deletions

View File

@ -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

View File

@ -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*

View File

@ -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
View 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

View File

@ -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

View 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

View File

@ -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`.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View 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)