local t = require('test.testutil') local n = require('test.functional.testnvim')() local t_lsp = require('test.functional.plugin.lsp.testutil') local clear = n.clear local exec_lua = n.exec_lua local eq = t.eq local neq = t.neq local create_server_definition = t_lsp.create_server_definition describe('vim.lsp.diagnostic', function() local fake_uri --- @type string local client_id --- @type integer local diagnostic_bufnr --- @type integer before_each(function() clear { env = { NVIM_LUA_NOTRACK = '1', VIMRUNTIME = os.getenv 'VIMRUNTIME', } } exec_lua(function() require('vim.lsp') _G.make_range = function(x1, y1, x2, y2) return { start = { line = x1, character = y1 }, ['end'] = { line = x2, character = y2 } } end _G.make_error = function(msg, x1, y1, x2, y2) return { range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 1, } end _G.make_warning = function(msg, x1, y1, x2, y2) return { range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 2, } end _G.make_information = function(msg, x1, y1, x2, y2) return { range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 3, } end function _G.get_extmarks(bufnr, client_id0) local namespace = vim.lsp.diagnostic.get_namespace(client_id0) local ns = vim.diagnostic.get_namespace(namespace) local extmarks = {} if ns.user_data.virt_text_ns then for _, e in pairs( vim.api.nvim_buf_get_extmarks( bufnr, ns.user_data.virt_text_ns, 0, -1, { details = true } ) ) do table.insert(extmarks, e) end end if ns.user_data.underline_ns then for _, e in pairs( vim.api.nvim_buf_get_extmarks( bufnr, ns.user_data.underline_ns, 0, -1, { details = true } ) ) do table.insert(extmarks, e) end end return extmarks end client_id = assert(vim.lsp.start({ cmd_env = { NVIM_LUA_NOTRACK = '1', }, cmd = { vim.v.progpath, '-es', '-u', 'NONE', '--headless', }, offset_encoding = 'utf-16', }, { attach = false })) end) fake_uri = 'file:///fake/uri' exec_lua(function() diagnostic_bufnr = vim.uri_to_bufnr(fake_uri) local lines = { '1st line of text', '2nd line of text', 'wow', 'cool', 'more', 'lines' } vim.fn.bufload(diagnostic_bufnr) vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, 1, false, lines) vim.api.nvim_win_set_buf(0, diagnostic_bufnr) end) end) after_each(function() clear() end) describe('vim.lsp.diagnostic.on_publish_diagnostics', function() it('correctly handles UTF-16 offsets', function() local line = 'All 💼 and no 🎉 makes Jack a dull 👦' local result = exec_lua(function() vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, -1, false, { line }) vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = fake_uri, diagnostics = { _G.make_error('UTF-16 Diagnostic', 0, 7, 0, 8), }, }, { client_id = client_id }) local diags = vim.diagnostic.get(diagnostic_bufnr) vim.lsp.stop_client(client_id) vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) return diags end) eq(1, #result) eq( exec_lua(function() return vim.str_byteindex(line, 'utf-16', 7) end), result[1].col ) eq( exec_lua(function() return vim.str_byteindex(line, 'utf-16', 8) end), result[1].end_col ) end) it('does not create buffer on empty diagnostics', function() -- No buffer is created without diagnostics eq( -1, exec_lua(function() vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = 'file:///fake/uri2', diagnostics = {}, }, { client_id = client_id }) return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2')) end) ) -- Create buffer on diagnostics neq( -1, exec_lua(function() vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = 'file:///fake/uri2', diagnostics = { _G.make_error('Diagnostic', 0, 0, 0, 0), }, }, { client_id = client_id }) return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2')) end) ) eq( 1, exec_lua(function() return #vim.diagnostic.get(_G.bufnr) end) ) -- Clear diagnostics after buffer was created neq( -1, exec_lua(function() vim.lsp.diagnostic.on_publish_diagnostics(nil, { uri = 'file:///fake/uri2', diagnostics = {}, }, { client_id = client_id }) return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2')) end) ) eq( 0, exec_lua(function() return #vim.diagnostic.get(_G.bufnr) end) ) end) end) describe('vim.lsp.diagnostic.on_diagnostic', function() before_each(function() exec_lua(create_server_definition) exec_lua(function() _G.requests = 0 _G.server = _G._create_server({ capabilities = { diagnosticProvider = {}, }, handlers = { [vim.lsp.protocol.Methods.textDocument_diagnostic] = function() _G.requests = _G.requests + 1 end, }, }) function _G.get_extmarks(bufnr, client_id0) local namespace = vim.lsp.diagnostic.get_namespace(client_id0, true) local ns = vim.diagnostic.get_namespace(namespace) local extmarks = {} if ns.user_data.virt_text_ns then for _, e in pairs( vim.api.nvim_buf_get_extmarks( bufnr, ns.user_data.virt_text_ns, 0, -1, { details = true } ) ) do table.insert(extmarks, e) end end if ns.user_data.underline_ns then for _, e in pairs( vim.api.nvim_buf_get_extmarks( bufnr, ns.user_data.underline_ns, 0, -1, { details = true } ) ) do table.insert(extmarks, e) end end return extmarks end client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) end) end) it('adds diagnostics to vim.diagnostics', function() local diags = exec_lua(function() vim.lsp.diagnostic.on_diagnostic(nil, { kind = 'full', items = { _G.make_error('Pull Diagnostic', 4, 4, 4, 4), }, }, { params = { textDocument = { uri = fake_uri }, }, uri = fake_uri, client_id = client_id, }, {}) return vim.diagnostic.get(diagnostic_bufnr) end) eq(1, #diags) eq('Pull Diagnostic', diags[1].message) end) it('severity defaults to error if missing', function() ---@type vim.Diagnostic[] local diagnostics = exec_lua(function() vim.lsp.diagnostic.on_diagnostic(nil, { kind = 'full', items = { { range = _G.make_range(4, 4, 4, 4), message = 'bad!', }, }, }, { params = { textDocument = { uri = fake_uri }, }, uri = fake_uri, client_id = client_id, }, {}) return vim.diagnostic.get(diagnostic_bufnr) end) eq(1, #diagnostics) eq(1, diagnostics[1].severity) end) it('clears diagnostics when client detaches', function() exec_lua(function() vim.lsp.diagnostic.on_diagnostic(nil, { kind = 'full', items = { _G.make_error('Pull Diagnostic', 4, 4, 4, 4), }, }, { params = { textDocument = { uri = fake_uri }, }, uri = fake_uri, client_id = client_id, }, {}) end) eq( 1, exec_lua(function() return #vim.diagnostic.get(diagnostic_bufnr) end) ) exec_lua(function() vim.lsp.stop_client(client_id) end) eq( 0, exec_lua(function() return #vim.diagnostic.get(diagnostic_bufnr) end) ) end) it('keeps diagnostics when one client detaches and others still are attached', function() local client_id2 exec_lua(function() client_id2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server.cmd }) vim.lsp.diagnostic.on_diagnostic(nil, { kind = 'full', items = { _G.make_error('Pull Diagnostic', 4, 4, 4, 4), }, }, { params = { textDocument = { uri = fake_uri }, }, uri = fake_uri, client_id = client_id, }, {}) end) eq( 1, exec_lua(function() return #vim.diagnostic.get(diagnostic_bufnr) end) ) exec_lua(function() vim.lsp.stop_client(client_id2) end) eq( 1, exec_lua(function() return #vim.diagnostic.get(diagnostic_bufnr) end) ) end) it('handles server cancellation', function() eq( 1, exec_lua(function() vim.lsp.diagnostic.on_diagnostic({ code = vim.lsp.protocol.ErrorCodes.ServerCancelled, -- Empty data defaults to retriggering request data = {}, message = '', }, {}, { method = vim.lsp.protocol.Methods.textDocument_diagnostic, client_id = client_id, }) return _G.requests end) ) eq( 2, exec_lua(function() vim.lsp.diagnostic.on_diagnostic({ code = vim.lsp.protocol.ErrorCodes.ServerCancelled, data = { retriggerRequest = true }, message = '', }, {}, { method = vim.lsp.protocol.Methods.textDocument_diagnostic, client_id = client_id, }) return _G.requests end) ) eq( 2, exec_lua(function() vim.lsp.diagnostic.on_diagnostic({ code = vim.lsp.protocol.ErrorCodes.ServerCancelled, data = { retriggerRequest = false }, message = '', }, {}, { method = vim.lsp.protocol.Methods.textDocument_diagnostic, client_id = client_id, }) return _G.requests end) ) end) end) end)