diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
commit | 1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch) | |
tree | cd08258054db80bb9a11b1061bb091c70b76926a /test/functional/api | |
parent | eaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff) | |
parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
download | rneovim-aucmd_textputpost.tar.gz rneovim-aucmd_textputpost.tar.bz2 rneovim-aucmd_textputpost.zip |
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
Diffstat (limited to 'test/functional/api')
-rw-r--r-- | test/functional/api/autocmd_spec.lua | 191 | ||||
-rw-r--r-- | test/functional/api/buffer_spec.lua | 1276 | ||||
-rw-r--r-- | test/functional/api/buffer_updates_spec.lua | 4 | ||||
-rw-r--r-- | test/functional/api/command_spec.lua | 16 | ||||
-rw-r--r-- | test/functional/api/extmark_spec.lua | 234 | ||||
-rw-r--r-- | test/functional/api/highlight_spec.lua | 369 | ||||
-rw-r--r-- | test/functional/api/keymap_spec.lua | 214 | ||||
-rw-r--r-- | test/functional/api/proc_spec.lua | 12 | ||||
-rw-r--r-- | test/functional/api/rpc_fixture.lua | 7 | ||||
-rw-r--r-- | test/functional/api/server_notifications_spec.lua | 3 | ||||
-rw-r--r-- | test/functional/api/server_requests_spec.lua | 19 | ||||
-rw-r--r-- | test/functional/api/ui_spec.lua | 84 | ||||
-rw-r--r-- | test/functional/api/version_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 876 | ||||
-rw-r--r-- | test/functional/api/window_spec.lua | 354 |
15 files changed, 3196 insertions, 465 deletions
diff --git a/test/functional/api/autocmd_spec.lua b/test/functional/api/autocmd_spec.lua index 22a1311ee9..fd46a1dcfa 100644 --- a/test/functional/api/autocmd_spec.lua +++ b/test/functional/api/autocmd_spec.lua @@ -14,14 +14,40 @@ before_each(clear) describe('autocmd api', function() describe('nvim_create_autocmd', function() - it('"command" and "callback" are mutually exclusive', function() - local rv = pcall_err(meths.create_autocmd, "BufReadPost", { - pattern = "*.py,*.pyi", + it('validation', function() + eq("Cannot use both 'callback' and 'command'", pcall_err(meths.create_autocmd, 'BufReadPost', { + pattern = '*.py,*.pyi', command = "echo 'Should Have Errored", - callback = "NotAllowed", - }) - - eq("specify either 'callback' or 'command', not both", rv) + callback = 'NotAllowed', + })) + eq("Cannot use both 'pattern' and 'buffer' for the same autocmd", pcall_err(meths.create_autocmd, 'FileType', { + command = 'let g:called = g:called + 1', + buffer = 0, + pattern = '*.py', + })) + eq("Required: 'event'", pcall_err(meths.create_autocmd, {}, { + command = 'ls', + })) + eq("Required: 'command' or 'callback'", pcall_err(meths.create_autocmd, 'FileType', { + })) + eq("Invalid 'desc': expected String, got Integer", pcall_err(meths.create_autocmd, 'FileType', { + command = 'ls', + desc = 42, + })) + eq("Invalid 'callback': expected Lua function or Vim function name, got Integer", pcall_err(meths.create_autocmd, 'FileType', { + callback = 0, + })) + eq("Invalid 'event' item: expected String, got Array", pcall_err(meths.create_autocmd, + {'FileType', {}}, {})) + eq("Invalid 'group': 0", pcall_err(meths.create_autocmd, 'FileType', { + group = 0, + command = 'ls', + })) + + eq("Invalid 'event': 'foo'", pcall_err(meths.create_autocmd, 'foo', { command = '' })) + eq("Invalid 'event': 'VimEnter '", pcall_err(meths.create_autocmd, 'VimEnter ', { command = '' })) + eq("Invalid 'event': 'VimEnter foo'", pcall_err(meths.create_autocmd, 'VimEnter foo', { command = '' })) + eq("Invalid 'event': 'BufAdd,BufDelete'", pcall_err(meths.create_autocmd, 'BufAdd,BufDelete', { command = '' })) end) it('doesnt leak when you use ++once', function() @@ -59,18 +85,8 @@ describe('autocmd api', function() eq(1, meths.get_var('called')) end) - it('does not allow passing buffer and patterns', function() - local rv = pcall_err(meths.create_autocmd, "Filetype", { - command = "let g:called = g:called + 1", - buffer = 0, - pattern = "*.py", - }) - - eq("cannot pass both: 'pattern' and 'buffer' for the same autocmd", rv) - end) - it('does not allow passing invalid buffers', function() - local ok, msg = pcall(meths.create_autocmd, "Filetype", { + local ok, msg = pcall(meths.create_autocmd, 'FileType', { command = "let g:called = g:called + 1", buffer = -1, }) @@ -209,7 +225,7 @@ describe('autocmd api', function() local aus = meths.get_autocmds({ event = 'User', pattern = 'Test' }) local first = aus[1] - eq(first.id, 1) + eq(true, first.id > 0) meths.set_var("some_condition", true) meths.exec_autocmds("User", {pattern = "Test"}) @@ -217,23 +233,28 @@ describe('autocmd api', function() end) it('receives an args table', function() - local res = exec_lua [[ - local group_id = vim.api.nvim_create_augroup("TestGroup", {}) - local autocmd_id = vim.api.nvim_create_autocmd("User", { + local group_id = meths.create_augroup("TestGroup", {}) + -- Having an existing autocmd calling expand("<afile>") shouldn't change args #18964 + meths.create_autocmd('User', { + group = 'TestGroup', + pattern = 'Te*', + command = 'call expand("<afile>")', + }) + + local autocmd_id = exec_lua [[ + return vim.api.nvim_create_autocmd("User", { group = "TestGroup", pattern = "Te*", callback = function(args) vim.g.autocmd_args = args end, }) - - return {group_id, autocmd_id} ]] meths.exec_autocmds("User", {pattern = "Test pattern"}) eq({ - id = res[2], - group = res[1], + id = autocmd_id, + group = group_id, event = "User", match = "Test pattern", file = "Test pattern", @@ -241,27 +262,24 @@ describe('autocmd api', function() }, meths.get_var("autocmd_args")) -- Test without a group - res = exec_lua [[ - local autocmd_id = vim.api.nvim_create_autocmd("User", { + autocmd_id = exec_lua [[ + return vim.api.nvim_create_autocmd("User", { pattern = "*", callback = function(args) vim.g.autocmd_args = args end, }) - - return {autocmd_id} ]] meths.exec_autocmds("User", {pattern = "some_pat"}) eq({ - id = res[1], + id = autocmd_id, group = nil, event = "User", match = "some_pat", file = "some_pat", buf = 1, }, meths.get_var("autocmd_args")) - end) it('can receive arbitrary data', function() @@ -294,8 +312,32 @@ describe('autocmd api', function() end) describe('nvim_get_autocmds', function() + it('validation', function() + eq("Invalid 'group': 9997999", pcall_err(meths.get_autocmds, { + group = 9997999, + })) + eq("Invalid 'group': 'bogus'", pcall_err(meths.get_autocmds, { + group = 'bogus', + })) + eq("Invalid 'group': 0", pcall_err(meths.get_autocmds, { + group = 0, + })) + eq("Invalid 'group': expected String or Integer, got Array", pcall_err(meths.get_autocmds, { + group = {}, + })) + eq("Invalid 'buffer': expected Integer or Array, got Boolean", pcall_err(meths.get_autocmds, { + buffer = true, + })) + eq("Invalid 'event': expected String or Array", pcall_err(meths.get_autocmds, { + event = true, + })) + eq("Invalid 'pattern': expected String or Array, got Boolean", pcall_err(meths.get_autocmds, { + pattern = true, + })) + end) + describe('events', function() - it('should return one autocmd when there is only one for an event', function() + it('returns one autocmd when there is only one for an event', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] @@ -303,7 +345,7 @@ describe('autocmd api', function() eq(1, #aus) end) - it('should return two autocmds when there are two for an event', function() + it('returns two autocmds when there are two for an event', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] command [[au InsertEnter * :echo "2"]] @@ -312,7 +354,7 @@ describe('autocmd api', function() eq(2, #aus) end) - it('should return the same thing if you use string or list', function() + it('returns the same thing if you use string or list', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] command [[au InsertEnter * :echo "2"]] @@ -322,7 +364,7 @@ describe('autocmd api', function() eq(string_aus, array_aus) end) - it('should return two autocmds when there are two for an event', function() + it('returns two autocmds when there are two for an event', function() command [[au! InsertEnter]] command [[au! InsertLeave]] command [[au InsertEnter * :echo "1"]] @@ -332,7 +374,7 @@ describe('autocmd api', function() eq(2, #aus) end) - it('should return different IDs for different autocmds', function() + it('returns different IDs for different autocmds', function() command [[au! InsertEnter]] command [[au! InsertLeave]] command [[au InsertEnter * :echo "1"]] @@ -356,7 +398,7 @@ describe('autocmd api', function() eq(first, new_aus[1]) end) - it('should return event name', function() + it('returns event name', function() command [[au! InsertEnter]] command [[au InsertEnter * :echo "1"]] @@ -364,7 +406,7 @@ describe('autocmd api', function() eq({ { buflocal = false, command = ':echo "1"', event = "InsertEnter", once = false, pattern = "*" } }, aus) end) - it('should work with buffer numbers', function() + it('works with buffer numbers', function() command [[new]] command [[au! InsertEnter]] command [[au InsertEnter <buffer=1> :echo "1"]] @@ -407,8 +449,8 @@ describe('autocmd api', function() pattern = "<buffer=2>", }}, aus) - eq("Invalid value for 'buffer': must be an integer or array of integers", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = "foo" })) - eq("Invalid value for 'buffer': must be an integer", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = { "foo", 42 } })) + eq("Invalid 'buffer': expected Integer or Array, got String", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = "foo" })) + eq("Invalid 'buffer': expected Integer, got String", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = { "foo", 42 } })) eq("Invalid buffer id: 42", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = { 42 } })) local bufs = {} @@ -416,10 +458,10 @@ describe('autocmd api', function() table.insert(bufs, meths.create_buf(true, false)) end - eq("Too many buffers. Please limit yourself to 256 or fewer", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = bufs })) + eq("Too many buffers (maximum of 256)", pcall_err(meths.get_autocmds, { event = "InsertEnter", buffer = bufs })) end) - it('should return autocmds when group is specified by id', function() + it('returns autocmds when group is specified by id', function() local auid = meths.create_augroup("nvim_test_augroup", { clear = true }) meths.create_autocmd("FileType", { group = auid, command = 'echo "1"' }) meths.create_autocmd("FileType", { group = auid, command = 'echo "2"' }) @@ -431,7 +473,7 @@ describe('autocmd api', function() eq(0, #aus2) end) - it('should return autocmds when group is specified by name', function() + it('returns autocmds when group is specified by name', function() local auname = "nvim_test_augroup" meths.create_augroup(auname, { clear = true }) meths.create_autocmd("FileType", { group = auname, command = 'echo "1"' }) @@ -531,7 +573,7 @@ describe('autocmd api', function() command [[augroup END]] end) - it('should return all groups if no group is specified', function() + it('returns all groups if no group is specified', function() local aus = meths.get_autocmds { event = "InsertEnter" } if #aus ~= 4 then eq({}, aus) @@ -540,7 +582,7 @@ describe('autocmd api', function() eq(4, #aus) end) - it('should return only the group specified', function() + it('returns only the group specified', function() local aus = meths.get_autocmds { event = "InsertEnter", group = "GroupOne", @@ -551,7 +593,7 @@ describe('autocmd api', function() eq("GroupOne", aus[1].group_name) end) - it('should return only the group specified, multiple values', function() + it('returns only the group specified, multiple values', function() local aus = meths.get_autocmds { event = "InsertEnter", group = "GroupTwo", @@ -578,7 +620,7 @@ describe('autocmd api', function() ]], {})) eq(false, success) - matches('invalid augroup: NotDefined', code) + matches("Invalid 'group': 'NotDefined'", code) end) it('raises error for undefined augroup id', function() @@ -596,7 +638,7 @@ describe('autocmd api', function() ]], {})) eq(false, success) - matches('invalid augroup: 1', code) + matches("Invalid 'group': 1", code) end) it('raises error for invalid group type', function() @@ -611,7 +653,7 @@ describe('autocmd api', function() ]], {})) eq(false, success) - matches("'group' must be a string or an integer", code) + matches("Invalid 'group': expected String or Integer, got Boolean", code) end) it('raises error for invalid pattern array', function() @@ -625,7 +667,7 @@ describe('autocmd api', function() ]], {})) eq(false, success) - matches("All entries in 'pattern' must be strings", code) + matches("Invalid 'pattern' item: expected String, got Array", code) end) end) @@ -640,7 +682,7 @@ describe('autocmd api', function() command [[au InsertEnter <buffer> :echo "Buffer"]] end) - it('should should return for literal match', function() + it('returns for literal match', function() local aus = meths.get_autocmds { event = "InsertEnter", pattern = "*" @@ -650,7 +692,7 @@ describe('autocmd api', function() eq([[:echo "No Group"]], aus[1].command) end) - it('should return for multiple matches', function() + it('returns for multiple matches', function() -- vim.api.nvim_get_autocmds local aus = meths.get_autocmds { event = "InsertEnter", @@ -687,6 +729,26 @@ describe('autocmd api', function() end) describe('nvim_exec_autocmds', function() + it('validation', function() + eq("Invalid 'group': 9997999", pcall_err(meths.exec_autocmds, 'FileType', { + group = 9997999, + })) + eq("Invalid 'group': 'bogus'", pcall_err(meths.exec_autocmds, 'FileType', { + group = 'bogus', + })) + eq("Invalid 'group': expected String or Integer, got Array", pcall_err(meths.exec_autocmds, 'FileType', { + group = {}, + })) + eq("Invalid 'group': 0", pcall_err(meths.exec_autocmds, 'FileType', { + group = 0, + })) + eq("Invalid 'buffer': expected Buffer, got Array", pcall_err(meths.exec_autocmds, 'FileType', { + buffer = {}, + })) + eq("Invalid 'event' item: expected String, got Array", pcall_err(meths.exec_autocmds, + {'FileType', {}}, {})) + end) + it("can trigger builtin autocmds", function() meths.set_var("autocmd_executed", false) @@ -1004,6 +1066,12 @@ describe('autocmd api', function() eq(false, exec_lua[[return pcall(vim.api.nvim_del_augroup_by_id, -12342)]]) eq('Vim:E367: No such group: "--Deleted--"', pcall_err(meths.del_augroup_by_id, -12312)) + + eq(false, exec_lua[[return pcall(vim.api.nvim_del_augroup_by_id, 0)]]) + eq('Vim:E367: No such group: "[NULL]"', pcall_err(meths.del_augroup_by_id, 0)) + + eq(false, exec_lua[[return pcall(vim.api.nvim_del_augroup_by_id, 12342)]]) + eq('Vim:E367: No such group: "[NULL]"', pcall_err(meths.del_augroup_by_id, 12312)) end) it('groups work with once', function() @@ -1036,7 +1104,7 @@ describe('autocmd api', function() local augroup = "WillBeDeleted" meths.create_augroup(augroup, { clear = true }) - meths.create_autocmd({"Filetype"}, { + meths.create_autocmd({"FileType"}, { pattern = "*", command = "echo 'does not matter'", }) @@ -1055,7 +1123,7 @@ describe('autocmd api', function() meths.set_var("value_set", false) meths.create_augroup(augroup, { clear = true }) - meths.create_autocmd("Filetype", { + meths.create_autocmd("FileType", { pattern = "<buffer>", command = "let g:value_set = v:true", }) @@ -1171,6 +1239,17 @@ describe('autocmd api', function() end) describe('nvim_clear_autocmds', function() + it('validation', function() + eq("Cannot use both 'pattern' and 'buffer'", pcall_err(meths.clear_autocmds, { + pattern = '*', + buffer = 42, + })) + eq("Invalid 'event' item: expected String, got Array", pcall_err(meths.clear_autocmds, { + event = {'FileType', {}} + })) + eq("Invalid 'group': 0", pcall_err(meths.clear_autocmds, {group = 0})) + end) + it('should clear based on event + pattern', function() command('autocmd InsertEnter *.py :echo "Python can be cool sometimes"') command('autocmd InsertEnter *.txt :echo "Text Files Are Cool"') @@ -1254,7 +1333,7 @@ describe('autocmd api', function() local without_group = meths.get_autocmds(search) eq(2, #without_group) - -- Doest clear with passing group. + -- Doesn't clear with passing group. meths.clear_autocmds { buffer = 0, group = search.group } local with_group = meths.get_autocmds(search) eq(1, #with_group) diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua index 6b13729994..6ed9aa574a 100644 --- a/test/functional/api/buffer_spec.lua +++ b/test/functional/api/buffer_spec.lua @@ -16,6 +16,7 @@ local command = helpers.command local bufmeths = helpers.bufmeths local feed = helpers.feed local pcall_err = helpers.pcall_err +local assert_alive = helpers.assert_alive describe('api/buf', function() before_each(clear) @@ -41,6 +42,14 @@ describe('api/buf', function() eq(1, curbuf_depr('line_count')) end) + it("doesn't crash just after set undolevels=1 #24894", function() + local buf = meths.create_buf(false, true) + meths.buf_set_option(buf, 'undolevels', -1) + meths.buf_set_lines(buf, 0, 1, false, { }) + + assert_alive() + end) + it('cursor position is maintained after lines are inserted #9961', function() -- replace the buffer contents with these three lines. request('nvim_buf_set_lines', 0, 0, -1, 1, {"line1", "line2", "line3", "line4"}) @@ -76,6 +85,38 @@ describe('api/buf', function() eq({4, 2}, curwin('get_cursor')) end) + it('cursor position is maintained in non-current window', function() + meths.buf_set_lines(0, 0, -1, 1, {"line1", "line2", "line3", "line4"}) + meths.win_set_cursor(0, {3, 2}) + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('new') + + meths.buf_set_lines(buf, 1, 2, 1, {"line5", "line6"}) + eq({"line1", "line5", "line6", "line3", "line4"}, meths.buf_get_lines(buf, 0, -1, true)) + eq({4, 2}, meths.win_get_cursor(win)) + end) + + it('cursor position is maintained in TWO non-current windows', function() + meths.buf_set_lines(0, 0, -1, 1, {"line1", "line2", "line3", "line4"}) + meths.win_set_cursor(0, {3, 2}) + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('split') + meths.win_set_cursor(0, {4, 2}) + local win2 = meths.get_current_win() + + -- set current window to third one with another buffer + command("new") + + meths.buf_set_lines(buf, 1, 2, 1, {"line5", "line6"}) + eq({"line1", "line5", "line6", "line3", "line4"}, meths.buf_get_lines(buf, 0, -1, true)) + eq({4, 2}, meths.win_get_cursor(win)) + eq({5, 2}, meths.win_get_cursor(win2)) + end) + it('line_count has defined behaviour for unloaded buffers', function() -- we'll need to know our bufnr for when it gets unloaded local bufnr = curbuf('get_number') @@ -105,6 +146,285 @@ describe('api/buf', function() -- it's impossible to get out-of-bounds errors for an unloaded buffer eq({}, buffer('get_lines', bufnr, 8888, 9999, 1)) end) + + describe('handles topline', function() + local screen + before_each(function() + screen = Screen.new(20, 12) + screen:set_default_attr_ids { + [1] = {bold = true, foreground = Screen.colors.Blue1}; + [2] = {reverse = true, bold = true}; + [3] = {reverse = true}; + } + screen:attach() + meths.buf_set_lines(0, 0, -1, 1, {"aaa", "bbb", "ccc", "ddd", "www", "xxx", "yyy", "zzz"}) + meths.set_option_value('modified', false, {}) + end) + + it('of current window', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('new | wincmd w') + meths.win_set_cursor(win, {8,0}) + + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + www | + xxx | + yyy | + ^zzz | + {2:[No Name] }| + | + ]]} + + meths.buf_set_lines(buf, 0, 2, true, {"aaabbb"}) + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + www | + xxx | + yyy | + ^zzz | + {2:[No Name] [+] }| + | + ]]} + + -- replacing topline keeps it the topline + meths.buf_set_lines(buf, 3, 4, true, {"wwweeee"}) + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + wwweeee | + xxx | + yyy | + ^zzz | + {2:[No Name] [+] }| + | + ]]} + + -- inserting just before topline does not scroll up if cursor would be moved + meths.buf_set_lines(buf, 3, 3, true, {"mmm"}) + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + wwweeee | + xxx | + yyy | + ^zzz | + {2:[No Name] [+] }| + | + ]], unchanged=true} + + meths.win_set_cursor(0, {7, 0}) + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + wwweeee | + xxx | + ^yyy | + zzz | + {2:[No Name] [+] }| + | + ]]} + + meths.buf_set_lines(buf, 4, 4, true, {"mmmeeeee"}) + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + mmmeeeee | + wwweeee | + xxx | + ^yyy | + {2:[No Name] [+] }| + | + ]]} + end) + + it('of non-current window', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('new') + meths.win_set_cursor(win, {8,0}) + + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] }| + | + ]]} + + meths.buf_set_lines(buf, 0, 2, true, {"aaabbb"}) + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + + -- replacing topline keeps it the topline + meths.buf_set_lines(buf, 3, 4, true, {"wwweeee"}) + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + wwweeee | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + + -- inserting just before topline scrolls up + meths.buf_set_lines(buf, 3, 3, true, {"mmm"}) + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + mmm | + wwweeee | + xxx | + yyy | + {3:[No Name] [+] }| + | + ]]} + end) + + it('of split windows with same buffer', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('split') + meths.win_set_cursor(win, {8,0}) + meths.win_set_cursor(0, {1,0}) + + screen:expect{grid=[[ + ^aaa | + bbb | + ccc | + ddd | + www | + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] }| + | + ]]} + meths.buf_set_lines(buf, 0, 2, true, {"aaabbb"}) + + screen:expect{grid=[[ + ^aaabbb | + ccc | + ddd | + www | + xxx | + {2:[No Name] [+] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + + -- replacing topline keeps it the topline + meths.buf_set_lines(buf, 3, 4, true, {"wwweeee"}) + screen:expect{grid=[[ + ^aaabbb | + ccc | + ddd | + wwweeee | + xxx | + {2:[No Name] [+] }| + wwweeee | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + + -- inserting just before topline scrolls up + meths.buf_set_lines(buf, 3, 3, true, {"mmm"}) + screen:expect{grid=[[ + ^aaabbb | + ccc | + ddd | + mmm | + wwweeee | + {2:[No Name] [+] }| + mmm | + wwweeee | + xxx | + yyy | + {3:[No Name] [+] }| + | + ]]} + end) + end) + + it('handles clearing out non-current buffer #24911', function() + local buf = meths.get_current_buf() + meths.buf_set_lines(buf, 0, -1, true, {"aaa", "bbb", "ccc"}) + command("new") + + meths.buf_set_lines(0, 0, -1, true, {"xxx", "yyy", "zzz"}) + + meths.buf_set_lines(buf, 0, -1, true, {}) + eq({"xxx", "yyy", "zzz"}, meths.buf_get_lines(0, 0, -1, true)) + eq({''}, meths.buf_get_lines(buf, 0, -1, true)) + end) end) describe('deprecated: {get,set,del}_line', function() @@ -193,7 +513,7 @@ describe('api/buf', function() it('fails correctly when input is not valid', function() eq(1, api.curbufmeths.get_number()) - eq([[String cannot contain newlines]], + eq([['replacement string' item contains newlines]], pcall_err(bufmeths.set_lines, 1, 1, 2, false, {'b\na'})) end) @@ -393,6 +713,21 @@ describe('api/buf', function() feed('<c-w>p') eq(3, funcs.winnr()) end) + + it('set_lines on unloaded buffer #8659 #22670', function() + local bufnr = curbuf('get_number') + meths.buf_set_lines(bufnr, 0, -1, false, {'a', 'b', 'c'}) + meths.buf_set_name(bufnr, 'set_lines') + finally(function() + os.remove('set_lines') + end) + command('write!') + command('new') + command('bunload! '..bufnr) + local new_bufnr = funcs.bufnr('set_lines', true) + meths.buf_set_lines(new_bufnr, 0, -1, false, {}) + eq({''}, meths.buf_get_lines(new_bufnr, 0, -1, false)) + end) end) describe('nvim_buf_set_text', function() @@ -437,6 +772,10 @@ describe('api/buf', function() -- can append to a line set_text(1, 4, -1, 4, {' and', 'more'}) eq({'goodbye bar', 'text and', 'more'}, get_lines(0, 3, true)) + + -- can use negative column numbers + set_text(0, -5, 0, -1, {'!'}) + eq({'goodbye!'}, get_lines(0, 1, true)) end) it('works with undo', function() @@ -480,6 +819,754 @@ describe('api/buf', function() eq({1, 9}, curwin('get_cursor')) end) + it('updates the cursor position in non-current window', function() + insert([[ + hello world!]]) + + -- position the cursor on `!` + meths.win_set_cursor(0, {1, 11}) + + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command("new") + + -- replace 'world' with 'foo' + meths.buf_set_text(buf, 0, 6, 0, 11, {'foo'}) + eq({'hello foo!'}, meths.buf_get_lines(buf, 0, -1, true)) + -- cursor should be moved left by two columns (replacement is shorter by 2 chars) + eq({1, 9}, meths.win_get_cursor(win)) + end) + + it('updates the cursor position in TWO non-current windows', function() + insert([[ + hello world!]]) + + -- position the cursor on `!` + meths.win_set_cursor(0, {1, 11}) + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command("split") + local win2 = meths.get_current_win() + -- position the cursor on `w` + meths.win_set_cursor(0, {1, 6}) + + command("new") + + -- replace 'hello' with 'foo' + meths.buf_set_text(buf, 0, 0, 0, 5, {'foo'}) + eq({'foo world!'}, meths.buf_get_lines(buf, 0, -1, true)) + + -- both cursors should be moved left by two columns (replacement is shorter by 2 chars) + eq({1, 9}, meths.win_get_cursor(win)) + eq({1, 4}, meths.win_get_cursor(win2)) + end) + + describe('when text is being added right at cursor position #22526', function() + it('updates the cursor position in NORMAL mode', function() + insert([[ + abcd]]) + + -- position the cursor on 'c' + curwin('set_cursor', {1, 2}) + -- add 'xxx' before 'c' + set_text(0, 2, 0, 2, {'xxx'}) + eq({'abxxxcd'}, get_lines(0, -1, true)) + -- cursor should be on 'c' + eq({1, 5}, curwin('get_cursor')) + end) + + it('updates the cursor position only in non-current window when in INSERT mode', function() + insert([[ + abcd]]) + + -- position the cursor on 'c' + curwin('set_cursor', {1, 2}) + -- open vertical split + feed('<c-w>v') + -- get into INSERT mode to treat cursor + -- as being after 'b', not on 'c' + feed('i') + -- add 'xxx' between 'b' and 'c' + set_text(0, 2, 0, 2, {'xxx'}) + eq({'abxxxcd'}, get_lines(0, -1, true)) + -- in the current window cursor should stay after 'b' + eq({1, 2}, curwin('get_cursor')) + -- quit INSERT mode + feed('<esc>') + -- close current window + feed('<c-w>c') + -- in another window cursor should be on 'c' + eq({1, 5}, curwin('get_cursor')) + end) + end) + + describe('when text is being deleted right at cursor position', function() + it('leaves cursor at the same position in NORMAL mode', function() + insert([[ + abcd]]) + + -- position the cursor on 'b' + curwin('set_cursor', {1, 1}) + -- delete 'b' + set_text(0, 1, 0, 2, {}) + eq({'acd'}, get_lines(0, -1, true)) + -- cursor is now on 'c' + eq({1, 1}, curwin('get_cursor')) + end) + + it('leaves cursor at the same position in INSERT mode in current and non-current window', function() + insert([[ + abcd]]) + + -- position the cursor on 'b' + curwin('set_cursor', {1, 1}) + -- open vertical split + feed('<c-w>v') + -- get into INSERT mode to treat cursor + -- as being after 'a', not on 'b' + feed('i') + -- delete 'b' + set_text(0, 1, 0, 2, {}) + eq({'acd'}, get_lines(0, -1, true)) + -- cursor in the current window should stay after 'a' + eq({1, 1}, curwin('get_cursor')) + -- quit INSERT mode + feed('<esc>') + -- close current window + feed('<c-w>c') + -- cursor in non-current window should stay on 'c' + eq({1, 1}, curwin('get_cursor')) + end) + end) + + describe('when cursor is inside replaced row range', function() + it('keeps cursor at the same position if cursor is at start_row, but before start_col', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on ' ' before 'first' + curwin('set_cursor', {1, 14}) + + set_text(0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should stay at the same position + eq({1, 14}, curwin('get_cursor')) + end) + + it('keeps cursor at the same position if cursor is at start_row and column is still valid', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'f' in 'first' + curwin('set_cursor', {1, 15}) + + set_text(0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should stay at the same position + eq({1, 15}, curwin('get_cursor')) + end) + + it('adjusts cursor column to keep it valid if start_row got smaller', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 't' in 'first' + curwin('set_cursor', {1, 19}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 24, {'last'}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'This should be last' }, get_lines(0, -1, true)) + -- cursor should end up on 't' in 'last' + eq({1, 18}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 18}, cursor) + end) + + it('adjusts cursor column to keep it valid if start_row got smaller in INSERT mode', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 't' in 'first' + curwin('set_cursor', {1, 19}) + -- enter INSERT mode to treat cursor as being after 't' + feed('a') + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 24, {'last'}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'This should be last' }, get_lines(0, -1, true)) + -- cursor should end up after 't' in 'last' + eq({1, 19}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 19}, cursor) + end) + + it('adjusts cursor column to keep it valid in a row after start_row if it got smaller', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'w' in 'want' + curwin('set_cursor', {2, 31}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + '1', + 'then 2', + 'and then', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be 1', + 'then 2', + 'and then the last one', + }, get_lines(0, -1, true)) + -- cursor column should end up at the end of a row + eq({2, 5}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 5}, cursor) + end) + + it('adjusts cursor column to keep it valid in a row after start_row if it got smaller in INSERT mode', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'w' in 'want' + curwin('set_cursor', {2, 31}) + -- enter INSERT mode + feed('a') + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + '1', + 'then 2', + 'and then', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be 1', + 'then 2', + 'and then the last one', + }, get_lines(0, -1, true)) + -- cursor column should end up at the end of a row + eq({2, 6}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 6}, cursor) + end) + + it('adjusts cursor line and column to keep it inside replacement range', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'n' in 'finally' + curwin('set_cursor', {3, 6}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should end up on 'y' in 'hopefully' + -- to stay in the range, because it got smaller + eq({2, 12}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 12}, cursor) + end) + + it('adjusts cursor line and column if replacement is empty', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'r' in 'there' + curwin('set_cursor', {2, 8}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 12, {}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'This should be the last one' }, get_lines(0, -1, true)) + -- cursor should end up on the next column after deleted range + eq({1, 15}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 15}, cursor) + end) + + it('adjusts cursor line and column if replacement is empty and start_col == 0', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'r' in 'there' + curwin('set_cursor', {2, 8}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 0, 2, 4, {}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'finally the last one' }, get_lines(0, -1, true)) + -- cursor should end up in column 0 + eq({1, 0}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 0}, cursor) + end) + + it('adjusts cursor column if replacement ends at cursor row, after cursor column', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'y' in 'finally' + curwin('set_cursor', {3, 10}) + set_text(0, 15, 2, 11, { '1', 'this 2', 'and then' }) + + eq({ + 'This should be 1', + 'this 2', + 'and then the last one', + }, get_lines(0, -1, true)) + -- cursor should end up on 'n' in 'then' + eq({3, 7}, curwin('get_cursor')) + end) + + it('adjusts cursor column if replacement ends at cursor row, at cursor column in INSERT mode', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'y' at 'finally' + curwin('set_cursor', {3, 10}) + -- enter INSERT mode to treat cursor as being between 'l' and 'y' + feed('i') + set_text(0, 15, 2, 11, { '1', 'this 2', 'and then' }) + + eq({ + 'This should be 1', + 'this 2', + 'and then the last one', + }, get_lines(0, -1, true)) + -- cursor should end up after 'n' in 'then' + eq({3, 8}, curwin('get_cursor')) + end) + + it('adjusts cursor column if replacement is inside of a single line', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'y' in 'finally' + curwin('set_cursor', {3, 10}) + set_text(2, 4, 2, 11, { 'then' }) + + eq({ + 'This should be first', + 'then there is a line we do not want', + 'and then the last one', + }, get_lines(0, -1, true)) + -- cursor should end up on 'n' in 'then' + eq({3, 7}, curwin('get_cursor')) + end) + + it('does not move cursor column after end of a line', function() + insert([[ + This should be the only line here + !!!]]) + + -- position cursor on the last '1' + curwin('set_cursor', {2, 2}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 33, 1, 3, {}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'This should be the only line here' }, get_lines(0, -1, true)) + -- cursor should end up on '!' + eq({1, 32}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 32}, cursor) + end) + + it('does not move cursor column before start of a line', function() + insert('\n!!!') + + -- position cursor on the last '1' + curwin('set_cursor', {2, 2}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 0, 1, 3, {}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ '' }, get_lines(0, -1, true)) + -- cursor should end up on '!' + eq({1, 0}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 0}, cursor) + end) + + describe('with virtualedit', function() + it('adjusts cursor line and column to keep it inside replacement range if cursor is not after eol', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position cursor on 't' in 'want' + curwin('set_cursor', {2, 34}) + -- turn on virtualedit + command('set virtualedit=all') + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should end up on 'y' in 'hopefully' + -- to stay in the range + eq({2, 12}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 12}, cursor) + -- coladd should be 0 + eq(0, exec_lua([[ + return vim.fn.winsaveview().coladd + ]])) + end) + + it('does not change cursor screen column when cursor is after eol and row got shorter', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position cursor on 't' in 'want' + curwin('set_cursor', {2, 34}) + -- turn on virtualedit + command('set virtualedit=all') + -- move cursor after eol + exec_lua([[ + vim.fn.winrestview({ coladd = 5 }) + ]]) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should end up at eol of a new row + eq({2, 26}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 26}, cursor) + -- coladd should be increased so that cursor stays in the same screen column + eq(13, exec_lua([[ + return vim.fn.winsaveview().coladd + ]])) + end) + + it('does not change cursor screen column when cursor is after eol and row got longer', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position cursor on 't' in 'first' + curwin('set_cursor', {1, 19}) + -- turn on virtualedit + command('set virtualedit=all') + -- move cursor after eol + exec_lua([[ + vim.fn.winrestview({ coladd = 21 }) + ]]) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should end up at eol of a new row + eq({1, 38}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 38}, cursor) + -- coladd should be increased so that cursor stays in the same screen column + eq(2, exec_lua([[ + return vim.fn.winsaveview().coladd + ]])) + end) + + it('does not change cursor screen column when cursor is after eol and row extended past cursor column', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position cursor on 't' in 'first' + curwin('set_cursor', {1, 19}) + -- turn on virtualedit + command('set virtualedit=all') + -- move cursor after eol just a bit + exec_lua([[ + vim.fn.winrestview({ coladd = 3 }) + ]]) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should stay at the same screen column + eq({1, 22}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 22}, cursor) + -- coladd should become 0 + eq(0, exec_lua([[ + return vim.fn.winsaveview().coladd + ]])) + end) + + it('does not change cursor screen column when cursor is after eol and row range decreased', function() + insert([[ + This should be first + then there is a line we do not want + and one more + and finally the last one]]) + + -- position cursor on 'e' in 'more' + curwin('set_cursor', {3, 11}) + -- turn on virtualedit + command('set virtualedit=all') + -- move cursor after eol + exec_lua([[ + vim.fn.winrestview({ coladd = 28 }) + ]]) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 15, 3, 11, { + 'the line we do not want', + 'but hopefully', + }) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should end up at eol of a new row + eq({2, 26}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({2, 26}, cursor) + -- coladd should be increased so that cursor stays in the same screen column + eq(13, exec_lua([[ + return vim.fn.winsaveview().coladd + ]])) + end) + end) + end) + + describe('when cursor is at end_row and after end_col', function() + it('adjusts cursor column when only a newline is added or deleted', function() + insert([[ + first line + second + line]]) + + -- position the cursor on 'i' + curwin('set_cursor', {3, 2}) + set_text(1, 6, 2, 0, {}) + eq({'first line', 'second line'}, get_lines(0, -1, true)) + -- cursor should stay on 'i' + eq({2, 8}, curwin('get_cursor')) + + -- add a newline back + set_text(1, 6, 1, 6, {'', ''}) + eq({'first line', 'second', ' line'}, get_lines(0, -1, true)) + -- cursor should return back to the original position + eq({3, 2}, curwin('get_cursor')) + end) + + it('adjusts cursor column if the range is not bound to either start or end of a line', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 'h' in 'the' + curwin('set_cursor', {3, 13}) + set_text(0, 14, 2, 11, {}) + eq({'This should be the last one'}, get_lines(0, -1, true)) + -- cursor should stay on 'h' + eq({1, 16}, curwin('get_cursor')) + -- add deleted lines back + set_text(0, 14, 0, 14, { + ' first', + 'then there is a line we do not want', + 'and finally', + }) + eq({ + 'This should be first', + 'then there is a line we do not want', + 'and finally the last one', + }, get_lines(0, -1, true)) + -- cursor should return back to the original position + eq({3, 13}, curwin('get_cursor')) + end) + + it('adjusts cursor column if replacing lines in range, not just deleting and adding', function() + insert([[ + This should be first + then there is a line we do not want + and finally the last one]]) + + -- position the cursor on 's' in 'last' + curwin('set_cursor', {3, 18}) + set_text(0, 15, 2, 11, { + 'the line we do not want', + 'but hopefully', + }) + + eq({ + 'This should be the line we do not want', + 'but hopefully the last one', + }, get_lines(0, -1, true)) + -- cursor should stay on 's' + eq({2, 20}, curwin('get_cursor')) + + set_text(0, 15, 1, 13, { + 'first', + 'then there is a line we do not want', + 'and finally', + }) + + eq({ + 'This should be first', + 'then there is a line we do not want', + 'and finally the last one', + }, get_lines(0, -1, true)) + -- cursor should return back to the original position + eq({3, 18}, curwin('get_cursor')) + end) + + it('does not move cursor column after end of a line', function() + insert([[ + This should be the only line here + ]]) + + -- position cursor at the empty line + curwin('set_cursor', {2, 0}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 33, 1, 0, {'!'}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ 'This should be the only line here!' }, get_lines(0, -1, true)) + -- cursor should end up on '!' + eq({1, 33}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 33}, cursor) + end) + + it('does not move cursor column before start of a line', function() + insert('\n') + + eq({ '', '' }, get_lines(0, -1, true)) + + -- position cursor on the last '1' + curwin('set_cursor', {2, 2}) + + local cursor = exec_lua([[ + vim.api.nvim_buf_set_text(0, 0, 0, 1, 0, {''}) + return vim.api.nvim_win_get_cursor(0) + ]]) + + eq({ '' }, get_lines(0, -1, true)) + -- cursor should end up on '!' + eq({1, 0}, curwin('get_cursor')) + -- immediate call to nvim_win_get_cursor should have returned the same position + eq({1, 0}, cursor) + end) + end) + it('can handle NULs', function() set_text(0, 0, 0, 0, {'ab\0cd'}) eq('ab\0cd', curbuf_depr('get_line', 0)) @@ -553,20 +1640,20 @@ describe('api/buf', function() insert([[ hello foo! text]]) - eq('start_row out of bounds', pcall_err(set_text, 2, 0, 3, 0, {})) - eq('start_row out of bounds', pcall_err(set_text, -3, 0, 0, 0, {})) - eq('end_row out of bounds', pcall_err(set_text, 0, 0, 2, 0, {})) - eq('end_row out of bounds', pcall_err(set_text, 0, 0, -3, 0, {})) - eq('start_col out of bounds', pcall_err(set_text, 1, 5, 1, 5, {})) - eq('end_col out of bounds', pcall_err(set_text, 1, 0, 1, 5, {})) + eq("Invalid 'start_row': out of range", pcall_err(set_text, 2, 0, 3, 0, {})) + eq("Invalid 'start_row': out of range", pcall_err(set_text, -3, 0, 0, 0, {})) + eq("Invalid 'end_row': out of range", pcall_err(set_text, 0, 0, 2, 0, {})) + eq("Invalid 'end_row': out of range", pcall_err(set_text, 0, 0, -3, 0, {})) + eq("Invalid 'start_col': out of range", pcall_err(set_text, 1, 5, 1, 5, {})) + eq("Invalid 'end_col': out of range", pcall_err(set_text, 1, 0, 1, 5, {})) end) it('errors when start is greater than end', function() insert([[ hello foo! text]]) - eq('start is higher than end', pcall_err(set_text, 1, 0, 0, 0, {})) - eq('start is higher than end', pcall_err(set_text, 0, 1, 0, 0, {})) + eq("'start' is higher than 'end'", pcall_err(set_text, 1, 0, 0, 0, {})) + eq("'start' is higher than 'end'", pcall_err(set_text, 0, 1, 0, 0, {})) end) it('no heap-use-after-free when called consecutively #19643', function() @@ -579,6 +1666,139 @@ describe('api/buf', function() ]]) eq({'one', 'two'}, get_lines(0, 2, true)) end) + + describe('handles topline', function() + local screen + before_each(function() + screen = Screen.new(20, 12) + screen:set_default_attr_ids { + [1] = {bold = true, foreground = Screen.colors.Blue1}; + [2] = {reverse = true, bold = true}; + [3] = {reverse = true}; + } + screen:attach() + meths.buf_set_lines(0, 0, -1, 1, {"aaa", "bbb", "ccc", "ddd", "www", "xxx", "yyy", "zzz"}) + meths.set_option_value('modified', false, {}) + end) + + it('of current window', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('new | wincmd w') + meths.win_set_cursor(win, {8,0}) + + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + www | + xxx | + yyy | + ^zzz | + {2:[No Name] }| + | + ]]} + meths.buf_set_text(buf, 0,3, 1,0, {"X"}) + + screen:expect{grid=[[ + | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {3:[No Name] }| + www | + xxx | + yyy | + ^zzz | + {2:[No Name] [+] }| + | + ]]} + end) + + it('of non-current window', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('new') + meths.win_set_cursor(win, {8,0}) + + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] }| + | + ]]} + + meths.buf_set_text(buf, 0,3, 1,0, {"X"}) + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + end) + + it('of split windows with same buffer', function() + local win = meths.get_current_win() + local buf = meths.get_current_buf() + + command('split') + meths.win_set_cursor(win, {8,0}) + meths.win_set_cursor(0, {1,1}) + + screen:expect{grid=[[ + a^aa | + bbb | + ccc | + ddd | + www | + {2:[No Name] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] }| + | + ]]} + meths.buf_set_text(buf, 0,3, 1,0, {"X"}) + + screen:expect{grid=[[ + a^aaXbbb | + ccc | + ddd | + www | + xxx | + {2:[No Name] [+] }| + www | + xxx | + yyy | + zzz | + {3:[No Name] [+] }| + | + ]]} + end) + end) end) describe_lua_and_rpc('nvim_buf_get_text', function(api) @@ -611,7 +1831,7 @@ describe('api/buf', function() end) it('errors when start is greater than end', function() - eq('start is higher than end', pcall_err(get_text, 1, 0, 0, 0, {})) + eq("'start' is higher than 'end'", pcall_err(get_text, 1, 0, 0, 0, {})) eq('start_col must be less than end_col', pcall_err(get_text, 0, 1, 0, 0, {})) end) end) @@ -630,19 +1850,19 @@ describe('api/buf', function() eq('Index out of bounds', pcall_err(get_offset, 6)) eq('Index out of bounds', pcall_err(get_offset, -1)) - curbufmeths.set_option('eol', false) - curbufmeths.set_option('fixeol', false) + meths.set_option_value('eol', false, {}) + meths.set_option_value('fixeol', false, {}) eq(28, get_offset(5)) -- fileformat is ignored - curbufmeths.set_option('fileformat', 'dos') + meths.set_option_value('fileformat', 'dos', {}) eq(0, get_offset(0)) eq(6, get_offset(1)) eq(15, get_offset(2)) eq(16, get_offset(3)) eq(24, get_offset(4)) eq(28, get_offset(5)) - curbufmeths.set_option('eol', true) + meths.set_option_value('eol', true, {}) eq(29, get_offset(5)) command("set hidden") @@ -650,6 +1870,18 @@ describe('api/buf', function() eq(6, bufmeths.get_offset(1,1)) command("bunload! 1") eq(-1, bufmeths.get_offset(1,1)) + eq(-1, bufmeths.get_offset(1,0)) + end) + + it('works in empty buffer', function() + eq(0, get_offset(0)) + eq(1, get_offset(1)) + end) + + it('works in buffer with one line inserted', function() + feed('itext') + eq(0, get_offset(0)) + eq(5, get_offset(1)) end) end) @@ -697,23 +1929,23 @@ describe('api/buf', function() end) end) - describe('nvim_buf_get_option, nvim_buf_set_option', function() + describe('nvim_get_option_value, nvim_set_option_value', function() it('works', function() - eq(8, curbuf('get_option', 'shiftwidth')) - curbuf('set_option', 'shiftwidth', 4) - eq(4, curbuf('get_option', 'shiftwidth')) + eq(8, nvim('get_option_value', 'shiftwidth', {})) + nvim('set_option_value', 'shiftwidth', 4, {}) + eq(4, nvim('get_option_value', 'shiftwidth', {})) -- global-local option - curbuf('set_option', 'define', 'test') - eq('test', curbuf('get_option', 'define')) + nvim('set_option_value', 'define', 'test', {buf = 0}) + eq('test', nvim('get_option_value', 'define', {buf = 0})) -- Doesn't change the global value - eq([[^\s*#\s*define]], nvim('get_option', 'define')) + eq("", nvim('get_option_value', 'define', {scope='global'})) end) it('returns values for unset local options', function() -- 'undolevels' is only set to its "unset" value when a new buffer is -- created command('enew') - eq(-123456, curbuf('get_option', 'undolevels')) + eq(-123456, nvim('get_option_value', 'undolevels', {buf=0})) end) end) diff --git a/test/functional/api/buffer_updates_spec.lua b/test/functional/api/buffer_updates_spec.lua index d5f06c8f1f..80e29c1ff2 100644 --- a/test/functional/api/buffer_updates_spec.lua +++ b/test/functional/api/buffer_updates_spec.lua @@ -75,7 +75,7 @@ local function reopenwithfolds(b) local tick = reopen(b, origlines) -- use markers for folds, make all folds open by default - command('setlocal foldmethod=marker foldlevel=20') + command('setlocal foldmethod=marker foldlevel=20 commentstring=/*%s*/') -- add a fold command('2,4fold') @@ -762,7 +762,7 @@ describe('API: buffer events:', function() it('returns a proper error on nonempty options dict', function() clear() local b = editoriginal(false) - eq("unexpected key: builtin", pcall_err(buffer, 'attach', b, false, {builtin="asfd"})) + eq("Invalid 'opts' key: 'builtin'", pcall_err(buffer, 'attach', b, false, {builtin="asfd"})) end) it('nvim_buf_attach returns response after delay #8634', function() diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index d0fb26edc7..1ddb289ded 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -24,7 +24,7 @@ describe('nvim_get_commands', function() eq({}, meths.get_commands({builtin=false})) end) - it('validates input', function() + it('validation', function() eq('builtin=true not implemented', pcall_err(meths.get_commands, {builtin=true})) eq("Invalid key: 'foo'", pcall_err(meths.get_commands, @@ -543,23 +543,19 @@ describe('nvim_create_user_command', function() end) it('does not allow invalid command names', function() - matches("'name' must begin with an uppercase letter", pcall_err(exec_lua, [[ + eq("Invalid command name (must start with uppercase): 'test'", pcall_err(exec_lua, [[ vim.api.nvim_create_user_command('test', 'echo "hi"', {}) ]])) - - matches('Invalid command name', pcall_err(exec_lua, [[ + eq("Invalid command name: 't@'", pcall_err(exec_lua, [[ vim.api.nvim_create_user_command('t@', 'echo "hi"', {}) ]])) - - matches('Invalid command name', pcall_err(exec_lua, [[ + eq("Invalid command name: 'T@st'", pcall_err(exec_lua, [[ vim.api.nvim_create_user_command('T@st', 'echo "hi"', {}) ]])) - - matches('Invalid command name', pcall_err(exec_lua, [[ + eq("Invalid command name: 'Test!'", pcall_err(exec_lua, [[ vim.api.nvim_create_user_command('Test!', 'echo "hi"', {}) ]])) - - matches('Invalid command name', pcall_err(exec_lua, [[ + eq("Invalid command name: '💩'", pcall_err(exec_lua, [[ vim.api.nvim_create_user_command('💩', 'echo "hi"', {}) ]])) end) diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua index 9902826c72..44a151cf6a 100644 --- a/test/functional/api/extmark_spec.lua +++ b/test/functional/api/extmark_spec.lua @@ -11,6 +11,7 @@ local insert = helpers.insert local feed = helpers.feed local clear = helpers.clear local command = helpers.command +local exec = helpers.exec local meths = helpers.meths local assert_alive = helpers.assert_alive @@ -101,12 +102,31 @@ describe('API/extmarks', function() ns2 = request('nvim_create_namespace', "my-fancy-plugin2") end) + it('validation', function() + eq("Invalid 'end_col': expected Integer, got Array", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = {}, end_row = 1 })) + eq("Invalid 'end_row': expected Integer, got Array", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = 1, end_row = {} })) + eq("Invalid 'virt_text_pos': expected String, got Integer", pcall_err(set_extmark, ns, marks[2], 0, 0, { virt_text_pos = 0 })) + eq("Invalid 'virt_text_pos': 'foo'", pcall_err(set_extmark, ns, marks[2], 0, 0, { virt_text_pos = 'foo' })) + eq("Invalid 'hl_mode': expected String, got Integer", pcall_err(set_extmark, ns, marks[2], 0, 0, { hl_mode = 0 })) + eq("Invalid 'hl_mode': 'foo'", pcall_err(set_extmark, ns, marks[2], 0, 0, { hl_mode = 'foo' })) + eq("Invalid 'id': expected Integer, got Array", pcall_err(set_extmark, ns, {}, 0, 0, { end_col = 1, end_row = 1 })) + eq("Invalid mark position: expected 2 Integer items", pcall_err(get_extmarks, ns, {}, {-1, -1})) + eq("Invalid mark position: expected mark id Integer or 2-item Array", pcall_err(get_extmarks, ns, true, {-1, -1})) + -- No memory leak with virt_text, virt_lines, sign_text + eq("right_gravity is not a boolean", pcall_err(set_extmark, ns, marks[2], 0, 0, { + virt_text = {{'foo', 'Normal'}}, + virt_lines = {{{'bar', 'Normal'}}}, + sign_text = 'a', + right_gravity = 'baz', + })) + end) + it("can end extranges past final newline using end_col = 0", function() set_extmark(ns, marks[1], 0, 0, { end_col = 0, end_row = 1 }) - eq("end_col value outside range", + eq("Invalid 'end_col': out of range", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = 1, end_row = 1 })) end) @@ -188,6 +208,23 @@ describe('API/extmarks', function() eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) end) + it('can undo with extmarks (#25147)', function() + feed('itest<esc>') + set_extmark(ns, 1, 0, 0) + set_extmark(ns, 2, 1, 0) + eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) + feed('dd') + eq({ { 1, 1, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) + curbufmeths.clear_namespace(ns, 0, -1) + eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) + set_extmark(ns, 1, 0, 0, { right_gravity = false }) + set_extmark(ns, 2, 1, 0, { right_gravity = false }) + eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) + feed('u') + eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) + curbufmeths.clear_namespace(ns, 0, -1) + end) + it('querying for information and ranges', function() --marks = {1, 2, 3} --positions = {{0, 0,}, {0, 2}, {0, 3}} @@ -733,7 +770,14 @@ describe('API/extmarks', function() }) end) - -- TODO(bfredl): add more tests! + it('can get overlapping extmarks', function() + set_extmark(ns, 1, 0, 0, {end_row = 5, end_col=0}) + set_extmark(ns, 2, 2, 5, {end_row = 2, end_col=30}) + set_extmark(ns, 3, 0, 5, {end_row = 2, end_col=10}) + set_extmark(ns, 4, 0, 0, {end_row = 1, end_col=0}) + eq({{ 2, 2, 5 }}, get_extmarks(ns, {2, 0}, {2, -1}, { overlap=false })) + eq({{ 1, 0, 0 }, { 3, 0, 5}, {2, 2, 5}}, get_extmarks(ns, {2, 0}, {2, -1}, { overlap=true })) + end) end) it('replace works', function() @@ -1344,10 +1388,10 @@ describe('API/extmarks', function() it('throws consistent error codes', function() local ns_invalid = ns2 + 1 - eq("Invalid ns_id", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2])) - eq("Invalid ns_id", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1])) - eq("Invalid ns_id", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2])) - eq("Invalid ns_id", pcall_err(get_extmark_by_id, ns_invalid, marks[1])) + eq("Invalid 'ns_id': 3", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2])) + eq("Invalid 'ns_id': 3", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1])) + eq("Invalid 'ns_id': 3", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2])) + eq("Invalid 'ns_id': 3", pcall_err(get_extmark_by_id, ns_invalid, marks[1])) end) it('when col = line-length, set the mark on eol', function() @@ -1362,13 +1406,13 @@ describe('API/extmarks', function() it('when col = line-length, set the mark on eol', function() local invalid_col = init_text:len() + 1 - eq("col value outside range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col)) + eq("Invalid 'col': out of range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col)) end) it('fails when line > line_count', function() local invalid_col = init_text:len() + 1 local invalid_lnum = 3 - eq('line value outside range', pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col)) + eq("Invalid 'line': out of range", pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col)) eq({}, get_extmark_by_id(ns, marks[1])) end) @@ -1385,7 +1429,7 @@ describe('API/extmarks', function() it('in read-only buffer', function() command("view! runtime/doc/help.txt") - eq(true, curbufmeths.get_option('ro')) + eq(true, meths.get_option_value('ro', {})) local id = set_extmark(ns, 0, 0, 2) eq({{id, 0, 2}}, get_extmarks(ns,0, -1)) end) @@ -1398,12 +1442,12 @@ describe('API/extmarks', function() end) it('does not crash with append/delete/undo sequence', function() - meths.exec([[ + exec([[ let ns = nvim_create_namespace('myplugin') call nvim_buf_set_extmark(0, ns, 0, 0, {}) call append(0, '') %delete - undo]],false) + undo]]) assert_alive() end) @@ -1435,7 +1479,7 @@ describe('API/extmarks', function() feed('u') -- handles pasting - meths.exec([[let @a='asdfasdf']], false) + exec([[let @a='asdfasdf']]) feed([["ap]]) eq({ {1, 0, 0}, {2, 0, 8} }, meths.buf_get_extmarks(0, ns, 0, -1, {})) @@ -1447,6 +1491,7 @@ describe('API/extmarks', function() end_line = 1 }) eq({ {1, 0, 0, { + ns_id = 1, end_col = 0, end_row = 1, right_gravity = true, @@ -1457,57 +1502,184 @@ describe('API/extmarks', function() it('in prompt buffer', function() feed('dd') local id = set_extmark(ns, marks[1], 0, 0, {}) - curbufmeths.set_option('buftype', 'prompt') + meths.set_option_value('buftype', 'prompt', {}) feed('i<esc>') eq({{id, 0, 2}}, get_extmarks(ns, 0, -1)) end) it('can get details', function() set_extmark(ns, marks[1], 0, 0, { + conceal = "c", + cursorline_hl_group = "Statement", end_col = 0, - end_row = 1, - right_gravity = false, end_right_gravity = true, - priority = 0, + end_row = 1, hl_eol = true, - hl_mode = "blend", hl_group = "String", - virt_text = { { "text", "Statement" } }, - virt_text_pos = "right_align", - virt_text_hide = true, - virt_lines = { { { "lines", "Statement" } }}, + hl_mode = "blend", + line_hl_group = "Statement", + number_hl_group = "Statement", + priority = 0, + right_gravity = false, + sign_hl_group = "Statement", + sign_text = ">>", + spell = true, + virt_lines = { + { { "lines", "Macro" }, { "???" } }, + { { "stack", { "Type", "Search" } }, { "!!!" } }, + }, virt_lines_above = true, virt_lines_leftcol = true, + virt_text = { { "text", "Macro" }, { "???" }, { "stack", { "Type", "Search" } } }, + virt_text_hide = true, + virt_text_pos = "right_align", }) set_extmark(ns, marks[2], 0, 0, { priority = 0, - virt_text = { { "text", "Statement" } }, + virt_text = { { "", "Macro" }, { "", { "Type", "Search" } }, { "" } }, virt_text_win_col = 1, }) eq({0, 0, { + conceal = "c", + cursorline_hl_group = "Statement", end_col = 0, - end_row = 1, - right_gravity = false, end_right_gravity = true, - priority = 0, + end_row = 1, hl_eol = true, - hl_mode = "blend", hl_group = "String", - virt_text = { { "text", "Statement" } }, - virt_text_pos = "right_align", - virt_text_hide = true, - virt_lines = { { { "lines", "Statement" } }}, + hl_mode = "blend", + line_hl_group = "Statement", + ns_id = 1, + number_hl_group = "Statement", + priority = 0, + right_gravity = false, + sign_hl_group = "Statement", + sign_text = ">>", + spell = true, + virt_lines = { + { { "lines", "Macro" }, { "???" } }, + { { "stack", { "Type", "Search" } }, { "!!!" } }, + }, virt_lines_above = true, virt_lines_leftcol = true, + virt_text = { { "text", "Macro" }, { "???" }, { "stack", { "Type", "Search" } } }, + virt_text_hide = true, + virt_text_pos = "right_align", } }, get_extmark_by_id(ns, marks[1], { details = true })) eq({0, 0, { + ns_id = 1, right_gravity = true, priority = 0, - virt_text = { { "text", "Statement" } }, + virt_text = { { "", "Macro" }, { "", { "Type", "Search" } }, { "" } }, virt_text_hide = false, virt_text_pos = "win_col", virt_text_win_col = 1, } }, get_extmark_by_id(ns, marks[2], { details = true })) + set_extmark(ns, marks[3], 0, 0, { cursorline_hl_group = "Statement" }) + eq({0, 0, { + ns_id = 1, + cursorline_hl_group = "Statement", + priority = 4096, + right_gravity = true, + } }, get_extmark_by_id(ns, marks[3], { details = true })) + curbufmeths.clear_namespace(ns, 0, -1) + -- legacy sign mark includes sign name + command('sign define sign1 text=s1 texthl=Title linehl=LineNR numhl=Normal culhl=CursorLine') + command('sign place 1 name=sign1 line=1') + eq({ {1, 0, 0, { + cursorline_hl_group = 'CursorLine', + invalidate = true, + line_hl_group = 'LineNr', + ns_id = 0, + number_hl_group = 'Normal', + priority = 10, + right_gravity = true, + sign_hl_group = 'Title', + sign_name = 'sign1', + sign_text = 's1', + undo_restore = false + } } }, get_extmarks(-1, 0, -1, { details = true })) + end) + + it('can get marks from anonymous namespaces', function() + ns = request('nvim_create_namespace', "") + ns2 = request('nvim_create_namespace', "") + set_extmark(ns, 1, 0, 0, {}) + set_extmark(ns2, 2, 1, 0, {}) + eq({{ 1, 0, 0, { ns_id = ns, right_gravity = true }}, + { 2, 1, 0, { ns_id = ns2, right_gravity = true }}}, + get_extmarks(-1, 0, -1, { details = true })) + end) + + it('can filter by extmark properties', function() + set_extmark(ns, 1, 0, 0, {}) + set_extmark(ns, 2, 0, 0, { hl_group = 'Normal' }) + set_extmark(ns, 3, 0, 0, { sign_text = '>>' }) + set_extmark(ns, 4, 0, 0, { virt_text = {{'text', 'Normal'}}}) + set_extmark(ns, 5, 0, 0, { virt_lines = {{{ 'line', 'Normal' }}}}) + eq(5, #get_extmarks(-1, 0, -1, {})) + eq({{ 2, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'highlight' })) + eq({{ 3, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'sign' })) + eq({{ 4, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_text' })) + eq({{ 5, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_lines' })) + end) + + it("invalidated marks are deleted", function() + screen = Screen.new(40, 6) + screen:attach() + feed('dd6iaaa bbb ccc<CR><ESC>gg') + set_extmark(ns, 1, 0, 0, { invalidate = true, sign_text = 'S1' }) + set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2' }) + -- mark with invalidate is removed + command('d') + screen:expect([[ + S2^aaa bbb ccc | + aaa bbb ccc | + aaa bbb ccc | + aaa bbb ccc | + aaa bbb ccc | + | + ]]) + -- mark is restored with undo_restore == true + command('silent undo') + screen:expect([[ + S1^aaa bbb ccc | + S2aaa bbb ccc | + aaa bbb ccc | + aaa bbb ccc | + aaa bbb ccc | + | + ]]) + -- mark is deleted with undo_restore == false + set_extmark(ns, 1, 0, 0, { invalidate = true, undo_restore = false, sign_text = 'S1' }) + set_extmark(ns, 2, 1, 0, { invalidate = true, undo_restore = false, sign_text = 'S2' }) + command('1d 2') + eq(0, #get_extmarks(-1, 0, -1, {})) + -- mark is not removed when deleting bytes before the range + set_extmark(ns, 3, 0, 4, { invalidate = true, undo_restore = false, + hl_group = 'Error', end_col = 7 }) + feed('dw') + eq(3, get_extmark_by_id(ns, 3, { details = true })[3].end_col) + -- mark is not removed when deleting bytes at the start of the range + feed('x') + eq(2, get_extmark_by_id(ns, 3, { details = true })[3].end_col) + -- mark is not removed when deleting bytes from the end of the range + feed('lx') + eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col) + -- mark is not removed when deleting bytes beyond end of the range + feed('x') + eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col) + -- mark is removed when all bytes in the range are deleted + feed('hx') + eq({}, get_extmark_by_id(ns, 3, {})) + -- multiline mark is not removed when start of its range is deleted + set_extmark(ns, 4, 1, 4, { undo_restore = false, invalidate = true, + hl_group = 'Error', end_col = 7, end_row = 3 }) + feed('ddDdd') + eq({0, 0}, get_extmark_by_id(ns, 4, {})) + -- multiline mark is removed when entirety of its range is deleted + feed('vj2ed') + eq({}, get_extmark_by_id(ns, 4, {})) end) end) diff --git a/test/functional/api/highlight_spec.lua b/test/functional/api/highlight_spec.lua index 7f044977cb..7d7d07e30e 100644 --- a/test/functional/api/highlight_spec.lua +++ b/test/functional/api/highlight_spec.lua @@ -57,9 +57,7 @@ describe('API: highlight',function() eq(expected_rgb, nvim("get_hl_by_id", hl_id, true)) -- Test invalid id. - local err, emsg = pcall(meths.get_hl_by_id, 30000, false) - eq(false, err) - eq('Invalid highlight id: 30000', string.match(emsg, 'Invalid.*')) + eq('Invalid highlight id: 30000', pcall_err(meths.get_hl_by_id, 30000, false)) -- Test all highlight properties. command('hi NewHighlight gui=underline,bold,italic,reverse,strikethrough,altfont,nocombine') @@ -70,22 +68,14 @@ describe('API: highlight',function() eq(expected_undercurl, nvim("get_hl_by_id", hl_id, true)) -- Test nil argument. - err, emsg = pcall(meths.get_hl_by_id, { nil }, false) - eq(false, err) eq('Wrong type for argument 1 when calling nvim_get_hl_by_id, expecting Integer', - string.match(emsg, 'Wrong.*')) + pcall_err(meths.get_hl_by_id, { nil }, false)) -- Test 0 argument. - err, emsg = pcall(meths.get_hl_by_id, 0, false) - eq(false, err) - eq('Invalid highlight id: 0', - string.match(emsg, 'Invalid.*')) + eq('Invalid highlight id: 0', pcall_err(meths.get_hl_by_id, 0, false)) -- Test -1 argument. - err, emsg = pcall(meths.get_hl_by_id, -1, false) - eq(false, err) - eq('Invalid highlight id: -1', - string.match(emsg, 'Invalid.*')) + eq('Invalid highlight id: -1', pcall_err(meths.get_hl_by_id, -1, false)) -- Test highlight group without ctermbg value. command('hi Normal ctermfg=red ctermbg=yellow') @@ -119,22 +109,16 @@ describe('API: highlight',function() eq(expected_normal, nvim("get_hl_by_name", 'Normal', true)) -- Test invalid name. - local err, emsg = pcall(meths.get_hl_by_name , 'unknown_highlight', false) - eq(false, err) - eq('Invalid highlight name: unknown_highlight', - string.match(emsg, 'Invalid.*')) + eq("Invalid highlight name: 'unknown_highlight'", + pcall_err(meths.get_hl_by_name , 'unknown_highlight', false)) -- Test nil argument. - err, emsg = pcall(meths.get_hl_by_name , { nil }, false) - eq(false, err) eq('Wrong type for argument 1 when calling nvim_get_hl_by_name, expecting String', - string.match(emsg, 'Wrong.*')) + pcall_err(meths.get_hl_by_name , { nil }, false)) -- Test empty string argument. - err, emsg = pcall(meths.get_hl_by_name , '', false) - eq(false, err) - eq('Invalid highlight name: ', - string.match(emsg, 'Invalid.*')) + eq('Invalid highlight name', + pcall_err(meths.get_hl_by_name , '', false)) -- Test "standout" attribute. #8054 eq({ underline = true, }, @@ -155,7 +139,7 @@ describe('API: highlight',function() it('nvim_get_hl_id_by_name', function() -- precondition: use a hl group that does not yet exist - eq('Invalid highlight name: Shrubbery', pcall_err(meths.get_hl_by_name, "Shrubbery", true)) + eq("Invalid highlight name: 'Shrubbery'", pcall_err(meths.get_hl_by_name, "Shrubbery", true)) eq(0, funcs.hlID("Shrubbery")) local hl_id = meths.get_hl_id_by_name("Shrubbery") @@ -171,9 +155,9 @@ describe('API: highlight',function() it("nvim_buf_add_highlight to other buffer doesn't crash if undo is disabled #12873", function() command('vsplit file') - local err, _ = pcall(meths.buf_set_option, 1, 'undofile', false) + local err, _ = pcall(meths.set_option_value, 'undofile', false, { buf = 1 }) eq(true, err) - err, _ = pcall(meths.buf_set_option, 1, 'undolevels', -1) + err, _ = pcall(meths.set_option_value, 'undolevels', -1, { buf = 1 }) eq(true, err) err, _ = pcall(meths.buf_add_highlight, 1, -1, 'Question', 0, 0, -1) eq(true, err) @@ -253,25 +237,32 @@ describe("API: set highlight", function() before_each(clear) - it ("can set gui highlight", function() + it('validation', function() + eq("Invalid 'blend': out of range", + pcall_err(meths.set_hl, 0, 'Test_hl3', {fg='#FF00FF', blend=999})) + eq("Invalid 'blend': expected Integer, got Array", + pcall_err(meths.set_hl, 0, 'Test_hl3', {fg='#FF00FF', blend={}})) + end) + + it("can set gui highlight", function() local ns = get_ns() meths.set_hl(ns, 'Test_hl', highlight1) eq(highlight1, meths.get_hl_by_name('Test_hl', true)) end) - it ("can set cterm highlight", function() + it("can set cterm highlight", function() local ns = get_ns() meths.set_hl(ns, 'Test_hl', highlight2_config) eq(highlight2_result, meths.get_hl_by_name('Test_hl', false)) end) - it ("can set empty cterm attr", function() + it("can set empty cterm attr", function() local ns = get_ns() meths.set_hl(ns, 'Test_hl', { cterm = {} }) eq({}, meths.get_hl_by_name('Test_hl', false)) end) - it ("cterm attr defaults to gui attr", function() + it("cterm attr defaults to gui attr", function() local ns = get_ns() meths.set_hl(ns, 'Test_hl', highlight1) eq({ @@ -280,14 +271,28 @@ describe("API: set highlight", function() }, meths.get_hl_by_name('Test_hl', false)) end) - it ("can overwrite attr for cterm", function() + it("can overwrite attr for cterm", function() local ns = get_ns() meths.set_hl(ns, 'Test_hl', highlight3_config) eq(highlight3_result_gui, meths.get_hl_by_name('Test_hl', true)) eq(highlight3_result_cterm, meths.get_hl_by_name('Test_hl', false)) end) - it ("can set a highlight in the global namespace", function() + it("only allows one underline attribute #22371", function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', { + underdouble = true, + underdotted = true, + cterm = { + underline = true, + undercurl = true, + }, + }) + eq({ undercurl = true }, meths.get_hl_by_name('Test_hl', false)) + eq({ underdotted = true }, meths.get_hl_by_name('Test_hl', true)) + end) + + it("can set a highlight in the global namespace", function() meths.set_hl(0, 'Test_hl', highlight2_config) eq('Test_hl xxx cterm=underline,reverse ctermfg=8 ctermbg=15 gui=underline,reverse', exec_capture('highlight Test_hl')) @@ -307,7 +312,7 @@ describe("API: set highlight", function() exec_capture('highlight Test_hl3')) end) - it ("can modify a highlight in the global namespace", function() + it("can modify a highlight in the global namespace", function() meths.set_hl(0, 'Test_hl3', { bg = 'red', fg = 'blue'}) eq('Test_hl3 xxx guifg=Blue guibg=Red', exec_capture('highlight Test_hl3')) @@ -328,17 +333,17 @@ describe("API: set highlight", function() eq('Test_hl3 xxx ctermbg=9', exec_capture('highlight Test_hl3')) - eq("'redd' is not a valid color", + eq("Invalid highlight color: 'redd'", pcall_err(meths.set_hl, 0, 'Test_hl3', {fg='redd'})) - eq("'bleu' is not a valid color", + eq("Invalid highlight color: 'bleu'", pcall_err(meths.set_hl, 0, 'Test_hl3', {ctermfg='bleu'})) meths.set_hl(0, 'Test_hl3', {fg='#FF00FF'}) eq('Test_hl3 xxx guifg=#ff00ff', exec_capture('highlight Test_hl3')) - eq("'#FF00FF' is not a valid color", + eq("Invalid highlight color: '#FF00FF'", pcall_err(meths.set_hl, 0, 'Test_hl3', {ctermfg='#FF00FF'})) for _, fg_val in ipairs{ nil, 'NONE', 'nOnE', '', -1 } do @@ -353,14 +358,298 @@ describe("API: set highlight", function() end) - it ("correctly sets 'Normal' internal properties", function() + it("correctly sets 'Normal' internal properties", function() -- Normal has some special handling internally. #18024 meths.set_hl(0, 'Normal', {fg='#000083', bg='#0000F3'}) eq({foreground = 131, background = 243}, nvim("get_hl_by_name", 'Normal', true)) end) it('does not segfault on invalid group name #20009', function() - eq('Invalid highlight name: foo bar', pcall_err(meths.set_hl, 0, 'foo bar', {bold = true})) + eq("Invalid highlight name: 'foo bar'", pcall_err(meths.set_hl, 0, 'foo bar', {bold = true})) assert_alive() end) end) + +describe('API: get highlight', function() + local highlight_color = { + fg = tonumber('0xff0000'), + bg = tonumber('0x0032aa'), + ctermfg = 8, + ctermbg = 15, + } + local highlight1 = { + bg = highlight_color.bg, + fg = highlight_color.fg, + bold = true, italic = true, + cterm = {bold = true, italic = true}, + } + local highlight2 = { + ctermbg = highlight_color.ctermbg, + ctermfg = highlight_color.ctermfg, + underline = true, reverse = true, + cterm = {underline = true, reverse = true}, + } + local highlight3_config = { + bg = highlight_color.bg, + fg = highlight_color.fg, + ctermbg = highlight_color.ctermbg, + ctermfg = highlight_color.ctermfg, + bold = true, + italic = true, + reverse = true, + underdashed = true, + strikethrough = true, + altfont = true, + cterm = { + italic = true, + reverse = true, + strikethrough = true, + altfont = true, + nocombine = true, + }, + } + local highlight3_result = { + bg = highlight_color.bg, + fg = highlight_color.fg, + ctermbg = highlight_color.ctermbg, + ctermfg = highlight_color.ctermfg, + bold = true, italic = true, reverse = true, underdashed = true, strikethrough = true, altfont = true, + cterm = {italic = true, nocombine = true, reverse = true, strikethrough = true, altfont = true} + } + + local function get_ns() + -- Test namespace filtering behavior + local ns2 = meths.create_namespace('Another_namespace') + meths.set_hl(ns2, 'Test_hl', { ctermfg = 23 }) + meths.set_hl(ns2, 'Test_another_hl', { link = 'Test_hl' }) + meths.set_hl(ns2, 'Test_hl_link', { link = 'Test_another_hl' }) + meths.set_hl(ns2, 'Test_another_hl_link', { link = 'Test_hl_link' }) + + local ns = meths.create_namespace('Test_set_hl') + meths.set_hl_ns(ns) + + return ns + end + + before_each(clear) + + it('validation', function() + eq("Invalid 'name': expected String, got Integer", + pcall_err(meths.get_hl, 0, { name = 177 })) + eq('Highlight id out of bounds', pcall_err(meths.get_hl, 0, { name = 'Test set hl' })) + end) + + it('nvim_get_hl with create flag', function() + eq({}, nvim("get_hl", 0, {name = 'Foo', create = false})) + eq(0, funcs.hlexists('Foo')) + meths.get_hl(0, {name = 'Bar', create = true}) + eq(1, funcs.hlexists('Bar')) + meths.get_hl(0, {name = 'FooBar'}) + eq(1, funcs.hlexists('FooBar')) + end) + + it('can get all highlights in current namespace', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', { bg = '#B4BEFE' }) + meths.set_hl(ns, 'Test_hl_link', { link = 'Test_hl' }) + eq({ + Test_hl = { + bg = 11845374 + }, + Test_hl_link = { + link = 'Test_hl' + } + }, meths.get_hl(ns, {})) + end) + + it('can get gui highlight', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', highlight1) + eq(highlight1, meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('can get cterm highlight', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', highlight2) + eq(highlight2, meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('can get empty cterm attr', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', { cterm = {} }) + eq({}, meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('cterm attr defaults to gui attr', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', highlight1) + eq(highlight1, meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('can overwrite attr for cterm', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', highlight3_config) + eq(highlight3_result, meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('only allows one underline attribute #22371', function() + local ns = get_ns() + meths.set_hl(ns, 'Test_hl', { + underdouble = true, + underdotted = true, + cterm = { + underline = true, + undercurl = true, + }, + }) + eq({ underdotted = true, cterm = { undercurl = true} }, + meths.get_hl(ns, { name = 'Test_hl' })) + end) + + it('can get a highlight in the global namespace', function() + meths.set_hl(0, 'Test_hl', highlight2) + eq(highlight2, meths.get_hl(0, { name = 'Test_hl' })) + + meths.set_hl(0, 'Test_hl', { background = highlight_color.bg }) + eq({ + bg = 12970, + }, meths.get_hl(0, { name = 'Test_hl' })) + + meths.set_hl(0, 'Test_hl2', highlight3_config) + eq(highlight3_result, meths.get_hl(0, { name = 'Test_hl2' })) + + -- Colors are stored with the name they are defined, but + -- with canonical casing + meths.set_hl(0, 'Test_hl3', { bg = 'reD', fg = 'bLue' }) + eq({ + bg = 16711680, + fg = 255, + }, meths.get_hl(0, { name = 'Test_hl3' })) + end) + + it('nvim_get_hl by id', function() + local hl_id = meths.get_hl_id_by_name('NewHighlight') + + command( + 'hi NewHighlight cterm=underline ctermbg=green guifg=red guibg=yellow guisp=blue gui=bold' + ) + eq({ fg = 16711680, bg = 16776960, sp = 255, bold = true, + ctermbg = 10, cterm = { underline = true }, + }, meths.get_hl(0, { id = hl_id })) + + -- Test 0 argument + eq('Highlight id out of bounds', pcall_err(meths.get_hl, 0, { id = 0 })) + + eq( + "Invalid 'id': expected Integer, got String", + pcall_err(meths.get_hl, 0, { id = 'Test_set_hl' }) + ) + + -- Test all highlight properties. + command('hi NewHighlight gui=underline,bold,italic,reverse,strikethrough,altfont,nocombine') + eq({ fg = 16711680, bg = 16776960, sp = 255, + altfont = true, bold = true, italic = true, nocombine = true, reverse = true, strikethrough = true, underline = true, + ctermbg = 10, cterm = {underline = true}, + }, meths.get_hl(0, { id = hl_id })) + + -- Test undercurl + command('hi NewHighlight gui=undercurl') + eq({ fg = 16711680, bg = 16776960, sp = 255, undercurl = true, + ctermbg = 10, + cterm = {underline = true}, + }, meths.get_hl(0, { id = hl_id })) + end) + + it('can correctly detect links', function() + command('hi String guifg=#a6e3a1') + command('hi link @string string') + command('hi link @string.cpp @string') + eq({ fg = 10937249 }, meths.get_hl(0, { name = 'String' })) + eq({ link = 'String' }, meths.get_hl(0, { name = '@string' })) + eq({ fg = 10937249 }, meths.get_hl(0, { name = '@string.cpp', link = false })) + end) + + it('can get all attributes for a linked group', function() + command('hi Bar guifg=red') + command('hi Foo guifg=#00ff00 gui=bold,underline') + command('hi! link Foo Bar') + eq({ link = 'Bar', fg = tonumber('00ff00', 16), bold = true, underline = true }, meths.get_hl(0, { name = 'Foo', link = true })) + end) + + it('can set link as well as other attributes', function() + command('hi Bar guifg=red') + local hl = { link = 'Bar', fg = tonumber('00ff00', 16), bold = true, cterm = { bold = true } } + meths.set_hl(0, 'Foo', hl) + eq(hl, meths.get_hl(0, { name = 'Foo', link = true })) + end) + + it("doesn't contain unset groups", function() + local id = meths.get_hl_id_by_name "@foobar.hubbabubba" + ok(id > 0) + + local data = meths.get_hl(0, {}) + eq(nil, data["@foobar.hubbabubba"]) + eq(nil, data["@foobar"]) + + command 'hi @foobar.hubbabubba gui=bold' + data = meths.get_hl(0, {}) + eq({bold = true}, data["@foobar.hubbabubba"]) + eq(nil, data["@foobar"]) + + -- @foobar.hubbabubba was explicitly cleared and thus shows up + -- but @foobar was never touched, and thus doesn't + command 'hi clear @foobar.hubbabubba' + data = meths.get_hl(0, {}) + eq({}, data["@foobar.hubbabubba"]) + eq(nil, data["@foobar"]) + end) + + it('should return default flag', function() + meths.set_hl(0, 'Tried', { fg = "#00ff00", default = true }) + eq({ fg = tonumber('00ff00', 16), default = true }, meths.get_hl(0, { name = 'Tried' })) + end) + + it('should not output empty gui and cterm #23474', function() + meths.set_hl(0, 'Foo', {default = true}) + meths.set_hl(0, 'Bar', { default = true, fg = '#ffffff' }) + meths.set_hl(0, 'FooBar', { default = true, fg = '#ffffff', cterm = {bold = true} }) + meths.set_hl(0, 'FooBarA', { default = true, fg = '#ffffff', cterm = {bold = true,italic = true}}) + + eq('Foo xxx cleared', + exec_capture('highlight Foo')) + eq({default = true}, meths.get_hl(0, {name = 'Foo'})) + eq('Bar xxx guifg=#ffffff', + exec_capture('highlight Bar')) + eq('FooBar xxx cterm=bold guifg=#ffffff', + exec_capture('highlight FooBar')) + eq('FooBarA xxx cterm=bold,italic guifg=#ffffff', + exec_capture('highlight FooBarA')) + end) + + it('can override exist highlight group by force #20323', function() + local white = tonumber('ffffff', 16) + local green = tonumber('00ff00', 16) + meths.set_hl(0, 'Foo', { fg=white }) + meths.set_hl(0, 'Foo', { fg=green, force = true }) + eq({ fg = green },meths.get_hl(0, {name = 'Foo'})) + meths.set_hl(0, 'Bar', {link = 'Comment', default = true}) + meths.set_hl(0, 'Bar', {link = 'Foo',default = true, force = true}) + eq({link ='Foo', default = true}, meths.get_hl(0, {name = 'Bar'})) + end) +end) + +describe('API: set/get highlight namespace', function() + it('set/get highlight namespace', function() + eq(0, meths.get_hl_ns({})) + local ns = meths.create_namespace('') + meths.set_hl_ns(ns) + eq(ns, meths.get_hl_ns({})) + end) + + it('set/get window highlight namespace', function() + eq(-1, meths.get_hl_ns({winid = 0})) + local ns = meths.create_namespace('') + meths.win_set_hl_ns(0, ns) + eq(ns, meths.get_hl_ns({winid = 0})) + end) +end) diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua index 5be4425162..434f117956 100644 --- a/test/functional/api/keymap_spec.lua +++ b/test/functional/api/keymap_spec.lua @@ -19,6 +19,20 @@ local sleep = helpers.sleep local sid_api_client = -9 local sid_lua = -8 +local mode_bits_map = { + ['n'] = 0x01, + ['x'] = 0x02, + ['o'] = 0x04, + ['c'] = 0x08, + ['i'] = 0x10, + ['l'] = 0x20, + ['s'] = 0x40, + ['t'] = 0x80, + [' '] = 0x47, + ['v'] = 0x42, + ['!'] = 0x18, +} + describe('nvim_get_keymap', function() before_each(clear) @@ -32,9 +46,12 @@ describe('nvim_get_keymap', function() rhs='bar', expr=0, sid=0, + scriptversion=1, buffer=0, nowait=0, mode='n', + mode_bits=0x01, + abbr=0, noremap=1, lnum=0, } @@ -81,6 +98,7 @@ describe('nvim_get_keymap', function() -- The table will be the same except for the mode local insert_table = shallowcopy(foo_bar_map_table) insert_table['mode'] = 'i' + insert_table['mode_bits'] = 0x10 eq({insert_table}, meths.get_keymap('i')) end) @@ -258,8 +276,10 @@ describe('nvim_get_keymap', function() silent=0, expr=0, sid=0, + scriptversion=1, buffer=0, nowait=0, + abbr=0, noremap=1, lnum=0, } @@ -268,6 +288,7 @@ describe('nvim_get_keymap', function() ret.lhs = lhs ret.rhs = rhs ret.mode = mode + ret.mode_bits = mode_bits_map[mode] return ret end @@ -323,10 +344,13 @@ describe('nvim_get_keymap', function() lhsraw='| |', rhs='| |', mode='n', + mode_bits=0x01, + abbr=0, script=0, silent=0, expr=0, sid=0, + scriptversion=1, buffer=0, nowait=0, noremap=1, @@ -365,15 +389,18 @@ describe('nvim_get_keymap', function() silent=0, expr=0, sid=sid_lua, + scriptversion=1, buffer=0, nowait=0, mode='n', + mode_bits=0x01, + abbr=0, noremap=0, lnum=0, }, mapargs[1]) end) - it ('can handle map descriptions', function() + it('can handle map descriptions', function() meths.set_keymap('n', 'lhs', 'rhs', {desc="map description"}) eq({ lhs='lhs', @@ -383,9 +410,12 @@ describe('nvim_get_keymap', function() silent=0, expr=0, sid=sid_api_client, + scriptversion=1, buffer=0, nowait=0, mode='n', + mode_bits=0x01, + abbr=0, noremap=0, lnum=0, desc='map description' @@ -400,6 +430,9 @@ describe('nvim_set_keymap, nvim_del_keymap', function() -- maparg(), which does not accept "!" (though it returns "!" in its output -- if getting a mapping set with |:map!|). local function normalize_mapmode(mode, generate_expected) + if mode:sub(-1) == 'a' then + mode = mode:sub(1, -2) + end if not generate_expected and mode == '!' then -- Cannot retrieve mapmode-ic mappings with "!", but can with "i" or "c". mode = 'i' @@ -417,7 +450,10 @@ describe('nvim_set_keymap, nvim_del_keymap', function() end local to_return = {} - to_return.mode = normalize_mapmode(mode, true) + local expected_mode = normalize_mapmode(mode, true) + to_return.mode = expected_mode + to_return.mode_bits = mode_bits_map[expected_mode] + to_return.abbr = mode:sub(-1) == 'a' and 1 or 0 to_return.noremap = not opts.noremap and 0 or 1 to_return.lhs = lhs to_return.rhs = rhs @@ -426,6 +462,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() to_return.nowait = not opts.nowait and 0 or 1 to_return.expr = not opts.expr and 0 or 1 to_return.sid = not opts.sid and sid_api_client or opts.sid + to_return.scriptversion = 1 to_return.buffer = not opts.buffer and 0 or opts.buffer to_return.lnum = not opts.lnum and 0 or opts.lnum to_return.desc = opts.desc @@ -435,7 +472,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() -- Gets a maparg() dict from Nvim, if one exists. local function get_mapargs(mode, lhs) - local mapargs = funcs.maparg(lhs, normalize_mapmode(mode), false, true) + local mapargs = funcs.maparg(lhs, normalize_mapmode(mode), mode:sub(-1) == 'a', true) -- drop "lhsraw" and "lhsrawalt" which are hard to check mapargs.lhsraw = nil mapargs.lhsrawalt = nil @@ -450,7 +487,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() end) it('error if LHS longer than MAXMAPLEN', function() - -- assume MAXMAPLEN of 50 chars, as declared in vim.h + -- assume MAXMAPLEN of 50 chars, as declared in mapping_defs.h local MAXMAPLEN = 50 local lhs = '' for i=1,MAXMAPLEN do @@ -484,35 +521,33 @@ describe('nvim_set_keymap, nvim_del_keymap', function() get_mapargs('', 'lhs')) end) - it('throws errors when given too-long mode shortnames', function() - eq('Shortname is too long: map', - pcall_err(meths.set_keymap, 'map', 'lhs', 'rhs', {})) - - eq('Shortname is too long: vmap', - pcall_err(meths.set_keymap, 'vmap', 'lhs', 'rhs', {})) - - eq('Shortname is too long: xnoremap', - pcall_err(meths.set_keymap, 'xnoremap', 'lhs', 'rhs', {})) - - eq('Shortname is too long: map', pcall_err(meths.del_keymap, 'map', 'lhs')) - eq('Shortname is too long: vmap', pcall_err(meths.del_keymap, 'vmap', 'lhs')) - eq('Shortname is too long: xnoremap', pcall_err(meths.del_keymap, 'xnoremap', 'lhs')) - end) - it('error on invalid mode shortname', function() - eq('Invalid mode shortname: " "', - pcall_err(meths.set_keymap, ' ', 'lhs', 'rhs', {})) - eq('Invalid mode shortname: "m"', - pcall_err(meths.set_keymap, 'm', 'lhs', 'rhs', {})) - eq('Invalid mode shortname: "?"', - pcall_err(meths.set_keymap, '?', 'lhs', 'rhs', {})) - eq('Invalid mode shortname: "y"', - pcall_err(meths.set_keymap, 'y', 'lhs', 'rhs', {})) - eq('Invalid mode shortname: "p"', - pcall_err(meths.set_keymap, 'p', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: " "', pcall_err(meths.set_keymap, ' ', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "m"', pcall_err(meths.set_keymap, 'm', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "?"', pcall_err(meths.set_keymap, '?', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "y"', pcall_err(meths.set_keymap, 'y', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "p"', pcall_err(meths.set_keymap, 'p', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "a"', pcall_err(meths.set_keymap, 'a', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "oa"', pcall_err(meths.set_keymap, 'oa', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "!o"', pcall_err(meths.set_keymap, '!o', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "!i"', pcall_err(meths.set_keymap, '!i', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "!!"', pcall_err(meths.set_keymap, '!!', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "map"', pcall_err(meths.set_keymap, 'map', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "vmap"', pcall_err(meths.set_keymap, 'vmap', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: "xnoremap"', pcall_err(meths.set_keymap, 'xnoremap', 'lhs', 'rhs', {})) + eq('Invalid mode shortname: " "', pcall_err(meths.del_keymap, ' ', 'lhs')) + eq('Invalid mode shortname: "m"', pcall_err(meths.del_keymap, 'm', 'lhs')) eq('Invalid mode shortname: "?"', pcall_err(meths.del_keymap, '?', 'lhs')) eq('Invalid mode shortname: "y"', pcall_err(meths.del_keymap, 'y', 'lhs')) eq('Invalid mode shortname: "p"', pcall_err(meths.del_keymap, 'p', 'lhs')) + eq('Invalid mode shortname: "a"', pcall_err(meths.del_keymap, 'a', 'lhs')) + eq('Invalid mode shortname: "oa"', pcall_err(meths.del_keymap, 'oa', 'lhs')) + eq('Invalid mode shortname: "!o"', pcall_err(meths.del_keymap, '!o', 'lhs')) + eq('Invalid mode shortname: "!i"', pcall_err(meths.del_keymap, '!i', 'lhs')) + eq('Invalid mode shortname: "!!"', pcall_err(meths.del_keymap, '!!', 'lhs')) + eq('Invalid mode shortname: "map"', pcall_err(meths.del_keymap, 'map', 'lhs')) + eq('Invalid mode shortname: "vmap"', pcall_err(meths.del_keymap, 'vmap', 'lhs')) + eq('Invalid mode shortname: "xnoremap"', pcall_err(meths.del_keymap, 'xnoremap', 'lhs')) end) it('error on invalid optnames', function() @@ -681,13 +716,13 @@ describe('nvim_set_keymap, nvim_del_keymap', function() end) it('can set <expr> mappings whose RHS change dynamically', function() - meths.exec([[ + exec([[ function! FlipFlop() abort if !exists('g:flip') | let g:flip = 0 | endif let g:flip = !g:flip return g:flip endfunction - ]], true) + ]]) eq(1, meths.call_function('FlipFlop', {})) eq(0, meths.call_function('FlipFlop', {})) eq(1, meths.call_function('FlipFlop', {})) @@ -744,7 +779,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() end) -- Perform exhaustive tests of basic functionality - local mapmodes = {'n', 'v', 'x', 's', 'o', '!', 'i', 'l', 'c', 't', ''} + local mapmodes = {'n', 'v', 'x', 's', 'o', '!', 'i', 'l', 'c', 't', '', 'ia', 'ca', '!a'} for _, mapmode in ipairs(mapmodes) do it('can set/unset normal mappings in mapmode '..mapmode, function() meths.set_keymap(mapmode, 'lhs', 'rhs', {}) @@ -773,11 +808,9 @@ describe('nvim_set_keymap, nvim_del_keymap', function() -- remove some map arguments that are harder to test, or were already tested optnames = {'nowait', 'silent', 'expr', 'noremap'} for _, mapmode in ipairs(mapmodes) do - local printable_mode = normalize_mapmode(mapmode) - -- Test with single mappings for _, maparg in ipairs(optnames) do - it('can set/unset '..printable_mode..'-mappings with maparg: '..maparg, + it('can set/unset '..mapmode..'-mappings with maparg: '..maparg, function() meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = true}) eq(generate_mapargs(mapmode, 'lhs', 'rhs', {[maparg] = true}), @@ -785,7 +818,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() meths.del_keymap(mapmode, 'lhs') eq({}, get_mapargs(mapmode, 'lhs')) end) - it ('can set/unset '..printable_mode..'-mode mappings with maparg '.. + it ('can set/unset '..mapmode..'-mode mappings with maparg '.. maparg..', whose value is false', function() meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = false}) eq(generate_mapargs(mapmode, 'lhs', 'rhs'), @@ -798,7 +831,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() -- Test with triplets of mappings, one of which is false for i = 1, (#optnames - 2) do local opt1, opt2, opt3 = optnames[i], optnames[i + 1], optnames[i + 2] - it('can set/unset '..printable_mode..'-mode mappings with mapargs '.. + it('can set/unset '..mapmode..'-mode mappings with mapargs '.. opt1..', '..opt2..', '..opt3, function() local opts = {[opt1] = true, [opt2] = false, [opt3] = true} meths.set_keymap(mapmode, 'lhs', 'rhs', opts) @@ -813,33 +846,36 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('can make lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) feed('asdf\n') eq(1, exec_lua[[return GlobalCount]]) - end) - it (':map command shows lua mapping correctly', function() + it(':map command shows lua mapping correctly', function() exec_lua [[ - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() print('jkl;') end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() print('jkl;') end }) ]] - assert.truthy(string.match(exec_lua[[return vim.api.nvim_exec(':nmap asdf', true)]], - "^\nn asdf <Lua %d+>")) + assert.truthy( + string.match( + exec_lua[[return vim.api.nvim_exec2(':nmap asdf', { output = true }).output]], + "^\nn asdf <Lua %d+>" + ) + ) end) - it ('mapcheck() returns lua mapping correctly', function() + it('mapcheck() returns lua mapping correctly', function() exec_lua [[ - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() print('jkl;') end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() print('jkl;') end }) ]] assert.truthy(string.match(funcs.mapcheck('asdf', 'n'), "^<Lua %d+>")) end) - it ('maparg() returns lua mapping correctly', function() + it('maparg() returns lua mapping correctly', function() eq(0, exec_lua([[ GlobalCount = 0 vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) @@ -867,7 +903,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('can make lua expr mappings replacing keycodes', function() exec_lua [[ - vim.api.nvim_set_keymap ('n', 'aa', '', {callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end, expr = true, replace_keycodes = true }) + vim.api.nvim_set_keymap('n', 'aa', '', {callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end, expr = true, replace_keycodes = true }) ]] feed('aa') @@ -877,7 +913,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('can make lua expr mappings without replacing keycodes', function() exec_lua [[ - vim.api.nvim_set_keymap ('i', 'aa', '', {callback = function() return '<space>' end, expr = true }) + vim.api.nvim_set_keymap('i', 'aa', '', {callback = function() return '<space>' end, expr = true }) ]] feed('iaa<esc>') @@ -887,7 +923,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('lua expr mapping returning nil is equivalent to returning an empty string', function() exec_lua [[ - vim.api.nvim_set_keymap ('i', 'aa', '', {callback = function() return nil end, expr = true }) + vim.api.nvim_set_keymap('i', 'aa', '', {callback = function() return nil end, expr = true }) ]] feed('iaa<esc>') @@ -898,17 +934,29 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('does not reset pum in lua mapping', function() eq(0, exec_lua [[ VisibleCount = 0 - vim.api.nvim_set_keymap ('i', '<F2>', '', {callback = function() VisibleCount = VisibleCount + vim.fn.pumvisible() end}) + vim.api.nvim_set_keymap('i', '<F2>', '', {callback = function() VisibleCount = VisibleCount + vim.fn.pumvisible() end}) return VisibleCount ]]) feed('i<C-X><C-V><F2><F2><esc>') eq(2, exec_lua[[return VisibleCount]]) end) + it('redo of lua mappings in op-pending mode work', function() + eq(0, exec_lua [[ + OpCount = 0 + vim.api.nvim_set_keymap('o', '<F2>', '', {callback = function() OpCount = OpCount + 1 end}) + return OpCount + ]]) + feed('d<F2>') + eq(1, exec_lua[[return OpCount]]) + feed('.') + eq(2, exec_lua[[return OpCount]]) + end) + it('can overwrite lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) @@ -917,7 +965,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() eq(1, exec_lua[[return GlobalCount]]) exec_lua [[ - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount - 1 end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount - 1 end }) ]] feed('asdf\n') @@ -928,7 +976,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() it('can unmap lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_set_keymap ('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_set_keymap('n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) @@ -973,6 +1021,54 @@ describe('nvim_set_keymap, nvim_del_keymap', function() eq("\nn lhs rhs\n map description", helpers.exec_capture("nmap lhs")) end) + + it('can define !-mode abbreviations with lua callbacks', function() + exec_lua [[ + GlobalCount = 0 + vim.api.nvim_set_keymap('!a', 'foo', '', {expr = true, callback = function() + GlobalCount = GlobalCount + 1 + return tostring(GlobalCount) + end}) + ]] + + feed 'iThe foo and the bar and the foo again<esc>' + eq('The 1 and the bar and the 2 again', meths.get_current_line()) + + feed ':let x = "The foo is the one"<cr>' + eq('The 3 is the one', meths.eval'x') + end) + + it('can define insert mode abbreviations with lua callbacks', function() + exec_lua [[ + GlobalCount = 0 + vim.api.nvim_set_keymap('ia', 'foo', '', {expr = true, callback = function() + GlobalCount = GlobalCount + 1 + return tostring(GlobalCount) + end}) + ]] + + feed 'iThe foo and the bar and the foo again<esc>' + eq('The 1 and the bar and the 2 again', meths.get_current_line()) + + feed ':let x = "The foo is the one"<cr>' + eq('The foo is the one', meths.eval'x') + end) + + it('can define cmdline mode abbreviations with lua callbacks', function() + exec_lua [[ + GlobalCount = 0 + vim.api.nvim_set_keymap('ca', 'foo', '', {expr = true, callback = function() + GlobalCount = GlobalCount + 1 + return tostring(GlobalCount) + end}) + ]] + + feed 'iThe foo and the bar and the foo again<esc>' + eq('The foo and the bar and the foo again', meths.get_current_line()) + + feed ':let x = "The foo is the one"<cr>' + eq('The 1 is the one', meths.eval'x') + end) end) describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() @@ -1074,7 +1170,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() it('can make lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_buf_set_keymap (0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) @@ -1085,7 +1181,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() it('can make lua expr mappings replacing keycodes', function() exec_lua [[ - vim.api.nvim_buf_set_keymap (0, 'n', 'aa', '', {callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end, expr = true, replace_keycodes = true }) + vim.api.nvim_buf_set_keymap(0, 'n', 'aa', '', {callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end, expr = true, replace_keycodes = true }) ]] feed('aa') @@ -1095,7 +1191,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() it('can make lua expr mappings without replacing keycodes', function() exec_lua [[ - vim.api.nvim_buf_set_keymap (0, 'i', 'aa', '', {callback = function() return '<space>' end, expr = true }) + vim.api.nvim_buf_set_keymap(0, 'i', 'aa', '', {callback = function() return '<space>' end, expr = true }) ]] feed('iaa<esc>') @@ -1107,7 +1203,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() it('can overwrite lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_buf_set_keymap (0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) @@ -1116,7 +1212,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() eq(1, exec_lua[[return GlobalCount]]) exec_lua [[ - vim.api.nvim_buf_set_keymap (0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount - 1 end }) + vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount - 1 end }) ]] feed('asdf\n') @@ -1127,7 +1223,7 @@ describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function() it('can unmap lua mappings', function() eq(0, exec_lua [[ GlobalCount = 0 - vim.api.nvim_buf_set_keymap (0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) + vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {callback = function() GlobalCount = GlobalCount + 1 end }) return GlobalCount ]]) diff --git a/test/functional/api/proc_spec.lua b/test/functional/api/proc_spec.lua index 2028a8fba5..20edea3feb 100644 --- a/test/functional/api/proc_spec.lua +++ b/test/functional/api/proc_spec.lua @@ -42,14 +42,14 @@ describe('API', function() end) end) - it('validates input', function() + it('validation', function() local status, rv = pcall(request, "nvim_get_proc_children", -1) eq(false, status) - eq("Invalid pid: -1", string.match(rv, "Invalid.*")) + eq("Invalid 'pid': -1", string.match(rv, "Invalid.*")) status, rv = pcall(request, "nvim_get_proc_children", 0) eq(false, status) - eq("Invalid pid: 0", string.match(rv, "Invalid.*")) + eq("Invalid 'pid': 0", string.match(rv, "Invalid.*")) -- Assume PID 99999 does not exist. status, rv = pcall(request, "nvim_get_proc_children", 99999) @@ -68,14 +68,14 @@ describe('API', function() neq(pid, pinfo.ppid) end) - it('validates input', function() + it('validation', function() local status, rv = pcall(request, "nvim_get_proc", -1) eq(false, status) - eq("Invalid pid: -1", string.match(rv, "Invalid.*")) + eq("Invalid 'pid': -1", string.match(rv, "Invalid.*")) status, rv = pcall(request, "nvim_get_proc", 0) eq(false, status) - eq("Invalid pid: 0", string.match(rv, "Invalid.*")) + eq("Invalid 'pid': 0", string.match(rv, "Invalid.*")) -- Assume PID 99999 does not exist. status, rv = pcall(request, "nvim_get_proc", 99999) diff --git a/test/functional/api/rpc_fixture.lua b/test/functional/api/rpc_fixture.lua index 94df751363..c860a6da59 100644 --- a/test/functional/api/rpc_fixture.lua +++ b/test/functional/api/rpc_fixture.lua @@ -4,9 +4,8 @@ package.path = arg[1] package.cpath = arg[2] -local mpack = require('mpack') -local StdioStream = require('nvim.stdio_stream') -local Session = require('nvim.session') +local StdioStream = require'test.client.uv_stream'.StdioStream +local Session = require'test.client.session' local stdio_stream = StdioStream.open() local session = Session.new(stdio_stream) @@ -19,7 +18,7 @@ local function on_request(method, args) return "done!" elseif method == "exit" then session:stop() - return mpack.NIL + return vim.NIL end end diff --git a/test/functional/api/server_notifications_spec.lua b/test/functional/api/server_notifications_spec.lua index 53642858b2..bc43f6564d 100644 --- a/test/functional/api/server_notifications_spec.lua +++ b/test/functional/api/server_notifications_spec.lua @@ -6,9 +6,7 @@ local eq, clear, eval, command, nvim, next_msg = local meths = helpers.meths local exec_lua = helpers.exec_lua local retry = helpers.retry -local is_ci = helpers.is_ci local assert_alive = helpers.assert_alive -local skip = helpers.skip local testlog = 'Xtest-server-notify-log' @@ -90,7 +88,6 @@ describe('notify', function() end) it('cancels stale events on channel close', function() - skip(is_ci(), 'hangs on CI #14083 #15251') local catchan = eval("jobstart(['cat'], {'rpc': v:true})") local catpath = eval('exepath("cat")') eq({id=catchan, argv={catpath}, stream='job', mode='rpc', client = {}}, exec_lua ([[ diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index ceff390dc5..1ad4ad3a02 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -199,7 +199,7 @@ describe('server -> client', function() it('can communicate buffers, tabpages, and windows', function() eq({1}, eval("rpcrequest(vim, 'nvim_list_tabpages')")) - -- Window IDs start at 1000 (LOWEST_WIN_ID in vim.h) + -- Window IDs start at 1000 (LOWEST_WIN_ID in window.h) eq({1000}, eval("rpcrequest(vim, 'nvim_list_wins')")) local buf = eval("rpcrequest(vim, 'nvim_list_bufs')")[1] @@ -237,7 +237,7 @@ describe('server -> client', function() \ } ]]) meths.set_var("args", { - helpers.test_lua_prg, + nvim_prog, '-ll', 'test/functional/api/rpc_fixture.lua', package.path, package.cpath, @@ -337,6 +337,21 @@ describe('server -> client', function() eq('localhost:', string.sub(address,1,10)) connect_test(server, 'tcp', address) end) + + it('does not crash on receiving UI events', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverlist()[1] + local client = spawn(nvim_argv, false, nil, true) + set_session(client) + + local id = funcs.sockconnect('pipe', address, {rpc=true}) + funcs.rpcrequest(id, 'nvim_ui_attach', 80, 24, {}) + assert_alive() + + server:close() + client:close() + end) end) describe('connecting to its own pipe address', function() diff --git a/test/functional/api/ui_spec.lua b/test/functional/api/ui_spec.lua index 279cd1856d..6efb6726fe 100644 --- a/test/functional/api/ui_spec.lua +++ b/test/functional/api/ui_spec.lua @@ -1,8 +1,11 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') local clear = helpers.clear +local command = helpers.command local eq = helpers.eq local eval = helpers.eval +local exec = helpers.exec +local feed = helpers.feed local meths = helpers.meths local request = helpers.request local pcall_err = helpers.pcall_err @@ -11,17 +14,35 @@ describe('nvim_ui_attach()', function() before_each(function() clear() end) + it('handles very large width/height #2180', function() local screen = Screen.new(999, 999) screen:attach() eq(999, eval('&lines')) eq(999, eval('&columns')) end) - it('invalid option returns error', function() + + it('validation', function() eq('No such UI option: foo', pcall_err(meths.ui_attach, 80, 24, { foo={'foo'} })) - end) - it('validates channel arg', function() + + eq("Invalid 'ext_linegrid': expected Boolean, got Array", + pcall_err(meths.ui_attach, 80, 24, { ext_linegrid={} })) + eq("Invalid 'override': expected Boolean, got Array", + pcall_err(meths.ui_attach, 80, 24, { override={} })) + eq("Invalid 'rgb': expected Boolean, got Array", + pcall_err(meths.ui_attach, 80, 24, { rgb={} })) + eq("Invalid 'term_name': expected String, got Boolean", + pcall_err(meths.ui_attach, 80, 24, { term_name=true })) + eq("Invalid 'term_colors': expected Integer, got Boolean", + pcall_err(meths.ui_attach, 80, 24, { term_colors=true })) + eq("Invalid 'stdin_fd': expected Integer, got String", + pcall_err(meths.ui_attach, 80, 24, { stdin_fd='foo' })) + eq("Invalid 'stdin_tty': expected Boolean, got String", + pcall_err(meths.ui_attach, 80, 24, { stdin_tty='foo' })) + eq("Invalid 'stdout_tty': expected Boolean, got String", + pcall_err(meths.ui_attach, 80, 24, { stdout_tty='foo' })) + eq('UI not attached to channel: 1', pcall_err(request, 'nvim_ui_try_resize', 40, 10)) eq('UI not attached to channel: 1', @@ -37,14 +58,13 @@ describe('nvim_ui_attach()', function() end) it('autocmds UIEnter/UILeave', function() - clear{ - args_rm={'--headless'}, - args={ - '--cmd', 'let g:evs = []', - '--cmd', 'autocmd UIEnter * :call add(g:evs, "UIEnter") | let g:uienter_ev = deepcopy(v:event)', - '--cmd', 'autocmd UILeave * :call add(g:evs, "UILeave") | let g:uileave_ev = deepcopy(v:event)', - '--cmd', 'autocmd VimEnter * :call add(g:evs, "VimEnter")', - }} + clear{args_rm={'--headless'}} + exec([[ + let g:evs = [] + autocmd UIEnter * call add(g:evs, "UIEnter") | let g:uienter_ev = deepcopy(v:event) + autocmd UILeave * call add(g:evs, "UILeave") | let g:uileave_ev = deepcopy(v:event) + autocmd VimEnter * call add(g:evs, "VimEnter") + ]]) local screen = Screen.new() screen:attach() eq({chan=1}, eval('g:uienter_ev')) @@ -56,3 +76,45 @@ it('autocmds UIEnter/UILeave', function() 'UILeave', }, eval('g:evs')) end) + +it('autocmds VimSuspend/VimResume #22041', function() + clear() + local screen = Screen.new() + screen:attach() + exec([[ + let g:ev = [] + autocmd VimResume * :call add(g:ev, 'r') + autocmd VimSuspend * :call add(g:ev, 's') + ]]) + + eq(false, screen.suspended) + feed('<C-Z>') + screen:expect(function() eq(true, screen.suspended) end) + eq({ 's' }, eval('g:ev')) + screen.suspended = false + feed('<Ignore>') + eq({ 's', 'r' }, eval('g:ev')) + + command('suspend') + screen:expect(function() eq(true, screen.suspended) end) + eq({ 's', 'r', 's' }, eval('g:ev')) + screen.suspended = false + meths.input_mouse('move', '', '', 0, 0, 0) + eq({ 's', 'r', 's', 'r' }, eval('g:ev')) + + feed('<C-Z><C-Z><C-Z>') + screen:expect(function() eq(true, screen.suspended) end) + meths.ui_set_focus(false) + eq({ 's', 'r', 's', 'r', 's' }, eval('g:ev')) + screen.suspended = false + meths.ui_set_focus(true) + eq({ 's', 'r', 's', 'r', 's', 'r' }, eval('g:ev')) + + command('suspend | suspend | suspend') + screen:expect(function() eq(true, screen.suspended) end) + screen:detach() + eq({ 's', 'r', 's', 'r', 's', 'r', 's' }, eval('g:ev')) + screen.suspended = false + screen:attach() + eq({ 's', 'r', 's', 'r', 's', 'r', 's', 'r' }, eval('g:ev')) +end) diff --git a/test/functional/api/version_spec.lua b/test/functional/api/version_spec.lua index 771192e9ab..6d466b0cc1 100644 --- a/test/functional/api/version_spec.lua +++ b/test/functional/api/version_spec.lua @@ -34,6 +34,7 @@ describe("api_info()['version']", function() local minor = version['minor'] local patch = version['patch'] local prerelease = version['prerelease'] + local build = version['build'] eq("number", type(major)) eq("number", type(minor)) eq("number", type(patch)) @@ -42,6 +43,7 @@ describe("api_info()['version']", function() eq(0, funcs.has("nvim-"..major.."."..minor.."."..(patch + 1))) eq(0, funcs.has("nvim-"..major.."."..(minor + 1).."."..patch)) eq(0, funcs.has("nvim-"..(major + 1).."."..minor.."."..patch)) + assert(build == nil or type(build) == 'string') end) end) diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 8fcdd9620b..8bbadda9b0 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -1,6 +1,5 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') -local lfs = require('lfs') local luv = require('luv') local fmt = string.format @@ -9,6 +8,7 @@ local NIL = helpers.NIL local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq local command = helpers.command local exec = helpers.exec +local exec_capture = helpers.exec_capture local eval = helpers.eval local expect = helpers.expect local funcs = helpers.funcs @@ -28,7 +28,6 @@ local write_file = helpers.write_file local exec_lua = helpers.exec_lua local exc_exec = helpers.exc_exec local insert = helpers.insert -local expect_exit = helpers.expect_exit local skip = helpers.skip local pcall_err = helpers.pcall_err @@ -59,7 +58,7 @@ describe('API', function() -- XXX: This must be the last one, else next one will fail: -- "Packer instance already working. Use another Packer ..." - matches("can't serialize object$", + matches("can't serialize object of type .$", pcall_err(request, nil)) end) @@ -89,132 +88,145 @@ describe('API', function() eq({mode='i', blocking=false}, nvim("get_mode")) end) - describe('nvim_exec', function() + describe('nvim_exec2', function() + it('always returns table', function() + -- In built version this results into `vim.empty_dict()` + eq({}, nvim('exec2', 'echo "Hello"', {})) + eq({}, nvim('exec2', 'echo "Hello"', { output = false })) + eq({ output = 'Hello' }, nvim('exec2', 'echo "Hello"', { output = true })) + end) + + it('default options', function() + -- Should be equivalent to { output = false } + nvim('exec2', "let x0 = 'a'", {}) + eq('a', nvim('get_var', 'x0')) + end) + it('one-line input', function() - nvim('exec', "let x1 = 'a'", false) + nvim('exec2', "let x1 = 'a'", { output = false }) eq('a', nvim('get_var', 'x1')) end) it(':verbose set {option}?', function() - nvim('exec', 'set nowrap', false) - eq('nowrap\n\tLast set from anonymous :source', - nvim('exec', 'verbose set wrap?', true)) + nvim('exec2', 'set nowrap', { output = false }) + eq({ output = 'nowrap\n\tLast set from anonymous :source' }, + nvim('exec2', 'verbose set wrap?', { output = true })) -- Using script var to force creation of a script item - nvim('exec', [[ + nvim('exec2', [[ let s:a = 1 set nowrap - ]], false) - eq('nowrap\n\tLast set from anonymous :source (script id 1)', - nvim('exec', 'verbose set wrap?', true)) + ]], { output = false }) + eq({ output = 'nowrap\n\tLast set from anonymous :source (script id 1)' }, + nvim('exec2', 'verbose set wrap?', { output = true })) end) it('multiline input', function() -- Heredoc + empty lines. - nvim('exec', "let x2 = 'a'\n", false) + nvim('exec2', "let x2 = 'a'\n", { output = false }) eq('a', nvim('get_var', 'x2')) - nvim('exec','lua <<EOF\n\n\n\ny=3\n\n\nEOF', false) + nvim('exec2','lua <<EOF\n\n\n\ny=3\n\n\nEOF', { output = false }) eq(3, nvim('eval', "luaeval('y')")) - eq('', nvim('exec', 'lua <<EOF\ny=3\nEOF', false)) + eq({}, nvim('exec2', 'lua <<EOF\ny=3\nEOF', { output = false })) eq(3, nvim('eval', "luaeval('y')")) -- Multiple statements - nvim('exec', 'let x1=1\nlet x2=2\nlet x3=3\n', false) + nvim('exec2', 'let x1=1\nlet x2=2\nlet x3=3\n', { output = false }) eq(1, nvim('eval', 'x1')) eq(2, nvim('eval', 'x2')) eq(3, nvim('eval', 'x3')) -- Functions - nvim('exec', 'function Foo()\ncall setline(1,["xxx"])\nendfunction', false) + nvim('exec2', 'function Foo()\ncall setline(1,["xxx"])\nendfunction', { output = false }) eq('', nvim('get_current_line')) - nvim('exec', 'call Foo()', false) + nvim('exec2', 'call Foo()', { output = false }) eq('xxx', nvim('get_current_line')) -- Autocmds - nvim('exec','autocmd BufAdd * :let x1 = "Hello"', false) + nvim('exec2','autocmd BufAdd * :let x1 = "Hello"', { output = false }) nvim('command', 'new foo') eq('Hello', request('nvim_eval', 'g:x1')) -- Line continuations - nvim('exec', [[ + nvim('exec2', [[ let abc = #{ \ a: 1, "\ b: 2, \ c: 3 - \ }]], false) + \ }]], { output = false }) eq({a = 1, c = 3}, request('nvim_eval', 'g:abc')) -- try no spaces before continuations to catch off-by-one error - nvim('exec', 'let ab = #{\n\\a: 98,\n"\\ b: 2\n\\}', false) + nvim('exec2', 'let ab = #{\n\\a: 98,\n"\\ b: 2\n\\}', { output = false }) eq({a = 98}, request('nvim_eval', 'g:ab')) -- Script scope (s:) - eq('ahoy! script-scoped varrrrr', nvim('exec', [[ + eq({ output = 'ahoy! script-scoped varrrrr' }, nvim('exec2', [[ let s:pirate = 'script-scoped varrrrr' function! s:avast_ye_hades(s) abort return a:s .. ' ' .. s:pirate endfunction echo <sid>avast_ye_hades('ahoy!') - ]], true)) + ]], { output = true })) - eq('ahoy! script-scoped varrrrr', nvim('exec', [[ + eq({ output = "{'output': 'ahoy! script-scoped varrrrr'}" }, nvim('exec2', [[ let s:pirate = 'script-scoped varrrrr' function! Avast_ye_hades(s) abort return a:s .. ' ' .. s:pirate endfunction - echo nvim_exec('echo Avast_ye_hades(''ahoy!'')', 1) - ]], true)) + echo nvim_exec2('echo Avast_ye_hades(''ahoy!'')', {'output': v:true}) + ]], { output = true })) matches('Vim%(echo%):E121: Undefined variable: s:pirate$', - pcall_err(request, 'nvim_exec', [[ + pcall_err(request, 'nvim_exec2', [[ let s:pirate = 'script-scoped varrrrr' - call nvim_exec('echo s:pirate', 1) - ]], false)) + call nvim_exec2('echo s:pirate', {'output': v:true}) + ]], { output = false })) -- Script items are created only on script var access - eq('1\n0', nvim('exec', [[ + eq({ output = '1\n0' }, nvim('exec2', [[ echo expand("<SID>")->empty() let s:a = 123 echo expand("<SID>")->empty() - ]], true)) + ]], { output = true })) - eq('1\n0', nvim('exec', [[ + eq({ output = '1\n0' }, nvim('exec2', [[ echo expand("<SID>")->empty() function s:a() abort endfunction echo expand("<SID>")->empty() - ]], true)) + ]], { output = true })) end) it('non-ASCII input', function() - nvim('exec', [=[ + nvim('exec2', [=[ new exe "normal! i ax \n Ax " :%s/ax/--a1234--/g | :%s/Ax/--A1234--/g - ]=], false) + ]=], { output = false }) nvim('command', '1') eq(' --a1234-- ', nvim('get_current_line')) nvim('command', '2') eq(' --A1234-- ', nvim('get_current_line')) - nvim('exec', [[ + nvim('exec2', [[ new call setline(1,['xxx']) call feedkeys('r') call feedkeys('ñ', 'xt') - ]], false) + ]], { output = false }) eq('ñxx', nvim('get_current_line')) end) it('execution error', function() - eq('nvim_exec(): Vim:E492: Not an editor command: bogus_command', - pcall_err(request, 'nvim_exec', 'bogus_command', false)) + eq('nvim_exec2(): Vim:E492: Not an editor command: bogus_command', + pcall_err(request, 'nvim_exec2', 'bogus_command', {})) eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated. eq('', eval('v:exception')) - eq('nvim_exec(): Vim(buffer):E86: Buffer 23487 does not exist', - pcall_err(request, 'nvim_exec', 'buffer 23487', false)) + eq('nvim_exec2(): Vim(buffer):E86: Buffer 23487 does not exist', + pcall_err(request, 'nvim_exec2', 'buffer 23487', {})) eq('', eval('v:errmsg')) -- v:errmsg was not updated. eq('', eval('v:exception')) end) @@ -222,17 +234,17 @@ describe('API', function() it('recursion', function() local fname = tmpname() write_file(fname, 'let x1 = "set from :source file"\n') - -- nvim_exec + -- nvim_exec2 -- :source - -- nvim_exec - request('nvim_exec', [[ + -- nvim_exec2 + request('nvim_exec2', [[ let x2 = substitute('foo','o','X','g') let x4 = 'should be overwritten' - call nvim_exec("source ]]..fname..[[\nlet x3 = substitute('foo','foo','set by recursive nvim_exec','g')\nlet x5='overwritten'\nlet x4=x5\n", v:false) - ]], false) + call nvim_exec2("source ]]..fname..[[\nlet x3 = substitute('foo','foo','set by recursive nvim_exec2','g')\nlet x5='overwritten'\nlet x4=x5\n", {'output': v:false}) + ]], { output = false }) eq('set from :source file', request('nvim_get_var', 'x1')) eq('fXX', request('nvim_get_var', 'x2')) - eq('set by recursive nvim_exec', request('nvim_get_var', 'x3')) + eq('set by recursive nvim_exec2', request('nvim_get_var', 'x3')) eq('overwritten', request('nvim_get_var', 'x4')) eq('overwritten', request('nvim_get_var', 'x5')) os.remove(fname) @@ -242,35 +254,35 @@ describe('API', function() local fname = tmpname() write_file(fname, 'echo "hello"\n') local sourcing_fname = tmpname() - write_file(sourcing_fname, 'call nvim_exec("source '..fname..'", v:false)\n') - meths.exec('set verbose=2', false) + write_file(sourcing_fname, 'call nvim_exec2("source '..fname..'", {"output": v:false})\n') + meths.exec2('set verbose=2', { output = false }) local traceback_output = 'line 0: sourcing "'..sourcing_fname..'"\n'.. 'line 0: sourcing "'..fname..'"\n'.. 'hello\n'.. 'finished sourcing '..fname..'\n'.. - 'continuing in nvim_exec() called at '..sourcing_fname..':1\n'.. + 'continuing in nvim_exec2() called at '..sourcing_fname..':1\n'.. 'finished sourcing '..sourcing_fname..'\n'.. - 'continuing in nvim_exec() called at nvim_exec():0' - eq(traceback_output, - meths.exec('call nvim_exec("source '..sourcing_fname..'", v:false)', true)) + 'continuing in nvim_exec2() called at nvim_exec2():0' + eq({ output = traceback_output }, + meths.exec2('call nvim_exec2("source '..sourcing_fname..'", {"output": v:false})', { output = true })) os.remove(fname) os.remove(sourcing_fname) end) it('returns output', function() - eq('this is spinal tap', - nvim('exec', 'lua <<EOF\n\n\nprint("this is spinal tap")\n\n\nEOF', true)) - eq('', nvim('exec', 'echo', true)) - eq('foo 42', nvim('exec', 'echo "foo" 42', true)) + eq({ output = 'this is spinal tap' }, + nvim('exec2', 'lua <<EOF\n\n\nprint("this is spinal tap")\n\n\nEOF', { output = true })) + eq({ output = '' }, nvim('exec2', 'echo', { output = true })) + eq({ output = 'foo 42' }, nvim('exec2', 'echo "foo" 42', { output = true })) end) - it('displays messages when output=false', function() + it('displays messages when opts.output=false', function() local screen = Screen.new(40, 8) screen:attach() screen:set_default_attr_ids({ [0] = {bold=true, foreground=Screen.colors.Blue}, }) - meths.exec("echo 'hello'", false) + meths.exec2("echo 'hello'", { output = false }) screen:expect{grid=[[ ^ | {0:~ }| @@ -289,7 +301,7 @@ describe('API', function() screen:set_default_attr_ids({ [0] = {bold=true, foreground=Screen.colors.Blue}, }) - meths.exec("echo 'hello'", true) + meths.exec2("echo 'hello'", { output = true }) screen:expect{grid=[[ ^ | {0:~ }| @@ -300,7 +312,7 @@ describe('API', function() ]]} exec([[ func Print() - call nvim_exec('echo "hello"', v:true) + call nvim_exec2('echo "hello"', { 'output': v:true }) endfunc ]]) feed([[:echon 1 | call Print() | echon 5<CR>]]) @@ -322,8 +334,7 @@ describe('API', function() nvim('command', 'edit '..fname) nvim('command', 'normal itesting\napi') nvim('command', 'w') - local f = io.open(fname) - ok(f ~= nil) + local f = assert(io.open(fname)) if is_os('win') then eq('testing\r\napi\r\n', f:read('*a')) else @@ -333,21 +344,27 @@ describe('API', function() os.remove(fname) end) - it('VimL validation error: fails with specific error', function() + it('Vimscript validation error: fails with specific error', function() local status, rv = pcall(nvim, "command", "bogus_command") eq(false, status) -- nvim_command() failed. - eq("E492:", string.match(rv, "E%d*:")) -- VimL error was returned. + eq("E492:", string.match(rv, "E%d*:")) -- Vimscript error was returned. eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated. eq('', eval('v:exception')) end) - it('VimL execution error: fails with specific error', function() + it('Vimscript execution error: fails with specific error', function() local status, rv = pcall(nvim, "command", "buffer 23487") eq(false, status) -- nvim_command() failed. eq("E86: Buffer 23487 does not exist", string.match(rv, "E%d*:.*")) eq('', eval('v:errmsg')) -- v:errmsg was not updated. eq('', eval('v:exception')) end) + + it('gives E493 instead of prompting on backwards range', function() + command('split') + eq('Vim(windo):E493: Backwards range given: 2,1windo echo', + pcall_err(command, '2,1windo echo')) + end) end) describe('nvim_command_output', function() @@ -403,7 +420,7 @@ describe('API', function() eq(':!echo foo\r\n\nfoo'..win_lf..'\n', nvim('command_output', [[!echo foo]])) end) - it('VimL validation error: fails with specific error', function() + it('Vimscript validation error: fails with specific error', function() local status, rv = pcall(nvim, "command_output", "bogus commannnd") eq(false, status) -- nvim_command_output() failed. eq("E492: Not an editor command: bogus commannnd", @@ -413,7 +430,7 @@ describe('API', function() eq({mode='n', blocking=false}, nvim("get_mode")) end) - it('VimL execution error: fails with specific error', function() + it('Vimscript execution error: fails with specific error', function() local status, rv = pcall(nvim, "command_output", "buffer 42") eq(false, status) -- nvim_command_output() failed. eq("E86: Buffer 42 does not exist", string.match(rv, "E%d*:.*")) @@ -422,7 +439,7 @@ describe('API', function() eq({mode='n', blocking=false}, nvim("get_mode")) end) - it('Does not cause heap buffer overflow with large output', function() + it('does not cause heap buffer overflow with large output', function() eq(eval('string(range(1000000))'), nvim('command_output', 'echo range(1000000)')) end) @@ -444,7 +461,7 @@ describe('API', function() eq(2, request("vim_eval", "1+1")) end) - it("VimL error: returns error details, does NOT update v:errmsg", function() + it("Vimscript error: returns error details, does NOT update v:errmsg", function() eq('Vim:E121: Undefined variable: bogus', pcall_err(request, 'nvim_eval', 'bogus expression')) eq('', eval('v:errmsg')) -- v:errmsg was not updated. @@ -459,7 +476,7 @@ describe('API', function() eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'})) end) - it("VimL validation error: returns specific error, does NOT update v:errmsg", function() + it("Vimscript validation error: returns specific error, does NOT update v:errmsg", function() eq('Vim:E117: Unknown function: bogus function', pcall_err(request, 'nvim_call_function', 'bogus function', {'arg1'})) eq('Vim:E119: Not enough arguments for function: atan', @@ -468,7 +485,7 @@ describe('API', function() eq('', eval('v:errmsg')) -- v:errmsg was not updated. end) - it("VimL error: returns error details, does NOT update v:errmsg", function() + it("Vimscript error: returns error details, does NOT update v:errmsg", function() eq('Vim:E808: Number or Float required', pcall_err(request, 'nvim_call_function', 'atan', {'foo'})) eq('Vim:Invalid channel stream "xxx"', @@ -479,7 +496,7 @@ describe('API', function() eq('', eval('v:errmsg')) -- v:errmsg was not updated. end) - it("VimL exception: returns exception details, does NOT update v:errmsg", function() + it("Vimscript exception: returns exception details, does NOT update v:errmsg", function() source([[ function! Foo() abort throw 'wtf' @@ -490,7 +507,7 @@ describe('API', function() eq('', eval('v:errmsg')) -- v:errmsg was not updated. end) - it('validates args', function() + it('validation', function() local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' } source([[ function! Foo(...) abort @@ -504,7 +521,7 @@ describe('API', function() end) describe('nvim_call_dict_function', function() - it('invokes VimL dict function', function() + it('invokes Vimscript dict function', function() source([[ function! F(name) dict return self.greeting.', '.a:name.'!' @@ -532,7 +549,7 @@ describe('API', function() eq('@it works@', nvim('call_dict_function', { result = 'it works', G = 'G'}, 'G', {})) end) - it('validates args', function() + it('validation', function() command('let g:d={"baz":"zub","meep":[]}') eq('Not found: bogus', pcall_err(request, 'nvim_call_dict_function', 'g:d', 'bogus', {1,2})) @@ -559,7 +576,6 @@ describe('API', function() local start_dir before_each(function() - clear() funcs.mkdir("Xtestdir") start_dir = funcs.getcwd() end) @@ -575,7 +591,7 @@ describe('API', function() it('sets previous directory', function() meths.set_current_dir("Xtestdir") - meths.exec('cd -', false) + command('cd -') eq(funcs.getcwd(), start_dir) end) end) @@ -634,7 +650,7 @@ describe('API', function() end) describe('nvim_input', function() - it("VimL error: does NOT fail, updates v:errmsg", function() + it("Vimscript error: does NOT fail, updates v:errmsg", function() local status, _ = pcall(nvim, "input", ":call bogus_fn()<CR>") local v_errnum = string.match(nvim("eval", "v:errmsg"), "E%d*:") eq(true, status) -- nvim_input() did not fail. @@ -648,10 +664,10 @@ describe('API', function() end) describe('nvim_paste', function() - it('validates args', function() - eq('Invalid phase: -2', + it('validation', function() + eq("Invalid 'phase': -2", pcall_err(request, 'nvim_paste', 'foo', true, -2)) - eq('Invalid phase: 4', + eq("Invalid 'phase': 4", pcall_err(request, 'nvim_paste', 'foo', true, 4)) end) local function run_streamed_paste_tests() @@ -1032,7 +1048,7 @@ describe('API', function() line 3 ]]) eq({0,4,1,0}, funcs.getpos('.')) -- Cursor follows the paste. - eq(false, nvim('get_option', 'paste')) + eq(false, nvim('get_option_value', 'paste', {})) command('%delete _') -- Without final "\n". nvim('paste', 'line 1\nline 2\nline 3', true, -1) @@ -1072,7 +1088,7 @@ describe('API', function() nvim('paste', 'line 1\r\n\r\rline 2\nline 3\rline 4\r', true, -1) expect('line 1\n\n\nline 2\nline 3\nline 4\n') eq({0,7,1,0}, funcs.getpos('.')) - eq(false, nvim('get_option', 'paste')) + eq(false, nvim('get_option_value', 'paste', {})) end) it('Replace-mode', function() -- Within single line @@ -1154,10 +1170,10 @@ describe('API', function() end) describe('nvim_put', function() - it('validates args', function() - eq('Invalid lines (expected array of strings)', + it('validation', function() + eq("Invalid 'line': expected String, got Integer", pcall_err(request, 'nvim_put', {42}, 'l', false, false)) - eq("Invalid type: 'x'", + eq("Invalid 'type': 'x'", pcall_err(request, 'nvim_put', {'foo'}, 'x', false, false)) end) it("fails if 'nomodifiable'", function() @@ -1259,9 +1275,9 @@ describe('API', function() yyybc line 2 line 3 ]]) - eq("Invalid type: 'bx'", + eq("Invalid 'type': 'bx'", pcall_err(meths.put, {'xxx', 'yyy'}, 'bx', false, true)) - eq("Invalid type: 'b3x'", + eq("Invalid 'type': 'b3x'", pcall_err(meths.put, {'xxx', 'yyy'}, 'b3x', false, true)) end) end) @@ -1288,6 +1304,11 @@ describe('API', function() end) describe('set/get/del variables', function() + it('validation', function() + eq('Key not found: bogus', pcall_err(meths.get_var, 'bogus')) + eq('Key not found: bogus', pcall_err(meths.del_var, 'bogus')) + end) + it('nvim_get_var, nvim_set_var, nvim_del_var', function() nvim('set_var', 'lua', {1, 2, {['3'] = 1}}) eq({1, 2, {['3'] = 1}}, nvim('get_var', 'lua')) @@ -1329,12 +1350,59 @@ describe('API', function() end) it('nvim_get_vvar, nvim_set_vvar', function() - -- Set readonly v: var. - eq('Key is read-only: count', - pcall_err(request, 'nvim_set_vvar', 'count', 42)) - -- Set writable v: var. + eq('Key is read-only: count', pcall_err(request, 'nvim_set_vvar', 'count', 42)) + eq('Dictionary is locked', pcall_err(request, 'nvim_set_vvar', 'nosuchvar', 42)) meths.set_vvar('errmsg', 'set by API') eq('set by API', meths.get_vvar('errmsg')) + meths.set_vvar('errmsg', 42) + eq('42', eval('v:errmsg')) + meths.set_vvar('oldfiles', { 'one', 'two' }) + eq({ 'one', 'two' }, eval('v:oldfiles')) + meths.set_vvar('oldfiles', {}) + eq({}, eval('v:oldfiles')) + eq('Setting v:oldfiles to value with wrong type', pcall_err(meths.set_vvar, 'oldfiles', 'a')) + eq({}, eval('v:oldfiles')) + + feed('i foo foo foo<Esc>0/foo<CR>') + eq({1, 1}, meths.win_get_cursor(0)) + eq(1, eval('v:searchforward')) + feed('n') + eq({1, 5}, meths.win_get_cursor(0)) + meths.set_vvar('searchforward', 0) + eq(0, eval('v:searchforward')) + feed('n') + eq({1, 1}, meths.win_get_cursor(0)) + meths.set_vvar('searchforward', 1) + eq(1, eval('v:searchforward')) + feed('n') + eq({1, 5}, meths.win_get_cursor(0)) + + local screen = Screen.new(60, 3) + screen:set_default_attr_ids({ + [0] = {bold = true, foreground = Screen.colors.Blue}, + [1] = {background = Screen.colors.Yellow}, + }) + screen:attach() + eq(1, eval('v:hlsearch')) + screen:expect{grid=[[ + {1:foo} {1:^foo} {1:foo} | + {0:~ }| + | + ]]} + meths.set_vvar('hlsearch', 0) + eq(0, eval('v:hlsearch')) + screen:expect{grid=[[ + foo ^foo foo | + {0:~ }| + | + ]]} + meths.set_vvar('hlsearch', 1) + eq(1, eval('v:hlsearch')) + screen:expect{grid=[[ + {1:foo} {1:^foo} {1:foo} | + {0:~ }| + | + ]]} end) it('vim_set_var returns the old value', function() @@ -1358,56 +1426,71 @@ describe('API', function() end) end) - describe('nvim_get_option, nvim_set_option', function() + describe('nvim_get_option_value, nvim_set_option_value', function() it('works', function() - ok(nvim('get_option', 'equalalways')) - nvim('set_option', 'equalalways', false) - ok(not nvim('get_option', 'equalalways')) + ok(nvim('get_option_value', 'equalalways', {})) + nvim('set_option_value', 'equalalways', false, {}) + ok(not nvim('get_option_value', 'equalalways', {})) end) it('works to get global value of local options', function() - eq(false, nvim('get_option', 'lisp')) - eq(8, nvim('get_option', 'shiftwidth')) + eq(false, nvim('get_option_value', 'lisp', {})) + eq(8, nvim('get_option_value', 'shiftwidth', {})) end) it('works to set global value of local options', function() - nvim('set_option', 'lisp', true) - eq(true, nvim('get_option', 'lisp')) - eq(false, helpers.curbuf('get_option', 'lisp')) + nvim('set_option_value', 'lisp', true, {scope='global'}) + eq(true, nvim('get_option_value', 'lisp', {scope='global'})) + eq(false, nvim('get_option_value', 'lisp', {})) eq(nil, nvim('command_output', 'setglobal lisp?'):match('nolisp')) eq('nolisp', nvim('command_output', 'setlocal lisp?'):match('nolisp')) - nvim('set_option', 'shiftwidth', 20) + nvim('set_option_value', 'shiftwidth', 20, {scope='global'}) eq('20', nvim('command_output', 'setglobal shiftwidth?'):match('%d+')) eq('8', nvim('command_output', 'setlocal shiftwidth?'):match('%d+')) end) - it('most window-local options have no global value', function() - local status, err = pcall(nvim, 'get_option', 'foldcolumn') - eq(false, status) - ok(err:match('Invalid option name') ~= nil) - end) - it('updates where the option was last set from', function() - nvim('set_option', 'equalalways', false) + nvim('set_option_value', 'equalalways', false, {}) local status, rv = pcall(nvim, 'command_output', 'verbose set equalalways?') eq(true, status) ok(nil ~= string.find(rv, 'noequalalways\n'.. '\tLast set from API client %(channel id %d+%)')) - nvim('exec_lua', 'vim.api.nvim_set_option("equalalways", true)', {}) + nvim('exec_lua', 'vim.api.nvim_set_option_value("equalalways", true, {})', {}) status, rv = pcall(nvim, 'command_output', 'verbose set equalalways?') eq(true, status) eq(' equalalways\n\tLast set from Lua', rv) end) - end) - describe('nvim_get_option_value, nvim_set_option_value', function() - it('works', function() - ok(nvim('get_option_value', 'equalalways', {})) - nvim('set_option_value', 'equalalways', false, {}) - ok(not nvim('get_option_value', 'equalalways', {})) + it('updates whether the option has ever been set #25025', function() + eq(false, nvim('get_option_info2', 'autochdir', {}).was_set) + nvim('set_option_value', 'autochdir', true, {}) + eq(true, nvim('get_option_info2', 'autochdir', {}).was_set) + + eq(false, nvim('get_option_info2', 'cmdwinheight', {}).was_set) + nvim('set_option_value', 'cmdwinheight', 10, {}) + eq(true, nvim('get_option_info2', 'cmdwinheight', {}).was_set) + + eq(false, nvim('get_option_info2', 'debug', {}).was_set) + nvim('set_option_value', 'debug', 'beep', {}) + eq(true, nvim('get_option_info2', 'debug', {}).was_set) + end) + + it('validation', function() + eq("Invalid 'scope': expected 'local' or 'global'", + pcall_err(nvim, 'get_option_value', 'scrolloff', {scope = 'bogus'})) + eq("Invalid 'scope': expected 'local' or 'global'", + pcall_err(nvim, 'set_option_value', 'scrolloff', 1, {scope = 'bogus'})) + eq("Invalid 'scope': expected String, got Integer", + pcall_err(nvim, 'get_option_value', 'scrolloff', {scope = 42})) + eq("Invalid 'value': expected valid option type, got Array", + pcall_err(nvim, 'set_option_value', 'scrolloff', {}, {})) + eq("Invalid value for option 'scrolloff': expected Number, got Boolean true", + pcall_err(nvim, 'set_option_value', 'scrolloff', true, {})) + eq("Invalid value for option 'scrolloff': expected Number, got String \"wrong\"", + pcall_err(nvim, 'set_option_value', 'scrolloff', 'wrong', {})) end) it('can get local values when global value is set', function() @@ -1453,19 +1536,22 @@ describe('API', function() -- Now try with options with a special "local is unset" value (e.g. 'undolevels') nvim('set_option_value', 'undolevels', 1000, {}) - eq(1000, nvim('get_option_value', 'undolevels', {scope = 'local'})) + nvim('set_option_value', 'undolevels', 1200, {scope = 'local'}) + eq(1200, nvim('get_option_value', 'undolevels', {scope = 'local'})) nvim('set_option_value', 'undolevels', NIL, {scope = 'local'}) eq(-123456, nvim('get_option_value', 'undolevels', {scope = 'local'})) + eq(1000, nvim('get_option_value', 'undolevels', {})) nvim('set_option_value', 'autoread', true, {}) - eq(true, nvim('get_option_value', 'autoread', {scope = 'local'})) + nvim('set_option_value', 'autoread', false, {scope = 'local'}) + eq(false, nvim('get_option_value', 'autoread', {scope = 'local'})) nvim('set_option_value', 'autoread', NIL, {scope = 'local'}) eq(NIL, nvim('get_option_value', 'autoread', {scope = 'local'})) + eq(true, nvim('get_option_value', 'autoread', {})) end) it('set window options', function() - -- Same as to nvim_win_set_option - nvim('set_option_value', 'colorcolumn', '4,3', {win=0}) + nvim('set_option_value', 'colorcolumn', '4,3', {}) eq('4,3', nvim('get_option_value', 'colorcolumn', {scope = 'local'})) command("set modified hidden") command("enew") -- edit new buffer, window option is preserved @@ -1473,7 +1559,6 @@ describe('API', function() end) it('set local window options', function() - -- Different to nvim_win_set_option nvim('set_option_value', 'colorcolumn', '4,3', {win=0, scope='local'}) eq('4,3', nvim('get_option_value', 'colorcolumn', {win = 0, scope = 'local'})) command("set modified hidden") @@ -1484,11 +1569,11 @@ describe('API', function() it('get buffer or window-local options', function() nvim('command', 'new') local buf = nvim('get_current_buf').id - nvim('buf_set_option', buf, 'tagfunc', 'foobar') + nvim('set_option_value', 'tagfunc', 'foobar', {buf=buf}) eq('foobar', nvim('get_option_value', 'tagfunc', {buf = buf})) local win = nvim('get_current_win').id - nvim('win_set_option', win, 'number', true) + nvim('set_option_value', 'number', true, {win=win}) eq(true, nvim('get_option_value', 'number', {win = win})) end) @@ -1502,6 +1587,40 @@ describe('API', function() nvim('get_option_value', 'filetype', {buf = buf}) eq({1, 9}, nvim('win_get_cursor', win)) end) + + it('can get default option values for filetypes', function() + command('filetype plugin on') + for ft, opts in pairs { + lua = { commentstring = '-- %s' }, + vim = { commentstring = '"%s' }, + man = { tagfunc = 'v:lua.require\'man\'.goto_tag' }, + xml = { formatexpr = 'xmlformat#Format()' } + } do + for option, value in pairs(opts) do + eq(value, nvim('get_option_value', option, { filetype = ft })) + end + end + + command'au FileType lua setlocal commentstring=NEW\\ %s' + + eq('NEW %s', nvim('get_option_value', 'commentstring', { filetype = 'lua' })) + end) + + it('errors for bad FileType autocmds', function() + command'au FileType lua setlocal commentstring=BAD' + eq([[FileType Autocommands for "lua": Vim(setlocal):E537: 'commentstring' must be empty or contain %s: commentstring=BAD]], + pcall_err(nvim, 'get_option_value', 'commentstring', { filetype = 'lua' })) + end) + + it("value of 'modified' is always false for scratch buffers", function() + nvim('set_current_buf', nvim('create_buf', true, true)) + insert([[ + foo + bar + baz + ]]) + eq(false, nvim('get_option_value', 'modified', {})) + end) end) describe('nvim_{get,set}_current_buf, nvim_list_bufs', function() @@ -1774,15 +1893,32 @@ describe('API', function() feed('<C-D>') expect('a') -- recognized i_0_CTRL-D end) + + it("does not interrupt with 'digraph'", function() + command('set digraph') + feed('i,') + eq(2, eval('1+1')) -- causes K_EVENT key + feed('<BS>') + eq(2, eval('1+1')) -- causes K_EVENT key + feed('.') + expect('…') -- digraph ",." worked + feed('<Esc>') + feed(':,') + eq(2, eval('1+1')) -- causes K_EVENT key + feed('<BS>') + eq(2, eval('1+1')) -- causes K_EVENT key + feed('.') + eq('…', funcs.getcmdline()) -- digraph ",." worked + end) end) describe('nvim_get_context', function() - it('validates args', function() + it('validation', function() eq("Invalid key: 'blah'", pcall_err(nvim, 'get_context', {blah={}})) - eq('invalid value for key: types', + eq("Invalid 'types': expected Array, got Integer", pcall_err(nvim, 'get_context', {types=42})) - eq('unexpected type: zub', + eq("Invalid 'type': 'zub'", pcall_err(nvim, 'get_context', {types={'jumps', 'zub', 'zam',}})) end) it('returns map of current editor state', function() @@ -1843,6 +1979,13 @@ describe('API', function() nvim('load_context', ctx) eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) end) + + it('errors when context dictionary is invalid', function() + eq('E474: Failed to convert list to msgpack string buffer', + pcall_err(nvim, 'load_context', { regs = { {} }, jumps = { {} } })) + eq("Empty dictionary keys aren't allowed", + pcall_err(nvim, 'load_context', { regs = { { [''] = '' } } })) + end) end) describe('nvim_replace_termcodes', function() @@ -1886,6 +2029,12 @@ describe('API', function() -- value. eq('', meths.replace_termcodes('', true, true, true)) end) + + -- Not exactly the case, as nvim_replace_termcodes() escapes K_SPECIAL in Unicode + it('translates the result of keytrans() on string with 0x80 byte back', function() + local s = 'ff\128\253\097tt' + eq(s, meths.replace_termcodes(funcs.keytrans(s), true, true, true)) + end) end) describe('nvim_feedkeys', function() @@ -1916,6 +2065,19 @@ describe('API', function() end) describe('nvim_out_write', function() + local screen + + before_each(function() + screen = Screen.new(40, 8) + screen:attach() + screen:set_default_attr_ids({ + [0] = {bold = true, foreground = Screen.colors.Blue}, + [1] = {bold = true, foreground = Screen.colors.SeaGreen}, + [2] = {bold = true, reverse = true}, + [3] = {foreground = Screen.colors.Blue}, + }) + end) + it('prints long messages correctly #20534', function() exec([[ set more @@ -1935,6 +2097,46 @@ describe('API', function() ]]) eq('\naaa\n' .. ('a'):rep(5002) .. '\naaa', meths.get_var('out')) end) + + it('blank line in message', function() + feed([[:call nvim_out_write("\na\n")<CR>]]) + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {2: }| + | + a | + {1:Press ENTER or type command to continue}^ | + ]]} + feed('<CR>') + feed([[:call nvim_out_write("b\n\nc\n")<CR>]]) + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {2: }| + b | + | + c | + {1:Press ENTER or type command to continue}^ | + ]]} + end) + + it('NUL bytes in message', function() + feed([[:lua vim.api.nvim_out_write('aaa\0bbb\0\0ccc\nddd\0\0\0eee\n')<CR>]]) + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {2: }| + aaa{3:^@}bbb{3:^@^@}ccc | + ddd{3:^@^@^@}eee | + {1:Press ENTER or type command to continue}^ | + ]]} + end) end) describe('nvim_err_write', function() @@ -2023,6 +2225,60 @@ describe('API', function() ]]) feed('<cr>') -- exit the press ENTER screen end) + + it('NUL bytes in message', function() + nvim_async('err_write', 'aaa\0bbb\0\0ccc\nddd\0\0\0eee\n') + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {3: }| + {1:aaa^@bbb^@^@ccc} | + {1:ddd^@^@^@eee} | + {2:Press ENTER or type command to continue}^ | + ]]} + end) + end) + + describe('nvim_err_writeln', function() + local screen + + before_each(function() + screen = Screen.new(40, 8) + screen:attach() + screen:set_default_attr_ids({ + [0] = {bold=true, foreground=Screen.colors.Blue}, + [1] = {foreground = Screen.colors.White, background = Screen.colors.Red}, + [2] = {bold = true, foreground = Screen.colors.SeaGreen}, + [3] = {bold = true, reverse = true}, + }) + end) + + it('shows only one return prompt after all lines are shown', function() + nvim_async('err_writeln', 'FAILURE\nERROR\nEXCEPTION\nTRACEBACK') + screen:expect([[ + | + {0:~ }| + {3: }| + {1:FAILURE} | + {1:ERROR} | + {1:EXCEPTION} | + {1:TRACEBACK} | + {2:Press ENTER or type command to continue}^ | + ]]) + feed('<CR>') + screen:expect([[ + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + end) end) describe('nvim_list_chans, nvim_get_chan_info', function() @@ -2115,7 +2371,7 @@ describe('API', function() it('stream=job :terminal channel', function() command(':terminal') eq({id=1}, meths.get_current_buf()) - eq(3, meths.buf_get_option(1, 'channel')) + eq(3, meths.get_option_value('channel', {buf=1})) local info = { stream='job', @@ -2223,15 +2479,14 @@ describe('API', function() eq(5, meths.get_var('avar')) end) - it('throws error on malformed arguments', function() + it('validation', function() local req = { {'nvim_set_var', {'avar', 1}}, {'nvim_set_var'}, {'nvim_set_var', {'avar', 2}}, } - local status, err = pcall(meths.call_atomic, req) - eq(false, status) - ok(err:match('Items in calls array must be arrays of size 2') ~= nil) + eq("Invalid 'calls' item: expected 2-item Array", + pcall_err(meths.call_atomic, req)) -- call before was done, but not after eq(1, meths.get_var('avar')) @@ -2239,18 +2494,16 @@ describe('API', function() { 'nvim_set_var', { 'bvar', { 2, 3 } } }, 12, } - status, err = pcall(meths.call_atomic, req) - eq(false, status) - ok(err:match('Items in calls array must be arrays') ~= nil) + eq("Invalid 'calls' item: expected Array, got Integer", + pcall_err(meths.call_atomic, req)) eq({2,3}, meths.get_var('bvar')) req = { {'nvim_set_current_line', 'little line'}, {'nvim_set_var', {'avar', 3}}, } - status, err = pcall(meths.call_atomic, req) - eq(false, status) - ok(err:match('Args must be Array') ~= nil) + eq("Invalid call args: expected Array, got String", + pcall_err(meths.call_atomic, req)) -- call before was done, but not after eq(1, meths.get_var('avar')) eq({''}, meths.buf_get_lines(0, 0, -1, true)) @@ -2271,45 +2524,45 @@ describe('API', function() end) it('returns nothing with empty &runtimepath', function() - meths.set_option('runtimepath', '') + meths.set_option_value('runtimepath', '', {}) eq({}, meths.list_runtime_paths()) end) it('returns single runtimepath', function() - meths.set_option('runtimepath', 'a') + meths.set_option_value('runtimepath', 'a', {}) eq({'a'}, meths.list_runtime_paths()) end) it('returns two runtimepaths', function() - meths.set_option('runtimepath', 'a,b') + meths.set_option_value('runtimepath', 'a,b', {}) eq({'a', 'b'}, meths.list_runtime_paths()) end) it('returns empty strings when appropriate', function() - meths.set_option('runtimepath', 'a,,b') + meths.set_option_value('runtimepath', 'a,,b', {}) eq({'a', '', 'b'}, meths.list_runtime_paths()) - meths.set_option('runtimepath', ',a,b') + meths.set_option_value('runtimepath', ',a,b', {}) eq({'', 'a', 'b'}, meths.list_runtime_paths()) -- Trailing "," is ignored. Use ",," if you really really want CWD. - meths.set_option('runtimepath', 'a,b,') + meths.set_option_value('runtimepath', 'a,b,', {}) eq({'a', 'b'}, meths.list_runtime_paths()) - meths.set_option('runtimepath', 'a,b,,') + meths.set_option_value('runtimepath', 'a,b,,', {}) eq({'a', 'b', ''}, meths.list_runtime_paths()) end) it('truncates too long paths', function() local long_path = ('/a'):rep(8192) - meths.set_option('runtimepath', long_path) + meths.set_option_value('runtimepath', long_path, {}) local paths_list = meths.list_runtime_paths() eq({}, paths_list) end) end) it('can throw exceptions', function() - local status, err = pcall(nvim, 'get_option', 'invalid-option') + local status, err = pcall(nvim, 'get_option_value', 'invalid-option', {}) eq(false, status) - ok(err:match('Invalid option name') ~= nil) + ok(err:match("Unknown option 'invalid%-option'") ~= nil) end) it('does not truncate error message <1 MB #5984', function() local very_long_name = 'A'..('x'):rep(10000)..'Z' - local status, err = pcall(nvim, 'get_option', very_long_name) + local status, err = pcall(nvim, 'get_option_value', very_long_name, {}) eq(false, status) eq(very_long_name, err:match('Ax+Z?')) end) @@ -2322,7 +2575,7 @@ describe('API', function() describe('nvim_parse_expression', function() before_each(function() - meths.set_option('isident', '') + meths.set_option_value('isident', '', {}) end) local function simplify_east_api_node(line, east_api_node) @@ -2536,20 +2789,26 @@ describe('API', function() { chan = 1, ext_cmdline = false, - ext_popupmenu = false, - ext_tabline = false, - ext_wildmenu = false, + ext_hlstate = false, ext_linegrid = screen._options.ext_linegrid or false, + ext_messages = false, ext_multigrid = false, - ext_hlstate = false, + ext_popupmenu = false, + ext_tabline = false, ext_termcolors = false, - ext_messages = false, + ext_wildmenu = false, height = 4, - rgb = true, override = true, + rgb = true, + stdin_tty = false, + stdout_tty = false, + term_background = '', + term_colors = 0, + term_name = '', width = 20, } } + eq(expected, nvim("list_uis")) screen:detach() @@ -2601,9 +2860,9 @@ describe('API', function() end) it('can change buftype before visiting', function() - meths.set_option("hidden", false) + meths.set_option_value("hidden", false, {}) eq({id=2}, meths.create_buf(true, false)) - meths.buf_set_option(2, "buftype", "nofile") + meths.set_option_value("buftype", "nofile", {buf=2}) meths.buf_set_lines(2, 0, -1, true, {"test text"}) command("split | buffer 2") eq({id=2}, meths.get_current_buf()) @@ -2622,6 +2881,18 @@ describe('API', function() eq(false, eval('g:fired')) end) + it('TextChanged and TextChangedI do not trigger without changes', function() + local buf = meths.create_buf(true, false) + command([[let g:changed = '']]) + meths.create_autocmd({'TextChanged', 'TextChangedI'}, { + buffer = buf, + command = 'let g:changed ..= mode()', + }) + meths.set_current_buf(buf) + feed('i') + eq('', meths.get_var('changed')) + end) + it('scratch-buffer', function() eq({id=2}, meths.create_buf(false, true)) eq({id=3}, meths.create_buf(true, true)) @@ -2630,7 +2901,7 @@ describe('API', function() eq(' 1 %a "[No Name]" line 1\n'.. ' 3 h "[Scratch]" line 0\n'.. ' 4 h "[Scratch]" line 0', - meths.exec('ls', true)) + exec_capture('ls')) -- current buffer didn't change eq({id=1}, meths.get_current_buf()) @@ -2646,10 +2917,10 @@ describe('API', function() local edited_buf = 2 meths.buf_set_lines(edited_buf, 0, -1, true, {"some text"}) for _,b in ipairs(scratch_bufs) do - eq('nofile', meths.buf_get_option(b, 'buftype')) - eq('hide', meths.buf_get_option(b, 'bufhidden')) - eq(false, meths.buf_get_option(b, 'swapfile')) - eq(false, meths.buf_get_option(b, 'modeline')) + eq('nofile', meths.get_option_value('buftype', {buf=b})) + eq('hide', meths.get_option_value('bufhidden', {buf=b})) + eq(false, meths.get_option_value('swapfile', {buf=b})) + eq(false, meths.get_option_value('modeline', {buf=b})) end -- @@ -2662,10 +2933,10 @@ describe('API', function() {1:~ }| | ]]) - eq('nofile', meths.buf_get_option(edited_buf, 'buftype')) - eq('hide', meths.buf_get_option(edited_buf, 'bufhidden')) - eq(false, meths.buf_get_option(edited_buf, 'swapfile')) - eq(false, meths.buf_get_option(edited_buf, 'modeline')) + eq('nofile', meths.get_option_value('buftype', {buf=edited_buf})) + eq('hide', meths.get_option_value('bufhidden', {buf=edited_buf})) + eq(false, meths.get_option_value('swapfile', {buf=edited_buf})) + eq(false, meths.get_option_value('modeline', {buf=edited_buf})) -- Scratch buffer can be wiped without error. command('bwipe') @@ -2679,7 +2950,9 @@ describe('API', function() it('does not cause heap-use-after-free on exit while setting options', function() command('au OptionSet * q') - expect_exit(command, 'silent! call nvim_create_buf(0, 1)') + command('silent! call nvim_create_buf(0, 1)') + -- nowadays this works because we don't execute any spurious autocmds at all #24824 + assert_alive() end) end) @@ -2744,13 +3017,13 @@ describe('API', function() end) it('should not crash when echoed', function() - meths.exec("echo nvim_get_all_options_info()", true) + meths.exec2("echo nvim_get_all_options_info()", { output = true }) end) end) describe('nvim_get_option_info', function() it('should error for unknown options', function() - eq("no such option: 'bogus'", pcall_err(meths.get_option_info, 'bogus')) + eq("Invalid option (not found): 'bogus'", pcall_err(meths.get_option_info, 'bogus')) end) it('should return the same options for short and long name', function() @@ -2796,7 +3069,7 @@ describe('API', function() it('should have information about global options', function() -- precondition: the option was changed from its default -- in test setup. - eq(false, meths.get_option'showcmd') + eq(false, meths.get_option_value('showcmd', {})) eq({ allows_duplicates = true, @@ -2813,6 +3086,117 @@ describe('API', function() type = "boolean", was_set = true }, meths.get_option_info'showcmd') + + meths.set_option_value('showcmd', true, {}) + + eq({ + allows_duplicates = true, + commalist = false, + default = true, + flaglist = false, + global_local = false, + last_set_chan = 1, + last_set_linenr = 0, + last_set_sid = -9, + name = "showcmd", + scope = "global", + shortname = "sc", + type = "boolean", + was_set = true + }, meths.get_option_info'showcmd') + end) + end) + + describe('nvim_get_option_info2', function() + local fname + local bufs + local wins + + before_each(function() + fname = tmpname() + write_file(fname, [[ + setglobal dictionary=mydict " 1, global-local (buffer) + setlocal formatprg=myprg " 2, global-local (buffer) + setglobal equalprg=prg1 " 3, global-local (buffer) + setlocal equalprg=prg2 " 4, global-local (buffer) + setglobal fillchars=stl:x " 5, global-local (window) + setlocal listchars=eol:c " 6, global-local (window) + setglobal showbreak=aaa " 7, global-local (window) + setlocal showbreak=bbb " 8, global-local (window) + setglobal completeopt=menu " 9, global + ]]) + + exec_lua 'vim.cmd.vsplit()' + meths.create_buf(false, false) + + bufs = meths.list_bufs() + wins = meths.list_wins() + + meths.win_set_buf(wins[1].id, bufs[1].id) + meths.win_set_buf(wins[2].id, bufs[2].id) + + meths.set_current_win(wins[2].id) + meths.exec('source ' .. fname, false) + + meths.set_current_win(wins[1].id) + end) + + after_each(function() + os.remove(fname) + end) + + it('should return option information', function() + eq(meths.get_option_info('dictionary'), meths.get_option_info2('dictionary', {})) -- buffer + eq(meths.get_option_info('fillchars'), meths.get_option_info2('fillchars', {})) -- window + eq(meths.get_option_info('completeopt'), meths.get_option_info2('completeopt', {})) -- global + end) + + describe('last set', function() + local tests = { + {desc="(buf option, global requested, global set) points to global", linenr=1, sid=1, args={'dictionary', {scope='global'}}}, + {desc="(buf option, global requested, local set) is not set", linenr=0, sid=0, args={'formatprg', {scope='global'}}}, + {desc="(buf option, global requested, both set) points to global", linenr=3, sid=1, args={'equalprg', {scope='global'}}}, + {desc="(buf option, local requested, global set) is not set", linenr=0, sid=0, args={'dictionary', {scope='local'}}}, + {desc="(buf option, local requested, local set) points to local", linenr=2, sid=1, args={'formatprg', {scope='local'}}}, + {desc="(buf option, local requested, both set) points to local", linenr=4, sid=1, args={'equalprg', {scope='local'}}}, + {desc="(buf option, fallback requested, global set) points to global", linenr=1, sid=1, args={'dictionary', {}}}, + {desc="(buf option, fallback requested, local set) points to local", linenr=2, sid=1, args={'formatprg', {}}}, + {desc="(buf option, fallback requested, both set) points to local", linenr=4, sid=1, args={'equalprg', {}}}, + {desc="(win option, global requested, global set) points to global", linenr=5, sid=1, args={'fillchars', {scope='global'}}}, + {desc="(win option, global requested, local set) is not set", linenr=0, sid=0, args={'listchars', {scope='global'}}}, + {desc="(win option, global requested, both set) points to global", linenr=7, sid=1, args={'showbreak', {scope='global'}}}, + {desc="(win option, local requested, global set) is not set", linenr=0, sid=0, args={'fillchars', {scope='local'}}}, + {desc="(win option, local requested, local set) points to local", linenr=6, sid=1, args={'listchars', {scope='local'}}}, + {desc="(win option, local requested, both set) points to local", linenr=8, sid=1, args={'showbreak', {scope='local'}}}, + {desc="(win option, fallback requested, global set) points to global", linenr=5, sid=1, args={'fillchars', {}}}, + {desc="(win option, fallback requested, local set) points to local", linenr=6, sid=1, args={'listchars', {}}}, + {desc="(win option, fallback requested, both set) points to local", linenr=8, sid=1, args={'showbreak', {}}}, + {desc="(global option, global requested) points to global", linenr=9, sid=1, args={'completeopt', {scope='global'}}}, + {desc="(global option, local requested) is not set", linenr=0, sid=0, args={'completeopt', {scope='local'}}}, + {desc="(global option, fallback requested) points to global", linenr=9, sid=1, args={'completeopt', {}}}, + } + + for _, t in pairs(tests) do + it(t.desc, function() + -- Switch to the target buffer/window so that curbuf/curwin are used. + meths.set_current_win(wins[2].id) + local info = meths.get_option_info2(unpack(t.args)) + eq(t.linenr, info.last_set_linenr) + eq(t.sid, info.last_set_sid) + end) + end + + it('is provided for cross-buffer requests', function() + local info = meths.get_option_info2('formatprg', {buf=bufs[2].id}) + eq(2, info.last_set_linenr) + eq(1, info.last_set_sid) + end) + + it('is provided for cross-window requests', function() + local info = meths.get_option_info2('listchars', {win=wins[2].id}) + eq(6, info.last_set_linenr) + eq(1, info.last_set_sid) + end) end) end) @@ -2820,11 +3204,10 @@ describe('API', function() local screen before_each(function() - clear() screen = Screen.new(40, 8) screen:attach() screen:set_default_attr_ids({ - [0] = {bold=true, foreground=Screen.colors.Blue}, + [0] = {bold = true, foreground = Screen.colors.Blue}, [1] = {bold = true, foreground = Screen.colors.SeaGreen}, [2] = {bold = true, reverse = true}, [3] = {foreground = Screen.colors.Brown, bold = true}, -- Statement @@ -2879,13 +3262,13 @@ describe('API', function() it('can save message history', function() nvim('command', 'set cmdheight=2') -- suppress Press ENTER nvim("echo", {{"msg\nmsg"}, {"msg"}}, true, {}) - eq("msg\nmsgmsg", meths.exec('messages', true)) + eq("msg\nmsgmsg", exec_capture('messages')) end) it('can disable saving message history', function() nvim('command', 'set cmdheight=2') -- suppress Press ENTER nvim_async("echo", {{"msg\nmsg"}, {"msg"}}, false, {}) - eq("", meths.exec("messages", true)) + eq("", exec_capture('messages')) end) end) @@ -2894,7 +3277,6 @@ describe('API', function() local screen before_each(function() - clear() screen = Screen.new(100, 35) screen:attach() screen:set_default_attr_ids({ @@ -3031,10 +3413,10 @@ describe('API', function() eq(true, meths.del_mark('F')) eq({0, 0}, meths.buf_get_mark(buf, 'F')) end) - it('fails when invalid marks are used', function() - eq(false, pcall(meths.del_mark, 'f')) - eq(false, pcall(meths.del_mark, '!')) - eq(false, pcall(meths.del_mark, 'fail')) + it('validation', function() + eq("Invalid mark name (must be file/uppercase): 'f'", pcall_err(meths.del_mark, 'f')) + eq("Invalid mark name (must be file/uppercase): '!'", pcall_err(meths.del_mark, '!')) + eq("Invalid mark name (must be a single char): 'fail'", pcall_err(meths.del_mark, 'fail')) end) end) describe('nvim_get_mark', function() @@ -3048,10 +3430,10 @@ describe('API', function() assert(string.find(mark[4], "mybuf$")) eq({2, 2, buf.id, mark[4]}, mark) end) - it('fails when invalid marks are used', function() - eq(false, pcall(meths.del_mark, 'f')) - eq(false, pcall(meths.del_mark, '!')) - eq(false, pcall(meths.del_mark, 'fail')) + it('validation', function() + eq("Invalid mark name (must be file/uppercase): 'f'", pcall_err(meths.get_mark, 'f', {})) + eq("Invalid mark name (must be file/uppercase): '!'", pcall_err(meths.get_mark, '!', {})) + eq("Invalid mark name (must be a single char): 'fail'", pcall_err(meths.get_mark, 'fail', {})) end) it('returns the expected when mark is not set', function() eq(true, meths.del_mark('A')) @@ -3113,15 +3495,15 @@ describe('API', function() meths.eval_statusline('a%=b', { fillchar = '\031', maxwidth = 5 })) end) it('rejects multiple-character fillchar', function() - eq('fillchar must be a single character', + eq("Invalid 'fillchar': expected single character", pcall_err(meths.eval_statusline, '', { fillchar = 'aa' })) end) it('rejects empty string fillchar', function() - eq('fillchar must be a single character', + eq("Invalid 'fillchar': expected single character", pcall_err(meths.eval_statusline, '', { fillchar = '' })) end) it('rejects non-string fillchar', function() - eq('fillchar must be a single character', + eq("Invalid 'fillchar': expected String, got Integer", pcall_err(meths.eval_statusline, '', { fillchar = 1 })) end) it('rejects invalid string', function() @@ -3213,6 +3595,38 @@ describe('API', function() 'TextWithNoHighlight%#WarningMsg#TextWithWarningHighlight', { use_winbar = true, highlights = true })) end) + it('works with statuscolumn', function() + exec([[ + let &stc='%C%s%=%l ' + set cul nu nuw=3 scl=yes:2 fdc=2 + call setline(1, repeat(['aaaaa'], 5)) + let g:ns = nvim_create_namespace('') + call sign_define('a', {'text':'aa', 'texthl':'IncSearch', 'numhl':'Normal'}) + call sign_place(2, 1, 'a', bufnr(), {'lnum':4}) + call nvim_buf_set_extmark(0, g:ns, 3, 1, { 'sign_text':'bb', 'sign_hl_group':'ErrorMsg' }) + 1,5fold | 1,5 fold | foldopen! + norm 4G + ]]) + eq({ + str = '││aabb 4 ', + width = 9, + highlights = { + { group = 'CursorLineFold', start = 0 }, + { group = 'Normal', start = 6 }, + { group = 'IncSearch', start = 6 }, + { group = 'ErrorMsg', start = 8 }, + { group = 'Normal', start = 10 } + } + }, meths.eval_statusline('%C%s%=%l ', { use_statuscol_lnum = 4, highlights = true })) + eq({ + str = '3 ' , + width = 2, + highlights = { + { group = 'LineNr', start = 0 }, + { group = 'ErrorMsg', start = 1 } + } + }, meths.eval_statusline('%l%#ErrorMsg# ', { use_statuscol_lnum = 3, highlights = true })) + end) it('no memory leak with click functions', function() meths.eval_statusline('%@ClickFunc@StatusLineStringWithClickFunc%T', {}) eq({ @@ -3226,6 +3640,7 @@ describe('API', function() end) end) end) + describe('nvim_parse_cmd', function() it('works', function() eq({ @@ -3708,7 +4123,7 @@ describe('API', function() } }, meths.parse_cmd('MyCommand test it', {})) end) - it('errors for invalid command', function() + it('validates command', function() eq('Error while parsing command line', pcall_err(meths.parse_cmd, '', {})) eq('Error while parsing command line', pcall_err(meths.parse_cmd, '" foo', {})) eq('Error while parsing command line: E492: Not an editor command: Fubar', @@ -3807,20 +4222,87 @@ describe('API', function() meths.cmd(meths.parse_cmd("set cursorline", {}), {}) eq(true, meths.get_option_value("cursorline", {})) end) + it('no side-effects (error messages) in pcall() #20339', function() + eq({ false, 'Error while parsing command line: E16: Invalid range' }, + exec_lua([=[return {pcall(vim.api.nvim_parse_cmd, "'<,'>n", {})}]=])) + eq('', eval('v:errmsg')) + end) end) + describe('nvim_cmd', function() it('works', function () meths.cmd({ cmd = "set", args = { "cursorline" } }, {}) eq(true, meths.get_option_value("cursorline", {})) end) + + it('validation', function() + eq("Invalid 'cmd': expected non-empty String", + pcall_err(meths.cmd, { cmd = ""}, {})) + eq("Invalid 'cmd': expected String, got Array", + pcall_err(meths.cmd, { cmd = {}}, {})) + eq("Invalid 'args': expected Array, got Boolean", + pcall_err(meths.cmd, { cmd = "set", args = true }, {})) + eq("Invalid command arg: expected non-whitespace", + pcall_err(meths.cmd, { cmd = "set", args = {' '}, }, {})) + eq("Invalid command arg: expected valid type, got Array", + pcall_err(meths.cmd, { cmd = "set", args = {{}}, }, {})) + eq("Wrong number of arguments", + pcall_err(meths.cmd, { cmd = "aboveleft", args = {}, }, {})) + eq("Command cannot accept bang: print", + pcall_err(meths.cmd, { cmd = "print", args = {}, bang = true }, {})) + + eq("Command cannot accept range: set", + pcall_err(meths.cmd, { cmd = "set", args = {}, range = {1} }, {})) + eq("Invalid 'range': expected Array, got Boolean", + pcall_err(meths.cmd, { cmd = "print", args = {}, range = true }, {})) + eq("Invalid 'range': expected <=2 elements", + pcall_err(meths.cmd, { cmd = "print", args = {}, range = {1,2,3,4} }, {})) + eq("Invalid range element: expected non-negative Integer", + pcall_err(meths.cmd, { cmd = "print", args = {}, range = {-1} }, {})) + + eq("Command cannot accept count: set", + pcall_err(meths.cmd, { cmd = "set", args = {}, count = 1 }, {})) + eq("Invalid 'count': expected Integer, got Boolean", + pcall_err(meths.cmd, { cmd = "print", args = {}, count = true }, {})) + eq("Invalid 'count': expected non-negative Integer", + pcall_err(meths.cmd, { cmd = "print", args = {}, count = -1 }, {})) + + eq("Command cannot accept register: set", + pcall_err(meths.cmd, { cmd = "set", args = {}, reg = 'x' }, {})) + eq('Cannot use register "=', + pcall_err(meths.cmd, { cmd = "put", args = {}, reg = '=' }, {})) + eq("Invalid 'reg': expected single character, got xx", + pcall_err(meths.cmd, { cmd = "put", args = {}, reg = 'xx' }, {})) + + -- #20681 + eq('Invalid command: "win_getid"', pcall_err(meths.cmd, { cmd = 'win_getid'}, {})) + eq('Invalid command: "echo "hi""', pcall_err(meths.cmd, { cmd = 'echo "hi"'}, {})) + eq('Invalid command: "win_getid"', pcall_err(exec_lua, [[return vim.cmd.win_getid{}]])) + + -- Lua call allows empty {} for dict item. + eq('', exec_lua([[return vim.cmd{ cmd = "set", args = {}, magic = {} }]])) + eq('', exec_lua([[return vim.cmd{ cmd = "set", args = {}, mods = {} }]])) + eq('', meths.cmd({ cmd = "set", args = {}, magic = {} }, {})) + + -- Lua call does not allow non-empty list-like {} for dict item. + eq("Invalid 'magic': Expected Dict-like Lua table", + pcall_err(exec_lua, [[return vim.cmd{ cmd = "set", args = {}, magic = { 'a' } }]])) + eq("Invalid key: 'bogus'", + pcall_err(exec_lua, [[return vim.cmd{ cmd = "set", args = {}, magic = { bogus = true } }]])) + eq("Invalid key: 'bogus'", + pcall_err(exec_lua, [[return vim.cmd{ cmd = "set", args = {}, mods = { bogus = true } }]])) + end) + it('captures output', function() eq("foo", meths.cmd({ cmd = "echo", args = { '"foo"' } }, { output = true })) end) + it('sets correct script context', function() meths.cmd({ cmd = "set", args = { "cursorline" } }, {}) - local str = meths.exec([[verbose set cursorline?]], true) + local str = exec_capture([[verbose set cursorline?]]) neq(nil, str:find("cursorline\n\tLast set from API client %(channel id %d+%)")) end) + it('works with range', function() insert [[ line1 @@ -3867,7 +4349,7 @@ describe('API', function() line6 ]] meths.cmd({ cmd = "del", range = { 2, 4 }, reg = 'a' }, {}) - meths.exec("1put a", false) + command('1put a') expect [[ line1 line2 @@ -3914,7 +4396,7 @@ describe('API', function() meths.cmd({ cmd = "buffers", mods = { filter = { pattern = "foo", force = true } } }, { output = true })) - -- with emsg_silent = true error is suppresed + -- with emsg_silent = true error is suppressed feed([[:lua vim.api.nvim_cmd({ cmd = 'call', mods = { emsg_silent = true } }, {})<CR>]]) eq('', meths.cmd({ cmd = 'messages' }, { output = true })) -- error from the next command typed is not suppressed #21420 @@ -3927,16 +4409,16 @@ describe('API', function() vim.api.nvim_echo({{ opts.fargs[1] }}, false, {}) end, { nargs = 1 }) ]]) - eq(lfs.currentdir(), + eq(luv.cwd(), meths.cmd({ cmd = "Foo", args = { '%:p:h' }, magic = { file = true } }, { output = true })) end) it('splits arguments correctly', function() - meths.exec([[ + exec([[ function! FooFunc(...) echo a:000 endfunction - ]], false) + ]]) meths.create_user_command("Foo", "call FooFunc(<f-args>)", { nargs = '+' }) eq([=[['a quick', 'brown fox', 'jumps over the', 'lazy dog']]=], meths.cmd({ cmd = "Foo", args = { "a quick", "brown fox", "jumps over the", "lazy dog"}}, @@ -3948,7 +4430,7 @@ describe('API', function() it('splits arguments correctly for Lua callback', function() meths.exec_lua([[ local function FooFunc(opts) - vim.pretty_print(opts.fargs) + vim.print(opts.fargs) end vim.api.nvim_create_user_command("Foo", FooFunc, { nargs = '+' }) @@ -4054,8 +4536,6 @@ describe('API', function() eq('1', meths.cmd({cmd = 'echo', args = {true}}, {output = true})) end) describe('first argument as count', function() - before_each(clear) - it('works', function() command('vsplit | enew') meths.cmd({cmd = 'bdelete', args = {meths.get_current_buf()}}, {}) diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index ecab6a4713..6737c2d15b 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -43,6 +43,25 @@ describe('API/win', function() eq('Invalid buffer id: 23', pcall_err(window, 'set_buf', nvim('get_current_win'), 23)) eq('Invalid window id: 23', pcall_err(window, 'set_buf', 23, nvim('get_current_buf'))) end) + + it('disallowed in cmdwin if win={old_}curwin or buf=curbuf', function() + local new_buf = meths.create_buf(true, true) + local old_win = meths.get_current_win() + local new_win = meths.open_win(new_buf, false, { + relative='editor', row=10, col=10, width=50, height=10, + }) + feed('q:') + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.win_set_buf, 0, new_buf)) + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.win_set_buf, old_win, new_buf)) + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.win_set_buf, new_win, 0)) + + local next_buf = meths.create_buf(true, true) + meths.win_set_buf(new_win, next_buf) + eq(next_buf, meths.win_get_buf(new_win)) + end) end) describe('{get,set}_cursor', function() @@ -285,6 +304,22 @@ describe('API/win', function() eq(2, window('get_height', nvim('list_wins')[2])) end) + it('correctly handles height=1', function() + nvim('command', 'split') + nvim('set_current_win', nvim('list_wins')[1]) + window('set_height', nvim('list_wins')[2], 1) + eq(1, window('get_height', nvim('list_wins')[2])) + end) + + it('correctly handles height=1 with a winbar', function() + nvim('command', 'set winbar=foobar') + nvim('command', 'set winminheight=0') + nvim('command', 'split') + nvim('set_current_win', nvim('list_wins')[1]) + window('set_height', nvim('list_wins')[2], 1) + eq(1, window('get_height', nvim('list_wins')[2])) + end) + it('do not cause ml_get errors with foldmethod=expr #19989', function() insert([[ aaaaa @@ -363,22 +398,22 @@ describe('API/win', function() end) end) - describe('nvim_win_get_option, nvim_win_set_option', function() + describe('nvim_get_option_value, nvim_set_option_value', function() it('works', function() - curwin('set_option', 'colorcolumn', '4,3') - eq('4,3', curwin('get_option', 'colorcolumn')) + nvim('set_option_value', 'colorcolumn', '4,3', {}) + eq('4,3', nvim('get_option_value', 'colorcolumn', {})) command("set modified hidden") command("enew") -- edit new buffer, window option is preserved - eq('4,3', curwin('get_option', 'colorcolumn')) + eq('4,3', nvim('get_option_value', 'colorcolumn', {})) -- global-local option - curwin('set_option', 'statusline', 'window-status') - eq('window-status', curwin('get_option', 'statusline')) - eq('', nvim('get_option', 'statusline')) + nvim('set_option_value', 'statusline', 'window-status', {win=0}) + eq('window-status', nvim('get_option_value', 'statusline', {win=0})) + eq('', nvim('get_option_value', 'statusline', {scope='global'})) command("set modified") command("enew") -- global-local: not preserved in new buffer -- confirm local value was not copied - eq('', curwin('get_option', 'statusline')) + eq('', nvim('get_option_value', 'statusline', {win = 0})) eq('', eval('&l:statusline')) end) @@ -386,16 +421,16 @@ describe('API/win', function() nvim('command', 'tabnew') local tab1 = unpack(nvim('list_tabpages')) local win1 = unpack(tabpage('list_wins', tab1)) - window('set_option', win1, 'statusline', 'window-status') + nvim('set_option_value', 'statusline', 'window-status', {win=win1.id}) nvim('command', 'split') nvim('command', 'wincmd J') nvim('command', 'wincmd j') - eq('window-status', window('get_option', win1, 'statusline')) + eq('window-status', nvim('get_option_value', 'statusline', {win = win1.id})) assert_alive() end) it('returns values for unset local options', function() - eq(-1, curwin('get_option', 'scrolloff')) + eq(-1, nvim('get_option_value', 'scrolloff', {win=0, scope='local'})) end) end) @@ -508,15 +543,21 @@ describe('API/win', function() command('split') eq(2, #meths.list_wins()) local oldwin = meths.get_current_win() + local otherwin = meths.open_win(0, false, { + relative='editor', row=10, col=10, width=10, height=10, + }) -- Open cmdline-window. feed('q:') - eq(3, #meths.list_wins()) + eq(4, #meths.list_wins()) eq(':', funcs.getcmdwintype()) - -- Vim: not allowed to close other windows from cmdline-window. + -- Not allowed to close previous window from cmdline-window. eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', - pcall_err(meths.win_close, oldwin, true)) + pcall_err(meths.win_close, oldwin, true)) + -- Closing other windows is fine. + meths.win_close(otherwin, true) + eq(false, meths.win_is_valid(otherwin)) -- Close cmdline-window. - meths.win_close(0,true) + meths.win_close(0, true) eq(2, #meths.list_wins()) eq('', funcs.getcmdwintype()) end) @@ -568,15 +609,247 @@ describe('API/win', function() it('deletes the buffer when bufhidden=wipe', function() local oldwin = meths.get_current_win() local oldbuf = meths.get_current_buf() - local buf = meths.create_buf(true, false) + local buf = meths.create_buf(true, false).id local newwin = meths.open_win(buf, true, { relative='win', row=3, col=3, width=12, height=3 }) - meths.buf_set_option(buf, 'bufhidden', 'wipe') + meths.set_option_value('bufhidden', 'wipe', {buf=buf}) meths.win_hide(newwin) eq({oldwin}, meths.list_wins()) eq({oldbuf}, meths.list_bufs()) end) + it('in the cmdwin', function() + feed('q:') + -- Can close the cmdwin. + meths.win_hide(0) + eq('', funcs.getcmdwintype()) + + local old_win = meths.get_current_win() + local other_win = meths.open_win(0, false, { + relative='win', row=3, col=3, width=12, height=3 + }) + feed('q:') + -- Cannot close the previous window. + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.win_hide, old_win)) + -- Can close other windows. + meths.win_hide(other_win) + eq(false, meths.win_is_valid(other_win)) + end) + end) + + describe('text_height', function() + it('validation', function() + local X = meths.get_vvar('maxcol') + insert([[ + aaa + bbb + ccc + ddd + eee]]) + eq("Invalid window id: 23", + pcall_err(meths.win_text_height, 23, {})) + eq("Line index out of bounds", + pcall_err(curwinmeths.text_height, { start_row = 5 })) + eq("Line index out of bounds", + pcall_err(curwinmeths.text_height, { start_row = -6 })) + eq("Line index out of bounds", + pcall_err(curwinmeths.text_height, { end_row = 5 })) + eq("Line index out of bounds", + pcall_err(curwinmeths.text_height, { end_row = -6 })) + eq("'start_row' is higher than 'end_row'", + pcall_err(curwinmeths.text_height, { start_row = 3, end_row = 1 })) + eq("'start_vcol' specified without 'start_row'", + pcall_err(curwinmeths.text_height, { end_row = 2, start_vcol = 0 })) + eq("'end_vcol' specified without 'end_row'", + pcall_err(curwinmeths.text_height, { start_row = 2, end_vcol = 0 })) + eq("Invalid 'start_vcol': out of range", + pcall_err(curwinmeths.text_height, { start_row = 2, start_vcol = -1 })) + eq("Invalid 'start_vcol': out of range", + pcall_err(curwinmeths.text_height, { start_row = 2, start_vcol = X + 1 })) + eq("Invalid 'end_vcol': out of range", + pcall_err(curwinmeths.text_height, { end_row = 2, end_vcol = -1 })) + eq("Invalid 'end_vcol': out of range", + pcall_err(curwinmeths.text_height, { end_row = 2, end_vcol = X + 1 })) + eq("'start_vcol' is higher than 'end_vcol'", + pcall_err(curwinmeths.text_height, { start_row = 2, end_row = 2, start_vcol = 10, end_vcol = 5 })) + end) + + it('with two diff windows', function() + local X = meths.get_vvar('maxcol') + local screen = Screen.new(45, 22) + screen:set_default_attr_ids({ + [0] = {foreground = Screen.colors.Blue1, bold = true}; + [1] = {foreground = Screen.colors.Blue4, background = Screen.colors.Grey}; + [2] = {foreground = Screen.colors.Brown}; + [3] = {foreground = Screen.colors.Blue1, background = Screen.colors.LightCyan1, bold = true}; + [4] = {background = Screen.colors.LightBlue}; + [5] = {foreground = Screen.colors.Blue4, background = Screen.colors.LightGrey}; + [6] = {background = Screen.colors.Plum1}; + [7] = {background = Screen.colors.Red, bold = true}; + [8] = {reverse = true}; + [9] = {bold = true, reverse = true}; + }) + screen:attach() + exec([[ + set diffopt+=context:2 number + let expr = 'printf("%08d", v:val) .. repeat("!", v:val)' + call setline(1, map(range(1, 20) + range(25, 45), expr)) + vnew + call setline(1, map(range(3, 20) + range(28, 50), expr)) + windo diffthis + ]]) + feed('24gg') + screen:expect{grid=[[ + {1: }{2: }{3:----------------}│{1: }{2: 1 }{4:00000001! }| + {1: }{2: }{3:----------------}│{1: }{2: 2 }{4:00000002!! }| + {1: }{2: 1 }00000003!!! │{1: }{2: 3 }00000003!!! | + {1: }{2: 2 }00000004!!!! │{1: }{2: 4 }00000004!!!! | + {1:+ }{2: 3 }{5:+-- 14 lines: 00}│{1:+ }{2: 5 }{5:+-- 14 lines: 00}| + {1: }{2: 17 }00000019!!!!!!!!│{1: }{2: 19 }00000019!!!!!!!!| + {1: }{2: 18 }00000020!!!!!!!!│{1: }{2: 20 }00000020!!!!!!!!| + {1: }{2: }{3:----------------}│{1: }{2: 21 }{4:00000025!!!!!!!!}| + {1: }{2: }{3:----------------}│{1: }{2: 22 }{4:00000026!!!!!!!!}| + {1: }{2: }{3:----------------}│{1: }{2: 23 }{4:00000027!!!!!!!!}| + {1: }{2: 19 }00000028!!!!!!!!│{1: }{2: 24 }^00000028!!!!!!!!| + {1: }{2: 20 }00000029!!!!!!!!│{1: }{2: 25 }00000029!!!!!!!!| + {1:+ }{2: 21 }{5:+-- 14 lines: 00}│{1:+ }{2: 26 }{5:+-- 14 lines: 00}| + {1: }{2: 35 }00000044!!!!!!!!│{1: }{2: 40 }00000044!!!!!!!!| + {1: }{2: 36 }00000045!!!!!!!!│{1: }{2: 41 }00000045!!!!!!!!| + {1: }{2: 37 }{4:00000046!!!!!!!!}│{1: }{2: }{3:----------------}| + {1: }{2: 38 }{4:00000047!!!!!!!!}│{1: }{2: }{3:----------------}| + {1: }{2: 39 }{4:00000048!!!!!!!!}│{1: }{2: }{3:----------------}| + {1: }{2: 40 }{4:00000049!!!!!!!!}│{1: }{2: }{3:----------------}| + {1: }{2: 41 }{4:00000050!!!!!!!!}│{1: }{2: }{3:----------------}| + {8:[No Name] [+] }{9:[No Name] [+] }| + | + ]]} + screen:try_resize(45, 3) + screen:expect{grid=[[ + {1: }{2: 19 }00000028!!!!!!!!│{1: }{2: 24 }^00000028!!!!!!!!| + {8:[No Name] [+] }{9:[No Name] [+] }| + | + ]]} + eq({ all = 20, fill = 5 }, meths.win_text_height(1000, {})) + eq({ all = 20, fill = 5 }, meths.win_text_height(1001, {})) + eq({ all = 20, fill = 5 }, meths.win_text_height(1000, { start_row = 0 })) + eq({ all = 20, fill = 5 }, meths.win_text_height(1001, { start_row = 0 })) + eq({ all = 15, fill = 0 }, meths.win_text_height(1000, { end_row = -1 })) + eq({ all = 15, fill = 0 }, meths.win_text_height(1000, { end_row = 40 })) + eq({ all = 20, fill = 5 }, meths.win_text_height(1001, { end_row = -1 })) + eq({ all = 20, fill = 5 }, meths.win_text_height(1001, { end_row = 40 })) + eq({ all = 10, fill = 5 }, meths.win_text_height(1000, { start_row = 23 })) + eq({ all = 13, fill = 3 }, meths.win_text_height(1001, { start_row = 18 })) + eq({ all = 11, fill = 0 }, meths.win_text_height(1000, { end_row = 23 })) + eq({ all = 11, fill = 5 }, meths.win_text_height(1001, { end_row = 18 })) + eq({ all = 11, fill = 0 }, meths.win_text_height(1000, { start_row = 3, end_row = 39 })) + eq({ all = 11, fill = 3 }, meths.win_text_height(1001, { start_row = 1, end_row = 34 })) + eq({ all = 9, fill = 0 }, meths.win_text_height(1000, { start_row = 4, end_row = 38 })) + eq({ all = 9, fill = 3 }, meths.win_text_height(1001, { start_row = 2, end_row = 33 })) + eq({ all = 9, fill = 0 }, meths.win_text_height(1000, { start_row = 5, end_row = 37 })) + eq({ all = 9, fill = 3 }, meths.win_text_height(1001, { start_row = 3, end_row = 32 })) + eq({ all = 9, fill = 0 }, meths.win_text_height(1000, { start_row = 17, end_row = 25 })) + eq({ all = 9, fill = 3 }, meths.win_text_height(1001, { start_row = 15, end_row = 20 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(1000, { start_row = 18, end_row = 24 })) + eq({ all = 7, fill = 3 }, meths.win_text_height(1001, { start_row = 16, end_row = 19 })) + eq({ all = 6, fill = 5 }, meths.win_text_height(1000, { start_row = -1 })) + eq({ all = 5, fill = 5 }, meths.win_text_height(1000, { start_row = -1, start_vcol = X })) + eq({ all = 0, fill = 0 }, meths.win_text_height(1000, { start_row = -1, start_vcol = X, end_row = -1 })) + eq({ all = 0, fill = 0 }, meths.win_text_height(1000, { start_row = -1, start_vcol = X, end_row = -1, end_vcol = X })) + eq({ all = 1, fill = 0 }, meths.win_text_height(1000, { start_row = -1, start_vcol = 0, end_row = -1, end_vcol = X })) + eq({ all = 3, fill = 2 }, meths.win_text_height(1001, { end_row = 0 })) + eq({ all = 2, fill = 2 }, meths.win_text_height(1001, { end_row = 0, end_vcol = 0 })) + eq({ all = 2, fill = 2 }, meths.win_text_height(1001, { start_row = 0, end_row = 0, end_vcol = 0 })) + eq({ all = 0, fill = 0 }, meths.win_text_height(1001, { start_row = 0, start_vcol = 0, end_row = 0, end_vcol = 0 })) + eq({ all = 1, fill = 0 }, meths.win_text_height(1001, { start_row = 0, start_vcol = 0, end_row = 0, end_vcol = X })) + eq({ all = 11, fill = 5 }, meths.win_text_height(1001, { end_row = 18 })) + eq({ all = 9, fill = 3 }, meths.win_text_height(1001, { start_row = 0, start_vcol = 0, end_row = 18 })) + eq({ all = 10, fill = 5 }, meths.win_text_height(1001, { end_row = 18, end_vcol = 0 })) + eq({ all = 8, fill = 3 }, meths.win_text_height(1001, { start_row = 0, start_vcol = 0, end_row = 18, end_vcol = 0 })) + end) + + it('with wrapped lines', function() + local X = meths.get_vvar('maxcol') + local screen = Screen.new(45, 22) + screen:set_default_attr_ids({ + [0] = {foreground = Screen.colors.Blue1, bold = true}; + [1] = {foreground = Screen.colors.Brown}; + [2] = {background = Screen.colors.Yellow}; + }) + screen:attach() + exec([[ + set number cpoptions+=n + call setline(1, repeat([repeat('foobar-', 36)], 3)) + ]]) + local ns = meths.create_namespace('') + meths.buf_set_extmark(0, ns, 1, 100, { virt_text = {{('?'):rep(15), 'Search'}}, virt_text_pos = 'inline' }) + meths.buf_set_extmark(0, ns, 2, 200, { virt_text = {{('!'):rep(75), 'Search'}}, virt_text_pos = 'inline' }) + screen:expect{grid=[[ + {1: 1 }^foobar-foobar-foobar-foobar-foobar-foobar| + -foobar-foobar-foobar-foobar-foobar-foobar-fo| + obar-foobar-foobar-foobar-foobar-foobar-fooba| + r-foobar-foobar-foobar-foobar-foobar-foobar-f| + oobar-foobar-foobar-foobar-foobar-foobar-foob| + ar-foobar-foobar-foobar-foobar- | + {1: 2 }foobar-foobar-foobar-foobar-foobar-foobar| + -foobar-foobar-foobar-foobar-foobar-foobar-fo| + obar-foobar-fo{2:???????????????}obar-foobar-foob| + ar-foobar-foobar-foobar-foobar-foobar-foobar-| + foobar-foobar-foobar-foobar-foobar-foobar-foo| + bar-foobar-foobar-foobar-foobar-foobar-foobar| + - | + {1: 3 }foobar-foobar-foobar-foobar-foobar-foobar| + -foobar-foobar-foobar-foobar-foobar-foobar-fo| + obar-foobar-foobar-foobar-foobar-foobar-fooba| + r-foobar-foobar-foobar-foobar-foobar-foobar-f| + oobar-foobar-foobar-foob{2:!!!!!!!!!!!!!!!!!!!!!}| + {2:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!}| + {2:!!!!!!!!!}ar-foobar-foobar-foobar-foobar-fooba| + r-foobar-foobar- | + | + ]]} + screen:try_resize(45, 2) + screen:expect{grid=[[ + {1: 1 }^foobar-foobar-foobar-foobar-foobar-foobar| + | + ]]} + eq({ all = 21, fill = 0 }, meths.win_text_height(0, {})) + eq({ all = 6, fill = 0 }, meths.win_text_height(0, { start_row = 0, end_row = 0 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, end_row = 1 })) + eq({ all = 8, fill = 0 }, meths.win_text_height(0, { start_row = 2, end_row = 2 })) + eq({ all = 0, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 0 })) + eq({ all = 1, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 41 })) + eq({ all = 2, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 42 })) + eq({ all = 2, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 86 })) + eq({ all = 3, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 87 })) + eq({ all = 6, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 266 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 267 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 311 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = 312 })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 0, end_row = 1, end_vcol = X })) + eq({ all = 7, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 40, end_row = 1, end_vcol = X })) + eq({ all = 6, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 41, end_row = 1, end_vcol = X })) + eq({ all = 6, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 85, end_row = 1, end_vcol = X })) + eq({ all = 5, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 86, end_row = 1, end_vcol = X })) + eq({ all = 2, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 265, end_row = 1, end_vcol = X })) + eq({ all = 1, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 266, end_row = 1, end_vcol = X })) + eq({ all = 1, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 310, end_row = 1, end_vcol = X })) + eq({ all = 0, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 311, end_row = 1, end_vcol = X })) + eq({ all = 1, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 86, end_row = 1, end_vcol = 131 })) + eq({ all = 1, fill = 0 }, meths.win_text_height(0, { start_row = 1, start_vcol = 221, end_row = 1, end_vcol = 266 })) + eq({ all = 18, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 131 })) + eq({ all = 19, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 130 })) + eq({ all = 20, fill = 0 }, meths.win_text_height(0, { end_row = 2, end_vcol = 311 })) + eq({ all = 21, fill = 0 }, meths.win_text_height(0, { end_row = 2, end_vcol = 312 })) + eq({ all = 17, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 131, end_row = 2, end_vcol = 311 })) + eq({ all = 19, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 130, end_row = 2, end_vcol = 312 })) + eq({ all = 16, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 221 })) + eq({ all = 17, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 220 })) + eq({ all = 14, fill = 0 }, meths.win_text_height(0, { end_row = 2, end_vcol = 41 })) + eq({ all = 15, fill = 0 }, meths.win_text_height(0, { end_row = 2, end_vcol = 42 })) + eq({ all = 9, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 221, end_row = 2, end_vcol = 41 })) + eq({ all = 11, fill = 0 }, meths.win_text_height(0, { start_row = 0, start_vcol = 220, end_row = 2, end_vcol = 42 })) + end) end) describe('open_win', function() @@ -591,19 +864,45 @@ describe('API/win', function() }) eq(1, funcs.exists('g:fired')) end) + + it('disallowed in cmdwin if enter=true or buf=curbuf', function() + local new_buf = meths.create_buf(true, true) + feed('q:') + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.open_win, new_buf, true, { + relative='editor', row=5, col=5, width=5, height=5, + })) + eq('E11: Invalid in command-line window; <CR> executes, CTRL-C quits', + pcall_err(meths.open_win, 0, false, { + relative='editor', row=5, col=5, width=5, height=5, + })) + + eq(new_buf, meths.win_get_buf(meths.open_win(new_buf, false, { + relative='editor', row=5, col=5, width=5, height=5, + }))) + end) + + it('aborts if buffer is invalid', function() + local wins_before = meths.list_wins() + eq('Invalid buffer id: 1337', pcall_err(meths.open_win, 1337, false, { + relative='editor', row=5, col=5, width=5, height=5, + })) + eq(wins_before, meths.list_wins()) + end) end) describe('get_config', function() it('includes border', function() local b = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' } local win = meths.open_win(0, true, { - relative='win', row=3, col=3, width=12, height=3, - border = b, + relative='win', row=3, col=3, width=12, height=3, + border = b, }) local cfg = meths.win_get_config(win) eq(b, cfg.border) end) + it('includes border with highlight group', function() local b = { {'a', 'Normal'}, @@ -616,12 +915,25 @@ describe('API/win', function() {'h', 'PreProc'}, } local win = meths.open_win(0, true, { - relative='win', row=3, col=3, width=12, height=3, - border = b, + relative='win', row=3, col=3, width=12, height=3, + border = b, }) local cfg = meths.win_get_config(win) eq(b, cfg.border) end) + + it('includes title and footer', function() + local title = { {'A', {'StatusLine', 'TabLine'}}, {'B'}, {'C', 'WinBar'} } + local footer = { {'A', 'WinBar'}, {'B'}, {'C', {'StatusLine', 'TabLine'}} } + local win = meths.open_win(0, true, { + relative='win', row=3, col=3, width=12, height=3, + border = 'single', title = title, footer = footer, + }) + + local cfg = meths.win_get_config(win) + eq(title, cfg.title) + eq(footer, cfg.footer) + end) end) end) |