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 assert_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('`bo` context', function() before_each(function() exec_lua [[ _G.other_buf, _G.cur_buf = setup_buffers() -- 'commentstring' is local to buffer and string vim.bo[other_buf].commentstring = '## %s' vim.bo[cur_buf].commentstring = '// %s' vim.go.commentstring = '$$ %s' -- 'undolevels' is global or local to buffer (global-local) and number vim.bo[other_buf].undolevels = 100 vim.bo[cur_buf].undolevels = 250 vim.go.undolevels = 500 _G.get_state = function() return { bo = { cms_cur = vim.bo[cur_buf].commentstring, cms_other = vim.bo[other_buf].commentstring, ul_cur = vim.bo[cur_buf].undolevels, ul_other = vim.bo[other_buf].undolevels, }, go = { cms = vim.go.commentstring, ul = vim.go.undolevels, }, } end ]] end) it('works', function() local out = exec_lua [[ local context = { bo = { commentstring = '-- %s', undolevels = 0 } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == cur_buf) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '-- %s', cms_other = '## %s', ul_cur = 0, ul_other = 100 }, go = { cms = '$$ %s', ul = 500 }, }, out.inner) eq(out.before, out.after) end) it('sets options in `buf` context', function() local out = exec_lua [[ local context = { buf = other_buf, bo = { commentstring = '-- %s', undolevels = 0 } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == other_buf) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '// %s', cms_other = '-- %s', ul_cur = 250, ul_other = 0 }, go = { cms = '$$ %s', ul = 500 }, }, out.inner) eq(out.before, out.after) end) it('restores only options from context', function() local out = exec_lua [[ local context = { bo = { commentstring = '-- %s' } } local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == cur_buf) vim.bo[cur_buf].undolevels = 750 vim.bo[cur_buf].commentstring = '!! %s' return get_state() end) return { inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '!! %s', cms_other = '## %s', ul_cur = 750, ul_other = 100 }, go = { cms = '$$ %s', ul = 500 }, }, out.inner) eq({ bo = { cms_cur = '// %s', cms_other = '## %s', ul_cur = 750, ul_other = 100 }, go = { cms = '$$ %s', ul = 500 }, }, out.after) end) it('does not trigger events', function() exec_lua [[ _G.test_events = { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' } _G.test_context = { bo = { commentstring = '-- %s' } } _G.test_trig_event = function() vim.cmd.new() end ]] assert_events_trigger() end) it('can be nested', function() local out = exec_lua [[ local before, before_inner, after_inner = get_state(), nil, nil vim._with({ bo = { commentstring = '-- %s', undolevels = 0 } }, function() before_inner = get_state() inner = vim._with({ bo = { commentstring = '!! %s' } }, get_state) after_inner = get_state() end) return { before = before, before_inner = before_inner, inner = inner, after_inner = after_inner, after = get_state(), } ]] eq('!! %s', out.inner.bo.cms_cur) eq(0, out.inner.bo.ul_cur) eq(out.before_inner, out.after_inner) eq(out.before, out.after) end) 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 ]] assert_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 assert_buf = function(buf) vim._with({ buf = buf }, function() assert(api.nvim_get_current_buf() == buf) end) end -- Current assert_buf(api.nvim_get_current_buf()) -- Hidden listed local listed = api.nvim_create_buf(true, true) assert_buf(listed) -- Visible local other_win, cur_win = setup_windows() api.nvim_win_set_buf(other_win, listed) assert_buf(listed) -- Shown but not visible vim.cmd.tabnew() assert_buf(listed) -- Shown in several windows api.nvim_win_set_buf(0, listed) assert_buf(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) assert_buf(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('`env` context', function() before_each(function() exec_lua [[ vim.fn.setenv('aaa', 'hello') _G.get_state = function() return { aaa = vim.fn.getenv('aaa'), bbb = vim.fn.getenv('bbb') } end ]] end) it('works', function() local out = exec_lua [[ local context = { env = { aaa = 'inside', bbb = 'wow' } } local before = get_state() local inner = vim._with(context, get_state) return { before = before, inner = inner, after = get_state() } ]] eq({ aaa = 'inside', bbb = 'wow' }, out.inner) eq(out.before, out.after) end) it('restores only variables from context', function() local out = exec_lua [[ local context = { env = { bbb = 'wow' } } local before = get_state() local inner = vim._with(context, function() vim.env.aaa = 'inside' return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ aaa = 'inside', bbb = 'wow' }, out.inner) eq({ aaa = 'inside', bbb = vim.NIL }, out.after) end) it('can be nested', function() local out = exec_lua [[ local before, before_inner, after_inner = get_state(), nil, nil vim._with({ env = { aaa = 'inside', bbb = 'wow' } }, function() before_inner = get_state() inner = vim._with({ env = { aaa = 'more inside' } }, get_state) after_inner = get_state() end) return { before = before, before_inner = before_inner, inner = inner, after_inner = after_inner, after = get_state(), } ]] eq('more inside', out.inner.aaa) eq('wow', out.inner.bbb) eq(out.before_inner, out.after_inner) eq(out.before, out.after) end) end) describe('`go` context', function() before_each(function() exec_lua [[ vim.bo.commentstring = '## %s' vim.go.commentstring = '$$ %s' vim.wo.winblend = 25 vim.go.winblend = 50 vim.go.langmap = 'xy,yx' _G.get_state = function() return { bo = { cms = vim.bo.commentstring }, wo = { winbl = vim.wo.winblend }, go = { cms = vim.go.commentstring, winbl = vim.go.winblend, lmap = vim.go.langmap, }, } end ]] end) it('works', function() local out = exec_lua [[ local context = { go = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba' }, } local before = get_state() local inner = vim._with(context, get_state) return { before = before, inner = inner, after = get_state() } ]] eq({ bo = { cms = '## %s' }, wo = { winbl = 25 }, go = { cms = '-- %s', winbl = 75, lmap = 'ab,ba' }, }, out.inner) eq(out.before, out.after) end) it('works with `eventignore`', function() -- This might be an issue if saving and restoring option context is done -- to account for triggering `OptionSet`, but in not a good way local out = exec_lua [[ vim.go.eventignore = 'ModeChanged' local inner = vim._with({ go = { eventignore = 'CursorMoved' } }, function() return vim.go.eventignore end) return { inner = inner, after = vim.go.eventignore } ]] eq({ inner = 'CursorMoved', after = 'ModeChanged' }, out) end) it('restores only options from context', function() local out = exec_lua [[ local context = { go = { langmap = 'ab,ba' } } local inner = vim._with(context, function() vim.go.commentstring = '!! %s' vim.go.winblend = 75 vim.go.langmap = 'uv,vu' return get_state() end) return { inner = inner, after = get_state() } ]] eq({ bo = { cms = '## %s' }, wo = { winbl = 25 }, go = { cms = '!! %s', winbl = 75, lmap = 'uv,vu' }, }, out.inner) eq({ bo = { cms = '## %s' }, wo = { winbl = 25 }, go = { cms = '!! %s', winbl = 75, lmap = 'xy,yx' }, }, out.after) end) it('does not trigger events', function() exec_lua [[ _G.test_events = { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave', 'WinEnter', 'WinLeave' } _G.test_context = { go = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba' } } _G.test_trig_event = function() vim.cmd.new() end ]] assert_events_trigger() end) it('can be nested', function() local out = exec_lua [[ local before, before_inner, after_inner = get_state(), nil, nil vim._with({ go = { langmap = 'ab,ba', commentstring = '-- %s' } }, function() before_inner = get_state() inner = vim._with({ go = { langmap = 'uv,vu' } }, get_state) after_inner = get_state() end) return { before = before, before_inner = before_inner, inner = inner, after_inner = after_inner, after = get_state(), } ]] eq('uv,vu', out.inner.go.lmap) eq('-- %s', out.inner.go.cms) eq(out.before_inner, out.after_inner) eq(out.before, out.after) 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('`o` context', function() before_each(function() exec_lua [[ _G.other_win, _G.cur_win = setup_windows() _G.other_buf, _G.cur_buf = setup_buffers() vim.bo[other_buf].commentstring = '## %s' vim.bo[cur_buf].commentstring = '// %s' vim.go.commentstring = '$$ %s' vim.bo[other_buf].undolevels = 100 vim.bo[cur_buf].undolevels = 250 vim.go.undolevels = 500 vim.wo[other_win].virtualedit = 'block' vim.wo[cur_win].virtualedit = 'insert' vim.go.virtualedit = 'none' vim.wo[other_win].winblend = 10 vim.wo[cur_win].winblend = 25 vim.go.winblend = 50 vim.go.langmap = 'xy,yx' _G.get_state = function() return { bo = { cms_cur = vim.bo[cur_buf].commentstring, cms_other = vim.bo[other_buf].commentstring, ul_cur = vim.bo[cur_buf].undolevels, ul_other = vim.bo[other_buf].undolevels, }, wo = { ve_cur = vim.wo[cur_win].virtualedit, ve_other = vim.wo[other_win].virtualedit, winbl_cur = vim.wo[cur_win].winblend, winbl_other = vim.wo[other_win].winblend, }, go = { cms = vim.go.commentstring, ul = vim.go.undolevels, ve = vim.go.virtualedit, winbl = vim.go.winblend, lmap = vim.go.langmap, }, } end ]] end) it('works', function() local out = exec_lua [[ local context = { o = { commentstring = '-- %s', undolevels = 0, virtualedit = 'all', winblend = 75, langmap = 'ab,ba', }, } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == cur_buf) assert(api.nvim_get_current_win() == cur_win) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] -- Options in context are set with `vim.o`, so usually both local -- and global values are affected. Yet all of them should be later -- restored to pre-context values. eq({ bo = { cms_cur = '-- %s', cms_other = '## %s', ul_cur = -123456, ul_other = 100 }, wo = { ve_cur = 'all', ve_other = 'block', winbl_cur = 75, winbl_other = 10 }, go = { cms = '-- %s', ul = 0, ve = 'all', winbl = 75, lmap = 'ab,ba' }, }, out.inner) eq(out.before, out.after) end) it('sets options in `buf` context', function() local out = exec_lua [[ local context = { buf = other_buf, o = { commentstring = '-- %s', undolevels = 0 } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == other_buf) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '// %s', cms_other = '-- %s', ul_cur = 250, ul_other = -123456 }, wo = { ve_cur = 'insert', ve_other = 'block', winbl_cur = 25, winbl_other = 10 }, -- Global `winbl` inside context ideally should be untouched and equal -- to 50. It seems to be equal to 0 because `context.buf` uses -- `aucmd_prepbuf` C approach which has no guarantees about window or -- window option values inside context. go = { cms = '-- %s', ul = 0, ve = 'none', winbl = 0, lmap = 'xy,yx' }, }, out.inner) eq(out.before, out.after) end) it('sets options in `win` context', function() local out = exec_lua [[ local context = { win = other_win, o = { winblend = 75, virtualedit = 'all' } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_win() == other_win) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '// %s', cms_other = '## %s', ul_cur = 250, ul_other = 100 }, wo = { winbl_cur = 25, winbl_other = 75, ve_cur = 'insert', ve_other = 'all' }, go = { cms = '$$ %s', ul = 500, winbl = 75, ve = 'all', lmap = 'xy,yx' }, }, out.inner) eq(out.before, out.after) end) it('restores only options from context', function() local out = exec_lua [[ local context = { o = { undolevels = 0, winblend = 75, langmap = 'ab,ba' } } local inner = vim._with(context, function() assert(api.nvim_get_current_buf() == cur_buf) assert(api.nvim_get_current_win() == cur_win) vim.o.commentstring = '!! %s' vim.o.undolevels = 750 vim.o.virtualedit = 'onemore' vim.o.winblend = 99 vim.o.langmap = 'uv,vu' return get_state() end) return { inner = inner, after = get_state() } ]] eq({ bo = { cms_cur = '!! %s', cms_other = '## %s', ul_cur = -123456, ul_other = 100 }, wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 99, winbl_other = 10 }, go = { cms = '!! %s', ul = 750, ve = 'onemore', winbl = 99, lmap = 'uv,vu' }, }, out.inner) eq({ bo = { cms_cur = '!! %s', cms_other = '## %s', ul_cur = 250, ul_other = 100 }, wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 25, winbl_other = 10 }, go = { cms = '!! %s', ul = 500, ve = 'onemore', winbl = 50, lmap = 'xy,yx' }, }, out.after) end) it('does not trigger events', function() exec_lua [[ _G.test_events = { 'BufEnter', 'BufLeave', 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave' } _G.test_context = { o = { undolevels = 0, winblend = 75, langmap = 'ab,ba' } } _G.test_trig_event = function() vim.cmd.new() end ]] assert_events_trigger() end) it('can be nested', function() local out = exec_lua [[ local before, before_inner, after_inner = get_state(), nil, nil local cxt_o = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba', undolevels = 0 } vim._with({ o = cxt_o }, function() before_inner = get_state() local inner_cxt_o = { commentstring = '!! %s', winblend = 99, langmap = 'uv,vu' } inner = vim._with({ o = inner_cxt_o }, get_state) after_inner = get_state() end) return { before = before, before_inner = before_inner, inner = inner, after_inner = after_inner, after = get_state(), } ]] eq('!! %s', out.inner.bo.cms_cur) eq(99, out.inner.wo.winbl_cur) eq('uv,vu', out.inner.go.lmap) eq(0, out.inner.go.ul) eq(out.before_inner, out.after_inner) eq(out.before, out.after) 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 }, } 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 ]] assert_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 assert_win = function(win) vim._with({ win = win }, function() assert(api.nvim_get_current_win() == win) end) end -- Current assert_win(api.nvim_get_current_win()) -- Not visible local other_win, cur_win = setup_windows() vim.cmd.tabnew() assert_win(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} ) assert_win(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 }, } 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) describe('`wo` context', function() before_each(function() exec_lua [[ _G.other_win, _G.cur_win = setup_windows() -- 'virtualedit' is global or local to window (global-local) and string vim.wo[other_win].virtualedit = 'block' vim.wo[cur_win].virtualedit = 'insert' vim.go.virtualedit = 'none' -- 'winblend' is local to window and number vim.wo[other_win].winblend = 10 vim.wo[cur_win].winblend = 25 vim.go.winblend = 50 _G.get_state = function() return { wo = { ve_cur = vim.wo[cur_win].virtualedit, ve_other = vim.wo[other_win].virtualedit, winbl_cur = vim.wo[cur_win].winblend, winbl_other = vim.wo[other_win].winblend, }, go = { ve = vim.go.virtualedit, winbl = vim.go.winblend, }, } end ]] end) it('works', function() local out = exec_lua [[ local context = { wo = { virtualedit = 'all', winblend = 75 } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_win() == cur_win) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ wo = { ve_cur = 'all', ve_other = 'block', winbl_cur = 75, winbl_other = 10 }, go = { ve = 'none', winbl = 75 }, }, out.inner) eq(out.before, out.after) end) it('sets options in `win` context', function() local out = exec_lua [[ local context = { win = other_win, wo = { virtualedit = 'all', winblend = 75 } } local before = get_state() local inner = vim._with(context, function() assert(api.nvim_get_current_win() == other_win) return get_state() end) return { before = before, inner = inner, after = get_state() } ]] eq({ wo = { ve_cur = 'insert', ve_other = 'all', winbl_cur = 25, winbl_other = 75 }, go = { ve = 'none', winbl = 75 }, }, out.inner) eq(out.before, out.after) end) it('restores only options from context', function() local out = exec_lua [[ local context = { wo = { winblend = 75 } } local inner = vim._with(context, function() assert(api.nvim_get_current_win() == cur_win) vim.wo[cur_win].virtualedit = 'onemore' vim.wo[cur_win].winblend = 99 return get_state() end) return { inner = inner, after = get_state() } ]] eq({ wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 99, winbl_other = 10 }, go = { ve = 'none', winbl = 99 }, }, out.inner) eq({ wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 25, winbl_other = 10 }, go = { ve = 'none', winbl = 50 }, }, out.after) end) it('does not trigger events', function() exec_lua [[ _G.test_events = { 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave' } _G.test_context = { wo = { winblend = 75 } } _G.test_trig_event = function() vim.cmd.new() end ]] assert_events_trigger() end) it('can be nested', function() local out = exec_lua [[ local before, before_inner, after_inner = get_state(), nil, nil vim._with({ wo = { winblend = 75, virtualedit = 'all' } }, function() before_inner = get_state() inner = vim._with({ wo = { winblend = 99 } }, get_state) after_inner = get_state() end) return { before = before, before_inner = before_inner, inner = inner, after_inner = after_inner, after = get_state(), } ]] eq(99, out.inner.wo.winbl_cur) eq('all', out.inner.wo.ve_cur) eq(out.before_inner, out.after_inner) eq(out.before, out.after) 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) it('works with several contexts at once', function() local out = exec_lua [[ local other_buf, cur_buf = setup_buffers() vim.bo[other_buf].commentstring = '## %s' api.nvim_buf_set_lines(other_buf, 0, -1, false, { 'aaa', 'bbb', 'ccc' }) api.nvim_buf_set_mark(other_buf, 'm', 2, 2, {}) vim.go.commentstring = '// %s' vim.go.langmap = 'xy,yx' local context = { buf = other_buf, bo = { commentstring = '-- %s' }, go = { langmap = 'ab,ba' }, lockmarks = true, } local inner = vim._with(context, function() api.nvim_buf_set_lines(0, 0, -1, false, { 'uuu', 'vvv', 'www' }) return { buf = api.nvim_get_current_buf(), bo = { cms = vim.bo.commentstring }, go = { cms = vim.go.commentstring, lmap = vim.go.langmap }, mark = api.nvim_buf_get_mark(0, 'm') } end) local after = { buf = api.nvim_get_current_buf(), bo = { cms = vim.bo[other_buf].commentstring }, go = { cms = vim.go.commentstring, lmap = vim.go.langmap }, mark = api.nvim_buf_get_mark(other_buf, 'm') } return { context_buf = other_buf, cur_buf = cur_buf, inner = inner, after = after } ]] eq({ buf = out.context_buf, bo = { cms = '-- %s' }, go = { cms = '// %s', lmap = 'ab,ba' }, mark = { 2, 2 }, }, out.inner) eq({ buf = out.cur_buf, bo = { cms = '## %s' }, go = { cms = '// %s', lmap = 'xy,yx' }, mark = { 2, 2 }, }, out.after) end) it('works with same option set in different contexts', function() local out = exec_lua [[ local get_state = function() return { bo = { cms = vim.bo.commentstring }, wo = { ve = vim.wo.virtualedit }, go = { cms = vim.go.commentstring, ve = vim.go.virtualedit }, } end vim.bo.commentstring = '// %s' vim.go.commentstring = '$$ %s' vim.wo.virtualedit = 'insert' vim.go.virtualedit = 'none' local before = get_state() local context_no_go = { o = { commentstring = '-- %s', virtualedit = 'all' }, bo = { commentstring = '!! %s' }, wo = { virtualedit = 'onemore' }, } local inner_no_go = vim._with(context_no_go, get_state) local middle = get_state() local context_with_go = { o = { commentstring = '-- %s', virtualedit = 'all' }, bo = { commentstring = '!! %s' }, wo = { virtualedit = 'onemore' }, go = { commentstring = '@@ %s', virtualedit = 'block' }, } local inner_with_go = vim._with(context_with_go, get_state) return { before = before, inner_no_go = inner_no_go, middle = middle, inner_with_go = inner_with_go, after = get_state(), } ]] -- Should prefer explicit local scopes instead of `o` eq({ bo = { cms = '!! %s' }, wo = { ve = 'onemore' }, go = { cms = '-- %s', ve = 'all' }, }, out.inner_no_go) eq(out.before, out.middle) -- Should prefer explicit global scopes instead of `o` eq({ bo = { cms = '!! %s' }, wo = { ve = 'onemore' }, go = { cms = '@@ %s', ve = 'block' }, }, out.inner_with_go) eq(out.middle, out.after) 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() vim.bo[other_buf].commentstring = '## %s' local context = { buf = other_buf, bo = { commentstring = '-- %s' } } local ok, err = pcall(vim._with, context, function() error('Oops buf', 0) end) return { ok, err, api.nvim_get_current_buf() == cur_buf, vim.bo[other_buf].commentstring, } ]] eq({ false, 'Oops buf', true, '## %s' }, 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('handles not supported option', function() local out = exec_lua [[ -- Should still restore initial state vim.bo.commentstring = '## %s' local context = { o = { commentstring = '-- %s' }, bo = { winblend = 10 } } local ok, err = pcall(vim._with, context, function() end) return { ok = ok, err = err, cms = vim.bo.commentstring } ]] eq(false, out.ok) matches('window.*option.*winblend', out.err) eq('## %s', out.cms) 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 assert_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 assert_context({ bo = 1 }, 'table') assert_context({ buf = 'a' }, 'number') assert_context({ emsg_silent = 1 }, 'boolean') assert_context({ env = 1 }, 'table') assert_context({ go = 1 }, 'table') assert_context({ hide = 1 }, 'boolean') assert_context({ keepalt = 1 }, 'boolean') assert_context({ keepjumps = 1 }, 'boolean') assert_context({ keepmarks = 1 }, 'boolean') assert_context({ keeppatterns = 1 }, 'boolean') assert_context({ lockmarks = 1 }, 'boolean') assert_context({ noautocmd = 1 }, 'boolean') assert_context({ o = 1 }, 'table') assert_context({ sandbox = 1 }, 'boolean') assert_context({ silent = 1 }, 'boolean') assert_context({ unsilent = 1 }, 'boolean') assert_context({ win = 'a' }, 'number') assert_context({ wo = 1 }, 'table') matches('Invalid buffer', get_error('{ buf = -1 }, function() end')) matches('Invalid window', get_error('{ win = -1 }, function() end')) end) it('no double-free when called from :filter browse oldfiles #31501', function() exec_lua([=[ vim.api.nvim_create_autocmd('BufEnter', { callback = function() vim._with({ lockmarks = true }, function() end) end, }) vim.cmd([[ let v:oldfiles = ['Xoldfile'] call nvim_input('1') noswapfile filter /Xoldfile/ browse oldfiles ]]) ]=]) n.assert_alive() eq('Xoldfile', fn.bufname('%')) end) end)