neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

with_spec.lua (54074B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 local Screen = require('test.functional.ui.screen')
      4 
      5 local fn = n.fn
      6 local api = n.api
      7 local command = n.command
      8 local eq = t.eq
      9 local exec_lua = n.exec_lua
     10 local exec_capture = n.exec_capture
     11 local matches = t.matches
     12 local pcall_err = t.pcall_err
     13 
     14 describe('vim._with', function()
     15  before_each(function()
     16    n.clear()
     17    exec_lua([[
     18      _G.fn = vim.fn
     19      _G.api = vim.api
     20 
     21      _G.setup_buffers = function()
     22        return api.nvim_create_buf(false, true), api.nvim_get_current_buf()
     23      end
     24 
     25      _G.setup_windows = function()
     26        local other_win = api.nvim_get_current_win()
     27        vim.cmd.new()
     28        return other_win, api.nvim_get_current_win()
     29      end
     30    ]])
     31  end)
     32 
     33  local assert_events_trigger = function()
     34    local out = exec_lua [[
     35      -- Needs three global values defined:
     36      -- - `test_events` - array of events which are tested.
     37      -- - `test_context` - context to be tested.
     38      -- - `test_trig_event` - callable triggering at least one tested event.
     39      _G.n_events = 0
     40      local opts = { callback = function() _G.n_events = _G.n_events + 1 end }
     41      api.nvim_create_autocmd(_G.test_events, opts)
     42 
     43      local context = { bo = { commentstring = '-- %s' } }
     44 
     45      -- Should not trigger events on its own
     46      vim._with(_G.test_context, function() end)
     47      local is_no_events = _G.n_events == 0
     48 
     49      -- Should trigger events if specifically asked inside callback
     50      local is_events = vim._with(_G.test_context, function()
     51        _G.test_trig_event()
     52        return _G.n_events > 0
     53      end)
     54      return { is_no_events, is_events }
     55    ]]
     56    eq({ true, true }, out)
     57  end
     58 
     59  describe('`bo` context', function()
     60    before_each(function()
     61      exec_lua [[
     62        _G.other_buf, _G.cur_buf = setup_buffers()
     63 
     64        -- 'commentstring' is local to buffer and string
     65        vim.bo[other_buf].commentstring = '## %s'
     66        vim.bo[cur_buf].commentstring = '// %s'
     67        vim.go.commentstring = '$$ %s'
     68 
     69        -- 'undolevels' is global or local to buffer (global-local) and number
     70        vim.bo[other_buf].undolevels = 100
     71        vim.bo[cur_buf].undolevels = 250
     72        vim.go.undolevels = 500
     73 
     74        -- 'autoread' is global or local to buffer (global-local) and boolean
     75        vim.bo[other_buf].autoread = false
     76        vim.bo[cur_buf].autoread = false
     77        vim.go.autoread = true
     78 
     79        _G.get_state = function()
     80          return {
     81            bo = {
     82              cms_cur = vim.bo[cur_buf].commentstring,
     83              cms_other = vim.bo[other_buf].commentstring,
     84              ul_cur = vim.bo[cur_buf].undolevels,
     85              ul_other = vim.bo[other_buf].undolevels,
     86              ar_cur = vim.bo[cur_buf].autoread,
     87              ar_other = vim.bo[other_buf].autoread,
     88            },
     89            go = {
     90              cms = vim.go.commentstring,
     91              ul = vim.go.undolevels,
     92              ar = vim.go.autoread,
     93            },
     94          }
     95        end
     96      ]]
     97    end)
     98 
     99    it('works', function()
    100      local out = exec_lua [[
    101        local context = { bo = { commentstring = '-- %s', undolevels = 0, autoread = true } }
    102 
    103        local before = get_state()
    104        local inner = vim._with(context, function()
    105          assert(api.nvim_get_current_buf() == cur_buf)
    106          return get_state()
    107        end)
    108 
    109        return { before = before, inner = inner, after = get_state() }
    110      ]]
    111 
    112      eq({
    113        bo = {
    114          cms_cur = '-- %s',
    115          cms_other = '## %s',
    116          ul_cur = 0,
    117          ul_other = 100,
    118          ar_cur = true,
    119          ar_other = false,
    120        },
    121        go = { cms = '$$ %s', ul = 500, ar = true },
    122      }, out.inner)
    123      eq(out.before, out.after)
    124    end)
    125 
    126    it('sets options in `buf` context', function()
    127      local out = exec_lua [[
    128        local context = { buf = other_buf, bo = { commentstring = '-- %s', undolevels = 0, autoread = true } }
    129 
    130        local before = get_state()
    131        local inner = vim._with(context, function()
    132          assert(api.nvim_get_current_buf() == other_buf)
    133          return get_state()
    134        end)
    135 
    136        return { before = before, inner = inner, after = get_state() }
    137      ]]
    138 
    139      eq({
    140        bo = {
    141          cms_cur = '// %s',
    142          cms_other = '-- %s',
    143          ul_cur = 250,
    144          ul_other = 0,
    145          ar_cur = false,
    146          ar_other = true,
    147        },
    148        go = { cms = '$$ %s', ul = 500, ar = true },
    149      }, out.inner)
    150      eq(out.before, out.after)
    151    end)
    152 
    153    it('restores only options from context', function()
    154      local out = exec_lua [[
    155        local context = { bo = { commentstring = '-- %s' } }
    156 
    157        local inner = vim._with(context, function()
    158          assert(api.nvim_get_current_buf() == cur_buf)
    159          vim.bo[cur_buf].undolevels = 750
    160          vim.bo[cur_buf].commentstring = '!! %s'
    161          return get_state()
    162        end)
    163 
    164        return { inner = inner, after = get_state() }
    165      ]]
    166 
    167      eq({
    168        bo = {
    169          cms_cur = '!! %s',
    170          cms_other = '## %s',
    171          ul_cur = 750,
    172          ul_other = 100,
    173          ar_cur = false,
    174          ar_other = false,
    175        },
    176        go = { cms = '$$ %s', ul = 500, ar = true },
    177      }, out.inner)
    178      eq({
    179        bo = {
    180          cms_cur = '// %s',
    181          cms_other = '## %s',
    182          ul_cur = 750,
    183          ul_other = 100,
    184          ar_cur = false,
    185          ar_other = false,
    186        },
    187        go = { cms = '$$ %s', ul = 500, ar = true },
    188      }, out.after)
    189    end)
    190 
    191    it('does not trigger events', function()
    192      exec_lua [[
    193        _G.test_events = { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' }
    194        _G.test_context = { bo = { commentstring = '-- %s' } }
    195        _G.test_trig_event = function() vim.cmd.new() end
    196      ]]
    197      assert_events_trigger()
    198    end)
    199 
    200    it('can be nested', function()
    201      local out = exec_lua [[
    202        local before, before_inner, after_inner = get_state(), nil, nil
    203        vim._with({ bo = { commentstring = '-- %s', undolevels = 0, autoread = true } }, function()
    204          before_inner = get_state()
    205          inner = vim._with({ bo = { commentstring = '!! %s', autoread = false } }, get_state)
    206          after_inner = get_state()
    207        end)
    208        return {
    209          before = before, before_inner = before_inner,
    210          inner = inner,
    211          after_inner = after_inner, after = get_state(),
    212        }
    213      ]]
    214      eq('!! %s', out.inner.bo.cms_cur)
    215      eq(0, out.inner.bo.ul_cur)
    216      eq(false, out.inner.bo.ar_cur)
    217      eq(out.before_inner, out.after_inner)
    218      eq(out.before, out.after)
    219    end)
    220  end)
    221 
    222  describe('`buf` context', function()
    223    it('works', function()
    224      local out = exec_lua [[
    225        local other_buf, cur_buf = setup_buffers()
    226        local inner = vim._with({ buf = other_buf }, function()
    227          return api.nvim_get_current_buf()
    228        end)
    229        return { inner == other_buf, api.nvim_get_current_buf() == cur_buf }
    230      ]]
    231      eq({ true, true }, out)
    232    end)
    233 
    234    it('does not trigger events', function()
    235      exec_lua [[
    236        _G.test_events = { 'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave' }
    237        _G.test_context = { buf = other_buf }
    238        _G.test_trig_event = function() vim.cmd.new() end
    239      ]]
    240      assert_events_trigger()
    241    end)
    242 
    243    it('can access buffer options', function()
    244      local out = exec_lua [[
    245        other_buf, cur_buf = setup_buffers()
    246        vim.bo[other_buf].commentstring = '## %s'
    247        vim.bo[cur_buf].commentstring = '// %s'
    248 
    249        vim._with({ buf = other_buf }, function()
    250          vim.cmd.set('commentstring=--\\ %s')
    251        end)
    252 
    253        return vim.bo[other_buf].commentstring == '-- %s' and
    254          vim.bo[cur_buf].commentstring == '// %s'
    255      ]]
    256      eq(true, out)
    257    end)
    258 
    259    it('works with different kinds of buffers', function()
    260      exec_lua [[
    261        local assert_buf = function(buf)
    262          vim._with({ buf = buf }, function()
    263            assert(api.nvim_get_current_buf() == buf)
    264          end)
    265        end
    266 
    267        -- Current
    268        assert_buf(api.nvim_get_current_buf())
    269 
    270        -- Hidden listed
    271        local listed = api.nvim_create_buf(true, true)
    272        assert_buf(listed)
    273 
    274        -- Visible
    275        local other_win, cur_win = setup_windows()
    276        api.nvim_win_set_buf(other_win, listed)
    277        assert_buf(listed)
    278 
    279        -- Shown but not visible
    280        vim.cmd.tabnew()
    281        assert_buf(listed)
    282 
    283        -- Shown in several windows
    284        api.nvim_win_set_buf(0, listed)
    285        assert_buf(listed)
    286 
    287        -- Shown in floating window
    288        local float_buf = api.nvim_create_buf(false, true)
    289        local config = { relative = 'editor', row = 1, col = 1, width = 5, height = 5 }
    290        api.nvim_open_win(float_buf, false, config)
    291        assert_buf(float_buf)
    292      ]]
    293    end)
    294 
    295    it('does not cause ml_get errors with invalid visual selection', function()
    296      exec_lua [[
    297        api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b', 'c' })
    298        api.nvim_feedkeys(vim.keycode('G<C-V>'), 'txn', false)
    299        local other_buf, _ = setup_buffers()
    300        vim._with({ buf = buf }, function() vim.cmd.redraw() end)
    301      ]]
    302    end)
    303 
    304    it('can be nested', function()
    305      exec_lua [[
    306        local other_buf, cur_buf = setup_buffers()
    307        vim._with({ buf = other_buf }, function()
    308          assert(api.nvim_get_current_buf() == other_buf)
    309          inner = vim._with({ buf = cur_buf }, function()
    310            assert(api.nvim_get_current_buf() == cur_buf)
    311          end)
    312          assert(api.nvim_get_current_buf() == other_buf)
    313        end)
    314        assert(api.nvim_get_current_buf() == cur_buf)
    315      ]]
    316    end)
    317 
    318    it('can be nested crazily with hidden buffers', function()
    319      local out = exec_lua([[
    320        local n = 0
    321        local function with_recursive_nested_bufs()
    322          n = n + 1
    323          if n > 20 then return true end
    324 
    325          local other_buf, _ = setup_buffers()
    326          vim.bo[other_buf].commentstring = '## %s'
    327          local callback = function()
    328            return api.nvim_get_current_buf() == other_buf
    329              and vim.bo[other_buf].commentstring == '## %s'
    330              and with_recursive_nested_bufs()
    331          end
    332          return vim._with({ buf = other_buf }, callback) and
    333            api.nvim_buf_delete(other_buf, {}) == nil
    334        end
    335 
    336        return with_recursive_nested_bufs()
    337      ]])
    338      eq(true, out)
    339    end)
    340  end)
    341 
    342  describe('`emsg_silent` context', function()
    343    pending('works', function()
    344      local ok = pcall(
    345        exec_lua,
    346        [[
    347          _G.f = function()
    348            error('This error should not interfer with execution', 0)
    349          end
    350          -- Should not produce error same as `vim.cmd('silent! lua _G.f()')`
    351          vim._with({ emsg_silent = true }, f)
    352        ]]
    353      )
    354      eq(true, ok)
    355 
    356      -- Should properly report errors afterwards
    357      ok = pcall(exec_lua, 'lua _G.f()')
    358      eq(false, ok)
    359    end)
    360 
    361    it('can be nested', function()
    362      local ok = pcall(
    363        exec_lua,
    364        [[
    365          _G.f = function()
    366            error('This error should not interfere with execution', 0)
    367          end
    368          -- Should produce error same as `_G.f()`
    369          vim._with({ emsg_silent = true }, function()
    370            vim._with( { emsg_silent = false }, f)
    371          end)
    372        ]]
    373      )
    374      eq(false, ok)
    375    end)
    376  end)
    377 
    378  describe('`env` context', function()
    379    before_each(function()
    380      exec_lua [[
    381        vim.fn.setenv('aaa', 'hello')
    382        _G.get_state = function()
    383          return { aaa = vim.fn.getenv('aaa'), bbb = vim.fn.getenv('bbb') }
    384        end
    385      ]]
    386    end)
    387 
    388    it('works', function()
    389      local out = exec_lua [[
    390        local context = { env = { aaa = 'inside', bbb = 'wow' } }
    391        local before = get_state()
    392        local inner = vim._with(context, get_state)
    393        return { before = before, inner = inner, after = get_state() }
    394      ]]
    395 
    396      eq({ aaa = 'inside', bbb = 'wow' }, out.inner)
    397      eq(out.before, out.after)
    398    end)
    399 
    400    it('restores only variables from context', function()
    401      local out = exec_lua [[
    402        local context = { env = { bbb = 'wow' } }
    403        local before = get_state()
    404        local inner = vim._with(context, function()
    405          vim.env.aaa = 'inside'
    406          return get_state()
    407        end)
    408        return { before = before, inner = inner, after = get_state() }
    409      ]]
    410 
    411      eq({ aaa = 'inside', bbb = 'wow' }, out.inner)
    412      eq({ aaa = 'inside', bbb = vim.NIL }, out.after)
    413    end)
    414 
    415    it('can be nested', function()
    416      local out = exec_lua [[
    417        local before, before_inner, after_inner = get_state(), nil, nil
    418        vim._with({ env = { aaa = 'inside', bbb = 'wow' } }, function()
    419          before_inner = get_state()
    420          inner = vim._with({ env = { aaa = 'more inside' } }, get_state)
    421          after_inner = get_state()
    422        end)
    423        return {
    424          before = before, before_inner = before_inner,
    425          inner = inner,
    426          after_inner = after_inner, after = get_state(),
    427        }
    428      ]]
    429      eq('more inside', out.inner.aaa)
    430      eq('wow', out.inner.bbb)
    431      eq(out.before_inner, out.after_inner)
    432      eq(out.before, out.after)
    433    end)
    434  end)
    435 
    436  describe('`go` context', function()
    437    before_each(function()
    438      exec_lua [[
    439        vim.bo.commentstring = '## %s'
    440        vim.go.commentstring = '$$ %s'
    441        vim.wo.winblend = 25
    442        vim.go.winblend = 50
    443        vim.go.langmap = 'xy,yx'
    444        vim.go.confirm = false
    445 
    446        _G.get_state = function()
    447          return {
    448            bo = { cms = vim.bo.commentstring },
    449            wo = { winbl = vim.wo.winblend },
    450            go = {
    451              cms = vim.go.commentstring,
    452              winbl = vim.go.winblend,
    453              lmap = vim.go.langmap,
    454              cf = vim.go.confirm,
    455            },
    456          }
    457        end
    458      ]]
    459    end)
    460 
    461    it('works', function()
    462      local out = exec_lua [[
    463        local context = {
    464          go = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba', cf = true },
    465        }
    466        local before = get_state()
    467        local inner = vim._with(context, get_state)
    468        return { before = before, inner = inner, after = get_state() }
    469      ]]
    470 
    471      eq({
    472        bo = { cms = '## %s' },
    473        wo = { winbl = 25 },
    474        go = { cms = '-- %s', winbl = 75, lmap = 'ab,ba', cf = true },
    475      }, out.inner)
    476      eq(out.before, out.after)
    477    end)
    478 
    479    it('works with `eventignore`', function()
    480      -- This might be an issue if saving and restoring option context is done
    481      -- to account for triggering `OptionSet`, but in not a good way
    482      local out = exec_lua [[
    483        vim.go.eventignore = 'ModeChanged'
    484        local inner = vim._with({ go = { eventignore = 'CursorMoved' } }, function()
    485          return vim.go.eventignore
    486        end)
    487        return { inner = inner, after = vim.go.eventignore }
    488      ]]
    489      eq({ inner = 'CursorMoved', after = 'ModeChanged' }, out)
    490    end)
    491 
    492    it('restores only options from context', function()
    493      local out = exec_lua [[
    494        local context = { go = { langmap = 'ab,ba' } }
    495 
    496        local inner = vim._with(context, function()
    497          vim.go.commentstring = '!! %s'
    498          vim.go.winblend = 75
    499          vim.go.langmap = 'uv,vu'
    500          return get_state()
    501        end)
    502 
    503        return { inner = inner, after = get_state() }
    504      ]]
    505 
    506      eq({
    507        bo = { cms = '## %s' },
    508        wo = { winbl = 25 },
    509        go = { cms = '!! %s', winbl = 75, lmap = 'uv,vu', cf = false },
    510      }, out.inner)
    511      eq({
    512        bo = { cms = '## %s' },
    513        wo = { winbl = 25 },
    514        go = { cms = '!! %s', winbl = 75, lmap = 'xy,yx', cf = false },
    515      }, out.after)
    516    end)
    517 
    518    it('does not trigger events', function()
    519      exec_lua [[
    520        _G.test_events = {
    521          'BufEnter', 'BufLeave', 'BufWinEnter', 'BufWinLeave', 'WinEnter', 'WinLeave'
    522        }
    523        _G.test_context = { go = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba' } }
    524        _G.test_trig_event = function() vim.cmd.new() end
    525      ]]
    526      assert_events_trigger()
    527    end)
    528 
    529    it('can be nested', function()
    530      local out = exec_lua [[
    531        local before, before_inner, after_inner = get_state(), nil, nil
    532        vim._with({ go = { langmap = 'ab,ba', commentstring = '-- %s' } }, function()
    533          before_inner = get_state()
    534          inner = vim._with({ go = { langmap = 'uv,vu' } }, get_state)
    535          after_inner = get_state()
    536        end)
    537        return {
    538          before = before, before_inner = before_inner,
    539          inner = inner,
    540          after_inner = after_inner, after = get_state(),
    541        }
    542      ]]
    543      eq('uv,vu', out.inner.go.lmap)
    544      eq('-- %s', out.inner.go.cms)
    545      eq(out.before_inner, out.after_inner)
    546      eq(out.before, out.after)
    547    end)
    548  end)
    549 
    550  describe('`hide` context', function()
    551    pending('works', function()
    552      local ok = pcall(
    553        exec_lua,
    554        [[
    555          vim.o.hidden = false
    556          vim.bo.modified = true
    557          local init_buf = api.nvim_get_current_buf()
    558          -- Should not produce error same as `vim.cmd('hide enew')`
    559          vim._with({ hide = true }, function()
    560            vim.cmd.enew()
    561          end)
    562          assert(api.nvim_get_current_buf() ~= init_buf)
    563        ]]
    564      )
    565      eq(true, ok)
    566    end)
    567 
    568    it('can be nested', function()
    569      local ok = pcall(
    570        exec_lua,
    571        [[
    572          vim.o.hidden = false
    573          vim.bo.modified = true
    574          -- Should produce error same as `vim.cmd.enew()`
    575          vim._with({ hide = true }, function()
    576            vim._with({ hide = false }, function()
    577              vim.cmd.enew()
    578            end)
    579          end)
    580        ]]
    581      )
    582      eq(false, ok)
    583    end)
    584  end)
    585 
    586  describe('`horizontal` context', function()
    587    local is_approx_eq = function(dim, id_1, id_2)
    588      local f = dim == 'height' and api.nvim_win_get_height or api.nvim_win_get_width
    589      return math.abs(f(id_1) - f(id_2)) <= 1
    590    end
    591 
    592    local win_id_1, win_id_2, win_id_3
    593    before_each(function()
    594      win_id_1 = api.nvim_get_current_win()
    595      command('wincmd v | wincmd 5>')
    596      win_id_2 = api.nvim_get_current_win()
    597      command('wincmd s | wincmd 5+')
    598      win_id_3 = api.nvim_get_current_win()
    599 
    600      eq(is_approx_eq('width', win_id_1, win_id_2), false)
    601      eq(is_approx_eq('height', win_id_3, win_id_2), false)
    602    end)
    603 
    604    pending('works', function()
    605      exec_lua [[
    606        -- Should be same as `vim.cmd('horizontal wincmd =')`
    607        vim._with({ horizontal = true }, function()
    608          vim.cmd.wincmd('=')
    609        end)
    610      ]]
    611      eq(is_approx_eq('width', win_id_1, win_id_2), true)
    612      eq(is_approx_eq('height', win_id_3, win_id_2), false)
    613    end)
    614 
    615    pending('can be nested', function()
    616      exec_lua [[
    617        -- Should be same as `vim.cmd.wincmd('=')`
    618        vim._with({ horizontal = true }, function()
    619          vim._with({ horizontal = false }, function()
    620            vim.cmd.wincmd('=')
    621          end)
    622        end)
    623      ]]
    624      eq(is_approx_eq('width', win_id_1, win_id_2), true)
    625      eq(is_approx_eq('height', win_id_3, win_id_2), true)
    626    end)
    627  end)
    628 
    629  describe('`keepalt` context', function()
    630    pending('works', function()
    631      local out = exec_lua [[
    632        vim.cmd('edit alt')
    633        vim.cmd('edit new')
    634        assert(fn.bufname('#') == 'alt')
    635 
    636        -- Should work as `vim.cmd('keepalt edit very-new')`
    637        vim._with({ keepalt = true }, function()
    638          vim.cmd.edit('very-new')
    639        end)
    640        return fn.bufname('#') == 'alt'
    641      ]]
    642      eq(true, out)
    643    end)
    644 
    645    it('can be nested', function()
    646      local out = exec_lua [[
    647        vim.cmd('edit alt')
    648        vim.cmd('edit new')
    649        assert(fn.bufname('#') == 'alt')
    650 
    651        -- Should work as `vim.cmd.edit('very-new')`
    652        vim._with({ keepalt = true }, function()
    653          vim._with({ keepalt = false }, function()
    654            vim.cmd.edit('very-new')
    655          end)
    656        end)
    657        return fn.bufname('#') == 'alt'
    658      ]]
    659      eq(false, out)
    660    end)
    661  end)
    662 
    663  describe('`keepjumps` context', function()
    664    pending('works', function()
    665      local out = exec_lua [[
    666        api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb', 'ccc' })
    667        local jumplist_before = fn.getjumplist()
    668        -- Should work as `vim.cmd('keepjumps normal! Ggg')`
    669        vim._with({ keepjumps = true }, function()
    670          vim.cmd('normal! Ggg')
    671        end)
    672        return vim.deep_equal(jumplist_before, fn.getjumplist())
    673      ]]
    674      eq(true, out)
    675    end)
    676 
    677    it('can be nested', function()
    678      local out = exec_lua [[
    679        api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb', 'ccc' })
    680        local jumplist_before = fn.getjumplist()
    681        vim._with({ keepjumps = true }, function()
    682          vim._with({ keepjumps = false }, function()
    683            vim.cmd('normal! Ggg')
    684          end)
    685        end)
    686        return vim.deep_equal(jumplist_before, fn.getjumplist())
    687      ]]
    688      eq(false, out)
    689    end)
    690  end)
    691 
    692  describe('`keepmarks` context', function()
    693    pending('works', function()
    694      local out = exec_lua [[
    695        vim.cmd('set cpoptions+=R')
    696        api.nvim_buf_set_lines(0, 0, -1, false, { 'bbb', 'ccc', 'aaa' })
    697        api.nvim_buf_set_mark(0, 'm', 2, 2, {})
    698 
    699        -- Should be the same as `vim.cmd('keepmarks %!sort')`
    700        vim._with({ keepmarks = true }, function()
    701          vim.cmd('%!sort')
    702        end)
    703        return api.nvim_buf_get_mark(0, 'm')
    704      ]]
    705      eq({ 2, 2 }, out)
    706    end)
    707 
    708    it('can be nested', function()
    709      local out = exec_lua [[
    710        vim.cmd('set cpoptions+=R')
    711        api.nvim_buf_set_lines(0, 0, -1, false, { 'bbb', 'ccc', 'aaa' })
    712        api.nvim_buf_set_mark(0, 'm', 2, 2, {})
    713 
    714        vim._with({ keepmarks = true }, function()
    715          vim._with({ keepmarks = false }, function()
    716            vim.cmd('%!sort')
    717          end)
    718        end)
    719        return api.nvim_buf_get_mark(0, 'm')
    720      ]]
    721      eq({ 0, 2 }, out)
    722    end)
    723  end)
    724 
    725  describe('`keepatterns` context', function()
    726    pending('works', function()
    727      local out = exec_lua [[
    728        api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb' })
    729        vim.cmd('/aaa')
    730        -- Should be the same as `vim.cmd('keeppatterns /bbb')`
    731        vim._with({ keeppatterns = true }, function()
    732          vim.cmd('/bbb')
    733        end)
    734        return fn.getreg('/')
    735      ]]
    736      eq('aaa', out)
    737    end)
    738 
    739    it('can be nested', function()
    740      local out = exec_lua [[
    741        api.nvim_buf_set_lines(0, 0, -1, false, { 'aaa', 'bbb' })
    742        vim.cmd('/aaa')
    743        vim._with({ keeppatterns = true }, function()
    744          vim._with({ keeppatterns = false }, function()
    745            vim.cmd('/bbb')
    746          end)
    747        end)
    748        return fn.getreg('/')
    749      ]]
    750      eq('bbb', out)
    751    end)
    752  end)
    753 
    754  describe('`lockmarks` context', function()
    755    it('works', function()
    756      local mark = exec_lua [[
    757        api.nvim_buf_set_lines(0, 0, 0, false, { 'aaa', 'bbb', 'ccc' })
    758        api.nvim_buf_set_mark(0, 'm', 2, 2, {})
    759        -- Should be same as `:lockmarks lua api.nvim_buf_set_lines(...)`
    760        vim._with({ lockmarks = true }, function()
    761          api.nvim_buf_set_lines(0, 0, 2, false, { 'uuu', 'vvv', 'www' })
    762        end)
    763        return api.nvim_buf_get_mark(0, 'm')
    764      ]]
    765      eq({ 2, 2 }, mark)
    766    end)
    767 
    768    it('can be nested', function()
    769      local mark = exec_lua [[
    770        api.nvim_buf_set_lines(0, 0, 0, false, { 'aaa', 'bbb', 'ccc' })
    771        api.nvim_buf_set_mark(0, 'm', 2, 2, {})
    772        vim._with({ lockmarks = true }, function()
    773          vim._with({ lockmarks = false }, function()
    774            api.nvim_buf_set_lines(0, 0, 2, false, { 'uuu', 'vvv', 'www' })
    775          end)
    776        end)
    777        return api.nvim_buf_get_mark(0, 'm')
    778      ]]
    779      eq({ 0, 2 }, mark)
    780    end)
    781  end)
    782 
    783  describe('`noautocmd` context', function()
    784    it('works', function()
    785      local out = exec_lua [[
    786        _G.n_events = 0
    787        vim.cmd('au ModeChanged * lua _G.n_events = _G.n_events + 1')
    788        -- Should be the same as `vim.cmd('noautocmd normal! vv')`
    789        vim._with({ noautocmd = true }, function()
    790          vim.cmd('normal! vv')
    791        end)
    792        return _G.n_events
    793      ]]
    794      eq(0, out)
    795    end)
    796 
    797    it('works with User events', function()
    798      local out = exec_lua [[
    799        _G.n_events = 0
    800        vim.cmd('au User MyEvent lua _G.n_events = _G.n_events + 1')
    801        -- Should be the same as `vim.cmd('noautocmd doautocmd User MyEvent')`
    802        vim._with({ noautocmd = true }, function()
    803          api.nvim_exec_autocmds('User', { pattern = 'MyEvent' })
    804        end)
    805        return _G.n_events
    806      ]]
    807      eq(0, out)
    808    end)
    809 
    810    pending('can be nested', function()
    811      local out = exec_lua [[
    812        _G.n_events = 0
    813        vim.cmd('au ModeChanged * lua _G.n_events = _G.n_events + 1')
    814        vim._with({ noautocmd = true }, function()
    815          vim._with({ noautocmd = false }, function()
    816            vim.cmd('normal! vv')
    817          end)
    818        end)
    819        return _G.n_events
    820      ]]
    821      eq(2, out)
    822    end)
    823  end)
    824 
    825  describe('`o` context', function()
    826    before_each(function()
    827      exec_lua [[
    828        _G.other_win, _G.cur_win = setup_windows()
    829        _G.other_buf, _G.cur_buf = setup_buffers()
    830 
    831        vim.bo[other_buf].commentstring = '## %s'
    832        vim.bo[cur_buf].commentstring = '// %s'
    833        vim.go.commentstring = '$$ %s'
    834 
    835        vim.bo[other_buf].undolevels = 100
    836        vim.bo[cur_buf].undolevels = 250
    837        vim.go.undolevels = 500
    838 
    839        vim.wo[other_win].virtualedit = 'block'
    840        vim.wo[cur_win].virtualedit = 'insert'
    841        vim.go.virtualedit = 'none'
    842 
    843        vim.wo[other_win].winblend = 10
    844        vim.wo[cur_win].winblend = 25
    845        vim.go.winblend = 50
    846 
    847        vim.go.langmap = 'xy,yx'
    848        vim.go.confirm = false
    849 
    850        _G.get_state = function()
    851          return {
    852            bo = {
    853              cms_cur = vim.bo[cur_buf].commentstring,
    854              cms_other = vim.bo[other_buf].commentstring,
    855              ul_cur = vim.bo[cur_buf].undolevels,
    856              ul_other = vim.bo[other_buf].undolevels,
    857            },
    858            wo = {
    859              ve_cur = vim.wo[cur_win].virtualedit,
    860              ve_other = vim.wo[other_win].virtualedit,
    861              winbl_cur = vim.wo[cur_win].winblend,
    862              winbl_other = vim.wo[other_win].winblend,
    863            },
    864            go = {
    865              cms = vim.go.commentstring,
    866              ul = vim.go.undolevels,
    867              ve = vim.go.virtualedit,
    868              winbl = vim.go.winblend,
    869              lmap = vim.go.langmap,
    870              cf = vim.go.confirm,
    871            },
    872          }
    873        end
    874      ]]
    875    end)
    876 
    877    it('works', function()
    878      local out = exec_lua [[
    879        local context = {
    880          o = {
    881            commentstring = '-- %s',
    882            undolevels = 0,
    883            virtualedit = 'all',
    884            winblend = 75,
    885            langmap = 'ab,ba',
    886            confirm = true,
    887          },
    888        }
    889 
    890        local before = get_state()
    891        local inner = vim._with(context, function()
    892          assert(api.nvim_get_current_buf() == cur_buf)
    893          assert(api.nvim_get_current_win() == cur_win)
    894          return get_state()
    895        end)
    896 
    897        return { before = before, inner = inner, after = get_state() }
    898      ]]
    899 
    900      -- Options in context are set with `vim.o`, so usually both local
    901      -- and global values are affected. Yet all of them should be later
    902      -- restored to pre-context values.
    903      eq({
    904        bo = { cms_cur = '-- %s', cms_other = '## %s', ul_cur = -123456, ul_other = 100 },
    905        wo = { ve_cur = 'all', ve_other = 'block', winbl_cur = 75, winbl_other = 10 },
    906        go = { cms = '-- %s', ul = 0, ve = 'all', winbl = 75, lmap = 'ab,ba', cf = true },
    907      }, out.inner)
    908      eq(out.before, out.after)
    909    end)
    910 
    911    it('sets options in `buf` context', function()
    912      local out = exec_lua [[
    913        local context = { buf = other_buf, o = { commentstring = '-- %s', undolevels = 0 } }
    914 
    915        local before = get_state()
    916        local inner = vim._with(context, function()
    917          assert(api.nvim_get_current_buf() == other_buf)
    918          return get_state()
    919        end)
    920 
    921        return { before = before, inner = inner, after = get_state() }
    922      ]]
    923 
    924      eq({
    925        bo = { cms_cur = '// %s', cms_other = '-- %s', ul_cur = 250, ul_other = -123456 },
    926        wo = { ve_cur = 'insert', ve_other = 'block', winbl_cur = 25, winbl_other = 10 },
    927        -- Global `winbl` inside context ideally should be untouched and equal
    928        -- to 50. It seems to be equal to 0 because `context.buf` uses
    929        -- `aucmd_prepbuf` C approach which has no guarantees about window or
    930        -- window option values inside context.
    931        go = { cms = '-- %s', ul = 0, ve = 'none', winbl = 0, lmap = 'xy,yx', cf = false },
    932      }, out.inner)
    933      eq(out.before, out.after)
    934    end)
    935 
    936    it('sets options in `win` context', function()
    937      local out = exec_lua [[
    938        local context = { win = other_win, o = { winblend = 75, virtualedit = 'all' } }
    939 
    940        local before = get_state()
    941        local inner = vim._with(context, function()
    942          assert(api.nvim_get_current_win() == other_win)
    943          return get_state()
    944        end)
    945 
    946        return { before = before, inner = inner, after = get_state() }
    947      ]]
    948 
    949      eq({
    950        bo = { cms_cur = '// %s', cms_other = '## %s', ul_cur = 250, ul_other = 100 },
    951        wo = { winbl_cur = 25, winbl_other = 75, ve_cur = 'insert', ve_other = 'all' },
    952        go = { cms = '$$ %s', ul = 500, winbl = 75, ve = 'all', lmap = 'xy,yx', cf = false },
    953      }, out.inner)
    954      eq(out.before, out.after)
    955    end)
    956 
    957    it('restores only options from context', function()
    958      local out = exec_lua [[
    959        local context = { o = { undolevels = 0, winblend = 75, langmap = 'ab,ba' } }
    960 
    961        local inner = vim._with(context, function()
    962          assert(api.nvim_get_current_buf() == cur_buf)
    963          assert(api.nvim_get_current_win() == cur_win)
    964 
    965          vim.o.commentstring = '!! %s'
    966          vim.o.undolevels = 750
    967          vim.o.virtualedit = 'onemore'
    968          vim.o.winblend = 99
    969          vim.o.langmap = 'uv,vu'
    970          return get_state()
    971        end)
    972 
    973        return { inner = inner, after = get_state() }
    974      ]]
    975 
    976      eq({
    977        bo = { cms_cur = '!! %s', cms_other = '## %s', ul_cur = -123456, ul_other = 100 },
    978        wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 99, winbl_other = 10 },
    979        go = { cms = '!! %s', ul = 750, ve = 'onemore', winbl = 99, lmap = 'uv,vu', cf = false },
    980      }, out.inner)
    981      eq({
    982        bo = { cms_cur = '!! %s', cms_other = '## %s', ul_cur = 250, ul_other = 100 },
    983        wo = { ve_cur = 'onemore', ve_other = 'block', winbl_cur = 25, winbl_other = 10 },
    984        go = { cms = '!! %s', ul = 500, ve = 'onemore', winbl = 50, lmap = 'xy,yx', cf = false },
    985      }, out.after)
    986    end)
    987 
    988    it('does not trigger events', function()
    989      exec_lua [[
    990        _G.test_events = {
    991          'BufEnter', 'BufLeave', 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave'
    992        }
    993        _G.test_context = { o = { undolevels = 0, winblend = 75, langmap = 'ab,ba' } }
    994        _G.test_trig_event = function() vim.cmd.new() end
    995      ]]
    996      assert_events_trigger()
    997    end)
    998 
    999    it('can be nested', function()
   1000      local out = exec_lua [[
   1001        local before, before_inner, after_inner = get_state(), nil, nil
   1002        local cxt_o = { commentstring = '-- %s', winblend = 75, langmap = 'ab,ba', undolevels = 0 }
   1003        vim._with({ o = cxt_o }, function()
   1004          before_inner = get_state()
   1005          local inner_cxt_o = { commentstring = '!! %s', winblend = 99, langmap = 'uv,vu' }
   1006          inner = vim._with({ o = inner_cxt_o }, get_state)
   1007          after_inner = get_state()
   1008        end)
   1009        return {
   1010          before = before, before_inner = before_inner,
   1011          inner = inner,
   1012          after_inner = after_inner, after = get_state(),
   1013        }
   1014      ]]
   1015      eq('!! %s', out.inner.bo.cms_cur)
   1016      eq(99, out.inner.wo.winbl_cur)
   1017      eq('uv,vu', out.inner.go.lmap)
   1018      eq(0, out.inner.go.ul)
   1019      eq(out.before_inner, out.after_inner)
   1020      eq(out.before, out.after)
   1021    end)
   1022  end)
   1023 
   1024  describe('`sandbox` context', function()
   1025    it('works', function()
   1026      local ok, err = pcall(
   1027        exec_lua,
   1028        [[
   1029          -- Should work as `vim.cmd('sandbox call append(0, "aaa")')`
   1030          vim._with({ sandbox = true }, function()
   1031            fn.append(0, 'aaa')
   1032          end)
   1033        ]]
   1034      )
   1035      eq(false, ok)
   1036      matches('Not allowed in sandbox', err)
   1037    end)
   1038 
   1039    it('can NOT be nested', function()
   1040      -- This behavior is intentionally different from other flags as allowing
   1041      -- disabling `sandbox` from nested function seems to be against the point
   1042      -- of using `sandbox` context in the first place
   1043      local ok, err = pcall(
   1044        exec_lua,
   1045        [[
   1046          vim._with({ sandbox = true }, function()
   1047            vim._with({ sandbox = false }, function()
   1048              fn.append(0, 'aaa')
   1049            end)
   1050          end)
   1051        ]]
   1052      )
   1053      eq(false, ok)
   1054      matches('Not allowed in sandbox', err)
   1055    end)
   1056  end)
   1057 
   1058  describe('`silent` context', function()
   1059    it('works', function()
   1060      exec_lua [[
   1061        -- Should be same as `vim.cmd('silent lua print("aaa")')`
   1062        vim._with({ silent = true }, function() print('aaa') end)
   1063      ]]
   1064      eq('', exec_capture('messages'))
   1065 
   1066      exec_lua [[ vim._with({ silent = true }, function() vim.cmd.echomsg('"bbb"') end) ]]
   1067      eq('', exec_capture('messages'))
   1068 
   1069      local screen = Screen.new(20, 5)
   1070      screen:set_default_attr_ids {
   1071        [1] = { bold = true, reverse = true },
   1072        [2] = { bold = true, foreground = Screen.colors.Blue },
   1073      }
   1074      exec_lua [[ vim._with({ silent = true }, function() vim.cmd.echo('"ccc"') end) ]]
   1075      screen:expect [[
   1076        ^                    |
   1077        {2:~                   }|*3
   1078                            |
   1079      ]]
   1080    end)
   1081 
   1082    pending('can be nested', function()
   1083      exec_lua [[ vim._with({ silent = true }, function()
   1084        vim._with({ silent = false }, function()
   1085          print('aaa')
   1086        end)
   1087      end)]]
   1088      eq('aaa', exec_capture('messages'))
   1089    end)
   1090  end)
   1091 
   1092  describe('`unsilent` context', function()
   1093    it('works', function()
   1094      exec_lua [[
   1095        _G.f = function()
   1096          -- Should be same as `vim.cmd('unsilent lua print("aaa")')`
   1097          vim._with({ unsilent = true }, function() print('aaa') end)
   1098        end
   1099      ]]
   1100      command('silent lua f()')
   1101      eq('aaa', exec_capture('messages'))
   1102    end)
   1103 
   1104    pending('can be nested', function()
   1105      exec_lua [[
   1106        _G.f = function()
   1107          vim._with({ unsilent = true }, function()
   1108            vim._with({ unsilent = false }, function() print('aaa') end)
   1109          end)
   1110        end
   1111      ]]
   1112      command('silent lua f()')
   1113      eq('', exec_capture('messages'))
   1114    end)
   1115  end)
   1116 
   1117  describe('`win` context', function()
   1118    it('works', function()
   1119      local out = exec_lua [[
   1120        local other_win, cur_win = setup_windows()
   1121        local inner = vim._with({ win = other_win }, function()
   1122          return api.nvim_get_current_win()
   1123        end)
   1124        return { inner == other_win, api.nvim_get_current_win() == cur_win }
   1125      ]]
   1126      eq({ true, true }, out)
   1127    end)
   1128 
   1129    it('does not trigger events', function()
   1130      exec_lua [[
   1131        _G.test_events = { 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave' }
   1132        _G.test_context = { win = other_win }
   1133        _G.test_trig_event = function() vim.cmd.new() end
   1134      ]]
   1135      assert_events_trigger()
   1136    end)
   1137 
   1138    it('can access window options', function()
   1139      local out = exec_lua [[
   1140        local other_win, cur_win = setup_windows()
   1141        vim.wo[other_win].winblend = 10
   1142        vim.wo[cur_win].winblend = 25
   1143 
   1144        vim._with({ win = other_win }, function()
   1145          vim.cmd.setlocal('winblend=0')
   1146        end)
   1147 
   1148        return vim.wo[other_win].winblend == 0 and vim.wo[cur_win].winblend == 25
   1149      ]]
   1150      eq(true, out)
   1151    end)
   1152 
   1153    it('works with different kinds of windows', function()
   1154      exec_lua [[
   1155        local assert_win = function(win)
   1156          vim._with({ win = win }, function()
   1157            assert(api.nvim_get_current_win() == win)
   1158          end)
   1159        end
   1160 
   1161        -- Current
   1162        assert_win(api.nvim_get_current_win())
   1163 
   1164        -- Not visible
   1165        local other_win, cur_win = setup_windows()
   1166        vim.cmd.tabnew()
   1167        assert_win(other_win)
   1168 
   1169        -- Floating
   1170        local float_win = api.nvim_open_win(
   1171          api.nvim_create_buf(false, true),
   1172          false,
   1173          { relative = 'editor', row = 1, col = 1, height = 5, width = 5}
   1174        )
   1175        assert_win(float_win)
   1176      ]]
   1177    end)
   1178 
   1179    it('does not cause ml_get errors with invalid visual selection', function()
   1180      exec_lua [[
   1181        local feedkeys = function(keys) api.nvim_feedkeys(vim.keycode(keys), 'txn', false) end
   1182 
   1183        -- Add lines to the current buffer and make another window looking into an empty buffer.
   1184        local win_empty, win_lines = setup_windows()
   1185        api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b', 'c' })
   1186 
   1187        -- Start Visual in current window, redraw in other window with fewer lines.
   1188        -- Should be fixed by vim-patch:8.2.4018.
   1189        feedkeys('G<C-V>')
   1190        vim._with({ win = win_empty }, function() vim.cmd.redraw() end)
   1191 
   1192        -- Start Visual in current window, extend it in other window with more lines.
   1193        -- Fixed for win_execute by vim-patch:8.2.4026, but nvim_win_call should also not be affected.
   1194        feedkeys('<Esc>gg')
   1195        api.nvim_set_current_win(win_empty)
   1196        feedkeys('gg<C-V>')
   1197        vim._with({ win = win_lines }, function() feedkeys('G<C-V>') end)
   1198        vim.cmd.redraw()
   1199      ]]
   1200    end)
   1201 
   1202    it('can be nested', function()
   1203      exec_lua [[
   1204        local other_win, cur_win = setup_windows()
   1205        vim._with({ win = other_win }, function()
   1206          assert(api.nvim_get_current_win() == other_win)
   1207          inner = vim._with({ win = cur_win }, function()
   1208            assert(api.nvim_get_current_win() == cur_win)
   1209          end)
   1210          assert(api.nvim_get_current_win() == other_win)
   1211        end)
   1212        assert(api.nvim_get_current_win() == cur_win)
   1213      ]]
   1214    end)
   1215 
   1216    it('updates ruler if cursor moved', function()
   1217      local screen = Screen.new(30, 5)
   1218      screen:set_default_attr_ids {
   1219        [1] = { reverse = true },
   1220        [2] = { bold = true, reverse = true },
   1221      }
   1222      exec_lua [[
   1223        vim.opt.ruler = true
   1224        local lines = {}
   1225        for i = 0, 499 do lines[#lines + 1] = tostring(i) end
   1226        api.nvim_buf_set_lines(0, 0, -1, true, lines)
   1227        api.nvim_win_set_cursor(0, { 20, 0 })
   1228        vim.cmd 'split'
   1229        _G.win = api.nvim_get_current_win()
   1230        vim.cmd "wincmd w | redraw"
   1231      ]]
   1232      screen:expect [[
   1233        19                            |
   1234        {1:< Name] [+] 20,1            3%}|
   1235        ^19                            |
   1236        {2:< Name] [+] 20,1            3%}|
   1237                                      |
   1238      ]]
   1239      exec_lua [[
   1240        vim._with({ win = win }, function() api.nvim_win_set_cursor(0, { 100, 0 }) end)
   1241        vim.cmd "redraw"
   1242      ]]
   1243      screen:expect [[
   1244        99                            |
   1245        {1:< Name] [+] 100,1          19%}|
   1246        ^19                            |
   1247        {2:< Name] [+] 20,1            3%}|
   1248                                      |
   1249      ]]
   1250    end)
   1251 
   1252    it('layout in current tabpage does not affect windows in others', function()
   1253      command('tab split')
   1254      local t2_move_win = api.nvim_get_current_win()
   1255      command('vsplit')
   1256      local t2_other_win = api.nvim_get_current_win()
   1257      command('tabprevious')
   1258      matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)'))
   1259      command('vsplit')
   1260 
   1261      exec_lua('vim._with({ win = ... }, function() vim.cmd.wincmd "J" end)', t2_move_win)
   1262      eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2))
   1263    end)
   1264  end)
   1265 
   1266  describe('`wo` context', function()
   1267    before_each(function()
   1268      exec_lua [[
   1269        _G.other_win, _G.cur_win = setup_windows()
   1270 
   1271        -- 'virtualedit' is global or local to window (global-local) and string
   1272        vim.wo[other_win].virtualedit = 'block'
   1273        vim.wo[cur_win].virtualedit = 'insert'
   1274        vim.go.virtualedit = 'none'
   1275 
   1276        -- 'winblend' is local to window and number
   1277        vim.wo[other_win].winblend = 10
   1278        vim.wo[cur_win].winblend = 25
   1279        vim.go.winblend = 50
   1280 
   1281        -- 'number' is local to window and boolean
   1282        vim.wo[other_win].number = false
   1283        vim.wo[cur_win].number = false
   1284        vim.go.number = true
   1285 
   1286        _G.get_state = function()
   1287          return {
   1288            wo = {
   1289              ve_cur = vim.wo[cur_win].virtualedit,
   1290              ve_other = vim.wo[other_win].virtualedit,
   1291              winbl_cur = vim.wo[cur_win].winblend,
   1292              winbl_other = vim.wo[other_win].winblend,
   1293              nu_cur = vim.wo[cur_win].number,
   1294              nu_other = vim.wo[other_win].number,
   1295            },
   1296            go = {
   1297              ve = vim.go.virtualedit,
   1298              winbl = vim.go.winblend,
   1299              nu = vim.go.number,
   1300            },
   1301          }
   1302        end
   1303      ]]
   1304    end)
   1305 
   1306    it('works', function()
   1307      local out = exec_lua [[
   1308        local context = { wo = { virtualedit = 'all', winblend = 75, number = true } }
   1309 
   1310        local before = get_state()
   1311        local inner = vim._with(context, function()
   1312          assert(api.nvim_get_current_win() == cur_win)
   1313          return get_state()
   1314        end)
   1315 
   1316        return { before = before, inner = inner, after = get_state() }
   1317      ]]
   1318 
   1319      eq({
   1320        wo = {
   1321          ve_cur = 'all',
   1322          ve_other = 'block',
   1323          winbl_cur = 75,
   1324          winbl_other = 10,
   1325          nu_cur = true,
   1326          nu_other = false,
   1327        },
   1328        go = { ve = 'none', winbl = 75, nu = true },
   1329      }, out.inner)
   1330      eq(out.before, out.after)
   1331    end)
   1332 
   1333    it('sets options in `win` context', function()
   1334      local out = exec_lua [[
   1335        local context = { win = other_win, wo = { virtualedit = 'all', winblend = 75, number = true } }
   1336 
   1337        local before = get_state()
   1338        local inner = vim._with(context, function()
   1339          assert(api.nvim_get_current_win() == other_win)
   1340          return get_state()
   1341        end)
   1342 
   1343        return { before = before, inner = inner, after = get_state() }
   1344      ]]
   1345 
   1346      eq({
   1347        wo = {
   1348          ve_cur = 'insert',
   1349          ve_other = 'all',
   1350          winbl_cur = 25,
   1351          winbl_other = 75,
   1352          nu_cur = false,
   1353          nu_other = true,
   1354        },
   1355        go = { ve = 'none', winbl = 75, nu = true },
   1356      }, out.inner)
   1357      eq(out.before, out.after)
   1358    end)
   1359 
   1360    it('restores only options from context', function()
   1361      local out = exec_lua [[
   1362        local context = { wo = { winblend = 75 } }
   1363 
   1364        local inner = vim._with(context, function()
   1365          assert(api.nvim_get_current_win() == cur_win)
   1366          vim.wo[cur_win].virtualedit = 'onemore'
   1367          vim.wo[cur_win].winblend = 99
   1368          return get_state()
   1369        end)
   1370 
   1371        return { inner = inner, after = get_state() }
   1372      ]]
   1373 
   1374      eq({
   1375        wo = {
   1376          ve_cur = 'onemore',
   1377          ve_other = 'block',
   1378          winbl_cur = 99,
   1379          winbl_other = 10,
   1380          nu_cur = false,
   1381          nu_other = false,
   1382        },
   1383        go = { ve = 'none', winbl = 99, nu = true },
   1384      }, out.inner)
   1385      eq({
   1386        wo = {
   1387          ve_cur = 'onemore',
   1388          ve_other = 'block',
   1389          winbl_cur = 25,
   1390          winbl_other = 10,
   1391          nu_cur = false,
   1392          nu_other = false,
   1393        },
   1394        go = { ve = 'none', winbl = 50, nu = true },
   1395      }, out.after)
   1396    end)
   1397 
   1398    it('does not trigger events', function()
   1399      exec_lua [[
   1400        _G.test_events = { 'WinEnter', 'WinLeave', 'BufWinEnter', 'BufWinLeave' }
   1401        _G.test_context = { wo = { winblend = 75 } }
   1402        _G.test_trig_event = function() vim.cmd.new() end
   1403      ]]
   1404      assert_events_trigger()
   1405    end)
   1406 
   1407    it('can be nested', function()
   1408      local out = exec_lua [[
   1409        local before, before_inner, after_inner = get_state(), nil, nil
   1410        vim._with({ wo = { winblend = 75, virtualedit = 'all', number = true } }, function()
   1411          before_inner = get_state()
   1412          inner = vim._with({ wo = { winblend = 99, number = false } }, get_state)
   1413          after_inner = get_state()
   1414        end)
   1415        return {
   1416          before = before, before_inner = before_inner,
   1417          inner = inner,
   1418          after_inner = after_inner, after = get_state(),
   1419        }
   1420      ]]
   1421      eq(99, out.inner.wo.winbl_cur)
   1422      eq('all', out.inner.wo.ve_cur)
   1423      eq(false, out.inner.wo.nu_cur)
   1424      eq(out.before_inner, out.after_inner)
   1425      eq(out.before, out.after)
   1426    end)
   1427  end)
   1428 
   1429  it('returns what callback returns', function()
   1430    local out_verify = exec_lua [[
   1431      out = { vim._with({}, function()
   1432        return 'a', 2, nil, { 4 }, function() end
   1433      end) }
   1434      return {
   1435        out[1] == 'a', out[2] == 2, out[3] == nil,
   1436        vim.deep_equal(out[4], { 4 }),
   1437        type(out[5]) == 'function',
   1438        vim.tbl_count(out),
   1439      }
   1440    ]]
   1441    eq({ true, true, true, true, true, 4 }, out_verify)
   1442  end)
   1443 
   1444  it('can return values by reference', function()
   1445    local out = exec_lua [[
   1446      local val = { 4, 10 }
   1447      local ref = vim._with({}, function() return val end)
   1448      ref[1] = 7
   1449      return val
   1450    ]]
   1451    eq({ 7, 10 }, out)
   1452  end)
   1453 
   1454  it('can not work with conflicting `buf` and `win`', function()
   1455    local out = exec_lua [[
   1456      local other_buf, cur_buf = setup_buffers()
   1457      local other_win, cur_win = setup_windows()
   1458      assert(api.nvim_win_get_buf(other_win) ~= other_buf)
   1459      local _, err = pcall(vim._with, { buf = other_buf, win = other_win }, function() end)
   1460      return err
   1461    ]]
   1462    matches('Can not set both `buf` and `win`', out)
   1463  end)
   1464 
   1465  it('works with several contexts at once', function()
   1466    local out = exec_lua [[
   1467      local other_buf, cur_buf = setup_buffers()
   1468      vim.bo[other_buf].commentstring = '## %s'
   1469      api.nvim_buf_set_lines(other_buf, 0, -1, false, { 'aaa', 'bbb', 'ccc' })
   1470      api.nvim_buf_set_mark(other_buf, 'm', 2, 2, {})
   1471 
   1472      vim.go.commentstring = '// %s'
   1473      vim.go.langmap = 'xy,yx'
   1474 
   1475      local context = {
   1476        buf = other_buf,
   1477        bo = { commentstring = '-- %s' },
   1478        go = { langmap = 'ab,ba' },
   1479        lockmarks = true,
   1480      }
   1481 
   1482      local inner = vim._with(context, function()
   1483        api.nvim_buf_set_lines(0, 0, -1, false, { 'uuu', 'vvv', 'www' })
   1484        return {
   1485          buf = api.nvim_get_current_buf(),
   1486          bo = { cms = vim.bo.commentstring },
   1487          go = { cms = vim.go.commentstring, lmap = vim.go.langmap },
   1488          mark = api.nvim_buf_get_mark(0, 'm')
   1489        }
   1490      end)
   1491 
   1492      local after = {
   1493        buf = api.nvim_get_current_buf(),
   1494        bo = { cms = vim.bo[other_buf].commentstring },
   1495        go = { cms = vim.go.commentstring, lmap = vim.go.langmap },
   1496        mark = api.nvim_buf_get_mark(other_buf, 'm')
   1497      }
   1498 
   1499      return {
   1500        context_buf = other_buf, cur_buf = cur_buf,
   1501        inner = inner, after = after
   1502      }
   1503    ]]
   1504 
   1505    eq({
   1506      buf = out.context_buf,
   1507      bo = { cms = '-- %s' },
   1508      go = { cms = '// %s', lmap = 'ab,ba' },
   1509      mark = { 2, 2 },
   1510    }, out.inner)
   1511    eq({
   1512      buf = out.cur_buf,
   1513      bo = { cms = '## %s' },
   1514      go = { cms = '// %s', lmap = 'xy,yx' },
   1515      mark = { 2, 2 },
   1516    }, out.after)
   1517  end)
   1518 
   1519  it('works with same option set in different contexts', function()
   1520    local out = exec_lua [[
   1521      local get_state = function()
   1522        return {
   1523          bo = { cms = vim.bo.commentstring },
   1524          wo = { ve = vim.wo.virtualedit },
   1525          go = { cms = vim.go.commentstring, ve = vim.go.virtualedit },
   1526        }
   1527      end
   1528 
   1529      vim.bo.commentstring = '// %s'
   1530      vim.go.commentstring = '$$ %s'
   1531      vim.wo.virtualedit = 'insert'
   1532      vim.go.virtualedit = 'none'
   1533 
   1534      local before = get_state()
   1535      local context_no_go = {
   1536        o = { commentstring = '-- %s', virtualedit = 'all' },
   1537        bo = { commentstring = '!! %s' },
   1538        wo = { virtualedit = 'onemore' },
   1539      }
   1540      local inner_no_go = vim._with(context_no_go, get_state)
   1541      local middle = get_state()
   1542      local context_with_go = {
   1543        o = { commentstring = '-- %s', virtualedit = 'all' },
   1544        bo = { commentstring = '!! %s' },
   1545        wo = { virtualedit = 'onemore' },
   1546        go = { commentstring = '@@ %s', virtualedit = 'block' },
   1547      }
   1548      local inner_with_go = vim._with(context_with_go, get_state)
   1549      return {
   1550        before = before,
   1551        inner_no_go = inner_no_go,
   1552        middle = middle,
   1553        inner_with_go = inner_with_go,
   1554        after = get_state(),
   1555      }
   1556    ]]
   1557 
   1558    -- Should prefer explicit local scopes instead of `o`
   1559    eq({
   1560      bo = { cms = '!! %s' },
   1561      wo = { ve = 'onemore' },
   1562      go = { cms = '-- %s', ve = 'all' },
   1563    }, out.inner_no_go)
   1564    eq(out.before, out.middle)
   1565 
   1566    -- Should prefer explicit global scopes instead of `o`
   1567    eq({
   1568      bo = { cms = '!! %s' },
   1569      wo = { ve = 'onemore' },
   1570      go = { cms = '@@ %s', ve = 'block' },
   1571    }, out.inner_with_go)
   1572    eq(out.middle, out.after)
   1573  end)
   1574 
   1575  pending('can forward command modifiers to user command', function()
   1576    local out = exec_lua [[
   1577      local test_flags = {
   1578        'emsg_silent',
   1579        'hide',
   1580        'keepalt',
   1581        'keepjumps',
   1582        'keepmarks',
   1583        'keeppatterns',
   1584        'lockmarks',
   1585        'noautocmd',
   1586        'silent',
   1587        'unsilent',
   1588      }
   1589 
   1590      local used_smods
   1591      local command = function(data)
   1592        used_smods = data.smods
   1593      end
   1594      api.nvim_create_user_command('DummyLog', command, {})
   1595 
   1596      local res = {}
   1597      for _, flag in ipairs(test_flags) do
   1598        used_smods = nil
   1599        vim._with({ [flag] = true }, function() vim.cmd('DummyLog') end)
   1600        res[flag] = used_smods[flag]
   1601      end
   1602      return res
   1603    ]]
   1604    for k, v in pairs(out) do
   1605      eq({ k, true }, { k, v })
   1606    end
   1607  end)
   1608 
   1609  it('handles error in callback', function()
   1610    -- Should still restore initial context
   1611    local out_buf = exec_lua [[
   1612      local other_buf, cur_buf = setup_buffers()
   1613      vim.bo[other_buf].commentstring = '## %s'
   1614 
   1615      local context = { buf = other_buf, bo = { commentstring = '-- %s' } }
   1616      local ok, err = pcall(vim._with, context, function() error('Oops buf', 0) end)
   1617 
   1618      return {
   1619        ok,
   1620        err,
   1621        api.nvim_get_current_buf() == cur_buf,
   1622        vim.bo[other_buf].commentstring,
   1623      }
   1624    ]]
   1625    eq({ false, 'Oops buf', true, '## %s' }, out_buf)
   1626 
   1627    local out_win = exec_lua [[
   1628      local other_win, cur_win = setup_windows()
   1629      vim.wo[other_win].winblend = 25
   1630 
   1631      local context = { win = other_win, wo = { winblend = 50 } }
   1632      local ok, err = pcall(vim._with, context, function() error('Oops win', 0) end)
   1633 
   1634      return {
   1635        ok,
   1636        err,
   1637        api.nvim_get_current_win() == cur_win,
   1638        vim.wo[other_win].winblend,
   1639      }
   1640    ]]
   1641    eq({ false, 'Oops win', true, 25 }, out_win)
   1642  end)
   1643 
   1644  it('handles not supported option', function()
   1645    local out = exec_lua [[
   1646      -- Should still restore initial state
   1647      vim.bo.commentstring = '## %s'
   1648 
   1649      local context = { o = { commentstring = '-- %s' }, bo = { winblend = 10 } }
   1650      local ok, err = pcall(vim._with, context, function() end)
   1651 
   1652      return { ok = ok, err = err, cms = vim.bo.commentstring }
   1653    ]]
   1654    eq(false, out.ok)
   1655    matches('window.*option.*winblend', out.err)
   1656    eq('## %s', out.cms)
   1657  end)
   1658 
   1659  it('validates arguments', function()
   1660    exec_lua [[
   1661      _G.get_error = function(...)
   1662        local _, err = pcall(vim._with, ...)
   1663        return err or ''
   1664      end
   1665    ]]
   1666    local get_error = function(string_args)
   1667      return exec_lua('return get_error(' .. string_args .. ')')
   1668    end
   1669 
   1670    matches('context.*table', get_error("'a', function() end"))
   1671    matches('f.*function', get_error('{}, 1'))
   1672 
   1673    local assert_context = function(bad_context, expected_type)
   1674      local bad_field = vim.tbl_keys(bad_context)[1]
   1675      matches(
   1676        'context%.' .. bad_field .. '.*' .. expected_type,
   1677        get_error(vim.inspect(bad_context) .. ', function() end')
   1678      )
   1679    end
   1680 
   1681    assert_context({ bo = 1 }, 'table')
   1682    assert_context({ buf = 'a' }, 'number')
   1683    assert_context({ emsg_silent = 1 }, 'boolean')
   1684    assert_context({ env = 1 }, 'table')
   1685    assert_context({ go = 1 }, 'table')
   1686    assert_context({ hide = 1 }, 'boolean')
   1687    assert_context({ keepalt = 1 }, 'boolean')
   1688    assert_context({ keepjumps = 1 }, 'boolean')
   1689    assert_context({ keepmarks = 1 }, 'boolean')
   1690    assert_context({ keeppatterns = 1 }, 'boolean')
   1691    assert_context({ lockmarks = 1 }, 'boolean')
   1692    assert_context({ noautocmd = 1 }, 'boolean')
   1693    assert_context({ o = 1 }, 'table')
   1694    assert_context({ sandbox = 1 }, 'boolean')
   1695    assert_context({ silent = 1 }, 'boolean')
   1696    assert_context({ unsilent = 1 }, 'boolean')
   1697    assert_context({ win = 'a' }, 'number')
   1698    assert_context({ wo = 1 }, 'table')
   1699 
   1700    matches('Invalid buffer', get_error('{ buf = -1 }, function() end'))
   1701    matches('Invalid window', get_error('{ win = -1 }, function() end'))
   1702  end)
   1703 
   1704  it('no double-free when called from :filter browse oldfiles #31501', function()
   1705    exec_lua([=[
   1706      vim.api.nvim_create_autocmd('BufEnter', {
   1707        callback = function()
   1708          vim._with({ lockmarks = true }, function() end)
   1709        end,
   1710      })
   1711      vim.cmd([[
   1712        let v:oldfiles = ['Xoldfile']
   1713        call nvim_input('1<CR>')
   1714        noswapfile filter /Xoldfile/ browse oldfiles
   1715      ]])
   1716    ]=])
   1717    n.assert_alive()
   1718    eq('Xoldfile', fn.bufname('%'))
   1719  end)
   1720 end)