diff options
author | Evgeni Chasnovski <evgeni.chasnovski@gmail.com> | 2024-06-21 16:23:05 +0300 |
---|---|---|
committer | Evgeni Chasnovski <evgeni.chasnovski@gmail.com> | 2024-06-24 20:23:11 +0300 |
commit | 07cc559cdf11acb3031b6b7ba53e65285a6f4b3f (patch) | |
tree | f52ec2651d005d0cd18b97018699d4bf32c8b291 | |
parent | f8795365deb88ab4e108858c563284b1082d06d4 (diff) | |
download | rneovim-07cc559cdf11acb3031b6b7ba53e65285a6f4b3f.tar.gz rneovim-07cc559cdf11acb3031b6b7ba53e65285a6f4b3f.tar.bz2 rneovim-07cc559cdf11acb3031b6b7ba53e65285a6f4b3f.zip |
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.
-rw-r--r-- | runtime/lua/vim/shared.lua | 99 | ||||
-rw-r--r-- | test/functional/lua/with_spec.lua | 751 |
2 files changed, 803 insertions, 47 deletions
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 79e614aaaa..0c20248d8e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1141,8 +1141,10 @@ end --- @nodoc --- @class vim.context.mods +--- @field bo? table<string, any> --- @field buf? integer --- @field emsg_silent? boolean +--- @field go? table<string, any> --- @field hide? boolean --- @field keepalt? boolean --- @field keepjumps? boolean @@ -1150,11 +1152,47 @@ end --- @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 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' } +local state_restore_order = { 'bo', 'wo', 'go' } + +--- 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 = {}, 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). + res.go[name] = res.go[name] or vim.go[name] + end + end + + return res +end --- Executes function `f` with the given context specification. --- @@ -1166,14 +1204,24 @@ end --- - 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.go', context.go, 'table', true) vim.validate('context.hide', context.hide, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) @@ -1181,11 +1229,12 @@ function vim._with(context, f) 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 @@ -1205,27 +1254,43 @@ function vim._with(context, f) 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, 1, table.maxn(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 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')) |