aboutsummaryrefslogtreecommitdiff
path: root/test/functional/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/plugin')
-rw-r--r--test/functional/plugin/editorconfig_spec.lua19
-rw-r--r--test/functional/plugin/health_spec.lua131
-rw-r--r--test/functional/plugin/lsp/completion_spec.lua239
-rw-r--r--test/functional/plugin/lsp/diagnostic_spec.lua159
-rw-r--r--test/functional/plugin/lsp/helpers.lua4
-rw-r--r--test/functional/plugin/lsp/incremental_sync_spec.lua2
-rw-r--r--test/functional/plugin/lsp/inlay_hint_spec.lua204
-rw-r--r--test/functional/plugin/lsp/semantic_tokens_spec.lua450
-rw-r--r--test/functional/plugin/lsp/snippet_spec.lua261
-rw-r--r--test/functional/plugin/lsp/utils_spec.lua226
-rw-r--r--test/functional/plugin/lsp/watchfiles_spec.lua222
-rw-r--r--test/functional/plugin/lsp_spec.lua1198
-rw-r--r--test/functional/plugin/man_spec.lua54
-rw-r--r--test/functional/plugin/shada_spec.lua34
-rw-r--r--test/functional/plugin/tutor_spec.lua95
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 &lt; 2",
+ "3 &gt; 2",
+ "&quot;quoted&quot;",
+ "&apos;apos&apos;",
+ "&ensp; &emsp;",
+ "&amp;",
+ }
+ 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)