diff options
author | Evgeni Chasnovski <evgeni.chasnovski@gmail.com> | 2024-06-21 16:23:02 +0300 |
---|---|---|
committer | Evgeni Chasnovski <evgeni.chasnovski@gmail.com> | 2024-06-24 20:23:11 +0300 |
commit | f8795365deb88ab4e108858c563284b1082d06d4 (patch) | |
tree | e84418252f17b60412e265b139e68ab99b1b1dd7 | |
parent | 5581a95534e44b8714e715c925c9de2d95ae1c21 (diff) | |
download | rneovim-f8795365deb88ab4e108858c563284b1082d06d4.tar.gz rneovim-f8795365deb88ab4e108858c563284b1082d06d4.tar.bz2 rneovim-f8795365deb88ab4e108858c563284b1082d06d4.zip |
test(lua): cover `vim._with()` with tests
Problem: `vim._with()` has many different use cases which are not
covered with tests.
Solution: cover with tests. Some (many) test cases are intentionally
marked as "pending" because they cover cases which don't work as
expected at the moment (and fixing them requires specific knowledge of
C codebase). Use them as a reference for future fixes.
Also some of "can be nested" tests currently might pass only because
the tested context doesn't work.
-rw-r--r-- | runtime/lua/vim/shared.lua | 17 | ||||
-rw-r--r-- | test/functional/lua/with_spec.lua | 1044 |
2 files changed, 828 insertions, 233 deletions
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 7fd29d5f7b..79e614aaaa 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1144,7 +1144,6 @@ end --- @field buf? integer --- @field emsg_silent? boolean --- @field hide? boolean ---- @field horizontal? boolean --- @field keepalt? boolean --- @field keepjumps? boolean --- @field keepmarks? boolean @@ -1159,6 +1158,15 @@ end --- Executes function `f` with the given context specification. --- +--- Notes: +--- - Context `{ buf = buf }` has no guarantees about current window when +--- inside context. +--- - Context `{ buf = buf, win = win }` is yet not allowed, but this seems +--- to be an implementation detail. +--- - There should be no way to revert currently set `context.sandbox = true` +--- (like with nested `vim._with()` calls). Otherwise it kind of breaks the +--- whole purpose of sandbox execution. +--- --- @param context vim.context.mods function vim._with(context, f) vim.validate('context', context, 'table') @@ -1167,7 +1175,6 @@ function vim._with(context, f) vim.validate('context.buf', context.buf, 'number', true) vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) vim.validate('context.hide', context.hide, 'boolean', true) - vim.validate('context.horizontal', context.horizontal, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) vim.validate('context.keepmarks', context.keepmarks, 'boolean', true) @@ -1192,6 +1199,10 @@ function vim._with(context, f) if not vim.api.nvim_win_is_valid(context.win) then error('Invalid window id: ' .. context.win) end + -- TODO: Maybe allow it? + if context.buf and vim.api.nvim_win_get_buf(context.win) ~= context.buf then + error('Can not set both `buf` and `win` context.') + end end -- Store original options @@ -1214,7 +1225,7 @@ function vim._with(context, f) end end - return unpack(retval) + return unpack(retval, 1, table.maxn(retval)) end return vim diff --git a/test/functional/lua/with_spec.lua b/test/functional/lua/with_spec.lua index 36dee9630a..f93ef6f26b 100644 --- a/test/functional/lua/with_spec.lua +++ b/test/functional/lua/with_spec.lua @@ -7,286 +7,870 @@ local api = n.api local command = n.command local eq = t.eq local exec_lua = n.exec_lua +local exec_capture = n.exec_capture local matches = t.matches local pcall_err = t.pcall_err -before_each(function() - n.clear() -end) +describe('vim._with', function() + before_each(function() + n.clear() + exec_lua([[ + _G.fn = vim.fn + _G.api = vim.api -describe('vim._with {buf = }', function() - it('does not trigger autocmd', function() - exec_lua [[ - local new = vim.api.nvim_create_buf(false, true) - vim.api.nvim_create_autocmd( { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' }, { - callback = function() _G.n = (_G.n or 0) + 1 end - }) - vim._with({buf = new}, function() - end) - assert(_G.n == nil) - ]] + _G.setup_buffers = function() + return api.nvim_create_buf(false, true), api.nvim_get_current_buf() + end + + _G.setup_windows = function() + local other_win = api.nvim_get_current_win() + vim.cmd.new() + return other_win, api.nvim_get_current_win() + end + ]]) end) - it('trigger autocmd if changed within context', function() - exec_lua [[ - local new = vim.api.nvim_create_buf(false, true) - vim.api.nvim_create_autocmd( { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' }, { - callback = function() _G.n = (_G.n or 0) + 1 end - }) - vim._with({}, function() - vim.api.nvim_set_current_buf(new) - assert(_G.n ~= nil) + local validate_events_trigger = function() + local out = exec_lua [[ + -- Needs three global values defined: + -- - `test_events` - array of events which are tested. + -- - `test_context` - context to be tested. + -- - `test_trig_event` - callable triggering at least one tested event. + _G.n_events = 0 + local opts = { callback = function() _G.n_events = _G.n_events + 1 end } + api.nvim_create_autocmd(_G.test_events, opts) + + local context = { bo = { commentstring = '-- %s' } } + + -- Should not trigger events on its own + vim._with(_G.test_context, function() end) + local is_no_events = _G.n_events == 0 + + -- Should trigger events if specifically asked inside callback + local is_events = vim._with(_G.test_context, function() + _G.test_trig_event() + return _G.n_events > 0 end) + return { is_no_events, is_events } ]] - end) + eq({ true, true }, out) + end - it('can access buf options', function() - local buf1 = api.nvim_get_current_buf() - local buf2 = exec_lua [[ - buf2 = vim.api.nvim_create_buf(false, true) - return buf2 - ]] + describe('`buf` context', function() + it('works', function() + local out = exec_lua [[ + local other_buf, cur_buf = setup_buffers() + local inner = vim._with({ buf = other_buf }, function() + return api.nvim_get_current_buf() + end) + return { inner == other_buf, api.nvim_get_current_buf() == cur_buf } + ]] + eq({ true, true }, out) + end) + + it('does not trigger events', function() + exec_lua [[ + _G.test_events = { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' } + _G.test_context = { buf = other_buf } + _G.test_trig_event = function() vim.cmd.new() end + ]] + validate_events_trigger() + end) - eq(false, api.nvim_get_option_value('autoindent', { buf = buf1 })) - eq(false, api.nvim_get_option_value('autoindent', { buf = buf2 })) + it('can access buffer options', function() + local out = exec_lua [[ + other_buf, cur_buf = setup_buffers() + vim.bo[other_buf].commentstring = '## %s' + vim.bo[cur_buf].commentstring = '// %s' - local val = exec_lua [[ - return vim._with({buf = buf2}, function() - vim.cmd "set autoindent" - return vim.api.nvim_get_current_buf() + vim._with({ buf = other_buf }, function() + vim.cmd.set('commentstring=--\\ %s') + end) + + return vim.bo[other_buf].commentstring == '-- %s' and + vim.bo[cur_buf].commentstring == '// %s' + ]] + eq(true, out) end) - ]] - eq(false, api.nvim_get_option_value('autoindent', { buf = buf1 })) - eq(true, api.nvim_get_option_value('autoindent', { buf = buf2 })) - eq(buf1, api.nvim_get_current_buf()) - eq(buf2, val) + it('works with different kinds of buffers', function() + exec_lua [[ + local validate = function(buf) + vim._with({ buf = buf }, function() + assert(api.nvim_get_current_buf() == buf) + end) + end + + -- Current + validate(api.nvim_get_current_buf()) + + -- Hidden listed + local listed = api.nvim_create_buf(true, true) + validate(listed) + + -- Visible + local other_win, cur_win = setup_windows() + api.nvim_win_set_buf(other_win, listed) + validate(listed) + + -- Shown but not visible + vim.cmd.tabnew() + validate(listed) + + -- Shown in several windows + api.nvim_win_set_buf(0, listed) + validate(listed) + + -- Shown in floating window + local float_buf = api.nvim_create_buf(false, true) + local config = { relative = 'editor', row = 1, col = 1, width = 5, height = 5 } + api.nvim_open_win(float_buf, false, config) + validate(float_buf) + ]] + end) + + it('does not cause ml_get errors with invalid visual selection', function() + exec_lua [[ + api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b', 'c' }) + api.nvim_feedkeys(vim.keycode('G<C-V>'), 'txn', false) + local other_buf, _ = setup_buffers() + vim._with({ buf = buf }, function() vim.cmd.redraw() end) + ]] + end) + + it('can be nested', function() + exec_lua [[ + local other_buf, cur_buf = setup_buffers() + vim._with({ buf = other_buf }, function() + assert(api.nvim_get_current_buf() == other_buf) + inner = vim._with({ buf = cur_buf }, function() + assert(api.nvim_get_current_buf() == cur_buf) + end) + assert(api.nvim_get_current_buf() == other_buf) + end) + assert(api.nvim_get_current_buf() == cur_buf) + ]] + end) + + it('can be nested crazily with hidden buffers', function() + local out = exec_lua([[ + local n = 0 + local function with_recursive_nested_bufs() + n = n + 1 + if n > 20 then return true end + + local other_buf, _ = setup_buffers() + vim.bo[other_buf].commentstring = '## %s' + local callback = function() + return api.nvim_get_current_buf() == other_buf + and vim.bo[other_buf].commentstring == '## %s' + and with_recursive_nested_bufs() + end + return vim._with({ buf = other_buf }, callback) and + api.nvim_buf_delete(other_buf, {}) == nil + end + + return with_recursive_nested_bufs() + ]]) + eq(true, out) + end) end) - it('does not cause ml_get errors with invalid visual selection', function() - exec_lua [[ - local api = vim.api - local t = function(s) return api.nvim_replace_termcodes(s, true, true, true) end - api.nvim_buf_set_lines(0, 0, -1, true, {"a", "b", "c"}) - api.nvim_feedkeys(t "G<C-V>", "txn", false) - vim._with({buf = api.nvim_create_buf(false, true)}, function() vim.cmd "redraw" end) - ]] + describe('`emsg_silent` context', function() + pending('works', function() + local ok = pcall( + exec_lua, + [[ + _G.f = function() + error('This error should not interfer with execution', 0) + end + -- Should not produce error same as `vim.cmd('silent! lua _G.f()')` + vim._with({ emsg_silent = true }, f) + ]] + ) + eq(true, ok) + + -- Should properly report errors afterwards + ok = pcall(exec_lua, 'lua _G.f()') + eq(false, ok) + end) + + it('can be nested', function() + local ok = pcall( + exec_lua, + [[ + _G.f = function() + error('This error should not interfer with execution', 0) + end + -- Should produce error same as `_G.f()` + vim._with({ emsg_silent = true }, function() + vim._with( { emsg_silent = false }, f) + end) + ]] + ) + eq(false, ok) + end) end) - it('can be nested crazily with hidden buffers', function() - eq( - true, - exec_lua([[ - local function scratch_buf_call(fn) - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_option_value('cindent', true, {buf = buf}) - return vim._with({buf = buf}, function() - return vim.api.nvim_get_current_buf() == buf - and vim.api.nvim_get_option_value('cindent', {buf = buf}) - and fn() - end) and vim.api.nvim_buf_delete(buf, {}) == nil - end + describe('`hide` context', function() + pending('works', function() + local ok = pcall( + exec_lua, + [[ + vim.o.hidden = false + vim.bo.modified = true + local init_buf = api.nvim_get_current_buf() + -- Should not produce error same as `vim.cmd('hide enew')` + vim._with({ hide = true }, function() + vim.cmd.enew() + end) + assert(api.nvim_get_current_buf() ~= init_buf) + ]] + ) + eq(true, ok) + end) - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return scratch_buf_call(function() - return true - end) - end) - end) - end) - end) - end) - end) + it('can be nested', function() + local ok = pcall( + exec_lua, + [[ + vim.o.hidden = false + vim.bo.modified = true + -- Should produce error same as `vim.cmd.enew()` + vim._with({ hide = true }, function() + vim._with({ hide = false }, function() + vim.cmd.enew() end) end) - end) - end) + ]] + ) + eq(false, ok) end) - ]]) - ) end) - it('can return values by reference', function() - eq( - { 4, 7 }, + describe('`horizontal` context', function() + local is_approx_eq = function(dim, id_1, id_2) + local f = dim == 'height' and api.nvim_win_get_height or api.nvim_win_get_width + return math.abs(f(id_1) - f(id_2)) <= 1 + end + + local win_id_1, win_id_2, win_id_3 + before_each(function() + win_id_1 = api.nvim_get_current_win() + command('wincmd v | wincmd 5>') + win_id_2 = api.nvim_get_current_win() + command('wincmd s | wincmd 5+') + win_id_3 = api.nvim_get_current_win() + + eq(is_approx_eq('width', win_id_1, win_id_2), false) + eq(is_approx_eq('height', win_id_3, win_id_2), false) + end) + + pending('works', function() exec_lua [[ - local val = {4, 10} - local ref = vim._with({ buf = 0}, function() return val end) - ref[2] = 7 - return val - ]] - ) + -- Should be same as `vim.cmd('horizontal wincmd =')` + vim._with({ horizontal = true }, function() + vim.cmd.wincmd('=') + end) + ]] + eq(is_approx_eq('width', win_id_1, win_id_2), true) + eq(is_approx_eq('height', win_id_3, win_id_2), false) + end) + + pending('can be nested', function() + exec_lua [[ + -- Should be same as `vim.cmd.wincmd('=')` + vim._with({ horizontal = true }, function() + vim._with({ horizontal = false }, function() + vim.cmd.wincmd('=') + end) + end) + ]] + eq(is_approx_eq('width', win_id_1, win_id_2), true) + eq(is_approx_eq('height', win_id_3, win_id_2), true) + end) end) -end) -describe('vim._with {win = }', function() - it('does not trigger autocmd', function() - exec_lua [[ - local old = vim.api.nvim_get_current_win() - vim.cmd("new") - local new = vim.api.nvim_get_current_win() - vim.api.nvim_create_autocmd( { 'WinEnter', 'WinLeave' }, { - callback = function() _G.n = (_G.n or 0) + 1 end - }) - vim._with({win = old}, function() - end) - assert(_G.n == nil) - ]] + describe('`keepalt` context', function() + pending('works', function() + local out = exec_lua [[ + vim.cmd('edit alt') + vim.cmd('edit new') + assert(fn.bufname('#') == 'alt') + + -- Should work as `vim.cmd('keepalt edit very-new')` + vim._with({ keepalt = true }, function() + vim.cmd.edit('very-new') + end) + return fn.bufname('#') == 'alt' + ]] + eq(true, out) + end) + + it('can be nested', function() + local out = exec_lua [[ + vim.cmd('edit alt') + vim.cmd('edit new') + assert(fn.bufname('#') == 'alt') + + -- Should work as `vim.cmd.edit('very-new')` + vim._with({ keepalt = true }, function() + vim._with({ keepalt = false }, function() + vim.cmd.edit('very-new') + end) + end) + return fn.bufname('#') == 'alt' + ]] + eq(false, out) + end) end) - it('trigger autocmd if changed within context', function() - exec_lua [[ - local old = vim.api.nvim_get_current_win() - vim.cmd("new") - local new = vim.api.nvim_get_current_win() - vim.api.nvim_create_autocmd( { 'WinEnter', 'WinLeave' }, { - callback = function() _G.n = (_G.n or 0) + 1 end - }) - vim._with({}, function() - vim.api.nvim_set_current_win(old) - assert(_G.n ~= nil) - end) - ]] + describe('`keepjumps` context', function() + pending('works', function() + local out = exec_lua [[ + api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb', 'ccc' }) + local jumplist_before = fn.getjumplist() + -- Should work as `vim.cmd('keepjumps normal! Ggg')` + vim._with({ keepjumps = true }, function() + vim.cmd('normal! Ggg') + end) + return vim.deep_equal(jumplist_before, fn.getjumplist()) + ]] + eq(true, out) + end) + + it('can be nested', function() + local out = exec_lua [[ + api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb', 'ccc' }) + local jumplist_before = fn.getjumplist() + vim._with({ keepjumps = true }, function() + vim._with({ keepjumps = false }, function() + vim.cmd('normal! Ggg') + end) + end) + return vim.deep_equal(jumplist_before, fn.getjumplist()) + ]] + eq(false, out) + end) end) - it('can access window options', function() - command('vsplit') - local win1 = api.nvim_get_current_win() - command('wincmd w') - local win2 = exec_lua [[ - win2 = vim.api.nvim_get_current_win() - return win2 - ]] - command('wincmd p') + describe('`keepmarks` context', function() + pending('works', function() + local out = exec_lua [[ + vim.cmd('set cpoptions+=R') + api.nvim_buf_set_lines(0, 0, -1, false, { 'bbb', 'ccc', 'aaa' }) + api.nvim_buf_set_mark(0, 'm', 2, 2, {}) - eq('', api.nvim_get_option_value('winhighlight', { win = win1 })) - eq('', api.nvim_get_option_value('winhighlight', { win = win2 })) + -- Should be the same as `vim.cmd('keepmarks %!sort')` + vim._with({ keepmarks = true }, function() + vim.cmd('%!sort') + end) + return api.nvim_buf_get_mark(0, 'm') + ]] + eq({ 2, 2 }, out) + end) - local val = exec_lua [[ - return vim._with({win = win2}, function() - vim.cmd "setlocal winhighlight=Normal:Normal" - return vim.api.nvim_get_current_win() - end) - ]] + it('can be nested', function() + local out = exec_lua [[ + vim.cmd('set cpoptions+=R') + api.nvim_buf_set_lines(0, 0, -1, false, { 'bbb', 'ccc', 'aaa' }) + api.nvim_buf_set_mark(0, 'm', 2, 2, {}) - eq('', api.nvim_get_option_value('winhighlight', { win = win1 })) - eq('Normal:Normal', api.nvim_get_option_value('winhighlight', { win = win2 })) - eq(win1, api.nvim_get_current_win()) - eq(win2, val) + vim._with({ keepmarks = true }, function() + vim._with({ keepmarks = false }, function() + vim.cmd('%!sort') + end) + end) + return api.nvim_buf_get_mark(0, 'm') + ]] + eq({ 0, 2 }, out) + end) end) - it('does not cause ml_get errors with invalid visual selection', function() - -- Add lines to the current buffer and make another window looking into an empty buffer. - exec_lua [[ - _G.api = vim.api - _G.t = function(s) return api.nvim_replace_termcodes(s, true, true, true) end - _G.win_lines = api.nvim_get_current_win() - vim.cmd "new" - _G.win_empty = api.nvim_get_current_win() - api.nvim_set_current_win(win_lines) - api.nvim_buf_set_lines(0, 0, -1, true, {"a", "b", "c"}) - ]] + describe('`keepatterns` context', function() + pending('works', function() + local out = exec_lua [[ + api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb' }) + vim.cmd('/aaa') + -- Should be the same as `vim.cmd('keeppatterns /bbb')` + vim._with({ keeppatterns = true }, function() + vim.cmd('/bbb') + end) + return fn.getreg('/') + ]] + eq('aaa', out) + end) - -- Start Visual in current window, redraw in other window with fewer lines. - exec_lua [[ - api.nvim_feedkeys(t "G<C-V>", "txn", false) - vim._with({win = win_empty}, function() vim.cmd "redraw" end) - ]] + it('can be nested', function() + local out = exec_lua [[ + api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb' }) + vim.cmd('/aaa') + vim._with({ keeppatterns = true }, function() + vim._with({ keeppatterns = false }, function() + vim.cmd('/bbb') + end) + end) + return fn.getreg('/') + ]] + eq('bbb', out) + end) + end) - -- Start Visual in current window, extend it in other window with more lines. - exec_lua [[ - api.nvim_feedkeys(t "<Esc>gg", "txn", false) - api.nvim_set_current_win(win_empty) - api.nvim_feedkeys(t "gg<C-V>", "txn", false) - vim._with({win = win_lines}, function() api.nvim_feedkeys(t "G<C-V>", "txn", false) end) - vim.cmd "redraw" - ]] + describe('`lockmarks` context', function() + it('works', function() + local mark = exec_lua [[ + api.nvim_buf_set_lines(0, 0, 0, false, { 'aaa', 'bbb', 'ccc' }) + api.nvim_buf_set_mark(0, 'm', 2, 2, {}) + -- Should be same as `:lockmarks lua api.nvim_buf_set_lines(...)` + vim._with({ lockmarks = true }, function() + api.nvim_buf_set_lines(0, 0, 2, false, { 'uuu', 'vvv', 'www' }) + end) + return api.nvim_buf_get_mark(0, 'm') + ]] + eq({ 2, 2 }, mark) + end) + + it('can be nested', function() + local mark = exec_lua [[ + api.nvim_buf_set_lines(0, 0, 0, false, { 'aaa', 'bbb', 'ccc' }) + api.nvim_buf_set_mark(0, 'm', 2, 2, {}) + vim._with({ lockmarks = true }, function() + vim._with({ lockmarks = false }, function() + api.nvim_buf_set_lines(0, 0, 2, false, { 'uuu', 'vvv', 'www' }) + end) + end) + return api.nvim_buf_get_mark(0, 'm') + ]] + eq({ 0, 2 }, mark) + end) end) - it('updates ruler if cursor moved', function() - local screen = Screen.new(30, 5) - screen:set_default_attr_ids { - [1] = { reverse = true }, - [2] = { bold = true, reverse = true }, - } - screen:attach() - exec_lua [[ - _G.api = vim.api - vim.opt.ruler = true - local lines = {} - for i = 0, 499 do lines[#lines + 1] = tostring(i) end - api.nvim_buf_set_lines(0, 0, -1, true, lines) - api.nvim_win_set_cursor(0, {20, 0}) - vim.cmd "split" - _G.win = api.nvim_get_current_win() - vim.cmd "wincmd w | redraw" - ]] - screen:expect [[ - 19 | - {1:[No Name] [+] 20,1 3%}| - ^19 | - {2:[No Name] [+] 20,1 3%}| - | - ]] - exec_lua [[ - vim._with({win = win}, function() api.nvim_win_set_cursor(0, {100, 0}) end) - vim.cmd "redraw" - ]] - screen:expect [[ - 99 | - {1:[No Name] [+] 100,1 19%}| - ^19 | - {2:[No Name] [+] 20,1 3%}| - | + describe('`noautocmd` context', function() + it('works', function() + local out = exec_lua [[ + _G.n_events = 0 + vim.cmd('au ModeChanged * lua _G.n_events = _G.n_events + 1') + -- Should be the same as `vim.cmd('noautocmd normal! vv')` + vim._with({ noautocmd = true }, function() + vim.cmd('normal! vv') + end) + return _G.n_events + ]] + eq(0, out) + end) + + it('works with User events', function() + local out = exec_lua [[ + _G.n_events = 0 + vim.cmd('au User MyEvent lua _G.n_events = _G.n_events + 1') + -- Should be the same as `vim.cmd('noautocmd doautocmd User MyEvent')` + vim._with({ noautocmd = true }, function() + api.nvim_exec_autocmds('User', { pattern = 'MyEvent' }) + end) + return _G.n_events + ]] + eq(0, out) + end) + + pending('can be nested', function() + local out = exec_lua [[ + _G.n_events = 0 + vim.cmd('au ModeChanged * lua _G.n_events = _G.n_events + 1') + vim._with({ noautocmd = true }, function() + vim._with({ noautocmd = false }, function() + vim.cmd('normal! vv') + end) + end) + return _G.n_events + ]] + eq(2, out) + end) + end) + + describe('`sandbox` context', function() + it('works', function() + local ok, err = pcall( + exec_lua, + [[ + -- Should work as `vim.cmd('sandbox call append(0, "aaa")')` + vim._with({ sandbox = true }, function() + fn.append(0, 'aaa') + end) + ]] + ) + eq(false, ok) + matches('Not allowed in sandbox', err) + end) + + it('can NOT be nested', function() + -- This behavior is intentionally different from other flags as allowing + -- disabling `sandbox` from nested function seems to be against the point + -- of using `sandbox` context in the first place + local ok, err = pcall( + exec_lua, + [[ + vim._with({ sandbox = true }, function() + vim._with({ sandbox = false }, function() + fn.append(0, 'aaa') + end) + end) + ]] + ) + eq(false, ok) + matches('Not allowed in sandbox', err) + end) + end) + + describe('`silent` context', function() + it('works', function() + exec_lua [[ + -- Should be same as `vim.cmd('silent lua print("aaa")')` + vim._with({ silent = true }, function() print('aaa') end) + ]] + eq('', exec_capture('messages')) + + exec_lua [[ vim._with({ silent = true }, function() vim.cmd.echomsg('"bbb"') end) ]] + eq('', exec_capture('messages')) + + local screen = Screen.new(20, 5) + screen:set_default_attr_ids { + [1] = { bold = true, reverse = true }, + [2] = { bold = true, foreground = Screen.colors.Blue }, + } + screen:attach() + exec_lua [[ vim._with({ silent = true }, function() vim.cmd.echo('"ccc"') end) ]] + screen:expect [[ + ^ | + {2:~ }|*3 + | + ]] + end) + + pending('can be nested', function() + exec_lua [[ vim._with({ silent = true }, function() + vim._with({ silent = false }, function() + print('aaa') + end) + end)]] + eq('aaa', exec_capture('messages')) + end) + end) + + describe('`unsilent` context', function() + it('works', function() + exec_lua [[ + _G.f = function() + -- Should be same as `vim.cmd('unsilent lua print("aaa")')` + vim._with({ unsilent = true }, function() print('aaa') end) + end + ]] + command('silent lua f()') + eq('aaa', exec_capture('messages')) + end) + + pending('can be nested', function() + exec_lua [[ + _G.f = function() + vim._with({ unsilent = true }, function() + vim._with({ unsilent = false }, function() print('aaa') end) + end) + end + ]] + command('silent lua f()') + eq('', exec_capture('messages')) + end) + end) + + describe('`win` context', function() + it('works', function() + local out = exec_lua [[ + local other_win, cur_win = setup_windows() + local inner = vim._with({ win = other_win }, function() + return api.nvim_get_current_win() + end) + return { inner == other_win, api.nvim_get_current_win() == cur_win } + ]] + eq({ true, true }, out) + end) + + it('does not trigger events', function() + exec_lua [[ + _G.test_events = { 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave' } + _G.test_context = { win = other_win } + _G.test_trig_event = function() vim.cmd.new() end + ]] + validate_events_trigger() + end) + + it('can access window options', function() + local out = exec_lua [[ + local other_win, cur_win = setup_windows() + vim.wo[other_win].winblend = 10 + vim.wo[cur_win].winblend = 25 + + vim._with({ win = other_win }, function() + vim.cmd.setlocal('winblend=0') + end) + + return vim.wo[other_win].winblend == 0 and vim.wo[cur_win].winblend == 25 + ]] + eq(true, out) + end) + + it('works with different kinds of windows', function() + exec_lua [[ + local validate = function(win) + vim._with({ win = win }, function() + assert(api.nvim_get_current_win() == win) + end) + end + + -- Current + validate(api.nvim_get_current_win()) + + -- Not visible + local other_win, cur_win = setup_windows() + vim.cmd.tabnew() + validate(other_win) + + -- Floating + local float_win = api.nvim_open_win( + api.nvim_create_buf(false, true), + false, + { relative = 'editor', row = 1, col = 1, height = 5, width = 5} + ) + validate(float_win) + ]] + end) + + it('does not cause ml_get errors with invalid visual selection', function() + exec_lua [[ + local feedkeys = function(keys) api.nvim_feedkeys(vim.keycode(keys), 'txn', false) end + + -- Add lines to the current buffer and make another window looking into an empty buffer. + local win_empty, win_lines = setup_windows() + api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b', 'c' }) + + -- Start Visual in current window, redraw in other window with fewer lines. + -- Should be fixed by vim-patch:8.2.4018. + feedkeys('G<C-V>') + vim._with({ win = win_empty }, function() vim.cmd.redraw() end) + + -- Start Visual in current window, extend it in other window with more lines. + -- Fixed for win_execute by vim-patch:8.2.4026, but nvim_win_call should also not be affected. + feedkeys('<Esc>gg') + api.nvim_set_current_win(win_empty) + feedkeys('gg<C-V>') + vim._with({ win = win_lines }, function() feedkeys('G<C-V>') end) + vim.cmd.redraw() + ]] + end) + + it('can be nested', function() + exec_lua [[ + local other_win, cur_win = setup_windows() + vim._with({ win = other_win }, function() + assert(api.nvim_get_current_win() == other_win) + inner = vim._with({ win = cur_win }, function() + assert(api.nvim_get_current_win() == cur_win) + end) + assert(api.nvim_get_current_win() == other_win) + end) + assert(api.nvim_get_current_win() == cur_win) + ]] + end) + + it('updates ruler if cursor moved', function() + local screen = Screen.new(30, 5) + screen:set_default_attr_ids { + [1] = { reverse = true }, + [2] = { bold = true, reverse = true }, + } + screen:attach() + exec_lua [[ + vim.opt.ruler = true + local lines = {} + for i = 0, 499 do lines[#lines + 1] = tostring(i) end + api.nvim_buf_set_lines(0, 0, -1, true, lines) + api.nvim_win_set_cursor(0, { 20, 0 }) + vim.cmd 'split' + _G.win = api.nvim_get_current_win() + vim.cmd "wincmd w | redraw" + ]] + screen:expect [[ + 19 | + {1:[No Name] [+] 20,1 3%}| + ^19 | + {2:[No Name] [+] 20,1 3%}| + | + ]] + exec_lua [[ + vim._with({ win = win }, function() api.nvim_win_set_cursor(0, { 100, 0 }) end) + vim.cmd "redraw" + ]] + screen:expect [[ + 99 | + {1:[No Name] [+] 100,1 19%}| + ^19 | + {2:[No Name] [+] 20,1 3%}| + | + ]] + end) + + it('layout in current tabpage does not affect windows in others', function() + command('tab split') + local t2_move_win = api.nvim_get_current_win() + command('vsplit') + local t2_other_win = api.nvim_get_current_win() + command('tabprevious') + matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)')) + command('vsplit') + + exec_lua('vim._with({ win = ... }, function() vim.cmd.wincmd "J" end)', t2_move_win) + eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2)) + end) + end) + + it('returns what callback returns', function() + local out_verify = exec_lua [[ + out = { vim._with({}, function() + return 'a', 2, nil, { 4 }, function() end + end) } + return { + out[1] == 'a', out[2] == 2, out[3] == nil, + vim.deep_equal(out[4], { 4 }), + type(out[5]) == 'function', + vim.tbl_count(out), + } ]] + eq({ true, true, true, true, true, 4 }, out_verify) end) it('can return values by reference', function() - eq( - { 7, 10 }, - exec_lua [[ - local val = {4, 10} - local ref = vim._with({win = 0}, function() return val end) + local out = exec_lua [[ + local val = { 4, 10 } + local ref = vim._with({}, function() return val end) ref[1] = 7 return val ]] - ) + eq({ 7, 10 }, out) end) - it('layout in current tabpage does not affect windows in others', function() - command('tab split') - local t2_move_win = api.nvim_get_current_win() - command('vsplit') - local t2_other_win = api.nvim_get_current_win() - command('tabprevious') - matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)')) - command('vsplit') - - exec_lua('vim._with({win = ...}, function() vim.cmd.wincmd "J" end)', t2_move_win) - eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2)) + it('can not work with conflicting `buf` and `win`', function() + local out = exec_lua [[ + local other_buf, cur_buf = setup_buffers() + local other_win, cur_win = setup_windows() + assert(api.nvim_win_get_buf(other_win) ~= other_buf) + local _, err = pcall(vim._with, { buf = other_buf, win = other_win }, function() end) + return err + ]] + matches('Can not set both `buf` and `win`', out) end) -end) -describe('vim._with {lockmarks = true}', function() - it('is reset', function() - local mark = exec_lua [[ - vim.api.nvim_buf_set_lines(0, 0, 0, false, {"marky", "snarky", "malarkey"}) - vim.api.nvim_buf_set_mark(0,"m",1,0, {}) - vim._with({lockmarks = true}, function() - vim.api.nvim_buf_set_lines(0, 0, 2, false, {"mass", "mess", "moss"}) - end) - return vim.api.nvim_buf_get_mark(0,"m") + pending('can forward command modifiers to user command', function() + local out = exec_lua [[ + local test_flags = { + 'emsg_silent', + 'hide', + 'keepalt', + 'keepjumps', + 'keepmarks', + 'keeppatterns', + 'lockmarks', + 'noautocmd', + 'silent', + 'unsilent', + } + + local used_smods + local command = function(data) + used_smods = data.smods + end + api.nvim_create_user_command('DummyLog', command, {}) + + local res = {} + for _, flag in ipairs(test_flags) do + used_smods = nil + vim._with({ [flag] = true }, function() vim.cmd('DummyLog') end) + res[flag] = used_smods[flag] + end + return res ]] - t.eq(mark, { 1, 0 }) + for k, v in pairs(out) do + eq({ k, true }, { k, v }) + end + end) + + it('handles error in callback', function() + -- Should still restore initial context + local out_buf = exec_lua [[ + local other_buf, cur_buf = setup_buffers() + + local context = { buf = other_buf } + local ok, err = pcall(vim._with, context, function() error('Oops buf', 0) end) + + return { + ok, + err, + api.nvim_get_current_buf() == cur_buf, + } + ]] + eq({ false, 'Oops buf', true }, out_buf) + + local out_win = exec_lua [[ + local other_win, cur_win = setup_windows() + vim.wo[other_win].winblend = 25 + + local context = { win = other_win, wo = { winblend = 50 } } + local ok, err = pcall(vim._with, context, function() error('Oops win', 0) end) + + return { + ok, + err, + api.nvim_get_current_win() == cur_win, + vim.wo[other_win].winblend, + } + ]] + eq({ false, 'Oops win', true, 25 }, out_win) + end) + + it('validates arguments', function() + exec_lua [[ + _G.get_error = function(...) + local _, err = pcall(vim._with, ...) + return err or '' + end + ]] + local get_error = function(string_args) + return exec_lua('return get_error(' .. string_args .. ')') + end + + matches('context.*table', get_error("'a', function() end")) + matches('f.*function', get_error('{}, 1')) + + local validate_context = function(bad_context, expected_type) + local bad_field = vim.tbl_keys(bad_context)[1] + matches( + 'context%.' .. bad_field .. '.*' .. expected_type, + get_error(vim.inspect(bad_context) .. ', function() end') + ) + end + + validate_context({ buf = 'a' }, 'number') + validate_context({ emsg_silent = 1 }, 'boolean') + validate_context({ hide = 1 }, 'boolean') + validate_context({ keepalt = 1 }, 'boolean') + validate_context({ keepjumps = 1 }, 'boolean') + validate_context({ keepmarks = 1 }, 'boolean') + validate_context({ keeppatterns = 1 }, 'boolean') + validate_context({ lockmarks = 1 }, 'boolean') + validate_context({ noautocmd = 1 }, 'boolean') + validate_context({ sandbox = 1 }, 'boolean') + validate_context({ silent = 1 }, 'boolean') + validate_context({ unsilent = 1 }, 'boolean') + validate_context({ win = 'a' }, 'number') + + matches('Invalid buffer', get_error('{ buf = -1 }, function() end')) + matches('Invalid window', get_error('{ win = -1 }, function() end')) end) end) |