Files
neovim/test/functional/plugin/lsp/buf_spec.lua
Ashley Hauck 7e006b06c4 fix(lsp): callHierarchy/outgoingCalls ranges are relative to caller, not callee #39336
Problem:
The fromRanges field of the result of callHierarchy/outgoingCalls is
documented as being relative to the caller. Using
vim.lsp.buf.outgoing_calls() opened the qflist with an entry with the
callee's filename, but the caller's line number.

Solution:
Open the qflist with the callers file (the bufnr from the request),
rather than the callees (the uri from the resulting CallHierarchyItem)
2026-04-23 08:20:58 -04:00

1856 lines
56 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local t_lsp = require('test.functional.plugin.lsp.testutil')
local command = n.command
local eq = t.eq
local exec_lua = n.exec_lua
local matches = t.matches
local pcall_err = t.pcall_err
local retry = t.retry
local stop = n.stop
local NIL = vim.NIL
local api = n.api
local skip = t.skip
local is_os = t.is_os
local clear_notrace = t_lsp.clear_notrace
local create_server_definition = t_lsp.create_server_definition
local fake_lsp_logfile = t_lsp.fake_lsp_logfile
local test_rpc_server = t_lsp.test_rpc_server
local test_root = vim.uv.cwd()
-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837
if skip(is_os('win')) then
return
end
describe('vim.lsp.buf', function()
local function exec_capture(cmd)
return exec_lua(function(cmd0)
return vim.api.nvim_exec2(cmd0, { output = true }).output
end, cmd)
end
before_each(function()
clear_notrace()
command('cd ' .. test_root)
end)
after_each(function()
stop()
exec_lua(function()
vim.iter(vim.lsp.get_clients({ _uninitialized = true })):each(function(client)
client:stop(true)
end)
end)
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
end)
teardown(function()
os.remove(fake_lsp_logfile)
end)
describe('vim.lsp.buf.outgoing_calls', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua(function()
require 'vim.lsp.handlers'['callHierarchy/outgoingCalls'](nil, nil, {}, nil)
return #vim.fn.getqflist()
end)
eq(0, qflist_count)
end)
it('opens the quickfix list with the right caller', function()
local qflist = exec_lua(function()
local rust_analyzer_response = {
{
fromRanges = {
{
['end'] = {
character = 7,
line = 3,
},
start = {
character = 4,
line = 3,
},
},
},
to = {
detail = 'fn foo()',
kind = 12,
name = 'foo',
range = {
['end'] = {
character = 11,
line = 0,
},
start = {
character = 0,
line = 0,
},
},
selectionRange = {
['end'] = {
character = 6,
line = 0,
},
start = {
character = 3,
line = 0,
},
},
uri = 'file:///src/main.rs',
},
},
}
local handler = require 'vim.lsp.handlers'['callHierarchy/outgoingCalls']
local bufnr = vim.api.nvim_get_current_buf()
handler(nil, rust_analyzer_response, { bufnr = bufnr })
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 1,
col = 5,
end_col = 0,
lnum = 4,
end_lnum = 0,
module = '',
nr = 0,
pattern = '',
text = 'foo',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
end)
describe('vim.lsp.buf.incoming_calls', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua(function()
require 'vim.lsp.handlers'['callHierarchy/incomingCalls'](nil, nil, {})
return #vim.fn.getqflist()
end)
eq(0, qflist_count)
end)
it('opens the quickfix list with the right callee', function()
local qflist = exec_lua(function()
local rust_analyzer_response = {
{
from = {
detail = 'fn main()',
kind = 12,
name = 'main',
range = {
['end'] = {
character = 1,
line = 4,
},
start = {
character = 0,
line = 2,
},
},
selectionRange = {
['end'] = {
character = 7,
line = 2,
},
start = {
character = 3,
line = 2,
},
},
uri = 'file:///src/main.rs',
},
fromRanges = {
{
['end'] = {
character = 7,
line = 3,
},
start = {
character = 4,
line = 3,
},
},
},
},
}
local handler = require 'vim.lsp.handlers'['callHierarchy/incomingCalls']
handler(nil, rust_analyzer_response, {})
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 2,
col = 5,
end_col = 0,
lnum = 4,
end_lnum = 0,
module = '',
nr = 0,
pattern = '',
text = 'main',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
end)
describe('vim.lsp.buf.typehierarchy subtypes', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua(function()
require 'vim.lsp.handlers'['typeHierarchy/subtypes'](nil, nil, {})
return #vim.fn.getqflist()
end)
eq(0, qflist_count)
end)
it('opens the quickfix list with the right subtypes', function()
exec_lua(create_server_definition)
local qflist = exec_lua(function()
local clangd_response = {
{
data = {
parents = {
{
parents = {
{
parents = {
{
parents = {},
symbolID = '62B3D268A01B9978',
},
},
symbolID = 'DC9B0AD433B43BEC',
},
},
symbolID = '06B5F6A19BA9F6A8',
},
},
symbolID = 'EDC336589C09ABB2',
},
kind = 5,
name = 'D2',
range = {
['end'] = {
character = 8,
line = 3,
},
start = {
character = 6,
line = 3,
},
},
selectionRange = {
['end'] = {
character = 8,
line = 3,
},
start = {
character = 6,
line = 3,
},
},
uri = 'file:///home/jiangyinzuo/hello.cpp',
},
{
data = {
parents = {
{
parents = {
{
parents = {
{
parents = {},
symbolID = '62B3D268A01B9978',
},
},
symbolID = 'DC9B0AD433B43BEC',
},
},
symbolID = '06B5F6A19BA9F6A8',
},
},
symbolID = 'AFFCAED15557EF08',
},
kind = 5,
name = 'D1',
range = {
['end'] = {
character = 8,
line = 2,
},
start = {
character = 6,
line = 2,
},
},
selectionRange = {
['end'] = {
character = 8,
line = 2,
},
start = {
character = 6,
line = 2,
},
},
uri = 'file:///home/jiangyinzuo/hello.cpp',
},
}
local server = _G._create_server({
capabilities = {
positionEncoding = 'utf-8',
},
})
local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes']
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
'class B : public A{};',
'class C : public B{};',
'class D1 : public C{};',
'class D2 : public C{};',
'class E : public D1, D2 {};',
})
handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr })
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 2,
col = 7,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'D2',
type = '',
valid = 1,
vcol = 0,
},
{
bufnr = 2,
col = 7,
end_col = 0,
end_lnum = 0,
lnum = 3,
module = '',
nr = 0,
pattern = '',
text = 'D1',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
it('opens the quickfix list with the right subtypes and details', function()
exec_lua(create_server_definition)
local qflist = exec_lua(function()
local jdtls_response = {
{
data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' },
detail = '',
kind = 5,
name = 'A',
range = {
['end'] = { character = 26, line = 3 },
start = { character = 1, line = 3 },
},
selectionRange = {
['end'] = { character = 8, line = 3 },
start = { character = 7, line = 3 },
},
tags = {},
uri = 'file:///home/jiangyinzuo/hello-java/Main.java',
},
{
data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' },
detail = 'mylist',
kind = 5,
name = 'MyList$Inner',
range = {
['end'] = { character = 37, line = 3 },
start = { character = 1, line = 3 },
},
selectionRange = {
['end'] = { character = 19, line = 3 },
start = { character = 14, line = 3 },
},
tags = {},
uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java',
},
}
local server = _G._create_server({
capabilities = {
positionEncoding = 'utf-8',
},
})
local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes']
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
'package mylist;',
'',
'public class MyList {',
' static class Inner extends MyList{}',
'~}',
})
handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr })
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 2,
col = 2,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'A',
type = '',
valid = 1,
vcol = 0,
},
{
bufnr = 3,
col = 2,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'MyList$Inner mylist',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
end)
describe('vim.lsp.buf.typehierarchy supertypes', function()
it('does nothing for an empty response', function()
local qflist_count = exec_lua(function()
require 'vim.lsp.handlers'['typeHierarchy/supertypes'](nil, nil, {})
return #vim.fn.getqflist()
end)
eq(0, qflist_count)
end)
it('opens the quickfix list with the right supertypes', function()
exec_lua(create_server_definition)
local qflist = exec_lua(function()
local clangd_response = {
{
data = {
parents = {
{
parents = {
{
parents = {
{
parents = {},
symbolID = '62B3D268A01B9978',
},
},
symbolID = 'DC9B0AD433B43BEC',
},
},
symbolID = '06B5F6A19BA9F6A8',
},
},
symbolID = 'EDC336589C09ABB2',
},
kind = 5,
name = 'D2',
range = {
['end'] = {
character = 8,
line = 3,
},
start = {
character = 6,
line = 3,
},
},
selectionRange = {
['end'] = {
character = 8,
line = 3,
},
start = {
character = 6,
line = 3,
},
},
uri = 'file:///home/jiangyinzuo/hello.cpp',
},
{
data = {
parents = {
{
parents = {
{
parents = {
{
parents = {},
symbolID = '62B3D268A01B9978',
},
},
symbolID = 'DC9B0AD433B43BEC',
},
},
symbolID = '06B5F6A19BA9F6A8',
},
},
symbolID = 'AFFCAED15557EF08',
},
kind = 5,
name = 'D1',
range = {
['end'] = {
character = 8,
line = 2,
},
start = {
character = 6,
line = 2,
},
},
selectionRange = {
['end'] = {
character = 8,
line = 2,
},
start = {
character = 6,
line = 2,
},
},
uri = 'file:///home/jiangyinzuo/hello.cpp',
},
}
local server = _G._create_server({
capabilities = {
positionEncoding = 'utf-8',
},
})
local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes']
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
'class B : public A{};',
'class C : public B{};',
'class D1 : public C{};',
'class D2 : public C{};',
'class E : public D1, D2 {};',
})
handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr })
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 2,
col = 7,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'D2',
type = '',
valid = 1,
vcol = 0,
},
{
bufnr = 2,
col = 7,
end_col = 0,
end_lnum = 0,
lnum = 3,
module = '',
nr = 0,
pattern = '',
text = 'D1',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
it('opens the quickfix list with the right supertypes and details', function()
exec_lua(create_server_definition)
local qflist = exec_lua(function()
local jdtls_response = {
{
data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' },
detail = '',
kind = 5,
name = 'A',
range = {
['end'] = { character = 26, line = 3 },
start = { character = 1, line = 3 },
},
selectionRange = {
['end'] = { character = 8, line = 3 },
start = { character = 7, line = 3 },
},
tags = {},
uri = 'file:///home/jiangyinzuo/hello-java/Main.java',
},
{
data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' },
detail = 'mylist',
kind = 5,
name = 'MyList$Inner',
range = {
['end'] = { character = 37, line = 3 },
start = { character = 1, line = 3 },
},
selectionRange = {
['end'] = { character = 19, line = 3 },
start = { character = 14, line = 3 },
},
tags = {},
uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java',
},
}
local server = _G._create_server({
capabilities = {
positionEncoding = 'utf-8',
},
})
local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes']
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
'package mylist;',
'',
'public class MyList {',
' static class Inner extends MyList{}',
'~}',
})
handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr })
return vim.fn.getqflist()
end)
local expected = {
{
bufnr = 2,
col = 2,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'A',
type = '',
valid = 1,
vcol = 0,
},
{
bufnr = 3,
col = 2,
end_col = 0,
end_lnum = 0,
lnum = 4,
module = '',
nr = 0,
pattern = '',
text = 'MyList$Inner mylist',
type = '',
valid = 1,
vcol = 0,
},
}
eq(expected, qflist)
end)
end)
describe('vim.lsp.buf.rename', function()
for _, test in ipairs({
{
it = 'does not attempt to rename on nil response',
name = 'prepare_rename_nil',
expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
},
},
{
it = 'handles prepareRename placeholder response',
name = 'prepare_rename_placeholder',
expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ {}, NIL, { method = 'textDocument/rename', client_id = 1, request_id = 3, bufnr = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
},
expected_text = 'placeholder', -- see fake lsp response
},
{
it = 'handles range response',
name = 'prepare_rename_range',
expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ {}, NIL, { method = 'textDocument/rename', client_id = 1, request_id = 3, bufnr = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
},
expected_text = 'line', -- see test case and fake lsp response
},
{
it = 'handles error',
name = 'prepare_rename_error',
expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
},
},
}) do
it(test.it, function()
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = test.name,
on_init = function(_client)
client = _client
eq(true, client.server_capabilities().renameProvider.prepareProvider)
end,
on_setup = function()
exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
vim.lsp._stubs = {}
--- @diagnostic disable-next-line:duplicate-set-field
vim.fn.input = function(opts, _)
vim.lsp._stubs.input_prompt = opts.prompt
vim.lsp._stubs.input_text = opts.default
return 'renameto' -- expect this value in fake lsp
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '', 'this is line two' })
vim.fn.cursor(2, 13) -- the space between "line" and "two"
end)
end,
on_exit = function(code, signal)
eq(0, code, 'exit code')
eq(0, signal, 'exit signal')
end,
on_handler = function(err, result, ctx)
-- 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
exec_lua(function()
vim.lsp.buf.rename()
end)
end
if ctx.method == 'shutdown' then
if test.expected_text then
eq(
'New Name: ',
exec_lua(function()
return vim.lsp._stubs.input_prompt
end)
)
eq(
test.expected_text,
exec_lua(function()
return vim.lsp._stubs.input_text
end)
)
end
client:stop()
end
end,
}
end)
end
end)
describe('vim.lsp.buf.code_action', function()
it('calls client side command if available', function()
local client --- @type vim.lsp.Client
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
test_rpc_server {
test_name = 'code_action_with_resolve',
on_init = function(client_)
client = client_
end,
on_setup = function() end,
on_exit = function(code, signal)
eq(0, code, 'exit code')
eq(0, signal, 'exit signal')
end,
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), { err, result, ctx })
if ctx.method == 'start' then
exec_lua(function()
vim.lsp.commands['dummy1'] = function(_)
vim.lsp.commands['dummy2'] = function() end
end
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
--- @diagnostic disable-next-line:duplicate-set-field
vim.fn.inputlist = function()
return 1
end
vim.lsp.buf.code_action()
end)
elseif ctx.method == 'shutdown' then
eq(
'function',
exec_lua(function()
return type(vim.lsp.commands['dummy2'])
end)
)
client:stop()
end
end,
}
end)
it('calls workspace/executeCommand if no client side command', function()
local client --- @type vim.lsp.Client
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{
NIL,
{ command = 'dummy1', title = 'Command 1' },
{ bufnr = 1, method = 'workspace/executeCommand', request_id = 3, client_id = 1 },
},
{ NIL, {}, { method = 'start', client_id = 1 } },
}
test_rpc_server({
test_name = 'code_action_server_side_command',
on_init = function(client_)
client = client_
end,
on_setup = function() end,
on_exit = function(code, signal)
eq(0, code, 'exit code', fake_lsp_logfile)
eq(0, signal, 'exit signal', fake_lsp_logfile)
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(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
vim.fn.inputlist = function()
return 1
end
vim.lsp.buf.code_action()
end)
elseif ctx.method == 'shutdown' then
client:stop()
end
end,
})
end)
it('filters and automatically applies action if requested', function()
local client --- @type vim.lsp.Client
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
test_rpc_server {
test_name = 'code_action_filter',
on_init = function(client_)
client = client_
end,
on_setup = function() end,
on_exit = function(code, signal)
eq(0, code, 'exit code')
eq(0, signal, 'exit signal')
end,
on_handler = function(err, result, ctx)
eq(table.remove(expected_handlers), { err, result, ctx })
if ctx.method == 'start' then
exec_lua(function()
vim.lsp.commands['preferred_command'] = function(_)
vim.lsp.commands['executed_preferred'] = function() end
end
vim.lsp.commands['type_annotate_command'] = function(_)
vim.lsp.commands['executed_type_annotate'] = function() end
end
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.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 'type-annotate' and 'type-annotate.foo'
context = { only = { 'type-annotate' } },
apply = true,
filter = function(a)
if a.kind == 'type-annotate.foo' then
vim.lsp.commands['filtered_type_annotate_foo'] = function() end
return false
elseif a.kind == 'type-annotate' then
return true
else
assert(nil, 'unreachable')
end
end,
})
end)
elseif ctx.method == 'shutdown' then
eq(
'function',
exec_lua(function()
return type(vim.lsp.commands['executed_preferred'])
end)
)
eq(
'function',
exec_lua(function()
return type(vim.lsp.commands['filtered_type_annotate_foo'])
end)
)
eq(
'function',
exec_lua(function()
return type(vim.lsp.commands['executed_type_annotate'])
end)
)
client:stop()
end
end,
}
end)
it('uses diagnostics at cursor position', function()
exec_lua(create_server_definition)
local severity = exec_lua(function()
return vim.diagnostic.severity.ERROR
end)
local messages = exec_lua(function(severity_)
local server = _G._create_server({
capabilities = {
codeActionProvider = true,
},
handlers = {
['textDocument/codeAction'] = function(_, _, callback)
callback(nil, {})
end,
},
})
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'local first, second = 1, 2' })
local client_id = assert(vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
}))
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()
vim.api.nvim_win_set_cursor(0, { 1, 15 })
local ns = vim.lsp.diagnostic.get_namespace(client_id)
vim.diagnostic.set(ns, bufnr, {
{
lnum = 0,
col = 6,
end_lnum = 0,
end_col = 11,
message = 'first',
severity = severity_,
user_data = {
lsp = {
range = {
start = { line = 0, character = 6 },
['end'] = { line = 0, character = 11 },
},
message = 'first',
severity = severity_,
},
},
},
{
lnum = 0,
col = 13,
end_lnum = 0,
end_col = 19,
message = 'second',
severity = severity_,
user_data = {
lsp = {
range = {
start = { line = 0, character = 13 },
['end'] = { line = 0, character = 19 },
},
message = 'second',
severity = severity_,
},
},
},
})
vim.lsp.buf.code_action()
expected_messages = expected_messages + 1
wait_for_messages()
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
end, severity)
eq('textDocument/codeAction', messages[3].method)
eq({
{
range = {
start = { line = 0, character = 13 },
['end'] = { line = 0, character = 19 },
},
message = 'second',
severity = severity,
},
}, messages[3].params.context.diagnostics)
end)
it('fallback to command execution on resolve error', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local server = _G._create_server({
capabilities = {
executeCommandProvider = {
commands = { 'command:1' },
},
codeActionProvider = {
resolveProvider = true,
},
},
handlers = {
['textDocument/codeAction'] = function(_, _, callback)
callback(nil, {
{
title = 'Code Action 1',
command = {
title = 'Command 1',
command = 'command:1',
},
},
})
end,
['codeAction/resolve'] = function(_, _, callback)
callback('resolve failed', nil)
end,
},
})
local client_id = assert(vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
}))
vim.lsp.buf.code_action({ apply = true })
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
end)
eq('codeAction/resolve', result[4].method)
eq('workspace/executeCommand', result[5].method)
eq('command:1', result[5].params.command)
end)
it('resolves command property', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local server = _G._create_server({
capabilities = {
executeCommandProvider = {
commands = { 'command:1' },
},
codeActionProvider = {
resolveProvider = true,
},
},
handlers = {
['textDocument/codeAction'] = function(_, _, callback)
callback(nil, {
{ title = 'Code Action 1' },
})
end,
['codeAction/resolve'] = function(_, _, callback)
callback(nil, {
title = 'Code Action 1',
command = {
title = 'Command 1',
command = 'command:1',
},
})
end,
},
})
local client_id = assert(vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
}))
vim.lsp.buf.code_action({ apply = true })
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
end)
eq('codeAction/resolve', result[4].method)
eq('workspace/executeCommand', result[5].method)
eq('command:1', result[5].params.command)
end)
it('supports disabled actions', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local server = _G._create_server({
capabilities = {
executeCommandProvider = {
commands = { 'command:1' },
},
codeActionProvider = {
resolveProvider = true,
},
},
handlers = {
['textDocument/codeAction'] = function(_, _, callback)
callback(nil, {
{
title = 'Code Action 1',
disabled = {
reason = 'This action is disabled',
},
},
})
end,
['codeAction/resolve'] = function(_, _, callback)
callback(nil, {
title = 'Code Action 1',
command = {
title = 'Command 1',
command = 'command:1',
},
})
end,
},
})
local client_id = assert(vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
}))
--- @diagnostic disable-next-line:duplicate-set-field
vim.notify = function(message, code)
server.messages[#server.messages + 1] = {
params = {
message = message,
code = code,
},
}
end
vim.lsp.buf.code_action({ apply = true })
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
end)
eq(
exec_lua(function()
return { message = 'This action is disabled', code = vim.log.levels.ERROR }
end),
result[4].params
)
-- No command is resolved/applied after selecting a disabled code action
eq('shutdown', result[5].method)
end)
end)
describe('vim.lsp.commands', function()
it('accepts only string keys', function()
matches(
'.*The key for commands in `vim.lsp.commands` must be a string',
pcall_err(exec_lua, 'vim.lsp.commands[1] = function() end')
)
end)
it('accepts only function values', function()
matches(
'.*Command added to `vim.lsp.commands` must be a function',
pcall_err(exec_lua, 'vim.lsp.commands.dummy = 10')
)
end)
end)
describe('vim.lsp.buf.format', function()
it('aborts with notify if no client matches filter', function()
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = 'basic_init',
on_init = function(c)
client = c
end,
on_handler = function()
local notify_msg = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
local notify_msg --- @type string?
local notify = vim.notify
vim.notify = function(msg, _)
notify_msg = msg
end
vim.lsp.buf.format({ name = 'does-not-exist' })
vim.notify = notify
return notify_msg
end)
eq('[LSP] Format request failed, no matching language servers.', notify_msg)
client:stop()
end,
}
end)
it('sends textDocument/formatting request to format buffer', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = 'basic_formatting',
on_init = function(c)
client = c
end,
on_handler = function(_, _, ctx)
table.remove(expected_handlers)
if ctx.method == 'start' then
local notify_msg = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
local notify_msg --- @type string?
local notify = vim.notify
vim.notify = function(msg, _)
notify_msg = msg
end
vim.lsp.buf.format({ bufnr = bufnr })
vim.notify = notify
return notify_msg
end)
eq(nil, notify_msg)
elseif ctx.method == 'shutdown' then
client:stop()
end
end,
}
end)
it('sends textDocument/rangeFormatting request to format a range', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = 'range_formatting',
on_init = function(c)
client = c
end,
on_handler = function(_, _, ctx)
table.remove(expected_handlers)
if ctx.method == 'start' then
local notify_msg = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar' })
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
local notify_msg --- @type string?
local notify = vim.notify
vim.notify = function(msg, _)
notify_msg = msg
end
vim.lsp.buf.format({
bufnr = bufnr,
range = {
start = { 1, 1 },
['end'] = { 1, 1 },
},
})
vim.notify = notify
return notify_msg
end)
eq(nil, notify_msg)
elseif ctx.method == 'shutdown' then
client:stop()
end
end,
}
end)
it('sends textDocument/rangesFormatting request to format multiple ranges', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = 'ranges_formatting',
on_init = function(c)
client = c
end,
on_handler = function(_, _, ctx)
table.remove(expected_handlers)
if ctx.method == 'start' then
local notify_msg = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar', 'baz' })
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
local notify_msg --- @type string?
local notify = vim.notify
vim.notify = function(msg, _)
notify_msg = msg
end
vim.lsp.buf.format({
bufnr = bufnr,
range = {
{
start = { 1, 1 },
['end'] = { 1, 1 },
},
{
start = { 2, 2 },
['end'] = { 2, 2 },
},
},
})
vim.notify = notify
return notify_msg
end)
eq(nil, notify_msg)
elseif ctx.method == 'shutdown' then
client:stop()
end
end,
}
end)
it('can format async', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
{ NIL, {}, { method = 'start', client_id = 1 } },
}
local client --- @type vim.lsp.Client
test_rpc_server {
test_name = 'basic_formatting',
on_init = function(c)
client = c
end,
on_handler = function(_, _, ctx)
table.remove(expected_handlers)
if ctx.method == 'start' then
local result = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
local notify_msg --- @type string?
local notify = vim.notify
vim.notify = function(msg, _)
notify_msg = msg
end
_G.handler_called = false
vim.lsp.buf.format({ bufnr = bufnr, async = true })
vim.wait(1000, function()
return _G.handler_called
end)
vim.notify = notify
return { notify_msg = notify_msg, handler_called = _G.handler_called }
end)
eq({ handler_called = true }, result)
elseif ctx.method == 'textDocument/formatting' then
exec_lua('_G.handler_called = true')
elseif ctx.method == 'shutdown' then
client:stop()
end
end,
}
end)
it('format formats range in visual mode', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local server = _G._create_server({
capabilities = {
documentFormattingProvider = true,
documentRangeFormattingProvider = true,
},
})
local bufnr = vim.api.nvim_get_current_buf()
local client_id = assert(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' })
vim.api.nvim_win_set_cursor(0, { 1, 0 })
vim.cmd.normal('v')
vim.api.nvim_win_set_cursor(0, { 2, 3 })
vim.lsp.buf.format({ bufnr = bufnr, false })
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
end)
eq('textDocument/rangeFormatting', result[3].method)
local expected_range = {
start = { line = 0, character = 0 },
['end'] = { line = 1, character = 4 },
}
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(function()
local server = _G._create_server({
capabilities = {
documentFormattingProvider = true,
documentRangeFormattingProvider = true,
},
})
local bufnr = vim.api.nvim_get_current_buf()
local client_id = assert(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.get_client_by_id(client_id):stop()
return server.messages
end)
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(function()
vim.notify = function(msg, _)
_G.notify_msg = msg
end
end)
local fail_msg = '[LSP] Format request failed, no matching language servers.'
--- @param name string
--- @param formatting boolean
--- @param range_formatting boolean
local function check_notify(name, formatting, range_formatting)
local timeout_msg = '[LSP][' .. name .. '] timeout'
exec_lua(function()
local server = _G._create_server({
capabilities = {
documentFormattingProvider = formatting,
documentRangeFormattingProvider = range_formatting,
},
})
vim.lsp.start({ name = name, cmd = server.cmd })
_G.notify_msg = nil
vim.lsp.buf.format({ name = name, timeout_ms = 1 })
end)
eq(
formatting and timeout_msg or fail_msg,
exec_lua(function()
return _G.notify_msg
end)
)
exec_lua(function()
_G.notify_msg = nil
vim.lsp.buf.format({
name = name,
timeout_ms = 1,
range = {
start = { 1, 0 },
['end'] = {
1,
0,
},
},
})
end)
eq(
range_formatting and timeout_msg or fail_msg,
exec_lua(function()
return _G.notify_msg
end)
)
end
check_notify('none', false, false)
check_notify('formatting', true, false)
check_notify('rangeFormatting', false, true)
check_notify('both', true, true)
end)
end)
describe('vim.lsp.buf.definition', function()
it('jumps to single location and can reuse win', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
local server = _G._create_server({
capabilities = {
definitionProvider = true,
},
handlers = {
['textDocument/definition'] = function(_, _, callback)
local location = {
range = {
start = { line = 0, character = 0 },
['end'] = { line = 0, character = 0 },
},
uri = vim.uri_from_bufnr(bufnr),
}
callback(nil, location)
end,
},
})
local win = vim.api.nvim_get_current_win()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'local x = 10', '', 'print(x)' })
vim.api.nvim_win_set_cursor(win, { 3, 6 })
local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd }))
vim.lsp.buf.definition()
return {
win = win,
bufnr = bufnr,
client_id = client_id,
cursor = vim.api.nvim_win_get_cursor(win),
messages = server.messages,
tagstack = vim.fn.gettagstack(win),
}
end)
eq('textDocument/definition', result.messages[3].method)
eq({ 1, 0 }, result.cursor)
eq(1, #result.tagstack.items)
eq('x', result.tagstack.items[1].tagname)
eq(3, result.tagstack.items[1].from[2])
eq(7, result.tagstack.items[1].from[3])
local result_bufnr = api.nvim_get_current_buf()
n.feed(':tabe<CR>')
api.nvim_win_set_buf(0, result_bufnr)
local displayed_result_win = api.nvim_get_current_win()
n.feed(':vnew<CR>')
api.nvim_win_set_buf(0, result.bufnr)
api.nvim_win_set_cursor(0, { 3, 6 })
n.feed(':set switchbuf=usetab<CR>')
n.feed(':=vim.lsp.buf.definition()<CR>')
eq(displayed_result_win, api.nvim_get_current_win())
exec_lua(function()
vim.lsp.get_client_by_id(result.client_id):stop()
end)
end)
it('merges results from multiple servers', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local bufnr = vim.api.nvim_get_current_buf()
local function serveropts(character)
return {
capabilities = {
definitionProvider = true,
},
handlers = {
['textDocument/definition'] = function(_, _, callback)
local location = {
range = {
start = { line = 0, character = character },
['end'] = { line = 0, character = character },
},
uri = vim.uri_from_bufnr(bufnr),
}
callback(nil, location)
end,
},
}
end
local server1 = _G._create_server(serveropts(0))
local server2 = _G._create_server(serveropts(7))
local win = vim.api.nvim_get_current_win()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'local x = 10', '', 'print(x)' })
vim.api.nvim_win_set_cursor(win, { 3, 6 })
local client_id1 = assert(vim.lsp.start({ name = 'dummy1', cmd = server1.cmd }))
local client_id2 = assert(vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }))
local response
vim.lsp.buf.definition({
on_list = function(r)
response = r
end,
})
vim.lsp.get_client_by_id(client_id1):stop()
vim.lsp.get_client_by_id(client_id2):stop()
return response
end)
eq(2, #result.items)
end)
end)
describe('vim.lsp.buf.workspace_diagnostics()', function()
local fake_uri = 'file:///fake/uri'
--- @param kind lsp.DocumentDiagnosticReportKind
--- @param msg string
--- @param pos integer
--- @return lsp.WorkspaceDocumentDiagnosticReport
local function make_report(kind, msg, pos)
return {
kind = kind,
uri = fake_uri,
items = {
{
range = {
start = { line = pos, character = pos },
['end'] = { line = pos, character = pos },
},
message = msg,
severity = 1,
},
},
}
end
--- @param items lsp.WorkspaceDocumentDiagnosticReport[]
--- @return integer
local function setup_server(items)
exec_lua(create_server_definition)
return exec_lua(function()
_G.server = _G._create_server({
capabilities = {
diagnosticProvider = { workspaceDiagnostics = true },
},
handlers = {
['workspace/diagnostic'] = function(_, _, callback)
callback(nil, { items = items })
end,
},
})
local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }))
vim.lsp.buf.workspace_diagnostics()
return client_id
end, { items })
end
it('updates diagnostics obtained with vim.diagnostic.get()', function()
setup_server({ make_report('full', 'Error here', 1) })
retry(nil, nil, function()
eq(
1,
exec_lua(function()
return #vim.diagnostic.get()
end)
)
end)
eq(
'Error here',
exec_lua(function()
return vim.diagnostic.get()[1].message
end)
)
end)
it('ignores unchanged diagnostic reports', function()
setup_server({ make_report('unchanged', '', 1) })
eq(
0,
exec_lua(function()
-- Wait for diagnostics to be processed.
vim.uv.sleep(50)
return #vim.diagnostic.get()
end)
)
end)
it('favors document diagnostics over workspace diagnostics', function()
local client_id = setup_server({ make_report('full', 'Workspace error', 1) })
local diagnostic_bufnr = exec_lua(function()
return vim.uri_to_bufnr(fake_uri)
end)
exec_lua(function()
vim.lsp.diagnostic.on_diagnostic(nil, {
kind = 'full',
items = {
{
range = {
start = { line = 2, character = 2 },
['end'] = { line = 2, character = 2 },
},
message = 'Document error',
severity = 1,
},
},
}, {
method = 'textDocument/diagnostic',
params = {
textDocument = { uri = fake_uri },
},
client_id = client_id,
bufnr = diagnostic_bufnr,
})
end)
eq(
1,
exec_lua(function()
return #vim.diagnostic.get(diagnostic_bufnr)
end)
)
eq(
'Document error',
exec_lua(function()
return vim.diagnostic.get(vim.uri_to_bufnr(fake_uri))[1].message
end)
)
end)
end)
describe('vim.lsp.buf.hover()', function()
it('handles empty contents', function()
exec_lua(create_server_definition)
exec_lua(function()
local server = _G._create_server({
capabilities = {
hoverProvider = true,
},
handlers = {
['textDocument/hover'] = function(_, _, callback)
local res = {
contents = {
kind = 'markdown',
value = '',
},
}
callback(nil, res)
end,
},
})
vim.lsp.start({ name = 'dummy', cmd = server.cmd })
end)
eq('Empty hover response', exec_capture('lua vim.lsp.buf.hover()'))
end)
it('treats markedstring array as not empty', function()
exec_lua(create_server_definition)
exec_lua(function()
local server = _G._create_server({
capabilities = {
hoverProvider = true,
},
handlers = {
['textDocument/hover'] = function(_, _, callback)
local res = {
contents = {
{
language = 'java',
value = 'Example',
},
'doc comment',
},
}
callback(nil, res)
end,
},
})
vim.lsp.start({ name = 'dummy', cmd = server.cmd })
end)
eq('', exec_capture('lua vim.lsp.buf.hover()'))
end)
end)
end)