aboutsummaryrefslogtreecommitdiff
path: root/test/functional/plugin/lsp_spec.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/plugin/lsp_spec.lua')
-rw-r--r--test/functional/plugin/lsp_spec.lua1198
1 files changed, 1096 insertions, 102 deletions
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index fd162961ff..56d31a0e44 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -23,6 +23,7 @@ local is_ci = helpers.is_ci
local meths = helpers.meths
local is_os = helpers.is_os
local skip = helpers.skip
+local mkdir = helpers.mkdir
local clear_notrace = lsp_helpers.clear_notrace
local create_server_definition = lsp_helpers.create_server_definition
@@ -30,6 +31,13 @@ local fake_lsp_code = lsp_helpers.fake_lsp_code
local fake_lsp_logfile = lsp_helpers.fake_lsp_logfile
local test_rpc_server = lsp_helpers.test_rpc_server
+local function get_buf_option(name, bufnr)
+ bufnr = bufnr or "BUFFER"
+ return exec_lua(
+ string.format("return vim.api.nvim_get_option_value('%s', { buf = %s })", name, bufnr)
+ )
+end
+
-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837
if skip(is_os('win')) then return end
@@ -51,12 +59,13 @@ describe('LSP', function()
return lsp.start_client {
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.loop.cwd(),
+ uri = 'file://' .. vim.uv.cwd(),
name = 'test_folder',
}};
}
@@ -73,7 +82,7 @@ describe('LSP', function()
describe('server_name specified', function()
it('start_client(), stop_client()', function()
retry(nil, 4000, function()
- eq(1, exec_lua('return #lsp.get_active_clients()'))
+ eq(1, exec_lua('return #lsp.get_clients()'))
end)
eq(2, exec_lua([[
TEST_CLIENT2 = test__start_client()
@@ -84,20 +93,20 @@ describe('LSP', function()
return TEST_CLIENT3
]]))
retry(nil, 4000, function()
- eq(3, exec_lua('return #lsp.get_active_clients()'))
+ eq(3, exec_lua('return #lsp.get_clients()'))
end)
eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil'))
eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).is_stopped()'))
exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).stop()')
retry(nil, 4000, function()
- eq(2, exec_lua('return #lsp.get_active_clients()'))
+ eq(2, exec_lua('return #lsp.get_clients()'))
end)
eq(true, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil'))
exec_lua('lsp.stop_client({TEST_CLIENT2, TEST_CLIENT3})')
retry(nil, 4000, function()
- eq(0, exec_lua('return #lsp.get_active_clients()'))
+ eq(0, exec_lua('return #lsp.get_clients()'))
end)
end)
@@ -107,12 +116,12 @@ describe('LSP', function()
TEST_CLIENT3 = test__start_client()
]])
retry(nil, 4000, function()
- eq(3, exec_lua('return #lsp.get_active_clients()'))
+ eq(3, exec_lua('return #lsp.get_clients()'))
end)
-- Stop all clients.
- exec_lua('lsp.stop_client(lsp.get_active_clients())')
+ exec_lua('lsp.stop_client(lsp.get_clients())')
retry(nil, 4000, function()
- eq(0, exec_lua('return #lsp.get_active_clients()'))
+ eq(0, exec_lua('return #lsp.get_clients()'))
end)
end)
end)
@@ -142,7 +151,7 @@ describe('LSP', function()
describe('basic_init test', function()
after_each(function()
stop()
- exec_lua("lsp.stop_client(lsp.get_active_clients(), true)")
+ exec_lua("lsp.stop_client(lsp.get_clients(), true)")
exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })")
end)
@@ -209,6 +218,34 @@ describe('LSP', function()
})
end)
+ it("should set the client's offset_encoding when positionEncoding capability is supported", function()
+ clear()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _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
+ ]])
+ eq('utf-8', result)
+ end)
+
it('should succeed with manual shutdown', function()
if is_ci() then
pending('hangs the build on CI #14028, re-enable with freeze timeout #14204')
@@ -312,6 +349,118 @@ describe('LSP', function()
}
end)
+ it('should set default options on attach', function()
+ local client
+ test_rpc_server {
+ test_name = "set_defaults_all_capabilities";
+ on_init = function(_client)
+ client = _client
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
+ ]]
+ 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[[
+ local keymap
+ vim.api.nvim_buf_call(BUFFER, function()
+ keymap = vim.fn.maparg("K", "n", false, true)
+ end)
+ return keymap.callback == vim.lsp.buf.hover
+ ]])
+ client.stop()
+ end
+ end;
+ on_exit = function(_, _)
+ eq('', get_buf_option("tagfunc"))
+ eq('', get_buf_option("omnifunc"))
+ eq('', get_buf_option("formatexpr"))
+ eq('', exec_lua[[
+ local keymap
+ vim.api.nvim_buf_call(BUFFER, function()
+ keymap = vim.fn.maparg("K", "n", false, false)
+ end)
+ return keymap
+ ]])
+ end;
+ }
+ end)
+
+ it('should overwrite options set by ftplugins', function()
+ local client
+ test_rpc_server {
+ test_name = "set_defaults_all_capabilities";
+ on_init = function(_client)
+ client = _client
+ exec_lua [[
+ 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 })
+ ]]
+
+ -- 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 [[
+ lsp.buf_attach_client(BUFFER_1, TEST_RPC_CLIENT_ID)
+ lsp.buf_attach_client(BUFFER_2, TEST_RPC_CLIENT_ID)
+ ]]
+ 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
+ test_rpc_server {
+ test_name = "set_defaults_all_capabilities";
+ on_init = function(_client)
+ client = _client
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_set_option_value('tagfunc', 'tfu', { buf = BUFFER })
+ vim.api.nvim_set_option_value('omnifunc', 'ofu', { buf = BUFFER })
+ vim.api.nvim_set_option_value('formatexpr', 'fex', { buf = BUFFER })
+ lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)
+ ]]
+ 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()
clear()
exec_lua(create_server_definition)
@@ -842,7 +991,7 @@ describe('LSP', function()
test_name = "check_tracked_requests_cleared";
on_init = function(_client)
command('let g:requests = 0')
- command('autocmd User LspRequest let g:requests+=1')
+ command('autocmd LspRequest * let g:requests+=1')
client = _client
client.request("slow_request")
eq(1, eval('g:requests'))
@@ -936,7 +1085,7 @@ describe('LSP', function()
eq(full_kind, client.server_capabilities().textDocumentSync.change)
eq(true, client.server_capabilities().textDocumentSync.openClose)
exec_lua [[
- assert(not lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Shouldn't attach twice")
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Already attached, returns true")
]]
end;
on_exit = function(code, signal)
@@ -1062,7 +1211,7 @@ describe('LSP', function()
"testing";
"123";
})
- vim.api.nvim_buf_set_option(BUFFER, 'eol', false)
+ vim.bo[BUFFER].eol = false
]]
end;
on_init = function(_client)
@@ -1095,6 +1244,67 @@ describe('LSP', function()
}
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,
+ }};
+ {NIL, {}, {method="start", client_id=1}};
+ }
+ local client
+ test_rpc_server {
+ test_name = "inlay_hint";
+ on_setup = function()
+ exec_lua [[
+ BUFFER = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, {
+ "testing";
+ "123";
+ })
+ vim.bo[BUFFER].eol = false
+ ]]
+ end;
+ on_init = function(_client)
+ client = _client
+ eq(true, client.supports_method('textDocument/inlayHint'))
+ exec_lua [[
+ assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID))
+ ]]
+ end;
+ on_exit = function(code, signal)
+ eq(0, code, "exit code")
+ eq(0, signal, "exit signal")
+ end;
+ on_handler = function(err, result, ctx)
+ if ctx.method == 'start' then
+ exec_lua [[
+ vim.lsp.inlay_hint.enable(BUFFER)
+ ]]
+ 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}};
@@ -1545,6 +1755,54 @@ describe('LSP', function()
'foobar';
}, buf_lines(1))
end)
+ it('it restores marks', function()
+ local edits = {
+ make_edit(1, 0, 2, 5, "foobar");
+ make_edit(4, 0, 5, 0, "barfoo");
+ }
+ eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 2, 1, {})'))
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16")
+ eq({
+ 'First line of text';
+ 'foobar line of text';
+ 'Fourth line of text';
+ 'barfoo';
+ }, buf_lines(1))
+ local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")')
+ eq({ 2, 1 }, mark)
+ end)
+
+ it('it restores marks to last valid col', function()
+ local edits = {
+ make_edit(1, 0, 2, 15, "foobar");
+ make_edit(4, 0, 5, 0, "barfoo");
+ }
+ eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 2, 10, {})'))
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16")
+ eq({
+ 'First line of text';
+ 'foobarext';
+ 'Fourth line of text';
+ 'barfoo';
+ }, buf_lines(1))
+ local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")')
+ eq({ 2, 9 }, mark)
+ end)
+
+ it('it restores marks to last valid line', function()
+ local edits = {
+ make_edit(1, 0, 4, 5, "foobar");
+ make_edit(4, 0, 5, 0, "barfoo");
+ }
+ eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 4, 1, {})'))
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16")
+ eq({
+ 'First line of text';
+ 'foobaro';
+ }, buf_lines(1))
+ local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")')
+ eq({ 2, 1 }, mark)
+ end)
describe('cursor position', function()
it('don\'t fix the cursor if the range contains the cursor', function()
@@ -1590,7 +1848,7 @@ describe('LSP', function()
eq({
'First line of text';
}, buf_lines(1))
- eq({ 1, 6 }, funcs.nvim_win_get_cursor(0))
+ eq({ 1, 17 }, funcs.nvim_win_get_cursor(0))
end)
it('fix the cursor row', function()
@@ -1609,6 +1867,9 @@ describe('LSP', function()
end)
it('fix the cursor col', function()
+ -- append empty last line. See #22636
+ exec_lua('vim.api.nvim_buf_set_lines(...)', 1, -1, -1, true, {''})
+
funcs.nvim_win_set_cursor(0, { 2, 11 })
local edits = {
make_edit(1, 7, 1, 11, '')
@@ -1620,6 +1881,7 @@ describe('LSP', function()
'Third line of text';
'Fourth line of text';
'å å ɧ 汉语 ↥ 🤦 🦄';
+ '';
}, buf_lines(1))
eq({ 2, 7 }, funcs.nvim_win_get_cursor(0))
end)
@@ -1921,7 +2183,7 @@ describe('LSP', function()
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
end)
it('Supports file creation in folder that needs to be created with CreateFile payload', function()
local tmpfile = helpers.tmpname()
@@ -1937,7 +2199,7 @@ describe('LSP', function()
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
end)
it('createFile does not touch file if it exists and ignoreIfExists is set', function()
local tmpfile = helpers.tmpname()
@@ -1955,7 +2217,7 @@ describe('LSP', function()
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
eq('Dummy content', read_file(tmpfile))
end)
it('createFile overrides file if overwrite is set', function()
@@ -1975,7 +2237,7 @@ describe('LSP', function()
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
eq('', read_file(tmpfile))
end)
it('DeleteFile delete file and buffer', function()
@@ -1996,7 +2258,7 @@ describe('LSP', function()
}
}
eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16'))
- eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
eq(false, exec_lua('return vim.api.nvim_buf_is_loaded(vim.fn.bufadd(...))', tmpfile))
end)
it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function()
@@ -2015,58 +2277,13 @@ describe('LSP', function()
}
}
eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit))
- eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
- end)
- end)
-
- describe('completion_list_to_complete_items', function()
- -- Completion option precedence:
- -- textEdit.newText > insertText > label
- -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
- it('should choose right completion option', function ()
- local prefix = 'foo'
- local completion_list = {
- -- resolves into label
- { label='foobar', sortText="a" },
- { label='foobar', sortText="b", textEdit={} },
- -- resolves into insertText
- { label='foocar', sortText="c", insertText='foobar' },
- { label='foocar', sortText="d", insertText='foobar', textEdit={} },
- -- resolves into textEdit.newText
- { label='foocar', sortText="e", insertText='foodar', textEdit={newText='foobar'} },
- { label='foocar', sortText="f", textEdit={newText='foobar'} },
- -- real-world snippet text
- { label='foocar', sortText="g", insertText='foodar', insertTextFormat=2, textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} },
- { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} },
- -- nested snippet tokens
- { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} },
- -- braced tabstop
- { label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} },
- -- plain text
- { label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} },
- }
- local completion_list_items = {items=completion_list}
- local expected = {
- { abbr = 'foobar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label = 'foobar', sortText="a" } } } } },
- { abbr = 'foobar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foobar', sortText="b", textEdit={} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="c", insertText='foobar' } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="d", insertText='foobar', textEdit={} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="e", insertText='foodar', textEdit={newText='foobar'} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="f", textEdit={newText='foobar'} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar(place holder, more ...holder{})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="g", insertText='foodar', insertTextFormat=2, textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ1, var2 *typ2) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ2 tail) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar()', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} } } } } },
- { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(${1:var1})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} } } } } },
- }
-
- eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list, prefix))
- eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list_items, prefix))
- eq({}, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], {}, prefix))
+ eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile))
end)
end)
describe('lsp.util.rename', function()
+ local pathsep = helpers.get_pathsep()
+
it('Can rename an existing file', function()
local old = helpers.tmpname()
write_file(old, 'Test content')
@@ -2083,12 +2300,56 @@ describe('LSP', function()
return vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
]], old, new)
eq({'Test content'}, lines)
- local exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', old)
+ local exists = exec_lua('return vim.uv.fs_stat(...) ~= nil', old)
eq(false, exists)
- exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', new)
+ exists = exec_lua('return vim.uv.fs_stat(...) ~= nil', new)
eq(true, exists)
os.remove(new)
end)
+ it("Kills old buffer after renaming an existing file", function()
+ local old = helpers.tmpname()
+ write_file(old, 'Test content')
+ local new = helpers.tmpname()
+ os.remove(new) -- only reserve the name, file must not exist for the test scenario
+ local lines = exec_lua([[
+ local old = select(1, ...)
+ local oldbufnr = vim.fn.bufadd(old)
+ local new = select(2, ...)
+ vim.lsp.util.rename(old, new)
+ return vim.fn.bufloaded(oldbufnr)
+ ]], old, new)
+ eq(0, lines)
+ os.remove(new)
+ end)
+ it('Can rename a directory', function()
+ -- only reserve the name, file must not exist for the test scenario
+ local old_dir = helpers.tmpname()
+ local new_dir = helpers.tmpname()
+ os.remove(old_dir)
+ os.remove(new_dir)
+
+ helpers.mkdir_p(old_dir)
+
+ local file = 'file.txt'
+ write_file(old_dir .. pathsep .. file, 'Test content')
+
+ local lines = exec_lua([[
+ local old_dir = select(1, ...)
+ local new_dir = select(2, ...)
+ local pathsep = select(3, ...)
+ local oldbufnr = vim.fn.bufadd(old_dir .. pathsep .. 'file')
+
+ vim.lsp.util.rename(old_dir, new_dir)
+ return vim.fn.bufloaded(oldbufnr)
+ ]], old_dir, new_dir, pathsep)
+ eq(0, lines)
+ eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', old_dir))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new_dir))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new_dir .. pathsep .. file))
+ eq('Test content', read_file(new_dir .. pathsep .. file))
+
+ os.remove(new_dir)
+ end)
it('Does not rename file if target exists and ignoreIfExists is set or overwrite is false', function()
local old = helpers.tmpname()
write_file(old, 'Old File')
@@ -2102,7 +2363,7 @@ describe('LSP', function()
vim.lsp.util.rename(old, new, { ignoreIfExists = true })
]], old, new)
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', old))
eq('New file', read_file(new))
exec_lua([[
@@ -2112,7 +2373,7 @@ describe('LSP', function()
vim.lsp.util.rename(old, new, { overwrite = false })
]], old, new)
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', old))
eq('New file', read_file(new))
end)
it('Does override target if overwrite is true', function()
@@ -2127,8 +2388,8 @@ describe('LSP', function()
vim.lsp.util.rename(old, new, { overwrite = true })
]], old, new)
- eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
- eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', new))
+ eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', old))
+ eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new))
eq('Old file\n', read_file(new))
end)
end)
@@ -2140,7 +2401,14 @@ describe('LSP', function()
filename = '/fake/uri',
lnum = 1,
col = 3,
- text = 'testing'
+ text = 'testing',
+ user_data = {
+ uri = 'file:///fake/uri',
+ range = {
+ start = { line = 0, character = 2 },
+ ['end'] = { line = 0, character = 3 },
+ }
+ }
},
}
local actual = exec_lua [[
@@ -2166,7 +2434,18 @@ describe('LSP', function()
filename = '/fake/uri',
lnum = 1,
col = 3,
- text = 'testing'
+ text = 'testing',
+ user_data = {
+ targetUri = "file:///fake/uri",
+ targetRange = {
+ start = { line = 0, character = 2 },
+ ['end'] = { line = 0, character = 3 },
+ },
+ targetSelectionRange = {
+ start = { line = 0, character = 2 },
+ ['end'] = { line = 0, character = 3 },
+ }
+ }
},
}
local actual = exec_lua [[
@@ -2443,18 +2722,6 @@ describe('LSP', function()
end)
end)
- describe('lsp.util._get_completion_item_kind_name', function()
- it('returns the name specified by protocol', function()
- eq("Text", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1)"))
- eq("TypeParameter", exec_lua("return vim.lsp.util._get_completion_item_kind_name(25)"))
- end)
- it('returns the name not specified by protocol', function()
- eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(nil)"))
- eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(vim.NIL)"))
- eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1000)"))
- end)
- end)
-
describe('lsp.util._get_symbol_kind_name', function()
it('returns the name specified by protocol', function()
eq("File", exec_lua("return vim.lsp.util._get_symbol_kind_name(1)"))
@@ -2743,6 +3010,18 @@ describe('LSP', function()
it('calculates size correctly with wrapping', function()
eq({15,5}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents,{width = 15, wrap_at = 14})} ]])
end)
+
+ it('handles NUL bytes in text', function()
+ exec_lua([[ contents = {
+ '\000\001\002\003\004\005\006\007\008\009',
+ '\010\011\012\013\014\015\016\017\018\019',
+ '\020\021\022\023\024\025\026\027\028\029',
+ } ]])
+ command('set list listchars=')
+ eq({20,3}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents)} ]])
+ command('set display+=uhex')
+ eq({40,3}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents)} ]])
+ end)
end)
describe('lsp.util.trim.trim_empty_lines', function()
@@ -2759,7 +3038,7 @@ describe('LSP', function()
activeSignature = -1,
signatures = {
{
- documentation = "",
+ documentation = "some doc",
label = "TestEntity.TestEntity()",
parameters = {}
},
@@ -2767,7 +3046,7 @@ describe('LSP', function()
}
return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', {','})
]]
- local expected = {'```cs', 'TestEntity.TestEntity()', '```', ''}
+ local expected = {'```cs', 'TestEntity.TestEntity()', '```', 'some doc'}
eq(expected, result)
end)
end)
@@ -2775,8 +3054,8 @@ describe('LSP', function()
describe('lsp.util.get_effective_tabstop', function()
local function test_tabstop(tabsize, shiftwidth)
exec_lua(string.format([[
- vim.api.nvim_buf_set_option(0, 'shiftwidth', %d)
- vim.api.nvim_buf_set_option(0, 'tabstop', 2)
+ vim.bo.shiftwidth = %d
+ vim.bo.tabstop = 2
]], shiftwidth))
eq(tabsize, exec_lua('return vim.lsp.util.get_effective_tabstop()'))
end
@@ -2998,9 +3277,10 @@ describe('LSP', function()
eq(0, signal, "exit signal")
end;
on_handler = function(err, result, ctx)
- -- Don't compare & assert params, they're not relevant for the testcase
+ -- Don't compare & assert params and version, they're not relevant for the testcase
-- This allows us to be lazy and avoid declaring them
ctx.params = nil
+ ctx.version = nil
eq(table.remove(test.expected_handlers), {err, result, ctx}, "expected handler")
if ctx.method == 'start' then
@@ -3082,6 +3362,7 @@ describe('LSP', function()
end,
on_handler = function(err, result, ctx)
ctx.params = nil -- don't compare in assert
+ ctx.version = nil
eq(table.remove(expected_handlers), { err, result, ctx })
if ctx.method == 'start' then
exec_lua([[
@@ -3123,22 +3404,22 @@ describe('LSP', function()
vim.lsp.commands['executed_preferred'] = function()
end
end
- vim.lsp.commands['quickfix_command'] = function(cmd)
- vim.lsp.commands['executed_quickfix'] = function()
+ vim.lsp.commands['type_annotate_command'] = function(cmd)
+ vim.lsp.commands['executed_type_annotate'] = function()
end
end
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID)
vim.lsp.buf.code_action({ filter = function(a) return a.isPreferred end, apply = true, })
vim.lsp.buf.code_action({
- -- expect to be returned actions 'quickfix' and 'quickfix.foo'
- context = { only = {'quickfix'}, },
+ -- expect to be returned actions 'type-annotate' and 'type-annotate.foo'
+ context = { only = { 'type-annotate' }, },
apply = true,
filter = function(a)
- if a.kind == 'quickfix.foo' then
- vim.lsp.commands['filtered_quickfix_foo'] = function() end
+ if a.kind == 'type-annotate.foo' then
+ vim.lsp.commands['filtered_type_annotate_foo'] = function() end
return false
- elseif a.kind == 'quickfix' then
+ elseif a.kind == 'type-annotate' then
return true
else
assert(nil, 'unreachable')
@@ -3148,13 +3429,57 @@ describe('LSP', function()
]])
elseif ctx.method == 'shutdown' then
eq('function', exec_lua[[return type(vim.lsp.commands['executed_preferred'])]])
- eq('function', exec_lua[[return type(vim.lsp.commands['filtered_quickfix_foo'])]])
- eq('function', exec_lua[[return type(vim.lsp.commands['executed_quickfix'])]])
+ eq('function', exec_lua[[return type(vim.lsp.commands['filtered_type_annotate_foo'])]])
+ eq('function', exec_lua[[return type(vim.lsp.commands['executed_type_annotate'])]])
client.stop()
end
end
}
end)
+ it("Fallback to command execution on resolve error", function()
+ clear()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server({
+ capabilities = {
+ executeCommandProvider = {
+ commands = {"command:1"},
+ },
+ codeActionProvider = {
+ resolveProvider = true
+ }
+ },
+ handlers = {
+ ["textDocument/codeAction"] = function()
+ return {
+ {
+ title = "Code Action 1",
+ command = {
+ title = "Command 1",
+ command = "command:1",
+ }
+ }
+ }
+ end,
+ ["codeAction/resolve"] = function()
+ return nil, "resolve failed"
+ end,
+ }
+ })
+
+ local client_id = vim.lsp.start({
+ name = "dummy",
+ cmd = server.cmd,
+ })
+
+ vim.lsp.buf.code_action({ apply = true })
+ vim.lsp.stop_client(client_id)
+ return server.messages
+ ]])
+ eq("codeAction/resolve", result[4].method)
+ eq("workspace/executeCommand", result[5].method)
+ eq("command:1", result[5].params.command)
+ end)
end)
describe('vim.lsp.commands', function()
it('Accepts only string keys', function()
@@ -3421,6 +3746,7 @@ describe('LSP', function()
vim.cmd.normal('v')
vim.api.nvim_win_set_cursor(0, { 2, 3 })
vim.lsp.buf.format({ bufnr = bufnr, false })
+ vim.lsp.stop_client(client_id)
return server.messages
]])
eq("textDocument/rangeFormatting", result[3].method)
@@ -3430,6 +3756,52 @@ describe('LSP', function()
}
eq(expected_range, result[3].params.range)
end)
+ it('format formats range in visual line mode', function()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server({ capabilities = {
+ documentFormattingProvider = true,
+ documentRangeFormattingProvider = true,
+ }})
+ local bufnr = vim.api.nvim_get_current_buf()
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ vim.api.nvim_win_set_buf(0, bufnr)
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {'foo', 'bar baz'})
+ vim.api.nvim_win_set_cursor(0, { 1, 2 })
+ vim.cmd.normal('V')
+ vim.api.nvim_win_set_cursor(0, { 2, 1 })
+ vim.lsp.buf.format({ bufnr = bufnr, false })
+
+ -- Format again with visual lines going from bottom to top
+ -- Must result in same formatting
+ vim.cmd.normal("<ESC>")
+ vim.api.nvim_win_set_cursor(0, { 2, 1 })
+ vim.cmd.normal('V')
+ vim.api.nvim_win_set_cursor(0, { 1, 2 })
+ vim.lsp.buf.format({ bufnr = bufnr, false })
+
+ vim.lsp.stop_client(client_id)
+ return server.messages
+ ]])
+ local expected_methods = {
+ "initialize",
+ "initialized",
+ "textDocument/rangeFormatting",
+ "$/cancelRequest",
+ "textDocument/rangeFormatting",
+ "$/cancelRequest",
+ "shutdown",
+ "exit",
+ }
+ eq(expected_methods, vim.tbl_map(function(x) return x.method end, result))
+ -- uses first column of start line and last column of end line
+ local expected_range = {
+ start = { line = 0, character = 0 },
+ ['end'] = { line = 1, character = 7 },
+ }
+ eq(expected_range, result[3].params.range)
+ eq(expected_range, result[5].params.range)
+ end)
it('Aborts with notify if no clients support requested method', function()
exec_lua(create_server_definition)
exec_lua([[
@@ -3466,7 +3838,7 @@ describe('LSP', function()
describe('cmd', function()
it('can connect to lsp server via rpc.connect', function()
local result = exec_lua [[
- local uv = vim.loop
+ local uv = vim.uv
local server = uv.new_tcp()
local init = nil
server:bind('127.0.0.1', 0)
@@ -3494,7 +3866,7 @@ describe('LSP', function()
describe('handlers', function()
it('handler can return false as response', function()
local result = exec_lua [[
- local uv = vim.loop
+ local uv = vim.uv
local server = uv.new_tcp()
local messages = {}
local responses = {}
@@ -3561,4 +3933,626 @@ describe('LSP', function()
eq(expected, result)
end)
end)
+
+ describe('#dynamic vim.lsp._dynamic', function()
+ it('supports dynamic registration', function()
+ local root_dir = helpers.tmpname()
+ os.remove(root_dir)
+ mkdir(root_dir)
+ local tmpfile = root_dir .. '/dynamic.foo'
+ local file = io.open(tmpfile, 'w')
+ file:close()
+
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local root_dir, tmpfile = ...
+
+ local server = _create_server()
+ local client_id = vim.lsp.start({
+ name = 'dynamic-test',
+ cmd = server.cmd,
+ root_dir = root_dir,
+ capabilities = {
+ textDocument = {
+ formatting = {
+ dynamicRegistration = true,
+ },
+ rangeFormatting = {
+ dynamicRegistration = true,
+ },
+ },
+ },
+ })
+
+ local expected_messages = 2 -- initialize, initialized
+
+ 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',
+ },
+ },
+ }, { 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 = vim.lsp.get_client_by_id(client_id)
+ result[#result + 1] = {method = method, fname = fname, supported = client.supports_method(method, {bufnr = bufnr})}
+ end
+
+
+ check("textDocument/formatting")
+ check("textDocument/formatting", tmpfile)
+ check("textDocument/rangeFormatting")
+ check("textDocument/rangeFormatting", tmpfile)
+ check("textDocument/completion")
+
+ return result
+ ]], root_dir, tmpfile)
+
+ eq(5, #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])
+ end)
+ end)
+
+ describe('vim.lsp._watchfiles', function()
+ it('sends notifications when files change', function()
+ skip(is_os('bsd'), "bsd only reports rename on folders if file inside change")
+ local root_dir = helpers.tmpname()
+ os.remove(root_dir)
+ mkdir(root_dir)
+
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local root_dir = ...
+
+ local server = _create_server()
+ local client_id = 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 watchfunc = require('vim.lsp._watchfiles')._watchfunc
+ local msg_wait_timeout = watchfunc == vim._watch.poll and 2500 or 200
+ local function wait_for_messages()
+ 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_messages()
+
+ 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 == vim._watch.poll then
+ vim.wait(100)
+ end
+
+ local path = root_dir .. '/watch'
+ local file = io.open(path, 'w')
+ file:close()
+
+ expected_messages = expected_messages + 1
+ wait_for_messages()
+
+ os.remove(path)
+
+ expected_messages = expected_messages + 1
+ wait_for_messages()
+
+ return server.messages
+ ]], root_dir)
+
+ local function watched_uri(fname)
+ return exec_lua([[
+ local root_dir, fname = ...
+ return vim.uri_from_fname(root_dir .. '/' .. fname)
+ ]], root_dir, fname)
+ end
+
+ eq(4, #result)
+ eq('workspace/didChangeWatchedFiles', result[3].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('watch'),
+ },
+ },
+ }, result[3].params)
+ eq('workspace/didChangeWatchedFiles', result[4].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
+ uri = watched_uri('watch'),
+ },
+ },
+ }, result[4].params)
+ end)
+
+ it('correctly registers and unregisters', function()
+ local root_dir = '/some_dir'
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local root_dir = ...
+
+ local server = _create_server()
+ local client_id = 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
+ 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
+ ]], root_dir)
+
+ local function watched_uri(fname)
+ return exec_lua([[
+ local root_dir, fname = ...
+ return vim.uri_from_fname(root_dir .. '/' .. fname)
+ ]], root_dir, fname)
+ end
+
+ eq(4, #result)
+ eq('workspace/didChangeWatchedFiles', result[3].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('file.watch0'),
+ },
+ },
+ }, result[3].params)
+ eq('workspace/didChangeWatchedFiles', result[4].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.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([[
+ local root_dir = ...
+
+ local server = _create_server()
+ local client_id = 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 = {}
+ 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
+ ]], root_dir)
+
+ local function watched_uri(fname)
+ return exec_lua([[
+ local fname = ...
+ return vim.uri_from_fname('/dir/' .. fname)
+ ]], fname)
+ end
+
+ eq(3, #result)
+ eq('workspace/didChangeWatchedFiles', result[3].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('watch1'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
+ uri = watched_uri('watch2'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('watch3'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
+ uri = watched_uri('watch3'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
+ uri = watched_uri('watch4'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('watch5'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
+ uri = watched_uri('watch5'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
+ uri = watched_uri('watch6'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
+ uri = watched_uri('watch6'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('watch7'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
+ uri = watched_uri('watch7'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.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([[
+ local root_dir = ...
+
+ local server = _create_server()
+ local client_id = 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
+ 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
+ ]], root_dir)
+
+ local function watched_uri(fname)
+ return exec_lua([[
+ return vim.uri_from_fname(...)
+ ]], fname)
+ end
+
+ eq(3, #result)
+ eq('workspace/didChangeWatchedFiles', result[3].method)
+ eq({
+ changes = {
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('file1'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
+ uri = watched_uri('file1'),
+ },
+ {
+ type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
+ uri = watched_uri('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([[
+ server = _create_server()
+ require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
+ -- Since the registration is ignored, this should not execute and `watching` should stay false
+ watching = true
+ return function() end
+ end
+ ]])
+
+ local function check_registered(capabilities)
+ return exec_lua([[
+ watching = false
+ local client_id = vim.lsp.start({
+ name = 'watchfiles-test',
+ cmd = server.cmd,
+ root_dir = 'some_dir',
+ 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.stop_client(client_id, true)
+ return watching
+ ]], capabilities)
+ end
+
+ eq(true, check_registered(nil)) -- start{_client}() defaults to make_client_capabilities().
+ eq(false, check_registered(vim.empty_dict()))
+ eq(false, check_registered({
+ workspace = {
+ ignoreMe = true,
+ },
+ }))
+ eq(false, check_registered({
+ workspace = {
+ didChangeWatchedFiles = {
+ dynamicRegistration = false,
+ },
+ },
+ }))
+ eq(true, check_registered({
+ workspace = {
+ didChangeWatchedFiles = {
+ dynamicRegistration = true,
+ },
+ },
+ }))
+ end)
+ end)
end)
+