- Use correct implementation of text_edits.
- Send indent options to rangeFormatting and formatting.
- Remove references to vim bindings and filetype from lsp.txt
- Add more examples to docs.
- Add before_init to allow changing initialize_params.
This commit is contained in:
Ashkan Kiani 2019-11-21 15:19:06 -08:00
parent 6a51401378
commit bcae04f6c6
5 changed files with 187 additions and 187 deletions

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
@ -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
@ -485,18 +400,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.
@ -511,43 +424,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

@ -160,13 +160,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)
@ -267,6 +267,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`
@ -385,7 +391,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)
@ -409,6 +414,10 @@ 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))

View File

@ -261,11 +261,14 @@ 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 or {};
options = options;
}
params.options[vim.type_idx] = vim.types.dictionary
return request('textDocument/formatting', params, function(_, _, result)
if not result then return end
util.apply_text_edits(result)
@ -278,6 +281,10 @@ function M.range_formatting(options, start_pos, end_pos)
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');
})
start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<')
end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>')
local params = {
@ -286,9 +293,8 @@ function M.range_formatting(options, start_pos, end_pos)
start = { line = start_pos[1]; character = start_pos[2]; };
["end"] = { line = end_pos[1]; character = end_pos[2]; };
};
options = options or {};
options = options;
}
params.options[vim.type_idx] = vim.types.dictionary
return request('textDocument/rangeFormatting', params, function(_, _, result)
if not result then return end
util.apply_text_edits(result)

View File

@ -26,89 +26,83 @@ local function remove_prefix(prefix, word)
return word:sub(prefix_length + 1)
end
function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines)
-- 0-indexing to 1-indexing makes things look a bit worse.
local i_0 = start_pos[1] + 1
local i_n = end_pos[1] + 1
local n = i_n - i_0 + 1
if not lines[i_0] or not lines[i_n] then
error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines})
-- 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
local prefix = ""
local suffix = lines[i_n]:sub(end_pos[2]+1)
lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1)
if start_pos[2] > 0 then
prefix = lines[i_0]:sub(1, start_pos[2])
-- lines[i_0] = lines[i_0]:sub(start.character+1)
local suffix = lines[i_n]:sub(B[2]+1)
if A[2] > 0 then
prefix = lines[i_0]:sub(1, A[2])
end
-- TODO(ashkan) figure out how to avoid copy here. likely by changing algo.
new_lines = vim.list_extend({}, new_lines)
new_lines = list_extend({}, new_lines)
if #suffix > 0 then
new_lines[#new_lines] = new_lines[#new_lines]..suffix
end
if #prefix > 0 then
new_lines[1] = prefix..new_lines[1]
end
if #new_lines >= n then
for i = 1, n do
lines[i + i_0 - 1] = new_lines[i]
end
for i = n+1,#new_lines do
table.insert(lines, i_n + 1, new_lines[i])
end
else
for i = 1, #new_lines do
lines[i + i_0 - 1] = new_lines[i]
end
for _ = #new_lines+1, n do
table.remove(lines, i_0 + #new_lines + 1)
local result = list_extend({}, lines, 1, i_0 - 1)
list_extend(result, new_lines)
list_extend(result, lines, i_n + 1)
return result
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
-- nvim.print("Start", #text_edits)
local start_line, finish_line = math.huge, -1
local cleaned = {}
for _, e in ipairs(text_edits) do
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)
for i, e in ipairs(cleaned) do
-- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines)
local y = 0
local x = 0
-- TODO(ashkan) this could be done in O(n) with dynamic programming
for j = 1, i-1 do
local o = cleaned[j]
-- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines)
if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then
y = y - (o.B[1] - o.A[1] + 1) + #o.lines
-- Same line
if #o.lines > 1 then
x = -e.A[2] + #o.lines[#o.lines]
else
if o.A[1] == e.A[1] then
-- Try to account for insertions.
-- TODO how to account for deletions?
x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines]
end
end
end
end
local A = {e.A[1] + y - start_line, e.A[2] + x}
local B = {e.B[1] + y - start_line, e.B[2] + x}
-- if x ~= 0 or y ~= 0 then
-- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines)
-- end
M.apply_edit_to_lines(lines, A, B, e.lines)
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

View File

@ -0,0 +1,78 @@
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
local command = helpers.command
local NIL = helpers.NIL
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)