mirror of
https://github.com/neovim/neovim.git
synced 2026-05-24 05:40:08 +00:00
Problem:
If a buffer's filetype changes after the LSP client has already
attached (e.g. from json to jsonc via a modeline), but the client
supports both filetypes, it stays attached. It does not notify the
server of the new languageId, causing the server to incorrectly process
the file using the old languageId.
Solution:
Save the languageId used during textDocument/didOpen, and send
textDocument/didClose + textDocument/didOpen when buffer's languageId
changed.
Lsp spec:
0003fb53f1/_specifications/lsp/3.18/textDocument/didOpen.md (L5)
> If the language id of a document changes, the client
> needs to send a textDocument/didClose to the server followed by a
> textDocument/didOpen with the new language id if the server handles
> the new language id as well.
AI-assisted: Gemini 3.1 Pro
4792 lines
152 KiB
Lua
4792 lines
152 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
|
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
|
|
|
local buf_lines = n.buf_lines
|
|
local command = n.command
|
|
local dedent = t.dedent
|
|
local exec_lua = n.exec_lua
|
|
local eq = t.eq
|
|
local eval = n.eval
|
|
local matches = t.matches
|
|
local pcall_err = t.pcall_err
|
|
local pesc = vim.pesc
|
|
local insert = n.insert
|
|
local fn = n.fn
|
|
local retry = t.retry
|
|
local stop = n.stop
|
|
local NIL = vim.NIL
|
|
local read_file = t.read_file
|
|
local write_file = t.write_file
|
|
local is_ci = t.is_ci
|
|
local api = n.api
|
|
local is_os = t.is_os
|
|
local skip = t.skip
|
|
local mkdir = t.mkdir
|
|
local tmpname = t.tmpname
|
|
|
|
local clear_notrace = t_lsp.clear_notrace
|
|
local create_server_definition = t_lsp.create_server_definition
|
|
local fake_lsp_code = t_lsp.fake_lsp_code
|
|
local fake_lsp_logfile = t_lsp.fake_lsp_logfile
|
|
local test_rpc_server = t_lsp.test_rpc_server
|
|
local create_tcp_echo_server = t_lsp.create_tcp_echo_server
|
|
|
|
local function get_buf_option(name, bufnr)
|
|
return exec_lua(function()
|
|
bufnr = bufnr or _G.BUFFER
|
|
return vim.api.nvim_get_option_value(name, { buf = bufnr })
|
|
end)
|
|
end
|
|
|
|
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
|
|
|
|
--- @param edits [integer, integer, integer, integer, string|string[]][]
|
|
--- @param encoding? string
|
|
local function apply_text_edits(edits, encoding)
|
|
local edits1 = vim.tbl_map(
|
|
--- @param edit [integer, integer, integer, integer, string|string[]]
|
|
function(edit)
|
|
return make_edit(unpack(edit))
|
|
end,
|
|
edits
|
|
)
|
|
exec_lua(function()
|
|
vim.lsp.util.apply_text_edits(edits1, 1, encoding or 'utf-16')
|
|
end)
|
|
end
|
|
|
|
--- @param notification_cb fun(method: 'body' | 'error', args: any)
|
|
local function verify_single_notification(notification_cb)
|
|
local called = false
|
|
n.run(nil, function(method, args)
|
|
notification_cb(method, args)
|
|
stop()
|
|
called = true
|
|
end, nil, 1000)
|
|
eq(true, called)
|
|
end
|
|
|
|
-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837
|
|
if skip(is_os('win')) then
|
|
return
|
|
end
|
|
|
|
describe('LSP', function()
|
|
before_each(function()
|
|
clear_notrace()
|
|
end)
|
|
|
|
after_each(function()
|
|
stop()
|
|
exec_lua(function()
|
|
vim.iter(vim.lsp.get_clients({ _uninitialized = true })):each(function(client)
|
|
client:stop(true)
|
|
end)
|
|
end)
|
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
|
end)
|
|
|
|
teardown(function()
|
|
os.remove(fake_lsp_logfile)
|
|
end)
|
|
|
|
describe('server_name specified', function()
|
|
before_each(function()
|
|
-- Run an instance of nvim on the file which contains our "scripts".
|
|
-- Pass TEST_NAME to pick the script.
|
|
local test_name = 'basic_init'
|
|
exec_lua(function()
|
|
_G.lsp = require('vim.lsp')
|
|
function _G.test__start_client()
|
|
return vim.lsp.start({
|
|
cmd_env = {
|
|
NVIM_LOG_FILE = fake_lsp_logfile,
|
|
NVIM_APPNAME = 'nvim_lsp_test',
|
|
},
|
|
cmd = {
|
|
vim.v.progpath,
|
|
'-l',
|
|
fake_lsp_code,
|
|
test_name,
|
|
},
|
|
workspace_folders = {
|
|
{
|
|
uri = 'file://' .. vim.uv.cwd(),
|
|
name = 'test_folder',
|
|
},
|
|
},
|
|
}, { attach = false })
|
|
end
|
|
_G.TEST_CLIENT1 = _G.test__start_client()
|
|
end)
|
|
end)
|
|
|
|
it('start_client(), Client:stop()', function()
|
|
retry(nil, 4000, function()
|
|
eq(
|
|
1,
|
|
exec_lua(function()
|
|
return #vim.lsp.get_clients()
|
|
end)
|
|
)
|
|
end)
|
|
eq(
|
|
2,
|
|
exec_lua(function()
|
|
_G.TEST_CLIENT2 = _G.test__start_client()
|
|
return _G.TEST_CLIENT2
|
|
end)
|
|
)
|
|
eq(
|
|
3,
|
|
exec_lua(function()
|
|
_G.TEST_CLIENT3 = _G.test__start_client()
|
|
return _G.TEST_CLIENT3
|
|
end)
|
|
)
|
|
retry(nil, 4000, function()
|
|
eq(
|
|
3,
|
|
exec_lua(function()
|
|
return #vim.lsp.get_clients()
|
|
end)
|
|
)
|
|
end)
|
|
|
|
eq(
|
|
false,
|
|
exec_lua(function()
|
|
return vim.lsp.get_client_by_id(_G.TEST_CLIENT1) == nil
|
|
end)
|
|
)
|
|
eq(
|
|
false,
|
|
exec_lua(function()
|
|
return vim.lsp.get_client_by_id(_G.TEST_CLIENT1).is_stopped()
|
|
end)
|
|
)
|
|
exec_lua(function()
|
|
return vim.lsp.get_client_by_id(_G.TEST_CLIENT1).stop()
|
|
end)
|
|
retry(nil, 4000, function()
|
|
eq(
|
|
2,
|
|
exec_lua(function()
|
|
return #vim.lsp.get_clients()
|
|
end)
|
|
)
|
|
end)
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
return vim.lsp.get_client_by_id(_G.TEST_CLIENT1) == nil
|
|
end)
|
|
)
|
|
|
|
exec_lua(function()
|
|
vim.lsp.get_client_by_id(_G.TEST_CLIENT2):stop()
|
|
vim.lsp.get_client_by_id(_G.TEST_CLIENT3):stop()
|
|
end)
|
|
retry(nil, 4000, function()
|
|
eq(
|
|
0,
|
|
exec_lua(function()
|
|
return #vim.lsp.get_clients()
|
|
end)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
it('does not reuse an already-stopping client #33616', function()
|
|
-- we immediately try to start a second client with the same name/root
|
|
-- before the first one has finished shutting down; we must get a new id.
|
|
local clients = exec_lua(function()
|
|
local client1 = assert(vim.lsp.start({
|
|
name = 'dup-test',
|
|
cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' },
|
|
}, { attach = false }))
|
|
vim.lsp.get_client_by_id(client1):stop()
|
|
local client2 = assert(vim.lsp.start({
|
|
name = 'dup-test',
|
|
cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' },
|
|
}, { attach = false }))
|
|
return { client1, client2 }
|
|
end)
|
|
local c1, c2 = clients[1], clients[2]
|
|
eq(false, c1 == c2, 'Expected a fresh client while the old one is stopping')
|
|
end)
|
|
end)
|
|
|
|
describe('basic_init test', function()
|
|
it('should run correctly', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'test', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_init = function(client, _)
|
|
-- client is a dummy object which will queue up commands to be run
|
|
-- once the server initializes. It can't accept lua callbacks or
|
|
-- other types that may be unserializable for now.
|
|
client:stop()
|
|
end,
|
|
-- If the program timed out, then code will be nil.
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
-- Note that NIL must be used here.
|
|
-- on_handler(err, method, result, client_id)
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... })
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should fail', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'test', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_init = function(client)
|
|
client:notify('test')
|
|
client:stop()
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(101, code, 'exit code') -- See fake-lsp-server.lua
|
|
eq(0, signal, 'exit signal')
|
|
t.assert_log(
|
|
pesc([[assert_eq failed: left == "\"shutdown\"", right == "\"test\""]]),
|
|
fake_lsp_logfile
|
|
)
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should send didChangeConfiguration after initialize if there are settings', function()
|
|
test_rpc_server({
|
|
test_name = 'basic_init_did_change_configuration',
|
|
on_init = function(client, _)
|
|
client:stop()
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
settings = {
|
|
dummy = 1,
|
|
},
|
|
})
|
|
end)
|
|
|
|
it(
|
|
"should set the client's offset_encoding when positionEncoding capability is supported",
|
|
function()
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server({
|
|
capabilities = {
|
|
positionEncoding = 'utf-8',
|
|
},
|
|
})
|
|
|
|
local client_id = vim.lsp.start({
|
|
name = 'dummy',
|
|
cmd = server.cmd,
|
|
})
|
|
|
|
if not client_id then
|
|
return 'vim.lsp.start did not return client_id'
|
|
end
|
|
|
|
local client = vim.lsp.get_client_by_id(client_id)
|
|
if not client then
|
|
return 'No client found with id ' .. client_id
|
|
end
|
|
return client.offset_encoding
|
|
end)
|
|
eq('utf-8', result)
|
|
end
|
|
)
|
|
|
|
it('should succeed with manual shutdown', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1, request_id = 2, version = 0 } },
|
|
{ NIL, {}, { method = 'test', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_init = function(client)
|
|
eq(0, client.server_capabilities().textDocumentSync.change)
|
|
client:request('shutdown')
|
|
client:notify('exit')
|
|
client:stop()
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should detach buffer in response to nvim_buf_detach', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_finish',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
end)
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
return vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
)
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
return vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
)
|
|
exec_lua(function()
|
|
vim.cmd(_G.BUFFER .. 'bwipeout')
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
client:notify('finish')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
exec_lua(function()
|
|
return vim.lsp.buf_detach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
eq(
|
|
false,
|
|
exec_lua(function()
|
|
return vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
)
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should fire autocommands on attach and detach', function()
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_create_autocmd('LspAttach', {
|
|
callback = function(ev)
|
|
local client0 = assert(vim.lsp.get_client_by_id(ev.data.client_id))
|
|
vim.g.lsp_attached = client0.name
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('LspDetach', {
|
|
callback = function(ev)
|
|
local client0 = assert(vim.lsp.get_client_by_id(ev.data.client_id))
|
|
vim.g.lsp_detached = client0.name
|
|
end,
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
return vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
)
|
|
client:notify('finish')
|
|
end,
|
|
on_handler = function(_, _, ctx)
|
|
if ctx.method == 'finish' then
|
|
eq('basic_init', api.nvim_get_var('lsp_attached'))
|
|
exec_lua(function()
|
|
return vim.lsp.buf_detach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
eq('basic_init', api.nvim_get_var('lsp_detached'))
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should set default options on attach', function()
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'set_defaults_all_capabilities',
|
|
on_init = function(_client)
|
|
client = _client
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
end,
|
|
on_handler = function(_, _, ctx)
|
|
if ctx.method == 'test' then
|
|
eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc'))
|
|
eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc'))
|
|
eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr'))
|
|
eq('', get_buf_option('keywordprg'))
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
local keymap --- @type table<string,any>
|
|
local called = false
|
|
local origin = vim.lsp.buf.hover
|
|
vim.lsp.buf.hover = function()
|
|
called = true
|
|
end
|
|
vim._with({ buf = _G.BUFFER }, function()
|
|
keymap = vim.fn.maparg('K', 'n', false, true)
|
|
end)
|
|
keymap.callback()
|
|
vim.lsp.buf.hover = origin
|
|
return called
|
|
end)
|
|
)
|
|
client:stop()
|
|
end
|
|
end,
|
|
on_exit = function(_, _)
|
|
eq('', get_buf_option('tagfunc'))
|
|
eq('', get_buf_option('omnifunc'))
|
|
eq('', get_buf_option('formatexpr'))
|
|
eq(
|
|
true,
|
|
exec_lua(function()
|
|
local keymap --- @type string
|
|
vim._with({ buf = _G.BUFFER }, function()
|
|
keymap = vim.fn.maparg('K', 'n', false, false)
|
|
end)
|
|
return keymap:match('<Lua %d+: .*runtime/lua/vim/lsp%.lua:%d+>') ~= nil
|
|
end)
|
|
)
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should overwrite options set by ftplugins', function()
|
|
if t.is_zig_build() then
|
|
return pending('TODO: broken with zig build')
|
|
end
|
|
local client --- @type vim.lsp.Client
|
|
local BUFFER_1 --- @type integer
|
|
local BUFFER_2 --- @type integer
|
|
test_rpc_server {
|
|
test_name = 'set_defaults_all_capabilities',
|
|
on_init = function(_client)
|
|
client = _client
|
|
exec_lua(function()
|
|
vim.api.nvim_command('filetype plugin on')
|
|
BUFFER_1 = vim.api.nvim_create_buf(false, true)
|
|
BUFFER_2 = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_set_option_value('filetype', 'man', { buf = BUFFER_1 })
|
|
vim.api.nvim_set_option_value('filetype', 'xml', { buf = BUFFER_2 })
|
|
end)
|
|
|
|
-- Sanity check to ensure that some values are set after setting filetype.
|
|
eq("v:lua.require'man'.goto_tag", get_buf_option('tagfunc', BUFFER_1))
|
|
eq('xmlcomplete#CompleteTags', get_buf_option('omnifunc', BUFFER_2))
|
|
eq('xmlformat#Format()', get_buf_option('formatexpr', BUFFER_2))
|
|
|
|
exec_lua(function()
|
|
vim.lsp.buf_attach_client(BUFFER_1, _G.TEST_RPC_CLIENT_ID)
|
|
vim.lsp.buf_attach_client(BUFFER_2, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
end,
|
|
on_handler = function(_, _, ctx)
|
|
if ctx.method == 'test' then
|
|
eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc', BUFFER_1))
|
|
eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc', BUFFER_2))
|
|
eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr', BUFFER_2))
|
|
client:stop()
|
|
end
|
|
end,
|
|
on_exit = function(_, _)
|
|
eq('', get_buf_option('tagfunc', BUFFER_1))
|
|
eq('', get_buf_option('omnifunc', BUFFER_2))
|
|
eq('', get_buf_option('formatexpr', BUFFER_2))
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should not overwrite user-defined options', function()
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'set_defaults_all_capabilities',
|
|
on_init = function(_client)
|
|
client = _client
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_set_option_value('tagfunc', 'tfu', { buf = _G.BUFFER })
|
|
vim.api.nvim_set_option_value('omnifunc', 'ofu', { buf = _G.BUFFER })
|
|
vim.api.nvim_set_option_value('formatexpr', 'fex', { buf = _G.BUFFER })
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
end)
|
|
end,
|
|
on_handler = function(_, _, ctx)
|
|
if ctx.method == 'test' then
|
|
eq('tfu', get_buf_option('tagfunc'))
|
|
eq('ofu', get_buf_option('omnifunc'))
|
|
eq('fex', get_buf_option('formatexpr'))
|
|
client:stop()
|
|
end
|
|
end,
|
|
on_exit = function(_, _)
|
|
eq('tfu', get_buf_option('tagfunc'))
|
|
eq('ofu', get_buf_option('omnifunc'))
|
|
eq('fex', get_buf_option('formatexpr'))
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should detach buffer on bufwipe', function()
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
|
local detach_called1 = false
|
|
local detach_called2 = false
|
|
vim.api.nvim_create_autocmd('LspDetach', {
|
|
buf = bufnr1,
|
|
callback = function()
|
|
detach_called1 = true
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('LspDetach', {
|
|
buf = bufnr2,
|
|
callback = function()
|
|
detach_called2 = true
|
|
end,
|
|
})
|
|
vim.api.nvim_set_current_buf(bufnr1)
|
|
local client_id = assert(vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd }))
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
vim.api.nvim_set_current_buf(bufnr2)
|
|
vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })
|
|
assert(vim.tbl_count(client.attached_buffers) == 2)
|
|
vim.api.nvim_buf_delete(bufnr1, { force = true })
|
|
assert(vim.tbl_count(client.attached_buffers) == 1)
|
|
vim.api.nvim_buf_delete(bufnr2, { force = true })
|
|
assert(vim.tbl_count(client.attached_buffers) == 0)
|
|
return detach_called1 and detach_called2
|
|
end)
|
|
eq(true, result)
|
|
end)
|
|
|
|
it('should not re-attach buffer if it was deleted in on_init #28575', function()
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
vim.schedule(function()
|
|
callback(nil, { capabilities = {} })
|
|
end)
|
|
end,
|
|
},
|
|
})
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
local on_init_called = false
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'detach-dummy',
|
|
cmd = server.cmd,
|
|
on_init = function()
|
|
vim.api.nvim_buf_delete(bufnr, {})
|
|
on_init_called = true
|
|
end,
|
|
}))
|
|
vim.lsp.buf_attach_client(bufnr, client_id)
|
|
local ok = vim.wait(1000, function()
|
|
return on_init_called
|
|
end)
|
|
assert(ok, 'on_init was not called')
|
|
end)
|
|
end)
|
|
|
|
it('should allow on_lines + nvim_buf_delete during LSP initialization #28575', function()
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
local initialized = false
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
vim.schedule(function()
|
|
callback(nil, { capabilities = {} })
|
|
initialized = true
|
|
end)
|
|
end,
|
|
},
|
|
})
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_set_current_buf(bufnr)
|
|
vim.lsp.start({
|
|
name = 'detach-dummy',
|
|
cmd = server.cmd,
|
|
})
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'hello' })
|
|
vim.api.nvim_buf_delete(bufnr, {})
|
|
local ok = vim.wait(1000, function()
|
|
return initialized
|
|
end)
|
|
assert(ok, 'lsp did not initialize')
|
|
end)
|
|
end)
|
|
|
|
it('client should return settings via workspace/configuration handler', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{
|
|
items = {
|
|
{ section = 'testSetting1' },
|
|
{ section = 'testSetting2' },
|
|
{ section = 'test.Setting3' },
|
|
{ section = 'test.Setting4' },
|
|
{},
|
|
{ section = '' },
|
|
},
|
|
},
|
|
{ method = 'workspace/configuration', client_id = 1 },
|
|
},
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_workspace_configuration',
|
|
on_init = function(_client)
|
|
client = _client
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
local client0 = vim.lsp.get_client_by_id(_G.TEST_RPC_CLIENT_ID)
|
|
client0.settings = {
|
|
testSetting1 = true,
|
|
testSetting2 = false,
|
|
test = { Setting3 = 'nested' },
|
|
}
|
|
end)
|
|
end
|
|
if ctx.method == 'workspace/configuration' then
|
|
local server_result = exec_lua(
|
|
[[
|
|
local method, params = ...
|
|
return require 'vim.lsp.handlers'['workspace/configuration'](
|
|
err,
|
|
params,
|
|
{ method = method, client_id = _G.TEST_RPC_CLIENT_ID }
|
|
)
|
|
]],
|
|
ctx.method,
|
|
result
|
|
)
|
|
client:notify('workspace/configuration', server_result)
|
|
end
|
|
if ctx.method == 'shutdown' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it(
|
|
'workspace/configuration returns NIL per section if client was started without config.settings',
|
|
function()
|
|
local result = nil
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_init = function(c)
|
|
c.stop()
|
|
end,
|
|
on_setup = function()
|
|
result = exec_lua(function()
|
|
local result0 = {
|
|
items = {
|
|
{ section = 'foo' },
|
|
{ section = 'bar' },
|
|
},
|
|
}
|
|
return vim.lsp.handlers['workspace/configuration'](
|
|
nil,
|
|
result0,
|
|
{ client_id = _G.TEST_RPC_CLIENT_ID }
|
|
)
|
|
end)
|
|
end,
|
|
}
|
|
eq({ NIL, NIL }, result)
|
|
end
|
|
)
|
|
|
|
it('should verify capabilities sent', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'basic_check_capabilities',
|
|
on_init = function(client)
|
|
client:stop()
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq({ includeText = false }, client.server_capabilities().textDocumentSync.save)
|
|
eq(false, client.server_capabilities().codeLensProvider)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('BufWritePost sends didSave with bool textDocumentSync.save', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'text_document_sync_save_bool',
|
|
on_init = function(c)
|
|
client = c
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_get_current_buf()
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
vim.api.nvim_exec_autocmds('BufWritePost', { buf = _G.BUFFER, modeline = false })
|
|
end)
|
|
else
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('saveas sends didOpen to multiple attached servers if filename changed', function()
|
|
local tmpfile_new = tmpname(false)
|
|
exec_lua(create_server_definition)
|
|
local messages = exec_lua(function()
|
|
local server1 = _G._create_server()
|
|
local server2 = _G._create_server()
|
|
local client1_id = assert(vim.lsp.start({ name = 'dummy1', cmd = server1.cmd }))
|
|
local client2_id = assert(vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }))
|
|
|
|
vim.cmd('saveas ' .. tmpfile_new)
|
|
|
|
vim.lsp.get_client_by_id(client1_id):stop()
|
|
vim.lsp.get_client_by_id(client2_id):stop()
|
|
|
|
return {
|
|
server1 = server1.messages,
|
|
server2 = server2.messages,
|
|
}
|
|
end)
|
|
eq('textDocument/didClose', messages.server1[3].method)
|
|
eq('textDocument/didOpen', messages.server1[4].method)
|
|
eq('textDocument/didSave', messages.server1[5].method)
|
|
|
|
eq('textDocument/didClose', messages.server2[3].method)
|
|
eq('textDocument/didOpen', messages.server2[4].method)
|
|
eq('textDocument/didSave', messages.server2[5].method)
|
|
end)
|
|
|
|
it('BufWritePre does not send notifications if server lacks willSave capabilities', function()
|
|
exec_lua(create_server_definition)
|
|
local messages = exec_lua(function()
|
|
local server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = {
|
|
willSave = false,
|
|
willSaveWaitUntil = false,
|
|
},
|
|
},
|
|
})
|
|
local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd }))
|
|
local buf = vim.api.nvim_get_current_buf()
|
|
vim.api.nvim_exec_autocmds('BufWritePre', { buf = buf, modeline = false })
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
return server.messages
|
|
end)
|
|
eq(4, #messages)
|
|
eq('initialize', messages[1].method)
|
|
eq('initialized', messages[2].method)
|
|
eq('shutdown', messages[3].method)
|
|
eq('exit', messages[4].method)
|
|
end)
|
|
|
|
it('BufWritePre sends willSave / willSaveWaitUntil, applies textEdits', function()
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = {
|
|
willSave = true,
|
|
willSaveWaitUntil = true,
|
|
},
|
|
},
|
|
handlers = {
|
|
['textDocument/willSaveWaitUntil'] = function(_, _, callback)
|
|
local text_edit = {
|
|
range = {
|
|
start = { line = 0, character = 0 },
|
|
['end'] = { line = 0, character = 0 },
|
|
},
|
|
newText = 'Hello',
|
|
}
|
|
callback(nil, { text_edit })
|
|
end,
|
|
},
|
|
})
|
|
local buf = vim.api.nvim_get_current_buf()
|
|
local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd }))
|
|
vim.api.nvim_exec_autocmds('BufWritePre', { buf = buf, modeline = false })
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
return {
|
|
messages = server.messages,
|
|
lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true),
|
|
}
|
|
end)
|
|
local messages = result.messages
|
|
eq('textDocument/willSave', messages[3].method)
|
|
eq('textDocument/willSaveWaitUntil', messages[4].method)
|
|
eq({ 'Hello' }, result.lines)
|
|
end)
|
|
|
|
it('saveas sends didOpen if filename changed', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server({
|
|
test_name = 'text_document_save_did_open',
|
|
on_init = function(c)
|
|
client = c
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'start' then
|
|
local tmpfile_old = tmpname()
|
|
local tmpfile_new = tmpname(false)
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_get_current_buf()
|
|
vim.api.nvim_buf_set_name(_G.BUFFER, tmpfile_old)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, true, { 'help me' })
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
vim._with({ buf = _G.BUFFER }, function()
|
|
vim.cmd('saveas ' .. tmpfile_new)
|
|
end)
|
|
end)
|
|
else
|
|
client:stop()
|
|
end
|
|
end,
|
|
})
|
|
end)
|
|
|
|
it('BufWritePost sends didSave including text if server capability is set', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'text_document_sync_save_includeText',
|
|
on_init = function(c)
|
|
client = c
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_get_current_buf()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, true, { 'help me' })
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
vim.api.nvim_exec_autocmds('BufWritePost', { buf = _G.BUFFER, modeline = false })
|
|
end)
|
|
else
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('client:supports_methods() should validate capabilities', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'capabilities_for_client_supports_method',
|
|
on_init = function(client)
|
|
client:stop()
|
|
local expected_sync_capabilities = {
|
|
change = 1,
|
|
openClose = true,
|
|
save = { includeText = false },
|
|
willSave = false,
|
|
willSaveWaitUntil = false,
|
|
}
|
|
eq(expected_sync_capabilities, client.server_capabilities().textDocumentSync)
|
|
eq(true, client.server_capabilities().completionProvider)
|
|
eq(true, client.server_capabilities().hoverProvider)
|
|
eq(false, client.server_capabilities().definitionProvider)
|
|
eq(false, client.server_capabilities().renameProvider)
|
|
eq(true, client.server_capabilities().codeLensProvider.resolveProvider)
|
|
|
|
-- known methods for resolved capabilities
|
|
eq(true, client:supports_method('textDocument/hover'))
|
|
eq(false, client:supports_method('textDocument/definition'))
|
|
|
|
-- Self-mapped methods do not have a related server capability and should be assumed
|
|
-- to be supported.
|
|
eq(true, client:supports_method('shutdown'))
|
|
|
|
-- unknown methods are assumed to be supported.
|
|
eq(true, client:supports_method('unknown-method'))
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should not call unsupported_method when trying to call an unsupported method', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'capabilities_for_client_supports_method',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_get_current_buf()
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)
|
|
vim.lsp.handlers['textDocument/typeDefinition'] = function() end
|
|
vim.cmd(_G.BUFFER .. 'bwipeout')
|
|
end)
|
|
end,
|
|
on_init = function(client)
|
|
client:stop()
|
|
exec_lua(function()
|
|
vim.lsp.buf.type_definition()
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it(
|
|
'should not call unsupported_method when no client and trying to call an unsupported method',
|
|
function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'capabilities_for_client_supports_method',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
vim.lsp.handlers['textDocument/typeDefinition'] = function() end
|
|
end)
|
|
end,
|
|
on_init = function(client)
|
|
client:stop()
|
|
exec_lua(function()
|
|
vim.lsp.buf.type_definition()
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(...)
|
|
eq(table.remove(expected_handlers), { ... }, 'expected handler')
|
|
end,
|
|
}
|
|
end
|
|
)
|
|
|
|
it('should not forward RequestCancelled to callback', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_forward_request_cancelled',
|
|
on_init = function(_client)
|
|
_client:request('error_code_test')
|
|
client = _client
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should forward ServerCancelled to callback', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
{ code = -32802 },
|
|
NIL,
|
|
{ method = 'error_code_test', bufnr = 1, client_id = 1, request_id = 2, version = 0 },
|
|
},
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_forward_server_cancelled',
|
|
on_init = function(_client)
|
|
_client:request('error_code_test')
|
|
client = _client
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler')
|
|
if ctx.method ~= 'finish' then
|
|
client:notify('finish')
|
|
end
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should forward ContentModified to callback', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
{ code = -32801 },
|
|
NIL,
|
|
{ method = 'error_code_test', bufnr = 1, client_id = 1, request_id = 2, version = 0 },
|
|
},
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_forward_content_modified',
|
|
on_init = function(_client)
|
|
_client:request('error_code_test')
|
|
client = _client
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler')
|
|
if ctx.method ~= 'finish' then
|
|
client:notify('finish')
|
|
end
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should track pending requests to the language server', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{},
|
|
{ method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 },
|
|
},
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_pending_request_tracked',
|
|
on_init = function(_client)
|
|
client = _client
|
|
client:request('slow_request')
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq('slow_request', request.method)
|
|
eq('pending', request.type)
|
|
client:notify('release')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
|
|
if ctx.method == 'slow_request' then
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq(nil, request)
|
|
client:notify('finish')
|
|
end
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should track cancel requests to the language server', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_cancel_request_tracked',
|
|
on_init = function(_client)
|
|
client = _client
|
|
client:request('slow_request')
|
|
client:cancel_request(2)
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq('slow_request', request.method)
|
|
eq('cancel', request.type)
|
|
client:notify('release')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq(nil, request)
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should clear pending and cancel requests on reply', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{},
|
|
{ method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 },
|
|
},
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_tracked_requests_cleared',
|
|
on_init = function(_client)
|
|
client = _client
|
|
client:request('slow_request')
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq('slow_request', request.method)
|
|
eq('pending', request.type)
|
|
client:cancel_request(2)
|
|
request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq('slow_request', request.method)
|
|
eq('cancel', request.type)
|
|
client:notify('release')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
|
|
if ctx.method == 'slow_request' then
|
|
local request = exec_lua(function()
|
|
return _G.TEST_RPC_CLIENT.requests[2]
|
|
end)
|
|
eq(nil, request)
|
|
client:notify('finish')
|
|
end
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('request should not be pending for sync responses (in-process LS)', function()
|
|
--- @type boolean
|
|
local pending_request = exec_lua(function()
|
|
local function server(dispatchers)
|
|
local closing = false
|
|
local srv = {}
|
|
local request_id = 0
|
|
|
|
function srv.request(method, _params, callback, notify_reply_callback)
|
|
if method == 'textDocument/formatting' then
|
|
callback(nil, {})
|
|
elseif method == 'initialize' then
|
|
callback(nil, {
|
|
capabilities = {
|
|
textDocument = {
|
|
formatting = true,
|
|
},
|
|
},
|
|
})
|
|
elseif method == 'shutdown' then
|
|
callback(nil, nil)
|
|
end
|
|
request_id = request_id + 1
|
|
if notify_reply_callback then
|
|
notify_reply_callback(request_id)
|
|
end
|
|
return true, request_id
|
|
end
|
|
|
|
function srv.notify(method)
|
|
if method == 'exit' then
|
|
dispatchers.on_exit(0, 15)
|
|
end
|
|
end
|
|
function srv.is_closing()
|
|
return closing
|
|
end
|
|
function srv.terminate()
|
|
closing = true
|
|
end
|
|
|
|
return srv
|
|
end
|
|
|
|
local client_id = assert(vim.lsp.start({ cmd = server }))
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
|
|
local ok, request_id = client:request('textDocument/formatting', {})
|
|
assert(ok)
|
|
|
|
local has_pending = client.requests[request_id] ~= nil
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
|
|
return has_pending
|
|
end)
|
|
|
|
eq(false, pending_request, 'expected no pending requests')
|
|
end)
|
|
|
|
it('should trigger LspRequest autocmd when requests table changes', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{},
|
|
{ method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 },
|
|
},
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'check_tracked_requests_cleared',
|
|
on_init = function(_client)
|
|
command('let g:requests = 0')
|
|
command('autocmd LspRequest * let g:requests+=1')
|
|
client = _client
|
|
client:request('slow_request')
|
|
eq(1, eval('g:requests'))
|
|
client:cancel_request(2)
|
|
eq(2, eval('g:requests'))
|
|
client:notify('release')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
eq(0, #expected_handlers, 'did not call expected handler')
|
|
eq(3, eval('g:requests'))
|
|
end,
|
|
on_handler = function(err, _, ctx)
|
|
eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler')
|
|
if ctx.method == 'slow_request' then
|
|
client:notify('finish')
|
|
end
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should not send didOpen if the buffer closes before init', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_finish',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
assert(_G.TEST_RPC_CLIENT_ID == 1)
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
assert(vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
vim.cmd(_G.BUFFER .. 'bwipeout')
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
client:notify('finish')
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body sent attaching before init', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(
|
|
vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID),
|
|
'Already attached, returns true'
|
|
)
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body sent attaching after init', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange full', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'boop',
|
|
})
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange full with noeol', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_noeol',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
vim.bo[_G.BUFFER].eol = false
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local full_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(full_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'boop',
|
|
})
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should send correct range for inlay hints with noeol', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{},
|
|
{
|
|
method = 'textDocument/inlayHint',
|
|
params = {
|
|
textDocument = {
|
|
uri = 'file://',
|
|
},
|
|
range = {
|
|
start = { line = 0, character = 0 },
|
|
['end'] = { line = 1, character = 3 },
|
|
},
|
|
},
|
|
bufnr = 2,
|
|
client_id = 1,
|
|
request_id = 2,
|
|
version = 0,
|
|
},
|
|
},
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'inlay_hint',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
vim.bo[_G.BUFFER].eol = false
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
eq(true, client:supports_method('textDocument/inlayHint'))
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { bufnr = _G.BUFFER })
|
|
end)
|
|
end
|
|
if ctx.method == 'textDocument/inlayHint' then
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange incremental', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_incremental',
|
|
options = {
|
|
allow_incremental_sync = true,
|
|
},
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local sync_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental
|
|
end)
|
|
eq(sync_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'123boop',
|
|
})
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange incremental with debounce', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_incremental',
|
|
options = {
|
|
allow_incremental_sync = true,
|
|
debounce_text_changes = 5,
|
|
},
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local sync_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental
|
|
end)
|
|
eq(sync_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'123boop',
|
|
})
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
-- TODO(askhan) we don't support full for now, so we can disable these tests.
|
|
pending('should check the body and didChange incremental normal mode editing', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_incremental_editing',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local sync_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental
|
|
end)
|
|
eq(sync_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
n.command('normal! 1Go')
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange full with 2 changes', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_multi',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local sync_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(sync_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'321',
|
|
})
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'boop',
|
|
})
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('should check the body and didChange full lifecycle', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'basic_check_buffer_open_and_change_multi_and_close',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
local sync_kind = exec_lua(function()
|
|
return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full
|
|
end)
|
|
eq(sync_kind, client.server_capabilities().textDocumentSync.change)
|
|
eq(true, client.server_capabilities().textDocumentSync.openClose)
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
if ctx.method == 'start' then
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'321',
|
|
})
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, {
|
|
'boop',
|
|
})
|
|
vim.api.nvim_command(_G.BUFFER .. 'bwipeout')
|
|
end)
|
|
client:notify('finish')
|
|
end
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
|
|
it('vim.lsp.start when existing client has no workspace_folders', function()
|
|
exec_lua(create_server_definition)
|
|
eq(
|
|
{ 2, 'foo', 'foo' },
|
|
exec_lua(function()
|
|
local server = _G._create_server()
|
|
vim.lsp.start { cmd = server.cmd, name = 'foo' }
|
|
vim.lsp.start { cmd = server.cmd, name = 'foo', root_dir = 'bar' }
|
|
local foos = vim.lsp.get_clients()
|
|
return { #foos, foos[1].name, foos[2].name }
|
|
end)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
describe('parsing tests', function()
|
|
local body = '{"jsonrpc":"2.0","id": 1,"method":"demo"}'
|
|
|
|
before_each(function()
|
|
exec_lua(create_tcp_echo_server)
|
|
end)
|
|
|
|
it('should catch error while parsing invalid header', function()
|
|
-- No whitespace is allowed between the header field-name and colon.
|
|
-- See https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
|
local field = 'Content-Length : 10 \r\n'
|
|
exec_lua(function()
|
|
_G._send_msg_to_server(field .. '\r\n')
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string, number]
|
|
eq('error', method)
|
|
eq(1, args[2])
|
|
matches(vim.pesc('Content-Length not found in header: ' .. field) .. '$', args[1])
|
|
end)
|
|
end)
|
|
|
|
it('value of Content-Length shoud be number', function()
|
|
local value = '123 foo'
|
|
exec_lua(function()
|
|
_G._send_msg_to_server('Content-Length: ' .. value .. '\r\n\r\n')
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string, number]
|
|
eq('error', method)
|
|
eq(1, args[2])
|
|
matches('value of Content%-Length is not number: ' .. value .. '$', args[1])
|
|
end)
|
|
end)
|
|
|
|
it('field name is case-insensitive', function()
|
|
exec_lua(function()
|
|
_G._send_msg_to_server('CONTENT-Length: ' .. #body .. ' \r\n\r\n' .. body)
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string]
|
|
eq('body', method)
|
|
eq(body, args[1])
|
|
end)
|
|
end)
|
|
|
|
it("ignore some lines ending with LF that don't contain content-length", function()
|
|
exec_lua(function()
|
|
_G._send_msg_to_server(
|
|
'foo \n bar\nWARN: no common words.\nContent-Length: ' .. #body .. ' \r\n\r\n' .. body
|
|
)
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string]
|
|
eq('body', method)
|
|
eq(body, args[1])
|
|
end)
|
|
end)
|
|
|
|
it('should not trim vim.NIL from the end of a list', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
{ NIL, {}, { method = 'finish', client_id = 1 } },
|
|
{
|
|
NIL,
|
|
{
|
|
arguments = { 'EXTRACT_METHOD', { metadata = { field = vim.NIL } }, 3, 0, 6123, NIL },
|
|
command = 'refactor.perform',
|
|
title = 'EXTRACT_METHOD',
|
|
},
|
|
{ method = 'workspace/executeCommand', client_id = 1 },
|
|
},
|
|
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
}
|
|
local client --- @type vim.lsp.Client
|
|
test_rpc_server {
|
|
test_name = 'decode_nil',
|
|
on_setup = function()
|
|
exec_lua(function()
|
|
_G.BUFFER = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, {
|
|
'testing',
|
|
'123',
|
|
})
|
|
end)
|
|
end,
|
|
on_init = function(_client)
|
|
client = _client
|
|
exec_lua(function()
|
|
assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID))
|
|
end)
|
|
end,
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
on_handler = function(err, result, ctx)
|
|
eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler')
|
|
if ctx.method == 'finish' then
|
|
client:stop()
|
|
end
|
|
end,
|
|
}
|
|
end)
|
|
end)
|
|
|
|
describe('apply vscode text_edits', function()
|
|
it('single replace', function()
|
|
insert('012345678901234567890123456789')
|
|
apply_text_edits({
|
|
{ 0, 3, 0, 6, { 'Hello' } },
|
|
})
|
|
eq({ '012Hello678901234567890123456789' }, buf_lines(1))
|
|
end)
|
|
|
|
it('two replaces', function()
|
|
insert('012345678901234567890123456789')
|
|
apply_text_edits({
|
|
{ 0, 3, 0, 6, { 'Hello' } },
|
|
{ 0, 6, 0, 9, { 'World' } },
|
|
})
|
|
eq({ '012HelloWorld901234567890123456789' }, buf_lines(1))
|
|
end)
|
|
|
|
it('same start pos insert are kept in order', function()
|
|
insert('012345678901234567890123456789')
|
|
apply_text_edits({
|
|
{ 0, 3, 0, 3, { 'World' } },
|
|
{ 0, 3, 0, 3, { 'Hello' } },
|
|
})
|
|
eq({ '012WorldHello345678901234567890123456789' }, buf_lines(1))
|
|
end)
|
|
|
|
it('same start pos insert and replace are kept in order', function()
|
|
insert('012345678901234567890123456789')
|
|
apply_text_edits({
|
|
{ 0, 3, 0, 3, { 'World' } },
|
|
{ 0, 3, 0, 3, { 'Hello' } },
|
|
{ 0, 3, 0, 8, { 'No' } },
|
|
})
|
|
eq({ '012WorldHelloNo8901234567890123456789' }, buf_lines(1))
|
|
end)
|
|
|
|
it('multiline', function()
|
|
exec_lua(function()
|
|
vim.api.nvim_buf_set_lines(1, 0, 0, true, { ' {', ' "foo": "bar"', ' }' })
|
|
end)
|
|
eq({ ' {', ' "foo": "bar"', ' }', '' }, buf_lines(1))
|
|
apply_text_edits({
|
|
{ 0, 0, 3, 0, { '' } },
|
|
{ 3, 0, 3, 0, { '{\n' } },
|
|
{ 3, 0, 3, 0, { ' "foo": "bar"\n' } },
|
|
{ 3, 0, 3, 0, { '}\n' } },
|
|
})
|
|
eq({ '{', ' "foo": "bar"', '}', '' }, buf_lines(1))
|
|
end)
|
|
end)
|
|
|
|
describe('apply_text_edits', function()
|
|
local buffer_text = {
|
|
'First line of text',
|
|
'Second line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}
|
|
|
|
before_each(function()
|
|
insert(dedent(table.concat(buffer_text, '\n')))
|
|
end)
|
|
|
|
it('applies simple edits', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 0, 0, { '123' } },
|
|
{ 1, 0, 1, 1, { '2' } },
|
|
{ 2, 0, 2, 2, { '3' } },
|
|
{ 3, 2, 3, 4, { '' } },
|
|
})
|
|
eq({
|
|
'123First line of text',
|
|
'2econd line of text',
|
|
'3ird line of text',
|
|
'Foth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('applies complex edits', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 0, 0, { '', '12' } },
|
|
{ 0, 0, 0, 0, { '3', 'foo' } },
|
|
{ 0, 1, 0, 1, { 'bar', '123' } },
|
|
{ 0, #'First ', 0, #'First line of text', { 'guy' } },
|
|
{ 1, 0, 1, #'Second', { 'baz' } },
|
|
{ 2, #'Th', 2, #'Third', { 'e next' } },
|
|
{ 3, #'', 3, #'Fourth', { 'another line of text', 'before this' } },
|
|
{ 3, #'Fourth', 3, #'Fourth line of text', { '!' } },
|
|
})
|
|
eq({
|
|
'',
|
|
'123',
|
|
'fooFbar',
|
|
'123irst guy',
|
|
'baz line of text',
|
|
'The next line of text',
|
|
'another line of text',
|
|
'before this!',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('applies complex edits (reversed range)', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 0, 0, { '', '12' } },
|
|
{ 0, 0, 0, 0, { '3', 'foo' } },
|
|
{ 0, 1, 0, 1, { 'bar', '123' } },
|
|
{ 0, #'First line of text', 0, #'First ', { 'guy' } },
|
|
{ 1, #'Second', 1, 0, { 'baz' } },
|
|
{ 2, #'Third', 2, #'Th', { 'e next' } },
|
|
{ 3, #'Fourth', 3, #'', { 'another line of text', 'before this' } },
|
|
{ 3, #'Fourth line of text', 3, #'Fourth', { '!' } },
|
|
})
|
|
eq({
|
|
'',
|
|
'123',
|
|
'fooFbar',
|
|
'123irst guy',
|
|
'baz line of text',
|
|
'The next line of text',
|
|
'another line of text',
|
|
'before this!',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('applies non-ASCII characters edits', function()
|
|
apply_text_edits({
|
|
{ 4, 3, 4, 4, { 'ä' } },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Second line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å ä ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('applies text edits at the end of the document', function()
|
|
apply_text_edits({
|
|
{ 5, 0, 5, 0, 'foobar' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Second line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
'foobar',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('applies multiple text edits at the end of the document', function()
|
|
apply_text_edits({
|
|
{ 4, 0, 5, 0, '' },
|
|
{ 5, 0, 5, 0, 'foobar' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Second line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'foobar',
|
|
}, buf_lines(1))
|
|
end)
|
|
|
|
it('it restores marks', function()
|
|
eq(true, api.nvim_buf_set_mark(1, 'a', 2, 1, {}))
|
|
apply_text_edits({
|
|
{ 1, 0, 2, 5, 'foobar' },
|
|
{ 4, 0, 5, 0, 'barfoo' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'foobar line of text',
|
|
'Fourth line of text',
|
|
'barfoo',
|
|
}, buf_lines(1))
|
|
eq({ 2, 1 }, api.nvim_buf_get_mark(1, 'a'))
|
|
end)
|
|
|
|
it('it restores marks to last valid col', function()
|
|
eq(true, api.nvim_buf_set_mark(1, 'a', 2, 10, {}))
|
|
apply_text_edits({
|
|
{ 1, 0, 2, 15, 'foobar' },
|
|
{ 4, 0, 5, 0, 'barfoo' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'foobarext',
|
|
'Fourth line of text',
|
|
'barfoo',
|
|
}, buf_lines(1))
|
|
eq({ 2, 9 }, api.nvim_buf_get_mark(1, 'a'))
|
|
end)
|
|
|
|
it('it restores marks to last valid line', function()
|
|
eq(true, api.nvim_buf_set_mark(1, 'a', 4, 1, {}))
|
|
apply_text_edits({
|
|
{ 1, 0, 4, 5, 'foobar' },
|
|
{ 4, 0, 5, 0, 'barfoo' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'foobaro',
|
|
}, buf_lines(1))
|
|
eq({ 2, 1 }, api.nvim_buf_get_mark(1, 'a'))
|
|
end)
|
|
|
|
it('applies edit based on confirmation response', function()
|
|
--- @type lsp.AnnotatedTextEdit
|
|
local edit = make_edit(0, 0, 5, 0, 'foo')
|
|
edit.annotationId = 'annotation-id'
|
|
|
|
local function test(response)
|
|
exec_lua(function()
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
vim.fn.confirm = function()
|
|
return response
|
|
end
|
|
|
|
vim.lsp.util.apply_text_edits(
|
|
{ edit },
|
|
1,
|
|
'utf-16',
|
|
{ ['annotation-id'] = { label = 'Insert "foo"', needsConfirmation = true } }
|
|
)
|
|
end, { response })
|
|
end
|
|
|
|
test(2) -- 2 = No
|
|
eq(buffer_text, buf_lines(1))
|
|
|
|
test(1) -- 1 = Yes
|
|
eq({ 'foo' }, buf_lines(1))
|
|
end)
|
|
|
|
describe('cursor position', function()
|
|
it("don't fix the cursor if the range contains the cursor", function()
|
|
api.nvim_win_set_cursor(0, { 2, 6 })
|
|
apply_text_edits({
|
|
{ 1, 0, 1, 19, 'Second line of text' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Second line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
eq({ 2, 6 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
|
|
it('fix the cursor to the valid col if the content was removed', function()
|
|
api.nvim_win_set_cursor(0, { 2, 6 })
|
|
apply_text_edits({
|
|
{ 1, 0, 1, 6, '' },
|
|
{ 1, 6, 1, 19, '' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
eq({ 2, 0 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
|
|
it('fix the cursor to the valid row if the content was removed', function()
|
|
api.nvim_win_set_cursor(0, { 2, 6 })
|
|
apply_text_edits({
|
|
{ 1, 0, 1, 6, '' },
|
|
{ 0, 18, 5, 0, '' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
}, buf_lines(1))
|
|
eq({ 1, 17 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
|
|
it('fix the cursor row', function()
|
|
api.nvim_win_set_cursor(0, { 3, 0 })
|
|
apply_text_edits({
|
|
{ 1, 0, 2, 0, '' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
eq({ 2, 0 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
|
|
it('fix the cursor col', function()
|
|
-- append empty last line. See #22636
|
|
api.nvim_buf_set_lines(1, -1, -1, true, { '' })
|
|
|
|
api.nvim_win_set_cursor(0, { 2, 11 })
|
|
apply_text_edits({
|
|
{ 1, 7, 1, 11, '' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Second of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
'',
|
|
}, buf_lines(1))
|
|
eq({ 2, 7 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
|
|
it('fix the cursor row and col', function()
|
|
api.nvim_win_set_cursor(0, { 2, 12 })
|
|
apply_text_edits({
|
|
{ 0, 11, 1, 12, '' },
|
|
})
|
|
eq({
|
|
'First line of text',
|
|
'Third line of text',
|
|
'Fourth line of text',
|
|
'å å ɧ 汉语 ↥ 🤦 🦄',
|
|
}, buf_lines(1))
|
|
eq({ 1, 11 }, api.nvim_win_get_cursor(0))
|
|
end)
|
|
end)
|
|
|
|
describe('with LSP end line after what Vim considers to be the end line', function()
|
|
it('applies edits when the last linebreak is considered a new line', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 5, 0, { 'All replaced' } },
|
|
})
|
|
eq({ 'All replaced' }, buf_lines(1))
|
|
end)
|
|
|
|
it("applies edits when the end line is 2 larger than vim's", function()
|
|
apply_text_edits({
|
|
{ 0, 0, 6, 0, { 'All replaced' } },
|
|
})
|
|
eq({ 'All replaced' }, buf_lines(1))
|
|
end)
|
|
|
|
it('applies edits with a column offset', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 5, 2, { 'All replaced' } },
|
|
})
|
|
eq({ 'All replaced' }, buf_lines(1))
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
describe('apply_text_edits regression tests for #20116', function()
|
|
before_each(function()
|
|
insert(dedent([[
|
|
Test line one
|
|
Test line two 21 char]]))
|
|
end)
|
|
|
|
describe('with LSP end column out of bounds and start column at 0', function()
|
|
it('applies edits at the end of the buffer', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } },
|
|
}, 'utf-8')
|
|
eq({ '#include "whatever.h"', '#include <algorithm>' }, buf_lines(1))
|
|
end)
|
|
|
|
it('applies edits in the middle of the buffer', function()
|
|
apply_text_edits({
|
|
{ 0, 0, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } },
|
|
}, 'utf-8')
|
|
eq(
|
|
{ '#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' },
|
|
buf_lines(1)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
describe('with LSP end column out of bounds and start column NOT at 0', function()
|
|
it('applies edits at the end of the buffer', function()
|
|
apply_text_edits({
|
|
{ 0, 2, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } },
|
|
}, 'utf-8')
|
|
eq({ 'Te#include "whatever.h"', '#include <algorithm>' }, buf_lines(1))
|
|
end)
|
|
|
|
it('applies edits in the middle of the buffer', function()
|
|
apply_text_edits({
|
|
{ 0, 2, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } },
|
|
}, 'utf-8')
|
|
eq(
|
|
{ 'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' },
|
|
buf_lines(1)
|
|
)
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
describe('apply_text_document_edit', function()
|
|
local target_bufnr --- @type integer
|
|
|
|
local text_document_edit = function(editVersion)
|
|
return {
|
|
edits = {
|
|
make_edit(0, 0, 0, 3, 'First ↥ 🤦 🦄'),
|
|
},
|
|
textDocument = {
|
|
uri = 'file:///fake/uri',
|
|
version = editVersion,
|
|
},
|
|
}
|
|
end
|
|
|
|
before_each(function()
|
|
target_bufnr = exec_lua(function()
|
|
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
|
|
local lines = { '1st line of text', '2nd line of 语text' }
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
|
|
return bufnr
|
|
end)
|
|
end)
|
|
|
|
it('correctly goes ahead with the edit if all is normal', function()
|
|
exec_lua(function(text_edit)
|
|
vim.lsp.util.apply_text_document_edit(text_edit, nil, 'utf-16')
|
|
end, text_document_edit(5))
|
|
eq({
|
|
'First ↥ 🤦 🦄 line of text',
|
|
'2nd line of 语text',
|
|
}, buf_lines(target_bufnr))
|
|
end)
|
|
|
|
it('always accepts edit with version = 0', function()
|
|
exec_lua(function(text_edit)
|
|
vim.lsp.util.buf_versions[target_bufnr] = 10
|
|
vim.lsp.util.apply_text_document_edit(text_edit, nil, 'utf-16')
|
|
end, text_document_edit(0))
|
|
eq({
|
|
'First ↥ 🤦 🦄 line of text',
|
|
'2nd line of 语text',
|
|
}, buf_lines(target_bufnr))
|
|
end)
|
|
|
|
it('skips the edit if the version of the edit is behind the local buffer ', function()
|
|
local apply_edit_mocking_current_version = function(edit, versionedBuf)
|
|
exec_lua(function()
|
|
vim.lsp.util.buf_versions[versionedBuf.bufnr] = versionedBuf.currentVersion
|
|
vim.lsp.util.apply_text_document_edit(edit, nil, 'utf-16')
|
|
end)
|
|
end
|
|
|
|
local baseText = {
|
|
'1st line of text',
|
|
'2nd line of 语text',
|
|
}
|
|
|
|
eq(baseText, buf_lines(target_bufnr))
|
|
|
|
-- Apply an edit for an old version, should skip
|
|
apply_edit_mocking_current_version(
|
|
text_document_edit(2),
|
|
{ currentVersion = 7, bufnr = target_bufnr }
|
|
)
|
|
eq(baseText, buf_lines(target_bufnr)) -- no change
|
|
|
|
-- Sanity check that next version to current does apply change
|
|
apply_edit_mocking_current_version(
|
|
text_document_edit(8),
|
|
{ currentVersion = 7, bufnr = target_bufnr }
|
|
)
|
|
eq({
|
|
'First ↥ 🤦 🦄 line of text',
|
|
'2nd line of 语text',
|
|
}, buf_lines(target_bufnr))
|
|
end)
|
|
end)
|
|
|
|
describe('workspace_apply_edit', function()
|
|
it('workspace/applyEdit returns ApplyWorkspaceEditResponse', function()
|
|
local expected_handlers = {
|
|
{ NIL, {}, { method = 'test', client_id = 1 } },
|
|
}
|
|
test_rpc_server {
|
|
test_name = 'basic_init',
|
|
on_init = function(client, _)
|
|
client:stop()
|
|
end,
|
|
-- If the program timed out, then code will be nil.
|
|
on_exit = function(code, signal)
|
|
eq(0, code, 'exit code')
|
|
eq(0, signal, 'exit signal')
|
|
end,
|
|
-- Note that NIL must be used here.
|
|
-- on_handler(err, method, result, client_id)
|
|
on_handler = function(...)
|
|
local expected = {
|
|
applied = true,
|
|
failureReason = nil,
|
|
}
|
|
eq(
|
|
expected,
|
|
exec_lua(function()
|
|
local apply_edit = {
|
|
label = nil,
|
|
edit = {},
|
|
}
|
|
return vim.lsp.handlers['workspace/applyEdit'](
|
|
nil,
|
|
apply_edit,
|
|
{ client_id = _G.TEST_RPC_CLIENT_ID }
|
|
)
|
|
end)
|
|
)
|
|
eq(table.remove(expected_handlers), { ... })
|
|
end,
|
|
}
|
|
end)
|
|
end)
|
|
|
|
describe('apply_workspace_edit', function()
|
|
local replace_line_edit = function(row, new_line, editVersion)
|
|
return {
|
|
edits = {
|
|
-- NOTE: This is a hack if you have a line longer than 1000 it won't replace it
|
|
make_edit(row, 0, row, 1000, new_line),
|
|
},
|
|
textDocument = {
|
|
uri = 'file:///fake/uri',
|
|
version = editVersion,
|
|
},
|
|
}
|
|
end
|
|
|
|
-- Some servers send all the edits separately, but with the same version.
|
|
-- We should not stop applying the edits
|
|
local make_workspace_edit = function(changes)
|
|
return {
|
|
documentChanges = changes,
|
|
}
|
|
end
|
|
|
|
local target_bufnr --- @type integer
|
|
local changedtick --- @type integer
|
|
|
|
before_each(function()
|
|
exec_lua(function()
|
|
target_bufnr = vim.uri_to_bufnr('file:///fake/uri')
|
|
local lines = {
|
|
'Original Line #1',
|
|
'Original Line #2',
|
|
}
|
|
|
|
vim.api.nvim_buf_set_lines(target_bufnr, 0, -1, false, lines)
|
|
|
|
local function update_changed_tick()
|
|
vim.lsp.util.buf_versions[target_bufnr] = vim.b[target_bufnr].changedtick
|
|
end
|
|
|
|
update_changed_tick()
|
|
vim.api.nvim_buf_attach(target_bufnr, false, {
|
|
on_changedtick = update_changed_tick,
|
|
})
|
|
|
|
changedtick = vim.b[target_bufnr].changedtick
|
|
end)
|
|
end)
|
|
|
|
it('apply_workspace_edit applies a single edit', function()
|
|
local new_lines = {
|
|
'First Line',
|
|
}
|
|
|
|
local edits = {}
|
|
for row, line in ipairs(new_lines) do
|
|
table.insert(edits, replace_line_edit(row - 1, line, changedtick))
|
|
end
|
|
|
|
eq(
|
|
{
|
|
'First Line',
|
|
'Original Line #2',
|
|
},
|
|
exec_lua(function(workspace_edits)
|
|
vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16')
|
|
|
|
return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false)
|
|
end, make_workspace_edit(edits))
|
|
)
|
|
eq(true, api.nvim_get_option_value('buflisted', { buf = target_bufnr }))
|
|
end)
|
|
|
|
it('apply_workspace_edit applies multiple edits', function()
|
|
local new_lines = {
|
|
'First Line',
|
|
'Second Line',
|
|
}
|
|
|
|
local edits = {}
|
|
for row, line in ipairs(new_lines) do
|
|
table.insert(edits, replace_line_edit(row - 1, line, changedtick))
|
|
end
|
|
|
|
eq(
|
|
new_lines,
|
|
exec_lua(function(workspace_edits)
|
|
vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16')
|
|
return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false)
|
|
end, make_workspace_edit(edits))
|
|
)
|
|
end)
|
|
|
|
it('supports file creation with CreateFile payload', function()
|
|
local tmpfile = tmpname(false)
|
|
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'create',
|
|
uri = uri,
|
|
},
|
|
},
|
|
}
|
|
exec_lua(function()
|
|
vim.lsp.util.apply_workspace_edit(edit, 'utf-16')
|
|
end)
|
|
eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
end)
|
|
|
|
it(
|
|
'supports file creation in folder that needs to be created with CreateFile payload',
|
|
function()
|
|
local tmpfile = tmpname(false) .. '/dummy/x/'
|
|
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'create',
|
|
uri = uri,
|
|
},
|
|
},
|
|
}
|
|
exec_lua(function()
|
|
vim.lsp.util.apply_workspace_edit(edit, 'utf-16')
|
|
end)
|
|
eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
end
|
|
)
|
|
|
|
it('createFile does not touch file if it exists and ignoreIfExists is set', function()
|
|
local tmpfile = tmpname()
|
|
write_file(tmpfile, 'Dummy content')
|
|
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'create',
|
|
uri = uri,
|
|
options = {
|
|
ignoreIfExists = true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
|
|
eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
eq('Dummy content', read_file(tmpfile))
|
|
end)
|
|
|
|
it('createFile overrides file if overwrite is set', function()
|
|
local tmpfile = tmpname()
|
|
write_file(tmpfile, 'Dummy content')
|
|
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'create',
|
|
uri = uri,
|
|
options = {
|
|
overwrite = true,
|
|
ignoreIfExists = true, -- overwrite must win over ignoreIfExists
|
|
},
|
|
},
|
|
},
|
|
}
|
|
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
|
|
eq(true, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
eq('', read_file(tmpfile))
|
|
end)
|
|
|
|
it('DeleteFile delete file and buffer', function()
|
|
local tmpfile = tmpname()
|
|
write_file(tmpfile, 'Be gone')
|
|
local uri = exec_lua(function()
|
|
local bufnr = vim.fn.bufadd(tmpfile)
|
|
vim.fn.bufload(bufnr)
|
|
return vim.uri_from_fname(tmpfile)
|
|
end)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'delete',
|
|
uri = uri,
|
|
},
|
|
},
|
|
}
|
|
eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16'))
|
|
eq(false, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
eq(false, api.nvim_buf_is_loaded(fn.bufadd(tmpfile)))
|
|
end)
|
|
|
|
it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function()
|
|
local tmpfile = tmpname(false)
|
|
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
|
|
local edit = {
|
|
documentChanges = {
|
|
{
|
|
kind = 'delete',
|
|
uri = uri,
|
|
options = {
|
|
ignoreIfNotExists = false,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16'))
|
|
eq(false, vim.uv.fs_stat(tmpfile) ~= nil)
|
|
end)
|
|
end)
|
|
|
|
describe('vim.lsp.tagfunc', function()
|
|
before_each(function()
|
|
---@type lsp.Location[]
|
|
local mock_locations = {
|
|
{
|
|
range = {
|
|
['start'] = { line = 5, character = 23 },
|
|
['end'] = { line = 10, character = 0 },
|
|
},
|
|
uri = 'test://buf',
|
|
},
|
|
{
|
|
range = {
|
|
['start'] = { line = 42, character = 10 },
|
|
['end'] = { line = 44, character = 0 },
|
|
},
|
|
uri = 'test://another-file',
|
|
},
|
|
}
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
_G.mock_locations = mock_locations
|
|
_G.server = _G._create_server({
|
|
---@type lsp.ServerCapabilities
|
|
capabilities = {
|
|
definitionProvider = true,
|
|
workspaceSymbolProvider = true,
|
|
},
|
|
handlers = {
|
|
---@return lsp.Location[]
|
|
['textDocument/definition'] = function(_, _, callback)
|
|
callback(nil, { _G.mock_locations[1] })
|
|
end,
|
|
---@return lsp.WorkspaceSymbol[]
|
|
['workspace/symbol'] = function(_, request, callback)
|
|
assert(request.query == 'foobar')
|
|
callback(nil, {
|
|
{
|
|
name = 'foobar',
|
|
kind = 13, ---@type lsp.SymbolKind
|
|
location = _G.mock_locations[1],
|
|
},
|
|
{
|
|
name = 'vim.foobar',
|
|
kind = 12, ---@type lsp.SymbolKind
|
|
location = _G.mock_locations[2],
|
|
},
|
|
})
|
|
end,
|
|
},
|
|
})
|
|
_G.client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
end)
|
|
end)
|
|
|
|
after_each(function()
|
|
exec_lua(function()
|
|
vim.lsp.get_client_by_id(_G.client_id):stop()
|
|
end)
|
|
end)
|
|
|
|
it('with flags=c, returns matching tags using textDocument/definition', function()
|
|
local result = exec_lua(function()
|
|
return vim.lsp.tagfunc('foobar', 'c')
|
|
end)
|
|
eq({
|
|
{
|
|
cmd = '/\\%6l\\%1c/', -- for location (5, 23)
|
|
filename = 'test://buf',
|
|
name = 'foobar',
|
|
},
|
|
}, result)
|
|
end)
|
|
|
|
it('without flags=c, returns all matching tags using workspace/symbol', function()
|
|
local result = exec_lua(function()
|
|
return vim.lsp.tagfunc('foobar', '')
|
|
end)
|
|
eq({
|
|
{
|
|
cmd = '/\\%6l\\%1c/', -- for location (5, 23)
|
|
filename = 'test://buf',
|
|
kind = 'Variable',
|
|
name = 'foobar',
|
|
},
|
|
{
|
|
cmd = '/\\%43l\\%1c/', -- for location (42, 10)
|
|
filename = 'test://another-file',
|
|
kind = 'Function',
|
|
name = 'vim.foobar',
|
|
},
|
|
}, result)
|
|
end)
|
|
|
|
it('with flags including i, returns NIL', function()
|
|
exec_lua(function()
|
|
local result = vim.lsp.tagfunc('foobar', 'cir')
|
|
assert(result == vim.NIL, 'should not issue LSP requests')
|
|
return {}
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
describe('cmd', function()
|
|
it('connects to lsp server via rpc.connect using ip address', function()
|
|
exec_lua(create_tcp_echo_server)
|
|
exec_lua(function()
|
|
local port = _G._create_tcp_server('127.0.0.1')
|
|
vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string]
|
|
eq('body', method)
|
|
eq('initialize', vim.json.decode(args[1]).method)
|
|
end)
|
|
end)
|
|
|
|
it('connects to lsp server via rpc.connect using hostname', function()
|
|
skip(is_os('bsd'), 'issue with host resolution in ci')
|
|
skip(t.is_arch('s390x'), 'issue with host resolution in ci')
|
|
exec_lua(create_tcp_echo_server)
|
|
exec_lua(function()
|
|
local port = _G._create_tcp_server('::1')
|
|
vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('localhost', port) })
|
|
end)
|
|
verify_single_notification(function(method, args) ---@param args [string]
|
|
eq('body', method)
|
|
eq('initialize', vim.json.decode(args[1]).method)
|
|
end)
|
|
end)
|
|
|
|
it('can connect to lsp server via pipe or domain_socket', function()
|
|
local tmpfile = is_os('win') and '\\\\.\\\\pipe\\pipe.test' or tmpname(false)
|
|
local result = exec_lua(function()
|
|
local uv = vim.uv
|
|
local server = assert(uv.new_pipe(false))
|
|
server:bind(tmpfile)
|
|
local init = nil
|
|
|
|
server:listen(127, function(err)
|
|
assert(not err, err)
|
|
local client = assert(vim.uv.new_pipe())
|
|
server:accept(client)
|
|
client:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
|
|
init = body
|
|
client:close()
|
|
end))
|
|
end)
|
|
vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect(tmpfile) })
|
|
vim.wait(1000, function()
|
|
return init ~= nil
|
|
end)
|
|
assert(init, 'server must receive `initialize` request')
|
|
server:close()
|
|
server:shutdown()
|
|
return vim.json.decode(init)
|
|
end)
|
|
eq('initialize', result.method)
|
|
end)
|
|
end)
|
|
|
|
describe('handlers', function()
|
|
it('handler can return false as response', function()
|
|
local result = exec_lua(function()
|
|
local server = assert(vim.uv.new_tcp())
|
|
local messages = {}
|
|
local responses = {}
|
|
server:bind('127.0.0.1', 0)
|
|
server:listen(127, function(err)
|
|
assert(not err, err)
|
|
local socket = assert(vim.uv.new_tcp())
|
|
server:accept(socket)
|
|
socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
|
|
local payload = vim.json.decode(body)
|
|
if payload.method then
|
|
table.insert(messages, payload.method)
|
|
if payload.method == 'initialize' then
|
|
local msg = vim.json.encode({
|
|
id = payload.id,
|
|
jsonrpc = '2.0',
|
|
result = {
|
|
capabilities = {},
|
|
},
|
|
})
|
|
socket:write(table.concat({ 'Content-Length: ', tostring(#msg), '\r\n\r\n', msg }))
|
|
elseif payload.method == 'initialized' then
|
|
local msg = vim.json.encode({
|
|
id = 10,
|
|
jsonrpc = '2.0',
|
|
method = 'dummy',
|
|
params = {},
|
|
})
|
|
socket:write(table.concat({ 'Content-Length: ', tostring(#msg), '\r\n\r\n', msg }))
|
|
end
|
|
else
|
|
table.insert(responses, payload)
|
|
socket:close()
|
|
end
|
|
end))
|
|
end)
|
|
local port = server:getsockname().port
|
|
local handler_called = false
|
|
vim.lsp.handlers['dummy'] = function(_, _)
|
|
handler_called = true
|
|
return false
|
|
end
|
|
local client_id =
|
|
assert(vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) }))
|
|
vim.lsp.get_client_by_id(client_id)
|
|
vim.wait(1000, function()
|
|
return #messages == 2 and handler_called and #responses == 1
|
|
end)
|
|
server:close()
|
|
server:shutdown()
|
|
return {
|
|
messages = messages,
|
|
handler_called = handler_called,
|
|
responses = responses,
|
|
}
|
|
end)
|
|
local expected = {
|
|
messages = { 'initialize', 'initialized' },
|
|
handler_called = true,
|
|
responses = {
|
|
{
|
|
id = 10,
|
|
jsonrpc = '2.0',
|
|
result = false,
|
|
},
|
|
},
|
|
}
|
|
eq(expected, result)
|
|
end)
|
|
|
|
--- Starts a TCP server that completes initialization, then sends `null_id_payload` after the
|
|
--- "initialized" notification. If `notification_method` is given, registers a handler
|
|
--- that tracks whether it was dispatched as a notification.
|
|
---
|
|
--- @param null_id_payload string JSON
|
|
--- @param notification_method? string
|
|
--- @return { on_error_called: table, notification_received: boolean, messages: boolean }.
|
|
local function test_null_id_response(null_id_payload, notification_method)
|
|
return exec_lua(function()
|
|
local server = assert(vim.uv.new_tcp())
|
|
local accepted
|
|
local messages = {}
|
|
server:bind('127.0.0.1', 0)
|
|
server:listen(127, function(err)
|
|
assert(not err, err)
|
|
accepted = assert(vim.uv.new_tcp())
|
|
server:accept(accepted)
|
|
accepted:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
|
|
local payload = vim.json.decode(body)
|
|
if payload.method then
|
|
table.insert(messages, payload.method)
|
|
if payload.method == 'initialize' then
|
|
-- Send a valid initialize response first
|
|
local msg = vim.json.encode({
|
|
id = payload.id,
|
|
jsonrpc = '2.0',
|
|
result = {
|
|
capabilities = {},
|
|
},
|
|
})
|
|
accepted:write(
|
|
table.concat({ 'Content-Length: ', tostring(#msg), '\r\n\r\n', msg })
|
|
)
|
|
elseif payload.method == 'initialized' then
|
|
accepted:write(table.concat({
|
|
'Content-Length: ',
|
|
tostring(#null_id_payload),
|
|
'\r\n\r\n',
|
|
null_id_payload,
|
|
}))
|
|
end
|
|
end
|
|
end, function()
|
|
if accepted and not accepted:is_closing() then
|
|
accepted:close()
|
|
end
|
|
end, function()
|
|
if accepted and not accepted:is_closing() then
|
|
accepted:close()
|
|
end
|
|
end))
|
|
end)
|
|
local port = server:getsockname().port
|
|
local on_error_called = false
|
|
local notification_received = false
|
|
local handlers = nil
|
|
if notification_method then
|
|
handlers = {
|
|
[notification_method] = function()
|
|
notification_received = true
|
|
return {}
|
|
end,
|
|
}
|
|
end
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'null-id-test',
|
|
cmd = vim.lsp.rpc.connect('127.0.0.1', port),
|
|
on_error = function(_code, _err)
|
|
on_error_called = true
|
|
end,
|
|
handlers = handlers,
|
|
}))
|
|
vim.lsp.get_client_by_id(client_id)
|
|
vim.wait(1000, function()
|
|
return #messages >= 2 and (on_error_called or notification_received)
|
|
end)
|
|
if accepted and not accepted:is_closing() then
|
|
accepted:close()
|
|
end
|
|
server:shutdown()
|
|
server:close()
|
|
return {
|
|
messages = messages,
|
|
on_error_called = on_error_called,
|
|
notification_received = notification_received,
|
|
}
|
|
end)
|
|
end
|
|
|
|
it('null-id in response (JSON-RPC 2.0 parse error) is handled, emits error', function()
|
|
local result = test_null_id_response(
|
|
'{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}'
|
|
)
|
|
eq(true, result.on_error_called)
|
|
eq(true, #result.messages >= 2)
|
|
end)
|
|
|
|
it('null-id in response does not misclassify as a notification', function()
|
|
-- Sanity check: a real notification (no id) dispatches the handler.
|
|
local valid = test_null_id_response(
|
|
'{"jsonrpc":"2.0","method":"workspace/configuration","params":{"items":[]}}',
|
|
'workspace/configuration'
|
|
)
|
|
eq(true, valid.notification_received)
|
|
|
|
local result = test_null_id_response(
|
|
-- Error response with null id (parse error per JSON-RPC 2.0 §5)
|
|
'{"jsonrpc":"2.0","method":"workspace/configuration","params":{"items":[]},"id":null}',
|
|
'workspace/configuration'
|
|
)
|
|
-- Should be dispatched as an error, NOT silently handled as a notification.
|
|
eq(true, result.on_error_called)
|
|
-- Null id must NOT be dispatched as a notification.
|
|
eq(false, result.notification_received)
|
|
end)
|
|
end)
|
|
|
|
describe('#dynamic vim.lsp._dynamic', function()
|
|
it('supports dynamic registration', function()
|
|
local root_dir = tmpname(false)
|
|
mkdir(root_dir)
|
|
local tmpfile = root_dir .. '/dynamic.foo'
|
|
local file = io.open(tmpfile, 'w')
|
|
if file then
|
|
file:close()
|
|
end
|
|
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'dynamic-test',
|
|
cmd = server.cmd,
|
|
root_dir = root_dir,
|
|
get_language_id = function()
|
|
return 'dummy-lang'
|
|
end,
|
|
capabilities = {
|
|
textDocument = {
|
|
formatting = {
|
|
dynamicRegistration = true,
|
|
},
|
|
rangeFormatting = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
didChangeConfiguration = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'formatting',
|
|
method = 'textDocument/formatting',
|
|
registerOptions = {
|
|
documentSelector = {
|
|
{
|
|
pattern = root_dir .. '/*.foo',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'range-formatting',
|
|
method = 'textDocument/rangeFormatting',
|
|
registerOptions = {
|
|
documentSelector = {
|
|
{
|
|
language = 'dummy-lang',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'completion',
|
|
method = 'textDocument/completion',
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
local result = {}
|
|
local function check(method, fname, ...)
|
|
local bufnr = fname and vim.fn.bufadd(fname) or nil
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
local keys = { ... }
|
|
local caps = {}
|
|
if #keys > 0 then
|
|
client:_provider_foreach(method, function(cap)
|
|
table.insert(caps, vim.tbl_get(cap, unpack(keys)) or vim.NIL)
|
|
end)
|
|
end
|
|
result[#result + 1] = {
|
|
method = method,
|
|
fname = fname,
|
|
supported = client:supports_method(method, bufnr),
|
|
cap = #keys > 0 and caps or nil,
|
|
}
|
|
end
|
|
|
|
check('textDocument/formatting')
|
|
check('textDocument/formatting', tmpfile)
|
|
check('textDocument/rangeFormatting')
|
|
check('textDocument/rangeFormatting', tmpfile)
|
|
check('textDocument/completion')
|
|
|
|
check('workspace/didChangeWatchedFiles')
|
|
check('workspace/didChangeWatchedFiles', tmpfile)
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'didChangeWatched',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = 'something',
|
|
kind = 4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
check('workspace/didChangeWatchedFiles')
|
|
check('workspace/didChangeWatchedFiles', tmpfile)
|
|
|
|
-- Initial support false
|
|
check('workspace/diagnostic')
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'diag1',
|
|
method = 'textDocument/diagnostic',
|
|
registerOptions = {
|
|
identifier = 'diag-ident-1',
|
|
-- workspaceDiagnostics field omitted
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
-- Checks after registering without workspaceDiagnostics support
|
|
-- Returns false
|
|
check('workspace/diagnostic', nil, 'identifier')
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'diag2',
|
|
method = 'textDocument/diagnostic',
|
|
registerOptions = {
|
|
identifier = 'diag-ident-2',
|
|
workspaceDiagnostics = true,
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
-- Check after second registration with support
|
|
-- Returns true
|
|
check('workspace/diagnostic', nil, 'identifier')
|
|
|
|
vim.lsp.handlers['client/unregisterCapability'](nil, {
|
|
unregisterations = {
|
|
{ id = 'diag2', method = 'textDocument/diagnostic' },
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
-- Check after unregistering
|
|
-- Returns false
|
|
check('workspace/diagnostic', nil, 'identifier')
|
|
|
|
check('textDocument/codeAction')
|
|
check('codeAction/resolve')
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'codeAction',
|
|
method = 'textDocument/codeAction',
|
|
registerOptions = {
|
|
resolveProvider = true,
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
check('textDocument/codeAction')
|
|
check('codeAction/resolve')
|
|
|
|
check('workspace/didChangeWorkspaceFolders')
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'didChangeWorkspaceFolders-id',
|
|
method = 'workspace/didChangeWorkspaceFolders',
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
check('workspace/didChangeWorkspaceFolders')
|
|
|
|
check('workspace/didChangeConfiguration')
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'didChangeConfiguration-id',
|
|
method = 'workspace/didChangeConfiguration',
|
|
registerOptions = {
|
|
section = 'dummy-section',
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
check('workspace/didChangeConfiguration', nil, 'section')
|
|
|
|
return result
|
|
end)
|
|
|
|
eq(21, #result)
|
|
eq({ method = 'textDocument/formatting', supported = false }, result[1])
|
|
eq({ method = 'textDocument/formatting', supported = true, fname = tmpfile }, result[2])
|
|
eq({ method = 'textDocument/rangeFormatting', supported = true }, result[3])
|
|
eq({ method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile }, result[4])
|
|
eq({ method = 'textDocument/completion', supported = false }, result[5])
|
|
eq({ method = 'workspace/didChangeWatchedFiles', supported = false }, result[6])
|
|
eq(
|
|
{ method = 'workspace/didChangeWatchedFiles', supported = false, fname = tmpfile },
|
|
result[7]
|
|
)
|
|
eq({ method = 'workspace/didChangeWatchedFiles', supported = true }, result[8])
|
|
eq(
|
|
{ method = 'workspace/didChangeWatchedFiles', supported = true, fname = tmpfile },
|
|
result[9]
|
|
)
|
|
eq({ method = 'workspace/diagnostic', supported = false }, result[10])
|
|
eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[11])
|
|
eq({
|
|
method = 'workspace/diagnostic',
|
|
supported = true,
|
|
cap = { 'diag-ident-2' },
|
|
}, result[12])
|
|
eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[13])
|
|
eq({ method = 'textDocument/codeAction', supported = false }, result[14])
|
|
eq({ method = 'codeAction/resolve', supported = false }, result[15])
|
|
eq({ method = 'textDocument/codeAction', supported = true }, result[16])
|
|
eq({ method = 'codeAction/resolve', supported = true }, result[17])
|
|
eq({ method = 'workspace/didChangeWorkspaceFolders', supported = false }, result[18])
|
|
eq({ method = 'workspace/didChangeWorkspaceFolders', supported = true }, result[19])
|
|
eq({ method = 'workspace/didChangeConfiguration', supported = false }, result[20])
|
|
eq(
|
|
{ method = 'workspace/didChangeConfiguration', supported = true, cap = { 'dummy-section' } },
|
|
result[21]
|
|
)
|
|
end)
|
|
|
|
it('identifies client dynamic registration capability', function()
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'dynamic-test',
|
|
cmd = server.cmd,
|
|
capabilities = {
|
|
textDocument = {
|
|
formatting = {
|
|
dynamicRegistration = true,
|
|
},
|
|
synchronization = {
|
|
dynamicRegistration = true,
|
|
},
|
|
diagnostic = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
local result = {}
|
|
local function check(method)
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
result[#result + 1] = {
|
|
method = method,
|
|
supports_reg = client:_supports_registration(method),
|
|
}
|
|
end
|
|
|
|
check('textDocument/formatting')
|
|
check('textDocument/didSave')
|
|
check('textDocument/didOpen')
|
|
check('textDocument/codeLens')
|
|
check('textDocument/diagnostic')
|
|
check('workspace/diagnostic')
|
|
|
|
return result
|
|
end)
|
|
|
|
eq(6, #result)
|
|
eq({ method = 'textDocument/formatting', supports_reg = true }, result[1])
|
|
eq({ method = 'textDocument/didSave', supports_reg = true }, result[2])
|
|
eq({ method = 'textDocument/didOpen', supports_reg = true }, result[3])
|
|
eq({ method = 'textDocument/codeLens', supports_reg = false }, result[4])
|
|
eq({ method = 'textDocument/diagnostic', supports_reg = true }, result[5])
|
|
eq({ method = 'workspace/diagnostic', supports_reg = true }, result[6])
|
|
end)
|
|
|
|
it('supports static registration', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local client_id = exec_lua(function()
|
|
local server = _G._create_server({
|
|
capabilities = {
|
|
colorProvider = { id = 'color-registration' },
|
|
diagnosticProvider = {
|
|
id = 'diag-registration',
|
|
identifier = 'diag-ident-static',
|
|
workspaceDiagnostics = true,
|
|
},
|
|
},
|
|
})
|
|
|
|
return assert(vim.lsp.start({ name = 'dynamic-test', cmd = server.cmd }))
|
|
end)
|
|
|
|
local function sort_method(tbl)
|
|
local result_t = vim.deepcopy(tbl)
|
|
table.sort(result_t, function(a, b)
|
|
return (a.method or '') < (b.method or '')
|
|
end)
|
|
return result_t
|
|
end
|
|
|
|
eq(
|
|
{
|
|
{
|
|
id = 'color-registration',
|
|
method = 'textDocument/colorPresentation',
|
|
registerOptions = { id = 'color-registration' },
|
|
},
|
|
{
|
|
id = 'color-registration',
|
|
method = 'textDocument/documentColor',
|
|
registerOptions = { id = 'color-registration' },
|
|
},
|
|
},
|
|
sort_method(exec_lua(function()
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
return client.dynamic_capabilities:get('colorProvider')
|
|
end))
|
|
)
|
|
|
|
eq(
|
|
{
|
|
{
|
|
id = 'diag-registration',
|
|
method = 'textDocument/diagnostic',
|
|
registerOptions = {
|
|
id = 'diag-registration',
|
|
identifier = 'diag-ident-static',
|
|
workspaceDiagnostics = true,
|
|
},
|
|
},
|
|
},
|
|
sort_method(exec_lua(function()
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
return client.dynamic_capabilities:get('diagnosticProvider')
|
|
end))
|
|
)
|
|
|
|
eq(
|
|
{ 'diag-ident-static' },
|
|
exec_lua(function()
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
local result = {}
|
|
client:_provider_foreach('textDocument/diagnostic', function(cap)
|
|
table.insert(result, cap.identifier)
|
|
end)
|
|
return result
|
|
end)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
describe('vim.lsp._watchfiles', function()
|
|
--- @type integer, integer, integer
|
|
local created, changed, deleted
|
|
|
|
setup(function()
|
|
n.clear()
|
|
created = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]])
|
|
changed = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]])
|
|
deleted = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]])
|
|
end)
|
|
|
|
local function test_filechanges(watchfunc)
|
|
it(
|
|
string.format('sends notifications when files change (watchfunc=%s)', watchfunc),
|
|
function()
|
|
if watchfunc == 'inotify' then
|
|
skip(is_os('win'), 'not supported on windows')
|
|
skip(is_os('mac'), 'flaky test on mac')
|
|
skip(
|
|
not is_ci() and fn.executable('inotifywait') == 0,
|
|
'inotify-tools not installed and not on CI'
|
|
)
|
|
skip(t.is_arch('s390x'), 'inotifywait not available on s390x CI')
|
|
end
|
|
|
|
if watchfunc == 'watch' then
|
|
skip(is_os('mac'), 'flaky test on mac')
|
|
skip(
|
|
is_os('bsd'),
|
|
'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38'
|
|
)
|
|
else
|
|
skip(
|
|
is_os('bsd'),
|
|
'kqueue only reports events on watched folder itself, not contained files #26110'
|
|
)
|
|
end
|
|
|
|
local root_dir = tmpname(false)
|
|
mkdir(root_dir)
|
|
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'watchfiles-test',
|
|
cmd = server.cmd,
|
|
root_dir = root_dir,
|
|
capabilities = {
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
require('vim.lsp._watchfiles')._watchfunc = require('vim._watch')[watchfunc]
|
|
|
|
local expected_messages = 0
|
|
|
|
local msg_wait_timeout = watchfunc == 'watch' and 200 or 2500
|
|
|
|
local function wait_for_message(incr)
|
|
expected_messages = expected_messages + (incr or 1)
|
|
assert(
|
|
vim.wait(msg_wait_timeout, function()
|
|
return #server.messages == expected_messages
|
|
end),
|
|
'Timed out waiting for expected number of messages. Current messages seen so far: '
|
|
.. vim.inspect(server.messages)
|
|
)
|
|
end
|
|
|
|
wait_for_message(2) -- initialize, initialized
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-0',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = '**/watch',
|
|
kind = 7,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
if watchfunc ~= 'watch' then
|
|
vim.wait(100)
|
|
end
|
|
|
|
local path = root_dir .. '/watch'
|
|
local tmp = vim.fn.tempname()
|
|
io.open(tmp, 'w'):close()
|
|
vim.uv.fs_rename(tmp, path)
|
|
|
|
wait_for_message()
|
|
|
|
os.remove(path)
|
|
|
|
wait_for_message()
|
|
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
|
|
return server.messages
|
|
end)
|
|
|
|
local uri = vim.uri_from_fname(root_dir .. '/watch')
|
|
|
|
eq(6, #result)
|
|
|
|
eq({
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
params = {
|
|
changes = {
|
|
{
|
|
type = created,
|
|
uri = uri,
|
|
},
|
|
},
|
|
},
|
|
}, result[3])
|
|
|
|
eq({
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
params = {
|
|
changes = {
|
|
{
|
|
type = deleted,
|
|
uri = uri,
|
|
},
|
|
},
|
|
},
|
|
}, result[4])
|
|
end
|
|
)
|
|
end
|
|
|
|
test_filechanges('watch')
|
|
test_filechanges('watchdirs')
|
|
test_filechanges('inotify')
|
|
|
|
it('correctly registers and unregisters', function()
|
|
local root_dir = '/some_dir'
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'watchfiles-test',
|
|
cmd = server.cmd,
|
|
root_dir = root_dir,
|
|
capabilities = {
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
local expected_messages = 2 -- initialize, initialized
|
|
local function wait_for_messages()
|
|
assert(
|
|
vim.wait(200, function()
|
|
return #server.messages == expected_messages
|
|
end),
|
|
'Timed out waiting for expected number of messages. Current messages seen so far: '
|
|
.. vim.inspect(server.messages)
|
|
)
|
|
end
|
|
|
|
wait_for_messages()
|
|
|
|
local send_event --- @type function
|
|
require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
|
|
local stopped = false
|
|
send_event = function(...)
|
|
if not stopped then
|
|
callback(...)
|
|
end
|
|
end
|
|
return function()
|
|
stopped = true
|
|
end
|
|
end
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-0',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = '**/*.watch0',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
|
|
send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)
|
|
|
|
expected_messages = expected_messages + 1
|
|
wait_for_messages()
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-1',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = '**/*.watch1',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
vim.lsp.handlers['client/unregisterCapability'](nil, {
|
|
unregisterations = {
|
|
{
|
|
id = 'watchfiles-test-0',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
|
|
send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)
|
|
|
|
expected_messages = expected_messages + 1
|
|
wait_for_messages()
|
|
|
|
return server.messages
|
|
end)
|
|
|
|
local function watched_uri(fname)
|
|
return vim.uri_from_fname(root_dir .. '/' .. fname)
|
|
end
|
|
|
|
eq(4, #result)
|
|
eq('workspace/didChangeWatchedFiles', result[3].method)
|
|
eq({
|
|
changes = {
|
|
{
|
|
type = created,
|
|
uri = watched_uri('file.watch0'),
|
|
},
|
|
},
|
|
}, result[3].params)
|
|
eq('workspace/didChangeWatchedFiles', result[4].method)
|
|
eq({
|
|
changes = {
|
|
{
|
|
type = created,
|
|
uri = watched_uri('file.watch1'),
|
|
},
|
|
},
|
|
}, result[4].params)
|
|
end)
|
|
|
|
it('correctly handles the registered watch kind', function()
|
|
local root_dir = 'some_dir'
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'watchfiles-test',
|
|
cmd = server.cmd,
|
|
root_dir = root_dir,
|
|
capabilities = {
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
local expected_messages = 2 -- initialize, initialized
|
|
local function wait_for_messages()
|
|
assert(
|
|
vim.wait(200, function()
|
|
return #server.messages == expected_messages
|
|
end),
|
|
'Timed out waiting for expected number of messages. Current messages seen so far: '
|
|
.. vim.inspect(server.messages)
|
|
)
|
|
end
|
|
|
|
wait_for_messages()
|
|
|
|
local watch_callbacks = {} --- @type function[]
|
|
local function send_event(...)
|
|
for _, cb in ipairs(watch_callbacks) do
|
|
cb(...)
|
|
end
|
|
end
|
|
require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
|
|
table.insert(watch_callbacks, callback)
|
|
return function()
|
|
-- noop because this test never stops the watch
|
|
end
|
|
end
|
|
|
|
local protocol = require('vim.lsp.protocol')
|
|
|
|
local watchers = {}
|
|
local max_kind = protocol.WatchKind.Create
|
|
+ protocol.WatchKind.Change
|
|
+ protocol.WatchKind.Delete
|
|
for i = 0, max_kind do
|
|
table.insert(watchers, {
|
|
globPattern = {
|
|
baseUri = vim.uri_from_fname('/dir'),
|
|
pattern = 'watch' .. tostring(i),
|
|
},
|
|
kind = i,
|
|
})
|
|
end
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-kind',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = watchers,
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
for i = 0, max_kind do
|
|
local filename = '/dir/watch' .. tostring(i)
|
|
send_event(filename, vim._watch.FileChangeType.Created)
|
|
send_event(filename, vim._watch.FileChangeType.Changed)
|
|
send_event(filename, vim._watch.FileChangeType.Deleted)
|
|
end
|
|
|
|
expected_messages = expected_messages + 1
|
|
wait_for_messages()
|
|
|
|
return server.messages
|
|
end)
|
|
|
|
local function watched_uri(fname)
|
|
return vim.uri_from_fname('/dir/' .. fname)
|
|
end
|
|
|
|
eq(3, #result)
|
|
eq('workspace/didChangeWatchedFiles', result[3].method)
|
|
eq({
|
|
changes = {
|
|
{
|
|
type = created,
|
|
uri = watched_uri('watch1'),
|
|
},
|
|
{
|
|
type = changed,
|
|
uri = watched_uri('watch2'),
|
|
},
|
|
{
|
|
type = created,
|
|
uri = watched_uri('watch3'),
|
|
},
|
|
{
|
|
type = changed,
|
|
uri = watched_uri('watch3'),
|
|
},
|
|
{
|
|
type = deleted,
|
|
uri = watched_uri('watch4'),
|
|
},
|
|
{
|
|
type = created,
|
|
uri = watched_uri('watch5'),
|
|
},
|
|
{
|
|
type = deleted,
|
|
uri = watched_uri('watch5'),
|
|
},
|
|
{
|
|
type = changed,
|
|
uri = watched_uri('watch6'),
|
|
},
|
|
{
|
|
type = deleted,
|
|
uri = watched_uri('watch6'),
|
|
},
|
|
{
|
|
type = created,
|
|
uri = watched_uri('watch7'),
|
|
},
|
|
{
|
|
type = changed,
|
|
uri = watched_uri('watch7'),
|
|
},
|
|
{
|
|
type = deleted,
|
|
uri = watched_uri('watch7'),
|
|
},
|
|
},
|
|
}, result[3].params)
|
|
end)
|
|
|
|
it('prunes duplicate events', function()
|
|
local root_dir = 'some_dir'
|
|
exec_lua(create_server_definition)
|
|
local result = exec_lua(function()
|
|
local server = _G._create_server()
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'watchfiles-test',
|
|
cmd = server.cmd,
|
|
root_dir = root_dir,
|
|
capabilities = {
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
local expected_messages = 2 -- initialize, initialized
|
|
local function wait_for_messages()
|
|
assert(
|
|
vim.wait(200, function()
|
|
return #server.messages == expected_messages
|
|
end),
|
|
'Timed out waiting for expected number of messages. Current messages seen so far: '
|
|
.. vim.inspect(server.messages)
|
|
)
|
|
end
|
|
|
|
wait_for_messages()
|
|
|
|
local send_event --- @type function
|
|
require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
|
|
send_event = callback
|
|
return function()
|
|
-- noop because this test never stops the watch
|
|
end
|
|
end
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-kind',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = '**/*',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
send_event('file1', vim._watch.FileChangeType.Created)
|
|
send_event('file1', vim._watch.FileChangeType.Created) -- pruned
|
|
send_event('file1', vim._watch.FileChangeType.Changed)
|
|
send_event('file2', vim._watch.FileChangeType.Created)
|
|
send_event('file1', vim._watch.FileChangeType.Changed) -- pruned
|
|
|
|
expected_messages = expected_messages + 1
|
|
wait_for_messages()
|
|
|
|
return server.messages
|
|
end)
|
|
|
|
eq(3, #result)
|
|
eq('workspace/didChangeWatchedFiles', result[3].method)
|
|
eq({
|
|
changes = {
|
|
{
|
|
type = created,
|
|
uri = vim.uri_from_fname('file1'),
|
|
},
|
|
{
|
|
type = changed,
|
|
uri = vim.uri_from_fname('file1'),
|
|
},
|
|
{
|
|
type = created,
|
|
uri = vim.uri_from_fname('file2'),
|
|
},
|
|
},
|
|
}, result[3].params)
|
|
end)
|
|
|
|
it("ignores registrations by servers when the client doesn't advertise support", function()
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
_G.server = _G._create_server()
|
|
require('vim.lsp._watchfiles')._watchfunc = function(_, _, _)
|
|
-- Since the registration is ignored, this should not execute and `watching` should stay false
|
|
_G.watching = true
|
|
return function() end
|
|
end
|
|
end)
|
|
|
|
local function check_registered(capabilities)
|
|
return exec_lua(function()
|
|
_G.watching = false
|
|
local client_id = assert(vim.lsp.start({
|
|
name = 'watchfiles-test',
|
|
cmd = _G.server.cmd,
|
|
root_dir = 'some_dir',
|
|
capabilities = capabilities,
|
|
}, {
|
|
reuse_client = function()
|
|
return false
|
|
end,
|
|
}))
|
|
|
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
|
registrations = {
|
|
{
|
|
id = 'watchfiles-test-kind',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
registerOptions = {
|
|
watchers = {
|
|
{
|
|
globPattern = '**/*',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
-- Ensure no errors occur when unregistering something that was never really registered.
|
|
vim.lsp.handlers['client/unregisterCapability'](nil, {
|
|
unregisterations = {
|
|
{
|
|
id = 'watchfiles-test-kind',
|
|
method = 'workspace/didChangeWatchedFiles',
|
|
},
|
|
},
|
|
}, { client_id = client_id })
|
|
|
|
vim.lsp.get_client_by_id(client_id):stop(true)
|
|
return _G.watching
|
|
end)
|
|
end
|
|
|
|
eq(is_os('mac') or is_os('win'), check_registered(nil)) -- start{_client}() defaults to make_client_capabilities().
|
|
eq(
|
|
false,
|
|
check_registered({
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = false,
|
|
},
|
|
},
|
|
})
|
|
)
|
|
eq(
|
|
true,
|
|
check_registered({
|
|
workspace = {
|
|
didChangeWatchedFiles = {
|
|
dynamicRegistration = true,
|
|
},
|
|
},
|
|
})
|
|
)
|
|
end)
|
|
end)
|
|
|
|
describe('vim.lsp.config() and vim.lsp.enable()', function()
|
|
---@param names string[]
|
|
local function get_resolved(names)
|
|
return exec_lua(function(names_)
|
|
local rv = {}
|
|
local cs = vim.lsp._enabled_configs
|
|
for _, k in ipairs(names_) do
|
|
rv[k] = not not (cs[k] and cs[k].resolved_config)
|
|
end
|
|
return rv
|
|
end, names)
|
|
end
|
|
|
|
it('merges settings from "*"', function()
|
|
eq(
|
|
{
|
|
name = 'foo',
|
|
cmd = { 'foo' },
|
|
root_markers = { '.git' },
|
|
},
|
|
exec_lua(function()
|
|
vim.lsp.config('*', { root_markers = { '.git' } })
|
|
vim.lsp.config('foo', { cmd = { 'foo' } })
|
|
|
|
return vim.lsp.config['foo']
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('config("bogus") shows a hint', function()
|
|
matches(
|
|
'hint%: to resolve a config',
|
|
pcall_err(exec_lua, function()
|
|
vim.print(vim.lsp.config('non-existent-config'))
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('sets up an autocmd', function()
|
|
eq(
|
|
1,
|
|
exec_lua(function()
|
|
vim.lsp.config('foo', {
|
|
cmd = { 'foo' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
vim.lsp.enable('foo')
|
|
return #vim.api.nvim_get_autocmds({
|
|
group = 'nvim.lsp.enable',
|
|
event = 'FileType',
|
|
})
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('handle nil config (some clients may not have a config!)', function()
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
local server = _G._create_server()
|
|
vim.bo.filetype = 'lua'
|
|
-- Attach a client without defining a config.
|
|
local client_id = vim.lsp.start({
|
|
name = 'test_ls',
|
|
cmd = function(dispatchers, config)
|
|
_G.test_resolved_root = config.root_dir --[[@type string]]
|
|
return server.cmd(dispatchers, config)
|
|
end,
|
|
}, { bufnr = 0 })
|
|
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local client = vim.lsp.get_client_by_id(client_id)
|
|
assert(client.attached_buffers[bufnr])
|
|
|
|
-- Exercise the codepath which had a regression:
|
|
vim.lsp.enable('test_ls')
|
|
vim.api.nvim_exec_autocmds('FileType', { buf = bufnr })
|
|
|
|
-- enable() does _not_ detach the client since it doesn't actually have a config.
|
|
-- XXX: otoh, is it confusing to allow `enable("foo")` if there a "foo" _client_ without a "foo" _config_?
|
|
assert(client.attached_buffers[bufnr])
|
|
assert(client_id == vim.lsp.get_client_by_id(bufnr).id)
|
|
end)
|
|
end)
|
|
|
|
it('attaches to buffers when they are opened', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
local tmp2 = t.tmpname(true)
|
|
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.lsp.config('foo', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'foo' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.config('bar', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'bar' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.enable('foo')
|
|
vim.lsp.enable('bar')
|
|
|
|
vim.cmd.edit(tmp1)
|
|
vim.bo.filetype = 'foo'
|
|
_G.foo_buf = vim.api.nvim_get_current_buf()
|
|
|
|
vim.cmd.edit(tmp2)
|
|
vim.bo.filetype = 'bar'
|
|
_G.bar_buf = vim.api.nvim_get_current_buf()
|
|
end)
|
|
|
|
eq(
|
|
{ 1, 'foo', 1, 'bar' },
|
|
exec_lua(function()
|
|
local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) })
|
|
local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) })
|
|
return { #foos, foos[1].name, #bars, bars[1].name }
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('attaches/detaches preexisting buffers', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
local tmp2 = t.tmpname(true)
|
|
|
|
exec_lua(function()
|
|
vim.cmd.edit(tmp1)
|
|
vim.bo.filetype = 'foo'
|
|
_G.foo_buf = vim.api.nvim_get_current_buf()
|
|
|
|
vim.cmd.edit(tmp2)
|
|
vim.bo.filetype = 'bar'
|
|
_G.bar_buf = vim.api.nvim_get_current_buf()
|
|
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.lsp.config('foo', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'foo' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.config('bar', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'bar' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.enable('foo')
|
|
vim.lsp.enable('bar')
|
|
end)
|
|
|
|
eq(
|
|
{ 1, 'foo', 1, 'bar' },
|
|
exec_lua(function()
|
|
local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) })
|
|
local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) })
|
|
return { #foos, foos[1].name, #bars, bars[1].name }
|
|
end)
|
|
)
|
|
|
|
-- Now disable the 'foo' lsp and confirm that it's detached from the buffer it was previous
|
|
-- attached to.
|
|
exec_lua([[vim.lsp.enable('foo', false)]])
|
|
eq(
|
|
{ 0, 'foo', 1, 'bar' },
|
|
exec_lua(function()
|
|
local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) })
|
|
local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) })
|
|
return { #foos, 'foo', #bars, bars[1].name }
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('in first FileType event (on startup)', function()
|
|
local tmp = tmpname()
|
|
write_file(tmp, string.dump(create_server_definition))
|
|
n.clear({
|
|
args = {
|
|
'--cmd',
|
|
string.format([[lua assert(loadfile(%q))()]], tmp),
|
|
'--cmd',
|
|
[[lua _G.server = _G._create_server({ handlers = {initialize = function(_, _, callback) callback(nil, {capabilities = {}}) end} })]],
|
|
'--cmd',
|
|
[[lua vim.lsp.config('foo', { cmd = _G.server.cmd, filetypes = { 'foo' }, root_markers = { '.foorc' } })]],
|
|
'--cmd',
|
|
[[au FileType * ++once lua vim.lsp.enable('foo')]],
|
|
'-c',
|
|
'set ft=foo',
|
|
},
|
|
})
|
|
|
|
eq(
|
|
{ 1, 'foo' },
|
|
exec_lua(function()
|
|
local foos = vim.lsp.get_clients({ bufnr = 0 })
|
|
return { #foos, (foos[1] or {}).name }
|
|
end)
|
|
)
|
|
exec_lua([[vim.lsp.enable('foo', false)]])
|
|
eq(
|
|
0,
|
|
exec_lua(function()
|
|
return #vim.lsp.get_clients({ bufnr = 0 })
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('does not attach to buffers more than once if no root_dir', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
|
|
eq(
|
|
1,
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.lsp.config('foo', { cmd = server.cmd, filetypes = { 'foo' } })
|
|
vim.lsp.enable('foo')
|
|
|
|
vim.cmd.edit(assert(tmp1))
|
|
vim.bo.filetype = 'foo'
|
|
vim.bo.filetype = 'foo'
|
|
|
|
return #vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('async root_dir, cmd(…,config) gets resolved config', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.lsp.config('foo', {
|
|
cmd = function(dispatchers, config)
|
|
_G.test_resolved_root = config.root_dir --[[@type string]]
|
|
return server.cmd(dispatchers, config)
|
|
end,
|
|
filetypes = { 'foo' },
|
|
root_dir = function(bufnr, cb)
|
|
assert(tmp1 == vim.api.nvim_buf_get_name(bufnr))
|
|
vim.system({ 'sleep', '0' }, {}, function()
|
|
cb('some_dir')
|
|
end)
|
|
end,
|
|
})
|
|
vim.lsp.enable('foo')
|
|
|
|
vim.cmd.edit(assert(tmp1))
|
|
vim.bo.filetype = 'foo'
|
|
end)
|
|
|
|
retry(nil, 1000, function()
|
|
eq(
|
|
'some_dir',
|
|
exec_lua(function()
|
|
return vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })[1].root_dir
|
|
end)
|
|
)
|
|
end)
|
|
eq(
|
|
'some_dir',
|
|
exec_lua(function()
|
|
return _G.test_resolved_root
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('starts correct LSP and stops incorrect LSP when filetype changes', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.lsp.config('foo', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'foo' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.config('bar', {
|
|
cmd = server.cmd,
|
|
filetypes = { 'bar' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
|
|
vim.lsp.enable('foo')
|
|
vim.lsp.enable('bar')
|
|
|
|
vim.cmd.edit(tmp1)
|
|
end)
|
|
|
|
local count_clients = function()
|
|
return exec_lua(function()
|
|
local foos = vim.lsp.get_clients({ name = 'foo', bufnr = 0 })
|
|
local bars = vim.lsp.get_clients({ name = 'bar', bufnr = 0 })
|
|
return { #foos, 'foo', #bars, 'bar' }
|
|
end)
|
|
end
|
|
|
|
-- No filetype on the buffer yet, so no LSPs.
|
|
eq({ 0, 'foo', 0, 'bar' }, count_clients())
|
|
|
|
-- Set the filetype to 'foo', confirm a LSP starts.
|
|
exec_lua([[vim.bo.filetype = 'foo']])
|
|
eq({ 1, 'foo', 0, 'bar' }, count_clients())
|
|
|
|
-- Set the filetype to 'bar', confirm a new LSP starts, and the old one goes away.
|
|
exec_lua([[vim.bo.filetype = 'bar']])
|
|
eq({ 0, 'foo', 1, 'bar' }, count_clients())
|
|
end)
|
|
|
|
it('sends didClose and didOpen when languageId changes', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
|
|
exec_lua(function()
|
|
_G.server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = { textDocumentSync = { openClose = true } } })
|
|
end,
|
|
},
|
|
})
|
|
vim.lsp.config('foo', { cmd = _G.server.cmd, filetypes = { 'foo', 'bar' } })
|
|
vim.lsp.enable('foo')
|
|
vim.cmd.edit(tmp1)
|
|
end)
|
|
|
|
local function test_messages()
|
|
local opens = 0
|
|
local closes = 0
|
|
local msgs = exec_lua([[ return _G.server.messages ]])
|
|
local num_clients = exec_lua([[ return #vim.lsp.get_clients() ]])
|
|
for _, msg in ipairs(msgs) do
|
|
opens = opens + (msg.method == 'textDocument/didOpen' and 1 or 0)
|
|
closes = closes + (msg.method == 'textDocument/didClose' and 1 or 0)
|
|
end
|
|
return { opens, 'did_open', closes, 'did_close', num_clients, 'clients' }
|
|
end
|
|
|
|
-- No filetype on the buffer yet.
|
|
eq({ 0, 'did_open', 0, 'did_close', 0, 'clients' }, test_messages())
|
|
|
|
-- Set the filetype to 'foo', confirm didOpen is sent.
|
|
exec_lua([[vim.bo.filetype = 'foo']])
|
|
retry(nil, 1000, function()
|
|
eq({ 1, 'did_open', 0, 'did_close', 1, 'clients' }, test_messages())
|
|
end)
|
|
|
|
-- Set to anohter lsp-supported filetype 'bar', confirm didClose and didOpen are sent.
|
|
exec_lua([[vim.bo.filetype = 'bar']])
|
|
retry(nil, 1000, function()
|
|
eq({ 2, 'did_open', 1, 'did_close', 1, 'clients' }, test_messages())
|
|
end)
|
|
end)
|
|
|
|
it('validates config on attach', function()
|
|
local tmp1 = t.tmpname(true)
|
|
exec_lua(function()
|
|
vim.fn.writefile({ '' }, fake_lsp_logfile)
|
|
vim.lsp.log._set_filename(fake_lsp_logfile)
|
|
end)
|
|
|
|
local function test_cfg(cfg, err)
|
|
exec_lua(function()
|
|
vim.lsp.config['foo'] = {}
|
|
vim.lsp.config('foo', cfg)
|
|
vim.lsp.enable('foo')
|
|
vim.cmd.edit(assert(tmp1))
|
|
vim.bo.filetype = 'non.applicable.filetype'
|
|
end)
|
|
|
|
-- Assert NO log for non-applicable 'filetype'. #35737
|
|
if type(cfg.filetypes) == 'table' then
|
|
t.assert_nolog(err, fake_lsp_logfile)
|
|
end
|
|
|
|
exec_lua(function()
|
|
vim.bo.filetype = 'foo'
|
|
end)
|
|
|
|
retry(nil, 1000, function()
|
|
t.assert_log(err, fake_lsp_logfile)
|
|
end)
|
|
end
|
|
|
|
test_cfg({
|
|
filetypes = { 'foo' },
|
|
cmd = { 'lolling' },
|
|
}, 'invalid "foo" config: .* lolling is not executable')
|
|
|
|
test_cfg({
|
|
cmd = { 'cat' },
|
|
filetypes = true,
|
|
}, 'invalid "foo" config: .* filetypes: expected table, got boolean')
|
|
end)
|
|
|
|
it('does not start without workspace if workspace_required=true', function()
|
|
exec_lua(create_server_definition)
|
|
|
|
local tmp1 = t.tmpname(true)
|
|
|
|
eq(
|
|
{ workspace_required = false },
|
|
exec_lua(function()
|
|
local server = _G._create_server({
|
|
handlers = {
|
|
initialize = function(_, _, callback)
|
|
callback(nil, { capabilities = {} })
|
|
end,
|
|
},
|
|
})
|
|
|
|
local ws_required = { cmd = server.cmd, workspace_required = true, filetypes = { 'foo' } }
|
|
local ws_not_required = vim.deepcopy(ws_required)
|
|
ws_not_required.workspace_required = false
|
|
|
|
vim.lsp.config('ws_required', ws_required)
|
|
vim.lsp.config('ws_not_required', ws_not_required)
|
|
vim.lsp.enable('ws_required')
|
|
vim.lsp.enable('ws_not_required')
|
|
|
|
vim.cmd.edit(assert(tmp1))
|
|
vim.bo.filetype = 'foo'
|
|
|
|
local clients = vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })
|
|
assert(1 == #clients)
|
|
return { workspace_required = clients[1].config.workspace_required }
|
|
end)
|
|
)
|
|
end)
|
|
|
|
it('does not allow wildcards in config name', function()
|
|
local err =
|
|
'.../lsp.lua:0: name: expected non%-wildcard string, got foo%*%. Info: LSP config name cannot contain wildcard %("%*"%)'
|
|
|
|
matches(
|
|
err,
|
|
pcall_err(exec_lua, function()
|
|
local _ = vim.lsp.config['foo*']
|
|
end)
|
|
)
|
|
matches(
|
|
err,
|
|
pcall_err(exec_lua, function()
|
|
vim.lsp.config['foo*'] = {}
|
|
end)
|
|
)
|
|
matches(
|
|
err,
|
|
pcall_err(exec_lua, function()
|
|
vim.lsp.config('foo*', {})
|
|
end)
|
|
)
|
|
-- Exception for '*'
|
|
pcall(exec_lua, function()
|
|
vim.lsp.config('*', {})
|
|
end)
|
|
end)
|
|
|
|
it('root_markers priority', function()
|
|
--- Setup directories for testing
|
|
-- root/
|
|
-- ├── dir_a/
|
|
-- │ ├── dir_b/
|
|
-- │ │ ├── target
|
|
-- │ │ └── marker_d
|
|
-- │ ├── marker_b
|
|
-- │ └── marker_c
|
|
-- └── marker_a
|
|
|
|
---@param filepath string
|
|
local function touch(filepath)
|
|
local file = io.open(filepath, 'w')
|
|
if file then
|
|
file:close()
|
|
end
|
|
end
|
|
|
|
local tmp_root = tmpname(false)
|
|
local marker_a = tmp_root .. '/marker_a'
|
|
local dir_a = tmp_root .. '/dir_a'
|
|
local marker_b = dir_a .. '/marker_b'
|
|
local marker_c = dir_a .. '/marker_c'
|
|
local dir_b = dir_a .. '/dir_b'
|
|
local marker_d = dir_b .. '/marker_d'
|
|
local target = dir_b .. '/target'
|
|
|
|
mkdir(tmp_root)
|
|
touch(marker_a)
|
|
mkdir(dir_a)
|
|
touch(marker_b)
|
|
touch(marker_c)
|
|
mkdir(dir_b)
|
|
touch(marker_d)
|
|
touch(target)
|
|
|
|
exec_lua(create_server_definition)
|
|
exec_lua(function()
|
|
_G._custom_server = _G._create_server()
|
|
end)
|
|
|
|
---@param root_markers (string|string[])[]
|
|
---@param expected_root_dir string?
|
|
local function markers_resolve_to(root_markers, expected_root_dir)
|
|
exec_lua(function()
|
|
vim.lsp.config['foo'] = {}
|
|
vim.lsp.config('foo', {
|
|
cmd = _G._custom_server.cmd,
|
|
reuse_client = function()
|
|
return false
|
|
end,
|
|
filetypes = { 'foo' },
|
|
root_markers = root_markers,
|
|
})
|
|
vim.lsp.enable('foo')
|
|
vim.cmd.edit(target)
|
|
vim.bo.filetype = 'foo'
|
|
end)
|
|
retry(nil, 1000, function()
|
|
eq(
|
|
expected_root_dir,
|
|
exec_lua(function()
|
|
local clients = vim.lsp.get_clients()
|
|
return clients[#clients].root_dir
|
|
end)
|
|
)
|
|
end)
|
|
end
|
|
|
|
markers_resolve_to({ 'marker_d' }, dir_b)
|
|
markers_resolve_to({ 'marker_b' }, dir_a)
|
|
markers_resolve_to({ 'marker_c' }, dir_a)
|
|
markers_resolve_to({ 'marker_a' }, tmp_root)
|
|
markers_resolve_to({ 'foo' }, nil)
|
|
markers_resolve_to({ { 'marker_b', 'marker_a' }, 'marker_d' }, dir_a)
|
|
markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root)
|
|
markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b)
|
|
end)
|
|
|
|
it('vim.lsp.is_enabled()', function()
|
|
exec_lua(function()
|
|
vim.lsp.config('foo', {
|
|
cmd = { 'foo' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
end)
|
|
|
|
-- LSP config defaults to disabled.
|
|
eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]]))
|
|
|
|
-- Confirm we can enable it.
|
|
exec_lua([[vim.lsp.enable('foo')]])
|
|
eq(true, exec_lua([[return vim.lsp.is_enabled('foo')]]))
|
|
|
|
-- And finally, disable it again.
|
|
exec_lua([[vim.lsp.enable('foo', false)]])
|
|
eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]]))
|
|
end)
|
|
|
|
it('vim.lsp.get_configs()', function()
|
|
exec_lua(function()
|
|
vim.lsp.config('foo', {
|
|
cmd = { 'foo' },
|
|
filetypes = { 'foofile' },
|
|
root_markers = { '.foorc' },
|
|
})
|
|
vim.lsp.config('bar', {
|
|
cmd = { 'bar' },
|
|
root_markers = { '.barrc' },
|
|
})
|
|
vim.lsp.enable('foo')
|
|
end)
|
|
|
|
local function names(configs)
|
|
local config_names = vim
|
|
.iter(configs)
|
|
:map(function(config)
|
|
return config.name
|
|
end)
|
|
:totable()
|
|
table.sort(config_names)
|
|
return config_names
|
|
end
|
|
|
|
eq({ 'foo' }, names(exec_lua([[return vim.lsp.get_configs { enabled = true }]])))
|
|
-- Does NOT resolve non-enabled configs.
|
|
eq({ foo = true, bar = false }, get_resolved({ 'bar', 'foo' }))
|
|
|
|
eq({ 'bar' }, names(exec_lua([[return vim.lsp.get_configs { enabled = false }]])))
|
|
|
|
-- With no filter, return all configs
|
|
eq({ 'bar', 'foo' }, names(exec_lua([[return vim.lsp.get_configs()]])))
|
|
|
|
-- Confirm `filetype` works
|
|
eq({ 'foo' }, names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile' }]])))
|
|
|
|
-- Confirm filters combine
|
|
eq(
|
|
{ 'foo' },
|
|
names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile', enabled = true }]]))
|
|
)
|
|
eq(
|
|
{},
|
|
names(exec_lua([[return vim.lsp.get_configs { filetype = 'foofile', enabled = false }]]))
|
|
)
|
|
-- Does NOT resolve non-enabled configs.
|
|
eq({ foo = true, bar = false }, get_resolved({ 'bar', 'foo' }))
|
|
end)
|
|
end)
|
|
end)
|