local t = require('test.testutil') local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') local fn = n.fn 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 describe('vim._with', function() before_each(function() n.clear() exec_lua([[ _G.fn = vim.fn _G.api = vim.api _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) 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 } ]] eq({ true, true }, out) end 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) 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' 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) 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'), '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) 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) 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) 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) ]] ) eq(false, ok) end) end) 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 [[ -- 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) 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) 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) 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, {}) -- 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) 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, {}) 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) 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) 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) 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) 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') 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('gg') api.nvim_set_current_win(win_empty) feedkeys('gg') vim._with({ win = win_lines }, function() feedkeys('G') 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() 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('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) 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 ]] 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)