diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2024-11-19 22:57:13 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2024-11-19 22:57:13 +0000 |
| commit | 9be89f131f87608f224f0ee06d199fcd09d32176 (patch) | |
| tree | 11022dcfa9e08cb4ac5581b16734196128688d48 /test/functional/plugin/lsp | |
| parent | ff7ed8f586589d620a806c3758fac4a47a8e7e15 (diff) | |
| parent | 88085c2e80a7e3ac29aabb6b5420377eed99b8b6 (diff) | |
| download | rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.gz rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.bz2 rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.zip | |
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'test/functional/plugin/lsp')
| -rw-r--r-- | test/functional/plugin/lsp/codelens_spec.lua | 84 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/completion_spec.lua | 547 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/diagnostic_spec.lua | 536 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/handler_spec.lua | 23 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/incremental_sync_spec.lua | 91 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/inlay_hint_spec.lua | 335 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/semantic_tokens_spec.lua | 378 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/testutil.lua | 125 | ||||
| -rw-r--r-- | test/functional/plugin/lsp/utils_spec.lua | 104 |
9 files changed, 1463 insertions, 760 deletions
diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua index cd20e95dd1..20ef1cb49e 100644 --- a/test/functional/plugin/lsp/codelens_spec.lua +++ b/test/functional/plugin/lsp/codelens_spec.lua @@ -13,36 +13,34 @@ describe('vim.lsp.codelens', function() it('on_codelens_stores_and_displays_lenses', function() local fake_uri = 'file:///fake/uri' - local bufnr = exec_lua( - [[ - fake_uri = ... + local bufnr = exec_lua(function() local bufnr = vim.uri_to_bufnr(fake_uri) - local lines = {'So', 'many', 'lines'} + local lines = { 'So', 'many', 'lines' } vim.fn.bufload(bufnr) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) return bufnr - ]], - fake_uri - ) + end) - exec_lua( - [[ - local bufnr = ... + exec_lua(function() local lenses = { { range = { - start = { line = 0, character = 0, }, - ['end'] = { line = 0, character = 0 } + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, }, - command = { title = 'Lens1', command = 'Dummy' } + command = { title = 'Lens1', command = 'Dummy' }, }, } - vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr}) - ]], - bufnr - ) + vim.lsp.codelens.on_codelens( + nil, + lenses, + { method = 'textDocument/codeLens', client_id = 1, bufnr = bufnr } + ) + end) - local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) + local stored_lenses = exec_lua(function() + return vim.lsp.codelens.get(bufnr) + end) local expected = { { range = { @@ -57,58 +55,54 @@ describe('vim.lsp.codelens', function() } eq(expected, stored_lenses) - local virtual_text_chunks = exec_lua( - [[ - local bufnr = ... + local virtual_text_chunks = exec_lua(function() local ns = vim.lsp.codelens.__namespaces[1] local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {}) return vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, extmarks[1][1], { details = true })[3].virt_text - ]], - bufnr - ) + end) eq({ [1] = { 'Lens1', 'LspCodeLens' } }, virtual_text_chunks) end) it('can clear all lens', function() local fake_uri = 'file:///fake/uri' - local bufnr = exec_lua( - [[ - fake_uri = ... + local bufnr = exec_lua(function() local bufnr = vim.uri_to_bufnr(fake_uri) - local lines = {'So', 'many', 'lines'} + local lines = { 'So', 'many', 'lines' } vim.fn.bufload(bufnr) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) return bufnr - ]], - fake_uri - ) + end) - exec_lua( - [[ - local bufnr = ... + exec_lua(function() local lenses = { { range = { - start = { line = 0, character = 0, }, - ['end'] = { line = 0, character = 0 } + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, }, - command = { title = 'Lens1', command = 'Dummy' } + command = { title = 'Lens1', command = 'Dummy' }, }, } - vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr}) - ]], - bufnr - ) + vim.lsp.codelens.on_codelens( + nil, + lenses, + { method = 'textDocument/codeLens', client_id = 1, bufnr = bufnr } + ) + end) - local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) + local stored_lenses = exec_lua(function() + return vim.lsp.codelens.get(bufnr) + end) eq(1, #stored_lenses) - exec_lua([[ + exec_lua(function() vim.lsp.codelens.clear() - ]]) + end) - stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) + stored_lenses = exec_lua(function() + return vim.lsp.codelens.get(bufnr) + end) eq(0, #stored_lenses) end) end) diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 2798d57381..4df8d77d44 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -1,9 +1,16 @@ ---@diagnostic disable: no-unknown local t = require('test.testutil') +local t_lsp = require('test.functional.plugin.lsp.testutil') local n = require('test.functional.testnvim')() +local clear = n.clear local eq = t.eq +local neq = t.neq local exec_lua = n.exec_lua +local feed = n.feed +local retry = t.retry + +local create_server_definition = t_lsp.create_server_definition --- Convert completion results. --- @@ -11,38 +18,32 @@ local exec_lua = n.exec_lua ---@param candidates lsp.CompletionList|lsp.CompletionItem[] ---@param lnum? integer 0-based, defaults to 0 ---@return {items: table[], server_start_boundary: integer?} -local function complete(line, candidates, lnum) +local function complete(line, candidates, lnum, server_boundary) lnum = lnum or 0 -- nvim_win_get_cursor returns 0 based column, line:find returns 1 based local cursor_col = line:find('|') - 1 line = line:gsub('|', '') - return exec_lua( - [[ - local line, cursor_col, lnum, result = ... + return exec_lua(function(result) local line_to_cursor = line:sub(1, cursor_col) local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') - local items, server_start_boundary = require("vim.lsp._completion")._convert_results( + local items, new_server_boundary = require('vim.lsp.completion')._convert_results( line, lnum, cursor_col, + 1, client_start_boundary, - nil, + server_boundary, result, - "utf-16" + 'utf-16' ) return { items = items, - server_start_boundary = server_start_boundary + server_start_boundary = new_server_boundary, } - ]], - line, - cursor_col, - lnum, - candidates - ) + end, candidates) end -describe('vim.lsp._completion', function() +describe('vim.lsp.completion: item conversion', function() before_each(n.clear) -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion @@ -70,39 +71,24 @@ describe('vim.lsp._completion', function() textEdit = { newText = 'foobar', range = range0 }, }, { label = 'foocar', sortText = 'f', textEdit = { newText = 'foobar', range = range0 } }, - -- real-world snippet text + -- plain text { label = 'foocar', sortText = 'g', - insertText = 'foodar', + insertText = 'foodar(${1:var1})', + insertTextFormat = 1, + }, + { + label = '•INT16_C(c)', + insertText = 'INT16_C(${1:c})', insertTextFormat = 2, + filterText = 'INT16_C', + sortText = 'h', textEdit = { - newText = 'foobar(${1:place holder}, ${2:more ...holder{\\}})', + newText = 'INT16_C(${1:c})', range = range0, }, }, - { - label = 'foocar', - sortText = 'h', - insertText = 'foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', - insertTextFormat = 2, - }, - -- nested snippet tokens - { - label = 'foocar', - sortText = 'i', - insertText = 'foodar(${1:${2|typ1,typ2|}}) {$0\\}', - insertTextFormat = 2, - }, - -- braced tabstop - { label = 'foocar', sortText = 'j', insertText = 'foodar()${0}', insertTextFormat = 2 }, - -- plain text - { - label = 'foocar', - sortText = 'k', - insertText = 'foodar(${1:var1})', - insertTextFormat = 1, - }, } local expected = { { @@ -131,23 +117,167 @@ describe('vim.lsp._completion', function() }, { abbr = 'foocar', - word = 'foobar(place holder, more ...holder{})', + word = 'foodar(${1:var1})', -- marked as PlainText, text is used as is }, { - abbr = 'foocar', - word = 'foodar(var1 typ1, var2 *typ2) {}', + abbr = '•INT16_C(c)', + word = 'INT16_C', }, + } + local result = complete('|', completion_list) + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + eq(expected, result) + end) + + it('filters on label if filterText is missing', function() + local completion_list = { + { label = 'foo' }, + { label = 'bar' }, + } + local result = complete('fo|', completion_list) + local expected = { { - abbr = 'foocar', - word = 'foodar(typ1) {}', + abbr = 'foo', + word = 'foo', }, + } + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + eq(expected, result) + end) + + it('works on non word prefix', function() + local completion_list = { + { label = ' foo', insertText = '->foo' }, + } + local result = complete('wp.|', completion_list, 0, 2) + local expected = { { - abbr = 'foocar', - word = 'foodar()', + abbr = ' foo', + word = '->foo', }, + } + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + eq(expected, result) + end) + + it('trims trailing newline or tab from textEdit', function() + local range0 = { + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, + } + local items = { { - abbr = 'foocar', - word = 'foodar(${1:var1})', + detail = 'ansible.builtin', + filterText = 'lineinfile ansible.builtin.lineinfile builtin ansible', + kind = 7, + label = 'ansible.builtin.lineinfile', + sortText = '2_ansible.builtin.lineinfile', + textEdit = { + newText = 'ansible.builtin.lineinfile:\n ', + range = range0, + }, + }, + } + local result = complete('|', items) + result = vim.tbl_map(function(x) + return { + abbr = x.abbr, + word = x.word, + } + end, result.items) + + local expected = { + { + abbr = 'ansible.builtin.lineinfile', + word = 'ansible.builtin.lineinfile:', + }, + } + eq(expected, result) + end) + + it('prefers wordlike components for snippets', function() + -- There are two goals here: + -- + -- 1. The `word` should match what the user started typing, so that vim.fn.complete() doesn't + -- filter it away, preventing snippet expansion + -- + -- For example, if they type `items@ins`, luals returns `table.insert(items, $0)` as + -- textEdit.newText and `insert` as label. + -- There would be no prefix match if textEdit.newText is used as `word` + -- + -- 2. If users do not expand a snippet, but continue typing, they should see a somewhat reasonable + -- `word` getting inserted. + -- + -- For example in: + -- + -- insertText: "testSuites ${1:Env}" + -- label: "testSuites" + -- + -- "testSuites" should have priority as `word`, as long as the full snippet gets expanded on accept (<c-y>) + local range0 = { + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, + } + local completion_list = { + -- luals postfix snippet (typed text: items@ins|) + { + label = 'insert', + insertTextFormat = 2, + textEdit = { + newText = 'table.insert(items, $0)', + range = range0, + }, + }, + + -- eclipse.jdt.ls `new` snippet + { + label = 'new', + insertTextFormat = 2, + textEdit = { + newText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}', + range = range0, + }, + textEditText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}', + }, + + -- eclipse.jdt.ls `List.copyO` function call completion + { + label = 'copyOf(Collection<? extends E> coll) : List<E>', + insertTextFormat = 2, + insertText = 'copyOf', + textEdit = { + newText = 'copyOf(${1:coll})', + range = range0, + }, + }, + } + local expected = { + { + abbr = 'copyOf(Collection<? extends E> coll) : List<E>', + word = 'copyOf', + }, + { + abbr = 'insert', + word = 'insert', + }, + { + abbr = 'new', + word = 'new', }, } local result = complete('|', completion_list) @@ -159,6 +289,7 @@ describe('vim.lsp._completion', function() end, result.items) eq(expected, result) end) + it('uses correct start boundary', function() local completion_list = { isIncomplete = false, @@ -186,8 +317,10 @@ describe('vim.lsp._completion', function() dup = 1, empty = 1, icase = 1, + info = '', kind = 'Module', menu = '', + hl_group = '', word = 'this_thread', } local result = complete(' std::this|', completion_list) @@ -218,7 +351,7 @@ describe('vim.lsp._completion', function() }, }, { - filterText = 'notthis_thread', + filterText = 'no_match', insertText = 'notthis_thread', insertTextFormat = 1, kind = 9, @@ -240,8 +373,10 @@ describe('vim.lsp._completion', function() dup = 1, empty = 1, icase = 1, + info = '', kind = 'Module', menu = '', + hl_group = '', word = 'this_thread', } local result = complete(' std::this|is', completion_list) @@ -278,4 +413,316 @@ describe('vim.lsp._completion', function() eq('item-property-has-priority', item.data) eq({ line = 1, character = 1 }, item.textEdit.range.start) end) + + it( + 'uses insertText as textEdit.newText if there are editRange defaults but no textEditText', + function() + --- @type lsp.CompletionList + local completion_list = { + isIncomplete = false, + itemDefaults = { + editRange = { + start = { line = 1, character = 1 }, + ['end'] = { line = 1, character = 4 }, + }, + insertTextFormat = 2, + data = 'foobar', + }, + items = { + { + insertText = 'the-insertText', + label = 'hello', + data = 'item-property-has-priority', + }, + }, + } + local result = complete('|', completion_list) + eq(1, #result.items) + local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText + eq('the-insertText', text) + end + ) + + it( + 'defaults to label as textEdit.newText if insertText or textEditText are not present', + function() + local completion_list = { + isIncomplete = false, + itemDefaults = { + editRange = { + start = { line = 1, character = 1 }, + ['end'] = { line = 1, character = 4 }, + }, + insertTextFormat = 2, + data = 'foobar', + }, + items = { + { + label = 'hello', + data = 'item-property-has-priority', + }, + }, + } + local result = complete('|', completion_list) + eq(1, #result.items) + local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText + eq('hello', text) + end + ) +end) + +describe('vim.lsp.completion: protocol', function() + before_each(function() + clear() + exec_lua(create_server_definition) + exec_lua(function() + _G.capture = {} + --- @diagnostic disable-next-line:duplicate-set-field + vim.fn.complete = function(col, matches) + _G.capture.col = col + _G.capture.matches = matches + end + end) + end) + + after_each(clear) + + --- @param completion_result lsp.CompletionList + --- @return integer + local function create_server(completion_result) + return exec_lua(function() + local server = _G._create_server({ + capabilities = { + completionProvider = { + triggerCharacters = { '.' }, + }, + }, + handlers = { + ['textDocument/completion'] = function(_, _, callback) + callback(nil, completion_result) + end, + }, + }) + + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_win_set_buf(0, bufnr) + return vim.lsp.start({ + name = 'dummy', + cmd = server.cmd, + on_attach = function(client, bufnr0) + vim.lsp.completion.enable(true, client.id, bufnr0, { + convert = function(item) + return { abbr = item.label:gsub('%b()', '') } + end, + }) + end, + }) + end) + end + + local function assert_matches(fn) + retry(nil, nil, function() + fn(exec_lua('return _G.capture.matches')) + end) + end + + --- @param pos [integer, integer] + local function trigger_at_pos(pos) + exec_lua(function() + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(win, pos) + vim.lsp.completion.trigger() + end) + + retry(nil, nil, function() + neq(nil, exec_lua('return _G.capture.col')) + end) + end + + it('fetches completions and shows them using complete on trigger', function() + create_server({ + isIncomplete = false, + items = { + { + label = 'hello', + }, + { + label = 'hercules', + tags = { 1 }, -- 1 represents Deprecated tag + }, + { + label = 'hero', + deprecated = true, + }, + }, + }) + + feed('ih') + trigger_at_pos({ 1, 1 }) + + assert_matches(function(matches) + eq({ + { + abbr = 'hello', + dup = 1, + empty = 1, + icase = 1, + info = '', + kind = 'Unknown', + menu = '', + hl_group = '', + user_data = { + nvim = { + lsp = { + client_id = 1, + completion_item = { + label = 'hello', + }, + }, + }, + }, + word = 'hello', + }, + { + abbr = 'hercules', + dup = 1, + empty = 1, + icase = 1, + info = '', + kind = 'Unknown', + menu = '', + hl_group = 'DiagnosticDeprecated', + user_data = { + nvim = { + lsp = { + client_id = 1, + completion_item = { + label = 'hercules', + tags = { 1 }, + }, + }, + }, + }, + word = 'hercules', + }, + { + abbr = 'hero', + dup = 1, + empty = 1, + icase = 1, + info = '', + kind = 'Unknown', + menu = '', + hl_group = 'DiagnosticDeprecated', + user_data = { + nvim = { + lsp = { + client_id = 1, + completion_item = { + label = 'hero', + deprecated = true, + }, + }, + }, + }, + word = 'hero', + }, + }, matches) + end) + end) + + it('merges results from multiple clients', function() + create_server({ + isIncomplete = false, + items = { + { + label = 'hello', + }, + }, + }) + create_server({ + isIncomplete = false, + items = { + { + label = 'hallo', + }, + }, + }) + + feed('ih') + trigger_at_pos({ 1, 1 }) + + assert_matches(function(matches) + eq(2, #matches) + eq('hello', matches[1].word) + eq('hallo', matches[2].word) + end) + end) + + it('executes commands', function() + local completion_list = { + isIncomplete = false, + items = { + { + label = 'hello', + command = { + arguments = { '1', '0' }, + command = 'dummy', + title = '', + }, + }, + }, + } + local client_id = create_server(completion_list) + + exec_lua(function() + _G.called = false + local client = assert(vim.lsp.get_client_by_id(client_id)) + client.commands.dummy = function() + _G.called = true + end + end) + + feed('ih') + trigger_at_pos({ 1, 1 }) + + local item = completion_list.items[1] + exec_lua(function() + vim.v.completed_item = { + user_data = { + nvim = { + lsp = { + client_id = client_id, + completion_item = item, + }, + }, + }, + } + end) + + feed('<C-x><C-o><C-y>') + + assert_matches(function(matches) + eq(1, #matches) + eq('hello', matches[1].word) + eq(true, exec_lua('return _G.called')) + end) + end) + + it('enable(…,{convert=fn}) custom word/abbr format', function() + create_server({ + isIncomplete = false, + items = { + { + label = 'foo(bar)', + }, + }, + }) + + feed('ifo') + trigger_at_pos({ 1, 1 }) + assert_matches(function(matches) + eq('foo', matches[1].abbr) + end) + end) end) diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index c5e14ffdc2..78c684083b 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -11,7 +11,9 @@ local neq = t.neq local create_server_definition = t_lsp.create_server_definition describe('vim.lsp.diagnostic', function() - local fake_uri + local fake_uri --- @type string + local client_id --- @type integer + local diagnostic_bufnr --- @type integer before_each(function() clear { env = { @@ -19,198 +21,174 @@ describe('vim.lsp.diagnostic', function() VIMRUNTIME = os.getenv 'VIMRUNTIME', } } - exec_lua [[ + exec_lua(function() require('vim.lsp') - make_range = function(x1, y1, x2, y2) + _G.make_range = function(x1, y1, x2, y2) return { start = { line = x1, character = y1 }, ['end'] = { line = x2, character = y2 } } end - make_error = function(msg, x1, y1, x2, y2) + _G.make_error = function(msg, x1, y1, x2, y2) return { - range = make_range(x1, y1, x2, y2), + range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 1, } end - make_warning = function(msg, x1, y1, x2, y2) + _G.make_warning = function(msg, x1, y1, x2, y2) return { - range = make_range(x1, y1, x2, y2), + range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 2, } end - make_information = function(msg, x1, y1, x2, y2) + _G.make_information = function(msg, x1, y1, x2, y2) return { - range = make_range(x1, y1, x2, y2), + range = _G.make_range(x1, y1, x2, y2), message = msg, severity = 3, } end - function get_extmarks(bufnr, client_id) - local namespace = vim.lsp.diagnostic.get_namespace(client_id) + 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 + 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 + 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_client { + client_id = assert(vim.lsp.start_client { cmd_env = { - NVIM_LUA_NOTRACK = "1"; - }; + NVIM_LUA_NOTRACK = '1', + }, cmd = { - vim.v.progpath, '-es', '-u', 'NONE', '--headless' - }; - offset_encoding = "utf-16"; - } - ]] + vim.v.progpath, + '-es', + '-u', + 'NONE', + '--headless', + }, + offset_encoding = 'utf-16', + }) + end) fake_uri = 'file:///fake/uri' - exec_lua( - [[ - 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"} + 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) - return diagnostic_bufnr - ]], - fake_uri - ) + end) end) after_each(function() clear() end) - describe('vim.lsp.diagnostic', function() - it('maintains LSP information when translating diagnostics', function() - local result = exec_lua [[ - local diagnostics = { - make_error("Error 1", 1, 1, 1, 5), - } - - diagnostics[1].code = 42 - diagnostics[1].data = "Hello world" - - vim.lsp.diagnostic.on_publish_diagnostics(nil, { - uri = fake_uri, - diagnostics = diagnostics, - }, {client_id=client_id}) - - return { - vim.diagnostic.get(diagnostic_bufnr, {lnum=1})[1], - vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)[1], - } - ]] - eq({ code = 42, data = 'Hello world' }, result[1].user_data.lsp) - eq(42, result[1].code) - eq(42, result[2].code) - eq('Hello world', result[2].data) - end) - end) - describe('vim.lsp.diagnostic.on_publish_diagnostics', function() it('allows configuring the virtual text via vim.lsp.with', function() local expected_spacing = 10 - local extmarks = exec_lua( - [[ - PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + local extmarks = exec_lua(function() + _G.PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { virtual_text = { - spacing = ..., + spacing = expected_spacing, }, }) - PublishDiagnostics(nil, { - uri = fake_uri, - diagnostics = { - make_error('Delayed Diagnostic', 4, 4, 4, 4), - } - }, {client_id=client_id} - ) + _G.PublishDiagnostics(nil, { + uri = fake_uri, + diagnostics = { + _G.make_error('Delayed Diagnostic', 4, 4, 4, 4), + }, + }, { client_id = client_id }) - return get_extmarks(diagnostic_bufnr, client_id) - ]], - expected_spacing - ) + return _G.get_extmarks(diagnostic_bufnr, client_id) + end) - local virt_text = extmarks[1][4].virt_text - local spacing = virt_text[1][1] + local spacing = extmarks[1][4].virt_text[1][1] eq(expected_spacing, #spacing) end) it('allows configuring the virtual text via vim.lsp.with using a function', function() local expected_spacing = 10 - local extmarks = exec_lua( - [[ - spacing = ... - - PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + local extmarks = exec_lua(function() + _G.PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { virtual_text = function() return { - spacing = spacing, + spacing = expected_spacing, } end, }) - PublishDiagnostics(nil, { - uri = fake_uri, - diagnostics = { - make_error('Delayed Diagnostic', 4, 4, 4, 4), - } - }, {client_id=client_id} - ) + _G.PublishDiagnostics(nil, { + uri = fake_uri, + diagnostics = { + _G.make_error('Delayed Diagnostic', 4, 4, 4, 4), + }, + }, { client_id = client_id }) - return get_extmarks(diagnostic_bufnr, client_id) - ]], - expected_spacing - ) + return _G.get_extmarks(diagnostic_bufnr, client_id) + end) - local virt_text = extmarks[1][4].virt_text - local spacing = virt_text[1][1] + local spacing = extmarks[1][4].virt_text[1][1] eq(expected_spacing, #spacing) end) it('allows filtering via severity limit', function() local get_extmark_count_with_severity = function(severity_limit) - return exec_lua( - [[ - PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + return exec_lua(function() + _G.PublishDiagnostics = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { underline = false, virtual_text = { - severity = { min = ... } + severity = { min = severity_limit }, }, }) - PublishDiagnostics(nil, { - uri = fake_uri, - diagnostics = { - make_warning('Delayed Diagnostic', 4, 4, 4, 4), - } - }, {client_id=client_id} - ) - - return #get_extmarks(diagnostic_bufnr, client_id) - ]], - severity_limit - ) + _G.PublishDiagnostics(nil, { + uri = fake_uri, + diagnostics = { + _G.make_warning('Delayed Diagnostic', 4, 4, 4, 4), + }, + }, { client_id = client_id }) + + return #_G.get_extmarks(diagnostic_bufnr, client_id) + end, client_id, fake_uri, severity_limit) end -- No messages with Error or higher @@ -223,218 +201,284 @@ describe('vim.lsp.diagnostic', function() it('correctly handles UTF-16 offsets', function() local line = 'All 💼 and no 🎉 makes Jack a dull 👦' - local result = exec_lua( - [[ - local line = ... - vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, -1, false, {line}) + 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 = { - make_error('UTF-16 Diagnostic', 0, 7, 0, 8), - } - }, {client_id=client_id} - ) + 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 - ]], - line - ) + end) eq(1, #result) - eq(exec_lua([[return vim.str_byteindex(..., 7, true)]], line), result[1].col) - eq(exec_lua([[return vim.str_byteindex(..., 8, true)]], line), result[1].end_col) + eq( + exec_lua(function() + return vim.str_byteindex(line, 7, true) + end), + result[1].col + ) + eq( + exec_lua(function() + return vim.str_byteindex(line, 8, true) + end), + result[1].end_col + ) end) it('does not create buffer on empty diagnostics', function() - local bufnr - -- No buffer is created without diagnostics - bufnr = exec_lua [[ - 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")) - ]] - eq(-1, bufnr) + 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 - bufnr = exec_lua [[ - vim.lsp.diagnostic.on_publish_diagnostics(nil, { - uri = "file:///fake/uri2", - diagnostics = { - make_error('Diagnostic', 0, 0, 0, 0), - }, - }, {client_id=client_id}) - return vim.fn.bufnr(vim.uri_to_fname("file:///fake/uri2")) - ]] - neq(-1, bufnr) - eq(1, exec_lua([[return #vim.diagnostic.get(...)]], bufnr)) + 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 - bufnr = exec_lua [[ - 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")) - ]] - neq(-1, bufnr) - eq(0, exec_lua([[return #vim.diagnostic.get(...)]], bufnr)) + 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([[ - server = _create_server({ + exec_lua(function() + _G.server = _G._create_server({ capabilities = { - diagnosticProvider = { - } - } + diagnosticProvider = {}, + }, }) - function get_extmarks(bufnr, client_id) - local namespace = vim.lsp.diagnostic.get_namespace(client_id, true) + 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 + 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 + 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 = server.cmd }) - ]]) + client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) end) it('adds diagnostics to vim.diagnostics', function() - local diags = exec_lua([[ - vim.lsp.diagnostic.on_diagnostic(nil, - { - kind = 'full', - items = { - make_error('Pull Diagnostic', 4, 4, 4, 4), - } + 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, + }, { + 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('allows configuring the virtual text via vim.lsp.with', function() local expected_spacing = 10 - local extmarks = exec_lua( - [[ - Diagnostic = vim.lsp.with(vim.lsp.diagnostic.on_diagnostic, { + local extmarks = exec_lua(function() + _G.Diagnostic = vim.lsp.with(vim.lsp.diagnostic.on_diagnostic, { virtual_text = { - spacing = ..., + spacing = expected_spacing, }, }) - Diagnostic(nil, - { - kind = 'full', - items = { - make_error('Pull Diagnostic', 4, 4, 4, 4), - } + _G.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, + }, { + params = { + textDocument = { uri = fake_uri }, }, - {} - ) + uri = fake_uri, + client_id = client_id, + }, {}) - return get_extmarks(diagnostic_bufnr, client_id) - ]], - expected_spacing - ) + return _G.get_extmarks(diagnostic_bufnr, client_id) + end) eq(2, #extmarks) eq(expected_spacing, #extmarks[1][4].virt_text[1][1]) end) it('clears diagnostics when client detaches', function() - exec_lua([[ - vim.lsp.diagnostic.on_diagnostic(nil, - { - kind = 'full', - items = { - make_error('Pull Diagnostic', 4, 4, 4, 4), - } + 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, + }, { + params = { + textDocument = { uri = fake_uri }, }, - {} - ) - ]]) - local diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) - eq(1, #diags) + uri = fake_uri, + client_id = client_id, + }, {}) + end) + + eq( + 1, + exec_lua(function() + return #vim.diagnostic.get(diagnostic_bufnr) + end) + ) - exec_lua([[ vim.lsp.stop_client(client_id) ]]) + exec_lua(function() + vim.lsp.stop_client(client_id) + end) - diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) - eq(0, #diags) + 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() - exec_lua([[ - client_id2 = vim.lsp.start({ name = 'dummy2', cmd = server.cmd }) - - vim.lsp.diagnostic.on_diagnostic(nil, - { - kind = 'full', - items = { - make_error('Pull Diagnostic', 4, 4, 4, 4), - } + 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, + }, { + params = { + textDocument = { uri = fake_uri }, }, - {} - ) - ]]) - local diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) - eq(1, #diags) + uri = fake_uri, + client_id = client_id, + }, {}) + end) + + eq( + 1, + exec_lua(function() + return #vim.diagnostic.get(diagnostic_bufnr) + end) + ) - exec_lua([[ vim.lsp.stop_client(client_id2) ]]) + exec_lua(function() + vim.lsp.stop_client(client_id2) + end) - diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) - eq(1, #diags) + eq( + 1, + exec_lua(function() + return #vim.diagnostic.get(diagnostic_bufnr) + end) + ) end) end) end) diff --git a/test/functional/plugin/lsp/handler_spec.lua b/test/functional/plugin/lsp/handler_spec.lua index 013a5fb5e7..4b05b676a8 100644 --- a/test/functional/plugin/lsp/handler_spec.lua +++ b/test/functional/plugin/lsp/handler_spec.lua @@ -11,28 +11,31 @@ describe('lsp-handlers', function() it('should return a table with the default keys', function() eq( { hello = 'world' }, - exec_lua [[ - return vim.lsp._with_extend('test', { hello = 'world' }) - ]] + exec_lua(function() + return vim.lsp._with_extend('test', { hello = 'world' }) + end) ) end) it('should override with config keys', function() eq( { hello = 'universe', other = true }, - exec_lua [[ - return vim.lsp._with_extend('test', { other = true, hello = 'world' }, { hello = 'universe' }) - ]] + exec_lua(function() + return vim.lsp._with_extend( + 'test', + { other = true, hello = 'world' }, + { hello = 'universe' } + ) + end) ) end) it('should not allow invalid keys', function() matches( '.*Invalid option for `test`.*', - pcall_err( - exec_lua, - "return vim.lsp._with_extend('test', { hello = 'world' }, { invalid = true })" - ) + pcall_err(exec_lua, function() + return vim.lsp._with_extend('test', { hello = 'world' }, { invalid = true }) + end) ) end) end) diff --git a/test/functional/plugin/lsp/incremental_sync_spec.lua b/test/functional/plugin/lsp/incremental_sync_spec.lua index 238b90b57d..f60e159d64 100644 --- a/test/functional/plugin/lsp/incremental_sync_spec.lua +++ b/test/functional/plugin/lsp/incremental_sync_spec.lua @@ -10,11 +10,9 @@ local feed = n.feed before_each(function() clear() - exec_lua [[ - local evname = ... + exec_lua(function() local sync = require('vim.lsp.sync') local events = {} - local buffer_cache = {} -- local format_line_ending = { -- ["unix"] = '\n', @@ -24,35 +22,43 @@ before_each(function() -- local line_ending = format_line_ending[vim.api.nvim_get_option_value('fileformat', {})] - - function test_register(bufnr, id, offset_encoding, line_ending) - local curr_lines + --- @diagnostic disable-next-line:duplicate-set-field + function _G.test_register(bufnr, id, offset_encoding, line_ending) local prev_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) - local function callback(_, bufnr, changedtick, firstline, lastline, new_lastline) - if test_unreg == id then + local function callback(_, bufnr0, _changedtick, firstline, lastline, new_lastline) + if _G.test_unreg == id then return true end - local curr_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + local curr_lines = vim.api.nvim_buf_get_lines(bufnr0, 0, -1, true) local incremental_change = sync.compute_diff( - prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding, line_ending) + prev_lines, + curr_lines, + firstline, + lastline, + new_lastline, + offset_encoding, + line_ending + ) table.insert(events, incremental_change) prev_lines = curr_lines end - local opts = {on_lines=callback, on_detach=callback, on_reload=callback} + local opts = { on_lines = callback, on_detach = callback, on_reload = callback } vim.api.nvim_buf_attach(bufnr, false, opts) end - function get_events() + --- @diagnostic disable-next-line:duplicate-set-field + function _G.get_events() local ret_events = events events = {} return ret_events end - ]] + end) end) +--- @param edit_operations string[] local function test_edit( prev_buffer, edit_operations, @@ -64,13 +70,22 @@ local function test_edit( line_ending = line_ending or '\n' api.nvim_buf_set_lines(0, 0, -1, true, prev_buffer) - exec_lua('return test_register(...)', 0, 'test1', offset_encoding, line_ending) + exec_lua(function() + return _G.test_register(0, 'test1', offset_encoding, line_ending) + end) for _, edit in ipairs(edit_operations) do feed(edit) end - eq(expected_text_changes, exec_lua('return get_events(...)')) - exec_lua("test_unreg = 'test1'") + eq( + expected_text_changes, + exec_lua(function() + return _G.get_events() + end) + ) + exec_lua(function() + _G.test_unreg = 'test1' + end) end describe('incremental synchronization', function() @@ -170,7 +185,7 @@ describe('incremental synchronization', function() } test_edit({ 'a' }, { 'rb' }, expected_text_changes, 'utf-16', '\n') end) - it('deleting a line', function() + it('deleting the first line', function() local expected_text_changes = { { range = { @@ -183,11 +198,49 @@ describe('incremental synchronization', function() line = 1, }, }, - rangeLength = 12, + rangeLength = 6, + text = '', + }, + } + test_edit({ 'hello', 'world' }, { 'ggdd' }, expected_text_changes, 'utf-16', '\n') + end) + it('deleting the last line', function() + local expected_text_changes = { + { + range = { + ['start'] = { + character = 0, + line = 1, + }, + ['end'] = { + character = 0, + line = 2, + }, + }, + rangeLength = 6, + text = '', + }, + } + test_edit({ 'hello', 'world' }, { '2ggdd' }, expected_text_changes, 'utf-16', '\n') + end) + it('deleting all lines', function() + local expected_text_changes = { + { + range = { + ['start'] = { + character = 0, + line = 0, + }, + ['end'] = { + character = 5, + line = 1, + }, + }, + rangeLength = 11, text = '', }, } - test_edit({ 'hello world' }, { 'dd' }, expected_text_changes, 'utf-16', '\n') + test_edit({ 'hello', 'world' }, { 'ggdG' }, expected_text_changes, 'utf-16', '\n') end) it('deleting an empty line', function() local expected_text_changes = { diff --git a/test/functional/plugin/lsp/inlay_hint_spec.lua b/test/functional/plugin/lsp/inlay_hint_spec.lua index d3b5ae0e4e..471f2cc3e8 100644 --- a/test/functional/plugin/lsp/inlay_hint_spec.lua +++ b/test/functional/plugin/lsp/inlay_hint_spec.lua @@ -12,7 +12,8 @@ local api = n.api local clear_notrace = t_lsp.clear_notrace local create_server_definition = t_lsp.create_server_definition -local text = dedent([[ +describe('vim.lsp.inlay_hint', function() + local text = dedent([[ auto add(int a, int b) { return a + b; } int main() { @@ -22,7 +23,7 @@ int main() { } }]]) -local response = [==[ + local response = [==[ [ {"kind":1,"paddingLeft":false,"label":"-> int","position":{"character":22,"line":0},"paddingRight":false}, {"kind":2,"paddingLeft":false,"label":"a:","position":{"character":15,"line":5},"paddingRight":true}, @@ -30,7 +31,7 @@ local response = [==[ ] ]==] -local grid_without_inlay_hints = [[ + local grid_without_inlay_hints = [[ auto add(int a, int b) { return a + b; } | | int main() { | @@ -42,7 +43,7 @@ local grid_without_inlay_hints = [[ | ]] -local grid_with_inlay_hints = [[ + local grid_with_inlay_hints = [[ auto add(int a, int b){1:-> int} { return a + b; } | | int main() { | @@ -54,54 +55,58 @@ local grid_with_inlay_hints = [[ | ]] ---- @type test.functional.ui.screen -local screen -before_each(function() - clear_notrace() - screen = Screen.new(50, 9) - screen:attach() - - exec_lua(create_server_definition) - exec_lua( - [[ - local response = ... - server = _create_server({ - capabilities = { - inlayHintProvider = true, - }, - handlers = { - ['textDocument/inlayHint'] = function(_, _, callback) - callback(nil, vim.json.decode(response)) - end, - } - }) + --- @type test.functional.ui.screen + local screen - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) + --- @type integer + local client_id - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]], - response - ) + --- @type integer + local bufnr - insert(text) - exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) - screen:expect({ grid = grid_with_inlay_hints }) -end) + before_each(function() + clear_notrace() + screen = Screen.new(50, 9) + screen:attach() -after_each(function() - api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) -end) + bufnr = n.api.nvim_get_current_buf() + exec_lua(create_server_definition) + client_id = exec_lua(function() + _G.server = _G._create_server({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function(_, _, callback) + callback(nil, vim.json.decode(response)) + end, + }, + }) + + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) + + insert(text) + exec_lua(function() + vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) + end) + screen:expect({ grid = grid_with_inlay_hints }) + end) + + after_each(function() + api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) + end) -describe('vim.lsp.inlay_hint', function() it('clears inlay hints when sole client detaches', function() - exec_lua([[vim.lsp.stop_client(client_id)]]) + exec_lua(function() + vim.lsp.stop_client(client_id) + end) screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) end) it('does not clear inlay hints when one of several clients detaches', function() - exec_lua([[ - server2 = _create_server({ + local client_id2 = exec_lua(function() + _G.server2 = _G._create_server({ capabilities = { inlayHintProvider = true, }, @@ -109,13 +114,16 @@ describe('vim.lsp.inlay_hint', function() ['textDocument/inlayHint'] = function(_, _, callback) callback(nil, {}) end, - } + }, }) - client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + local client_id2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd }) vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) - ]]) + return client_id2 + end) - exec_lua([[ vim.lsp.stop_client(client2) ]]) + exec_lua(function() + vim.lsp.stop_client(client_id2) + end) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) end) @@ -123,61 +131,85 @@ describe('vim.lsp.inlay_hint', function() it('validation', function() t.matches( 'enable: expected boolean, got table', - t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable({}, { bufnr = bufnr })]]) + t.pcall_err(exec_lua, function() + --- @diagnostic disable-next-line:param-type-mismatch + vim.lsp.inlay_hint.enable({}, { bufnr = bufnr }) + end) ) t.matches( 'enable: expected boolean, got number', - t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable(42)]]) + t.pcall_err(exec_lua, function() + --- @diagnostic disable-next-line:param-type-mismatch + vim.lsp.inlay_hint.enable(42) + end) ) t.matches( 'filter: expected table, got number', - t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable(true, 42)]]) + t.pcall_err(exec_lua, function() + --- @diagnostic disable-next-line:param-type-mismatch + vim.lsp.inlay_hint.enable(true, 42) + end) ) end) describe('clears/applies inlay hints when passed false/true/nil', function() + local bufnr2 --- @type integer before_each(function() - exec_lua([[ - bufnr2 = vim.api.nvim_create_buf(true, false) - vim.lsp.buf_attach_client(bufnr2, client_id) - vim.api.nvim_win_set_buf(0, bufnr2) - ]]) + bufnr2 = exec_lua(function() + local bufnr2_0 = vim.api.nvim_create_buf(true, false) + vim.lsp.buf_attach_client(bufnr2_0, client_id) + vim.api.nvim_win_set_buf(0, bufnr2_0) + return bufnr2_0 + end) insert(text) - exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr2 })]]) - exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]]) + exec_lua(function() + vim.lsp.inlay_hint.enable(true, { bufnr = bufnr2 }) + end) + n.api.nvim_win_set_buf(0, bufnr) screen:expect({ grid = grid_with_inlay_hints }) end) it('for one single buffer', function() - exec_lua([[ + exec_lua(function() vim.lsp.inlay_hint.enable(false, { bufnr = bufnr }) vim.api.nvim_win_set_buf(0, bufnr2) - ]]) + end) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) - exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]]) + n.api.nvim_win_set_buf(0, bufnr) screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) - exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) + exec_lua(function() + vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) + end) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) - exec_lua( - [[vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }), { bufnr = bufnr })]] - ) + exec_lua(function() + vim.lsp.inlay_hint.enable( + not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }), + { bufnr = bufnr } + ) + end) screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) - exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) + exec_lua(function() + vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) + end) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) end) it('for all buffers', function() - exec_lua([[vim.lsp.inlay_hint.enable(false)]]) + exec_lua(function() + vim.lsp.inlay_hint.enable(false) + end) screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) - exec_lua([[vim.api.nvim_win_set_buf(0, bufnr2)]]) + n.api.nvim_win_set_buf(0, bufnr2) screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) - exec_lua([[vim.lsp.inlay_hint.enable(true)]]) + exec_lua(function() + vim.lsp.inlay_hint.enable(true) + end) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) - exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]]) + n.api.nvim_win_set_buf(0, bufnr) screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) end) end) @@ -198,10 +230,8 @@ describe('vim.lsp.inlay_hint', function() paddingRight = false, } - exec_lua( - [[ - local expected2 = ... - server2 = _create_server({ + exec_lua(function() + _G.server2 = _G._create_server({ capabilities = { inlayHintProvider = true, }, @@ -209,52 +239,139 @@ describe('vim.lsp.inlay_hint', function() ['textDocument/inlayHint'] = function(_, _, callback) callback(nil, { expected2 }) end, - } + }, }) - client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + _G.client2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd }) vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) - ]], - expected2 - ) + end) --- @type vim.lsp.inlay_hint.get.ret - local res = exec_lua([[return vim.lsp.inlay_hint.get()]]) - eq({ - { bufnr = 1, client_id = 1, inlay_hint = expected[1] }, - { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, - { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, - { bufnr = 1, client_id = 2, inlay_hint = expected2 }, - }, res) + eq( + { + { bufnr = 1, client_id = 1, inlay_hint = expected[1] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, + { bufnr = 1, client_id = 2, inlay_hint = expected2 }, + }, + exec_lua(function() + return vim.lsp.inlay_hint.get() + end) + ) - --- @type vim.lsp.inlay_hint.get.ret - res = exec_lua([[return vim.lsp.inlay_hint.get({ - range = { - start = { line = 2, character = 10 }, - ["end"] = { line = 2, character = 10 }, + eq( + { + { bufnr = 1, client_id = 2, inlay_hint = expected2 }, }, - })]]) - eq({ - { bufnr = 1, client_id = 2, inlay_hint = expected2 }, - }, res) + exec_lua(function() + return vim.lsp.inlay_hint.get({ + range = { + start = { line = 2, character = 10 }, + ['end'] = { line = 2, character = 10 }, + }, + }) + end) + ) - --- @type vim.lsp.inlay_hint.get.ret - res = exec_lua([[return vim.lsp.inlay_hint.get({ - bufnr = vim.api.nvim_get_current_buf(), - range = { - start = { line = 4, character = 18 }, - ["end"] = { line = 5, character = 17 }, + eq( + { + { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, }, - })]]) - eq({ - { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, - { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, - }, res) + exec_lua(function() + return vim.lsp.inlay_hint.get({ + bufnr = vim.api.nvim_get_current_buf(), + range = { + start = { line = 4, character = 18 }, + ['end'] = { line = 5, character = 17 }, + }, + }) + end) + ) - --- @type vim.lsp.inlay_hint.get.ret - res = exec_lua([[return vim.lsp.inlay_hint.get({ - bufnr = vim.api.nvim_get_current_buf() + 1, - })]]) - eq({}, res) + eq( + {}, + exec_lua(function() + return vim.lsp.inlay_hint.get({ + bufnr = vim.api.nvim_get_current_buf() + 1, + }) + end) + ) + end) + end) +end) + +describe('Inlay hints handler', function() + local text = dedent([[ +test text + ]]) + + local response = [==[ + [ + { "position": { "line": 0, "character": 0 }, "label": "0" }, + { "position": { "line": 0, "character": 0 }, "label": "1" }, + { "position": { "line": 0, "character": 0 }, "label": "2" }, + { "position": { "line": 0, "character": 0 }, "label": "3" }, + { "position": { "line": 0, "character": 0 }, "label": "4" } + ] + ]==] + + local grid_without_inlay_hints = [[ + test text | + ^ | + | +]] + + local grid_with_inlay_hints = [[ + {1:01234}test text | + ^ | + | +]] + + --- @type test.functional.ui.screen + local screen + + --- @type integer + local client_id + + --- @type integer + local bufnr + + before_each(function() + clear_notrace() + screen = Screen.new(50, 3) + screen:attach() + + exec_lua(create_server_definition) + bufnr = n.api.nvim_get_current_buf() + client_id = exec_lua(function() + _G.server = _G._create_server({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function(_, _, callback) + callback(nil, vim.json.decode(response)) + end, + }, + }) + + vim.api.nvim_win_set_buf(0, bufnr) + + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) + insert(text) + end) + + it('renders hints with same position in received order', function() + exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) + screen:expect({ grid = grid_with_inlay_hints }) + exec_lua(function() + vim.lsp.stop_client(client_id) end) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) + end) + + after_each(function() + api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) end) end) diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index 7908c5d2e7..f72aab7e0b 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -25,7 +25,7 @@ after_each(function() end) describe('semantic token highlighting', function() - local screen + local screen --- @type test.functional.ui.screen before_each(function() screen = Screen.new(40, 16) screen:attach() @@ -84,10 +84,8 @@ describe('semantic token highlighting', function() before_each(function() exec_lua(create_server_definition) - exec_lua( - [[ - local legend, response, edit_response = ... - server = _create_server({ + exec_lua(function() + _G.server = _G._create_server({ capabilities = { semanticTokensProvider = { full = { delta = true }, @@ -101,24 +99,19 @@ describe('semantic token highlighting', function() ['textDocument/semanticTokens/full/delta'] = function(_, _, callback) callback(nil, vim.fn.json_decode(edit_response)) end, - } + }, }) - ]], - legend, - response, - edit_response - ) + end, legend, response, edit_response) end) it('buffer is highlighted when attached', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() + insert(text) + exec_lua(function() + local bufnr = vim.api.nvim_get_current_buf() vim.api.nvim_win_set_buf(0, bufnr) vim.bo[bufnr].filetype = 'some-filetype' - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - - insert(text) + vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) screen:expect { grid = [[ @@ -141,23 +134,20 @@ describe('semantic token highlighting', function() end) it('use LspTokenUpdate and highlight_token', function() - exec_lua([[ - vim.api.nvim_create_autocmd("LspTokenUpdate", { + insert(text) + exec_lua(function() + vim.api.nvim_create_autocmd('LspTokenUpdate', { callback = function(args) - local token = args.data.token - if token.type == "function" and token.modifiers.declaration then - vim.lsp.semantic_tokens.highlight_token( - token, args.buf, args.data.client_id, "Macro" - ) + local token = args.data.token --- @type STTokenRange + if token.type == 'function' and token.modifiers.declaration then + vim.lsp.semantic_tokens.highlight_token(token, args.buf, args.data.client_id, 'Macro') end end, }) - bufnr = vim.api.nvim_get_current_buf() + local bufnr = vim.api.nvim_get_current_buf() vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - - insert(text) + vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) screen:expect { grid = [[ @@ -180,18 +170,23 @@ describe('semantic token highlighting', function() end) it('buffer is unhighlighted when client is detached', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - insert(text) - exec_lua([[ + local bufnr = n.api.nvim_get_current_buf() + local client_id = exec_lua(function() + vim.api.nvim_win_set_buf(0, bufnr) + local client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + vim.wait(1000, function() + return #_G.server.messages > 1 + end) + return client_id + end) + + exec_lua(function() + --- @diagnostic disable-next-line:duplicate-set-field vim.notify = function() end vim.lsp.buf_detach_client(bufnr, client_id) - ]]) + end) screen:expect { grid = [[ @@ -216,18 +211,19 @@ describe('semantic token highlighting', function() it( 'buffer is highlighted and unhighlighted when semantic token highlighting is started and stopped', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) + local bufnr = n.api.nvim_get_current_buf() + local client_id = exec_lua(function() + vim.api.nvim_win_set_buf(0, bufnr) + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) insert(text) - exec_lua([[ - vim.notify = function() end - vim.lsp.semantic_tokens.stop(bufnr, client_id) - ]]) + exec_lua(function() + --- @diagnostic disable-next-line:duplicate-set-field + vim.notify = function() end + vim.lsp.semantic_tokens.stop(bufnr, client_id) + end) screen:expect { grid = [[ @@ -248,9 +244,9 @@ describe('semantic token highlighting', function() ]], } - exec_lua([[ - vim.lsp.semantic_tokens.start(bufnr, client_id) - ]]) + exec_lua(function() + vim.lsp.semantic_tokens.start(bufnr, client_id) + end) screen:expect { grid = [[ @@ -274,18 +270,17 @@ describe('semantic token highlighting', function() ) it('highlights start and stop when using "0" for current buffer', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) + local client_id = exec_lua(function() + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) insert(text) - exec_lua([[ + exec_lua(function() + --- @diagnostic disable-next-line:duplicate-set-field vim.notify = function() end vim.lsp.semantic_tokens.stop(0, client_id) - ]]) + end) screen:expect { grid = [[ @@ -306,9 +301,9 @@ describe('semantic token highlighting', function() ]], } - exec_lua([[ + exec_lua(function() vim.lsp.semantic_tokens.start(0, client_id) - ]]) + end) screen:expect { grid = [[ @@ -331,13 +326,10 @@ describe('semantic token highlighting', function() end) it('buffer is re-highlighted when force refreshed', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - insert(text) + exec_lua(function() + vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) screen:expect { grid = [[ @@ -358,9 +350,9 @@ describe('semantic token highlighting', function() ]], } - exec_lua([[ - vim.lsp.semantic_tokens.force_refresh(bufnr) - ]]) + exec_lua(function() + vim.lsp.semantic_tokens.force_refresh() + end) screen:expect { grid = [[ @@ -384,7 +376,9 @@ describe('semantic token highlighting', function() local messages = exec_lua('return server.messages') local token_request_count = 0 - for _, message in ipairs(messages) do + for _, message in + ipairs(messages --[[@as {method:string,params:table}[] ]]) + do assert(message.method ~= 'textDocument/semanticTokens/full/delta', 'delta request received') if message.method == 'textDocument/semanticTokens/full' then token_request_count = token_request_count + 1 @@ -394,31 +388,29 @@ describe('semantic token highlighting', function() end) it('destroys the highlighter if the buffer is deleted', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) + exec_lua(function() + vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) insert(text) - local highlighters = exec_lua([[ - vim.api.nvim_buf_delete(bufnr, { force = true }) - local semantic_tokens = vim.lsp.semantic_tokens - return semantic_tokens.__STHighlighter.active - ]]) - - eq({}, highlighters) + eq( + {}, + exec_lua(function() + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_delete(bufnr, { force = true }) + return vim.lsp.semantic_tokens.__STHighlighter.active + end) + ) end) it('updates highlights with delta request on buffer change', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - insert(text) + + exec_lua(function() + vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) + screen:expect { grid = [[ #include <iostream> | @@ -459,45 +451,49 @@ describe('semantic token highlighting', function() end) it('prevents starting semantic token highlighting with invalid conditions', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start_client({ name = 'dummy', cmd = server.cmd }) - notifications = {} - vim.notify = function(...) table.insert(notifications, 1, {...}) end - ]]) - eq(false, exec_lua('return vim.lsp.buf_is_attached(bufnr, client_id)')) + local client_id = exec_lua(function() + _G.notifications = {} + --- @diagnostic disable-next-line:duplicate-set-field + vim.notify = function(...) + table.insert(_G.notifications, 1, { ... }) + end + return vim.lsp.start_client({ name = 'dummy', cmd = _G.server.cmd }) + end) + eq(false, exec_lua('return vim.lsp.buf_is_attached(0, ...)', client_id)) insert(text) - local notifications = exec_lua([[ - vim.lsp.semantic_tokens.start(bufnr, client_id) - return notifications - ]]) - matches('%[LSP%] Client with id %d not attached to buffer %d', notifications[1][1]) - - notifications = exec_lua([[ - vim.lsp.semantic_tokens.start(bufnr, client_id + 1) - return notifications - ]]) - matches('%[LSP%] No client with id %d', notifications[1][1]) + matches( + '%[LSP%] Client with id %d not attached to buffer %d', + exec_lua(function() + vim.lsp.semantic_tokens.start(0, client_id) + return _G.notifications[1][1] + end) + ) + + matches( + '%[LSP%] No client with id %d', + exec_lua(function() + vim.lsp.semantic_tokens.start(0, client_id + 1) + return _G.notifications[1][1] + end) + ) end) it( 'opt-out: does not activate semantic token highlighting if disabled in client attach', function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ + local client_id = exec_lua(function() + return vim.lsp.start({ name = 'dummy', - cmd = server.cmd, - on_attach = vim.schedule_wrap(function(client, bufnr) + cmd = _G.server.cmd, + --- @param client vim.lsp.Client + on_attach = vim.schedule_wrap(function(client, _bufnr) client.server_capabilities.semanticTokensProvider = nil end), }) - ]]) - eq(true, exec_lua('return vim.lsp.buf_is_attached(bufnr, client_id)')) + end) + eq(true, exec_lua('return vim.lsp.buf_is_attached(0, ...)', client_id)) insert(text) @@ -520,13 +516,18 @@ describe('semantic token highlighting', function() ]], } - local notifications = exec_lua([[ - local notifications = {} - vim.notify = function(...) table.insert(notifications, 1, {...}) end - vim.lsp.semantic_tokens.start(bufnr, client_id) - return notifications - ]]) - eq('[LSP] Server does not support semantic tokens', notifications[1][1]) + eq( + '[LSP] Server does not support semantic tokens', + exec_lua(function() + local notifications = {} + --- @diagnostic disable-next-line:duplicate-set-field + vim.notify = function(...) + table.insert(notifications, 1, { ... }) + end + vim.lsp.semantic_tokens.start(0, client_id) + return notifications[1][1] + end) + ) screen:expect { grid = [[ @@ -551,28 +552,32 @@ describe('semantic token highlighting', function() ) it('ignores null responses from the server', function() - exec_lua([[ - local legend, response, edit_response = ... - server2 = _create_server({ + local client_id = exec_lua(function() + _G.server2 = _G._create_server({ capabilities = { semanticTokensProvider = { full = { delta = false }, }, }, handlers = { + --- @param callback function ['textDocument/semanticTokens/full'] = function(_, _, callback) callback(nil, nil) end, - ['textDocument/semanticTokens/full/delta'] = function() + --- @param callback function + ['textDocument/semanticTokens/full/delta'] = function(_, _, callback) callback(nil, nil) end, - } + }, }) - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server2.cmd }) - ]]) - eq(true, exec_lua('return vim.lsp.buf_is_attached(bufnr, client_id)')) + return vim.lsp.start({ name = 'dummy', cmd = _G.server2.cmd }) + end) + eq( + true, + exec_lua(function() + return vim.lsp.buf_is_attached(0, client_id) + end) + ) insert(text) @@ -597,10 +602,9 @@ describe('semantic token highlighting', function() end) it('does not send delta requests if not supported by server', function() - exec_lua( - [[ - local legend, response, edit_response = ... - server2 = _create_server({ + insert(text) + exec_lua(function() + _G.server2 = _G._create_server({ capabilities = { semanticTokensProvider = { full = { delta = false }, @@ -614,18 +618,11 @@ describe('semantic token highlighting', function() ['textDocument/semanticTokens/full/delta'] = function(_, _, callback) callback(nil, vim.fn.json_decode(edit_response)) end, - } + }, }) - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server2.cmd }) - ]], - legend, - response, - edit_response - ) + return vim.lsp.start({ name = 'dummy', cmd = _G.server2.cmd }) + end) - insert(text) screen:expect { grid = [[ #include <iostream> | @@ -669,7 +666,9 @@ describe('semantic token highlighting', function() } local messages = exec_lua('return server2.messages') local token_request_count = 0 - for _, message in ipairs(messages) do + for _, message in + ipairs(messages --[[@as {method:string,params:table}[] ]]) + do assert(message.method ~= 'textDocument/semanticTokens/full/delta', 'delta request received') if message.method == 'textDocument/semanticTokens/full' then token_request_count = token_request_count + 1 @@ -1064,10 +1063,8 @@ b = "as"]], }) do it(test.it, function() exec_lua(create_server_definition) - exec_lua( - [[ - local legend, resp = ... - server = _create_server({ + local client_id = exec_lua(function(legend, resp) + _G.server = _G._create_server({ capabilities = { semanticTokensProvider = { full = { delta = false }, @@ -1078,25 +1075,22 @@ b = "as"]], ['textDocument/semanticTokens/full'] = function(_, _, callback) callback(nil, vim.fn.json_decode(resp)) end, - } + }, }) - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]], - test.legend, - test.response - ) + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end, test.legend, test.response) insert(test.text) test.expected_screen() - local highlights = exec_lua([[ - local semantic_tokens = vim.lsp.semantic_tokens - return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights - ]]) - eq(test.expected, highlights) + eq( + test.expected, + exec_lua(function() + local bufnr = vim.api.nvim_get_current_buf() + return vim.lsp.semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights + end) + ) end) end end) @@ -1449,11 +1443,11 @@ int main() }, }) do it(test.it, function() + local bufnr = n.api.nvim_get_current_buf() + insert(test.text1) exec_lua(create_server_definition) - exec_lua( - [[ - local legend, resp1, resp2 = ... - server = _create_server({ + local client_id = exec_lua(function(legend, resp1, resp2) + _G.server = _G._create_server({ capabilities = { semanticTokensProvider = { full = { delta = true }, @@ -1467,54 +1461,44 @@ int main() ['textDocument/semanticTokens/full/delta'] = function(_, _, callback) callback(nil, vim.fn.json_decode(resp2)) end, - } + }, }) - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })) -- speed up vim.api.nvim_buf_set_lines calls by changing debounce to 10 for these tests - semantic_tokens = vim.lsp.semantic_tokens vim.schedule(function() - semantic_tokens.stop(bufnr, client_id) - semantic_tokens.start(bufnr, client_id, { debounce = 10 }) + vim.lsp.semantic_tokens.stop(bufnr, client_id) + vim.lsp.semantic_tokens.start(bufnr, client_id, { debounce = 10 }) end) - ]], - test.legend, - test.response1, - test.response2 - ) - - insert(test.text1) + return client_id + end, test.legend, test.response1, test.response2) test.expected_screen1() - local highlights = exec_lua([[ - return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights - ]]) - - eq(test.expected1, highlights) + eq( + test.expected1, + exec_lua(function() + return vim.lsp.semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights + end) + ) if test.edit then feed(test.edit) else - exec_lua( - [[ - local text = ... - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.fn.split(text, "\n")) + exec_lua(function(text) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.fn.split(text, '\n')) vim.wait(15) -- wait for debounce - ]], - test.text2 - ) + end, test.text2) end test.expected_screen2() - highlights = exec_lua([[ - return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights - ]]) - - eq(test.expected2, highlights) + eq( + test.expected2, + exec_lua(function() + return vim.lsp.semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights + end) + ) end) end end) diff --git a/test/functional/plugin/lsp/testutil.lua b/test/functional/plugin/lsp/testutil.lua index 3430a1e1a3..a36cbac568 100644 --- a/test/functional/plugin/lsp/testutil.lua +++ b/test/functional/plugin/lsp/testutil.lua @@ -21,8 +21,35 @@ function M.clear_notrace() } end -M.create_server_definition = [[ - function _create_server(opts) +M.create_tcp_echo_server = function() + --- Create a TCP server that echos the first message it receives. + --- @param host string + ---@return uv.uv_tcp_t + ---@return integer + ---@return fun():string|nil + function _G._create_tcp_server(host) + local uv = vim.uv + local server = assert(uv.new_tcp()) + local init = nil + server:bind(host, 0) + server:listen(127, function(err) + assert(not err, err) + local socket = assert(uv.new_tcp()) + server:accept(socket) + socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body) + init = body + socket:close() + end)) + end) + local port = server:getsockname().port + return server, port, function() + return init + end + end +end + +M.create_server_definition = function() + function _G._create_server(opts) opts = opts or {} local server = {} server.messages = {} @@ -42,7 +69,7 @@ M.create_server_definition = [[ handler(method, params, callback) elseif method == 'initialize' then callback(nil, { - capabilities = opts.capabilities or {} + capabilities = opts.capabilities or {}, }) elseif method == 'shutdown' then callback(nil, nil) @@ -54,7 +81,7 @@ M.create_server_definition = [[ function srv.notify(method, params) table.insert(server.messages, { method = method, - params = params + params = params, }) if method == 'exit' then dispatchers.on_exit(0, 15) @@ -74,63 +101,62 @@ M.create_server_definition = [[ return server end -]] +end -- Fake LSP server. M.fake_lsp_code = 'test/functional/fixtures/fake-lsp-server.lua' M.fake_lsp_logfile = 'Xtest-fake-lsp.log' local function fake_lsp_server_setup(test_name, timeout_ms, options, settings) - exec_lua( - [=[ - lsp = require('vim.lsp') - local test_name, fake_lsp_code, fake_lsp_logfile, timeout, options, settings = ... - TEST_RPC_CLIENT_ID = lsp.start_client { + exec_lua(function(fake_lsp_code, fake_lsp_logfile, timeout) + options = options or {} + settings = settings or {} + _G.lsp = require('vim.lsp') + _G.TEST_RPC_CLIENT_ID = _G.lsp.start_client { cmd_env = { - NVIM_LOG_FILE = fake_lsp_logfile; - NVIM_LUA_NOTRACK = "1"; - NVIM_APPNAME = "nvim_lsp_test"; - }; + NVIM_LOG_FILE = fake_lsp_logfile, + NVIM_LUA_NOTRACK = '1', + NVIM_APPNAME = 'nvim_lsp_test', + }, cmd = { - vim.v.progpath, '-l', fake_lsp_code, test_name, tostring(timeout), - }; + vim.v.progpath, + '-l', + fake_lsp_code, + test_name, + tostring(timeout), + }, handlers = setmetatable({}, { - __index = function(t, method) + __index = function(_t, _method) return function(...) return vim.rpcrequest(1, 'handler', ...) end - end; - }); - workspace_folders = {{ + end, + }), + workspace_folders = { + { uri = 'file://' .. vim.uv.cwd(), name = 'test_folder', - }}; - before_init = function(params, config) + }, + }, + before_init = function(_params, _config) vim.schedule(function() - vim.rpcrequest(1, "setup") + vim.rpcrequest(1, 'setup') end) end, on_init = function(client, result) - TEST_RPC_CLIENT = client - vim.rpcrequest(1, "init", result) - end; + _G.TEST_RPC_CLIENT = client + vim.rpcrequest(1, 'init', result) + end, flags = { - allow_incremental_sync = options.allow_incremental_sync or false; - debounce_text_changes = options.debounce_text_changes or 0; - }; - settings = settings; + allow_incremental_sync = options.allow_incremental_sync or false, + debounce_text_changes = options.debounce_text_changes or 0, + }, + settings = settings, on_exit = function(...) - vim.rpcnotify(1, "exit", ...) - end; + vim.rpcnotify(1, 'exit', ...) + end, } - ]=], - test_name, - M.fake_lsp_code, - M.fake_lsp_logfile, - timeout_ms or 1e3, - options or {}, - settings or {} - ) + end, M.fake_lsp_code, M.fake_lsp_logfile, timeout_ms or 1e3) end --- @class test.lsp.Config @@ -160,18 +186,13 @@ function M.test_rpc_server(config) -- Workaround for not being able to yield() inside __index for Lua 5.1 :( -- Otherwise I would just return the value here. return function(...) - return exec_lua( - [=[ - local name = ... - if type(TEST_RPC_CLIENT[name]) == 'function' then - return TEST_RPC_CLIENT[name](select(2, ...)) - else - return TEST_RPC_CLIENT[name] - end - ]=], - name, - ... - ) + return exec_lua(function(...) + if type(_G.TEST_RPC_CLIENT[name]) == 'function' then + return _G.TEST_RPC_CLIENT[name](...) + else + return _G.TEST_RPC_CLIENT[name] + end + end, ...) end end, }) diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua index 6c6dec0667..64d58eeffd 100644 --- a/test/functional/plugin/lsp/utils_spec.lua +++ b/test/functional/plugin/lsp/utils_spec.lua @@ -11,21 +11,11 @@ describe('vim.lsp.util', function() describe('stylize_markdown', function() local stylize_markdown = function(content, opts) - return exec_lua( - [[ - local bufnr = vim.uri_to_bufnr("file:///fake/uri") + return exec_lua(function() + local bufnr = vim.uri_to_bufnr('file:///fake/uri') vim.fn.bufload(bufnr) - - local args = { ... } - local content = args[1] - local opts = args[2] - local stripped_content = vim.lsp.util.stylize_markdown(bufnr, content, opts) - - return stripped_content - ]], - content, - opts - ) + return vim.lsp.util.stylize_markdown(bufnr, content, opts) + end) end it('code fences', function() @@ -93,9 +83,64 @@ describe('vim.lsp.util', function() end) end) - describe('normalize_markdown', function() + 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 [[ + local result = exec_lua(function() local lines = { 'foo', '', @@ -103,25 +148,25 @@ describe('vim.lsp.util', function() '', 'bar', '', - 'baz' + '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 [[ + 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) @@ -129,19 +174,14 @@ describe('vim.lsp.util', function() describe('make_floating_popup_options', function() local function assert_anchor(anchor_bias, expected_anchor) - local opts = exec_lua( - [[ - local args = { ... } - local anchor_bias = args[1] - return vim.lsp.util.make_floating_popup_options(30, 10, { anchor_bias = anchor_bias }) - ]], - anchor_bias - ) + 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 - local screen + local screen --- @type test.functional.ui.screen before_each(function() n.clear() screen = Screen.new(80, 80) @@ -221,9 +261,9 @@ describe('vim.lsp.util', function() end) it('bordered window truncates dimensions correctly', function() - local opts = exec_lua([[ + local opts = exec_lua(function() return vim.lsp.util.make_floating_popup_options(100, 100, { border = 'single' }) - ]]) + end) eq(56, opts.height) end) |