diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2023-01-25 18:31:31 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2023-01-25 18:31:31 +0000 |
commit | 9243becbedbb6a1592208051f8fa2b090dcc5e7d (patch) | |
tree | 607c2a862ec3f4399b8766383f6f8e04c4aa43b4 /test/functional/plugin | |
parent | 9e40b6e9e1bc67f2d856adb837ee64dd0e25b717 (diff) | |
parent | 3c48d3c83fc21dbc0841f9210f04bdb073d73cd1 (diff) | |
download | rneovim-usermarks.tar.gz rneovim-usermarks.tar.bz2 rneovim-usermarks.zip |
Merge remote-tracking branch 'upstream/master' into usermarksusermarks
Diffstat (limited to 'test/functional/plugin')
-rw-r--r-- | test/functional/plugin/ccomplete_spec.lua | 61 | ||||
-rw-r--r-- | test/functional/plugin/cfilter_spec.lua | 106 | ||||
-rw-r--r-- | test/functional/plugin/editorconfig_spec.lua | 210 | ||||
-rw-r--r-- | test/functional/plugin/health_spec.lua | 178 | ||||
-rw-r--r-- | test/functional/plugin/lsp/codelens_spec.lua | 35 | ||||
-rw-r--r-- | test/functional/plugin/lsp/helpers.lua | 176 | ||||
-rw-r--r-- | test/functional/plugin/lsp/semantic_tokens_spec.lua | 1219 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 626 | ||||
-rw-r--r-- | test/functional/plugin/man_spec.lua | 38 | ||||
-rw-r--r-- | test/functional/plugin/shada_spec.lua | 73 |
10 files changed, 2500 insertions, 222 deletions
diff --git a/test/functional/plugin/ccomplete_spec.lua b/test/functional/plugin/ccomplete_spec.lua new file mode 100644 index 0000000000..903f16fc73 --- /dev/null +++ b/test/functional/plugin/ccomplete_spec.lua @@ -0,0 +1,61 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local eval = helpers.eval +local feed = helpers.feed +local write_file = helpers.write_file + +describe('ccomplete#Complete', function() + setup(function() + -- Realistic tags generated from neovim source tree using `ctags -R *` + write_file( + 'Xtags', + [[ +augroup_del src/nvim/autocmd.c /^void augroup_del(char *name, bool stupid_legacy_mode)$/;" f typeref:typename:void +augroup_exists src/nvim/autocmd.c /^bool augroup_exists(const char *name)$/;" f typeref:typename:bool +augroup_find src/nvim/autocmd.c /^int augroup_find(const char *name)$/;" f typeref:typename:int +aupat_get_buflocal_nr src/nvim/autocmd.c /^int aupat_get_buflocal_nr(char *pat, int patlen)$/;" f typeref:typename:int +aupat_is_buflocal src/nvim/autocmd.c /^bool aupat_is_buflocal(char *pat, int patlen)$/;" f typeref:typename:bool +expand_get_augroup_name src/nvim/autocmd.c /^char *expand_get_augroup_name(expand_T *xp, int idx)$/;" f typeref:typename:char * +expand_get_event_name src/nvim/autocmd.c /^char *expand_get_event_name(expand_T *xp, int idx)$/;" f typeref:typename:char * +]] + ) + end) + + before_each(function() + clear() + command('set tags=Xtags') + end) + + teardown(function() + os.remove('Xtags') + end) + + it('can complete from Xtags', function() + local completed = eval('ccomplete#Complete(0, "a")') + eq(5, #completed) + eq('augroup_del(', completed[1].word) + eq('f', completed[1].kind) + + local aupat = eval('ccomplete#Complete(0, "aupat")') + eq(2, #aupat) + eq('aupat_get_buflocal_nr(', aupat[1].word) + eq('f', aupat[1].kind) + end) + + it('does not error when returning no matches', function() + local completed = eval('ccomplete#Complete(0, "doesnotmatch")') + eq({}, completed) + end) + + it('can find the beginning of a word for C', function() + command('set filetype=c') + feed('i int something = augroup') + local result = eval('ccomplete#Complete(1, "")') + eq(#' int something = ', result) + + local completed = eval('ccomplete#Complete(0, "augroup")') + eq(3, #completed) + end) +end) diff --git a/test/functional/plugin/cfilter_spec.lua b/test/functional/plugin/cfilter_spec.lua new file mode 100644 index 0000000000..8b1e75b495 --- /dev/null +++ b/test/functional/plugin/cfilter_spec.lua @@ -0,0 +1,106 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local funcs = helpers.funcs + +describe('cfilter.lua', function() + before_each(function() + clear() + command('packadd cfilter') + end) + + for _, list in ipairs({ + { + name = 'Cfilter', + get = funcs.getqflist, + set = funcs.setqflist, + }, + { + name = 'Lfilter', + get = function() + return funcs.getloclist(0) + end, + set = function(items) + return funcs.setloclist(0, items) + end, + }, + }) do + local filter = function(s, bang) + if not bang then + bang = '' + else + bang = '!' + end + + command(string.format('%s%s %s', list.name, bang, s)) + end + + describe((':%s'):format(list.name), function() + it('does not error on empty list', function() + filter('nothing') + eq({}, funcs.getqflist()) + end) + + it('requires an argument', function() + local ok = pcall(filter, '') + eq(false, ok) + end) + + local test = function(name, s, res, map, bang) + it(('%s (%s)'):format(name, s), function() + list.set({ + { filename = 'foo', lnum = 1, text = 'bar' }, + { filename = 'foo', lnum = 2, text = 'baz' }, + { filename = 'foo', lnum = 3, text = 'zed' }, + }) + + filter(s, bang) + + local got = list.get() + if map then + got = map(got) + end + eq(res, got) + end) + end + + local toname = function(qflist) + return funcs.map(qflist, 'v:val.text') + end + + test('filters with no matches', 'does not match', {}) + + test('filters with matches', 'ba', { 'bar', 'baz' }, toname) + test('filters with matches', 'z', { 'baz', 'zed' }, toname) + test('filters with matches', '^z', { 'zed' }, toname) + test('filters with not matches', '^z', { 'bar', 'baz' }, toname, true) + + it('also supports using the / register', function() + list.set({ + { filename = 'foo', lnum = 1, text = 'bar' }, + { filename = 'foo', lnum = 2, text = 'baz' }, + { filename = 'foo', lnum = 3, text = 'zed' }, + }) + + funcs.setreg('/', 'ba') + filter('/') + + eq({ 'bar', 'baz' }, toname(list.get())) + end) + + it('also supports using the / register with bang', function() + list.set({ + { filename = 'foo', lnum = 1, text = 'bar' }, + { filename = 'foo', lnum = 2, text = 'baz' }, + { filename = 'foo', lnum = 3, text = 'zed' }, + }) + + funcs.setreg('/', 'ba') + filter('/', true) + + eq({ 'zed' }, toname(list.get())) + end) + end) + end +end) diff --git a/test/functional/plugin/editorconfig_spec.lua b/test/functional/plugin/editorconfig_spec.lua new file mode 100644 index 0000000000..e6a2550aba --- /dev/null +++ b/test/functional/plugin/editorconfig_spec.lua @@ -0,0 +1,210 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local pathsep = helpers.get_pathsep() +local curbufmeths = helpers.curbufmeths +local funcs = helpers.funcs +local meths = helpers.meths + +local testdir = 'Xtest-editorconfig' + +local function test_case(name, expected) + local filename = testdir .. pathsep .. name + command('edit ' .. filename) + for opt, val in pairs(expected) do + eq(val, curbufmeths.get_option(opt), name) + end +end + +setup(function() + helpers.mkdir_p(testdir) + helpers.write_file( + testdir .. pathsep .. '.editorconfig', + [[ + root = true + + [3_space.txt] + indent_style = space + indent_size = 3 + tab_width = 3 + + [4_space.py] + indent_style = space + indent_size = 4 + tab_width = 8 + + [space.txt] + indent_style = space + indent_size = tab + + [tab.txt] + indent_style = tab + + [4_tab.txt] + indent_style = tab + indent_size = 4 + tab_width = 4 + + [4_tab_width_of_8.txt] + indent_style = tab + indent_size = 4 + tab_width = 8 + + [lf.txt] + end_of_line = lf + + [crlf.txt] + end_of_line = crlf + + [cr.txt] + end_of_line = cr + + [utf-8.txt] + charset = utf-8 + + [utf-8-bom.txt] + charset = utf-8-bom + + [utf-16be.txt] + charset = utf-16be + + [utf-16le.txt] + charset = utf-16le + + [latin1.txt] + charset = latin1 + + [with_newline.txt] + insert_final_newline = true + + [without_newline.txt] + insert_final_newline = false + + [trim.txt] + trim_trailing_whitespace = true + + [no_trim.txt] + trim_trailing_whitespace = false + + [max_line_length.txt] + max_line_length = 42 + ]] + ) +end) + +teardown(function() + helpers.rmdir(testdir) +end) + +describe('editorconfig', function() + before_each(function() + -- Remove -u NONE so that plugins (i.e. editorconfig.lua) are loaded + clear({ args_rm = { '-u' } }) + end) + + it('sets indent options', function() + test_case('3_space.txt', { + expandtab = true, + shiftwidth = 3, + softtabstop = -1, + tabstop = 3, + }) + + test_case('4_space.py', { + expandtab = true, + shiftwidth = 4, + softtabstop = -1, + tabstop = 8, + }) + + test_case('space.txt', { + expandtab = true, + shiftwidth = 0, + softtabstop = 0, + }) + + test_case('tab.txt', { + expandtab = false, + shiftwidth = 0, + softtabstop = 0, + }) + + test_case('4_tab.txt', { + expandtab = false, + shiftwidth = 4, + softtabstop = -1, + tabstop = 4, + }) + + test_case('4_tab_width_of_8.txt', { + expandtab = false, + shiftwidth = 4, + softtabstop = -1, + tabstop = 8, + }) + end) + + it('sets end-of-line options', function() + test_case('lf.txt', { fileformat = 'unix' }) + test_case('crlf.txt', { fileformat = 'dos' }) + test_case('cr.txt', { fileformat = 'mac' }) + end) + + it('sets encoding options', function() + test_case('utf-8.txt', { fileencoding = 'utf-8', bomb = false }) + test_case('utf-8-bom.txt', { fileencoding = 'utf-8', bomb = true }) + test_case('utf-16be.txt', { fileencoding = 'utf-16', bomb = false }) + test_case('utf-16le.txt', { fileencoding = 'utf-16le', bomb = false }) + test_case('latin1.txt', { fileencoding = 'latin1', bomb = false }) + end) + + it('sets newline options', function() + test_case('with_newline.txt', { fixendofline = true, endofline = true }) + test_case('without_newline.txt', { fixendofline = false, endofline = false }) + end) + + it('respects trim_trailing_whitespace', function() + local filename = testdir .. pathsep .. 'trim.txt' + -- luacheck: push ignore 613 + local untrimmed = [[ +This line ends in whitespace +So does this one +And this one +But not this one +]] + -- luacheck: pop + local trimmed = untrimmed:gsub('%s+\n', '\n') + + helpers.write_file(filename, untrimmed) + command('edit ' .. filename) + command('write') + command('bdelete') + eq(trimmed, helpers.read_file(filename)) + + filename = testdir .. pathsep .. 'no_trim.txt' + helpers.write_file(filename, untrimmed) + command('edit ' .. filename) + command('write') + command('bdelete') + eq(untrimmed, helpers.read_file(filename)) + end) + + it('sets textwidth', function() + test_case('max_line_length.txt', { textwidth = 42 }) + end) + + it('can be disabled globally', function() + meths.set_var('editorconfig', false) + meths.set_option_value('shiftwidth', 42, {}) + test_case('3_space.txt', { shiftwidth = 42 }) + end) + + it('can be disabled per-buffer', function() + meths.set_option_value('shiftwidth', 42, {}) + local bufnr = funcs.bufadd(testdir .. pathsep .. '3_space.txt') + meths.buf_set_var(bufnr, 'editorconfig', false) + test_case('3_space.txt', { shiftwidth = 42 }) + test_case('4_space.py', { shiftwidth = 4 }) + end) +end) diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua index ba66117fb1..97d32313e5 100644 --- a/test/functional/plugin/health_spec.lua +++ b/test/functional/plugin/health_spec.lua @@ -5,7 +5,7 @@ local Screen = require('test.functional.ui.screen') local clear = helpers.clear local curbuf_contents = helpers.curbuf_contents local command = helpers.command -local eq, neq = helpers.eq, helpers.neq +local eq, neq, matches = helpers.eq, helpers.neq, helpers.matches local getcompletion = helpers.funcs.getcompletion describe(':checkhealth', function() @@ -29,8 +29,7 @@ describe(':checkhealth', function() -- Do this after startup, otherwise it just breaks $VIMRUNTIME. command("let $VIM='zub'") command("checkhealth nvim") - eq("ERROR: $VIM is invalid: zub", - string.match(curbuf_contents(), "ERROR: $VIM .* zub")) + matches('ERROR $VIM .* zub', curbuf_contents()) end) it('completions can be listed via getcompletion()', function() clear() @@ -56,21 +55,22 @@ describe('health.vim', function() command("checkhealth full_render") helpers.expect([[ + ============================================================================== full_render: health#full_render#check - ======================================================================== - ## report 1 - - OK: life is fine - - WARNING: no what installed - - ADVICE: - - pip what - - make what - - ## report 2 - - INFO: stuff is stable - - ERROR: why no hardcopy - - ADVICE: - - :help |:hardcopy| - - :help |:TOhtml| + + report 1 ~ + - OK life is fine + - WARNING no what installed + - ADVICE: + - pip what + - make what + + report 2 ~ + - stuff is stable + - ERROR why no hardcopy + - ADVICE: + - :help |:hardcopy| + - :help |:TOhtml| ]]) end) @@ -78,26 +78,29 @@ describe('health.vim', function() command("checkhealth success1 success2 test_plug") helpers.expect([[ + ============================================================================== success1: health#success1#check - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + + report 2 ~ + - OK nothing to see here + ============================================================================== success2: health#success2#check - ======================================================================== - ## another 1 - - OK: ok + another 1 ~ + - OK ok + + ============================================================================== test_plug: require("test_plug.health").check() - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + + report 2 ~ + - OK nothing to see here ]]) end) @@ -107,13 +110,14 @@ describe('health.vim', function() -- and the Lua healthcheck is used instead. helpers.expect([[ + ============================================================================== test_plug: require("test_plug.health").check() - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + + report 2 ~ + - OK nothing to see here ]]) end) @@ -121,13 +125,14 @@ describe('health.vim', function() command("checkhealth test_plug.submodule") helpers.expect([[ + ============================================================================== test_plug.submodule: require("test_plug.submodule.health").check() - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + + report 2 ~ + - OK nothing to see here ]]) end) @@ -138,30 +143,34 @@ describe('health.vim', function() local received = table.concat(buf_lines, '\n', 1, #buf_lines - 5) local expected = helpers.dedent([[ + ============================================================================== test_plug: require("test_plug.health").check() - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + + report 2 ~ + - OK nothing to see here + ============================================================================== test_plug.submodule: require("test_plug.submodule.health").check() - ======================================================================== - ## report 1 - - OK: everything is fine - ## report 2 - - OK: nothing to see here + report 1 ~ + - OK everything is fine + report 2 ~ + - OK nothing to see here + + ============================================================================== test_plug.submodule_empty: require("test_plug.submodule_empty.health").check() - ======================================================================== - - ERROR: The healthcheck report for "test_plug.submodule_empty" plugin is empty. + - ERROR The healthcheck report for "test_plug.submodule_empty" plugin is empty. + + ============================================================================== test_plug.submodule_failed: require("test_plug.submodule_failed.health").check() - ======================================================================== - - ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception: - function health#check, line 20]]) + + - ERROR Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception: + function health#check, line 25]]) eq(expected, received) end) @@ -169,11 +178,12 @@ describe('health.vim', function() command("checkhealth broken") helpers.expect([[ + ============================================================================== broken: health#broken#check - ======================================================================== - - ERROR: Failed to run healthcheck for "broken" plugin. Exception: - function health#check[20]..health#broken#check, line 1 - caused an error + + - ERROR Failed to run healthcheck for "broken" plugin. Exception: + function health#check[25]..health#broken#check, line 1 + caused an error ]]) end) @@ -181,9 +191,10 @@ describe('health.vim', function() command("checkhealth test_plug.submodule_empty") helpers.expect([[ + ============================================================================== test_plug.submodule_empty: require("test_plug.submodule_empty.health").check() - ======================================================================== - - ERROR: The healthcheck report for "test_plug.submodule_empty" plugin is empty. + + - ERROR The healthcheck report for "test_plug.submodule_empty" plugin is empty. ]]) end) @@ -198,38 +209,38 @@ describe('health.vim', function() local expected = global_helpers.dedent([[ + ============================================================================== test_plug.submodule_failed: require("test_plug.submodule_failed.health").check() - ======================================================================== - - ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception: - function health#check, line 20]]) + + - ERROR Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception: + function health#check, line 25]]) eq(expected, received) end) it("highlights OK, ERROR", function() - local screen = Screen.new(72, 10) + local screen = Screen.new(50, 12) screen:attach() screen:set_default_attr_ids({ Ok = { foreground = Screen.colors.Grey3, background = 6291200 }, Error = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, - Heading = { bold=true, foreground=Screen.colors.Magenta }, - Heading2 = { foreground = Screen.colors.SlateBlue }, - Bar = { foreground = 0x6a0dad }, - Bullet = { bold=true, foreground=Screen.colors.Brown }, + Heading = { foreground = tonumber('0x6a0dad') }, + Bar = { foreground = Screen.colors.LightGrey, background = Screen.colors.DarkGrey }, }) command("checkhealth foo success1") - command("1tabclose") - command("set laststatus=0") + command("set nowrap laststatus=0") screen:expect{grid=[[ - ^ | - {Heading:foo: } | - {Bar:========================================================================}| - {Bullet: -} {Error:ERROR}: No healthcheck found for "foo" plugin. | - | - {Heading:success1: health#success1#check} | - {Bar:========================================================================}| - {Heading2:##}{Heading: report 1} | - {Bullet: -} {Ok:OK}: everything is fine | - | + ^ | + {Bar:──────────────────────────────────────────────────}| + {Heading:foo: } | + | + - {Error:ERROR} No healthcheck found for "foo" plugin. | + | + {Bar:──────────────────────────────────────────────────}| + {Heading:success1: health#success1#check} | + | + {Heading:report 1} | + - {Ok:OK} everything is fine | + | ]]} end) @@ -238,9 +249,10 @@ describe('health.vim', function() -- luacheck: ignore 613 helpers.expect([[ + ============================================================================== non_existent_healthcheck: - ======================================================================== - - ERROR: No healthcheck found for "non_existent_healthcheck" plugin. + + - ERROR No healthcheck found for "non_existent_healthcheck" plugin. ]]) end) diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua index ecc2f579b8..3d7a15a191 100644 --- a/test/functional/plugin/lsp/codelens_spec.lua +++ b/test/functional/plugin/lsp/codelens_spec.lua @@ -58,6 +58,41 @@ describe('vim.lsp.codelens', function() ]], bufnr) 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 = vim.uri_to_bufnr(fake_uri) + local lines = {'So', 'many', 'lines'} + vim.fn.bufload(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + return bufnr + ]], fake_uri) + + exec_lua([[ + local bufnr = ... + local lenses = { + { + range = { + start = { line = 0, character = 0, }, + ['end'] = { line = 0, character = 0 } + }, + command = { title = 'Lens1', command = 'Dummy' } + }, + } + vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr}) + ]], bufnr) + + local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) + eq(1, #stored_lenses) + + exec_lua([[ + vim.lsp.codelens.clear() + ]]) + stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) + eq(0, #stored_lenses) end) end) diff --git a/test/functional/plugin/lsp/helpers.lua b/test/functional/plugin/lsp/helpers.lua new file mode 100644 index 0000000000..028ccb9e2c --- /dev/null +++ b/test/functional/plugin/lsp/helpers.lua @@ -0,0 +1,176 @@ +local helpers = require('test.functional.helpers')(nil) + +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local run = helpers.run +local stop = helpers.stop +local NIL = helpers.NIL + +local M = {} + +function M.clear_notrace() + -- problem: here be dragons + -- solution: don't look too closely for dragons + clear {env={ + NVIM_LUA_NOTRACK="1"; + VIMRUNTIME=os.getenv"VIMRUNTIME"; + }} +end + +M.create_server_definition = [[ + function _create_server(opts) + opts = opts or {} + local server = {} + server.messages = {} + + function server.cmd(dispatchers) + local closing = false + local handlers = opts.handlers or {} + local srv = {} + + function srv.request(method, params, callback) + table.insert(server.messages, { + method = method, + params = params, + }) + local handler = handlers[method] + if handler then + local response, err = handler(method, params) + callback(err, response) + elseif method == 'initialize' then + callback(nil, { + capabilities = opts.capabilities or {} + }) + elseif method == 'shutdown' then + callback(nil, nil) + end + local request_id = #server.messages + return true, request_id + end + + function srv.notify(method, params) + table.insert(server.messages, { + method = method, + params = params + }) + if method == 'exit' then + dispatchers.on_exit(0, 15) + end + end + + function srv.is_closing() + return closing + end + + function srv.terminate() + closing = true + end + + return srv + end + + return server + 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, fixture_filename, logfile, timeout, options, settings = ... + TEST_RPC_CLIENT_ID = lsp.start_client { + cmd_env = { + NVIM_LOG_FILE = logfile; + NVIM_LUA_NOTRACK = "1"; + }; + cmd = { + vim.v.progpath, '-Es', '-u', 'NONE', '--headless', + "-c", string.format("lua TEST_NAME = %q", test_name), + "-c", string.format("lua TIMEOUT = %d", timeout), + "-c", "luafile "..fixture_filename, + }; + handlers = setmetatable({}, { + __index = function(t, method) + return function(...) + return vim.rpcrequest(1, 'handler', ...) + end + end; + }); + workspace_folders = {{ + uri = 'file://' .. vim.loop.cwd(), + name = 'test_folder', + }}; + on_init = function(client, result) + 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; + on_exit = function(...) + vim.rpcnotify(1, "exit", ...) + end; + } + ]=], test_name, M.fake_lsp_code, M.fake_lsp_logfile, timeout_ms or 1e3, options or {}, settings or {}) +end + +function M.test_rpc_server(config) + if config.test_name then + M.clear_notrace() + fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3, config.options, config.settings) + end + local client = setmetatable({}, { + __index = function(_, name) + -- 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, ...) + end + end; + }) + local code, signal + local function on_request(method, args) + if method == "init" then + if config.on_init then + config.on_init(client, unpack(args)) + end + return NIL + end + if method == 'handler' then + if config.on_handler then + config.on_handler(unpack(args)) + end + end + return NIL + end + local function on_notify(method, args) + if method == 'exit' then + code, signal = unpack(args) + return stop() + end + end + -- TODO specify timeout? + -- run(on_request, on_notify, config.on_setup, 1000) + run(on_request, on_notify, config.on_setup) + if config.on_exit then + config.on_exit(code, signal) + end + stop() + if config.test_name then + exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })") + end +end + +return M diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua new file mode 100644 index 0000000000..9c1ba86fe1 --- /dev/null +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -0,0 +1,1219 @@ +local helpers = require('test.functional.helpers')(after_each) +local lsp_helpers = require('test.functional.plugin.lsp.helpers') +local Screen = require('test.functional.ui.screen') + +local command = helpers.command +local dedent = helpers.dedent +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local feed = helpers.feed +local feed_command = helpers.feed_command +local insert = helpers.insert +local matches = helpers.matches + +local clear_notrace = lsp_helpers.clear_notrace +local create_server_definition = lsp_helpers.create_server_definition + +before_each(function() + clear_notrace() +end) + +after_each(function() + exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })") +end) + +describe('semantic token highlighting', function() + + describe('general', function() + local text = dedent([[ + #include <iostream> + + int main() + { + int x; + #ifdef __cplusplus + std::cout << x << "\n"; + #else + printf("%d\n", x); + #endif + } + }]]) + + local legend = [[{ + "tokenTypes": [ + "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment" + ], + "tokenModifiers": [ + "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope" + ] + }]] + + local response = [[{ + "data": [ 2, 4, 4, 3, 8193, 2, 8, 1, 1, 1025, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1024, 1, 0, 5, 20, 0, 1, 0, 22, 20, 0, 1, 0, 6, 20, 0 ], + "resultId": 1 + }]] + + local edit_response = [[{ + "edits": [ {"data": [ 2, 8, 1, 3, 8193, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 3, 8192 ], "deleteCount": 25, "start": 5 } ], + "resultId":"2" + }]] + + local screen + before_each(function() + screen = Screen.new(40, 16) + screen:attach() + screen:set_default_attr_ids { + [1] = { bold = true, foreground = Screen.colors.Blue1 }; + [2] = { foreground = Screen.colors.DarkCyan }; + [3] = { foreground = Screen.colors.SlateBlue }; + [4] = { bold = true, foreground = Screen.colors.SeaGreen }; + [5] = { foreground = tonumber('0x6a0dad') }; + [6] = { foreground = Screen.colors.Blue1 }; + [7] = { bold = true, foreground = Screen.colors.DarkCyan }; + [8] = { bold = true, foreground = Screen.colors.SlateBlue }; + } + command([[ hi link @namespace Type ]]) + command([[ hi link @function Special ]]) + command([[ hi @declaration gui=bold ]]) + + exec_lua(create_server_definition) + exec_lua([[ + local legend, response, edit_response = ... + server = _create_server({ + capabilities = { + semanticTokensProvider = { + full = { delta = true }, + legend = vim.fn.json_decode(legend), + }, + }, + handlers = { + ['textDocument/semanticTokens/full'] = function() + return vim.fn.json_decode(response) + end, + ['textDocument/semanticTokens/full/delta'] = function() + return vim.fn.json_decode(edit_response) + end, + } + }) + ]], legend, response, edit_response) + end) + + it('buffer is highlighted when attached', 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) + + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + 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([[ + vim.notify = function() end + vim.lsp.buf_detach_client(bufnr, client_id) + ]]) + + screen:expect { grid = [[ + #include <iostream> | + | + int main() | + { | + int x; | + #ifdef __cplusplus | + std::cout << x << "\n"; | + #else | + printf("%d\n", x); | + #endif | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + end) + + 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 }) + ]]) + + insert(text) + + exec_lua([[ + vim.notify = function() end + vim.lsp.semantic_tokens.stop(bufnr, client_id) + ]]) + + screen:expect { grid = [[ + #include <iostream> | + | + int main() | + { | + int x; | + #ifdef __cplusplus | + std::cout << x << "\n"; | + #else | + printf("%d\n", x); | + #endif | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + + exec_lua([[ + vim.lsp.semantic_tokens.start(bufnr, client_id) + ]]) + + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + 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) + + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + + exec_lua([[ + vim.lsp.semantic_tokens.force_refresh(bufnr) + ]]) + + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]], unchanged = true } + + local messages = exec_lua('return server.messages') + local token_request_count = 0 + for _, message in ipairs(messages) 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 + end + end + eq(2, token_request_count) + 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 }) + ]]) + + 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) + 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) + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + feed_command('%s/int x/int x()/') + feed_command('noh') + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + ^int {8:x}(); | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {3:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + } | + {1:~ }| + {1:~ }| + {1:~ }| + :noh | + ]] } + 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)")) + + 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]) + 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({ + name = 'dummy', + cmd = server.cmd, + 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)")) + + insert(text) + + screen:expect { grid = [[ + #include <iostream> | + | + int main() | + { | + int x; | + #ifdef __cplusplus | + std::cout << x << "\n"; | + #else | + printf("%d\n", x); | + #endif | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + + 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]) + + screen:expect { grid = [[ + #include <iostream> | + | + int main() | + { | + int x; | + #ifdef __cplusplus | + std::cout << x << "\n"; | + #else | + printf("%d\n", x); | + #endif | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]], unchanged = true } + end) + + it('ignores null responses from the server', function() + exec_lua([[ + local legend, response, edit_response = ... + server2 = _create_server({ + capabilities = { + semanticTokensProvider = { + full = { delta = false }, + }, + }, + handlers = { + ['textDocument/semanticTokens/full'] = function() + return nil + end, + ['textDocument/semanticTokens/full/delta'] = function() + return 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)")) + + insert(text) + + screen:expect { grid = [[ + #include <iostream> | + | + int main() | + { | + int x; | + #ifdef __cplusplus | + std::cout << x << "\n"; | + #else | + printf("%d\n", x); | + #endif | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + end) + + it('does not send delta requests if not supported by server', function() + exec_lua([[ + local legend, response, edit_response = ... + server2 = _create_server({ + capabilities = { + semanticTokensProvider = { + full = { delta = false }, + legend = vim.fn.json_decode(legend), + }, + }, + handlers = { + ['textDocument/semanticTokens/full'] = function() + return vim.fn.json_decode(response) + end, + ['textDocument/semanticTokens/full/delta'] = function() + return 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) + + insert(text) + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + int {7:x}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]] } + feed_command('%s/int x/int x()/') + feed_command('noh') + + -- the highlights don't change because our fake server sent the exact + -- same result for the same method (the full request). "x" would have + -- changed to highlight index 3 had we sent a delta request + screen:expect { grid = [[ + #include <iostream> | + | + int {8:main}() | + { | + ^int {7:x}(); | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {2:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:#endif} | + } | + } | + {1:~ }| + {1:~ }| + {1:~ }| + :noh | + ]] } + local messages = exec_lua('return server2.messages') + local token_request_count = 0 + for _, message in ipairs(messages) 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 + end + end + eq(2, token_request_count) + end) + end) + + describe('token array decoding', function() + for _, test in ipairs({ + { + it = 'clangd-15 on C', + text = [[char* foo = "\n";]], + response = [[{"data": [0, 6, 3, 0, 8193], "resultId": "1"}]], + legend = [[{ + "tokenTypes": [ + "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment" + ], + "tokenModifiers": [ + "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope" + ] + }]], + expected = { + { + line = 0, + modifiers = { + 'declaration', + 'globalScope', + }, + start_col = 6, + end_col = 9, + type = 'variable', + extmark_added = true, + }, + }, + }, + { + it = 'clangd-15 on C++', + text = [[#include <iostream> +int main() +{ + #ifdef __cplusplus + const int x = 1; + std::cout << x << std::endl; + #else + comment + #endif +}]], + response = [[{"data": [1, 4, 4, 3, 8193, 2, 9, 11, 19, 8192, 1, 12, 1, 1, 1033, 1, 2, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1032, 0, 5, 3, 15, 8448, 0, 5, 4, 3, 8448, 1, 0, 7, 20, 0, 1, 0, 11, 20, 0, 1, 0, 8, 20, 0], "resultId": "1"}]], + legend = [[{ + "tokenTypes": [ + "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment" + ], + "tokenModifiers": [ + "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope" + ] + }]], + expected = { + { -- main + line = 1, + modifiers = { 'declaration', 'globalScope' }, + start_col = 4, + end_col = 8, + type = 'function', + extmark_added = true, + }, + { -- __cplusplus + line = 3, + modifiers = { 'globalScope' }, + start_col = 9, + end_col = 20, + type = 'macro', + extmark_added = true, + }, + { -- x + line = 4, + modifiers = { 'declaration', 'readonly', 'functionScope' }, + start_col = 12, + end_col = 13, + type = 'variable', + extmark_added = true, + }, + { -- std + line = 5, + modifiers = { 'defaultLibrary', 'globalScope' }, + start_col = 2, + end_col = 5, + type = 'namespace', + extmark_added = true, + }, + { -- cout + line = 5, + modifiers = { 'defaultLibrary', 'globalScope' }, + start_col = 7, + end_col = 11, + type = 'variable', + extmark_added = true, + }, + { -- x + line = 5, + modifiers = { 'readonly', 'functionScope' }, + start_col = 15, + end_col = 16, + type = 'variable', + extmark_added = true, + }, + { -- std + line = 5, + modifiers = { 'defaultLibrary', 'globalScope' }, + start_col = 20, + end_col = 23, + type = 'namespace', + extmark_added = true, + }, + { -- endl + line = 5, + modifiers = { 'defaultLibrary', 'globalScope' }, + start_col = 25, + end_col = 29, + type = 'function', + extmark_added = true, + }, + { -- #else comment #endif + line = 6, + modifiers = {}, + start_col = 0, + end_col = 7, + type = 'comment', + extmark_added = true, + }, + { + line = 7, + modifiers = {}, + start_col = 0, + end_col = 11, + type = 'comment', + extmark_added = true, + }, + { + line = 8, + modifiers = {}, + start_col = 0, + end_col = 8, + type = 'comment', + extmark_added = true, + }, + }, + }, + { + it = 'sumneko_lua', + text = [[-- comment +local a = 1 +b = "as"]], + response = [[{"data": [0, 0, 10, 17, 0, 1, 6, 1, 8, 1, 1, 0, 1, 8, 8]}]], + legend = [[{ + "tokenTypes": [ + "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator" + ], + "tokenModifiers": [ + "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary" + ] + }]], + expected = { + { + line = 0, + modifiers = {}, + start_col = 0, + end_col = 10, + type = 'comment', -- comment + extmark_added = true, + }, + { + line = 1, + modifiers = { 'declaration' }, -- a + start_col = 6, + end_col = 7, + type = 'variable', + extmark_added = true, + }, + { + line = 2, + modifiers = { 'static' }, -- b (global) + start_col = 0, + end_col = 1, + type = 'variable', + extmark_added = true, + }, + }, + }, + { + it = 'rust-analyzer', + text = [[pub fn main() { + break rust; + /// what? +} +]], + response = [[{"data": [0, 0, 3, 1, 0, 0, 4, 2, 1, 0, 0, 3, 4, 14, 524290, 0, 4, 1, 45, 0, 0, 1, 1, 45, 0, 0, 2, 1, 26, 0, 1, 4, 5, 1, 8192, 0, 6, 4, 52, 0, 0, 4, 1, 48, 0, 1, 4, 9, 0, 1, 1, 0, 1, 26, 0], "resultId": "1"}]], + legend = [[{ + "tokenTypes": [ + "comment", "keyword", "string", "number", "regexp", "operator", "namespace", "type", "struct", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "macro", "variable", + "parameter", "angle", "arithmetic", "attribute", "attributeBracket", "bitwise", "boolean", "brace", "bracket", "builtinAttribute", "builtinType", "character", "colon", "comma", "comparison", "constParameter", "derive", + "dot", "escapeSequence", "formatSpecifier", "generic", "label", "lifetime", "logical", "macroBang", "operator", "parenthesis", "punctuation", "selfKeyword", "semicolon", "typeAlias", "toolModule", "union", "unresolvedReference" + ], + "tokenModifiers": [ + "documentation", "declaration", "definition", "static", "abstract", "deprecated", "readonly", "defaultLibrary", "async", "attribute", "callable", "constant", "consuming", "controlFlow", "crateRoot", "injected", "intraDocLink", + "library", "mutable", "public", "reference", "trait", "unsafe" + ] + }]], + expected = { + { + line = 0, + modifiers = {}, + start_col = 0, + end_col = 3, -- pub + type = 'keyword', + extmark_added = true, + }, + { + line = 0, + modifiers = {}, + start_col = 4, + end_col = 6, -- fn + type = 'keyword', + extmark_added = true, + }, + { + line = 0, + modifiers = { 'declaration', 'public' }, + start_col = 7, + end_col = 11, -- main + type = 'function', + extmark_added = true, + }, + { + line = 0, + modifiers = {}, + start_col = 11, + end_col = 12, + type = 'parenthesis', + extmark_added = true, + }, + { + line = 0, + modifiers = {}, + start_col = 12, + end_col = 13, + type = 'parenthesis', + extmark_added = true, + }, + { + line = 0, + modifiers = {}, + start_col = 14, + end_col = 15, + type = 'brace', + extmark_added = true, + }, + { + line = 1, + modifiers = { 'controlFlow' }, + start_col = 4, + end_col = 9, -- break + type = 'keyword', + extmark_added = true, + }, + { + line = 1, + modifiers = {}, + start_col = 10, + end_col = 13, -- rust + type = 'unresolvedReference', + extmark_added = true, + }, + { + line = 1, + modifiers = {}, + start_col = 13, + end_col = 13, + type = 'semicolon', + extmark_added = true, + }, + { + line = 2, + modifiers = { 'documentation' }, + start_col = 4, + end_col = 11, + type = 'comment', -- /// what? + extmark_added = true, + }, + { + line = 3, + modifiers = {}, + start_col = 0, + end_col = 1, + type = 'brace', + extmark_added = true, + }, + }, + }, + }) do + it(test.it, function() + exec_lua(create_server_definition) + exec_lua([[ + local legend, resp = ... + server = _create_server({ + capabilities = { + semanticTokensProvider = { + full = { delta = false }, + legend = vim.fn.json_decode(legend), + }, + }, + handlers = { + ['textDocument/semanticTokens/full'] = function() + return 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) + + insert(test.text) + + 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) + end) + end + end) + + describe('token decoding with deltas', function() + for _, test in ipairs({ + { + it = 'semantic_tokens_delta: clangd-15 on C', + legend = [[{ + "tokenTypes": [ + "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment" + ], + "tokenModifiers": [ + "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope" + ] + }]], + text1 = [[char* foo = "\n";]], + edit = [[ggO<Esc>]], + response1 = [[{"data": [0, 6, 3, 0, 8193], "resultId": "1"}]], + response2 = [[{"edits": [{ "start": 0, "deleteCount": 1, "data": [1] }], "resultId": "2"}]], + expected1 = { + { + line = 0, + modifiers = { + 'declaration', + 'globalScope', + }, + start_col = 6, + end_col = 9, + type = 'variable', + extmark_added = true, + } + }, + expected2 = { + { + line = 1, + modifiers = { + 'declaration', + 'globalScope', + }, + start_col = 6, + end_col = 9, + type = 'variable', + extmark_added = true, + } + }, + }, + { + it = 'response with multiple delta edits', + legend = [[{ + "tokenTypes": [ + "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment" + ], + "tokenModifiers": [ + "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope" + ] + }]], + text1 = dedent([[ + #include <iostream> + + int main() + { + int x; + #ifdef __cplusplus + std::cout << x << "\n"; + #else + printf("%d\n", x); + #endif + }]]), + text2 = [[#include <iostream> + +int main() +{ + int x(); + double y; +#ifdef __cplusplus + std::cout << x << "\n"; +#else + printf("%d\n", x); +#endif +}]], + response1 = [[{ + "data": [ 2, 4, 4, 3, 8193, 2, 8, 1, 1, 1025, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1024, 1, 0, 5, 20, 0, 1, 0, 22, 20, 0, 1, 0, 6, 20, 0 ], + "resultId": 1 + }]], + response2 = [[{ + "edits": [ {"data": [ 2, 8, 1, 3, 8193, 1, 11, 1, 1, 1025 ], "deleteCount": 5, "start": 5}, {"data": [ 0, 8, 1, 3, 8192 ], "deleteCount": 5, "start": 25 } ], + "resultId":"2" + }]], + expected1 = { + { + line = 2, + start_col = 4, + end_col = 8, + modifiers = { 'declaration', 'globalScope' }, + type = 'function', + extmark_added = true, + }, + { + line = 4, + start_col = 8, + end_col = 9, + modifiers = { 'declaration', 'functionScope' }, + type = 'variable', + extmark_added = true, + }, + { + line = 5, + start_col = 7, + end_col = 18, + modifiers = { 'globalScope' }, + type = 'macro', + extmark_added = true, + }, + { + line = 6, + start_col = 4, + end_col = 7, + modifiers = { 'defaultLibrary', 'globalScope' }, + type = 'namespace', + extmark_added = true, + }, + { + line = 6, + start_col = 9, + end_col = 13, + modifiers = { 'defaultLibrary', 'globalScope' }, + type = 'variable', + extmark_added = true, + }, + { + line = 6, + start_col = 17, + end_col = 18, + extmark_added = true, + modifiers = { 'functionScope' }, + type = 'variable', + }, + { + line = 7, + start_col = 0, + end_col = 5, + extmark_added = true, + modifiers = {}, + type = 'comment', + }, + { + line = 8, + end_col = 22, + modifiers = {}, + start_col = 0, + type = 'comment', + extmark_added = true, + }, + { + line = 9, + start_col = 0, + end_col = 6, + modifiers = {}, + type = 'comment', + extmark_added = true, + } + }, + expected2 = { + { + line = 2, + start_col = 4, + end_col = 8, + modifiers = { 'declaration', 'globalScope' }, + type = 'function', + extmark_added = true, + }, + { + line = 4, + start_col = 8, + end_col = 9, + modifiers = { 'declaration', 'globalScope' }, + type = 'function', + extmark_added = true, + }, + { + line = 5, + end_col = 12, + start_col = 11, + modifiers = { 'declaration', 'functionScope' }, + type = 'variable', + extmark_added = true, + }, + { + line = 6, + start_col = 7, + end_col = 18, + modifiers = { 'globalScope' }, + type = 'macro', + extmark_added = true, + }, + { + line = 7, + start_col = 4, + end_col = 7, + modifiers = { 'defaultLibrary', 'globalScope' }, + type = 'namespace', + extmark_added = true, + }, + { + line = 7, + start_col = 9, + end_col = 13, + modifiers = { 'defaultLibrary', 'globalScope' }, + type = 'variable', + extmark_added = true, + }, + { + line = 7, + start_col = 17, + end_col = 18, + extmark_added = true, + modifiers = { 'globalScope' }, + type = 'function', + }, + { + line = 8, + start_col = 0, + end_col = 5, + extmark_added = true, + modifiers = {}, + type = 'comment', + }, + { + line = 9, + end_col = 22, + modifiers = {}, + start_col = 0, + type = 'comment', + extmark_added = true, + }, + { + line = 10, + start_col = 0, + end_col = 6, + modifiers = {}, + type = 'comment', + extmark_added = true, + } + }, + }, + { + it = 'optional token_edit.data on deletion', + legend = [[{ + "tokenTypes": [ + "comment", "keyword", "operator", "string", "number", "regexp", "type", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "variable", "parameter", "module", "intrinsic", "selfParameter", "clsParameter", "magicFunction", "builtinConstant", "parenthesis", "curlybrace", "bracket", "colon", "semicolon", "arrow" + ], + "tokenModifiers": [ + "declaration", "static", "abstract", "async", "documentation", "typeHint", "typeHintComment", "readonly", "decorator", "builtin" + ] + }]], + text1 = [[string = "test"]], + text2 = [[]], + response1 = [[{"data": [0, 0, 6, 15, 1], "resultId": "1"}]], + response2 = [[{"edits": [{ "start": 0, "deleteCount": 5 }], "resultId": "2"}]], + expected1 = { + { + line = 0, + modifiers = { + 'declaration', + }, + start_col = 0, + end_col = 6, + type = 'variable', + extmark_added = true, + } + }, + expected2 = { + }, + }, + }) do + it(test.it, function() + exec_lua(create_server_definition) + exec_lua([[ + local legend, resp1, resp2 = ... + server = _create_server({ + capabilities = { + semanticTokensProvider = { + full = { delta = true }, + legend = vim.fn.json_decode(legend), + }, + }, + handlers = { + ['textDocument/semanticTokens/full'] = function() + return vim.fn.json_decode(resp1) + end, + ['textDocument/semanticTokens/full/delta'] = function() + return 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 }) + + -- 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 }) + end) + ]], test.legend, test.response1, test.response2) + + insert(test.text1) + + local highlights = exec_lua([[ + return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights + ]]) + + eq(test.expected1, highlights) + + 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")) + vim.wait(15) -- wait fot debounce + ]], test.text2) + end + + highlights = exec_lua([[ + return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights + ]]) + + eq(test.expected2, highlights) + end) + end + end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index cd7415de90..5229022564 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1,8 +1,9 @@ local helpers = require('test.functional.helpers')(after_each) +local lsp_helpers = require('test.functional.plugin.lsp.helpers') local assert_log = helpers.assert_log -local clear = helpers.clear local buf_lines = helpers.buf_lines +local clear = helpers.clear local command = helpers.command local dedent = helpers.dedent local exec_lua = helpers.exec_lua @@ -14,133 +15,28 @@ local pesc = helpers.pesc local insert = helpers.insert local funcs = helpers.funcs local retry = helpers.retry +local stop = helpers.stop local NIL = helpers.NIL local read_file = require('test.helpers').read_file local write_file = require('test.helpers').write_file -local isCI = helpers.isCI +local is_ci = helpers.is_ci local meths = helpers.meths +local is_os = helpers.is_os +local skip = helpers.skip --- Use these to get access to a coroutine so that I can run async tests and use --- yield. -local run, stop = helpers.run, helpers.stop +local clear_notrace = lsp_helpers.clear_notrace +local create_server_definition = lsp_helpers.create_server_definition +local fake_lsp_code = lsp_helpers.fake_lsp_code +local fake_lsp_logfile = lsp_helpers.fake_lsp_logfile +local test_rpc_server = lsp_helpers.test_rpc_server -- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837 -if helpers.pending_win32(pending) then return end - --- Fake LSP server. -local fake_lsp_code = 'test/functional/fixtures/fake-lsp-server.lua' -local fake_lsp_logfile = 'Xtest-fake-lsp.log' +if skip(is_os('win')) then return end teardown(function() os.remove(fake_lsp_logfile) end) -local function clear_notrace() - -- problem: here be dragons - -- solution: don't look for dragons to closely - clear {env={ - NVIM_LUA_NOTRACK="1"; - VIMRUNTIME=os.getenv"VIMRUNTIME"; - }} -end - - -local function fake_lsp_server_setup(test_name, timeout_ms, options, settings) - exec_lua([=[ - lsp = require('vim.lsp') - local test_name, fixture_filename, logfile, timeout, options, settings = ... - TEST_RPC_CLIENT_ID = lsp.start_client { - cmd_env = { - NVIM_LOG_FILE = logfile; - NVIM_LUA_NOTRACK = "1"; - }; - cmd = { - vim.v.progpath, '-Es', '-u', 'NONE', '--headless', - "-c", string.format("lua TEST_NAME = %q", test_name), - "-c", string.format("lua TIMEOUT = %d", timeout), - "-c", "luafile "..fixture_filename, - }; - handlers = setmetatable({}, { - __index = function(t, method) - return function(...) - return vim.rpcrequest(1, 'handler', ...) - end - end; - }); - workspace_folders = {{ - uri = 'file://' .. vim.loop.cwd(), - name = 'test_folder', - }}; - on_init = function(client, result) - 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; - on_exit = function(...) - vim.rpcnotify(1, "exit", ...) - end; - } - ]=], test_name, fake_lsp_code, fake_lsp_logfile, timeout_ms or 1e3, options or {}, settings or {}) -end - -local function test_rpc_server(config) - if config.test_name then - clear_notrace() - fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3, config.options, config.settings) - end - local client = setmetatable({}, { - __index = function(_, name) - -- 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, ...) - end - end; - }) - local code, signal - local function on_request(method, args) - if method == "init" then - if config.on_init then - config.on_init(client, unpack(args)) - end - return NIL - end - if method == 'handler' then - if config.on_handler then - config.on_handler(unpack(args)) - end - end - return NIL - end - local function on_notify(method, args) - if method == 'exit' then - code, signal = unpack(args) - return stop() - end - end - -- TODO specify timeout? - -- run(on_request, on_notify, config.on_setup, 1000) - run(on_request, on_notify, config.on_setup) - if config.on_exit then - config.on_exit(code, signal) - end - stop() - if config.test_name then - exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })") - end -end - describe('LSP', function() before_each(function() clear_notrace() @@ -236,9 +132,9 @@ describe('LSP', function() end) it('should invalid cmd argument', function() - eq('Error executing lua: .../lsp.lua:0: cmd: expected list, got nvim', + eq('.../lsp.lua:0: cmd: expected list, got nvim', pcall_err(_cmd_parts, 'nvim')) - eq('Error executing lua: .../lsp.lua:0: cmd argument: expected string, got number', + eq('.../lsp.lua:0: cmd argument: expected string, got number', pcall_err(_cmd_parts, {'nvim', 1})) end) end) @@ -316,7 +212,7 @@ describe('LSP', function() end) it('should succeed with manual shutdown', function() - if isCI() then + if is_ci() then pending('hangs the build on CI #14028, re-enable with freeze timeout #14204') return elseif helpers.skip_fragile(pending) then @@ -418,6 +314,31 @@ describe('LSP', function() } end) + it('should detach buffer on bufwipe', function() + clear() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(bufnr) + local client_id = vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd }) + assert(client_id, "lsp.start must return client_id") + local client = vim.lsp.get_client_by_id(client_id) + local num_attached_before = vim.tbl_count(client.attached_buffers) + vim.api.nvim_buf_delete(bufnr, { force = true }) + local num_attached_after = vim.tbl_count(client.attached_buffers) + return { + bufnr = bufnr, + client_id = client_id, + num_attached_before = num_attached_before, + num_attached_after = num_attached_after, + } + ]]) + eq(true, result ~= nil, "exec_lua must return result") + eq(1, result.num_attached_before) + eq(0, result.num_attached_after) + end) + it('client should return settings via workspace/configuration handler', function() local expected_handlers = { {NIL, {}, {method="shutdown", client_id=1}}; @@ -535,6 +456,70 @@ describe('LSP', function() } end) + it('BufWritePre does not send notifications if server lacks willSave capabilities', function() + clear() + exec_lua(create_server_definition) + local messages = exec_lua([[ + local server = _create_server({ + capabilities = { + textDocumentSync = { + willSave = false, + willSaveWaitUntil = false, + } + }, + }) + local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + local buf = vim.api.nvim_get_current_buf() + vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false }) + vim.lsp.stop_client(client_id) + return server.messages + ]]) + eq(#messages, 4) + eq(messages[1].method, 'initialize') + eq(messages[2].method, 'initialized') + eq(messages[3].method, 'shutdown') + eq(messages[4].method, 'exit') + end) + + it('BufWritePre sends willSave / willSaveWaitUntil, applies textEdits', function() + clear() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server({ + capabilities = { + textDocumentSync = { + willSave = true, + willSaveWaitUntil = true, + } + }, + handlers = { + ['textDocument/willSaveWaitUntil'] = function() + local text_edit = { + range = { + start = { line = 0, character = 0 }, + ['end'] = { line = 0, character = 0 }, + }, + newText = 'Hello' + } + return { text_edit, } + end + }, + }) + local buf = vim.api.nvim_get_current_buf() + local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false }) + vim.lsp.stop_client(client_id) + return { + messages = server.messages, + lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true) + } + ]]) + local messages = result.messages + eq('textDocument/willSave', messages[3].method) + eq('textDocument/willSaveWaitUntil', messages[4].method) + eq({'Hello'}, result.lines) + end) + it('saveas sends didOpen if filename changed', function() local expected_handlers = { { NIL, {}, { method = 'shutdown', client_id = 1 } }, @@ -1682,6 +1667,46 @@ describe('LSP', function() end) end) + describe('apply_text_edits regression tests for #20116', function() + before_each(function() + insert(dedent([[ + Test line one + Test line two 21 char]])) + end) + describe('with LSP end column out of bounds and start column at 0', function() + it('applies edits at the end of the buffer', function() + local edits = { + make_edit(0, 0, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8") + eq({'#include "whatever.h"', '#include <algorithm>'}, buf_lines(1)) + end) + it('applies edits in the middle of the buffer', function() + local edits = { + make_edit(0, 0, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8") + eq({'#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1)) + end) + end) + describe('with LSP end column out of bounds and start column NOT at 0', function() + it('applies edits at the end of the buffer', function() + local edits = { + make_edit(0, 2, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8") + eq({'Te#include "whatever.h"', '#include <algorithm>'}, buf_lines(1)) + end) + it('applies edits in the middle of the buffer', function() + local edits = { + make_edit(0, 2, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8") + eq({'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1)) + end) + end) + end) + describe('apply_text_document_edit', function() local target_bufnr local text_document_edit = function(editVersion) @@ -1900,6 +1925,22 @@ describe('LSP', function() exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16') eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile)) end) + it('Supports file creation in folder that needs to be created with CreateFile payload', function() + local tmpfile = helpers.tmpname() + os.remove(tmpfile) -- Should not exist, only interested in a tmpname + tmpfile = tmpfile .. '/dummy/x/' + local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) + local edit = { + documentChanges = { + { + kind = 'create', + uri = uri, + }, + } + } + exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16') + eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile)) + end) it('createFile does not touch file if it exists and ignoreIfExists is set', function() local tmpfile = helpers.tmpname() write_file(tmpfile, 'Dummy content') @@ -2377,7 +2418,7 @@ describe('LSP', function() }, uri = "file:///test_a" }, - contanerName = "TestAContainer" + containerName = "TestAContainer" }, { deprecated = false, @@ -2396,7 +2437,7 @@ describe('LSP', function() }, uri = "file:///test_b" }, - contanerName = "TestBContainer" + containerName = "TestBContainer" } } return vim.lsp.util.symbols_to_items(sym_info, nil) @@ -2493,7 +2534,7 @@ describe('LSP', function() local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") eq({ 1, 0 }, mark) - funcs.nvim_win_set_cursor(0, {2, 3}) + funcs.nvim_win_set_cursor(0, { 2, 3 }) jump(location(0, 9, 0, 9)) mark = funcs.nvim_buf_get_mark(target_bufnr, "'") @@ -2501,6 +2542,193 @@ describe('LSP', function() end) end) + describe('lsp.util.show_document', function() + local target_bufnr + local target_bufnr2 + + before_each(function() + target_bufnr = exec_lua([[ + 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 + ]]) + + target_bufnr2 = exec_lua([[ + 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) + + 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, exec_lua([[return vim.fn.bufnr('%')]])) + end + return { + line = exec_lua([[return vim.fn.line('.')]]), + col = exec_lua([[return vim.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) + end) + + it('jumps to a Location if focus is true via handler', function() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server() + local client_id = 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.stop_client(client_id) + return { + cursor = vim.api.nvim_win_get_cursor(0) + } + ]]) + 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() + funcs.nvim_win_set_buf(0, target_bufnr) + local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + + funcs.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 = funcs.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() + funcs.nvim_win_set_buf(0, target_bufnr) + local cursor = funcs.nvim_win_get_cursor(0) + + show_document(location(0, 9, 0, 9), false, false) + eq(cursor, funcs.nvim_win_get_cursor(0)) + end) + + it('does not change window if not focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local win = funcs.nvim_get_current_win() + + -- same document/bufnr + show_document(location(0, 9, 0, 9), false, true) + eq(win, funcs.nvim_get_current_win()) + + -- different document/bufnr, new window/split + show_document(location(0, 9, 0, 9, true), false, true) + eq(2, #funcs.nvim_list_wins()) + eq(win, funcs.nvim_get_current_win()) + end) + + it("respects 'reuse_win' parameter", function() + funcs.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, #funcs.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, #funcs.nvim_list_wins()) + end) + + it('correctly sets the cursor of the split if range is given without focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + + show_document(location(0, 9, 0, 9, true), false, true) + + local wins = funcs.nvim_list_wins() + eq(2, #wins) + table.sort(wins) + + eq({ 1, 0 }, funcs.nvim_win_get_cursor(wins[1])) + eq({ 1, 9 }, funcs.nvim_win_get_cursor(wins[2])) + end) + + it('does not change cursor of the split if not range and not focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + + exec_lua([[vim.cmd.new()]]) + funcs.nvim_win_set_buf(0, target_bufnr2) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + + show_document({ uri = 'file:///fake/uri2' }, false, true) + + local wins = funcs.nvim_list_wins() + eq(2, #wins) + eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[1])) + eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[2])) + end) + + it('respects existing buffers', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local win = funcs.nvim_get_current_win() + + exec_lua([[vim.cmd.new()]]) + funcs.nvim_win_set_buf(0, target_bufnr2) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + local split = funcs.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 }, funcs.nvim_win_get_cursor(split)) + eq(2, #funcs.nvim_list_wins()) + + funcs.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 }, funcs.nvim_win_get_cursor(split)) + eq(2, #funcs.nvim_list_wins()) + end) + end) + describe('lsp.util._make_floating_popup_size', function() before_each(function() exec_lua [[ contents = @@ -3180,5 +3408,159 @@ describe('LSP', function() end, } end) + it('format formats range in visual mode', function() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server({ capabilities = { + documentFormattingProvider = true, + documentRangeFormattingProvider = true, + }}) + local bufnr = vim.api.nvim_get_current_buf() + local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + vim.api.nvim_win_set_buf(0, bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {'foo', 'bar'}) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + vim.cmd.normal('v') + vim.api.nvim_win_set_cursor(0, { 2, 3 }) + vim.lsp.buf.format({ bufnr = bufnr, false }) + return server.messages + ]]) + eq("textDocument/rangeFormatting", result[3].method) + local expected_range = { + start = { line = 0, character = 0 }, + ['end'] = { line = 1, character = 4 }, + } + eq(expected_range, result[3].params.range) + end) + it('Aborts with notify if no clients support requested method', function() + exec_lua(create_server_definition) + exec_lua([[ + vim.notify = function(msg, _) + notify_msg = msg + end + ]]) + local fail_msg = "[LSP] Format request failed, no matching language servers." + local function check_notify(name, formatting, range_formatting) + local timeout_msg = "[LSP][" .. name .. "] timeout" + exec_lua([[ + local formatting, range_formatting, name = ... + local server = _create_server({ capabilities = { + documentFormattingProvider = formatting, + documentRangeFormattingProvider = range_formatting, + }}) + vim.lsp.start({ name = name, cmd = server.cmd }) + notify_msg = nil + vim.lsp.buf.format({ name = name, timeout_ms = 1 }) + ]], formatting, range_formatting, name) + eq(formatting and timeout_msg or fail_msg, exec_lua('return notify_msg')) + exec_lua([[ + notify_msg = nil + vim.lsp.buf.format({ name = name, timeout_ms = 1, range = {start={1, 0}, ['end']={1, 0}}}) + ]]) + eq(range_formatting and timeout_msg or fail_msg, exec_lua('return notify_msg')) + end + check_notify("none", false, false) + check_notify("formatting", true, false) + check_notify("rangeFormatting", false, true) + check_notify("both", true, true) + end) + end) + describe('cmd', function() + it('can connect to lsp server via rpc.connect', function() + local result = exec_lua [[ + local uv = vim.loop + local server = uv.new_tcp() + local init = nil + server:bind('127.0.0.1', 0) + server:listen(127, function(err) + assert(not err, err) + local socket = 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 + vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) }) + vim.wait(1000, function() return init ~= nil end) + assert(init, "server must receive `initialize` request") + server:close() + server:shutdown() + return vim.json.decode(init) + ]] + eq(result.method, "initialize") + end) + end) + + describe('handlers', function() + it('handler can return false as response', function() + local result = exec_lua [[ + local uv = vim.loop + local server = uv.new_tcp() + local messages = {} + local responses = {} + server:bind('127.0.0.1', 0) + server:listen(127, function(err) + assert(not err, err) + local socket = uv.new_tcp() + server:accept(socket) + socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body) + local payload = vim.json.decode(body) + if payload.method then + table.insert(messages, payload.method) + if payload.method == 'initialize' then + local msg = vim.json.encode({ + id = payload.id, + jsonrpc = '2.0', + result = { + capabilities = {} + }, + }) + socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg})) + elseif payload.method == 'initialized' then + local msg = vim.json.encode({ + id = 10, + jsonrpc = '2.0', + method = 'dummy', + params = {}, + }) + socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg})) + end + else + table.insert(responses, payload) + socket:close() + end + end)) + end) + local port = server:getsockname().port + local handler_called = false + vim.lsp.handlers['dummy'] = function(err, result) + handler_called = true + return false + end + local client_id = vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) }) + local client = vim.lsp.get_client_by_id(client_id) + vim.wait(1000, function() return #messages == 2 and handler_called and #responses == 1 end) + server:close() + server:shutdown() + return { + messages = messages, + handler_called = handler_called, + responses = responses } + ]] + local expected = { + messages = { 'initialize', 'initialized' }, + handler_called = true, + responses = { + { + id = 10, + jsonrpc = '2.0', + result = false + } + } + } + eq(expected, result) + end) end) end) diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua index c8da5a711f..c6c7d2b03d 100644 --- a/test/functional/plugin/man_spec.lua +++ b/test/functional/plugin/man_spec.lua @@ -1,10 +1,21 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') -local command, eval, rawfeed = helpers.command, helpers.eval, helpers.rawfeed +local command, rawfeed = helpers.command, helpers.rawfeed local clear = helpers.clear +local exec_lua = helpers.exec_lua local funcs = helpers.funcs local nvim_prog = helpers.nvim_prog local matches = helpers.matches +local write_file = helpers.write_file +local tmpname = helpers.tmpname +local skip = helpers.skip +local is_ci = helpers.is_ci + +clear() +if funcs.executable('man') == 0 then + pending('missing "man" command', function() end) + return +end describe(':Man', function() before_each(function() @@ -44,7 +55,7 @@ describe(':Man', function() | ]]} - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ ^this {b:is} {b:a} test | @@ -68,7 +79,7 @@ describe(':Man', function() | ]=]} - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ ^this {b:is }{bi:a }{biu:test} | @@ -83,7 +94,7 @@ describe(':Man', function() rawfeed([[ ithis i<C-v><C-h>is<C-v><C-h>s あ<C-v><C-h>あ test with _<C-v><C-h>ö_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>̃_<C-v><C-h>c_<C-v><C-h>k te<C-v><ESC>[3mxt¶<C-v><ESC>[0m<ESC>]]) - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ ^this {b:is} {b:あ} test | @@ -99,7 +110,7 @@ describe(':Man', function() i_<C-v><C-h>_b<C-v><C-h>be<C-v><C-h>eg<C-v><C-h>gi<C-v><C-h>in<C-v><C-h>ns<C-v><C-h>s m<C-v><C-h>mi<C-v><C-h>id<C-v><C-h>d_<C-v><C-h>_d<C-v><C-h>dl<C-v><C-h>le<C-v><C-h>e _<C-v><C-h>m_<C-v><C-h>i_<C-v><C-h>d_<C-v><C-h>__<C-v><C-h>d_<C-v><C-h>l_<C-v><C-h>e<ESC>]]) - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ {b:^_begins} | @@ -115,7 +126,7 @@ describe(':Man', function() i· ·<C-v><C-h>· +<C-v><C-h>o +<C-v><C-h>+<C-v><C-h>o<C-v><C-h>o double<ESC>]]) - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ ^· {b:·} | @@ -132,7 +143,7 @@ describe(':Man', function() <C-v><C-[>[44m 4 <C-v><C-[>[45m 5 <C-v><C-[>[46m 6 <C-v><C-[>[47m 7 <C-v><C-[>[100m 8 <C-v><C-[>[101m 9 <C-v><C-[>[102m 10 <C-v><C-[>[103m 11 <C-v><C-[>[104m 12 <C-v><C-[>[105m 13 <C-v><C-[>[106m 14 <C-v><C-[>[107m 15 <C-v><C-[>[48:5:16m 16 <ESC>]]) - eval('man#init_pager()') + exec_lua[[require'man'.init_pager()]] screen:expect([[ ^ 0 1 2 3 | @@ -149,4 +160,17 @@ describe(':Man', function() local args = {nvim_prog, '--headless', '+autocmd VimLeave * echo "quit works!!"', '+Man!', '+call nvim_input("q")'} matches('quit works!!', funcs.system(args, {'manpage contents'})) end) + + it('reports non-existent man pages for absolute paths', function() + skip(is_ci('cirrus')) + local actual_file = tmpname() + -- actual_file must be an absolute path to an existent file for us to test against it + matches('^/.+', actual_file) + write_file(actual_file, '') + local args = {nvim_prog, '--headless', '+:Man ' .. actual_file, '+q'} + matches(('Error detected while processing command line:\r\n' .. + 'man.lua: "no manual entry for %s"'):format(actual_file), + funcs.system(args, {''})) + os.remove(actual_file) + end) end) diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua index 6f22f865e6..93cf6d2b77 100644 --- a/test/functional/plugin/shada_spec.lua +++ b/test/functional/plugin/shada_spec.lua @@ -1,4 +1,5 @@ local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') local clear = helpers.clear local eq, meths, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf = helpers.eq, helpers.meths, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec, @@ -2139,7 +2140,7 @@ end) describe('plugin/shada.vim', function() local epoch = os.date('%Y-%m-%dT%H:%M:%S', 0) - local eol = helpers.iswin() and '\r\n' or '\n' + local eol = helpers.is_os('win') and '\r\n' or '\n' before_each(function() -- Note: reset() is called explicitly in each test. os.remove(fname) @@ -2538,13 +2539,26 @@ describe('ftplugin/shada.vim', function() end) describe('syntax/shada.vim', function() - local epoch = os.date('%Y-%m-%dT%H:%M:%S', 0) + local epoch = os.date('!%Y-%m-%dT%H:%M:%S', 0) before_each(reset) it('works', function() nvim_command('syntax on') nvim_command('setlocal syntax=shada') nvim_command('set laststatus&') + local screen = Screen.new(60, 37) + screen:set_default_attr_ids { + [1] = {bold = true, foreground = Screen.colors.Brown}; + [2] = {foreground = tonumber('0x6a0dad')}; + [3] = {foreground = Screen.colors.Fuchsia}; + [4] = {foreground = Screen.colors.Blue1}; + [5] = {bold = true, foreground = Screen.colors.SeaGreen4}; + [6] = {foreground = Screen.colors.SlateBlue}; + [7] = {bold = true, reverse = true}; + [8] = {bold = true, foreground = Screen.colors.Blue}; + } + screen:attach() + curbuf('set_lines', 0, 1, true, { 'Header with timestamp ' .. epoch .. ':', ' % Key Value', @@ -2580,6 +2594,46 @@ describe('syntax/shada.vim', function() ' % Key Description________ Value', ' + se place cursor at end TRUE', }) + screen:expect{grid=[=[ + {1:^Header} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: % Key Value} | + {1: +} {3:t } {1:"}{3:test}{1:"} | + {1:Jump} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: % Key________ Description Value} | + {1: +} {3:n } {4:name } {3:'A'} | + {1: +} {3:f } {4:file name } {1:["}{3:foo}{1:"]} | + {1: +} {3:l } {4:line number} {3:2} | + {1: +} {3:c } {4:column } {3:-200} | + {1:Register} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: % Key Description Value} | + {1: +} {3:rc } {4:contents } {1:@} | + {1: | -} {1:{"}{3:abcdefghijklmnopqrstuvwxyz}{1:":} {3:1.0}{1:}} | + {1: +} {3:rt } {4:type } {1:CHARACTERWISE} | + {1: +} {3:rt } {4:type } {1:LINEWISE} | + {1: +} {3:rt } {4:type } {1:BLOCKWISE} | + {1:Replacement string} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: @ Description__________ Value} | + {1: -} {4::s replacement string} {1:CMD} | + {1: -} {4::s replacement string} {1:SEARCH} | + {1: -} {4::s replacement string} {1:EXPR} | + {1: -} {4::s replacement string} {1:INPUT} | + {1: -} {4::s replacement string} {1:DEBUG} | + {1:Buffer list} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {4: # Expected array of maps} | + = {1:[{="}{3:a}{1:":} {1:+(}{5:10}{1:)"}{3:ac}{6:\0}{3:df}{6:\n}{3:gi}{6:\"}{3:tt\.}{1:",} {1:TRUE:} {1:FALSE},} {1:[NIL,} {1:+(}{5:-}| + {5:10}{1:)""]]} | + {1:Buffer list} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: % Key Description Value} | + | + {2: % Key Description Value} | + {1:Header} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: | + {2: % Key Description________ Value} | + {1: +} {3:se } {4:place cursor at end} {1:TRUE} | + {8:~ }| + {7:[No Name] [+] }| + | + ]=]} + nvim_command([[ function GetSyntax() let lines = [] @@ -2613,7 +2667,7 @@ describe('syntax/shada.vim', function() year = htsnum(os.date('%Y', 0)), month = htsnum(os.date('%m', 0)), day = htsnum(os.date('%d', 0)), - hour = htsnum(os.date('%H', 0)), + hour = htsnum(os.date('!%H', 0)), minute = htsnum(os.date('%M', 0)), second = htsnum(os.date('%S', 0)), } @@ -2768,9 +2822,8 @@ describe('syntax/shada.vim', function() {{'ShaDaEntryArray', 'ShaDaMsgpackShaDaKeyword'}, 'INPUT'}, }, { - {{'ShaDaEntryArrayEntryStart'}, ' - '}, - {{'ShaDaEntryArrayDescription'}, ':s replacement string '}, - {{'ShaDaMsgpackShaDaKeyword'}, 'DEBUG'}, + as(), ad(':s replacement string '), + {{'ShaDaEntryArray', 'ShaDaMsgpackShaDaKeyword'}, 'DEBUG'}, }, { hname('Buffer list'), h(' with timestamp '), @@ -2872,10 +2925,10 @@ describe('syntax/shada.vim', function() mlh(' % Key Description________ Value'), }, { - {{'ShaDaEntryMapLongEntryStart'}, ' + '}, - {{'ShaDaEntryMapLongKey'}, 'se '}, - {{'ShaDaEntryMapLongDescription'}, 'place cursor at end '}, - {{'ShaDaMsgpackKeyword'}, 'TRUE'}, + mles(' + '), + mlk('se '), + mld('place cursor at end '), + {{'ShaDaEntryMapLong', 'ShaDaMsgpackKeyword'}, 'TRUE'}, }, } eq(exp, act) |