diff options
| author | dundargoc <33953936+dundargoc@users.noreply.github.com> | 2024-06-26 12:23:34 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-26 12:23:34 +0200 |
| commit | 76dd07e572935742f6dfa0d3364e262ebc047dcc (patch) | |
| tree | de09e39db4c1a18577f3482759f0c29b2f246a94 | |
| parent | 9dc09a4cdde9fad4e7861b9467276260bd9b82a9 (diff) | |
| parent | cd53db2157f0cd27877451a6b00d66e9bed74e73 (diff) | |
| download | rneovim-76dd07e572935742f6dfa0d3364e262ebc047dcc.tar.gz rneovim-76dd07e572935742f6dfa0d3364e262ebc047dcc.tar.bz2 rneovim-76dd07e572935742f6dfa0d3364e262ebc047dcc.zip | |
Merge pull request #29280 from echasnovski/with-owobogo
Add several updates to `vim._with` (tests, granular option contexts, `env` context)
| -rw-r--r-- | runtime/lua/vim/shared.lua | 119 | ||||
| -rw-r--r-- | test/functional/lua/with_spec.lua | 1782 |
2 files changed, 1658 insertions, 243 deletions
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 7fd29d5f7b..6de1ce9d0c 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1141,44 +1141,105 @@ end --- @nodoc --- @class vim.context.mods +--- @field bo? table<string, any> --- @field buf? integer --- @field emsg_silent? boolean +--- @field env? table<string, any> +--- @field go? table<string, any> --- @field hide? boolean ---- @field horizontal? boolean --- @field keepalt? boolean --- @field keepjumps? boolean --- @field keepmarks? boolean --- @field keeppatterns? boolean --- @field lockmarks? boolean --- @field noautocmd? boolean ---- @field options? table<string, any> +--- @field o? table<string, any> --- @field sandbox? boolean --- @field silent? boolean --- @field unsilent? boolean --- @field win? integer +--- @field wo? table<string, any> + +--- @nodoc +--- @class vim.context.state +--- @field bo? table<string, any> +--- @field env? table<string, any> +--- @field go? table<string, any> +--- @field wo? table<string, any> + +local scope_map = { buf = 'bo', global = 'go', win = 'wo' } +local scope_order = { 'o', 'wo', 'bo', 'go', 'env' } +local state_restore_order = { 'bo', 'wo', 'go', 'env' } + +--- Gets data about current state, enough to properly restore specified options/env/etc. +--- @param context vim.context.mods +--- @return vim.context.state +local get_context_state = function(context) + local res = { bo = {}, env = {}, go = {}, wo = {} } + + -- Use specific order from possibly most to least intrusive + for _, scope in ipairs(scope_order) do + for name, _ in pairs(context[scope] or {}) do + local sc = scope == 'o' and scope_map[vim.api.nvim_get_option_info2(name, {}).scope] or scope + + -- Do not override already set state and fall back to `vim.NIL` for + -- state `nil` values (which still needs restoring later) + res[sc][name] = res[sc][name] or vim[sc][name] or vim.NIL + + -- Always track global option value to properly restore later. + -- This matters for at least `o` and `wo` (which might set either/both + -- local and global option values). + if sc ~= 'env' then + res.go[name] = res.go[name] or vim.go[name] + end + end + end + + return res +end --- Executes function `f` with the given context specification. --- +--- Notes: +--- - Context `{ buf = buf }` has no guarantees about current window when +--- inside context. +--- - Context `{ buf = buf, win = win }` is yet not allowed, but this seems +--- to be an implementation detail. +--- - There should be no way to revert currently set `context.sandbox = true` +--- (like with nested `vim._with()` calls). Otherwise it kind of breaks the +--- whole purpose of sandbox execution. +--- - Saving and restoring option contexts (`bo`, `go`, `o`, `wo`) trigger +--- `OptionSet` events. This is an implementation issue because not doing it +--- seems to mean using either 'eventignore' option or extra nesting with +--- `{ noautocmd = true }` (which itself is a wrapper for 'eventignore'). +--- As `{ go = { eventignore = '...' } }` is a valid context which should be +--- properly set and restored, this is not a good approach. +--- Not triggering `OptionSet` seems to be a good idea, though. So probably +--- only moving context save and restore to lower level might resolve this. +--- --- @param context vim.context.mods function vim._with(context, f) vim.validate('context', context, 'table') vim.validate('f', f, 'function') + vim.validate('context.bo', context.bo, 'table', true) vim.validate('context.buf', context.buf, 'number', true) vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) + vim.validate('context.env', context.env, 'table', true) + vim.validate('context.go', context.go, 'table', true) vim.validate('context.hide', context.hide, 'boolean', true) - vim.validate('context.horizontal', context.horizontal, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) vim.validate('context.keepmarks', context.keepmarks, 'boolean', true) vim.validate('context.keeppatterns', context.keeppatterns, 'boolean', true) vim.validate('context.lockmarks', context.lockmarks, 'boolean', true) vim.validate('context.noautocmd', context.noautocmd, 'boolean', true) - vim.validate('context.options', context.options, 'table', true) + vim.validate('context.o', context.o, 'table', true) vim.validate('context.sandbox', context.sandbox, 'boolean', true) vim.validate('context.silent', context.silent, 'boolean', true) vim.validate('context.unsilent', context.unsilent, 'boolean', true) vim.validate('context.win', context.win, 'number', true) + vim.validate('context.wo', context.wo, 'table', true) -- Check buffer exists if context.buf then @@ -1192,29 +1253,49 @@ function vim._with(context, f) if not vim.api.nvim_win_is_valid(context.win) then error('Invalid window id: ' .. context.win) end + -- TODO: Maybe allow it? + if context.buf and vim.api.nvim_win_get_buf(context.win) ~= context.buf then + error('Can not set both `buf` and `win` context.') + end end - -- Store original options - local previous_options ---@type table<string, any> - if context.options then - previous_options = {} - for k, v in pairs(context.options) do - previous_options[k] = - vim.api.nvim_get_option_value(k, { win = context.win, buf = context.buf }) - vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + -- Decorate so that save-set-restore options is done in correct window-buffer + local callback = function() + -- Cache current values to be changed by context + -- Abort early in case of bad context value + local ok, state = pcall(get_context_state, context) + if not ok then + error(state, 0) end - end - local retval = { vim._with_c(context, f) } + -- Apply some parts of the context in specific order + -- NOTE: triggers `OptionSet` event + for _, scope in ipairs(scope_order) do + for name, context_value in pairs(context[scope] or {}) do + vim[scope][name] = context_value + end + end + + -- Execute + local res = { pcall(f) } + + -- Restore relevant cached values in specific order, global scope last + -- NOTE: triggers `OptionSet` event + for _, scope in ipairs(state_restore_order) do + for name, cached_value in pairs(state[scope]) do + vim[scope][name] = cached_value + end + end - -- Restore original options - if previous_options then - for k, v in pairs(previous_options) do - vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + -- Return + if not res[1] then + error(res[2], 0) end + table.remove(res, 1) + return unpack(res, 1, table.maxn(res)) end - return unpack(retval) + return vim._with_c(context, callback) end return vim diff --git a/test/functional/lua/with_spec.lua b/test/functional/lua/with_spec.lua index 36dee9630a..99b80ef749 100644 --- a/test/functional/lua/with_spec.lua +++ b/test/functional/lua/with_spec.lua @@ -7,286 +7,1620 @@ 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 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) - 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 + ]] + assert_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) + + 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<C-V>'), 'txn', false) + local other_buf, _ = setup_buffers() + vim._with({ buf = buf }, function() vim.cmd.redraw() end) + ]] + end) + + it('can be nested', function() + exec_lua [[ + local other_buf, cur_buf = setup_buffers() + vim._with({ buf = other_buf }, function() + assert(api.nvim_get_current_buf() == other_buf) + inner = vim._with({ buf = cur_buf }, function() + assert(api.nvim_get_current_buf() == cur_buf) + end) + assert(api.nvim_get_current_buf() == other_buf) + end) + assert(api.nvim_get_current_buf() == cur_buf) + ]] end) - ]] - 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('can be nested crazily with hidden buffers', function() + local out = exec_lua([[ + local n = 0 + local function with_recursive_nested_bufs() + n = n + 1 + if n > 20 then return true end + + local other_buf, _ = setup_buffers() + vim.bo[other_buf].commentstring = '## %s' + local callback = function() + return api.nvim_get_current_buf() == other_buf + and vim.bo[other_buf].commentstring == '## %s' + and with_recursive_nested_bufs() + end + return vim._with({ buf = other_buf }, callback) and + api.nvim_buf_delete(other_buf, {}) == nil + end + + return with_recursive_nested_bufs() + ]]) + eq(true, out) + end) end) - it('does not cause ml_get errors with invalid visual selection', function() - exec_lua [[ - local api = vim.api - local t = function(s) return api.nvim_replace_termcodes(s, true, true, true) end - api.nvim_buf_set_lines(0, 0, -1, true, {"a", "b", "c"}) - api.nvim_feedkeys(t "G<C-V>", "txn", false) - vim._with({buf = api.nvim_create_buf(false, true)}, function() vim.cmd "redraw" end) - ]] + describe('`emsg_silent` context', function() + pending('works', function() + local ok = pcall( + exec_lua, + [[ + _G.f = function() + error('This error should not interfer with execution', 0) + end + -- Should not produce error same as `vim.cmd('silent! lua _G.f()')` + vim._with({ emsg_silent = true }, f) + ]] + ) + eq(true, ok) + + -- Should properly report errors afterwards + ok = pcall(exec_lua, 'lua _G.f()') + eq(false, ok) + end) + + it('can be nested', function() + local ok = pcall( + exec_lua, + [[ + _G.f = function() + error('This error should not interfer with execution', 0) + end + -- Should produce error same as `_G.f()` + vim._with({ emsg_silent = true }, function() + vim._with( { emsg_silent = false }, f) + end) + ]] + ) + eq(false, ok) + end) end) - it('can be nested crazily with hidden buffers', function() - eq( - true, - exec_lua([[ - local function scratch_buf_call(fn) - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_option_value('cindent', true, {buf = buf}) - return vim._with({buf = buf}, function() - return vim.api.nvim_get_current_buf() == buf - and vim.api.nvim_get_option_value('cindent', {buf = buf}) - and fn() - end) and vim.api.nvim_buf_delete(buf, {}) == nil + 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 - 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) + 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< |