From f8795365deb88ab4e108858c563284b1082d06d4 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 21 Jun 2024 16:23:02 +0300 Subject: 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. --- test/functional/lua/with_spec.lua | 1044 +++++++++++++++++++++++++++++-------- 1 file changed, 814 insertions(+), 230 deletions(-) (limited to 'test') 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'), '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", "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", "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 "gg", "txn", false) - api.nvim_set_current_win(win_empty) - api.nvim_feedkeys(t "gg", "txn", false) - vim._with({win = win_lines}, function() api.nvim_feedkeys(t "G", "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') + 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() - 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) -- cgit From 07cc559cdf11acb3031b6b7ba53e65285a6f4b3f Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 21 Jun 2024 16:23:05 +0300 Subject: feat(lua): update `vim._with` to allow more granular option contexts Problem: with a single `context.options` there is no way for user to force which scope (local, global, both) is being temporarily set and later restored. Solution: replace single `options` context with `bo`, `go`, `wo`, and `o`. Naming and implementation follows how options can be set directly with `vim.*` (like `vim.bo`, etc.). Options are set for possible target `win` or `buf` context. --- test/functional/lua/with_spec.lua | 751 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 721 insertions(+), 30 deletions(-) (limited to 'test') diff --git a/test/functional/lua/with_spec.lua b/test/functional/lua/with_spec.lua index f93ef6f26b..aa2f4bb7f5 100644 --- a/test/functional/lua/with_spec.lua +++ b/test/functional/lua/with_spec.lua @@ -30,7 +30,7 @@ describe('vim._with', function() ]]) end) - local validate_events_trigger = function() + local assert_events_trigger = function() local out = exec_lua [[ -- Needs three global values defined: -- - `test_events` - array of events which are tested. @@ -56,6 +56,132 @@ describe('vim._with', function() 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 [[ @@ -74,7 +200,7 @@ describe('vim._with', function() _G.test_context = { buf = other_buf } _G.test_trig_event = function() vim.cmd.new() end ]] - validate_events_trigger() + assert_events_trigger() end) it('can access buffer options', function() @@ -95,37 +221,37 @@ describe('vim._with', function() it('works with different kinds of buffers', function() exec_lua [[ - local validate = function(buf) + local assert_buf = function(buf) vim._with({ buf = buf }, function() assert(api.nvim_get_current_buf() == buf) end) end -- Current - validate(api.nvim_get_current_buf()) + assert_buf(api.nvim_get_current_buf()) -- Hidden listed local listed = api.nvim_create_buf(true, true) - validate(listed) + assert_buf(listed) -- Visible local other_win, cur_win = setup_windows() api.nvim_win_set_buf(other_win, listed) - validate(listed) + assert_buf(listed) -- Shown but not visible vim.cmd.tabnew() - validate(listed) + assert_buf(listed) -- Shown in several windows api.nvim_win_set_buf(0, listed) - validate(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) - validate(float_buf) + assert_buf(float_buf) ]] end) @@ -212,6 +338,118 @@ describe('vim._with', function() 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( @@ -487,6 +725,202 @@ describe('vim._with', function() 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( @@ -599,7 +1033,7 @@ describe('vim._with', function() _G.test_context = { win = other_win } _G.test_trig_event = function() vim.cmd.new() end ]] - validate_events_trigger() + assert_events_trigger() end) it('can access window options', function() @@ -619,19 +1053,19 @@ describe('vim._with', function() it('works with different kinds of windows', function() exec_lua [[ - local validate = function(win) + local assert_win = function(win) vim._with({ win = win }, function() assert(api.nvim_get_current_win() == win) end) end -- Current - validate(api.nvim_get_current_win()) + assert_win(api.nvim_get_current_win()) -- Not visible local other_win, cur_win = setup_windows() vim.cmd.tabnew() - validate(other_win) + assert_win(other_win) -- Floating local float_win = api.nvim_open_win( @@ -639,7 +1073,7 @@ describe('vim._with', function() false, { relative = 'editor', row = 1, col = 1, height = 5, width = 5} ) - validate(float_win) + assert_win(float_win) ]] end) @@ -731,6 +1165,132 @@ describe('vim._with', function() 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() @@ -767,6 +1327,116 @@ describe('vim._with', function() 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 = { @@ -805,17 +1475,19 @@ describe('vim._with', 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 } + 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 }, out_buf) + eq({ false, 'Oops buf', true, '## %s' }, out_buf) local out_win = exec_lua [[ local other_win, cur_win = setup_windows() @@ -834,6 +1506,21 @@ describe('vim._with', function() 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(...) @@ -848,7 +1535,7 @@ describe('vim._with', function() matches('context.*table', get_error("'a', function() end")) matches('f.*function', get_error('{}, 1')) - local validate_context = function(bad_context, expected_type) + local assert_context = function(bad_context, expected_type) local bad_field = vim.tbl_keys(bad_context)[1] matches( 'context%.' .. bad_field .. '.*' .. expected_type, @@ -856,19 +1543,23 @@ describe('vim._with', function() ) 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') + assert_context({ bo = 1 }, 'table') + assert_context({ buf = 'a' }, 'number') + assert_context({ emsg_silent = 1 }, 'boolean') + 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')) -- cgit From cd53db2157f0cd27877451a6b00d66e9bed74e73 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Sun, 23 Jun 2024 17:13:06 +0300 Subject: feat(lua): add `context.env` (environment variables) to `vim._with()` --- test/functional/lua/with_spec.lua | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'test') diff --git a/test/functional/lua/with_spec.lua b/test/functional/lua/with_spec.lua index aa2f4bb7f5..99b80ef749 100644 --- a/test/functional/lua/with_spec.lua +++ b/test/functional/lua/with_spec.lua @@ -338,6 +338,64 @@ describe('vim._with', function() 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 [[ @@ -1546,6 +1604,7 @@ describe('vim._with', function() 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') -- cgit