diff --git a/test/functional/plugin/lsp/buf_spec.lua b/test/functional/plugin/lsp/buf_spec.lua new file mode 100644 index 0000000000..a2afbe56f7 --- /dev/null +++ b/test/functional/plugin/lsp/buf_spec.lua @@ -0,0 +1,1751 @@ +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'] + 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 = '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) diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua new file mode 100644 index 0000000000..7f4ef702e9 --- /dev/null +++ b/test/functional/plugin/lsp/util_spec.lua @@ -0,0 +1,1552 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local Screen = require('test.functional.ui.screen') + +local t_lsp = require('test.functional.plugin.lsp.testutil') + +local feed = n.feed +local eq = t.eq +local exec_lua = n.exec_lua +local pcall_err = t.pcall_err +local stop = n.stop +local read_file = t.read_file +local write_file = t.write_file +local api = n.api +local is_os = t.is_os +local skip = t.skip +local command = n.command +local fn = n.fn +local tmpname = t.tmpname + +local clear_notrace = t_lsp.clear_notrace +local create_server_definition = t_lsp.create_server_definition + +-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837 +if skip(is_os('win')) then + return +end + +describe('vim.lsp.util', function() + before_each(function() + clear_notrace() + 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) + + describe('lsp.util.rename', function() + local pathsep = n.get_pathsep() + + it('can rename an existing file', function() + local old = tmpname() + write_file(old, 'Test content') + local new = tmpname(false) + local lines = exec_lua(function() + local old_bufnr = vim.fn.bufadd(old) + vim.fn.bufload(old_bufnr) + vim.lsp.util.rename(old, new) + -- the existing buffer is renamed in-place and its contents is kept + local new_bufnr = vim.fn.bufadd(new) + vim.fn.bufload(new_bufnr) + return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) + end) + eq({ 'Test content' }, lines) + local exists = vim.uv.fs_stat(old) ~= nil + eq(false, exists) + exists = vim.uv.fs_stat(new) ~= nil + eq(true, exists) + os.remove(new) + end) + + it('can rename a directory', function() + -- only reserve the name, file must not exist for the test scenario + local old_dir = tmpname(false) + local new_dir = tmpname(false) + + n.mkdir_p(old_dir) + + local file = 'file.txt' + write_file(old_dir .. pathsep .. file, 'Test content') + + local lines = exec_lua(function() + local old_bufnr = vim.fn.bufadd(old_dir .. pathsep .. file) + vim.fn.bufload(old_bufnr) + vim.lsp.util.rename(old_dir, new_dir) + -- the existing buffer is renamed in-place and its contents is kept + local new_bufnr = vim.fn.bufadd(new_dir .. pathsep .. file) + vim.fn.bufload(new_bufnr) + return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) + end) + eq({ 'Test content' }, lines) + eq(false, vim.uv.fs_stat(old_dir) ~= nil) + eq(true, vim.uv.fs_stat(new_dir) ~= nil) + eq(true, vim.uv.fs_stat(new_dir .. pathsep .. file) ~= nil) + + os.remove(new_dir) + end) + + it('does not touch buffers that do not match path prefix', function() + local old = tmpname(false) + local new = tmpname(false) + n.mkdir_p(old) + + eq( + true, + exec_lua(function() + local old_prefixed = 'explorer://' .. old + local old_suffixed = old .. '.bak' + local new_prefixed = 'explorer://' .. new + local new_suffixed = new .. '.bak' + + local old_prefixed_buf = vim.fn.bufadd(old_prefixed) + local old_suffixed_buf = vim.fn.bufadd(old_suffixed) + local new_prefixed_buf = vim.fn.bufadd(new_prefixed) + local new_suffixed_buf = vim.fn.bufadd(new_suffixed) + + vim.lsp.util.rename(old, new) + + return vim.api.nvim_buf_is_valid(old_prefixed_buf) + and vim.api.nvim_buf_is_valid(old_suffixed_buf) + and vim.api.nvim_buf_is_valid(new_prefixed_buf) + and vim.api.nvim_buf_is_valid(new_suffixed_buf) + and vim.api.nvim_buf_get_name(old_prefixed_buf) == old_prefixed + and vim.api.nvim_buf_get_name(old_suffixed_buf) == old_suffixed + and vim.api.nvim_buf_get_name(new_prefixed_buf) == new_prefixed + and vim.api.nvim_buf_get_name(new_suffixed_buf) == new_suffixed + end) + ) + + os.remove(new) + end) + + it( + 'does not rename file if target exists and ignoreIfExists is set or overwrite is false', + function() + local old = tmpname() + write_file(old, 'Old File') + local new = tmpname() + write_file(new, 'New file') + + exec_lua(function() + vim.lsp.util.rename(old, new, { ignoreIfExists = true }) + end) + + eq(true, vim.uv.fs_stat(old) ~= nil) + eq('New file', read_file(new)) + + exec_lua(function() + vim.lsp.util.rename(old, new, { overwrite = false }) + end) + + eq(true, vim.uv.fs_stat(old) ~= nil) + eq('New file', read_file(new)) + end + ) + + it('maintains undo information for loaded buffer', function() + local old = tmpname() + write_file(old, 'line') + local new = tmpname(false) + + local undo_kept = exec_lua(function() + vim.opt.undofile = true + vim.cmd.edit(old) + vim.cmd.normal('dd') + vim.cmd.write() + local undotree = vim.fn.undotree() + vim.lsp.util.rename(old, new) + -- Renaming uses :saveas, which updates the "last write" information. + -- Other than that, the undotree should remain the same. + undotree.save_cur = undotree.save_cur + 1 + undotree.save_last = undotree.save_last + 1 + undotree.entries[1].save = undotree.entries[1].save + 1 + return vim.deep_equal(undotree, vim.fn.undotree()) + end) + eq(false, vim.uv.fs_stat(old) ~= nil) + eq(true, vim.uv.fs_stat(new) ~= nil) + eq(true, undo_kept) + end) + + it('maintains undo information for unloaded buffer', function() + local old = tmpname() + write_file(old, 'line') + local new = tmpname(false) + + local undo_kept = exec_lua(function() + vim.opt.undofile = true + vim.cmd.split(old) + vim.cmd.normal('dd') + vim.cmd.write() + local undotree = vim.fn.undotree() + vim.cmd.bdelete() + vim.lsp.util.rename(old, new) + vim.cmd.edit(new) + return vim.deep_equal(undotree, vim.fn.undotree()) + end) + eq(false, vim.uv.fs_stat(old) ~= nil) + eq(true, vim.uv.fs_stat(new) ~= nil) + eq(true, undo_kept) + end) + + it('does not rename file when it conflicts with a buffer without file', function() + local old = tmpname() + write_file(old, 'Old File') + local new = tmpname(false) + + local lines = exec_lua(function() + local old_buf = vim.fn.bufadd(old) + vim.fn.bufload(old_buf) + local conflict_buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(conflict_buf, new) + vim.api.nvim_buf_set_lines(conflict_buf, 0, -1, true, { 'conflict' }) + vim.api.nvim_win_set_buf(0, conflict_buf) + vim.lsp.util.rename(old, new) + return vim.api.nvim_buf_get_lines(conflict_buf, 0, -1, true) + end) + eq({ 'conflict' }, lines) + eq('Old File', read_file(old)) + end) + + it('does override target if overwrite is true', function() + local old = tmpname() + write_file(old, 'Old file') + local new = tmpname() + write_file(new, 'New file') + exec_lua(function() + vim.lsp.util.rename(old, new, { overwrite = true }) + end) + + eq(false, vim.uv.fs_stat(old) ~= nil) + eq(true, vim.uv.fs_stat(new) ~= nil) + eq('Old file', read_file(new)) + end) + end) + + describe('lsp.util.locations_to_items', function() + it('convert Location[] to items', function() + local expected_template = { + { + filename = '/fake/uri', + lnum = 1, + end_lnum = 2, + col = 3, + end_col = 4, + text = 'testing', + user_data = {}, + }, + } + local test_params = { + { + { + uri = 'file:///fake/uri', + range = { + start = { line = 0, character = 2 }, + ['end'] = { line = 1, character = 3 }, + }, + }, + }, + { + { + uri = 'file:///fake/uri', + range = { + start = { line = 0, character = 2 }, + -- LSP spec: if character > line length, default to the line length. + ['end'] = { line = 1, character = 10000 }, + }, + }, + }, + } + for _, params in ipairs(test_params) do + local actual = exec_lua(function(params0) + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + local lines = { 'testing', '123' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return vim.lsp.util.locations_to_items(params0, 'utf-16') + end, params) + local expected = vim.deepcopy(expected_template) + expected[1].user_data = params[1] + eq(expected, actual) + end + end) + + it('convert LocationLink[] to items', function() + local expected = { + { + filename = '/fake/uri', + lnum = 1, + end_lnum = 1, + col = 3, + end_col = 4, + text = 'testing', + user_data = { + targetUri = 'file:///fake/uri', + targetRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + }, + targetSelectionRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + }, + }, + }, + } + local actual = exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + local lines = { 'testing', '123' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + local locations = { + { + targetUri = vim.uri_from_bufnr(bufnr), + targetRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + }, + targetSelectionRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + }, + }, + } + return vim.lsp.util.locations_to_items(locations, 'utf-16') + end) + eq(expected, actual) + end) + end) + + describe('lsp.util.symbols_to_items', function() + describe('convert DocumentSymbol[] to items', function() + it('documentSymbol has children', function() + local expected = { + { + col = 1, + end_col = 1, + end_lnum = 2, + filename = '', + kind = 'File', + lnum = 2, + text = '[File] TestA', + }, + { + col = 1, + end_col = 1, + end_lnum = 4, + filename = '', + kind = 'Module', + lnum = 4, + text = '[Module] TestB', + }, + { + col = 1, + end_col = 1, + end_lnum = 6, + filename = '', + kind = 'Namespace', + lnum = 6, + text = '[Namespace] TestC', + }, + } + eq( + expected, + exec_lua(function() + local doc_syms = { + { + deprecated = false, + detail = 'A', + kind = 1, + name = 'TestA', + range = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + selectionRange = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 4, + line = 1, + }, + }, + children = { + { + children = {}, + deprecated = false, + detail = 'B', + kind = 2, + name = 'TestB', + range = { + start = { + character = 0, + line = 3, + }, + ['end'] = { + character = 0, + line = 4, + }, + }, + selectionRange = { + start = { + character = 0, + line = 3, + }, + ['end'] = { + character = 4, + line = 3, + }, + }, + }, + }, + }, + { + deprecated = false, + detail = 'C', + kind = 3, + name = 'TestC', + range = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 0, + line = 6, + }, + }, + selectionRange = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 4, + line = 5, + }, + }, + }, + } + return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') + end) + ) + end) + + it('documentSymbol has no children', function() + local expected = { + { + col = 1, + end_col = 1, + end_lnum = 2, + filename = '', + kind = 'File', + lnum = 2, + text = '[File] TestA', + }, + { + col = 1, + end_col = 1, + end_lnum = 6, + filename = '', + kind = 'Namespace', + lnum = 6, + text = '[Namespace] TestC', + }, + } + eq( + expected, + exec_lua(function() + local doc_syms = { + { + deprecated = false, + detail = 'A', + kind = 1, + name = 'TestA', + range = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + selectionRange = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 4, + line = 1, + }, + }, + }, + { + deprecated = false, + detail = 'C', + kind = 3, + name = 'TestC', + range = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 0, + line = 6, + }, + }, + selectionRange = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 4, + line = 5, + }, + }, + }, + } + return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') + end) + ) + end) + + it('handles deprecated items', function() + local expected = { + { + col = 1, + end_col = 1, + end_lnum = 2, + filename = '', + kind = 'File', + lnum = 2, + text = '[File] TestA (deprecated)', + }, + { + col = 1, + end_col = 1, + end_lnum = 6, + filename = '', + kind = 'Namespace', + lnum = 6, + text = '[Namespace] TestC (deprecated)', + }, + } + eq( + expected, + exec_lua(function() + local doc_syms = { + { + deprecated = true, + detail = 'A', + kind = 1, + name = 'TestA', + range = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + selectionRange = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 4, + line = 1, + }, + }, + }, + { + detail = 'C', + kind = 3, + name = 'TestC', + range = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 0, + line = 6, + }, + }, + selectionRange = { + start = { + character = 0, + line = 5, + }, + ['end'] = { + character = 4, + line = 5, + }, + }, + tags = { 1 }, -- deprecated + }, + } + return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') + end) + ) + end) + end) + + it('convert SymbolInformation[] to items', function() + local expected = { + { + col = 1, + end_col = 1, + end_lnum = 3, + filename = '/test_a', + kind = 'File', + lnum = 2, + text = '[File] TestA in TestAContainer', + }, + { + col = 1, + end_col = 1, + end_lnum = 5, + filename = '/test_b', + kind = 'Module', + lnum = 4, + text = '[Module] TestB in TestBContainer (deprecated)', + }, + } + eq( + expected, + exec_lua(function() + local sym_info = { + { + deprecated = false, + kind = 1, + name = 'TestA', + location = { + range = { + start = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + uri = 'file:///test_a', + }, + containerName = 'TestAContainer', + }, + { + deprecated = true, + kind = 2, + name = 'TestB', + location = { + range = { + start = { + character = 0, + line = 3, + }, + ['end'] = { + character = 0, + line = 4, + }, + }, + uri = 'file:///test_b', + }, + containerName = 'TestBContainer', + }, + } + return vim.lsp.util.symbols_to_items(sym_info, nil, 'utf-16') + end) + ) + end) + end) + + describe('lsp.util.jump_to_location', function() + local target_bufnr --- @type integer + + before_each(function() + target_bufnr = exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + end) + end) + + local location = function(start_line, start_char, end_line, end_char) + return { + uri = 'file:///fake/uri', + range = { + start = { line = start_line, character = start_char }, + ['end'] = { line = end_line, character = end_char }, + }, + } + end + + local jump = function(msg) + eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg, 'utf-16')) + eq(target_bufnr, fn.bufnr('%')) + return { + line = fn.line('.'), + col = fn.col('.'), + } + end + + it('jumps to a Location', function() + local pos = jump(location(0, 9, 0, 9)) + eq(1, pos.line) + eq(10, pos.col) + end) + + it('jumps to a LocationLink', function() + local pos = jump({ + targetUri = 'file:///fake/uri', + targetSelectionRange = { + start = { line = 0, character = 4 }, + ['end'] = { line = 0, character = 4 }, + }, + targetRange = { + start = { line = 1, character = 5 }, + ['end'] = { line = 1, character = 5 }, + }, + }) + eq(1, pos.line) + eq(5, pos.col) + end) + + it('jumps to the correct multibyte column', function() + local pos = jump(location(1, 2, 1, 2)) + eq(2, pos.line) + eq(4, pos.col) + eq('å', fn.expand('')) + end) + + it('adds current position to jumplist before jumping', function() + api.nvim_win_set_buf(0, target_bufnr) + local mark = api.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + + api.nvim_win_set_cursor(0, { 2, 3 }) + jump(location(0, 9, 0, 9)) + + mark = api.nvim_buf_get_mark(target_bufnr, "'") + eq({ 2, 3 }, mark) + end) + end) + + describe('lsp.util.show_document', function() + local target_bufnr --- @type integer + local target_bufnr2 --- @type integer + + before_each(function() + target_bufnr = exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + end) + + target_bufnr2 = exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri2') + local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + end) + end) + + local location = function(start_line, start_char, end_line, end_char, second_uri) + return { + uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri', + range = { + start = { line = start_line, character = start_char }, + ['end'] = { line = end_line, character = end_char }, + }, + } + end + + local show_document = function(msg, focus, reuse_win) + eq( + true, + exec_lua( + 'return vim.lsp.util.show_document(...)', + msg, + 'utf-16', + { reuse_win = reuse_win, focus = focus } + ) + ) + if focus == true or focus == nil then + eq(target_bufnr, fn.bufnr('%')) + end + return { + line = fn.line('.'), + col = fn.col('.'), + } + end + + it('jumps to a Location if focus is true', function() + local pos = show_document(location(0, 9, 0, 9), true, true) + eq(1, pos.line) + eq(10, pos.col) + + -- expectation: Cursor is placed past EOL (append position) in insert mode + n.feed('I') + pos = show_document(location(0, 16, 0, 16), true, true) + eq(1, pos.line) + eq(17, pos.col) + eq('i', api.nvim_get_mode().mode) + end) + + it('jumps to a Location if focus is true via handler', function() + exec_lua(create_server_definition) + local result = exec_lua(function() + local server = _G._create_server() + local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) + local result = { + uri = 'file:///fake/uri', + selection = { + start = { line = 0, character = 9 }, + ['end'] = { line = 0, character = 9 }, + }, + takeFocus = true, + } + local ctx = { + client_id = client_id, + method = 'window/showDocument', + } + vim.lsp.handlers['window/showDocument'](nil, result, ctx) + vim.lsp.get_client_by_id(client_id):stop() + return { + cursor = vim.api.nvim_win_get_cursor(0), + } + end) + eq(1, result.cursor[1]) + eq(9, result.cursor[2]) + end) + + it('jumps to a Location if focus not set', function() + local pos = show_document(location(0, 9, 0, 9), nil, true) + eq(1, pos.line) + eq(10, pos.col) + end) + + it('does not add current position to jumplist if not focus', function() + api.nvim_win_set_buf(0, target_bufnr) + local mark = api.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + + api.nvim_win_set_cursor(0, { 2, 3 }) + show_document(location(0, 9, 0, 9), false, true) + show_document(location(0, 9, 0, 9, true), false, true) + + mark = api.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + end) + + it('does not change cursor position if not focus and not reuse_win', function() + api.nvim_win_set_buf(0, target_bufnr) + local cursor = api.nvim_win_get_cursor(0) + + show_document(location(0, 9, 0, 9), false, false) + eq(cursor, api.nvim_win_get_cursor(0)) + end) + + it('does not change window if not focus', function() + api.nvim_win_set_buf(0, target_bufnr) + local win = api.nvim_get_current_win() + + -- same document/bufnr + show_document(location(0, 9, 0, 9), false, true) + eq(win, api.nvim_get_current_win()) + + -- different document/bufnr, new window/split + show_document(location(0, 9, 0, 9, true), false, true) + eq(2, #api.nvim_list_wins()) + eq(win, api.nvim_get_current_win()) + end) + + it("respects 'reuse_win' parameter", function() + api.nvim_win_set_buf(0, target_bufnr) + + -- does not create a new window if the buffer is already open + show_document(location(0, 9, 0, 9), false, true) + eq(1, #api.nvim_list_wins()) + + -- creates a new window even if the buffer is already open + show_document(location(0, 9, 0, 9), false, false) + eq(2, #api.nvim_list_wins()) + end) + + it('correctly sets the cursor of the split if range is given without focus', function() + api.nvim_win_set_buf(0, target_bufnr) + + show_document(location(0, 9, 0, 9, true), false, true) + + local wins = api.nvim_list_wins() + eq(2, #wins) + table.sort(wins) + + eq({ 1, 0 }, api.nvim_win_get_cursor(wins[1])) + eq({ 1, 9 }, api.nvim_win_get_cursor(wins[2])) + end) + + it('does not change cursor of the split if not range and not focus', function() + api.nvim_win_set_buf(0, target_bufnr) + api.nvim_win_set_cursor(0, { 2, 3 }) + + exec_lua(function() + vim.cmd.new() + end) + api.nvim_win_set_buf(0, target_bufnr2) + api.nvim_win_set_cursor(0, { 2, 3 }) + + show_document({ uri = 'file:///fake/uri2' }, false, true) + + local wins = api.nvim_list_wins() + eq(2, #wins) + eq({ 2, 3 }, api.nvim_win_get_cursor(wins[1])) + eq({ 2, 3 }, api.nvim_win_get_cursor(wins[2])) + end) + + it('respects existing buffers', function() + api.nvim_win_set_buf(0, target_bufnr) + local win = api.nvim_get_current_win() + + exec_lua(function() + vim.cmd.new() + end) + api.nvim_win_set_buf(0, target_bufnr2) + api.nvim_win_set_cursor(0, { 2, 3 }) + local split = api.nvim_get_current_win() + + -- reuse win for open document/bufnr if called from split + show_document(location(0, 9, 0, 9, true), false, true) + eq({ 1, 9 }, api.nvim_win_get_cursor(split)) + eq(2, #api.nvim_list_wins()) + + api.nvim_set_current_win(win) + + -- reuse win for open document/bufnr if called outside the split + show_document(location(0, 9, 0, 9, true), false, true) + eq({ 1, 9 }, api.nvim_win_get_cursor(split)) + eq(2, #api.nvim_list_wins()) + end) + end) + + describe('lsp.util._make_floating_popup_size', function() + before_each(function() + exec_lua(function() + _G.contents = { 'text tαxt txtα tex', 'text tααt tααt text', 'text tαxt tαxt' } + end) + end) + + it('calculates size correctly', function() + eq( + { 19, 3 }, + exec_lua(function() + return { vim.lsp.util._make_floating_popup_size(_G.contents) } + end) + ) + end) + + it('calculates size correctly with wrapping', function() + eq( + { 15, 5 }, + exec_lua(function() + return { + vim.lsp.util._make_floating_popup_size(_G.contents, { width = 15, wrap_at = 14 }), + } + end) + ) + end) + + it('handles NUL bytes in text', function() + exec_lua(function() + _G.contents = { + '\000\001\002\003\004\005\006\007\008\009', + '\010\011\012\013\014\015\016\017\018\019', + '\020\021\022\023\024\025\026\027\028\029', + } + end) + command('set list listchars=') + eq( + { 20, 3 }, + exec_lua(function() + return { vim.lsp.util._make_floating_popup_size(_G.contents) } + end) + ) + command('set display+=uhex') + eq( + { 40, 3 }, + exec_lua(function() + return { vim.lsp.util._make_floating_popup_size(_G.contents) } + end) + ) + end) + it('handles empty line', function() + exec_lua(function() + _G.contents = { + '', + } + end) + eq( + { 20, 1 }, + exec_lua(function() + return { vim.lsp.util._make_floating_popup_size(_G.contents, { width = 20 }) } + end) + ) + end) + + it('considers string title when computing width', function() + eq( + { 17, 2 }, + exec_lua(function() + return { + vim.lsp.util._make_floating_popup_size( + { 'foo', 'bar' }, + { title = 'A very long title' } + ), + } + end) + ) + end) + + it('considers [string,string][] title when computing width', function() + eq( + { 17, 2 }, + exec_lua(function() + return { + vim.lsp.util._make_floating_popup_size( + { 'foo', 'bar' }, + { title = { { 'A very ', 'Normal' }, { 'long title', 'Normal' } } } + ), + } + end) + ) + end) + end) + + describe('lsp.util.trim.trim_empty_lines', function() + it('properly trims empty lines', function() + eq( + { { 'foo', 'bar' } }, + exec_lua(function() + --- @diagnostic disable-next-line:deprecated + return vim.lsp.util.trim_empty_lines({ { 'foo', 'bar' }, nil }) + end) + ) + end) + end) + + describe('lsp.util.convert_signature_help_to_markdown_lines', function() + it('can handle negative activeSignature', function() + local result = exec_lua(function() + local signature_help = { + activeParameter = 0, + activeSignature = -1, + signatures = { + { + documentation = 'some doc', + label = 'TestEntity.TestEntity()', + parameters = {}, + }, + }, + } + return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', { ',' }) + end) + local expected = { '```cs', 'TestEntity.TestEntity()', '```', '---', 'some doc' } + eq(expected, result) + end) + + it('highlights active parameters in multiline signature labels', function() + local _, hl = exec_lua(function() + local signature_help = { + activeSignature = 0, + signatures = { + { + activeParameter = 1, + label = 'fn bar(\n _: void,\n _: void,\n) void', + parameters = { + { label = '_: void' }, + { label = '_: void' }, + }, + }, + }, + } + return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'zig', { '(' }) + end) + -- Note that although the highlight positions below are 0-indexed, the 2nd parameter + -- corresponds to the 3rd line because the first line is the ``` from the + -- Markdown block. + local expected = { 3, 4, 3, 11 } + eq(expected, hl) + end) + end) + + describe('lsp.util.get_effective_tabstop', function() + local function test_tabstop(tabsize, shiftwidth) + exec_lua(string.format( + [[ + vim.bo.shiftwidth = %d + vim.bo.tabstop = 2 + ]], + shiftwidth + )) + eq( + tabsize, + exec_lua(function() + return vim.lsp.util.get_effective_tabstop() + end) + ) + end + + it('with shiftwidth = 1', function() + test_tabstop(1, 1) + end) + + it('with shiftwidth = 0', function() + test_tabstop(2, 0) + end) + end) + + describe('markdown and floating helpers', function() + before_each(n.clear) + + describe('stylize_markdown', function() + local stylize_markdown = function(content, opts) + return exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + vim.fn.bufload(bufnr) + return vim.lsp.util.stylize_markdown(bufnr, content, opts) + end) + end + + it('code fences', function() + local lines = { + '```lua', + "local hello = 'world'", + '```', + } + local expected = { + "local hello = 'world'", + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + + it('code fences with whitespace surrounded info string', function() + local lines = { + '``` lua ', + "local hello = 'world'", + '```', + } + local expected = { + "local hello = 'world'", + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + + it('adds separator after code block', function() + local lines = { + '```lua', + "local hello = 'world'", + '```', + '', + 'something', + } + local expected = { + "local hello = 'world'", + '─────────────────────', + 'something', + } + local opts = { separator = true } + eq(expected, stylize_markdown(lines, opts)) + end) + + it('replaces supported HTML entities', function() + local lines = { + '1 < 2', + '3 > 2', + '"quoted"', + ''apos'', + '   ', + '&', + } + local expected = { + '1 < 2', + '3 > 2', + '"quoted"', + "'apos'", + ' ', + '&', + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + end) + + it('convert_input_to_markdown_lines', function() + local r = exec_lua(function() + local hover_data = { + kind = 'markdown', + value = '```lua\nfunction vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)\n -> boolean\n```\n\n---\n\n Activates buffer-update events. Example:\n\n\n\n ```lua\n events = {}\n vim.api.nvim_buf_attach(0, false, {\n on_lines = function(...)\n table.insert(events, {...})\n end,\n })\n ```\n\n\n @see `nvim_buf_detach()`\n @see `api-buffer-updates-lua`\n@*param* `buffer` — Buffer handle, or 0 for current buffer\n\n\n\n@*param* `send_buffer` — True if whole buffer.\n Else the first notification will be `nvim_buf_changedtick_event`.\n\n\n@*param* `opts` — Optional parameters.\n\n - on_lines: Lua callback. Args:\n - the string "lines"\n - buffer handle\n - b:changedtick\n@*return* — False if foo;\n\n otherwise True.\n\n@see foo\n@see bar\n\n', + } + return vim.lsp.util.convert_input_to_markdown_lines(hover_data) + end) + local expected = { + '```lua', + 'function vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)', + ' -> boolean', + '```', + '', + '---', + '', + ' Activates buffer-update events. Example:', + '', + '', + '', + ' ```lua', + ' events = {}', + ' vim.api.nvim_buf_attach(0, false, {', + ' on_lines = function(...)', + ' table.insert(events, {...})', + ' end,', + ' })', + ' ```', + '', + '', + ' @see `nvim_buf_detach()`', + ' @see `api-buffer-updates-lua`', + '', + '@*param* `buffer` — Buffer handle, or 0 for current buffer', + '', + '@*param* `send_buffer` — True if whole buffer.', + ' Else the first notification will be `nvim_buf_changedtick_event`.', + '', + '@*param* `opts` — Optional parameters.', + ' - on_lines: Lua callback. Args:', + ' - the string "lines"', + ' - buffer handle', + ' - b:changedtick', + '', + '@*return* — False if foo;', + ' otherwise True.', + '@see foo', + '@see bar', + } + eq(expected, r) + end) + + describe('_normalize_markdown', function() + it('collapses consecutive blank lines', function() + local result = exec_lua(function() + local lines = { + 'foo', + '', + '', + '', + 'bar', + '', + 'baz', + } + return vim.lsp.util._normalize_markdown(lines) + end) + eq({ 'foo', '', 'bar', '', 'baz' }, result) + end) + + it('removes preceding and trailing empty lines', function() + local result = exec_lua(function() + local lines = { + '', + 'foo', + 'bar', + '', + '', + } + return vim.lsp.util._normalize_markdown(lines) + end) + eq({ 'foo', 'bar' }, result) + end) + end) + + describe('make_floating_popup_options', function() + local function assert_anchor(anchor_bias, expected_anchor) + local opts = exec_lua(function() + return vim.lsp.util.make_floating_popup_options(30, 10, { anchor_bias = anchor_bias }) + end) + + eq(expected_anchor, string.sub(opts.anchor, 1, 1)) + end + + before_each(function() + local _ = Screen.new(80, 80) + feed('79i') + end) + + describe('when on the first line it places window below', function() + before_each(function() + feed('gg') + end) + + it('for anchor_bias = "auto"', function() + assert_anchor('auto', 'N') + end) + + it('for anchor_bias = "above"', function() + assert_anchor('above', 'N') + end) + + it('for anchor_bias = "below"', function() + assert_anchor('below', 'N') + end) + end) + + describe('when on the last line it places window above', function() + before_each(function() + feed('G') + end) + + it('for anchor_bias = "auto"', function() + assert_anchor('auto', 'S') + end) + + it('for anchor_bias = "above"', function() + assert_anchor('above', 'S') + end) + + it('for anchor_bias = "below"', function() + assert_anchor('below', 'S') + end) + end) + + describe('with 20 lines above, 59 lines below', function() + before_each(function() + feed('gg20j') + end) + + it('places window below for anchor_bias = "auto"', function() + assert_anchor('auto', 'N') + end) + + it('places window above for anchor_bias = "above"', function() + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function() + assert_anchor('below', 'N') + end) + end) + + describe('with 59 lines above, 20 lines below', function() + before_each(function() + feed('G20k') + end) + + it('places window above for anchor_bias = "auto"', function() + assert_anchor('auto', 'S') + end) + + it('places window above for anchor_bias = "above"', function() + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function() + assert_anchor('below', 'N') + end) + + it('bordered window truncates dimensions correctly', function() + local opts = exec_lua(function() + return vim.lsp.util.make_floating_popup_options(100, 100, { border = 'single' }) + end) + + eq(56, opts.height) + end) + + it('title with winborder option #35179', function() + local opts = exec_lua(function() + vim.o.winborder = 'single' + return vim.lsp.util.make_floating_popup_options(100, 100, { title = 'Title' }) + end) + eq('Title', opts.title) + end) + end) + end) + + describe('open_floating_preview', function() + local curbuf + + before_each(function() + Screen.new(10, 10) + feed('9iG4k') + curbuf = api.nvim_get_current_buf() + end) + + local var_name = 'lsp_floating_preview' + + it('clean bufvar after fclose', function() + exec_lua(function() + vim.lsp.util.open_floating_preview({ 'test' }, '', { height = 5, width = 2 }) + end) + eq(true, api.nvim_win_is_valid(api.nvim_buf_get_var(curbuf, var_name))) + command('fclose') + eq('Key not found: lsp_floating_preview', pcall_err(api.nvim_buf_get_var, curbuf, var_name)) + end) + + it('clean bufvar after CursorMoved', function() + local result, winfixbuf = exec_lua(function() + vim.lsp.util.open_floating_preview({ 'test' }, '', { height = 5, width = 2 }) + local winnr = vim.b[vim.api.nvim_get_current_buf()].lsp_floating_preview + local result = vim.api.nvim_win_is_valid(winnr) + local winfixbuf = vim.wo[winnr].winfixbuf + vim.api.nvim_feedkeys(vim.keycode('G'), 'txn', false) + return result, winfixbuf + end) + eq(true, result) + eq(true, winfixbuf) + eq('Key not found: lsp_floating_preview', pcall_err(api.nvim_buf_get_var, curbuf, var_name)) + end) + end) + + it('open_floating_preview zindex greater than current window', function() + local screen = Screen.new() + exec_lua(function() + vim.api.nvim_open_win(0, true, { + relative = 'editor', + border = 'single', + height = 11, + width = 51, + row = 2, + col = 2, + }) + vim.keymap.set('n', 'K', function() + vim.lsp.util.open_floating_preview({ 'foo' }, '', { border = 'single' }) + end, {}) + end) + feed('K') + screen:expect([[ + ┌───────────────────────────────────────────────────┐| + │{4:^ }│| + │┌───┐{11: }│| + ││{4:foo}│{11: }│| + │└───┘{11: }│| + │{11:~ }│|*7 + └───────────────────────────────────────────────────┘| + | + ]]) + end) + + it('open_floating_preview height reduced for concealed lines', function() + local screen = Screen.new() + screen:add_extra_attr_ids({ + [100] = { + background = Screen.colors.LightMagenta, + foreground = Screen.colors.Brown, + bold = true, + }, + [101] = { background = Screen.colors.LightMagenta, foreground = Screen.colors.Blue }, + [102] = { background = Screen.colors.LightMagenta, foreground = Screen.colors.DarkCyan }, + }) + exec_lua([[ + vim.g.syntax_on = false + vim.lsp.util.open_floating_preview({ '```lua', 'local foo', '```' }, 'markdown', { + border = 'single', + focus = false, + }) + ]]) + screen:expect([[ + ^ | + ┌─────────┐{1: }| + │{100:local}{101: }{102:foo}│{1: }| + └─────────┘{1: }| + {1:~ }|*9 + | + ]]) + feed('wG') + screen:expect([[ + | + ┌─────────┐{1: }| + │{101:^```}{4: }│{1: }| + └─────────┘{1: }| + {1:~ }|*9 + | + ]]) + command('close | set conceallevel=2') + feed('') + exec_lua([[ + vim.lsp.util.open_floating_preview({ '```lua', 'local foo', '```' }, 'markdown', { + border = 'single', + focus = false, + }) + ]]) + screen:expect([[ + ^ | + ┌─────────┐{1: }| + │{100:local}{101: }{102:foo}│{1: }| + └─────────┘{1: }| + {1:~ }|*9 + | + ]]) + exec_lua([[ + vim.cmd.only() + vim.lsp.util.open_floating_preview({ 'foo', '```lua', 'local bar', '```' }, 'markdown', { + border = 'single', + focus = false, + }) + ]]) + feed('wG') + screen:expect([[ + | + ┌─────────┐{1: }| + │{100:local}{101: }{102:bar}│{1: }| + │{101:^```}{4: }│{1: }| + └─────────┘{1: }| + {1:~ }|*8 + | + ]]) + end) + + it('open_floating_preview height does not exceed max_height', function() + local screen = Screen.new() + exec_lua([[ + vim.lsp.util.open_floating_preview(vim.fn.range(1, 10), 'markdown', { + border = 'single', + width = 5, + max_height = 5, + focus = false, + }) + ]]) + screen:expect([[ + ^ | + ┌─────┐{1: }| + │{4:1 }│{1: }| + │{4:2 }│{1: }| + │{4:3 }│{1: }| + │{4:4 }│{1: }| + │{4:5 }│{1: }| + └─────┘{1: }| + {1:~ }|*5 + | + ]]) + end) + end) +end) diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua deleted file mode 100644 index f6c4b0be88..0000000000 --- a/test/functional/plugin/lsp/utils_spec.lua +++ /dev/null @@ -1,437 +0,0 @@ -local t = require('test.testutil') -local n = require('test.functional.testnvim')() -local Screen = require('test.functional.ui.screen') - -local feed = n.feed -local eq = t.eq -local exec_lua = n.exec_lua -local command, api = n.command, n.api -local pcall_err = t.pcall_err - -describe('vim.lsp.util', function() - before_each(n.clear) - - describe('stylize_markdown', function() - local stylize_markdown = function(content, opts) - return exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - vim.fn.bufload(bufnr) - return vim.lsp.util.stylize_markdown(bufnr, content, opts) - end) - end - - it('code fences', function() - local lines = { - '```lua', - "local hello = 'world'", - '```', - } - local expected = { - "local hello = 'world'", - } - local opts = {} - eq(expected, stylize_markdown(lines, opts)) - end) - - it('code fences with whitespace surrounded info string', function() - local lines = { - '``` lua ', - "local hello = 'world'", - '```', - } - local expected = { - "local hello = 'world'", - } - local opts = {} - eq(expected, stylize_markdown(lines, opts)) - end) - - it('adds separator after code block', function() - local lines = { - '```lua', - "local hello = 'world'", - '```', - '', - 'something', - } - local expected = { - "local hello = 'world'", - '─────────────────────', - 'something', - } - local opts = { separator = true } - eq(expected, stylize_markdown(lines, opts)) - end) - - it('replaces supported HTML entities', function() - local lines = { - '1 < 2', - '3 > 2', - '"quoted"', - ''apos'', - '   ', - '&', - } - local expected = { - '1 < 2', - '3 > 2', - '"quoted"', - "'apos'", - ' ', - '&', - } - local opts = {} - eq(expected, stylize_markdown(lines, opts)) - end) - end) - - it('convert_input_to_markdown_lines', function() - local r = exec_lua(function() - local hover_data = { - kind = 'markdown', - value = '```lua\nfunction vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)\n -> boolean\n```\n\n---\n\n Activates buffer-update events. Example:\n\n\n\n ```lua\n events = {}\n vim.api.nvim_buf_attach(0, false, {\n on_lines = function(...)\n table.insert(events, {...})\n end,\n })\n ```\n\n\n @see `nvim_buf_detach()`\n @see `api-buffer-updates-lua`\n@*param* `buffer` — Buffer handle, or 0 for current buffer\n\n\n\n@*param* `send_buffer` — True if whole buffer.\n Else the first notification will be `nvim_buf_changedtick_event`.\n\n\n@*param* `opts` — Optional parameters.\n\n - on_lines: Lua callback. Args:\n - the string "lines"\n - buffer handle\n - b:changedtick\n@*return* — False if foo;\n\n otherwise True.\n\n@see foo\n@see bar\n\n', - } - return vim.lsp.util.convert_input_to_markdown_lines(hover_data) - end) - local expected = { - '```lua', - 'function vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)', - ' -> boolean', - '```', - '', - '---', - '', - ' Activates buffer-update events. Example:', - '', - '', - '', - ' ```lua', - ' events = {}', - ' vim.api.nvim_buf_attach(0, false, {', - ' on_lines = function(...)', - ' table.insert(events, {...})', - ' end,', - ' })', - ' ```', - '', - '', - ' @see `nvim_buf_detach()`', - ' @see `api-buffer-updates-lua`', - '', - -- For each @param/@return: #30695 - -- - Separate each by one empty line. - -- - Remove all other blank lines. - '@*param* `buffer` — Buffer handle, or 0 for current buffer', - '', - '@*param* `send_buffer` — True if whole buffer.', - ' Else the first notification will be `nvim_buf_changedtick_event`.', - '', - '@*param* `opts` — Optional parameters.', - ' - on_lines: Lua callback. Args:', - ' - the string "lines"', - ' - buffer handle', - ' - b:changedtick', - '', - '@*return* — False if foo;', - ' otherwise True.', - '@see foo', - '@see bar', - } - eq(expected, r) - end) - - describe('_normalize_markdown', function() - it('collapses consecutive blank lines', function() - local result = exec_lua(function() - local lines = { - 'foo', - '', - '', - '', - 'bar', - '', - 'baz', - } - return vim.lsp.util._normalize_markdown(lines) - end) - local expected = { 'foo', '', 'bar', '', 'baz' } - eq(expected, result) - end) - - it('removes preceding and trailing empty lines', function() - local result = exec_lua(function() - local lines = { - '', - 'foo', - 'bar', - '', - '', - } - return vim.lsp.util._normalize_markdown(lines) - end) - local expected = { 'foo', 'bar' } - eq(expected, result) - end) - end) - - describe('make_floating_popup_options', function() - local function assert_anchor(anchor_bias, expected_anchor) - local opts = exec_lua(function() - return vim.lsp.util.make_floating_popup_options(30, 10, { anchor_bias = anchor_bias }) - end) - - eq(expected_anchor, string.sub(opts.anchor, 1, 1)) - end - - before_each(function() - local _ = Screen.new(80, 80) - feed('79i') -- fill screen with empty lines - end) - - describe('when on the first line it places window below', function() - before_each(function() - feed('gg') - end) - - it('for anchor_bias = "auto"', function() - assert_anchor('auto', 'N') - end) - - it('for anchor_bias = "above"', function() - assert_anchor('above', 'N') - end) - - it('for anchor_bias = "below"', function() - assert_anchor('below', 'N') - end) - end) - - describe('when on the last line it places window above', function() - before_each(function() - feed('G') - end) - - it('for anchor_bias = "auto"', function() - assert_anchor('auto', 'S') - end) - - it('for anchor_bias = "above"', function() - assert_anchor('above', 'S') - end) - - it('for anchor_bias = "below"', function() - assert_anchor('below', 'S') - end) - end) - - describe('with 20 lines above, 59 lines below', function() - before_each(function() - feed('gg20j') - end) - - it('places window below for anchor_bias = "auto"', function() - assert_anchor('auto', 'N') - end) - - it('places window above for anchor_bias = "above"', function() - assert_anchor('above', 'S') - end) - - it('places window below for anchor_bias = "below"', function() - assert_anchor('below', 'N') - end) - end) - - describe('with 59 lines above, 20 lines below', function() - before_each(function() - feed('G20k') - end) - - it('places window above for anchor_bias = "auto"', function() - assert_anchor('auto', 'S') - end) - - it('places window above for anchor_bias = "above"', function() - assert_anchor('above', 'S') - end) - - it('places window below for anchor_bias = "below"', function() - assert_anchor('below', 'N') - end) - - it('bordered window truncates dimensions correctly', function() - local opts = exec_lua(function() - return vim.lsp.util.make_floating_popup_options(100, 100, { border = 'single' }) - end) - - eq(56, opts.height) - end) - - it('title with winborder option #35179', function() - local opts = exec_lua(function() - vim.o.winborder = 'single' - return vim.lsp.util.make_floating_popup_options(100, 100, { title = 'Title' }) - end) - eq('Title', opts.title) - end) - end) - end) - - describe('open_floating_preview', function() - before_each(function() - Screen.new(10, 10) - feed('9iG4k') - end) - - local var_name = 'lsp_floating_preview' - local curbuf = api.nvim_get_current_buf() - - it('clean bufvar after fclose', function() - exec_lua(function() - vim.lsp.util.open_floating_preview({ 'test' }, '', { height = 5, width = 2 }) - end) - eq(true, api.nvim_win_is_valid(api.nvim_buf_get_var(curbuf, var_name))) - command('fclose') - eq('Key not found: lsp_floating_preview', pcall_err(api.nvim_buf_get_var, curbuf, var_name)) - end) - - it('clean bufvar after CursorMoved', function() - local result, winfixbuf = exec_lua(function() - vim.lsp.util.open_floating_preview({ 'test' }, '', { height = 5, width = 2 }) - local winnr = vim.b[vim.api.nvim_get_current_buf()].lsp_floating_preview - local result = vim.api.nvim_win_is_valid(winnr) - local winfixbuf = vim.wo[winnr].winfixbuf - vim.api.nvim_feedkeys(vim.keycode('G'), 'txn', false) - return result, winfixbuf - end) - eq(true, result) - eq(true, winfixbuf) - eq('Key not found: lsp_floating_preview', pcall_err(api.nvim_buf_get_var, curbuf, var_name)) - end) - end) - - it('open_floating_preview zindex greater than current window', function() - local screen = Screen.new() - exec_lua(function() - vim.api.nvim_open_win(0, true, { - relative = 'editor', - border = 'single', - height = 11, - width = 51, - row = 2, - col = 2, - }) - vim.keymap.set('n', 'K', function() - vim.lsp.util.open_floating_preview({ 'foo' }, '', { border = 'single' }) - end, {}) - end) - feed('K') - screen:expect([[ - ┌───────────────────────────────────────────────────┐| - │{4:^ }│| - │┌───┐{11: }│| - ││{4:foo}│{11: }│| - │└───┘{11: }│| - │{11:~ }│|*7 - └───────────────────────────────────────────────────┘| - | - ]]) - end) - - it('open_floating_preview height reduced for concealed lines', function() - local screen = Screen.new() - screen:add_extra_attr_ids({ - [100] = { - background = Screen.colors.LightMagenta, - foreground = Screen.colors.Brown, - bold = true, - }, - [101] = { background = Screen.colors.LightMagenta, foreground = Screen.colors.Blue }, - [102] = { background = Screen.colors.LightMagenta, foreground = Screen.colors.DarkCyan }, - }) - exec_lua([[ - vim.g.syntax_on = false - vim.lsp.util.open_floating_preview({ '```lua', 'local foo', '```' }, 'markdown', { - border = 'single', - focus = false, - }) - ]]) - screen:expect([[ - ^ | - ┌─────────┐{1: }| - │{100:local}{101: }{102:foo}│{1: }| - └─────────┘{1: }| - {1:~ }|*9 - | - ]]) - -- Entering window keeps lines concealed and doesn't end up below inner window size. - feed('wG') - screen:expect([[ - | - ┌─────────┐{1: }| - │{101:^```}{4: }│{1: }| - └─────────┘{1: }| - {1:~ }|*9 - | - ]]) - -- Correct height when float inherits 'conceallevel' >= 2 #32639 - command('close | set conceallevel=2') - feed('') -- Prevent CursorMoved closing the next float immediately - exec_lua([[ - vim.lsp.util.open_floating_preview({ '```lua', 'local foo', '```' }, 'markdown', { - border = 'single', - focus = false, - }) - ]]) - screen:expect([[ - ^ | - ┌─────────┐{1: }| - │{100:local}{101: }{102:foo}│{1: }| - └─────────┘{1: }| - {1:~ }|*9 - | - ]]) - -- This tests the valid winline code path (why doesn't the above?). - exec_lua([[ - vim.cmd.only() - vim.lsp.util.open_floating_preview({ 'foo', '```lua', 'local bar', '```' }, 'markdown', { - border = 'single', - focus = false, - }) - ]]) - feed('wG') - screen:expect([[ - | - ┌─────────┐{1: }| - │{100:local}{101: }{102:bar}│{1: }| - │{101:^```}{4: }│{1: }| - └─────────┘{1: }| - {1:~ }|*8 - | - ]]) - end) - - it('open_floating_preview height does not exceed max_height', function() - local screen = Screen.new() - exec_lua([[ - vim.lsp.util.open_floating_preview(vim.fn.range(1, 10), 'markdown', { - border = 'single', - width = 5, - max_height = 5, - focus = false, - }) - ]]) - screen:expect([[ - ^ | - ┌─────┐{1: }| - │{4:1 }│{1: }| - │{4:2 }│{1: }| - │{4:3 }│{1: }| - │{4:4 }│{1: }| - │{4:5 }│{1: }| - └─────┘{1: }| - {1:~ }|*5 - | - ]]) - end) -end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 26e29d7fbe..84eb63beea 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -2808,2611 +2808,6 @@ describe('LSP', function() end) end) - describe('lsp.util.rename', function() - local pathsep = n.get_pathsep() - - it('can rename an existing file', function() - local old = tmpname() - write_file(old, 'Test content') - local new = tmpname(false) - local lines = exec_lua(function() - local old_bufnr = vim.fn.bufadd(old) - vim.fn.bufload(old_bufnr) - vim.lsp.util.rename(old, new) - -- the existing buffer is renamed in-place and its contents is kept - local new_bufnr = vim.fn.bufadd(new) - vim.fn.bufload(new_bufnr) - return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) - end) - eq({ 'Test content' }, lines) - local exists = vim.uv.fs_stat(old) ~= nil - eq(false, exists) - exists = vim.uv.fs_stat(new) ~= nil - eq(true, exists) - os.remove(new) - end) - - it('can rename a directory', function() - -- only reserve the name, file must not exist for the test scenario - local old_dir = tmpname(false) - local new_dir = tmpname(false) - - n.mkdir_p(old_dir) - - local file = 'file.txt' - write_file(old_dir .. pathsep .. file, 'Test content') - - local lines = exec_lua(function() - local old_bufnr = vim.fn.bufadd(old_dir .. pathsep .. file) - vim.fn.bufload(old_bufnr) - vim.lsp.util.rename(old_dir, new_dir) - -- the existing buffer is renamed in-place and its contents is kept - local new_bufnr = vim.fn.bufadd(new_dir .. pathsep .. file) - vim.fn.bufload(new_bufnr) - return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) - end) - eq({ 'Test content' }, lines) - eq(false, vim.uv.fs_stat(old_dir) ~= nil) - eq(true, vim.uv.fs_stat(new_dir) ~= nil) - eq(true, vim.uv.fs_stat(new_dir .. pathsep .. file) ~= nil) - - os.remove(new_dir) - end) - - it('does not touch buffers that do not match path prefix', function() - local old = tmpname(false) - local new = tmpname(false) - n.mkdir_p(old) - - eq( - true, - exec_lua(function() - local old_prefixed = 'explorer://' .. old - local old_suffixed = old .. '.bak' - local new_prefixed = 'explorer://' .. new - local new_suffixed = new .. '.bak' - - local old_prefixed_buf = vim.fn.bufadd(old_prefixed) - local old_suffixed_buf = vim.fn.bufadd(old_suffixed) - local new_prefixed_buf = vim.fn.bufadd(new_prefixed) - local new_suffixed_buf = vim.fn.bufadd(new_suffixed) - - vim.lsp.util.rename(old, new) - - return vim.api.nvim_buf_is_valid(old_prefixed_buf) - and vim.api.nvim_buf_is_valid(old_suffixed_buf) - and vim.api.nvim_buf_is_valid(new_prefixed_buf) - and vim.api.nvim_buf_is_valid(new_suffixed_buf) - and vim.api.nvim_buf_get_name(old_prefixed_buf) == old_prefixed - and vim.api.nvim_buf_get_name(old_suffixed_buf) == old_suffixed - and vim.api.nvim_buf_get_name(new_prefixed_buf) == new_prefixed - and vim.api.nvim_buf_get_name(new_suffixed_buf) == new_suffixed - end) - ) - - os.remove(new) - end) - - it( - 'does not rename file if target exists and ignoreIfExists is set or overwrite is false', - function() - local old = tmpname() - write_file(old, 'Old File') - local new = tmpname() - write_file(new, 'New file') - - exec_lua(function() - vim.lsp.util.rename(old, new, { ignoreIfExists = true }) - end) - - eq(true, vim.uv.fs_stat(old) ~= nil) - eq('New file', read_file(new)) - - exec_lua(function() - vim.lsp.util.rename(old, new, { overwrite = false }) - end) - - eq(true, vim.uv.fs_stat(old) ~= nil) - eq('New file', read_file(new)) - end - ) - - it('maintains undo information for loaded buffer', function() - local old = tmpname() - write_file(old, 'line') - local new = tmpname(false) - - local undo_kept = exec_lua(function() - vim.opt.undofile = true - vim.cmd.edit(old) - vim.cmd.normal('dd') - vim.cmd.write() - local undotree = vim.fn.undotree() - vim.lsp.util.rename(old, new) - -- Renaming uses :saveas, which updates the "last write" information. - -- Other than that, the undotree should remain the same. - undotree.save_cur = undotree.save_cur + 1 - undotree.save_last = undotree.save_last + 1 - undotree.entries[1].save = undotree.entries[1].save + 1 - return vim.deep_equal(undotree, vim.fn.undotree()) - end) - eq(false, vim.uv.fs_stat(old) ~= nil) - eq(true, vim.uv.fs_stat(new) ~= nil) - eq(true, undo_kept) - end) - - it('maintains undo information for unloaded buffer', function() - local old = tmpname() - write_file(old, 'line') - local new = tmpname(false) - - local undo_kept = exec_lua(function() - vim.opt.undofile = true - vim.cmd.split(old) - vim.cmd.normal('dd') - vim.cmd.write() - local undotree = vim.fn.undotree() - vim.cmd.bdelete() - vim.lsp.util.rename(old, new) - vim.cmd.edit(new) - return vim.deep_equal(undotree, vim.fn.undotree()) - end) - eq(false, vim.uv.fs_stat(old) ~= nil) - eq(true, vim.uv.fs_stat(new) ~= nil) - eq(true, undo_kept) - end) - - it('does not rename file when it conflicts with a buffer without file', function() - local old = tmpname() - write_file(old, 'Old File') - local new = tmpname(false) - - local lines = exec_lua(function() - local old_buf = vim.fn.bufadd(old) - vim.fn.bufload(old_buf) - local conflict_buf = vim.api.nvim_create_buf(true, false) - vim.api.nvim_buf_set_name(conflict_buf, new) - vim.api.nvim_buf_set_lines(conflict_buf, 0, -1, true, { 'conflict' }) - vim.api.nvim_win_set_buf(0, conflict_buf) - vim.lsp.util.rename(old, new) - return vim.api.nvim_buf_get_lines(conflict_buf, 0, -1, true) - end) - eq({ 'conflict' }, lines) - eq('Old File', read_file(old)) - end) - - it('does override target if overwrite is true', function() - local old = tmpname() - write_file(old, 'Old file') - local new = tmpname() - write_file(new, 'New file') - exec_lua(function() - vim.lsp.util.rename(old, new, { overwrite = true }) - end) - - eq(false, vim.uv.fs_stat(old) ~= nil) - eq(true, vim.uv.fs_stat(new) ~= nil) - eq('Old file', read_file(new)) - end) - end) - - describe('lsp.util.locations_to_items', function() - it('convert Location[] to items', function() - local expected_template = { - { - filename = '/fake/uri', - lnum = 1, - end_lnum = 2, - col = 3, - end_col = 4, - text = 'testing', - user_data = {}, - }, - } - local test_params = { - { - { - uri = 'file:///fake/uri', - range = { - start = { line = 0, character = 2 }, - ['end'] = { line = 1, character = 3 }, - }, - }, - }, - { - { - uri = 'file:///fake/uri', - range = { - start = { line = 0, character = 2 }, - -- LSP spec: if character > line length, default to the line length. - ['end'] = { line = 1, character = 10000 }, - }, - }, - }, - } - for _, params in ipairs(test_params) do - local actual = exec_lua(function(params0) - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - local lines = { 'testing', '123' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - return vim.lsp.util.locations_to_items(params0, 'utf-16') - end, params) - local expected = vim.deepcopy(expected_template) - expected[1].user_data = params[1] - eq(expected, actual) - end - end) - - it('convert LocationLink[] to items', function() - local expected = { - { - filename = '/fake/uri', - lnum = 1, - end_lnum = 1, - col = 3, - end_col = 4, - text = 'testing', - user_data = { - targetUri = 'file:///fake/uri', - targetRange = { - start = { line = 0, character = 2 }, - ['end'] = { line = 0, character = 3 }, - }, - targetSelectionRange = { - start = { line = 0, character = 2 }, - ['end'] = { line = 0, character = 3 }, - }, - }, - }, - } - local actual = exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - local lines = { 'testing', '123' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - local locations = { - { - targetUri = vim.uri_from_bufnr(bufnr), - targetRange = { - start = { line = 0, character = 2 }, - ['end'] = { line = 0, character = 3 }, - }, - targetSelectionRange = { - start = { line = 0, character = 2 }, - ['end'] = { line = 0, character = 3 }, - }, - }, - } - return vim.lsp.util.locations_to_items(locations, 'utf-16') - end) - eq(expected, actual) - end) - end) - - describe('lsp.util.symbols_to_items', function() - describe('convert DocumentSymbol[] to items', function() - it('documentSymbol has children', function() - local expected = { - { - col = 1, - end_col = 1, - end_lnum = 2, - filename = '', - kind = 'File', - lnum = 2, - text = '[File] TestA', - }, - { - col = 1, - end_col = 1, - end_lnum = 4, - filename = '', - kind = 'Module', - lnum = 4, - text = '[Module] TestB', - }, - { - col = 1, - end_col = 1, - end_lnum = 6, - filename = '', - kind = 'Namespace', - lnum = 6, - text = '[Namespace] TestC', - }, - } - eq( - expected, - exec_lua(function() - local doc_syms = { - { - deprecated = false, - detail = 'A', - kind = 1, - name = 'TestA', - range = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 0, - line = 2, - }, - }, - selectionRange = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 4, - line = 1, - }, - }, - children = { - { - children = {}, - deprecated = false, - detail = 'B', - kind = 2, - name = 'TestB', - range = { - start = { - character = 0, - line = 3, - }, - ['end'] = { - character = 0, - line = 4, - }, - }, - selectionRange = { - start = { - character = 0, - line = 3, - }, - ['end'] = { - character = 4, - line = 3, - }, - }, - }, - }, - }, - { - deprecated = false, - detail = 'C', - kind = 3, - name = 'TestC', - range = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 0, - line = 6, - }, - }, - selectionRange = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 4, - line = 5, - }, - }, - }, - } - return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') - end) - ) - end) - - it('documentSymbol has no children', function() - local expected = { - { - col = 1, - end_col = 1, - end_lnum = 2, - filename = '', - kind = 'File', - lnum = 2, - text = '[File] TestA', - }, - { - col = 1, - end_col = 1, - end_lnum = 6, - filename = '', - kind = 'Namespace', - lnum = 6, - text = '[Namespace] TestC', - }, - } - eq( - expected, - exec_lua(function() - local doc_syms = { - { - deprecated = false, - detail = 'A', - kind = 1, - name = 'TestA', - range = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 0, - line = 2, - }, - }, - selectionRange = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 4, - line = 1, - }, - }, - }, - { - deprecated = false, - detail = 'C', - kind = 3, - name = 'TestC', - range = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 0, - line = 6, - }, - }, - selectionRange = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 4, - line = 5, - }, - }, - }, - } - return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') - end) - ) - end) - - it('handles deprecated items', function() - local expected = { - { - col = 1, - end_col = 1, - end_lnum = 2, - filename = '', - kind = 'File', - lnum = 2, - text = '[File] TestA (deprecated)', - }, - { - col = 1, - end_col = 1, - end_lnum = 6, - filename = '', - kind = 'Namespace', - lnum = 6, - text = '[Namespace] TestC (deprecated)', - }, - } - eq( - expected, - exec_lua(function() - local doc_syms = { - { - deprecated = true, - detail = 'A', - kind = 1, - name = 'TestA', - range = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 0, - line = 2, - }, - }, - selectionRange = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 4, - line = 1, - }, - }, - }, - { - detail = 'C', - kind = 3, - name = 'TestC', - range = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 0, - line = 6, - }, - }, - selectionRange = { - start = { - character = 0, - line = 5, - }, - ['end'] = { - character = 4, - line = 5, - }, - }, - tags = { 1 }, -- deprecated - }, - } - return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') - end) - ) - end) - end) - - it('convert SymbolInformation[] to items', function() - local expected = { - { - col = 1, - end_col = 1, - end_lnum = 3, - filename = '/test_a', - kind = 'File', - lnum = 2, - text = '[File] TestA in TestAContainer', - }, - { - col = 1, - end_col = 1, - end_lnum = 5, - filename = '/test_b', - kind = 'Module', - lnum = 4, - text = '[Module] TestB in TestBContainer (deprecated)', - }, - } - eq( - expected, - exec_lua(function() - local sym_info = { - { - deprecated = false, - kind = 1, - name = 'TestA', - location = { - range = { - start = { - character = 0, - line = 1, - }, - ['end'] = { - character = 0, - line = 2, - }, - }, - uri = 'file:///test_a', - }, - containerName = 'TestAContainer', - }, - { - deprecated = true, - kind = 2, - name = 'TestB', - location = { - range = { - start = { - character = 0, - line = 3, - }, - ['end'] = { - character = 0, - line = 4, - }, - }, - uri = 'file:///test_b', - }, - containerName = 'TestBContainer', - }, - } - return vim.lsp.util.symbols_to_items(sym_info, nil, 'utf-16') - end) - ) - end) - end) - - describe('lsp.util.jump_to_location', function() - local target_bufnr --- @type integer - - before_each(function() - target_bufnr = exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - return bufnr - end) - end) - - local location = function(start_line, start_char, end_line, end_char) - return { - uri = 'file:///fake/uri', - range = { - start = { line = start_line, character = start_char }, - ['end'] = { line = end_line, character = end_char }, - }, - } - end - - local jump = function(msg) - eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg, 'utf-16')) - eq(target_bufnr, fn.bufnr('%')) - return { - line = fn.line('.'), - col = fn.col('.'), - } - end - - it('jumps to a Location', function() - local pos = jump(location(0, 9, 0, 9)) - eq(1, pos.line) - eq(10, pos.col) - end) - - it('jumps to a LocationLink', function() - local pos = jump({ - targetUri = 'file:///fake/uri', - targetSelectionRange = { - start = { line = 0, character = 4 }, - ['end'] = { line = 0, character = 4 }, - }, - targetRange = { - start = { line = 1, character = 5 }, - ['end'] = { line = 1, character = 5 }, - }, - }) - eq(1, pos.line) - eq(5, pos.col) - end) - - it('jumps to the correct multibyte column', function() - local pos = jump(location(1, 2, 1, 2)) - eq(2, pos.line) - eq(4, pos.col) - eq('å', fn.expand('')) - end) - - it('adds current position to jumplist before jumping', function() - api.nvim_win_set_buf(0, target_bufnr) - local mark = api.nvim_buf_get_mark(target_bufnr, "'") - eq({ 1, 0 }, mark) - - api.nvim_win_set_cursor(0, { 2, 3 }) - jump(location(0, 9, 0, 9)) - - mark = api.nvim_buf_get_mark(target_bufnr, "'") - eq({ 2, 3 }, mark) - end) - end) - - describe('lsp.util.show_document', function() - local target_bufnr --- @type integer - local target_bufnr2 --- @type integer - - before_each(function() - target_bufnr = exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - return bufnr - end) - - target_bufnr2 = exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri2') - local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - return bufnr - end) - end) - - local location = function(start_line, start_char, end_line, end_char, second_uri) - return { - uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri', - range = { - start = { line = start_line, character = start_char }, - ['end'] = { line = end_line, character = end_char }, - }, - } - end - - local show_document = function(msg, focus, reuse_win) - eq( - true, - exec_lua( - 'return vim.lsp.util.show_document(...)', - msg, - 'utf-16', - { reuse_win = reuse_win, focus = focus } - ) - ) - if focus == true or focus == nil then - eq(target_bufnr, fn.bufnr('%')) - end - return { - line = fn.line('.'), - col = fn.col('.'), - } - end - - it('jumps to a Location if focus is true', function() - local pos = show_document(location(0, 9, 0, 9), true, true) - eq(1, pos.line) - eq(10, pos.col) - - -- expectation: Cursor is placed past EOL (append position) in insert mode - n.feed('I') - pos = show_document(location(0, 16, 0, 16), true, true) - eq(1, pos.line) - eq(17, pos.col) - eq('i', api.nvim_get_mode().mode) - end) - - it('jumps to a Location if focus is true via handler', function() - exec_lua(create_server_definition) - local result = exec_lua(function() - local server = _G._create_server() - local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) - local result = { - uri = 'file:///fake/uri', - selection = { - start = { line = 0, character = 9 }, - ['end'] = { line = 0, character = 9 }, - }, - takeFocus = true, - } - local ctx = { - client_id = client_id, - method = 'window/showDocument', - } - vim.lsp.handlers['window/showDocument'](nil, result, ctx) - vim.lsp.get_client_by_id(client_id):stop() - return { - cursor = vim.api.nvim_win_get_cursor(0), - } - end) - eq(1, result.cursor[1]) - eq(9, result.cursor[2]) - end) - - it('jumps to a Location if focus not set', function() - local pos = show_document(location(0, 9, 0, 9), nil, true) - eq(1, pos.line) - eq(10, pos.col) - end) - - it('does not add current position to jumplist if not focus', function() - api.nvim_win_set_buf(0, target_bufnr) - local mark = api.nvim_buf_get_mark(target_bufnr, "'") - eq({ 1, 0 }, mark) - - api.nvim_win_set_cursor(0, { 2, 3 }) - show_document(location(0, 9, 0, 9), false, true) - show_document(location(0, 9, 0, 9, true), false, true) - - mark = api.nvim_buf_get_mark(target_bufnr, "'") - eq({ 1, 0 }, mark) - end) - - it('does not change cursor position if not focus and not reuse_win', function() - api.nvim_win_set_buf(0, target_bufnr) - local cursor = api.nvim_win_get_cursor(0) - - show_document(location(0, 9, 0, 9), false, false) - eq(cursor, api.nvim_win_get_cursor(0)) - end) - - it('does not change window if not focus', function() - api.nvim_win_set_buf(0, target_bufnr) - local win = api.nvim_get_current_win() - - -- same document/bufnr - show_document(location(0, 9, 0, 9), false, true) - eq(win, api.nvim_get_current_win()) - - -- different document/bufnr, new window/split - show_document(location(0, 9, 0, 9, true), false, true) - eq(2, #api.nvim_list_wins()) - eq(win, api.nvim_get_current_win()) - end) - - it("respects 'reuse_win' parameter", function() - api.nvim_win_set_buf(0, target_bufnr) - - -- does not create a new window if the buffer is already open - show_document(location(0, 9, 0, 9), false, true) - eq(1, #api.nvim_list_wins()) - - -- creates a new window even if the buffer is already open - show_document(location(0, 9, 0, 9), false, false) - eq(2, #api.nvim_list_wins()) - end) - - it('correctly sets the cursor of the split if range is given without focus', function() - api.nvim_win_set_buf(0, target_bufnr) - - show_document(location(0, 9, 0, 9, true), false, true) - - local wins = api.nvim_list_wins() - eq(2, #wins) - table.sort(wins) - - eq({ 1, 0 }, api.nvim_win_get_cursor(wins[1])) - eq({ 1, 9 }, api.nvim_win_get_cursor(wins[2])) - end) - - it('does not change cursor of the split if not range and not focus', function() - api.nvim_win_set_buf(0, target_bufnr) - api.nvim_win_set_cursor(0, { 2, 3 }) - - exec_lua(function() - vim.cmd.new() - end) - api.nvim_win_set_buf(0, target_bufnr2) - api.nvim_win_set_cursor(0, { 2, 3 }) - - show_document({ uri = 'file:///fake/uri2' }, false, true) - - local wins = api.nvim_list_wins() - eq(2, #wins) - eq({ 2, 3 }, api.nvim_win_get_cursor(wins[1])) - eq({ 2, 3 }, api.nvim_win_get_cursor(wins[2])) - end) - - it('respects existing buffers', function() - api.nvim_win_set_buf(0, target_bufnr) - local win = api.nvim_get_current_win() - - exec_lua(function() - vim.cmd.new() - end) - api.nvim_win_set_buf(0, target_bufnr2) - api.nvim_win_set_cursor(0, { 2, 3 }) - local split = api.nvim_get_current_win() - - -- reuse win for open document/bufnr if called from split - show_document(location(0, 9, 0, 9, true), false, true) - eq({ 1, 9 }, api.nvim_win_get_cursor(split)) - eq(2, #api.nvim_list_wins()) - - api.nvim_set_current_win(win) - - -- reuse win for open document/bufnr if called outside the split - show_document(location(0, 9, 0, 9, true), false, true) - eq({ 1, 9 }, api.nvim_win_get_cursor(split)) - eq(2, #api.nvim_list_wins()) - end) - end) - - describe('lsp.util._make_floating_popup_size', function() - before_each(function() - exec_lua(function() - _G.contents = { 'text tαxt txtα tex', 'text tααt tααt text', 'text tαxt tαxt' } - end) - end) - - it('calculates size correctly', function() - eq( - { 19, 3 }, - exec_lua(function() - return { vim.lsp.util._make_floating_popup_size(_G.contents) } - end) - ) - end) - - it('calculates size correctly with wrapping', function() - eq( - { 15, 5 }, - exec_lua(function() - return { - vim.lsp.util._make_floating_popup_size(_G.contents, { width = 15, wrap_at = 14 }), - } - end) - ) - end) - - it('handles NUL bytes in text', function() - exec_lua(function() - _G.contents = { - '\000\001\002\003\004\005\006\007\008\009', - '\010\011\012\013\014\015\016\017\018\019', - '\020\021\022\023\024\025\026\027\028\029', - } - end) - command('set list listchars=') - eq( - { 20, 3 }, - exec_lua(function() - return { vim.lsp.util._make_floating_popup_size(_G.contents) } - end) - ) - command('set display+=uhex') - eq( - { 40, 3 }, - exec_lua(function() - return { vim.lsp.util._make_floating_popup_size(_G.contents) } - end) - ) - end) - it('handles empty line', function() - exec_lua(function() - _G.contents = { - '', - } - end) - eq( - { 20, 1 }, - exec_lua(function() - return { vim.lsp.util._make_floating_popup_size(_G.contents, { width = 20 }) } - end) - ) - end) - - it('considers string title when computing width', function() - eq( - { 17, 2 }, - exec_lua(function() - return { - vim.lsp.util._make_floating_popup_size( - { 'foo', 'bar' }, - { title = 'A very long title' } - ), - } - end) - ) - end) - - it('considers [string,string][] title when computing width', function() - eq( - { 17, 2 }, - exec_lua(function() - return { - vim.lsp.util._make_floating_popup_size( - { 'foo', 'bar' }, - { title = { { 'A very ', 'Normal' }, { 'long title', 'Normal' } } } - ), - } - end) - ) - end) - end) - - describe('lsp.util.trim.trim_empty_lines', function() - it('properly trims empty lines', function() - eq( - { { 'foo', 'bar' } }, - exec_lua(function() - --- @diagnostic disable-next-line:deprecated - return vim.lsp.util.trim_empty_lines({ { 'foo', 'bar' }, nil }) - end) - ) - end) - end) - - describe('lsp.util.convert_signature_help_to_markdown_lines', function() - it('can handle negative activeSignature', function() - local result = exec_lua(function() - local signature_help = { - activeParameter = 0, - activeSignature = -1, - signatures = { - { - documentation = 'some doc', - label = 'TestEntity.TestEntity()', - parameters = {}, - }, - }, - } - return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', { ',' }) - end) - local expected = { '```cs', 'TestEntity.TestEntity()', '```', '---', 'some doc' } - eq(expected, result) - end) - - it('highlights active parameters in multiline signature labels', function() - local _, hl = exec_lua(function() - local signature_help = { - activeSignature = 0, - signatures = { - { - activeParameter = 1, - label = 'fn bar(\n _: void,\n _: void,\n) void', - parameters = { - { label = '_: void' }, - { label = '_: void' }, - }, - }, - }, - } - return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'zig', { '(' }) - end) - -- Note that although the highlight positions below are 0-indexed, the 2nd parameter - -- corresponds to the 3rd line because the first line is the ``` from the - -- Markdown block. - local expected = { 3, 4, 3, 11 } - eq(expected, hl) - end) - end) - - describe('lsp.util.get_effective_tabstop', function() - local function test_tabstop(tabsize, shiftwidth) - exec_lua(string.format( - [[ - vim.bo.shiftwidth = %d - vim.bo.tabstop = 2 - ]], - shiftwidth - )) - eq( - tabsize, - exec_lua(function() - return vim.lsp.util.get_effective_tabstop() - end) - ) - end - - it('with shiftwidth = 1', function() - test_tabstop(1, 1) - end) - - it('with shiftwidth = 0', function() - test_tabstop(2, 0) - end) - 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'] - 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 = '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('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.tagfunc', function() before_each(function() ---@type lsp.Location[] @@ -7227,183 +4622,4 @@ describe('LSP', function() eq({ foo = true, bar = false }, get_resolved({ 'bar', 'foo' })) 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', n.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('', n.exec_capture('lua vim.lsp.buf.hover()')) - end) - end) end) diff --git a/test/functional/treesitter/utils_spec.lua b/test/functional/treesitter/util_spec.lua similarity index 100% rename from test/functional/treesitter/utils_spec.lua rename to test/functional/treesitter/util_spec.lua