diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
commit | 1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch) | |
tree | cd08258054db80bb9a11b1061bb091c70b76926a /test/functional/plugin | |
parent | eaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff) | |
parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
download | rneovim-aucmd_textputpost.tar.gz rneovim-aucmd_textputpost.tar.bz2 rneovim-aucmd_textputpost.zip |
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
Diffstat (limited to 'test/functional/plugin')
-rw-r--r-- | test/functional/plugin/editorconfig_spec.lua | 19 | ||||
-rw-r--r-- | test/functional/plugin/health_spec.lua | 131 | ||||
-rw-r--r-- | test/functional/plugin/lsp/completion_spec.lua | 239 | ||||
-rw-r--r-- | test/functional/plugin/lsp/diagnostic_spec.lua | 159 | ||||
-rw-r--r-- | test/functional/plugin/lsp/helpers.lua | 4 | ||||
-rw-r--r-- | test/functional/plugin/lsp/incremental_sync_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/plugin/lsp/inlay_hint_spec.lua | 204 | ||||
-rw-r--r-- | test/functional/plugin/lsp/semantic_tokens_spec.lua | 450 | ||||
-rw-r--r-- | test/functional/plugin/lsp/snippet_spec.lua | 261 | ||||
-rw-r--r-- | test/functional/plugin/lsp/utils_spec.lua | 226 | ||||
-rw-r--r-- | test/functional/plugin/lsp/watchfiles_spec.lua | 222 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 1198 | ||||
-rw-r--r-- | test/functional/plugin/man_spec.lua | 54 | ||||
-rw-r--r-- | test/functional/plugin/shada_spec.lua | 34 | ||||
-rw-r--r-- | test/functional/plugin/tutor_spec.lua | 95 |
15 files changed, 2804 insertions, 494 deletions
diff --git a/test/functional/plugin/editorconfig_spec.lua b/test/functional/plugin/editorconfig_spec.lua index e6a2550aba..ac78003a8c 100644 --- a/test/functional/plugin/editorconfig_spec.lua +++ b/test/functional/plugin/editorconfig_spec.lua @@ -3,9 +3,9 @@ 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 exec_lua = helpers.exec_lua local testdir = 'Xtest-editorconfig' @@ -13,7 +13,7 @@ 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) + eq(val, meths.get_option_value(opt, {buf=0}), name) end end @@ -160,8 +160,8 @@ describe('editorconfig', function() end) it('sets newline options', function() - test_case('with_newline.txt', { fixendofline = true, endofline = true }) - test_case('without_newline.txt', { fixendofline = false, endofline = false }) + test_case('with_newline.txt', { fixendofline = true }) + test_case('without_newline.txt', { fixendofline = false }) end) it('respects trim_trailing_whitespace', function() @@ -207,4 +207,15 @@ But not this one test_case('3_space.txt', { shiftwidth = 42 }) test_case('4_space.py', { shiftwidth = 4 }) end) + + it('does not operate on invalid buffers', function() + local ok, err = unpack(exec_lua([[ + vim.cmd.edit('test.txt') + local bufnr = vim.api.nvim_get_current_buf() + vim.cmd.bwipeout(bufnr) + return {pcall(require('editorconfig').config, bufnr)} + ]])) + + eq(true, ok, err) + end) end) diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua index 97d32313e5..50b1d03f36 100644 --- a/test/functional/plugin/health_spec.lua +++ b/test/functional/plugin/health_spec.lua @@ -1,5 +1,4 @@ local helpers = require('test.functional.helpers')(after_each) -local global_helpers = require('test.helpers') local Screen = require('test.functional.ui.screen') local clear = helpers.clear @@ -43,20 +42,17 @@ end) describe('health.vim', function() before_each(function() clear{args={'-u', 'NORC'}} - -- Provides functions: - -- health#broken#check() - -- health#success1#check() - -- health#success2#check() + -- Provides healthcheck functions command("set runtimepath+=test/functional/fixtures") end) describe(":checkhealth", function() - it("functions health#report_*() render correctly", function() + it("functions report_*() render correctly", function() command("checkhealth full_render") helpers.expect([[ ============================================================================== - full_render: health#full_render#check + test_plug.full_render: require("test_plug.full_render.health").check() report 1 ~ - OK life is fine @@ -79,7 +75,7 @@ describe('health.vim', function() helpers.expect([[ ============================================================================== - success1: health#success1#check + test_plug: require("test_plug.health").check() report 1 ~ - OK everything is fine @@ -88,36 +84,19 @@ describe('health.vim', function() - OK nothing to see here ============================================================================== - success2: health#success2#check - - another 1 ~ - - OK ok - - ============================================================================== - test_plug: require("test_plug.health").check() + test_plug.success1: require("test_plug.success1.health").check() report 1 ~ - OK everything is fine report 2 ~ - OK nothing to see here - ]]) - end) - - it("lua plugins, skips vimscript healthchecks with the same name", function() - command("checkhealth test_plug") - -- Existing file in test/functional/fixtures/lua/test_plug/autoload/health/test_plug.vim - -- and the Lua healthcheck is used instead. - helpers.expect([[ ============================================================================== - test_plug: require("test_plug.health").check() - - report 1 ~ - - OK everything is fine + test_plug.success2: require("test_plug.success2.health").check() - report 2 ~ - - OK nothing to see here + another 1 ~ + - OK ok ]]) end) @@ -136,57 +115,6 @@ describe('health.vim', function() ]]) end) - it("lua plugins submodules with expression '*'", function() - command("checkhealth test_plug*") - local buf_lines = helpers.curbuf('get_lines', 0, -1, true) - -- avoid dealing with path separators - 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 - - ============================================================================== - test_plug.submodule: require("test_plug.submodule.health").check() - - 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. - - ============================================================================== - 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 25]]) - eq(expected, received) - end) - - it("gracefully handles broken healthcheck", function() - command("checkhealth broken") - helpers.expect([[ - - ============================================================================== - broken: health#broken#check - - - ERROR Failed to run healthcheck for "broken" plugin. Exception: - function health#check[25]..health#broken#check, line 1 - caused an error - ]]) - end) - it("... including empty reports", function() command("checkhealth test_plug.submodule_empty") helpers.expect([[ @@ -198,36 +126,17 @@ describe('health.vim', function() ]]) end) - it("gracefully handles broken lua healthcheck", function() - command("checkhealth test_plug.submodule_failed") - local buf_lines = helpers.curbuf('get_lines', 0, -1, true) - local received = table.concat(buf_lines, '\n', 1, #buf_lines - 5) - -- avoid dealing with path separators - local lua_err = "attempt to perform arithmetic on a nil value" - local last_line = buf_lines[#buf_lines - 4] - assert(string.find(last_line, lua_err) ~= nil, "Lua error not present") - - 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 25]]) - eq(expected, received) - end) - it("highlights OK, ERROR", function() 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 }, + Ok = { foreground = Screen.colors.LightGreen }, + Error = { foreground = Screen.colors.Red }, Heading = { foreground = tonumber('0x6a0dad') }, Bar = { foreground = Screen.colors.LightGrey, background = Screen.colors.DarkGrey }, }) command("checkhealth foo success1") - command("set nowrap laststatus=0") + command("set nofoldenable nowrap laststatus=0") screen:expect{grid=[[ ^ | {Bar:──────────────────────────────────────────────────}| @@ -236,7 +145,7 @@ describe('health.vim', function() - {Error:ERROR} No healthcheck found for "foo" plugin. | | {Bar:──────────────────────────────────────────────────}| - {Heading:success1: health#success1#check} | + {Heading:test_plug.success1: require("test_plug.success1.he}| | {Heading:report 1} | - {Ok:OK} everything is fine | @@ -244,6 +153,22 @@ describe('health.vim', function() ]]} end) + it("fold healthchecks", function() + local screen = Screen.new(50, 7) + screen:attach() + command("checkhealth foo success1") + command("set nowrap laststatus=0") + screen:expect{grid=[[ + ^ | + ──────────────────────────────────────────────────| + +WE 4 lines: foo: ·······························| + ──────────────────────────────────────────────────| + +-- 8 lines: test_plug.success1: require("test_pl| + ~ | + | + ]]} + end) + it("gracefully handles invalid healthcheck", function() command("checkhealth non_existent_healthcheck") -- luacheck: ignore 613 diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua new file mode 100644 index 0000000000..9354654afe --- /dev/null +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -0,0 +1,239 @@ +---@diagnostic disable: no-unknown +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local exec_lua = helpers.exec_lua + + +--- Convert completion results. +--- +---@param line string line contents. Mark cursor position with `|` +---@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) + 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 = ... + 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( + line, + lnum, + cursor_col, + client_start_boundary, + nil, + result, + "utf-16" + ) + return { + items = items, + server_start_boundary = server_start_boundary + } + ]], line, cursor_col, lnum, candidates) +end + + +describe("vim.lsp._completion", function() + before_each(helpers.clear) + + -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion + it('prefers textEdit over label as word', function() + local range0 = { + start = { line = 0, character = 0 }, + ["end"] = { line = 0, character = 0 }, + } + local completion_list = { + -- resolves into label + { label = 'foobar', sortText = 'a', documentation = 'documentation' }, + { + label = 'foobar', + sortText = 'b', + documentation = { value = 'documentation' }, + }, + -- resolves into insertText + { label = 'foocar', sortText = 'c', insertText = 'foobar' }, + { label = 'foocar', sortText = 'd', insertText = 'foobar' }, + -- resolves into textEdit.newText + { label = 'foocar', sortText = 'e', insertText = 'foodar', textEdit = { newText = 'foobar', range = range0 } }, + { label = 'foocar', sortText = 'f', textEdit = { newText = 'foobar', range = range0 } }, + -- real-world snippet text + { + label = 'foocar', + sortText = 'g', + insertText = 'foodar', + insertTextFormat = 2, + textEdit = { newText = 'foobar(${1:place holder}, ${2:more ...holder{\\}})', 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 = { + { + abbr = 'foobar', + word = 'foobar', + }, + { + abbr = 'foobar', + word = 'foobar', + }, + { + abbr = 'foocar', + word = 'foobar', + }, + { + abbr = 'foocar', + word = 'foobar', + }, + { + abbr = 'foocar', + word = 'foobar', + }, + { + abbr = 'foocar', + word = 'foobar', + }, + { + abbr = 'foocar', + word = 'foobar(place holder, more ...holder{})', + }, + { + abbr = 'foocar', + word = 'foodar(var1 typ1, var2 *typ2) {}', + }, + { + abbr = 'foocar', + word = 'foodar(typ1) {}', + }, + { + abbr = 'foocar', + word = 'foodar()', + }, + { + abbr = 'foocar', + word = 'foodar(${1:var1})', + }, + } + 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("uses correct start boundary", function() + local completion_list = { + isIncomplete = false, + items = { + { + filterText = "this_thread", + insertText = "this_thread", + insertTextFormat = 1, + kind = 9, + label = " this_thread", + score = 1.3205767869949, + sortText = "4056f757this_thread", + textEdit = { + newText = "this_thread", + range = { + start = { line = 0, character = 7 }, + ["end"] = { line = 0, character = 11 }, + }, + } + }, + } + } + local expected = { + abbr = ' this_thread', + dup = 1, + empty = 1, + icase = 1, + kind = 'Module', + menu = '', + word = 'this_thread', + } + local result = complete(" std::this|", completion_list) + eq(7, result.server_start_boundary) + local item = result.items[1] + item.user_data = nil + eq(expected, item) + end) + + it("should search from start boundary to cursor position", function() + local completion_list = { + isIncomplete = false, + items = { + { + filterText = "this_thread", + insertText = "this_thread", + insertTextFormat = 1, + kind = 9, + label = " this_thread", + score = 1.3205767869949, + sortText = "4056f757this_thread", + textEdit = { + newText = "this_thread", + range = { + start = { line = 0, character = 7 }, + ["end"] = { line = 0, character = 11 }, + }, + } + }, + { + filterText = "notthis_thread", + insertText = "notthis_thread", + insertTextFormat = 1, + kind = 9, + label = " notthis_thread", + score = 1.3205767869949, + sortText = "4056f757this_thread", + textEdit = { + newText = "notthis_thread", + range = { + start = { line = 0, character = 7 }, + ["end"] = { line = 0, character = 11 }, + }, + } + }, + } + } + local expected = { + abbr = ' this_thread', + dup = 1, + empty = 1, + icase = 1, + kind = 'Module', + menu = '', + word = 'this_thread', + } + local result = complete(" std::this|is", completion_list) + eq(1, #result.items) + local item = result.items[1] + item.user_data = nil + eq(expected, item) + end) +end) diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index f73ffc29b0..1da0222114 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -1,10 +1,13 @@ local helpers = require('test.functional.helpers')(after_each) +local lsp_helpers = require('test.functional.plugin.lsp.helpers') local clear = helpers.clear local exec_lua = helpers.exec_lua local eq = helpers.eq local neq = require('test.helpers').neq +local create_server_definition = lsp_helpers.create_server_definition + describe('vim.lsp.diagnostic', function() local fake_uri @@ -81,6 +84,7 @@ describe('vim.lsp.diagnostic', function() 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) @@ -97,7 +101,6 @@ describe('vim.lsp.diagnostic', function() } diagnostics[1].code = 42 - diagnostics[1].tags = {"foo", "bar"} diagnostics[1].data = "Hello world" vim.lsp.diagnostic.on_publish_diagnostics(nil, { @@ -110,10 +113,9 @@ describe('vim.lsp.diagnostic', function() vim.lsp.diagnostic.get_line_diagnostics(diagnostic_bufnr, 1)[1], } ]] - eq({code = 42, tags = {"foo", "bar"}, data = "Hello world"}, result[1].user_data.lsp) + eq({code = 42, data = "Hello world"}, result[1].user_data.lsp) eq(42, result[1].code) eq(42, result[2].code) - eq({"foo", "bar"}, result[2].tags) eq("Hello world", result[2].data) end) end) @@ -267,4 +269,155 @@ describe('vim.lsp.diagnostic', function() eq(exec_lua([[return #vim.diagnostic.get(...)]], bufnr), 0) end) end) + + describe('vim.lsp.diagnostic.on_diagnostic', function() + before_each(function() + exec_lua(create_server_definition) + exec_lua([[ + server = _create_server({ + capabilities = { + diagnosticProvider = { + } + } + }) + + function get_extmarks(bufnr, client_id) + local namespace = vim.lsp.diagnostic.get_namespace(client_id, true) + local ns = vim.diagnostic.get_namespace(namespace) + local extmarks = {} + if ns.user_data.virt_text_ns then + for _, e in pairs(vim.api.nvim_buf_get_extmarks(bufnr, ns.user_data.virt_text_ns, 0, -1, {details=true})) do + table.insert(extmarks, e) + end + end + if ns.user_data.underline_ns then + for _, e in pairs(vim.api.nvim_buf_get_extmarks(bufnr, ns.user_data.underline_ns, 0, -1, {details=true})) do + table.insert(extmarks, e) + end + end + return extmarks + end + + client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + ]]) + 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), + } + }, + { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + }, + {} + ) + + return vim.diagnostic.get(diagnostic_bufnr) + ]]) + eq(1, #diags) + eq('Pull Diagnostic', diags[1].message) + 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, { + virtual_text = { + spacing = ..., + }, + }) + + Diagnostic(nil, + { + kind = 'full', + items = { + make_error('Pull Diagnostic', 4, 4, 4, 4), + } + }, + { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + }, + {} + ) + + return get_extmarks(diagnostic_bufnr, client_id) + ]], + expected_spacing + ) + 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), + } + }, + { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + }, + {} + ) + ]]) + local diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) + eq(1, #diags) + + exec_lua([[ vim.lsp.stop_client(client_id) ]]) + + diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) + eq(0, #diags) + 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), + } + }, + { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + }, + {} + ) + ]]) + local diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) + eq(1, #diags) + + exec_lua([[ vim.lsp.stop_client(client_id2) ]]) + + diags = exec_lua([[return vim.diagnostic.get(diagnostic_bufnr)]]) + eq(1, #diags) + end) + end) end) diff --git a/test/functional/plugin/lsp/helpers.lua b/test/functional/plugin/lsp/helpers.lua index caab174b4d..15e6a62781 100644 --- a/test/functional/plugin/lsp/helpers.lua +++ b/test/functional/plugin/lsp/helpers.lua @@ -13,6 +13,7 @@ function M.clear_notrace() -- solution: don't look too closely for dragons clear {env={ NVIM_LUA_NOTRACK="1"; + NVIM_APPNAME="nvim_lsp_test"; VIMRUNTIME=os.getenv"VIMRUNTIME"; }} end @@ -85,6 +86,7 @@ local function fake_lsp_server_setup(test_name, timeout_ms, options, settings) cmd_env = { 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), @@ -97,7 +99,7 @@ local function fake_lsp_server_setup(test_name, timeout_ms, options, settings) end; }); workspace_folders = {{ - uri = 'file://' .. vim.loop.cwd(), + uri = 'file://' .. vim.uv.cwd(), name = 'test_folder', }}; on_init = function(client, result) diff --git a/test/functional/plugin/lsp/incremental_sync_spec.lua b/test/functional/plugin/lsp/incremental_sync_spec.lua index 4985da9cd7..724b3efb97 100644 --- a/test/functional/plugin/lsp/incremental_sync_spec.lua +++ b/test/functional/plugin/lsp/incremental_sync_spec.lua @@ -21,7 +21,7 @@ before_each(function () -- ["mac"] = '\r', -- } - -- local line_ending = format_line_ending[vim.api.nvim_buf_get_option(0, 'fileformat')] + -- local line_ending = format_line_ending[vim.api.nvim_get_option_value('fileformat', {})] function test_register(bufnr, id, offset_encoding, line_ending) diff --git a/test/functional/plugin/lsp/inlay_hint_spec.lua b/test/functional/plugin/lsp/inlay_hint_spec.lua new file mode 100644 index 0000000000..d0d55df72b --- /dev/null +++ b/test/functional/plugin/lsp/inlay_hint_spec.lua @@ -0,0 +1,204 @@ +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 eq = helpers.eq +local dedent = helpers.dedent +local exec_lua = helpers.exec_lua +local insert = helpers.insert + +local clear_notrace = lsp_helpers.clear_notrace +local create_server_definition = lsp_helpers.create_server_definition + +local text = dedent([[ +auto add(int a, int b) { return a + b; } + +int main() { + int x = 1; + int y = 2; + return add(x,y); +} +}]]) + +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}, +{"kind":2,"paddingLeft":false,"label":"b:","position":{"character":17,"line":5},"paddingRight":true} +] +]==] + +local grid_without_inlay_hints = [[ + auto add(int a, int b) { return a + b; } | + | + int main() { | + int x = 1; | + int y = 2; | + return add(x,y); | + } | + ^} | + | +]] + +local grid_with_inlay_hints = [[ + auto add(int a, int b)-> int { return a + b; } | + | + int main() { | + int x = 1; | + int y = 2; | + return add(a: x,b: y); | + } | + ^} | + | +]] + +--- @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() + return vim.json.decode(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 = server.cmd }) + ]], response) + + insert(text) + exec_lua([[vim.lsp.inlay_hint.enable(bufnr)]]) + screen:expect({ grid = grid_with_inlay_hints }) +end) + +after_each(function() + exec_lua("vim.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)]]) + 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({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function() + return {} + end, + } + }) + client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + vim.lsp.inlay_hint.enable(bufnr) + ]]) + + exec_lua([[ vim.lsp.stop_client(client2) ]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + end) + + describe('enable()', function() + it('clears/applies inlay hints when passed false/true/nil', function() + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, false)]]) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) + + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, true)]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, not vim.lsp.inlay_hint.is_enabled(bufnr))]]) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) + + exec_lua([[vim.lsp.inlay_hint.enable(bufnr)]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + end) + end) + + describe('get()', function() + it('returns filtered inlay hints', function() + --- @type lsp.InlayHint[] + local expected = vim.json.decode(response) + local expected2 = { + kind = 1, + paddingLeft = false, + label = ': int', + position = { + character = 10, + line = 2, + }, + paddingRight = false, + } + + exec_lua([[ + local expected2 = ... + server2 = _create_server({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function() + return { expected2 } + end, + } + }) + client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + vim.lsp.inlay_hint.enable(bufnr) + ]], expected2) + + --- @type vim.lsp.inlay_hint.get.ret + local res = exec_lua([[return vim.lsp.inlay_hint.get()]]) + eq(res, { + { 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 }, + }) + + --- @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(res, { + { bufnr = 1, client_id = 2, inlay_hint = expected2 }, + }) + + --- @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(res, { + { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, + }) + + --- @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, {}) + end) + end) +end) diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index 9c1ba86fe1..b7ac53f270 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -24,6 +24,27 @@ end) describe('semantic token highlighting', function() + 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 }; + [9] = { bold = true, foreground = tonumber('0x6a0dad') }; + } + command([[ hi link @lsp.type.namespace Type ]]) + command([[ hi link @lsp.type.function Special ]]) + command([[ hi link @lsp.type.comment Comment ]]) + command([[ hi @lsp.mod.declaration gui=bold ]]) + end) + describe('general', function() local text = dedent([[ #include <iostream> @@ -58,24 +79,7 @@ describe('semantic token highlighting', function() "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 = ... @@ -102,6 +106,7 @@ describe('semantic token highlighting', function() exec_lua([[ 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 }) ]]) @@ -127,6 +132,46 @@ describe('semantic token highlighting', function() ]] } end) + it('use LspTokenUpdate and highlight_token', function() + exec_lua([[ + 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" + ) + end + 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 }) + ]]) + + insert(text) + + screen:expect { grid = [[ + #include <iostream> | + | + int {9: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() @@ -578,16 +623,33 @@ describe('semantic token highlighting', function() expected = { { line = 0, - modifiers = { - 'declaration', - 'globalScope', - }, + modifiers = { declaration = true, globalScope = true }, start_col = 6, end_col = 9, type = 'variable', - extmark_added = true, + marked = true, }, }, + expected_screen = function() + screen:expect{grid=[[ + char* {7:foo} = "\n"^; | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, { it = 'clangd-15 on C++', @@ -613,67 +675,67 @@ int main() expected = { { -- main line = 1, - modifiers = { 'declaration', 'globalScope' }, + modifiers = { declaration = true, globalScope = true }, start_col = 4, end_col = 8, type = 'function', - extmark_added = true, + marked = true, }, { -- __cplusplus line = 3, - modifiers = { 'globalScope' }, + modifiers = { globalScope = true }, start_col = 9, end_col = 20, type = 'macro', - extmark_added = true, + marked = true, }, { -- x line = 4, - modifiers = { 'declaration', 'readonly', 'functionScope' }, + modifiers = { declaration = true, readonly = true, functionScope = true }, start_col = 12, end_col = 13, type = 'variable', - extmark_added = true, + marked = true, }, { -- std line = 5, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, start_col = 2, end_col = 5, type = 'namespace', - extmark_added = true, + marked = true, }, { -- cout line = 5, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, start_col = 7, end_col = 11, type = 'variable', - extmark_added = true, + marked = true, }, { -- x line = 5, - modifiers = { 'readonly', 'functionScope' }, + modifiers = { readonly = true, functionScope = true }, start_col = 15, end_col = 16, type = 'variable', - extmark_added = true, + marked = true, }, { -- std line = 5, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, start_col = 20, end_col = 23, type = 'namespace', - extmark_added = true, + marked = true, }, { -- endl line = 5, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, start_col = 25, end_col = 29, type = 'function', - extmark_added = true, + marked = true, }, { -- #else comment #endif line = 6, @@ -681,7 +743,7 @@ int main() start_col = 0, end_col = 7, type = 'comment', - extmark_added = true, + marked = true, }, { line = 7, @@ -689,7 +751,7 @@ int main() start_col = 0, end_col = 11, type = 'comment', - extmark_added = true, + marked = true, }, { line = 8, @@ -697,9 +759,29 @@ int main() start_col = 0, end_col = 8, type = 'comment', - extmark_added = true, + marked = true, }, }, + expected_screen = function() + screen:expect{grid=[[ + #include <iostream> | + int {8:main}() | + { | + #ifdef {5:__cplusplus} | + const int {7:x} = 1; | + {4:std}::{2:cout} << {2:x} << {4:std}::{3:endl}; | + {6: #else} | + {6: comment} | + {6: #endif} | + ^} | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, { it = 'sumneko_lua', @@ -722,25 +804,45 @@ b = "as"]], start_col = 0, end_col = 10, type = 'comment', -- comment - extmark_added = true, + marked = true, }, { line = 1, - modifiers = { 'declaration' }, -- a + modifiers = { declaration = true }, -- a start_col = 6, end_col = 7, type = 'variable', - extmark_added = true, + marked = true, }, { line = 2, - modifiers = { 'static' }, -- b (global) + modifiers = { static = true }, -- b (global) start_col = 0, end_col = 1, type = 'variable', - extmark_added = true, + marked = true, }, }, + expected_screen = function() + screen:expect{grid=[[ + {6:-- comment} | + local {7:a} = 1 | + {2:b} = "as^" | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, { it = 'rust-analyzer', @@ -768,7 +870,7 @@ b = "as"]], start_col = 0, end_col = 3, -- pub type = 'keyword', - extmark_added = true, + marked = true, }, { line = 0, @@ -776,15 +878,15 @@ b = "as"]], start_col = 4, end_col = 6, -- fn type = 'keyword', - extmark_added = true, + marked = true, }, { line = 0, - modifiers = { 'declaration', 'public' }, + modifiers = { declaration = true, public = true }, start_col = 7, end_col = 11, -- main type = 'function', - extmark_added = true, + marked = true, }, { line = 0, @@ -792,7 +894,7 @@ b = "as"]], start_col = 11, end_col = 12, type = 'parenthesis', - extmark_added = true, + marked = true, }, { line = 0, @@ -800,7 +902,7 @@ b = "as"]], start_col = 12, end_col = 13, type = 'parenthesis', - extmark_added = true, + marked = true, }, { line = 0, @@ -808,15 +910,15 @@ b = "as"]], start_col = 14, end_col = 15, type = 'brace', - extmark_added = true, + marked = true, }, { line = 1, - modifiers = { 'controlFlow' }, + modifiers = { controlFlow = true }, start_col = 4, end_col = 9, -- break type = 'keyword', - extmark_added = true, + marked = true, }, { line = 1, @@ -824,7 +926,7 @@ b = "as"]], start_col = 10, end_col = 13, -- rust type = 'unresolvedReference', - extmark_added = true, + marked = true, }, { line = 1, @@ -832,15 +934,15 @@ b = "as"]], start_col = 13, end_col = 13, type = 'semicolon', - extmark_added = true, + marked = true, }, { line = 2, - modifiers = { 'documentation' }, + modifiers = { documentation = true }, start_col = 4, end_col = 11, type = 'comment', -- /// what? - extmark_added = true, + marked = true, }, { line = 3, @@ -848,9 +950,29 @@ b = "as"]], start_col = 0, end_col = 1, type = 'brace', - extmark_added = true, + marked = true, }, }, + expected_screen = function() + screen:expect{grid=[[ + pub fn {8:main}() { | + break rust; | + //{6:/ what?} | + } | + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, }) do it(test.it, function() @@ -877,6 +999,8 @@ b = "as"]], 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 @@ -906,28 +1030,68 @@ b = "as"]], { line = 0, modifiers = { - 'declaration', - 'globalScope', + declaration = true, + globalScope = true, }, start_col = 6, end_col = 9, type = 'variable', - extmark_added = true, + marked = true, } }, expected2 = { { line = 1, modifiers = { - 'declaration', - 'globalScope', + declaration = true, + globalScope = true, }, start_col = 6, end_col = 9, type = 'variable', - extmark_added = true, + marked = true, } }, + expected_screen1 = function() + screen:expect{grid=[[ + char* {7:foo} = "\n"^; | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, + expected_screen2 = function() + screen:expect{grid=[[ + ^ | + char* {7:foo} = "\n"; | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, { it = 'response with multiple delta edits', @@ -976,55 +1140,55 @@ int main() line = 2, start_col = 4, end_col = 8, - modifiers = { 'declaration', 'globalScope' }, + modifiers = { declaration = true, globalScope = true }, type = 'function', - extmark_added = true, + marked = true, }, { line = 4, start_col = 8, end_col = 9, - modifiers = { 'declaration', 'functionScope' }, + modifiers = { declaration = true, functionScope = true }, type = 'variable', - extmark_added = true, + marked = true, }, { line = 5, start_col = 7, end_col = 18, - modifiers = { 'globalScope' }, + modifiers = { globalScope = true }, type = 'macro', - extmark_added = true, + marked = true, }, { line = 6, start_col = 4, end_col = 7, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, type = 'namespace', - extmark_added = true, + marked = true, }, { line = 6, start_col = 9, end_col = 13, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, type = 'variable', - extmark_added = true, + marked = true, }, { line = 6, start_col = 17, end_col = 18, - extmark_added = true, - modifiers = { 'functionScope' }, + marked = true, + modifiers = { functionScope = true }, type = 'variable', }, { line = 7, start_col = 0, end_col = 5, - extmark_added = true, + marked = true, modifiers = {}, type = 'comment', }, @@ -1034,7 +1198,7 @@ int main() modifiers = {}, start_col = 0, type = 'comment', - extmark_added = true, + marked = true, }, { line = 9, @@ -1042,7 +1206,7 @@ int main() end_col = 6, modifiers = {}, type = 'comment', - extmark_added = true, + marked = true, } }, expected2 = { @@ -1050,63 +1214,63 @@ int main() line = 2, start_col = 4, end_col = 8, - modifiers = { 'declaration', 'globalScope' }, + modifiers = { declaration = true, globalScope = true }, type = 'function', - extmark_added = true, + marked = true, }, { line = 4, start_col = 8, end_col = 9, - modifiers = { 'declaration', 'globalScope' }, + modifiers = { declaration = true, globalScope = true }, type = 'function', - extmark_added = true, + marked = true, }, { line = 5, end_col = 12, start_col = 11, - modifiers = { 'declaration', 'functionScope' }, + modifiers = { declaration = true, functionScope = true }, type = 'variable', - extmark_added = true, + marked = true, }, { line = 6, start_col = 7, end_col = 18, - modifiers = { 'globalScope' }, + modifiers = { globalScope = true }, type = 'macro', - extmark_added = true, + marked = true, }, { line = 7, start_col = 4, end_col = 7, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, type = 'namespace', - extmark_added = true, + marked = true, }, { line = 7, start_col = 9, end_col = 13, - modifiers = { 'defaultLibrary', 'globalScope' }, + modifiers = { defaultLibrary = true, globalScope = true }, type = 'variable', - extmark_added = true, + marked = true, }, { line = 7, start_col = 17, end_col = 18, - extmark_added = true, - modifiers = { 'globalScope' }, + marked = true, + modifiers = { globalScope = true }, type = 'function', }, { line = 8, start_col = 0, end_col = 5, - extmark_added = true, + marked = true, modifiers = {}, type = 'comment', }, @@ -1116,7 +1280,7 @@ int main() modifiers = {}, start_col = 0, type = 'comment', - extmark_added = true, + marked = true, }, { line = 10, @@ -1124,9 +1288,49 @@ int main() end_col = 6, modifiers = {}, type = 'comment', - extmark_added = true, + marked = true, } }, + expected_screen1 = function() + 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:~ }| + {1:~ }| + | + ]]} + end, + expected_screen2 = function() + screen:expect{grid=[[ + #include <iostream> | + | + int {8:main}() | + { | + int {8:x}(); | + double {7:y}; | + #ifdef {5:__cplusplus} | + {4:std}::{2:cout} << {3:x} << "\n"; | + {6:#else} | + {6: printf("%d\n", x);} | + {6:^#endif} | + } | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, { it = 'optional token_edit.data on deletion', @@ -1146,16 +1350,56 @@ int main() { line = 0, modifiers = { - 'declaration', + declaration = true, }, start_col = 0, end_col = 6, type = 'variable', - extmark_added = true, + marked = true, } }, expected2 = { }, + expected_screen1 = function() + screen:expect{grid=[[ + {7:string} = "test^" | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, + expected_screen2 = function() + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end, }, }) do it(test.it, function() @@ -1192,6 +1436,8 @@ int main() insert(test.text1) + test.expected_screen1() + local highlights = exec_lua([[ return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights ]]) @@ -1204,10 +1450,12 @@ int main() 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 + vim.wait(15) -- wait for debounce ]], test.text2) end + test.expected_screen2() + highlights = exec_lua([[ return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights ]]) diff --git a/test/functional/plugin/lsp/snippet_spec.lua b/test/functional/plugin/lsp/snippet_spec.lua index 7903885420..ba8bc7fe04 100644 --- a/test/functional/plugin/lsp/snippet_spec.lua +++ b/test/functional/plugin/lsp/snippet_spec.lua @@ -1,130 +1,71 @@ local helpers = require('test.functional.helpers')(after_each) -local snippet = require('vim.lsp._snippet') +local snippet = require('vim.lsp._snippet_grammar') +local type = snippet.NodeType local eq = helpers.eq local exec_lua = helpers.exec_lua -describe('vim.lsp._snippet', function() +describe('vim.lsp._snippet_grammar', function() before_each(helpers.clear) after_each(helpers.clear) local parse = function(...) - return exec_lua('return require("vim.lsp._snippet").parse(...)', ...) + local res = exec_lua('return require("vim.lsp._snippet_grammar").parse(...)', ...) + return res.data.children end - it('should parse only text', function() + it('parses only text', function() eq({ - type = snippet.NodeType.SNIPPET, - children = { - { - type = snippet.NodeType.TEXT, - raw = 'TE\\$\\}XT', - esc = 'TE$}XT', - }, - }, + { type = type.Text, data = { text = 'TE$}XT' } }, }, parse('TE\\$\\}XT')) end) - it('should parse tabstop', function() + it('parses tabstops', function() eq({ - type = snippet.NodeType.SNIPPET, - children = { - { - type = snippet.NodeType.TABSTOP, - tabstop = 1, - }, - { - type = snippet.NodeType.TABSTOP, - tabstop = 2, - }, - }, + { type = type.Tabstop, data = { tabstop = 1 } }, + { type = type.Tabstop, data = { tabstop = 2 } }, }, parse('$1${2}')) end) - it('should parse placeholders', function() + it('parses nested placeholders', function() eq({ - type = snippet.NodeType.SNIPPET, - children = { - { - type = snippet.NodeType.PLACEHOLDER, + { + type = type.Placeholder, + data = { tabstop = 1, - children = { - { - type = snippet.NodeType.PLACEHOLDER, + value = { + type = type.Placeholder, + data = { tabstop = 2, - children = { - { - type = snippet.NodeType.TEXT, - raw = 'TE\\$\\}XT', - esc = 'TE$}XT', - }, - { - type = snippet.NodeType.TABSTOP, - tabstop = 3, - }, - { - type = snippet.NodeType.TABSTOP, - tabstop = 1, - transform = { - type = snippet.NodeType.TRANSFORM, - pattern = 'regex', - option = 'i', - format = { - { - type = snippet.NodeType.FORMAT, - capture_index = 1, - modifier = 'upcase', - }, - }, - }, - }, - { - type = snippet.NodeType.TEXT, - raw = 'TE\\$\\}XT', - esc = 'TE$}XT', - }, - }, + value = { type = type.Tabstop, data = { tabstop = 3 } }, }, }, }, }, - }, parse('${1:${2:TE\\$\\}XT$3${1/regex/${1:/upcase}/i}TE\\$\\}XT}}')) + }, parse('${1:${2:${3}}}')) end) - it('should parse variables', function() + it('parses variables', function() eq({ - type = snippet.NodeType.SNIPPET, - children = { - { - type = snippet.NodeType.VARIABLE, - name = 'VAR', - }, - { - type = snippet.NodeType.VARIABLE, + { type = type.Variable, data = { name = 'VAR' } }, + { type = type.Variable, data = { name = 'VAR' } }, + { + type = type.Variable, + data = { name = 'VAR', + default = { type = type.Tabstop, data = { tabstop = 1 } }, }, - { - type = snippet.NodeType.VARIABLE, + }, + { + type = type.Variable, + data = { name = 'VAR', - children = { + regex = 'regex', + options = '', + format = { { - type = snippet.NodeType.TABSTOP, - tabstop = 1, - }, - }, - }, - { - type = snippet.NodeType.VARIABLE, - name = 'VAR', - transform = { - type = snippet.NodeType.TRANSFORM, - pattern = 'regex', - format = { - { - type = snippet.NodeType.FORMAT, - capture_index = 1, - modifier = 'upcase', - }, + type = type.Format, + data = { capture = 1, modifier = 'upcase' }, }, }, }, @@ -132,105 +73,99 @@ describe('vim.lsp._snippet', function() }, parse('$VAR${VAR}${VAR:$1}${VAR/regex/${1:/upcase}/}')) end) - it('should parse choice', function() + it('parses choice', function() eq({ - type = snippet.NodeType.SNIPPET, - children = { - { - type = snippet.NodeType.CHOICE, - tabstop = 1, - items = { - ',', - '|', - }, - }, + { + type = type.Choice, + data = { tabstop = 1, values = { ',', '|' } }, }, }, parse('${1|\\,,\\||}')) end) - it('should parse format', function() - eq({ - type = snippet.NodeType.SNIPPET, - children = { + it('parses format', function() + eq( + { { - type = snippet.NodeType.VARIABLE, - name = 'VAR', - transform = { - type = snippet.NodeType.TRANSFORM, - pattern = 'regex', + type = type.Variable, + data = { + name = 'VAR', + regex = 'regex', + options = '', format = { { - type = snippet.NodeType.FORMAT, - capture_index = 1, - modifier = 'upcase', + type = type.Format, + data = { capture = 1, modifier = 'upcase' }, }, { - type = snippet.NodeType.FORMAT, - capture_index = 1, - if_text = 'if_text', - else_text = '', + type = type.Format, + data = { capture = 1, if_text = 'if_text' }, }, { - type = snippet.NodeType.FORMAT, - capture_index = 1, - if_text = '', - else_text = 'else_text', + type = type.Format, + data = { capture = 1, else_text = 'else_text' }, }, { - type = snippet.NodeType.FORMAT, - capture_index = 1, - else_text = 'else_text', - if_text = 'if_text', + type = type.Format, + data = { capture = 1, if_text = 'if_text', else_text = 'else_text' }, }, { - type = snippet.NodeType.FORMAT, - capture_index = 1, - if_text = '', - else_text = 'else_text', + type = type.Format, + data = { capture = 1, else_text = 'else_text' }, }, }, }, }, }, - }, parse('${VAR/regex/${1:/upcase}${1:+if_text}${1:-else_text}${1:?if_text:else_text}${1:else_text}/}')) + parse( + '${VAR/regex/${1:/upcase}${1:+if_text}${1:-else_text}${1:?if_text:else_text}${1:else_text}/}' + ) + ) end) - it('should parse empty strings', function() + it('parses empty strings', function() eq({ - children = { - { - children = { { - esc = '', - raw = '', - type = 7, - } }, + { + type = type.Placeholder, + data = { tabstop = 1, - type = 2, - }, - { - esc = ' ', - raw = ' ', - type = 7, + value = { type = type.Text, data = { text = '' } }, }, - { + }, + { + type = type.Text, + data = { text = ' ' }, + }, + { + type = type.Variable, + data = { name = 'VAR', - transform = { - format = { - { - capture_index = 1, - else_text = '', - if_text = '', - type = 6, - }, + regex = 'erg', + format = { + { + type = type.Format, + data = { capture = 1, if_text = '' }, }, - option = 'g', - pattern = 'erg', - type = 5, }, - type = 3, + options = 'g', }, }, - type = 0, - }, parse('${1:} ${VAR/erg/${1:?:}/g}')) + }, parse('${1:} ${VAR/erg/${1:+}/g}')) + end) + + it('parses closing curly brace as text', function() + eq( + { + { type = type.Text, data = { text = 'function ' } }, + { type = type.Tabstop, data = { tabstop = 1 } }, + { type = type.Text, data = { text = '() {\n ' } }, + { type = type.Tabstop, data = { tabstop = 0 } }, + { type = type.Text, data = { text = '\n}' } }, + }, + parse(table.concat({ + 'function $1() {', + ' $0', + '}', + }, '\n')) + ) end) end) diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua new file mode 100644 index 0000000000..804dc32f0d --- /dev/null +++ b/test/functional/plugin/lsp/utils_spec.lua @@ -0,0 +1,226 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local feed = helpers.feed + +local eq = helpers.eq +local exec_lua = helpers.exec_lua + +describe('vim.lsp.util', function() + before_each(helpers.clear) + + describe('stylize_markdown', function() + local stylize_markdown = function(content, opts) + return exec_lua([[ + 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) + end + + it('code fences', function() + local lines = { + "```lua", + "local hello = 'world'", + "```", + } + local expected = { + "local hello = 'world'", + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + + it('code fences with whitespace surrounded info string', function() + local lines = { + "``` lua ", + "local hello = 'world'", + "```", + } + local expected = { + "local hello = 'world'", + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + + it('adds separator after code block', function() + local lines = { + "```lua", + "local hello = 'world'", + "```", + "", + "something", + } + local expected = { + "local hello = 'world'", + "─────────────────────", + "something", + } + local opts = { separator = true } + eq(expected, stylize_markdown(lines, opts)) + end) + + it('replaces supported HTML entities', function() + local lines = { + "1 < 2", + "3 > 2", + ""quoted"", + "'apos'", + "   ", + "&", + } + local expected = { + "1 < 2", + "3 > 2", + '"quoted"', + "'apos'", + " ", + "&", + } + local opts = {} + eq(expected, stylize_markdown(lines, opts)) + end) + end) + + describe('normalize_markdown', function () + it('collapses consecutive blank lines', function () + local result = exec_lua [[ + local lines = { + 'foo', + '', + '', + '', + 'bar', + '', + 'baz' + } + return vim.lsp.util._normalize_markdown(lines) + ]] + local expected = {'foo', '', 'bar', '', 'baz'} + eq(expected, result) + end) + + it('removes preceding and trailing empty lines', function () + local result = exec_lua [[ + local lines = { + '', + 'foo', + 'bar', + '', + '' + } + return vim.lsp.util._normalize_markdown(lines) + ]] + local expected = {'foo', 'bar'} + eq(expected, result) + end) + end) + + describe("make_floating_popup_options", function () + + local function assert_anchor(anchor_bias, expected_anchor) + local opts = exec_lua([[ + local args = { ... } + local anchor_bias = args[1] + return vim.lsp.util.make_floating_popup_options(30, 10, { anchor_bias = anchor_bias }) + ]], anchor_bias) + + eq(expected_anchor, string.sub(opts.anchor, 1, 1)) + end + + local screen + before_each(function () + helpers.clear() + screen = Screen.new(80, 80) + screen:attach() + feed("79i<CR><Esc>") -- fill screen with empty lines + end) + + describe('when on the first line it places window below', function () + before_each(function () + feed('gg') + end) + + it('for anchor_bias = "auto"', function () + assert_anchor('auto', 'N') + end) + + it('for anchor_bias = "above"', function () + assert_anchor('above', 'N') + end) + + it('for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + end) + + describe('when on the last line it places window above', function () + before_each(function () + feed('G') + end) + + it('for anchor_bias = "auto"', function () + assert_anchor('auto', 'S') + end) + + it('for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('for anchor_bias = "below"', function () + assert_anchor('below', 'S') + end) + end) + + describe('with 20 lines above, 59 lines below', function () + before_each(function () + feed('gg20j') + end) + + it('places window below for anchor_bias = "auto"', function () + assert_anchor('auto', 'N') + end) + + it('places window above for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + end) + + describe('with 59 lines above, 20 lines below', function () + before_each(function () + feed('G20k') + end) + + it('places window above for anchor_bias = "auto"', function () + assert_anchor('auto', 'S') + end) + + it('places window above for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + + it('bordered window truncates dimensions correctly', function () + local opts = exec_lua([[ + return vim.lsp.util.make_floating_popup_options(100, 100, { border = 'single' }) + ]]) + + eq(56, opts.height) + end) + end) + end) + +end) diff --git a/test/functional/plugin/lsp/watchfiles_spec.lua b/test/functional/plugin/lsp/watchfiles_spec.lua new file mode 100644 index 0000000000..a8260e0c98 --- /dev/null +++ b/test/functional/plugin/lsp/watchfiles_spec.lua @@ -0,0 +1,222 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local exec_lua = helpers.exec_lua + +describe('vim.lsp._watchfiles', function() + before_each(helpers.clear) + after_each(helpers.clear) + + local match = function(...) + return exec_lua('return require("vim.lsp._watchfiles")._match(...)', ...) + end + + describe('glob matching', function() + it('should match literal strings', function() + eq(true, match('', '')) + eq(false, match('', 'a')) + eq(true, match('a', 'a')) + eq(true, match('/', '/')) + eq(true, match('abc', 'abc')) + eq(false, match('abc', 'abcdef')) + eq(false, match('abc', 'a')) + eq(false, match('abc', 'bc')) + eq(false, match('a', 'b')) + eq(false, match('.', 'a')) + eq(true, match('$', '$')) + eq(true, match('/dir', '/dir')) + eq(true, match('dir/', 'dir/')) + eq(true, match('dir/subdir', 'dir/subdir')) + eq(false, match('dir/subdir', 'subdir')) + eq(false, match('dir/subdir', 'dir/subdir/file')) + eq(true, match('🤠', '🤠')) + end) + + it('should match * wildcards', function() + eq(false, match('*', '')) + eq(true, match('*', 'a')) + eq(false, match('*', '/')) + eq(false, match('*', '/a')) + eq(false, match('*', 'a/')) + eq(true, match('*', 'aaa')) + eq(true, match('*a', 'aa')) + eq(true, match('*a', 'abca')) + eq(true, match('*.txt', 'file.txt')) + eq(false, match('*.txt', 'file.txtxt')) + eq(false, match('*.txt', 'dir/file.txt')) + eq(false, match('*.txt', '/dir/file.txt')) + eq(false, match('*.txt', 'C:/dir/file.txt')) + eq(false, match('*.dir', 'test.dir/file')) + eq(true, match('file.*', 'file.txt')) + eq(false, match('file.*', 'not-file.txt')) + eq(true, match('*/file.txt', 'dir/file.txt')) + eq(false, match('*/file.txt', 'dir/subdir/file.txt')) + eq(false, match('*/file.txt', '/dir/file.txt')) + eq(true, match('dir/*', 'dir/file.txt')) + eq(false, match('dir/*', 'dir')) + eq(false, match('dir/*.txt', 'file.txt')) + eq(true, match('dir/*.txt', 'dir/file.txt')) + eq(false, match('dir/*.txt', 'dir/subdir/file.txt')) + eq(false, match('dir/*/file.txt', 'dir/file.txt')) + eq(true, match('dir/*/file.txt', 'dir/subdir/file.txt')) + eq(false, match('dir/*/file.txt', 'dir/subdir/subdir/file.txt')) + + -- TODO: The spec does not describe this, but VSCode only interprets ** when it's by + -- itself in a path segment, and otherwise interprets ** as consecutive * directives. + -- The following tests show how this behavior should work, but is not yet fully implemented. + -- Currently, "a**" parses incorrectly as "a" "**" and "**a" parses correctly as "*" "*" "a". + -- see: https://github.com/microsoft/vscode/blob/eef30e7165e19b33daa1e15e92fa34ff4a5df0d3/src/vs/base/common/glob.ts#L112 + eq(true, match('a**', 'abc')) -- '**' should parse as two '*'s when not by itself in a path segment + eq(true, match('**c', 'abc')) + -- eq(false, match('a**', 'ab')) -- each '*' should still represent at least one character + eq(false, match('**c', 'bc')) + eq(true, match('a**', 'abcd')) + eq(true, match('**d', 'abcd')) + -- eq(false, match('a**', 'abc/d')) + eq(false, match('**d', 'abc/d')) + end) + + it('should match ? wildcards', function() + eq(false, match('?', '')) + eq(true, match('?', 'a')) + eq(false, match('??', 'a')) + eq(false, match('?', 'ab')) + eq(true, match('??', 'ab')) + eq(true, match('a?c', 'abc')) + eq(false, match('a?c', 'a/c')) + end) + + it('should match ** wildcards', function() + eq(true, match('**', '')) + eq(true, match('**', 'a')) + eq(true, match('**', '/')) + eq(true, match('**', 'a/')) + eq(true, match('**', '/a')) + eq(true, match('**', 'C:/a')) + eq(true, match('**', 'a/a')) + eq(true, match('**', 'a/a/a')) + eq(false, match('/**', '')) -- /** matches leading / literally + eq(true, match('/**', '/')) + eq(true, match('/**', '/a/b/c')) + eq(true, match('**/', '')) -- **/ absorbs trailing / + eq(true, match('**/', '/a/b/c')) + eq(true, match('**/**', '')) + eq(true, match('**/**', 'a')) + eq(false, match('a/**', '')) + eq(false, match('a/**', 'a')) + eq(true, match('a/**', 'a/b')) + eq(true, match('a/**', 'a/b/c')) + eq(false, match('a/**', 'b/a')) + eq(false, match('a/**', '/a')) + eq(false, match('**/a', '')) + eq(true, match('**/a', 'a')) + eq(false, match('**/a', 'a/b')) + eq(true, match('**/a', '/a')) + eq(true, match('**/a', '/b/a')) + eq(true, match('**/a', '/c/b/a')) + eq(true, match('**/a', '/a/a')) + eq(true, match('**/a', '/abc/a')) + eq(false, match('a/**/c', 'a')) + eq(false, match('a/**/c', 'c')) + eq(true, match('a/**/c', 'a/c')) + eq(true, match('a/**/c', 'a/b/c')) + eq(true, match('a/**/c', 'a/b/b/c')) + eq(false, match('**/a/**', 'a')) + eq(true, match('**/a/**', 'a/')) + eq(false, match('**/a/**', '/dir/a')) + eq(false, match('**/a/**', 'dir/a')) + eq(true, match('**/a/**', 'dir/a/')) + eq(true, match('**/a/**', 'a/dir')) + eq(true, match('**/a/**', 'dir/a/dir')) + eq(true, match('**/a/**', '/a/dir')) + eq(true, match('**/a/**', 'C:/a/dir')) + eq(false, match('**/a/**', 'a.txt')) + end) + + it('should match {} groups', function() + eq(true, match('{}', '')) + eq(false, match('{}', 'a')) + eq(true, match('a{}', 'a')) + eq(true, match('{}a', 'a')) + eq(true, match('{,}', '')) + eq(true, match('{a,}', '')) + eq(true, match('{a,}', 'a')) + eq(true, match('{a}', 'a')) + eq(false, match('{a}', 'aa')) + eq(false, match('{a}', 'ab')) + eq(true, match('{a?c}', 'abc')) + eq(false, match('{ab}', 'a')) + eq(false, match('{ab}', 'b')) + eq(true, match('{ab}', 'ab')) + eq(true, match('{a,b}', 'a')) + eq(true, match('{a,b}', 'b')) + eq(false, match('{a,b}', 'ab')) + eq(true, match('{ab,cd}', 'ab')) + eq(false, match('{ab,cd}', 'a')) + eq(true, match('{ab,cd}', 'cd')) + eq(true, match('{a,b,c}', 'c')) + eq(true, match('{a,{b,c}}', 'c')) + end) + + it('should match [] groups', function() + eq(true, match('[]', '[]')) -- empty [] is a literal + eq(false, match('[a-z]', '')) + eq(true, match('[a-z]', 'a')) + eq(false, match('[a-z]', 'ab')) + eq(true, match('[a-z]', 'z')) + eq(true, match('[a-z]', 'j')) + eq(false, match('[a-f]', 'j')) + eq(false, match('[a-z]', '`')) -- 'a' - 1 + eq(false, match('[a-z]', '{')) -- 'z' + 1 + eq(false, match('[a-z]', 'A')) + eq(false, match('[a-z]', '5')) + eq(true, match('[A-Z]', 'A')) + eq(true, match('[A-Z]', 'Z')) + eq(true, match('[A-Z]', 'J')) + eq(false, match('[A-Z]', '@')) -- 'A' - 1 + eq(false, match('[A-Z]', '[')) -- 'Z' + 1 + eq(false, match('[A-Z]', 'a')) + eq(false, match('[A-Z]', '5')) + eq(true, match('[a-zA-Z0-9]', 'z')) + eq(true, match('[a-zA-Z0-9]', 'Z')) + eq(true, match('[a-zA-Z0-9]', '9')) + eq(false, match('[a-zA-Z0-9]', '&')) + end) + + it('should match [!...] groups', function() + eq(true, match('[!]', '[!]')) -- [!] is a literal + eq(false, match('[!a-z]', '')) + eq(false, match('[!a-z]', 'a')) + eq(false, match('[!a-z]', 'z')) + eq(false, match('[!a-z]', 'j')) + eq(true, match('[!a-f]', 'j')) + eq(false, match('[!a-f]', 'jj')) + eq(true, match('[!a-z]', '`')) -- 'a' - 1 + eq(true, match('[!a-z]', '{')) -- 'z' + 1 + eq(false, match('[!a-zA-Z0-9]', 'a')) + eq(false, match('[!a-zA-Z0-9]', 'A')) + eq(false, match('[!a-zA-Z0-9]', '0')) + eq(true, match('[!a-zA-Z0-9]', '!')) + end) + + it('should match complex patterns', function() + eq(false, match('**/*.{c,h}', '')) + eq(false, match('**/*.{c,h}', 'c')) + eq(false, match('**/*.{c,h}', 'file.m')) + eq(true, match('**/*.{c,h}', 'file.c')) + eq(true, match('**/*.{c,h}', 'file.h')) + eq(true, match('**/*.{c,h}', '/file.c')) + eq(true, match('**/*.{c,h}', 'dir/subdir/file.c')) + eq(true, match('**/*.{c,h}', 'dir/subdir/file.h')) + eq(true, match('**/*.{c,h}', '/dir/subdir/file.c')) + eq(true, match('**/*.{c,h}', 'C:/dir/subdir/file.c')) + eq(true, match('/dir/**/*.{c,h}', '/dir/file.c')) + eq(false, match('/dir/**/*.{c,h}', 'dir/file.c')) + eq(true, match('/dir/**/*.{c,h}', '/dir/subdir/subdir/file.c')) + + eq(true, match('{[0-9],[a-z]}', '0')) + eq(true, match('{[0-9],[a-z]}', 'a')) + eq(false, match('{[0-9],[a-z]}', 'A')) + end) + end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index fd162961ff..56d31a0e44 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -23,6 +23,7 @@ local is_ci = helpers.is_ci local meths = helpers.meths local is_os = helpers.is_os local skip = helpers.skip +local mkdir = helpers.mkdir local clear_notrace = lsp_helpers.clear_notrace local create_server_definition = lsp_helpers.create_server_definition @@ -30,6 +31,13 @@ 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 +local function get_buf_option(name, bufnr) + bufnr = bufnr or "BUFFER" + return exec_lua( + string.format("return vim.api.nvim_get_option_value('%s', { buf = %s })", name, bufnr) + ) +end + -- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837 if skip(is_os('win')) then return end @@ -51,12 +59,13 @@ describe('LSP', function() return lsp.start_client { cmd_env = { NVIM_LOG_FILE = fake_lsp_logfile; + NVIM_APPNAME = "nvim_lsp_test"; }; cmd = { vim.v.progpath, '-l', fake_lsp_code, test_name; }; workspace_folders = {{ - uri = 'file://' .. vim.loop.cwd(), + uri = 'file://' .. vim.uv.cwd(), name = 'test_folder', }}; } @@ -73,7 +82,7 @@ describe('LSP', function() describe('server_name specified', function() it('start_client(), stop_client()', function() retry(nil, 4000, function() - eq(1, exec_lua('return #lsp.get_active_clients()')) + eq(1, exec_lua('return #lsp.get_clients()')) end) eq(2, exec_lua([[ TEST_CLIENT2 = test__start_client() @@ -84,20 +93,20 @@ describe('LSP', function() return TEST_CLIENT3 ]])) retry(nil, 4000, function() - eq(3, exec_lua('return #lsp.get_active_clients()')) + eq(3, exec_lua('return #lsp.get_clients()')) end) eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil')) eq(false, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).is_stopped()')) exec_lua('return lsp.get_client_by_id(TEST_CLIENT1).stop()') retry(nil, 4000, function() - eq(2, exec_lua('return #lsp.get_active_clients()')) + eq(2, exec_lua('return #lsp.get_clients()')) end) eq(true, exec_lua('return lsp.get_client_by_id(TEST_CLIENT1) == nil')) exec_lua('lsp.stop_client({TEST_CLIENT2, TEST_CLIENT3})') retry(nil, 4000, function() - eq(0, exec_lua('return #lsp.get_active_clients()')) + eq(0, exec_lua('return #lsp.get_clients()')) end) end) @@ -107,12 +116,12 @@ describe('LSP', function() TEST_CLIENT3 = test__start_client() ]]) retry(nil, 4000, function() - eq(3, exec_lua('return #lsp.get_active_clients()')) + eq(3, exec_lua('return #lsp.get_clients()')) end) -- Stop all clients. - exec_lua('lsp.stop_client(lsp.get_active_clients())') + exec_lua('lsp.stop_client(lsp.get_clients())') retry(nil, 4000, function() - eq(0, exec_lua('return #lsp.get_active_clients()')) + eq(0, exec_lua('return #lsp.get_clients()')) end) end) end) @@ -142,7 +151,7 @@ describe('LSP', function() describe('basic_init test', function() after_each(function() stop() - exec_lua("lsp.stop_client(lsp.get_active_clients(), true)") + exec_lua("lsp.stop_client(lsp.get_clients(), true)") exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })") end) @@ -209,6 +218,34 @@ describe('LSP', function() }) end) + it("should set the client's offset_encoding when positionEncoding capability is supported", function() + clear() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server({ + capabilities = { + positionEncoding = "utf-8" + }, + }) + + local client_id = vim.lsp.start({ + name = 'dummy', + cmd = server.cmd, + }) + + if not client_id then + return 'vim.lsp.start did not return client_id' + end + + local client = vim.lsp.get_client_by_id(client_id) + if not client then + return 'No client found with id ' .. client_id + end + return client.offset_encoding + ]]) + eq('utf-8', result) + end) + it('should succeed with manual shutdown', function() if is_ci() then pending('hangs the build on CI #14028, re-enable with freeze timeout #14204') @@ -312,6 +349,118 @@ describe('LSP', function() } end) + it('should set default options on attach', function() + local client + test_rpc_server { + test_name = "set_defaults_all_capabilities"; + on_init = function(_client) + client = _client + exec_lua [[ + BUFFER = vim.api.nvim_create_buf(false, true) + lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID) + ]] + end; + on_handler = function(_, _, ctx) + if ctx.method == 'test' then + eq('v:lua.vim.lsp.tagfunc', get_buf_option("tagfunc")) + eq('v:lua.vim.lsp.omnifunc', get_buf_option("omnifunc")) + eq('v:lua.vim.lsp.formatexpr()', get_buf_option("formatexpr")) + eq('', get_buf_option("keywordprg")) + eq(true, exec_lua[[ + local keymap + vim.api.nvim_buf_call(BUFFER, function() + keymap = vim.fn.maparg("K", "n", false, true) + end) + return keymap.callback == vim.lsp.buf.hover + ]]) + client.stop() + end + end; + on_exit = function(_, _) + eq('', get_buf_option("tagfunc")) + eq('', get_buf_option("omnifunc")) + eq('', get_buf_option("formatexpr")) + eq('', exec_lua[[ + local keymap + vim.api.nvim_buf_call(BUFFER, function() + keymap = vim.fn.maparg("K", "n", false, false) + end) + return keymap + ]]) + end; + } + end) + + it('should overwrite options set by ftplugins', function() + local client + test_rpc_server { + test_name = "set_defaults_all_capabilities"; + on_init = function(_client) + client = _client + exec_lua [[ + vim.api.nvim_command('filetype plugin on') + BUFFER_1 = vim.api.nvim_create_buf(false, true) + BUFFER_2 = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value('filetype', 'man', { buf = BUFFER_1 }) + vim.api.nvim_set_option_value('filetype', 'xml', { buf = BUFFER_2 }) + ]] + + -- Sanity check to ensure that some values are set after setting filetype. + eq('v:lua.require\'man\'.goto_tag', get_buf_option("tagfunc", "BUFFER_1")) + eq('xmlcomplete#CompleteTags', get_buf_option("omnifunc", "BUFFER_2")) + eq('xmlformat#Format()', get_buf_option("formatexpr", "BUFFER_2")) + + exec_lua [[ + lsp.buf_attach_client(BUFFER_1, TEST_RPC_CLIENT_ID) + lsp.buf_attach_client(BUFFER_2, TEST_RPC_CLIENT_ID) + ]] + end; + on_handler = function(_, _, ctx) + if ctx.method == 'test' then + eq('v:lua.vim.lsp.tagfunc', get_buf_option("tagfunc", "BUFFER_1")) + eq('v:lua.vim.lsp.omnifunc', get_buf_option("omnifunc", "BUFFER_2")) + eq('v:lua.vim.lsp.formatexpr()', get_buf_option("formatexpr", "BUFFER_2")) + client.stop() + end + end; + on_exit = function(_, _) + eq('', get_buf_option("tagfunc", "BUFFER_1")) + eq('', get_buf_option("omnifunc", "BUFFER_2")) + eq('', get_buf_option("formatexpr", "BUFFER_2")) + end; + } + end) + + it('should not overwrite user-defined options', function() + local client + test_rpc_server { + test_name = "set_defaults_all_capabilities"; + on_init = function(_client) + client = _client + exec_lua [[ + BUFFER = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value('tagfunc', 'tfu', { buf = BUFFER }) + vim.api.nvim_set_option_value('omnifunc', 'ofu', { buf = BUFFER }) + vim.api.nvim_set_option_value('formatexpr', 'fex', { buf = BUFFER }) + lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID) + ]] + end; + on_handler = function(_, _, ctx) + if ctx.method == 'test' then + eq('tfu', get_buf_option("tagfunc")) + eq('ofu', get_buf_option("omnifunc")) + eq('fex', get_buf_option("formatexpr")) + client.stop() + end + end; + on_exit = function(_, _) + eq('tfu', get_buf_option("tagfunc")) + eq('ofu', get_buf_option("omnifunc")) + eq('fex', get_buf_option("formatexpr")) + end; + } + end) + it('should detach buffer on bufwipe', function() clear() exec_lua(create_server_definition) @@ -842,7 +991,7 @@ describe('LSP', function() test_name = "check_tracked_requests_cleared"; on_init = function(_client) command('let g:requests = 0') - command('autocmd User LspRequest let g:requests+=1') + command('autocmd LspRequest * let g:requests+=1') client = _client client.request("slow_request") eq(1, eval('g:requests')) @@ -936,7 +1085,7 @@ describe('LSP', function() eq(full_kind, client.server_capabilities().textDocumentSync.change) eq(true, client.server_capabilities().textDocumentSync.openClose) exec_lua [[ - assert(not lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Shouldn't attach twice") + assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID), "Already attached, returns true") ]] end; on_exit = function(code, signal) @@ -1062,7 +1211,7 @@ describe('LSP', function() "testing"; "123"; }) - vim.api.nvim_buf_set_option(BUFFER, 'eol', false) + vim.bo[BUFFER].eol = false ]] end; on_init = function(_client) @@ -1095,6 +1244,67 @@ describe('LSP', function() } end) + it('should send correct range for inlay hints with noeol', function() + local expected_handlers = { + {NIL, {}, {method="shutdown", client_id=1}}; + {NIL, {}, {method="finish", client_id=1}}; + {NIL, {}, { + method="textDocument/inlayHint", + params = { + textDocument = { + uri = 'file://', + }, + range = { + start = { line = 0, character = 0 }, + ['end'] = { line = 1, character = 3 }, + } + }, + bufnr=2, + client_id=1, + }}; + {NIL, {}, {method="start", client_id=1}}; + } + local client + test_rpc_server { + test_name = "inlay_hint"; + on_setup = function() + exec_lua [[ + BUFFER = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(BUFFER, 0, -1, false, { + "testing"; + "123"; + }) + vim.bo[BUFFER].eol = false + ]] + end; + on_init = function(_client) + client = _client + eq(true, client.supports_method('textDocument/inlayHint')) + exec_lua [[ + assert(lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)) + ]] + end; + on_exit = function(code, signal) + eq(0, code, "exit code") + eq(0, signal, "exit signal") + end; + on_handler = function(err, result, ctx) + if ctx.method == 'start' then + exec_lua [[ + vim.lsp.inlay_hint.enable(BUFFER) + ]] + end + if ctx.method == 'textDocument/inlayHint' then + client.notify('finish') + end + eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") + if ctx.method == 'finish' then + client.stop() + end + end; + } + end) + it('should check the body and didChange incremental', function() local expected_handlers = { {NIL, {}, {method="shutdown", client_id=1}}; @@ -1545,6 +1755,54 @@ describe('LSP', function() 'foobar'; }, buf_lines(1)) end) + it('it restores marks', function() + local edits = { + make_edit(1, 0, 2, 5, "foobar"); + make_edit(4, 0, 5, 0, "barfoo"); + } + eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 2, 1, {})')) + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16") + eq({ + 'First line of text'; + 'foobar line of text'; + 'Fourth line of text'; + 'barfoo'; + }, buf_lines(1)) + local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")') + eq({ 2, 1 }, mark) + end) + + it('it restores marks to last valid col', function() + local edits = { + make_edit(1, 0, 2, 15, "foobar"); + make_edit(4, 0, 5, 0, "barfoo"); + } + eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 2, 10, {})')) + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16") + eq({ + 'First line of text'; + 'foobarext'; + 'Fourth line of text'; + 'barfoo'; + }, buf_lines(1)) + local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")') + eq({ 2, 9 }, mark) + end) + + it('it restores marks to last valid line', function() + local edits = { + make_edit(1, 0, 4, 5, "foobar"); + make_edit(4, 0, 5, 0, "barfoo"); + } + eq(true, exec_lua('return vim.api.nvim_buf_set_mark(1, "a", 4, 1, {})')) + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-16") + eq({ + 'First line of text'; + 'foobaro'; + }, buf_lines(1)) + local mark = exec_lua('return vim.api.nvim_buf_get_mark(1, "a")') + eq({ 2, 1 }, mark) + end) describe('cursor position', function() it('don\'t fix the cursor if the range contains the cursor', function() @@ -1590,7 +1848,7 @@ describe('LSP', function() eq({ 'First line of text'; }, buf_lines(1)) - eq({ 1, 6 }, funcs.nvim_win_get_cursor(0)) + eq({ 1, 17 }, funcs.nvim_win_get_cursor(0)) end) it('fix the cursor row', function() @@ -1609,6 +1867,9 @@ describe('LSP', function() end) it('fix the cursor col', function() + -- append empty last line. See #22636 + exec_lua('vim.api.nvim_buf_set_lines(...)', 1, -1, -1, true, {''}) + funcs.nvim_win_set_cursor(0, { 2, 11 }) local edits = { make_edit(1, 7, 1, 11, '') @@ -1620,6 +1881,7 @@ describe('LSP', function() 'Third line of text'; 'Fourth line of text'; 'å å ɧ 汉语 ↥ 🤦 🦄'; + ''; }, buf_lines(1)) eq({ 2, 7 }, funcs.nvim_win_get_cursor(0)) end) @@ -1921,7 +2183,7 @@ 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)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) end) it('Supports file creation in folder that needs to be created with CreateFile payload', function() local tmpfile = helpers.tmpname() @@ -1937,7 +2199,7 @@ 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)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) end) it('createFile does not touch file if it exists and ignoreIfExists is set', function() local tmpfile = helpers.tmpname() @@ -1955,7 +2217,7 @@ 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)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) eq('Dummy content', read_file(tmpfile)) end) it('createFile overrides file if overwrite is set', function() @@ -1975,7 +2237,7 @@ 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)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) eq('', read_file(tmpfile)) end) it('DeleteFile delete file and buffer', function() @@ -1996,7 +2258,7 @@ describe('LSP', function() } } eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')) - eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile)) + eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) eq(false, exec_lua('return vim.api.nvim_buf_is_loaded(vim.fn.bufadd(...))', tmpfile)) end) it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function() @@ -2015,58 +2277,13 @@ describe('LSP', function() } } eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit)) - eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile)) - end) - end) - - describe('completion_list_to_complete_items', function() - -- Completion option precedence: - -- textEdit.newText > insertText > label - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion - it('should choose right completion option', function () - local prefix = 'foo' - local completion_list = { - -- resolves into label - { label='foobar', sortText="a" }, - { label='foobar', sortText="b", textEdit={} }, - -- resolves into insertText - { label='foocar', sortText="c", insertText='foobar' }, - { label='foocar', sortText="d", insertText='foobar', textEdit={} }, - -- resolves into textEdit.newText - { label='foocar', sortText="e", insertText='foodar', textEdit={newText='foobar'} }, - { label='foocar', sortText="f", textEdit={newText='foobar'} }, - -- real-world snippet text - { label='foocar', sortText="g", insertText='foodar', insertTextFormat=2, textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} }, - { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} }, - -- nested snippet tokens - { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} }, - -- braced tabstop - { label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} }, - -- plain text - { label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} }, - } - local completion_list_items = {items=completion_list} - local expected = { - { abbr = 'foobar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label = 'foobar', sortText="a" } } } } }, - { abbr = 'foobar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foobar', sortText="b", textEdit={} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="c", insertText='foobar' } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="d", insertText='foobar', textEdit={} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="e", insertText='foodar', textEdit={newText='foobar'} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="f", textEdit={newText='foobar'} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar(place holder, more ...holder{})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="g", insertText='foodar', insertTextFormat=2, textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ1, var2 *typ2) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="h", insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', insertTextFormat=2, textEdit={} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ2 tail) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="i", insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', insertTextFormat=2, textEdit={} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar()', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="j", insertText='foodar()${0}', insertTextFormat=2, textEdit={} } } } } }, - { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(${1:var1})', user_data = { nvim = { lsp = { completion_item = { label='foocar', sortText="k", insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} } } } } }, - } - - eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list, prefix)) - eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list_items, prefix)) - eq({}, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], {}, prefix)) + eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', tmpfile)) end) end) describe('lsp.util.rename', function() + local pathsep = helpers.get_pathsep() + it('Can rename an existing file', function() local old = helpers.tmpname() write_file(old, 'Test content') @@ -2083,12 +2300,56 @@ describe('LSP', function() return vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) ]], old, new) eq({'Test content'}, lines) - local exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', old) + local exists = exec_lua('return vim.uv.fs_stat(...) ~= nil', old) eq(false, exists) - exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', new) + exists = exec_lua('return vim.uv.fs_stat(...) ~= nil', new) eq(true, exists) os.remove(new) end) + it("Kills old buffer after renaming an existing file", function() + local old = helpers.tmpname() + write_file(old, 'Test content') + local new = helpers.tmpname() + os.remove(new) -- only reserve the name, file must not exist for the test scenario + local lines = exec_lua([[ + local old = select(1, ...) + local oldbufnr = vim.fn.bufadd(old) + local new = select(2, ...) + vim.lsp.util.rename(old, new) + return vim.fn.bufloaded(oldbufnr) + ]], old, new) + eq(0, lines) + os.remove(new) + end) + it('Can rename a directory', function() + -- only reserve the name, file must not exist for the test scenario + local old_dir = helpers.tmpname() + local new_dir = helpers.tmpname() + os.remove(old_dir) + os.remove(new_dir) + + helpers.mkdir_p(old_dir) + + local file = 'file.txt' + write_file(old_dir .. pathsep .. file, 'Test content') + + local lines = exec_lua([[ + local old_dir = select(1, ...) + local new_dir = select(2, ...) + local pathsep = select(3, ...) + local oldbufnr = vim.fn.bufadd(old_dir .. pathsep .. 'file') + + vim.lsp.util.rename(old_dir, new_dir) + return vim.fn.bufloaded(oldbufnr) + ]], old_dir, new_dir, pathsep) + eq(0, lines) + eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', old_dir)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new_dir)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new_dir .. pathsep .. file)) + eq('Test content', read_file(new_dir .. pathsep .. file)) + + os.remove(new_dir) + end) it('Does not rename file if target exists and ignoreIfExists is set or overwrite is false', function() local old = helpers.tmpname() write_file(old, 'Old File') @@ -2102,7 +2363,7 @@ describe('LSP', function() vim.lsp.util.rename(old, new, { ignoreIfExists = true }) ]], old, new) - eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', old)) eq('New file', read_file(new)) exec_lua([[ @@ -2112,7 +2373,7 @@ describe('LSP', function() vim.lsp.util.rename(old, new, { overwrite = false }) ]], old, new) - eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', old)) eq('New file', read_file(new)) end) it('Does override target if overwrite is true', function() @@ -2127,8 +2388,8 @@ describe('LSP', function() vim.lsp.util.rename(old, new, { overwrite = true }) ]], old, new) - eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', old)) - eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', new)) + eq(false, exec_lua('return vim.uv.fs_stat(...) ~= nil', old)) + eq(true, exec_lua('return vim.uv.fs_stat(...) ~= nil', new)) eq('Old file\n', read_file(new)) end) end) @@ -2140,7 +2401,14 @@ describe('LSP', function() filename = '/fake/uri', lnum = 1, col = 3, - text = 'testing' + text = 'testing', + user_data = { + uri = 'file:///fake/uri', + range = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + } + } }, } local actual = exec_lua [[ @@ -2166,7 +2434,18 @@ describe('LSP', function() filename = '/fake/uri', lnum = 1, col = 3, - text = 'testing' + text = 'testing', + user_data = { + targetUri = "file:///fake/uri", + targetRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + }, + targetSelectionRange = { + start = { line = 0, character = 2 }, + ['end'] = { line = 0, character = 3 }, + } + } }, } local actual = exec_lua [[ @@ -2443,18 +2722,6 @@ describe('LSP', function() end) end) - describe('lsp.util._get_completion_item_kind_name', function() - it('returns the name specified by protocol', function() - eq("Text", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1)")) - eq("TypeParameter", exec_lua("return vim.lsp.util._get_completion_item_kind_name(25)")) - end) - it('returns the name not specified by protocol', function() - eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(nil)")) - eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(vim.NIL)")) - eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1000)")) - end) - end) - describe('lsp.util._get_symbol_kind_name', function() it('returns the name specified by protocol', function() eq("File", exec_lua("return vim.lsp.util._get_symbol_kind_name(1)")) @@ -2743,6 +3010,18 @@ describe('LSP', function() it('calculates size correctly with wrapping', function() eq({15,5}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents,{width = 15, wrap_at = 14})} ]]) end) + + it('handles NUL bytes in text', function() + exec_lua([[ contents = { + '\000\001\002\003\004\005\006\007\008\009', + '\010\011\012\013\014\015\016\017\018\019', + '\020\021\022\023\024\025\026\027\028\029', + } ]]) + command('set list listchars=') + eq({20,3}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents)} ]]) + command('set display+=uhex') + eq({40,3}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents)} ]]) + end) end) describe('lsp.util.trim.trim_empty_lines', function() @@ -2759,7 +3038,7 @@ describe('LSP', function() activeSignature = -1, signatures = { { - documentation = "", + documentation = "some doc", label = "TestEntity.TestEntity()", parameters = {} }, @@ -2767,7 +3046,7 @@ describe('LSP', function() } return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', {','}) ]] - local expected = {'```cs', 'TestEntity.TestEntity()', '```', ''} + local expected = {'```cs', 'TestEntity.TestEntity()', '```', 'some doc'} eq(expected, result) end) end) @@ -2775,8 +3054,8 @@ describe('LSP', function() describe('lsp.util.get_effective_tabstop', function() local function test_tabstop(tabsize, shiftwidth) exec_lua(string.format([[ - vim.api.nvim_buf_set_option(0, 'shiftwidth', %d) - vim.api.nvim_buf_set_option(0, 'tabstop', 2) + vim.bo.shiftwidth = %d + vim.bo.tabstop = 2 ]], shiftwidth)) eq(tabsize, exec_lua('return vim.lsp.util.get_effective_tabstop()')) end @@ -2998,9 +3277,10 @@ describe('LSP', function() eq(0, signal, "exit signal") end; on_handler = function(err, result, ctx) - -- Don't compare & assert params, they're not relevant for the testcase + -- Don't compare & assert params and version, they're not relevant for the testcase -- This allows us to be lazy and avoid declaring them ctx.params = nil + ctx.version = nil eq(table.remove(test.expected_handlers), {err, result, ctx}, "expected handler") if ctx.method == 'start' then @@ -3082,6 +3362,7 @@ describe('LSP', function() end, on_handler = function(err, result, ctx) ctx.params = nil -- don't compare in assert + ctx.version = nil eq(table.remove(expected_handlers), { err, result, ctx }) if ctx.method == 'start' then exec_lua([[ @@ -3123,22 +3404,22 @@ describe('LSP', function() vim.lsp.commands['executed_preferred'] = function() end end - vim.lsp.commands['quickfix_command'] = function(cmd) - vim.lsp.commands['executed_quickfix'] = function() + vim.lsp.commands['type_annotate_command'] = function(cmd) + vim.lsp.commands['executed_type_annotate'] = function() end end local bufnr = vim.api.nvim_get_current_buf() vim.lsp.buf_attach_client(bufnr, TEST_RPC_CLIENT_ID) vim.lsp.buf.code_action({ filter = function(a) return a.isPreferred end, apply = true, }) vim.lsp.buf.code_action({ - -- expect to be returned actions 'quickfix' and 'quickfix.foo' - context = { only = {'quickfix'}, }, + -- expect to be returned actions 'type-annotate' and 'type-annotate.foo' + context = { only = { 'type-annotate' }, }, apply = true, filter = function(a) - if a.kind == 'quickfix.foo' then - vim.lsp.commands['filtered_quickfix_foo'] = function() end + if a.kind == 'type-annotate.foo' then + vim.lsp.commands['filtered_type_annotate_foo'] = function() end return false - elseif a.kind == 'quickfix' then + elseif a.kind == 'type-annotate' then return true else assert(nil, 'unreachable') @@ -3148,13 +3429,57 @@ describe('LSP', function() ]]) elseif ctx.method == 'shutdown' then eq('function', exec_lua[[return type(vim.lsp.commands['executed_preferred'])]]) - eq('function', exec_lua[[return type(vim.lsp.commands['filtered_quickfix_foo'])]]) - eq('function', exec_lua[[return type(vim.lsp.commands['executed_quickfix'])]]) + eq('function', exec_lua[[return type(vim.lsp.commands['filtered_type_annotate_foo'])]]) + eq('function', exec_lua[[return type(vim.lsp.commands['executed_type_annotate'])]]) client.stop() end end } end) + it("Fallback to command execution on resolve error", function() + clear() + exec_lua(create_server_definition) + local result = exec_lua([[ + local server = _create_server({ + capabilities = { + executeCommandProvider = { + commands = {"command:1"}, + }, + codeActionProvider = { + resolveProvider = true + } + }, + handlers = { + ["textDocument/codeAction"] = function() + return { + { + title = "Code Action 1", + command = { + title = "Command 1", + command = "command:1", + } + } + } + end, + ["codeAction/resolve"] = function() + return nil, "resolve failed" + end, + } + }) + + local client_id = vim.lsp.start({ + name = "dummy", + cmd = server.cmd, + }) + + vim.lsp.buf.code_action({ apply = true }) + vim.lsp.stop_client(client_id) + return server.messages + ]]) + eq("codeAction/resolve", result[4].method) + eq("workspace/executeCommand", result[5].method) + eq("command:1", result[5].params.command) + end) end) describe('vim.lsp.commands', function() it('Accepts only string keys', function() @@ -3421,6 +3746,7 @@ describe('LSP', function() vim.cmd.normal('v') vim.api.nvim_win_set_cursor(0, { 2, 3 }) vim.lsp.buf.format({ bufnr = bufnr, false }) + vim.lsp.stop_client(client_id) return server.messages ]]) eq("textDocument/rangeFormatting", result[3].method) @@ -3430,6 +3756,52 @@ describe('LSP', function() } eq(expected_range, result[3].params.range) end) + it('format formats range in visual line 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 baz'}) + vim.api.nvim_win_set_cursor(0, { 1, 2 }) + vim.cmd.normal('V') + vim.api.nvim_win_set_cursor(0, { 2, 1 }) + vim.lsp.buf.format({ bufnr = bufnr, false }) + + -- Format again with visual lines going from bottom to top + -- Must result in same formatting + vim.cmd.normal("<ESC>") + vim.api.nvim_win_set_cursor(0, { 2, 1 }) + vim.cmd.normal('V') + vim.api.nvim_win_set_cursor(0, { 1, 2 }) + vim.lsp.buf.format({ bufnr = bufnr, false }) + + vim.lsp.stop_client(client_id) + return server.messages + ]]) + local expected_methods = { + "initialize", + "initialized", + "textDocument/rangeFormatting", + "$/cancelRequest", + "textDocument/rangeFormatting", + "$/cancelRequest", + "shutdown", + "exit", + } + eq(expected_methods, vim.tbl_map(function(x) return x.method end, result)) + -- uses first column of start line and last column of end line + local expected_range = { + start = { line = 0, character = 0 }, + ['end'] = { line = 1, character = 7 }, + } + eq(expected_range, result[3].params.range) + eq(expected_range, result[5].params.range) + end) it('Aborts with notify if no clients support requested method', function() exec_lua(create_server_definition) exec_lua([[ @@ -3466,7 +3838,7 @@ describe('LSP', function() describe('cmd', function() it('can connect to lsp server via rpc.connect', function() local result = exec_lua [[ - local uv = vim.loop + local uv = vim.uv local server = uv.new_tcp() local init = nil server:bind('127.0.0.1', 0) @@ -3494,7 +3866,7 @@ describe('LSP', function() describe('handlers', function() it('handler can return false as response', function() local result = exec_lua [[ - local uv = vim.loop + local uv = vim.uv local server = uv.new_tcp() local messages = {} local responses = {} @@ -3561,4 +3933,626 @@ describe('LSP', function() eq(expected, result) end) end) + + describe('#dynamic vim.lsp._dynamic', function() + it('supports dynamic registration', function() + local root_dir = helpers.tmpname() + os.remove(root_dir) + mkdir(root_dir) + local tmpfile = root_dir .. '/dynamic.foo' + local file = io.open(tmpfile, 'w') + file:close() + + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir, tmpfile = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'dynamic-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + textDocument = { + formatting = { + dynamicRegistration = true, + }, + rangeFormatting = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'formatting', + method = 'textDocument/formatting', + registerOptions = { + documentSelector = {{ + pattern = root_dir .. '/*.foo', + }}, + }, + }, + }, + }, { client_id = client_id }) + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'range-formatting', + method = 'textDocument/rangeFormatting', + }, + }, + }, { client_id = client_id }) + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'completion', + method = 'textDocument/completion', + }, + }, + }, { client_id = client_id }) + + local result = {} + local function check(method, fname) + local bufnr = fname and vim.fn.bufadd(fname) or nil + local client = vim.lsp.get_client_by_id(client_id) + result[#result + 1] = {method = method, fname = fname, supported = client.supports_method(method, {bufnr = bufnr})} + end + + + check("textDocument/formatting") + check("textDocument/formatting", tmpfile) + check("textDocument/rangeFormatting") + check("textDocument/rangeFormatting", tmpfile) + check("textDocument/completion") + + return result + ]], root_dir, tmpfile) + + eq(5, #result) + eq({method = 'textDocument/formatting', supported = false}, result[1]) + eq({method = 'textDocument/formatting', supported = true, fname = tmpfile}, result[2]) + eq({method = 'textDocument/rangeFormatting', supported = true}, result[3]) + eq({method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile}, result[4]) + eq({method = 'textDocument/completion', supported = false}, result[5]) + end) + end) + + describe('vim.lsp._watchfiles', function() + it('sends notifications when files change', function() + skip(is_os('bsd'), "bsd only reports rename on folders if file inside change") + local root_dir = helpers.tmpname() + os.remove(root_dir) + mkdir(root_dir) + + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'watchfiles-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + + local watchfunc = require('vim.lsp._watchfiles')._watchfunc + local msg_wait_timeout = watchfunc == vim._watch.poll and 2500 or 200 + local function wait_for_messages() + assert(vim.wait(msg_wait_timeout, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages)) + end + + wait_for_messages() + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-0', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/watch', + kind = 7, + }, + }, + }, + }, + }, + }, { client_id = client_id }) + + if watchfunc == vim._watch.poll then + vim.wait(100) + end + + local path = root_dir .. '/watch' + local file = io.open(path, 'w') + file:close() + + expected_messages = expected_messages + 1 + wait_for_messages() + + os.remove(path) + + expected_messages = expected_messages + 1 + wait_for_messages() + + return server.messages + ]], root_dir) + + local function watched_uri(fname) + return exec_lua([[ + local root_dir, fname = ... + return vim.uri_from_fname(root_dir .. '/' .. fname) + ]], root_dir, fname) + end + + eq(4, #result) + eq('workspace/didChangeWatchedFiles', result[3].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('watch'), + }, + }, + }, result[3].params) + eq('workspace/didChangeWatchedFiles', result[4].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]), + uri = watched_uri('watch'), + }, + }, + }, result[4].params) + end) + + it('correctly registers and unregisters', function() + local root_dir = '/some_dir' + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'watchfiles-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + local function wait_for_messages() + assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages)) + end + + wait_for_messages() + + local send_event + require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) + local stopped = false + send_event = function(...) + if not stopped then + callback(...) + end + end + return function() + stopped = true + end + end + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-0', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/*.watch0', + }, + }, + }, + }, + }, + }, { client_id = client_id }) + + send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created) + send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created) + + expected_messages = expected_messages + 1 + wait_for_messages() + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-1', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/*.watch1', + }, + }, + }, + }, + }, + }, { client_id = client_id }) + + vim.lsp.handlers['client/unregisterCapability'](nil, { + unregisterations = { + { + id = 'watchfiles-test-0', + method = 'workspace/didChangeWatchedFiles', + }, + }, + }, { client_id = client_id }) + + send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created) + send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created) + + expected_messages = expected_messages + 1 + wait_for_messages() + + return server.messages + ]], root_dir) + + local function watched_uri(fname) + return exec_lua([[ + local root_dir, fname = ... + return vim.uri_from_fname(root_dir .. '/' .. fname) + ]], root_dir, fname) + end + + eq(4, #result) + eq('workspace/didChangeWatchedFiles', result[3].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('file.watch0'), + }, + }, + }, result[3].params) + eq('workspace/didChangeWatchedFiles', result[4].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('file.watch1'), + }, + }, + }, result[4].params) + end) + + it('correctly handles the registered watch kind', function() + local root_dir = 'some_dir' + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'watchfiles-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + local function wait_for_messages() + assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages)) + end + + wait_for_messages() + + local watch_callbacks = {} + local function send_event(...) + for _, cb in ipairs(watch_callbacks) do + cb(...) + end + end + require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) + table.insert(watch_callbacks, callback) + return function() + -- noop because this test never stops the watch + end + end + + local protocol = require('vim.lsp.protocol') + + local watchers = {} + local max_kind = protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete + for i = 0, max_kind do + table.insert(watchers, { + globPattern = { + baseUri = vim.uri_from_fname('/dir'), + pattern = 'watch'..tostring(i), + }, + kind = i, + }) + end + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-kind', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = watchers, + }, + }, + }, + }, { client_id = client_id }) + + for i = 0, max_kind do + local filename = '/dir/watch' .. tostring(i) + send_event(filename, vim._watch.FileChangeType.Created) + send_event(filename, vim._watch.FileChangeType.Changed) + send_event(filename, vim._watch.FileChangeType.Deleted) + end + + expected_messages = expected_messages + 1 + wait_for_messages() + + return server.messages + ]], root_dir) + + local function watched_uri(fname) + return exec_lua([[ + local fname = ... + return vim.uri_from_fname('/dir/' .. fname) + ]], fname) + end + + eq(3, #result) + eq('workspace/didChangeWatchedFiles', result[3].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('watch1'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]), + uri = watched_uri('watch2'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('watch3'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]), + uri = watched_uri('watch3'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]), + uri = watched_uri('watch4'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('watch5'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]), + uri = watched_uri('watch5'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]), + uri = watched_uri('watch6'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]), + uri = watched_uri('watch6'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('watch7'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]), + uri = watched_uri('watch7'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]), + uri = watched_uri('watch7'), + }, + }, + }, result[3].params) + end) + + it('prunes duplicate events', function() + local root_dir = 'some_dir' + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'watchfiles-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + local function wait_for_messages() + assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages)) + end + + wait_for_messages() + + local send_event + require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) + send_event = callback + return function() + -- noop because this test never stops the watch + end + end + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-kind', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/*', + }, + }, + }, + }, + }, + }, { client_id = client_id }) + + send_event('file1', vim._watch.FileChangeType.Created) + send_event('file1', vim._watch.FileChangeType.Created) -- pruned + send_event('file1', vim._watch.FileChangeType.Changed) + send_event('file2', vim._watch.FileChangeType.Created) + send_event('file1', vim._watch.FileChangeType.Changed) -- pruned + + expected_messages = expected_messages + 1 + wait_for_messages() + + return server.messages + ]], root_dir) + + local function watched_uri(fname) + return exec_lua([[ + return vim.uri_from_fname(...) + ]], fname) + end + + eq(3, #result) + eq('workspace/didChangeWatchedFiles', result[3].method) + eq({ + changes = { + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('file1'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]), + uri = watched_uri('file1'), + }, + { + type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]), + uri = watched_uri('file2'), + }, + }, + }, result[3].params) + end) + + it("ignores registrations by servers when the client doesn't advertise support", function() + exec_lua(create_server_definition) + exec_lua([[ + server = _create_server() + require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) + -- Since the registration is ignored, this should not execute and `watching` should stay false + watching = true + return function() end + end + ]]) + + local function check_registered(capabilities) + return exec_lua([[ + watching = false + local client_id = vim.lsp.start({ + name = 'watchfiles-test', + cmd = server.cmd, + root_dir = 'some_dir', + capabilities = ..., + }, { + reuse_client = function() return false end, + }) + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'watchfiles-test-kind', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/*', + }, + }, + }, + }, + }, + }, { client_id = client_id }) + + -- Ensure no errors occur when unregistering something that was never really registered. + vim.lsp.handlers['client/unregisterCapability'](nil, { + unregisterations = { + { + id = 'watchfiles-test-kind', + method = 'workspace/didChangeWatchedFiles', + }, + }, + }, { client_id = client_id }) + + vim.lsp.stop_client(client_id, true) + return watching + ]], capabilities) + end + + eq(true, check_registered(nil)) -- start{_client}() defaults to make_client_capabilities(). + eq(false, check_registered(vim.empty_dict())) + eq(false, check_registered({ + workspace = { + ignoreMe = true, + }, + })) + eq(false, check_registered({ + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = false, + }, + }, + })) + eq(true, check_registered({ + workspace = { + didChangeWatchedFiles = { + dynamicRegistration = true, + }, + }, + })) + end) + end) end) + diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua index 58da059be6..815ddbc523 100644 --- a/test/functional/plugin/man_spec.lua +++ b/test/functional/plugin/man_spec.lua @@ -8,9 +8,30 @@ local nvim_prog = helpers.nvim_prog local matches = helpers.matches local write_file = helpers.write_file local tmpname = helpers.tmpname +local eq = helpers.eq +local pesc = helpers.pesc local skip = helpers.skip local is_ci = helpers.is_ci +-- Collects all names passed to find_path() after attempting ":Man foo". +local function get_search_history(name) + local args = vim.split(name, ' ') + local code = [[ + local args = ... + local man = require('runtime.lua.man') + local res = {} + man.find_path = function(sect, name) + table.insert(res, {sect, name}) + return nil + end + local ok, rv = pcall(man.open_page, -1, {tab = 0}, args) + assert(not ok) + assert(rv and rv:match('no manual entry')) + return res + ]] + return exec_lua(code, args) +end + clear() if funcs.executable('man') == 0 then pending('missing "man" command', function() end) @@ -169,8 +190,39 @@ describe(':Man', function() 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), + 'man.lua: "no manual entry for %s"'):format(pesc(actual_file)), funcs.system(args, {''})) os.remove(actual_file) end) + + it('tries variants with spaces, underscores #22503', function() + eq({ + {'', 'NAME WITH SPACES'}, + {'', 'NAME_WITH_SPACES'}, + }, get_search_history('NAME WITH SPACES')) + eq({ + {'3', 'some other man'}, + {'3', 'some_other_man'}, + }, get_search_history('3 some other man')) + eq({ + {'3x', 'some other man'}, + {'3x', 'some_other_man'}, + }, get_search_history('3X some other man')) + eq({ + {'3tcl', 'some other man'}, + {'3tcl', 'some_other_man'}, + }, get_search_history('3tcl some other man')) + eq({ + {'n', 'some other man'}, + {'n', 'some_other_man'}, + }, get_search_history('n some other man')) + eq({ + {'', '123some other man'}, + {'', '123some_other_man'}, + }, get_search_history('123some other man')) + eq({ + {'1', 'other_man'}, + {'1', 'other_man'}, + }, get_search_history('other_man(1)')) + end) end) diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua index 93cf6d2b77..8d37100607 100644 --- a/test/functional/plugin/shada_spec.lua +++ b/test/functional/plugin/shada_spec.lua @@ -2150,6 +2150,7 @@ describe('plugin/shada.vim', function() teardown(function() os.remove(fname) + os.remove(fname .. '.tst') os.remove(fname_tmp) end) @@ -2180,8 +2181,8 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "a"', }, nvim_eval('getline(1, "$")')) - eq(false, curbuf('get_option', 'modified')) - eq('shada', curbuf('get_option', 'filetype')) + eq(false, nvim('get_option_value', 'modified', {})) + eq('shada', nvim('get_option_value', 'filetype', {})) nvim_command('edit ' .. fname_tmp) eq({ 'History entry with timestamp ' .. epoch .. ':', @@ -2190,8 +2191,8 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "b"', }, nvim_eval('getline(1, "$")')) - eq(false, curbuf('get_option', 'modified')) - eq('shada', curbuf('get_option', 'filetype')) + eq(false, nvim('get_option_value', 'modified', {})) + eq('shada', nvim('get_option_value', 'filetype', {})) eq('++opt not supported', exc_exec('edit ++enc=latin1 ' .. fname)) neq({ 'History entry with timestamp ' .. epoch .. ':', @@ -2200,7 +2201,7 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "a"', }, nvim_eval('getline(1, "$")')) - neq(true, curbuf('get_option', 'modified')) + neq(true, nvim('get_option_value', 'modified', {})) end) it('event FileReadCmd', function() @@ -2216,8 +2217,8 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "a"', }, nvim_eval('getline(1, "$")')) - eq(true, curbuf('get_option', 'modified')) - neq('shada', curbuf('get_option', 'filetype')) + eq(true, nvim('get_option_value', 'modified', {})) + neq('shada', nvim('get_option_value', 'filetype', {})) nvim_command('1,$read ' .. fname_tmp) eq({ '', @@ -2232,9 +2233,9 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "b"', }, nvim_eval('getline(1, "$")')) - eq(true, curbuf('get_option', 'modified')) - neq('shada', curbuf('get_option', 'filetype')) - curbuf('set_option', 'modified', false) + eq(true, nvim('get_option_value', 'modified', {})) + neq('shada', nvim('get_option_value', 'filetype', {})) + nvim('set_option_value', 'modified', false, {}) eq('++opt not supported', exc_exec('$read ++enc=latin1 ' .. fname)) eq({ '', @@ -2249,7 +2250,7 @@ describe('plugin/shada.vim', function() ' - contents "ab"', ' - "b"', }, nvim_eval('getline(1, "$")')) - neq(true, curbuf('get_option', 'modified')) + neq(true, nvim('get_option_value', 'modified', {})) end) it('event BufWriteCmd', function() @@ -2419,6 +2420,9 @@ describe('plugin/shada.vim', function() it('event SourceCmd', function() reset(fname) + finally(function() + nvim_command('set shadafile=NONE') -- Avoid writing shada file on exit + end) wshada('\004\000\006\146\000\196\002ab') wshada_tmp('\004\001\006\146\000\196\002bc') eq(0, exc_exec('source ' .. fname)) @@ -2513,10 +2517,10 @@ describe('ftplugin/shada.vim', function() it('sets options correctly', function() nvim_command('filetype plugin indent on') nvim_command('setlocal filetype=shada') - eq(true, curbuf('get_option', 'expandtab')) - eq(2, curbuf('get_option', 'tabstop')) - eq(2, curbuf('get_option', 'softtabstop')) - eq(2, curbuf('get_option', 'shiftwidth')) + eq(true, nvim('get_option_value', 'expandtab', {})) + eq(2, nvim('get_option_value', 'tabstop', {})) + eq(2, nvim('get_option_value', 'softtabstop', {})) + eq(2, nvim('get_option_value', 'shiftwidth', {})) end) it('sets indentkeys correctly', function() diff --git a/test/functional/plugin/tutor_spec.lua b/test/functional/plugin/tutor_spec.lua new file mode 100644 index 0000000000..bd214e9c03 --- /dev/null +++ b/test/functional/plugin/tutor_spec.lua @@ -0,0 +1,95 @@ +local Screen = require('test.functional.ui.screen') +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local feed = helpers.feed +local is_os = helpers.is_os + +describe(':Tutor', function() + local screen + + before_each(function() + clear({ args = { '--clean' } }) + command('set cmdheight=0') + command('Tutor') + screen = Screen.new(80, 30) + screen:set_default_attr_ids({ + [0] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.Gray }, + [1] = { bold = true }, + [2] = { underline = true, foreground = tonumber('0x0088ff') }, + [3] = { foreground = Screen.colors.SlateBlue }, + [4] = { bold = true, foreground = Screen.colors.Brown }, + [5] = { bold = true, foreground = Screen.colors.Magenta1 }, + }) + screen:attach() + end) + + it('applies {unix:…,win:…} transform', function() + local expected = is_os('win') and [[ + {0: }^ | + {0: } 3. To verify that a file was retrieved, cursor back and notice that there | + {0: } are now two copies of Lesson 5.3, the original and the retrieved version. | + {0: } | + {0: }{1:NOTE}: You can also read the output of an external command. For example, | + {0: } | + {0: } :r {4:!}dir | + {0: } | + {0: } reads the output of the ls command and puts it below the cursor. | + {0: } | + {0: }{3:#}{5: Lesson 5 SUMMARY} | + {0: } | + {0: } 1. {2::!command} executes an external command. | + {0: } | + {0: } Some useful examples are: | + {0: } :{4:!}dir - shows a directory listing | + {0: } :{4:!}del FILENAME - removes file FILENAME | + {0: } | + {0: } 2. {2::w} FILENAME writes the current Neovim file to disk with | + {0: } name FILENAME. | + {0: } | + {0: } 3. {2:v} motion :w FILENAME saves the Visually selected lines in file | + {0: } FILENAME. | + {0: } | + {0: } 4. {2::r} FILENAME retrieves disk file FILENAME and puts it | + {0: } below the cursor position. | + {0: } | + {0: } 5. {2::r !dir} reads the output of the dir command and | + {0: } puts it below the cursor position. | + {0: } | + ]] or [[ + {0: }^ | + {0: } 3. To verify that a file was retrieved, cursor back and notice that there | + {0: } are now two copies of Lesson 5.3, the original and the retrieved version. | + {0: } | + {0: }{1:NOTE}: You can also read the output of an external command. For example, | + {0: } | + {0: } :r {4:!}ls | + {0: } | + {0: } reads the output of the ls command and puts it below the cursor. | + {0: } | + {0: }{3:#}{5: Lesson 5 SUMMARY} | + {0: } | + {0: } 1. {2::!command} executes an external command. | + {0: } | + {0: } Some useful examples are: | + {0: } :{4:!}ls - shows a directory listing | + {0: } :{4:!}rm FILENAME - removes file FILENAME | + {0: } | + {0: } 2. {2::w} FILENAME writes the current Neovim file to disk with | + {0: } name FILENAME. | + {0: } | + {0: } 3. {2:v} motion :w FILENAME saves the Visually selected lines in file | + {0: } FILENAME. | + {0: } | + {0: } 4. {2::r} FILENAME retrieves disk file FILENAME and puts it | + {0: } below the cursor position. | + {0: } | + {0: } 5. {2::r !ls} reads the output of the ls command and | + {0: } puts it below the cursor position. | + {0: } | + ]] + + feed(':700<CR>zt') + screen:expect(expected) + end) +end) |