aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordundargoc <33953936+dundargoc@users.noreply.github.com>2024-06-26 12:23:34 +0200
committerGitHub <noreply@github.com>2024-06-26 12:23:34 +0200
commit76dd07e572935742f6dfa0d3364e262ebc047dcc (patch)
treede09e39db4c1a18577f3482759f0c29b2f246a94
parent9dc09a4cdde9fad4e7861b9467276260bd9b82a9 (diff)
parentcd53db2157f0cd27877451a6b00d66e9bed74e73 (diff)
downloadrneovim-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.lua119
-rw-r--r--test/functional/lua/with_spec.lua1782
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<