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/_') 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') api.nvim_win_set_buf(0, result_bufnr) local displayed_result_win = api.nvim_get_current_win() n.feed(':vnew') api.nvim_win_set_buf(0, result.bufnr) api.nvim_win_set_cursor(0, { 3, 6 }) n.feed(':set switchbuf=usetab') n.feed(':=vim.lsp.buf.definition()') 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)