neovim/test/functional/fixtures/fake-lsp-server.lua
Gregory Anders 29c72cdf4a
fix(lsp): retrigger diagnostics request on server cancellation (#31345)
Co-authored-by: Jesse <github@jessebakker.com>
2024-11-25 11:48:11 -06:00

1061 lines
26 KiB
Lua

local protocol = require 'vim.lsp.protocol'
-- Logs to $NVIM_LOG_FILE.
--
-- TODO(justinmk): remove after https://github.com/neovim/neovim/pull/7062
local function log(loglevel, area, msg)
vim.fn.writefile({ string.format('%s %s: %s', loglevel, area, msg) }, vim.env.NVIM_LOG_FILE, 'a')
end
local function message_parts(sep, ...)
local parts = {}
for i = 1, select('#', ...) do
local arg = select(i, ...)
if arg ~= nil then
table.insert(parts, arg)
end
end
return table.concat(parts, sep)
end
-- Assert utility methods
local function assert_eq(a, b, ...)
if not vim.deep_equal(a, b) then
error(
message_parts(
': ',
...,
'assert_eq failed',
string.format(
'left == %q, right == %q',
table.concat(vim.split(vim.inspect(a), '\n'), ''),
table.concat(vim.split(vim.inspect(b), '\n'), '')
)
)
)
end
end
local function format_message_with_content_length(encoded_message)
return table.concat {
'Content-Length: ',
tostring(#encoded_message),
'\r\n\r\n',
encoded_message,
}
end
local function read_message()
local line = io.read('*l')
local length = line:lower():match('content%-length:%s*(%d+)')
return vim.json.decode(io.read(2 + length):sub(2))
end
local function send(payload)
io.stdout:write(format_message_with_content_length(vim.json.encode(payload)))
end
local function respond(id, err, result)
assert(type(id) == 'number', 'id must be a number')
send { jsonrpc = '2.0', id = id, error = err, result = result }
end
local function notify(method, params)
assert(type(method) == 'string', 'method must be a string')
send { method = method, params = params or {} }
end
local function expect_notification(method, params, ...)
local message = read_message()
assert_eq(method, message.method, ..., 'expect_notification', 'method')
if params then
assert_eq(params, message.params, ..., 'expect_notification', method, 'params')
assert_eq(
{ jsonrpc = '2.0', method = method, params = params },
message,
...,
'expect_notification',
'message'
)
end
end
local function expect_request(method, handler, ...)
local req = read_message()
assert_eq(method, req.method, ..., 'expect_request', 'method')
local err, result = handler(req.params)
respond(req.id, err, result)
end
io.stderr:setvbuf('no')
local function skeleton(config)
local on_init = assert(config.on_init)
local body = assert(config.body)
expect_request('initialize', function(params)
return nil, on_init(params)
end)
expect_notification('initialized', {})
body()
expect_request('shutdown', function()
return nil, {}
end)
expect_notification('exit', nil)
end
-- The actual tests.
local tests = {}
function tests.basic_init()
skeleton {
on_init = function(params)
assert_eq(params.workDoneToken, '1')
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.None,
},
}
end,
body = function()
notify('test')
end,
}
end
function tests.basic_init_did_change_configuration()
skeleton({
on_init = function(_)
return {
capabilities = {},
}
end,
body = function()
expect_notification('workspace/didChangeConfiguration', { settings = { dummy = 1 } })
end,
})
end
function tests.check_workspace_configuration()
skeleton {
on_init = function(_params)
return { capabilities = {} }
end,
body = function()
notify('start')
notify('workspace/configuration', {
items = {
{ section = 'testSetting1' },
{ section = 'testSetting2' },
{ section = 'test.Setting3' },
{ section = 'test.Setting4' },
},
})
expect_notification('workspace/configuration', { true, false, 'nested', vim.NIL })
notify('shutdown')
end,
}
end
function tests.prepare_rename_nil()
skeleton {
on_init = function()
return {
capabilities = {
renameProvider = {
prepareProvider = true,
},
},
}
end,
body = function()
notify('start')
expect_request('textDocument/prepareRename', function()
return nil, nil
end)
notify('shutdown')
end,
}
end
function tests.prepare_rename_placeholder()
skeleton {
on_init = function()
return {
capabilities = {
renameProvider = {
prepareProvider = true,
},
},
}
end,
body = function()
notify('start')
expect_request('textDocument/prepareRename', function()
return nil, { placeholder = 'placeholder' }
end)
expect_request('textDocument/rename', function(params)
assert_eq(params.newName, 'renameto')
return nil, nil
end)
notify('shutdown')
end,
}
end
function tests.prepare_rename_range()
skeleton {
on_init = function()
return {
capabilities = {
renameProvider = {
prepareProvider = true,
},
},
}
end,
body = function()
notify('start')
expect_request('textDocument/prepareRename', function()
return nil,
{
start = { line = 1, character = 8 },
['end'] = { line = 1, character = 12 },
}
end)
expect_request('textDocument/rename', function(params)
assert_eq(params.newName, 'renameto')
return nil, nil
end)
notify('shutdown')
end,
}
end
function tests.prepare_rename_error()
skeleton {
on_init = function()
return {
capabilities = {
renameProvider = {
prepareProvider = true,
},
},
}
end,
body = function()
notify('start')
expect_request('textDocument/prepareRename', function()
return {}, nil
end)
notify('shutdown')
end,
}
end
function tests.basic_check_capabilities()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
codeLensProvider = false,
},
}
end,
body = function() end,
}
end
function tests.text_document_save_did_open()
skeleton {
on_init = function()
return {
capabilities = {
textDocumentSync = {
save = true,
},
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didClose')
expect_notification('textDocument/didOpen')
expect_notification('textDocument/didSave')
notify('shutdown')
end,
}
end
function tests.text_document_sync_save_bool()
skeleton {
on_init = function()
return {
capabilities = {
textDocumentSync = {
save = true,
},
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didSave', { textDocument = { uri = 'file://' } })
notify('shutdown')
end,
}
end
function tests.text_document_sync_save_includeText()
skeleton {
on_init = function()
return {
capabilities = {
textDocumentSync = {
save = {
includeText = true,
},
},
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didSave', {
textDocument = {
uri = 'file://',
},
text = 'help me\n',
})
notify('shutdown')
end,
}
end
function tests.capabilities_for_client_supports_method()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
completionProvider = true,
hoverProvider = true,
renameProvider = false,
definitionProvider = false,
referencesProvider = false,
codeLensProvider = { resolveProvider = true },
},
}
end,
body = function() end,
}
end
function tests.check_forward_request_cancelled()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
expect_request('error_code_test', function()
return { code = -32800 }, nil, { method = 'error_code_test', client_id = 1 }
end)
notify('finish')
end,
}
end
function tests.check_forward_content_modified()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
expect_request('error_code_test', function()
return { code = -32801 }, nil, { method = 'error_code_test', client_id = 1 }
end)
expect_notification('finish')
notify('finish')
end,
}
end
function tests.check_forward_server_cancelled()
skeleton {
on_init = function()
return { capabilities = {} }
end,
body = function()
expect_request('error_code_test', function()
return { code = -32802 }, nil, { method = 'error_code_test', client_id = 1 }
end)
expect_notification('finish')
notify('finish')
end,
}
end
function tests.check_pending_request_tracked()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('release')
respond(msg.id, nil, {})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.check_cancel_request_tracked()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('$/cancelRequest', { id = msg.id })
expect_notification('release')
respond(msg.id, { code = -32800 }, nil)
notify('finish')
end,
}
end
function tests.check_tracked_requests_cleared()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('$/cancelRequest', { id = msg.id })
expect_notification('release')
respond(msg.id, nil, {})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_finish()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n') .. '\n',
uri = 'file://',
version = 0,
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n') .. '\n',
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{ text = table.concat({ 'testing', 'boop' }, '\n') .. '\n' },
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change_noeol()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n'),
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{ text = table.concat({ 'testing', 'boop' }, '\n') },
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change_multi()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n') .. '\n',
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{ text = table.concat({ 'testing', '321' }, '\n') .. '\n' },
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 4,
},
contentChanges = {
{ text = table.concat({ 'testing', 'boop' }, '\n') .. '\n' },
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change_multi_and_close()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Full,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n') .. '\n',
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{ text = table.concat({ 'testing', '321' }, '\n') .. '\n' },
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 4,
},
contentChanges = {
{ text = table.concat({ 'testing', 'boop' }, '\n') .. '\n' },
},
})
expect_notification('textDocument/didClose', {
textDocument = {
uri = 'file://',
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change_incremental()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = {
openClose = true,
change = protocol.TextDocumentSyncKind.Incremental,
willSave = true,
willSaveWaitUntil = true,
save = {
includeText = true,
},
},
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n') .. '\n',
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{
range = {
start = { line = 1, character = 3 },
['end'] = { line = 1, character = 3 },
},
rangeLength = 0,
text = 'boop',
},
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.basic_check_buffer_open_and_change_incremental_editing()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
textDocumentSync = protocol.TextDocumentSyncKind.Incremental,
},
}
end,
body = function()
notify('start')
expect_notification('textDocument/didOpen', {
textDocument = {
languageId = '',
text = table.concat({ 'testing', '123' }, '\n'),
uri = 'file://',
version = 0,
},
})
expect_notification('textDocument/didChange', {
textDocument = {
uri = 'file://',
version = 3,
},
contentChanges = {
{
range = {
start = { line = 0, character = 0 },
['end'] = { line = 1, character = 0 },
},
rangeLength = 4,
text = 'testing\n\n',
},
},
})
expect_notification('finish')
notify('finish')
end,
}
end
function tests.invalid_header()
io.stdout:write('Content-length: \r\n')
end
function tests.decode_nil()
skeleton {
on_init = function(_)
return { capabilities = {} }
end,
body = function()
notify('start')
notify('workspace/executeCommand', {
arguments = { 'EXTRACT_METHOD', { metadata = { field = vim.NIL } }, 3, 0, 6123, vim.NIL },
command = 'refactor.perform',
title = 'EXTRACT_METHOD',
})
notify('finish')
end,
}
end
function tests.code_action_with_resolve()
skeleton {
on_init = function()
return {
capabilities = {
codeActionProvider = {
resolveProvider = true,
},
},
}
end,
body = function()
notify('start')
local cmd = {
title = 'Command 1',
command = 'dummy1',
}
expect_request('textDocument/codeAction', function()
return nil, { cmd }
end)
expect_request('codeAction/resolve', function()
return nil, cmd
end)
notify('shutdown')
end,
}
end
function tests.code_action_server_side_command()
skeleton({
on_init = function()
return {
capabilities = {
codeActionProvider = {
resolveProvider = false,
},
executeCommandProvider = {
commands = { 'dummy1' },
},
},
}
end,
body = function()
notify('start')
local cmd = {
title = 'Command 1',
command = 'dummy1',
}
expect_request('textDocument/codeAction', function()
return nil, { cmd }
end)
expect_request('workspace/executeCommand', function()
return nil, cmd
end)
notify('shutdown')
end,
})
end
function tests.code_action_filter()
skeleton {
on_init = function()
return {
capabilities = {
codeActionProvider = {
resolveProvider = false,
},
},
}
end,
body = function()
notify('start')
local action = {
title = 'Action 1',
command = 'command',
}
local preferred_action = {
title = 'Action 2',
isPreferred = true,
command = 'preferred_command',
}
local type_annotate_action = {
title = 'Action 3',
kind = 'type-annotate',
command = 'type_annotate_command',
}
local type_annotate_foo_action = {
title = 'Action 4',
kind = 'type-annotate.foo',
command = 'type_annotate_foo_command',
}
expect_request('textDocument/codeAction', function()
return nil, { action, preferred_action, type_annotate_action, type_annotate_foo_action }
end)
expect_request('textDocument/codeAction', function()
return nil, { action, preferred_action, type_annotate_action, type_annotate_foo_action }
end)
notify('shutdown')
end,
}
end
function tests.clientside_commands()
skeleton {
on_init = function()
return {
capabilities = {},
}
end,
body = function()
notify('start')
notify('shutdown')
end,
}
end
function tests.codelens_refresh_lock()
skeleton {
on_init = function()
return {
capabilities = {
codeLensProvider = { resolveProvider = true },
},
}
end,
body = function()
notify('start')
expect_request('textDocument/codeLens', function()
return { code = -32002, message = 'ServerNotInitialized' }, nil
end)
expect_request('textDocument/codeLens', function()
local lenses = {
{
range = {
start = { line = 0, character = 0 },
['end'] = { line = 0, character = 3 },
},
command = { title = 'Lens1', command = 'Dummy' },
},
}
return nil, lenses
end)
expect_request('textDocument/codeLens', function()
local lenses = {
{
range = {
start = { line = 0, character = 0 },
['end'] = { line = 0, character = 3 },
},
command = { title = 'Lens2', command = 'Dummy' },
},
}
return nil, lenses
end)
notify('shutdown')
end,
}
end
function tests.basic_formatting()
skeleton {
on_init = function()
return {
capabilities = {
documentFormattingProvider = true,
},
}
end,
body = function()
notify('start')
expect_request('textDocument/formatting', function()
return nil, {}
end)
notify('shutdown')
end,
}
end
function tests.range_formatting()
skeleton {
on_init = function()
return {
capabilities = {
documentFormattingProvider = true,
documentRangeFormattingProvider = true,
},
}
end,
body = function()
notify('start')
expect_request('textDocument/rangeFormatting', function()
return nil, {}
end)
notify('shutdown')
end,
}
end
function tests.ranges_formatting()
skeleton {
on_init = function()
return {
capabilities = {
documentFormattingProvider = true,
documentRangeFormattingProvider = {
rangesSupport = true,
},
},
}
end,
body = function()
notify('start')
expect_request('textDocument/rangesFormatting', function()
return nil, {}
end)
notify('shutdown')
end,
}
end
function tests.set_defaults_all_capabilities()
skeleton {
on_init = function(_)
return {
capabilities = {
definitionProvider = true,
completionProvider = true,
documentRangeFormattingProvider = true,
hoverProvider = true,
},
}
end,
body = function()
notify('test')
end,
}
end
function tests.inlay_hint()
skeleton {
on_init = function(params)
local expected_capabilities = protocol.make_client_capabilities()
assert_eq(params.capabilities, expected_capabilities)
return {
capabilities = {
inlayHintProvider = true,
},
}
end,
body = function()
notify('start')
expect_request('textDocument/inlayHint', function()
return nil, {}
end)
expect_notification('finish')
notify('finish')
end,
}
end
-- Tests will be indexed by test_name
local test_name = arg[1]
local timeout = arg[2]
assert(type(test_name) == 'string', 'test_name must be specified as first arg.')
local kill_timer = assert(vim.uv.new_timer())
kill_timer:start(timeout or 1e3, 0, function()
kill_timer:stop()
kill_timer:close()
log('ERROR', 'LSP', 'TIMEOUT')
io.stderr:write('TIMEOUT')
os.exit(100)
end)
local status, err = pcall(assert(tests[test_name], 'Test not found'))
kill_timer:stop()
kill_timer:close()
if not status then
log('ERROR', 'LSP', tostring(err))
io.stderr:write(err)
vim.cmd [[101cquit]]
end