mirror of
https://github.com/neovim/neovim.git
synced 2025-12-15 19:05:40 +00:00
Problem: Some servers write log to stdout and there's no way to avoid it. See https://github.com/neovim/neovim/pull/35743#pullrequestreview-3379705828 Solution: We can extract `content-length` field byte by byte and skip invalid lines via a simple state machine (name/colon/value/invalid), with minimal performance impact. I chose byte parsing here instead of pattern. Although it's a bit more complex, it provides more stable performance and allows for more accurate error info when needed. Here is a bench result and script: parse header1 by pattern: 59.52377ms 45 parse header1 by byte: 7.531128ms 45 parse header2 by pattern: 26.06936ms 45 parse header2 by byte: 5.235724ms 45 parse header3 by pattern: 9.348495ms 45 parse header3 by byte: 3.452389ms 45 parse header4 by pattern: 9.73156ms 45 parse header4 by byte: 3.638386ms 45 Script: ```lua local strbuffer = require('string.buffer') --- @param header string local function get_content_length(header) for line in header:gmatch('(.-)\r?\n') do if line == '' then break end local key, value = line:match('^%s*(%S+)%s*:%s*(%d+)%s*$') if key and key:lower() == 'content-length' then return assert(tonumber(value)) end end error('Content-Length not found in header: ' .. header) end --- @param header string local function get_content_length_by_byte(header) local state = 'name' local i, len = 1, #header local j, name = 1, 'content-length' local buf = strbuffer.new() local digit = true while i <= len do local c = header:byte(i) if state == 'name' then if c >= 65 and c <= 90 then -- lower case c = c + 32 end if (c == 32 or c == 9) and j == 1 then -- skip OWS for compatibility only elseif c == name:byte(j) then j = j + 1 elseif c == 58 and j == 15 then state = 'colon' else state = 'invalid' end elseif state == 'colon' then if c ~= 32 and c ~= 9 then -- skip OWS normally state = 'value' i = i - 1 end elseif state == 'value' then if c == 13 and header:byte(i + 1) == 10 then -- must end with \r\n local value = buf:get() return assert(digit and tonumber(value), 'value of Content-Length is not number: ' .. value) else buf:put(string.char(c)) end if c < 48 and c ~= 32 and c ~= 9 or c > 57 then digit = false end elseif state == 'invalid' then if c == 10 then -- reset for next line state, j = 'name', 1 end end i = i + 1 end error('Content-Length not found in header: ' .. header) end --- @param fn fun(header: string): number local function bench(label, header, fn, count) local start = vim.uv.hrtime() local value --- @type number for _ = 1, count do value = fn(header) end local elapsed = (vim.uv.hrtime() - start) / 1e6 print(label .. ':', elapsed .. 'ms', value) end -- header starting with log lines local header1 = 'WARN: no common words file defined for Khmer - this language might not be correctly auto-detected\nWARN: no common words file defined for Japanese - this language might not be correctly auto-detected\nContent-Length: 45 \r\n\r\n' -- header starting with content-type local header2 = 'Content-Type: application/json-rpc; charset=utf-8\r\nContent-Length: 45 \r\n' -- regular header local header3 = ' Content-Length: 45\r\n' -- regular header ending with content-type local header4 = ' Content-Length: 45 \r\nContent-Type: application/json-rpc; charset=utf-8\r\n' local count = 10000 collectgarbage('collect') bench('parse header1 by pattern', header1, get_content_length, count) collectgarbage('collect') bench('parse header1 by byte', header1, get_content_length_by_byte, count) collectgarbage('collect') bench('parse header2 by pattern', header2, get_content_length, count) collectgarbage('collect') bench('parse header2 by byte', header2, get_content_length_by_byte, count) collectgarbage('collect') bench('parse header3 by pattern', header3, get_content_length, count) collectgarbage('collect') bench('parse header3 by byte', header3, get_content_length_by_byte, count) collectgarbage('collect') bench('parse header4 by pattern', header4, get_content_length, count) collectgarbage('collect') bench('parse header4 by byte', header4, get_content_length_by_byte, count) ``` Also, I removed an outdated testaccd392f4d/test/functional/plugin/lsp_spec.lua (L1950)and tweaked the boilerplate in two other tests for reusability while keeping the final assertions the same.accd392f4d/test/functional/plugin/lsp_spec.lua (L5704)accd392f4d/test/functional/plugin/lsp_spec.lua (L5721)
1061 lines
26 KiB
Lua
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
|
|
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
|
|
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
|
|
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.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 = 'Action 1' }
|
|
expect_request('textDocument/codeAction', function()
|
|
return nil, { cmd }
|
|
end)
|
|
expect_request('codeAction/resolve', function()
|
|
return nil,
|
|
{
|
|
title = 'Action 1',
|
|
command = {
|
|
title = 'Command 1',
|
|
command = 'dummy1',
|
|
},
|
|
}
|
|
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
|