diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2021-09-17 09:16:40 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-09-17 09:16:40 -0700 |
| commit | d8de4eb685e35646c7d541e9a75bdc296127b7e2 (patch) | |
| tree | 4bb05ec713856715ac9ba57e5d116eed344511b9 /test/functional/editor | |
| parent | d56002f7b722facd97b0958e141c8ed2d01495f7 (diff) | |
| download | rneovim-d8de4eb685e35646c7d541e9a75bdc296127b7e2.tar.gz rneovim-d8de4eb685e35646c7d541e9a75bdc296127b7e2.tar.bz2 rneovim-d8de4eb685e35646c7d541e9a75bdc296127b7e2.zip | |
test: reorg #15698
Problem:
Subdirectories like "visual", "insert", "normal" encourage people to
separate *related* tests for no good reason. Typically the _mode_ is
not the relevant topic of a test (and when it is, _then_ create
an appropriate describe() or it()).
Solution:
- Delete the various `test/functional/<mode>/` subdirectories, move
their tests to more meaningful topics.
- Rename `…/normal/` to `…/editor/`.
- Move or merge `…/visual/*` and `…/insert/*` tests into here where
appropriate.
- Rename `…/eval/` to `…/vimscript/`.
- Move `…/viml/*` into here also.
* test(reorg): insert/* => editor/mode_insert_spec.lua
* test(reorg): cmdline/* => editor/mode_cmdline_spec.lua
* test(reorg): eval core tests => eval_spec.lua
Diffstat (limited to 'test/functional/editor')
| -rw-r--r-- | test/functional/editor/K_spec.lua | 38 | ||||
| -rw-r--r-- | test/functional/editor/completion_spec.lua | 1196 | ||||
| -rw-r--r-- | test/functional/editor/count_spec.lua | 39 | ||||
| -rw-r--r-- | test/functional/editor/fold_spec.lua | 362 | ||||
| -rw-r--r-- | test/functional/editor/jump_spec.lua | 139 | ||||
| -rw-r--r-- | test/functional/editor/lang_spec.lua | 63 | ||||
| -rw-r--r-- | test/functional/editor/langmap_spec.lua | 280 | ||||
| -rw-r--r-- | test/functional/editor/macro_spec.lua | 30 | ||||
| -rw-r--r-- | test/functional/editor/meta_key_spec.lua | 55 | ||||
| -rw-r--r-- | test/functional/editor/mode_cmdline_spec.lua | 69 | ||||
| -rw-r--r-- | test/functional/editor/mode_insert_spec.lua | 89 | ||||
| -rw-r--r-- | test/functional/editor/mode_visual_spec.lua | 26 | ||||
| -rw-r--r-- | test/functional/editor/put_spec.lua | 939 | ||||
| -rw-r--r-- | test/functional/editor/search_spec.lua | 17 | ||||
| -rw-r--r-- | test/functional/editor/tabpage_spec.lua | 38 | ||||
| -rw-r--r-- | test/functional/editor/undo_spec.lua | 61 |
16 files changed, 3441 insertions, 0 deletions
diff --git a/test/functional/editor/K_spec.lua b/test/functional/editor/K_spec.lua new file mode 100644 index 0000000000..40f36491e4 --- /dev/null +++ b/test/functional/editor/K_spec.lua @@ -0,0 +1,38 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq, clear, eval, feed, retry = + helpers.eq, helpers.clear, helpers.eval, helpers.feed, helpers.retry + +describe('K', function() + local test_file = 'K_spec_out' + before_each(function() + clear() + os.remove(test_file) + end) + after_each(function() + os.remove(test_file) + end) + + it("invokes colon-prefixed 'keywordprg' as Vim command", function() + helpers.source([[ + let @a='fnord' + set keywordprg=:put]]) + + -- K on the text "a" resolves to `:put a`. + feed('ia<ESC>K') + helpers.expect([[ + a + fnord]]) + end) + + it("invokes non-prefixed 'keywordprg' as shell command", function() + helpers.source([[ + let @a='fnord' + set keywordprg=echo\ fnord>>]]) + + -- K on the text "K_spec_out" resolves to `!echo fnord >> K_spec_out`. + feed('i'..test_file..'<ESC>K') + retry(nil, nil, function() eq(1, eval('filereadable("'..test_file..'")')) end) + eq({'fnord'}, eval("readfile('"..test_file.."')")) + end) + +end) diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua new file mode 100644 index 0000000000..befad29922 --- /dev/null +++ b/test/functional/editor/completion_spec.lua @@ -0,0 +1,1196 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local assert_alive = helpers.assert_alive +local clear, feed = helpers.clear, helpers.feed +local eval, eq, neq = helpers.eval, helpers.eq, helpers.neq +local feed_command, source, expect = helpers.feed_command, helpers.source, helpers.expect +local funcs = helpers.funcs +local curbufmeths = helpers.curbufmeths +local command = helpers.command +local meths = helpers.meths +local poke_eventloop = helpers.poke_eventloop + +describe('completion', function() + local screen + + before_each(function() + clear() + screen = Screen.new(60, 8) + screen:attach() + screen:set_default_attr_ids({ + [0] = {bold=true, foreground=Screen.colors.Blue}, + [1] = {background = Screen.colors.LightMagenta}, + [2] = {background = Screen.colors.Grey}, + [3] = {bold = true}, + [4] = {bold = true, foreground = Screen.colors.SeaGreen}, + [5] = {foreground = Screen.colors.Red}, + [6] = {background = Screen.colors.Black}, + [7] = {foreground = Screen.colors.White, background = Screen.colors.Red}, + [8] = {reverse = true}, + [9] = {bold = true, reverse = true}, + [10] = {foreground = Screen.colors.Grey0, background = Screen.colors.Yellow}, + }) + end) + + describe('v:completed_item', function() + it('is empty dict until completion', function() + eq({}, eval('v:completed_item')) + end) + it('is empty dict if the candidate is not inserted', function() + feed('ifoo<ESC>o<C-x><C-n>') + screen:expect([[ + foo | + foo^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) The only match} | + ]]) + feed('<C-e>') + screen:expect([[ + foo | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('<ESC>') + eq({}, eval('v:completed_item')) + end) + it('returns expected dict in normal completion', function() + feed('ifoo<ESC>o<C-x><C-n>') + eq('foo', eval('getline(2)')) + eq({word = 'foo', abbr = '', menu = '', + info = '', kind = '', user_data = ''}, + eval('v:completed_item')) + end) + it('is readonly', function() + screen:try_resize(80, 8) + feed('ifoo<ESC>o<C-x><C-n><ESC>') + feed_command('let v:completed_item.word = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + + feed_command('let v:completed_item.abbr = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + + feed_command('let v:completed_item.menu = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + + feed_command('let v:completed_item.info = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + + feed_command('let v:completed_item.kind = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + + feed_command('let v:completed_item.user_data = "bar"') + neq(nil, string.find(eval('v:errmsg'), '^E46: ')) + feed_command('let v:errmsg = ""') + end) + it('returns expected dict in omni completion', function() + source([[ + function! TestOmni(findstart, base) abort + return a:findstart ? 0 : [{'word': 'foo', 'abbr': 'bar', + \ 'menu': 'baz', 'info': 'foobar', 'kind': 'foobaz'}, + \ {'word': 'word', 'abbr': 'abbr', 'menu': 'menu', + \ 'info': 'info', 'kind': 'kind'}] + endfunction + setlocal omnifunc=TestOmni + ]]) + feed('i<C-x><C-o>') + eq('foo', eval('getline(1)')) + screen:expect([[ + foo^ | + {2:bar foobaz baz }{0: }| + {1:abbr kind menu }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Omni completion (^O^N^P) }{4:match 1 of 2} | + ]]) + eq({word = 'foo', abbr = 'bar', menu = 'baz', + info = 'foobar', kind = 'foobaz', user_data = ''}, + eval('v:completed_item')) + end) + end) + + describe('completeopt', function() + before_each(function() + source([[ + function! TestComplete() abort + call complete(1, ['foo']) + return '' + endfunction + ]]) + end) + + it('inserts the first candidate if default', function() + feed_command('set completeopt+=menuone') + feed('ifoo<ESC>o') + screen:expect([[ + foo | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('<C-x>') + -- the ^X prompt, only test this once + screen:expect([[ + foo | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)} | + ]]) + feed('<C-n>') + screen:expect([[ + foo | + foo^ | + {2:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) The only match} | + ]]) + feed('bar<ESC>') + eq('foobar', eval('getline(2)')) + feed('o<C-r>=TestComplete()<CR>') + screen:expect([[ + foo | + foobar | + foo^ | + {2:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + eq('foo', eval('getline(3)')) + end) + it('selects the first candidate if noinsert', function() + feed_command('set completeopt+=menuone,noinsert') + feed('ifoo<ESC>o<C-x><C-n>') + screen:expect([[ + foo | + ^ | + {2:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) The only match} | + ]]) + feed('<C-y>') + screen:expect([[ + foo | + foo^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('<ESC>') + eq('foo', eval('getline(2)')) + feed('o<C-r>=TestComplete()<CR>') + screen:expect([[ + foo | + foo | + ^ | + {2:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('<C-y><ESC>') + eq('foo', eval('getline(3)')) + end) + it('does not insert the first candidate if noselect', function() + feed_command('set completeopt+=menuone,noselect') + feed('ifoo<ESC>o<C-x><C-n>') + screen:expect([[ + foo | + ^ | + {1:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{5:Back at original} | + ]]) + feed('b') + screen:expect([[ + foo | + b^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{5:Back at original} | + ]]) + feed('ar<ESC>') + eq('bar', eval('getline(2)')) + feed('o<C-r>=TestComplete()<CR>') + screen:expect([[ + foo | + bar | + ^ | + {1:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('bar<ESC>') + eq('bar', eval('getline(3)')) + end) + it('does not select/insert the first candidate if noselect and noinsert', function() + feed_command('set completeopt+=menuone,noselect,noinsert') + feed('ifoo<ESC>o<C-x><C-n>') + screen:expect([[ + foo | + ^ | + {1:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{5:Back at original} | + ]]) + feed('<ESC>') + screen:expect([[ + foo | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + eq('', eval('getline(2)')) + feed('o<C-r>=TestComplete()<CR>') + screen:expect([[ + foo | + | + ^ | + {1:foo }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + feed('<ESC>') + screen:expect([[ + foo | + | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + eq('', eval('getline(3)')) + end) + it('does not change modified state if noinsert', function() + feed_command('set completeopt+=menuone,noinsert') + feed_command('setlocal nomodified') + feed('i<C-r>=TestComplete()<CR><ESC>') + eq(0, eval('&l:modified')) + end) + it('does not change modified state if noselect', function() + feed_command('set completeopt+=menuone,noselect') + feed_command('setlocal nomodified') + feed('i<C-r>=TestComplete()<CR><ESC>') + eq(0, eval('&l:modified')) + end) + end) + + describe('completeopt+=noinsert does not add blank undo items', function() + before_each(function() + source([[ + function! TestComplete() abort + call complete(1, ['foo', 'bar']) + return '' + endfunction + ]]) + feed_command('set completeopt+=noselect,noinsert') + feed_command('inoremap <right> <c-r>=TestComplete()<cr>') + end) + + local tests = { + ['<up>, <down>, <cr>'] = {'<down><cr>', '<up><cr>'}, + ['<c-n>, <c-p>, <c-y>'] = {'<c-n><c-y>', '<c-p><c-y>'}, + } + + for name, seq in pairs(tests) do + it('using ' .. name, function() + feed('iaaa<esc>') + feed('A<right>' .. seq[1] .. '<esc>') + feed('A<right><esc>A<right><esc>') + feed('A<cr>bbb<esc>') + feed('A<right>' .. seq[2] .. '<esc>') + feed('A<right><esc>A<right><esc>') + feed('A<cr>ccc<esc>') + feed('A<right>' .. seq[1] .. '<esc>') + feed('A<right><esc>A<right><esc>') + + local expected = { + {'foo', 'bar', 'foo'}, + {'foo', 'bar', 'ccc'}, + {'foo', 'bar'}, + {'foo', 'bbb'}, + {'foo'}, + {'aaa'}, + {''}, + } + + for i = 1, #expected do + if i > 1 then + feed('u') + end + eq(expected[i], eval('getline(1, "$")')) + end + + for i = #expected, 1, -1 do + if i < #expected then + feed('<c-r>') + end + eq(expected[i], eval('getline(1, "$")')) + end + end) + end + end) + + describe("refresh:always", function() + before_each(function() + source([[ + function! TestCompletion(findstart, base) abort + if a:findstart + let line = getline('.') + let start = col('.') - 1 + while start > 0 && line[start - 1] =~ '\a' + let start -= 1 + endwhile + return start + else + let ret = [] + for m in split("January February March April May June July August September October November December") + if m =~ a:base " match by regex + call add(ret, m) + endif + endfor + return {'words':ret, 'refresh':'always'} + endif + endfunction + + set completeopt=menuone,noselect + set completefunc=TestCompletion + ]]) + end ) + + it('completes on each input char', function () + feed('i<C-x><C-u>') + screen:expect([[ + ^ | + {1:January }{6: }{0: }| + {1:February }{6: }{0: }| + {1:March }{6: }{0: }| + {1:April }{2: }{0: }| + {1:May }{2: }{0: }| + {1:June }{2: }{0: }| + {3:-- User defined completion (^U^N^P) }{5:Back at original} | + ]]) + feed('u') + screen:expect([[ + u^ | + {1:January }{0: }| + {1:February }{0: }| + {1:June }{0: }| + {1:July }{0: }| + {1:August }{0: }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{5:Back at original} | + ]]) + feed('g') + screen:expect([[ + ug^ | + {1:August }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{5:Back at original} | + ]]) + feed('<Down>') + screen:expect([[ + ug^ | + {2:August }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) The only match} | + ]]) + feed('<C-y>') + screen:expect([[ + August^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + expect('August') + end) + + it("repeats correctly after backspace #2674", function () + feed('o<C-x><C-u>Ja') + screen:expect([[ + | + Ja^ | + {1:January }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{5:Back at original} | + ]]) + feed('<BS>') + screen:expect([[ + | + J^ | + {1:January }{0: }| + {1:June }{0: }| + {1:July }{0: }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{5:Back at original} | + ]]) + feed('<C-n>') + screen:expect([[ + | + January^ | + {2:January }{0: }| + {1:June }{0: }| + {1:July }{0: }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{4:match 1 of 3} | + ]]) + feed('<C-n>') + screen:expect([[ + | + June^ | + {1:January }{0: }| + {2:June }{0: }| + {1:July }{0: }| + {0:~ }| + {0:~ }| + {3:-- User defined completion (^U^N^P) }{4:match 2 of 3} | + ]]) + feed('<Esc>') + screen:expect([[ + | + Jun^e | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + feed('.') + screen:expect([[ + | + June | + Jun^e | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + expect([[ + + June + June]]) + end) + end) + + describe('with a lot of items', function() + before_each(function() + source([[ + function! TestComplete() abort + call complete(1, map(range(0,100), "string(v:val)")) + return '' + endfunction + ]]) + feed_command("set completeopt=menuone,noselect") + end) + + it("works", function() + feed('i<C-r>=TestComplete()<CR>') + screen:expect([[ + ^ | + {1:0 }{6: }{0: }| + {1:1 }{2: }{0: }| + {1:2 }{2: }{0: }| + {1:3 }{2: }{0: }| + {1:4 }{2: }{0: }| + {1:5 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('7') + screen:expect([[ + 7^ | + {1:7 }{6: }{0: }| + {1:70 }{6: }{0: }| + {1:71 }{6: }{0: }| + {1:72 }{2: }{0: }| + {1:73 }{2: }{0: }| + {1:74 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<c-n>') + screen:expect([[ + 7^ | + {2:7 }{6: }{0: }| + {1:70 }{6: }{0: }| + {1:71 }{6: }{0: }| + {1:72 }{2: }{0: }| + {1:73 }{2: }{0: }| + {1:74 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<c-n>') + screen:expect([[ + 70^ | + {1:7 }{6: }{0: }| + {2:70 }{6: }{0: }| + {1:71 }{6: }{0: }| + {1:72 }{2: }{0: }| + {1:73 }{2: }{0: }| + {1:74 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + end) + + it('can be navigated with <PageDown>, <PageUp>', function() + feed('i<C-r>=TestComplete()<CR>') + screen:expect([[ + ^ | + {1:0 }{6: }{0: }| + {1:1 }{2: }{0: }| + {1:2 }{2: }{0: }| + {1:3 }{2: }{0: }| + {1:4 }{2: }{0: }| + {1:5 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageDown>') + screen:expect([[ + ^ | + {1:0 }{6: }{0: }| + {1:1 }{2: }{0: }| + {1:2 }{2: }{0: }| + {2:3 }{0: }| + {1:4 }{2: }{0: }| + {1:5 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageDown>') + screen:expect([[ + ^ | + {1:5 }{6: }{0: }| + {1:6 }{2: }{0: }| + {2:7 }{0: }| + {1:8 }{2: }{0: }| + {1:9 }{2: }{0: }| + {1:10 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<Down>') + screen:expect([[ + ^ | + {1:5 }{6: }{0: }| + {1:6 }{2: }{0: }| + {1:7 }{2: }{0: }| + {2:8 }{0: }| + {1:9 }{2: }{0: }| + {1:10 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageUp>') + screen:expect([[ + ^ | + {1:2 }{6: }{0: }| + {1:3 }{2: }{0: }| + {2:4 }{0: }| + {1:5 }{2: }{0: }| + {1:6 }{2: }{0: }| + {1:7 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageUp>') -- stop on first item + screen:expect([[ + ^ | + {2:0 }{6: }{0: }| + {1:1 }{2: }{0: }| + {1:2 }{2: }{0: }| + {1:3 }{2: }{0: }| + {1:4 }{2: }{0: }| + {1:5 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageUp>') -- when on first item, unselect + screen:expect([[ + ^ | + {1:0 }{6: }{0: }| + {1:1 }{2: }{0: }| + {1:2 }{2: }{0: }| + {1:3 }{2: }{0: }| + {1:4 }{2: }{0: }| + {1:5 }{2: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageUp>') -- when unselected, select last item + screen:expect([[ + ^ | + {1:95 }{2: }{0: }| + {1:96 }{2: }{0: }| + {1:97 }{2: }{0: }| + {1:98 }{2: }{0: }| + {1:99 }{2: }{0: }| + {2:100 }{6: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<PageUp>') + screen:expect([[ + ^ | + {1:94 }{2: }{0: }| + {1:95 }{2: }{0: }| + {2:96 }{0: }| + {1:97 }{2: }{0: }| + {1:98 }{2: }{0: }| + {1:99 }{6: }{0: }| + {3:-- INSERT --} | + ]]) + feed('<cr>') + screen:expect([[ + 96^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + end) + end) + + it("does not indent until an item is selected #8345", function () + -- Indents on "ind", unindents on "unind". + source([[ + function! TestIndent() + let line = getline(v:lnum) + if (line =~ '^\s*ind') + return indent(v:lnum-1) + shiftwidth() + elseif (line =~ '^\s*unind') + return indent(v:lnum-1) - shiftwidth() + else + return indent(v:lnum-1) + endif + endfunction + set indentexpr=TestIndent() + set indentkeys=o,O,!^F,=ind,=unind + set completeopt+=menuone + ]]) + + -- Give some words to complete. + feed("iinc uninc indent unindent<CR>") + + -- Does not indent when "ind" is typed. + feed("in<C-X><C-N>") + -- Completion list is generated incorrectly if we send everything at once + -- via nvim_input(). So poke_eventloop() before sending <BS>. #8480 + poke_eventloop() + feed("<BS>d") + + screen:expect([[ + inc uninc indent unindent | + ind^ | + {2:indent }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 2} | + ]]) + + -- Indents when the item is selected + feed("<C-Y>") + screen:expect([[ + inc uninc indent unindent | + indent^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + -- Indents when completion is exited using ESC. + feed("<CR>in<C-N><BS>d<Esc>") + screen:expect([[ + inc uninc indent unindent | + indent | + in^d | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + -- Works for unindenting too. + feed("ounin<C-X><C-N>") + helpers.poke_eventloop() + feed("<BS>d") + screen:expect([[ + inc uninc indent unindent | + indent | + ind | + unind^ | + {0:~ }{2: unindent }{0: }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 2} | + ]]) + -- Works when going back and forth. + feed("<BS>c") + screen:expect([[ + inc uninc indent unindent | + indent | + ind | + uninc^ | + {0:~ }{2: uninc }{0: }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 2} | + ]]) + feed("<BS>d") + screen:expect([[ + inc uninc indent unindent | + indent | + ind | + unind^ | + {0:~ }{2: unindent }{0: }| + {0:~ }| + {0:~ }| + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 2} | + ]]) + feed("<C-N><C-N><C-Y><Esc>") + screen:expect([[ + inc uninc indent unindent | + indent | + ind | + uninden^t | + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + end) + + it('disables folding during completion', function () + feed_command("set foldmethod=indent") + feed('i<Tab>foo<CR><Tab>bar<Esc>gg') + screen:expect([[ + ^foo | + bar | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + feed('A<C-x><C-l>') + screen:expect([[ + foo^ | + bar | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Whole line completion (^L^N^P) }{7:Pattern not found} | + ]]) + eq(-1, eval('foldclosed(1)')) + end) + + it('popupmenu is not interrupted by events', function () + feed_command("set complete=.") + + feed('ifoobar fooegg<cr>f<c-p>') + screen:expect([[ + foobar fooegg | + fooegg^ | + {1:foobar }{0: }| + {2:fooegg }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 1 of 2} | + ]]) + + assert_alive() + -- popupmenu still visible + screen:expect{grid=[[ + foobar fooegg | + fooegg^ | + {1:foobar }{0: }| + {2:fooegg }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 1 of 2} | + ]], unchanged=true} + + feed('<c-p>') + -- Didn't restart completion: old matches still used + screen:expect([[ + foobar fooegg | + foobar^ | + {2:foobar }{0: }| + {1:fooegg }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 2 of 2} | + ]]) + end) + + describe('lua completion', function() + it('expands when there is only one match', function() + feed(':lua CURRENT_TESTING_VAR = 1<CR>') + feed(':lua CURRENT_TESTING_<TAB>') + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + :lua CURRENT_TESTING_VAR^ | + ]]} + end) + + it('expands when there is only one match', function() + feed(':lua CURRENT_TESTING_FOO = 1<CR>') + feed(':lua CURRENT_TESTING_BAR = 1<CR>') + feed(':lua CURRENT_TESTING_<TAB>') + screen:expect{ grid = [[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {10:CURRENT_TESTING_BAR}{9: CURRENT_TESTING_FOO }| + :lua CURRENT_TESTING_BAR^ | + ]], unchanged = true } + end) + + it('provides completion from `getcompletion()`', function() + eq({'vim'}, funcs.getcompletion('vi', 'lua')) + eq({'api'}, funcs.getcompletion('vim.ap', 'lua')) + eq({'tbl_filter'}, funcs.getcompletion('vim.tbl_fil', 'lua')) + eq({'vim'}, funcs.getcompletion('print(vi', 'lua')) + end) + end) + + describe('from the commandline window', function() + it('is cleared after CTRL-C', function () + feed('q:') + feed('ifoo faa fee f') + screen:expect([[ + | + {8:[No Name] }| + {0::}foo faa fee f^ | + {0:~ }| + {0:~ }| + {0:~ }| + {9:[Command Line] }| + {3:-- INSERT --} | + ]] ) + feed('<c-x><c-n>') + screen:expect([[ + | + {8:[No Name] }| + {0::}foo faa fee foo^ | + {0:~ }{2: foo }{0: }| + {0:~ }{1: faa }{0: }| + {0:~ }{1: fee }{0: }| + {9:[Command Line] }| + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 3} | + ]]) + feed('<c-c>') + screen:expect([[ + | + {8:[No Name] }| + {0::}foo faa fee foo | + {0:~ }| + {0:~ }| + {0:~ }| + {9:[Command Line] }| + :foo faa fee foo^ | + ]]) + end) + end) + + describe('with numeric items', function() + before_each(function() + source([[ + function! TestComplete() abort + call complete(1, g:_complist) + return '' + endfunction + ]]) + meths.set_option('completeopt', 'menuone,noselect') + meths.set_var('_complist', {{ + word=0, + abbr=1, + menu=2, + kind=3, + info=4, + icase=5, + dup=6, + empty=7, + }}) + end) + + it('shows correct variant as word', function() + feed('i<C-r>=TestComplete()<CR>') + screen:expect([[ + ^ | + {1:1 3 2 }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + end) + end) + + it("'ignorecase' 'infercase' CTRL-X CTRL-N #6451", function() + feed_command('set ignorecase infercase') + feed_command('edit BACKERS.md') + feed('oX<C-X><C-N>') + screen:expect([[ + # Bountysource Backers | + Xnull^ | + {2:Xnull }{6: } | + {1:Xoxomoon }{6: }ryone who backed our [Bountysource fundraise| + {1:Xu }{6: }ountysource.com/teams/neovim/fundraiser)! | + {1:Xpayn }{2: } | + {1:Xinity }{2: }d URL in BACKERS.md. | + {3:-- Keyword Local completion (^N^P) }{4:match 1 of 7} | + ]]) + end) + + it('TextChangedP autocommand', function() + curbufmeths.set_lines(0, 1, false, { 'foo', 'bar', 'foobar'}) + source([[ + set complete=. completeopt=menuone + let g:foo = [] + autocmd! TextChanged * :call add(g:foo, "N") + autocmd! TextChangedI * :call add(g:foo, "I") + autocmd! TextChangedP * :call add(g:foo, "P") + call cursor(3, 1) + ]]) + + command('let g:foo = []') + feed('o') + poke_eventloop() + feed('<esc>') + eq({'I'}, eval('g:foo')) + + command('let g:foo = []') + feed('S') + poke_eventloop() + feed('f') + poke_eventloop() + eq({'I', 'I'}, eval('g:foo')) + feed('<esc>') + + command('let g:foo = []') + feed('S') + poke_eventloop() + feed('f') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + eq({'I', 'I', 'P'}, eval('g:foo')) + feed('<esc>') + + command('let g:foo = []') + feed('S') + poke_eventloop() + feed('f') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + eq({'I', 'I', 'P', 'P'}, eval('g:foo')) + feed('<esc>') + + command('let g:foo = []') + feed('S') + poke_eventloop() + feed('f') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + eq({'I', 'I', 'P', 'P', 'P'}, eval('g:foo')) + feed('<esc>') + + command('let g:foo = []') + feed('S') + poke_eventloop() + feed('f') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + poke_eventloop() + feed('<C-N>') + eq({'I', 'I', 'P', 'P', 'P', 'P'}, eval('g:foo')) + feed('<esc>') + + eq({'foo', 'bar', 'foobar', 'foo'}, eval('getline(1, "$")')) + + source([[ + au! TextChanged + au! TextChangedI + au! TextChangedP + set complete&vim completeopt&vim + ]]) + end) + + it('CompleteChanged autocommand', function() + curbufmeths.set_lines(0, 1, false, { 'foo', 'bar', 'foobar', ''}) + source([[ + set complete=. completeopt=noinsert,noselect,menuone + function! OnPumChange() + let g:event = copy(v:event) + let g:item = get(v:event, 'completed_item', {}) + let g:word = get(g:item, 'word', v:null) + endfunction + autocmd! CompleteChanged * :call OnPumChange() + call cursor(4, 1) + ]]) + + feed('Sf<C-N>') + screen:expect([[ + foo | + bar | + foobar | + f^ | + {1:foo }{0: }| + {1:foobar }{0: }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{5:Back at original} | + ]]) + eq({completed_item = {}, width = 15, + height = 2, size = 2, + col = 0, row = 4, scrollbar = false}, + eval('g:event')) + feed('<C-N>') + screen:expect([[ + foo | + bar | + foobar | + foo^ | + {2:foo }{0: }| + {1:foobar }{0: }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 1 of 2} | + ]]) + eq('foo', eval('g:word')) + feed('<C-N>') + screen:expect([[ + foo | + bar | + foobar | + foobar^ | + {1:foo }{0: }| + {2:foobar }{0: }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 2 of 2} | + ]]) + eq('foobar', eval('g:word')) + feed('<up>') + screen:expect([[ + foo | + bar | + foobar | + foobar^ | + {2:foo }{0: }| + {1:foobar }{0: }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 1 of 2} | + ]]) + eq('foo', eval('g:word')) + feed('<down>') + screen:expect([[ + foo | + bar | + foobar | + foobar^ | + {1:foo }{0: }| + {2:foobar }{0: }| + {0:~ }| + {3:-- Keyword completion (^N^P) }{4:match 2 of 2} | + ]]) + eq('foobar', eval('g:word')) + feed('<esc>') + end) +end) diff --git a/test/functional/editor/count_spec.lua b/test/functional/editor/count_spec.lua new file mode 100644 index 0000000000..94f741250a --- /dev/null +++ b/test/functional/editor/count_spec.lua @@ -0,0 +1,39 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local eval = helpers.eval +local feed = helpers.feed +local clear = helpers.clear +local command = helpers.command + +describe('v:count/v:count1', function() + before_each(function() + clear() + + command('map <silent> _x :<C-u>let g:count = "v:count=". v:count .", v:count1=". v:count1<CR>') + end) + + describe('in cmdwin', function() + it('equal 0/1 when no count is given', function() + feed('q:_x') + eq('v:count=0, v:count1=1', eval('g:count')) + end) + + it('equal 2/2 when count of 2 is given', function() + feed('q:2_x') + eq('v:count=2, v:count1=2', eval('g:count')) + end) + end) + + describe('in normal mode', function() + it('equal 0/1 when no count is given', function() + feed('_x') + eq('v:count=0, v:count1=1', eval('g:count')) + end) + + it('equal 2/2 when count of 2 is given', function() + feed('2_x') + eq('v:count=2, v:count1=2', eval('g:count')) + end) + end) +end) diff --git a/test/functional/editor/fold_spec.lua b/test/functional/editor/fold_spec.lua new file mode 100644 index 0000000000..00e83bedc8 --- /dev/null +++ b/test/functional/editor/fold_spec.lua @@ -0,0 +1,362 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local insert = helpers.insert +local feed = helpers.feed +local expect = helpers.expect +local feed_command = helpers.feed_command +local funcs = helpers.funcs +local foldlevel = funcs.foldlevel +local foldclosedend = funcs.foldclosedend +local eq = helpers.eq + +describe('Folds', function() + local tempfname = 'Xtest-fold.txt' + clear() + before_each(function() feed_command('enew!') end) + after_each(function() os.remove(tempfname) end) + it('manual folding adjusts with filter', function() + insert([[ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20]]) + feed_command('4,$fold', '%foldopen', '10,$fold', '%foldopen') + feed_command('1,8! cat') + feed('5ggzdzMGdd') + expect([[ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9]]) + end) + describe('adjusting folds after :move', function() + local function manually_fold_indent() + -- setting foldmethod twice is a trick to get vim to set the folds for me + feed_command('set foldmethod=indent', 'set foldmethod=manual') + -- Ensure that all folds will get closed (makes it easier to test the + -- length of folds). + feed_command('set foldminlines=0') + -- Start with all folds open (so :move ranges aren't affected by closed + -- folds). + feed_command('%foldopen!') + end + + local function get_folds() + local rettab = {} + for i = 1, funcs.line('$') do + table.insert(rettab, foldlevel(i)) + end + return rettab + end + + local function test_move_indent(insert_string, move_command) + -- This test is easy because we just need to ensure that the resulting + -- fold is the same as calculated when creating folds from scratch. + insert(insert_string) + feed_command(move_command) + local after_move_folds = get_folds() + -- Doesn't change anything, but does call foldUpdateAll() + feed_command('set foldminlines=0') + eq(after_move_folds, get_folds()) + -- Set up the buffer with insert_string for the manual fold testing. + feed_command('enew!') + insert(insert_string) + manually_fold_indent() + feed_command(move_command) + end + + it('neither closes nor corrupts folds', function() + test_move_indent([[ +a + a + a + a + a + a +a + a + a + a + a + a +a + a + a + a + a + a]], '7,12m0') + expect([[ +a + a + a + a + a + a +a + a + a + a + a + a +a + a + a + a + a + a]]) + -- lines are not closed, folds are correct + for i = 1,funcs.line('$') do + eq(-1, funcs.foldclosed(i)) + if i == 1 or i == 7 or i == 13 then + eq(0, foldlevel(i)) + elseif i == 4 then + eq(2, foldlevel(i)) + else + eq(1, foldlevel(i)) + end + end + -- folds are not corrupted + feed('zM') + eq(6, foldclosedend(2)) + eq(12, foldclosedend(8)) + eq(18, foldclosedend(14)) + end) + it("doesn't split a fold when the move is within it", function() + test_move_indent([[ +a + a + a + a + a + a + a + a + a +a]], '5m6') + eq({0, 1, 1, 2, 2, 2, 2, 1, 1, 0}, get_folds()) + end) + it('truncates folds that end in the moved range', function() + test_move_indent([[ +a + a + a + a + a +a +a]], '4,5m6') + eq({0, 1, 2, 0, 0, 0, 0}, get_folds()) + end) + it('moves folds that start between moved range and destination', function() + test_move_indent([[ +a + a + a + a + a +a +a + a + a + a +a +a + a]], '3,4m$') + eq({0, 1, 1, 0, 0, 1, 2, 1, 0, 0, 1, 0, 0}, get_folds()) + end) + it('does not affect folds outside changed lines', function() + test_move_indent([[ + a + a + a +a +a +a + a + a + a]], '4m5') + eq({1, 1, 1, 0, 0, 0, 1, 1, 1}, get_folds()) + end) + it('moves and truncates folds that start in moved range', function() + test_move_indent([[ +a + a + a + a + a +a +a +a +a +a]], '1,3m7') + eq({0, 0, 0, 0, 0, 1, 2, 0, 0, 0}, get_folds()) + end) + it('breaks a fold when moving text into it', function() + test_move_indent([[ +a + a + a + a + a +a +a]], '$m4') + eq({0, 1, 2, 2, 0, 0, 0}, get_folds()) + end) + it('adjusts correctly when moving a range backwards', function() + test_move_indent([[ +a + a + a + a +a]], '2,3m0') + eq({1, 2, 0, 0, 0}, get_folds()) + end) + it('handles shifting all remaining folds', function() + test_move_indent([[ + a + a + a + a + a + a + a + a + a + a + a + a + a + a +a]], '13m7') + eq({1, 2, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 1, 0}, get_folds()) + end) + end) + it('updates correctly on :read', function() + -- luacheck: ignore 621 + helpers.write_file(tempfname, [[ + a + + + a]]) + insert([[ + a + a + a + a + ]]) + feed_command('set foldmethod=indent', '2', '%foldopen') + feed_command('read ' .. tempfname) + -- Just to check we have the correct file text. + expect([[ + a + a + a + + + a + a + a + ]]) + for i = 1,2 do + eq(1, funcs.foldlevel(i)) + end + for i = 3,5 do + eq(0, funcs.foldlevel(i)) + end + for i = 6,8 do + eq(1, funcs.foldlevel(i)) + end + end) + it('combines folds when removing separating space', function() + -- luacheck: ignore 621 + insert([[ + a + a + a + a + a + a + a + a + ]]) + feed_command('set foldmethod=indent', '3,5d') + eq(5, funcs.foldclosedend(1)) + end) + it("doesn't combine folds that have a specified end", function() + insert([[ + {{{ + }}} + + + + {{{ + + }}} + ]]) + feed_command('set foldmethod=marker', '3,5d', '%foldclose') + eq(2, funcs.foldclosedend(1)) + end) + it('splits folds according to >N and <N with foldexpr', function() + helpers.source([[ + function TestFoldExpr(lnum) + let thisline = getline(a:lnum) + if thisline == 'a' + return 1 + elseif thisline == 'b' + return 0 + elseif thisline == 'c' + return '<1' + elseif thisline == 'd' + return '>1' + endif + return 0 + endfunction + ]]) + helpers.write_file(tempfname, [[ + b + b + a + a + d + a + a + c]]) + insert([[ + a + a + a + a + a + a + ]]) + feed_command('set foldmethod=expr', 'set foldexpr=TestFoldExpr(v:lnum)', '2', 'foldopen') + feed_command('read ' .. tempfname, '%foldclose') + eq(2, funcs.foldclosedend(1)) + eq(0, funcs.foldlevel(3)) + eq(0, funcs.foldlevel(4)) + eq(6, funcs.foldclosedend(5)) + eq(10, funcs.foldclosedend(7)) + eq(14, funcs.foldclosedend(11)) + end) +end) diff --git a/test/functional/editor/jump_spec.lua b/test/functional/editor/jump_spec.lua new file mode 100644 index 0000000000..9e7158e2f7 --- /dev/null +++ b/test/functional/editor/jump_spec.lua @@ -0,0 +1,139 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local funcs = helpers.funcs +local feed = helpers.feed +local redir_exec = helpers.redir_exec +local write_file = helpers.write_file + +describe('jumplist', function() + local fname1 = 'Xtest-functional-normal-jump' + local fname2 = fname1..'2' + before_each(clear) + after_each(function() + os.remove(fname1) + os.remove(fname2) + end) + + it('does not add a new entry on startup', function() + eq('\n jump line col file/text\n>', funcs.execute('jumps')) + end) + + it('does not require two <C-O> strokes to jump back', function() + write_file(fname1, 'first file contents') + write_file(fname2, 'second file contents') + + command('args '..fname1..' '..fname2) + local buf1 = funcs.bufnr(fname1) + local buf2 = funcs.bufnr(fname2) + + command('next') + feed('<C-O>') + eq(buf1, funcs.bufnr('%')) + + command('first') + command('snext') + feed('<C-O>') + eq(buf1, funcs.bufnr('%')) + feed('<C-I>') + eq(buf2, funcs.bufnr('%')) + feed('<C-O>') + eq(buf1, funcs.bufnr('%')) + + command('drop '..fname2) + feed('<C-O>') + eq(buf1, funcs.bufnr('%')) + end) +end) + +describe("jumpoptions=stack behaves like 'tagstack'", function() + before_each(function() + clear() + feed(':clearjumps<cr>') + + -- Add lines so that we have locations to jump to. + for i = 1,101,1 + do + feed('iLine ' .. i .. '<cr><esc>') + end + + -- Jump around to add some locations to the jump list. + feed('0gg') + feed('10gg') + feed('20gg') + feed('30gg') + feed('40gg') + feed('50gg') + + feed(':set jumpoptions=stack<cr>') + end) + + after_each(function() + feed('set jumpoptions=') + end) + + it('discards the tail when navigating from the middle', function() + feed('<C-O>') + feed('<C-O>') + + eq( '\n' + .. ' jump line col file/text\n' + .. ' 4 102 0 \n' + .. ' 3 1 0 Line 1\n' + .. ' 2 10 0 Line 10\n' + .. ' 1 20 0 Line 20\n' + .. '> 0 30 0 Line 30\n' + .. ' 1 40 0 Line 40\n' + .. ' 2 50 0 Line 50', + redir_exec('jumps')) + + feed('90gg') + + eq( '\n' + .. ' jump line col file/text\n' + .. ' 5 102 0 \n' + .. ' 4 1 0 Line 1\n' + .. ' 3 10 0 Line 10\n' + .. ' 2 20 0 Line 20\n' + .. ' 1 30 0 Line 30\n' + .. '>', + redir_exec('jumps')) + end) + + it('does not add the same location twice adjacently', function() + feed('60gg') + feed('60gg') + + eq( '\n' + .. ' jump line col file/text\n' + .. ' 7 102 0 \n' + .. ' 6 1 0 Line 1\n' + .. ' 5 10 0 Line 10\n' + .. ' 4 20 0 Line 20\n' + .. ' 3 30 0 Line 30\n' + .. ' 2 40 0 Line 40\n' + .. ' 1 50 0 Line 50\n' + .. '>', + redir_exec('jumps')) + end) + + it('does add the same location twice nonadjacently', function() + feed('10gg') + feed('20gg') + + eq( '\n' + .. ' jump line col file/text\n' + .. ' 8 102 0 \n' + .. ' 7 1 0 Line 1\n' + .. ' 6 10 0 Line 10\n' + .. ' 5 20 0 Line 20\n' + .. ' 4 30 0 Line 30\n' + .. ' 3 40 0 Line 40\n' + .. ' 2 50 0 Line 50\n' + .. ' 1 10 0 Line 10\n' + .. '>', + redir_exec('jumps')) + end) +end) diff --git a/test/functional/editor/lang_spec.lua b/test/functional/editor/lang_spec.lua new file mode 100644 index 0000000000..24d1262f5f --- /dev/null +++ b/test/functional/editor/lang_spec.lua @@ -0,0 +1,63 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear, insert, eq = helpers.clear, helpers.insert, helpers.eq +local command, expect = helpers.command, helpers.expect +local feed, eval = helpers.feed, helpers.eval +local exc_exec = helpers.exc_exec + +describe('gu and gU', function() + before_each(clear) + + it('works in any locale with default casemap', function() + eq('internal,keepascii', eval('&casemap')) + insert("iI") + feed("VgU") + expect("II") + feed("Vgu") + expect("ii") + end) + + describe('works in Turkish locale', function() + clear() + + local err = exc_exec('lang ctype tr_TR.UTF-8') + if err ~= 0 then + pending("Locale tr_TR.UTF-8 not supported", function() end) + return + end + + before_each(function() + command('lang ctype tr_TR.UTF-8') + end) + + it('with default casemap', function() + eq('internal,keepascii', eval('&casemap')) + -- expect ASCII behavior + insert("iI") + feed("VgU") + expect("II") + feed("Vgu") + expect("ii") + end) + + it('with casemap=""', function() + command('set casemap=') + -- expect either Turkish locale behavior or ASCII behavior + local iupper = eval("toupper('i')") + if iupper == "İ" then + insert("iI") + feed("VgU") + expect("İI") + feed("Vgu") + expect("iı") + elseif iupper == "I" then + insert("iI") + feed("VgU") + expect("II") + feed("Vgu") + expect("ii") + else + error("expected toupper('i') to be either 'I' or 'İ'") + end + end) + end) +end) diff --git a/test/functional/editor/langmap_spec.lua b/test/functional/editor/langmap_spec.lua new file mode 100644 index 0000000000..e4349a22e7 --- /dev/null +++ b/test/functional/editor/langmap_spec.lua @@ -0,0 +1,280 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq, neq, call = helpers.eq, helpers.neq, helpers.call +local eval, feed, clear = helpers.eval, helpers.feed, helpers.clear +local command, insert, expect = helpers.command, helpers.insert, helpers.expect +local feed_command = helpers.feed_command +local curwin = helpers.curwin + +describe("'langmap'", function() + before_each(function() + clear() + insert('iii www') + command('set langmap=iw,wi') + feed('gg0') + end) + + it("converts keys in normal mode", function() + feed('ix') + expect('iii ww') + feed('whello<esc>') + expect('iii helloww') + end) + it("gives characters that are mapped by :nmap.", function() + command('map i 0x') + feed('w') + expect('ii www') + end) + describe("'langnoremap' option.", function() + before_each(function() + command('nmapclear') + end) + it("'langnoremap' is by default ON", function() + eq(eval('&langnoremap'), 1) + end) + it("Results of maps are not converted when 'langnoremap' ON.", + function() + command('nmap x i') + feed('xdl<esc>') + expect('dliii www') + end) + it("applies when deciding whether to map recursively", function() + command('nmap l i') + command('nmap w j') + feed('ll') + expect('liii www') + end) + it("does not stop applying 'langmap' on first character of a mapping", + function() + command('1t1') + command('1t1') + command('goto 1') + command('nmap w j') + feed('iiahello') + expect([[ + iii www + iii www + ihelloii www]]) + end) + it("Results of maps are converted when 'langnoremap' OFF.", + function() + command('set nolangnoremap') + command('nmap x i') + feed('xdl<esc>') + expect('iii ww') + end) + end) + -- e.g. CTRL-W_j , mj , 'j and "jp + it('conversions are applied to keys in middle of command', + function() + -- Works in middle of window command + feed('<C-w>s') + local origwin = curwin() + feed('<C-w>i') + neq(curwin(), origwin) + -- Works when setting a mark + feed('yy3p3gg0mwgg0mi') + eq(call('getpos', "'i"), {0, 3, 1, 0}) + eq(call('getpos', "'w"), {0, 1, 1, 0}) + feed('3dd') + -- Works when moving to a mark + feed("'i") + eq(call('getpos', '.'), {0, 1, 1, 0}) + -- Works when selecting a register + feed('qillqqwhhq') + eq(eval('@i'), 'hh') + eq(eval('@w'), 'll') + feed('a<C-r>i<esc>') + expect('illii www') + feed('"ip') + expect('illllii www') + -- Works with i_CTRL-O + feed('0a<C-O>ihi<esc>') + expect('illllii hiwww') + end) + + describe('exceptions', function() + -- All "command characters" that 'langmap' does not apply to. + -- These tests consist of those places where some subset of ASCII + -- characters define certain commands, yet 'langmap' is not applied to + -- them. + -- n.b. I think these shouldn't be exceptions. + it(':s///c confirmation', function() + command('set langmap=yn,ny') + feed('qa') + feed_command('s/i/w/gc') + feed('yynq') + expect('wwi www') + feed('u@a') + expect('wwi www') + eq(eval('@a'), ':s/i/w/gc\ryyn') + end) + it('insert-mode CTRL-G', function() + command('set langmap=jk,kj') + command('d') + insert([[ + hello + hello + hello]]) + expect([[ + hello + hello + hello]]) + feed('qa') + feed('gg3|ahello<C-G>jx<esc>') + feed('q') + expect([[ + helhellolo + helxlo + hello]]) + eq(eval('@a'), 'gg3|ahellojx') + end) + it('command-line CTRL-\\', function() + command('set langmap=en,ne') + feed(':<C-\\>e\'hello\'\r<C-B>put ="<C-E>"<CR>') + expect([[ + iii www + hello]]) + end) + it('command-line CTRL-R', function() + helpers.source([[ + let i_value = 0 + let j_value = 0 + call setreg('i', 'i_value') + call setreg('j', 'j_value') + set langmap=ij,ji + ]]) + feed(':let <C-R>i=1<CR>') + eq(eval('i_value'), 1) + eq(eval('j_value'), 0) + end) + -- it('-- More -- prompt', function() + -- -- The 'b' 'j' 'd' 'f' commands at the -- More -- prompt + -- end) + it('ask yes/no after backwards range', function() + command('set langmap=yn,ny') + feed('dd') + insert([[ + hello + there + these + are + some + lines + ]]) + feed_command('4,2d') + feed('n') + expect([[ + hello + there + these + are + some + lines + ]]) + end) + it('prompt for number', function() + command('set langmap=12,21') + helpers.source([[ + let gotten_one = 0 + function Map() + let answer = inputlist(['a', '1.', '2.', '3.']) + if answer == 1 + let g:gotten_one = 1 + endif + endfunction + nnoremap x :call Map()<CR> + ]]) + feed('x1<CR>') + eq(eval('gotten_one'), 1) + command('let g:gotten_one = 0') + feed_command('call Map()') + feed('1<CR>') + eq(eval('gotten_one'), 1) + end) + end) + it('conversions are not applied during setreg()', + function() + call('setreg', 'i', 'ww') + eq(eval('@i'), 'ww') + end) + it('conversions not applied in insert mode', function() + feed('aiiiwww') + expect('iiiiwwwii www') + end) + it('conversions not applied in search mode', function() + feed('/iii<cr>x') + expect('ii www') + end) + it('conversions not applied in cmdline mode', function() + feed(':call append(1, "iii")<cr>') + expect([[ + iii www + iii]]) + end) + + local function testrecording(command_string, expect_string, setup_function) + if setup_function then setup_function() end + feed('qa' .. command_string .. 'q') + expect(expect_string) + eq(helpers.funcs.nvim_replace_termcodes(command_string, true, true, true), + eval('@a')) + if setup_function then setup_function() end + -- n.b. may need nvim_replace_termcodes() here. + feed('@a') + expect(expect_string) + end + + local function local_setup() + -- Can't use `insert` as it uses `i` and we've swapped the meaning of that + -- with the `langmap` setting. + command('%d') + command("put ='hello'") + command('1d') + end + + it('does not affect recording special keys', function() + testrecording('A<BS><esc>', 'hell', local_setup) + testrecording('>><lt><lt>', 'hello', local_setup) + command('nnoremap \\ x') + testrecording('\\', 'ello', local_setup) + testrecording('A<C-V><BS><esc>', 'hello<BS>', local_setup) + end) + pending('Translates modified keys correctly', function() + command('nnoremap <M-i> x') + command('nnoremap <M-w> l') + testrecording('<M-w>', 'ello', local_setup) + testrecording('<M-i>x', 'hllo', local_setup) + end) + pending('handles multi-byte characters', function() + command('set langmap=ïx') + testrecording('ï', 'ello', local_setup) + -- The test below checks that what's recorded is correct. + -- It doesn't check the behaviour, as in order to cause some behaviour we + -- need to map the multi-byte character, and there is a known bug + -- preventing this from working (see the test below). + command('set langmap=xï') + testrecording('x', 'hello', local_setup) + end) + pending('handles multibyte mappings', function() + -- See this vim issue for the problem, may as well add a test. + -- https://github.com/vim/vim/issues/297 + command('set langmap=ïx') + command('nnoremap x diw') + testrecording('ï', '', local_setup) + command('set nolangnoremap') + command('set langmap=xï') + command('nnoremap ï ix<esc>') + testrecording('x', 'xhello', local_setup) + end) + -- This test is to ensure the behaviour doesn't change from what's already + -- around. I (hardenedapple) personally think this behaviour should be + -- changed. + it('treats control modified keys as characters', function() + command('nnoremap <C-w> iw<esc>') + command('nnoremap <C-i> ii<esc>') + testrecording('<C-w>', 'whello', local_setup) + testrecording('<C-i>', 'ihello', local_setup) + end) + +end) diff --git a/test/functional/editor/macro_spec.lua b/test/functional/editor/macro_spec.lua new file mode 100644 index 0000000000..102d8fc723 --- /dev/null +++ b/test/functional/editor/macro_spec.lua @@ -0,0 +1,30 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local eval = helpers.eval +local feed = helpers.feed +local clear = helpers.clear +local expect = helpers.expect +local command = helpers.command + +describe('macros', function() + before_each(clear) + it('can be recorded and replayed', function() + feed('qiahello<esc>q') + expect('hello') + eq(eval('@i'), 'ahello') + feed('@i') + expect('hellohello') + eq(eval('@i'), 'ahello') + end) + it('applies maps', function() + command('imap x l') + command('nmap l a') + feed('qilxxx<esc>q') + expect('lll') + eq(eval('@i'), 'lxxx') + feed('@i') + expect('llllll') + eq(eval('@i'), 'lxxx') + end) +end) diff --git a/test/functional/editor/meta_key_spec.lua b/test/functional/editor/meta_key_spec.lua new file mode 100644 index 0000000000..2a9541ba96 --- /dev/null +++ b/test/functional/editor/meta_key_spec.lua @@ -0,0 +1,55 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert +local command = helpers.command +local expect = helpers.expect +local funcs = helpers.funcs +local eq = helpers.eq + +describe('meta-keys #8226 #13042', function() + before_each(function() + clear() + end) + + it('ALT/META, normal-mode', function() + -- Unmapped ALT-chords behave as ESC+c + insert('hello') + feed('0<A-x><M-x>') + expect('llo') + -- Mapped ALT-chord behaves as mapped. + command('nnoremap <M-l> Ameta-l<Esc>') + command('nnoremap <A-j> Aalt-j<Esc>') + feed('<A-j><M-l>') + expect('lloalt-jmeta-l') + end) + + it('ALT/META, visual-mode', function() + -- Unmapped ALT-chords behave as ESC+c + insert('peaches') + feed('viw<A-x>viw<M-x>') + expect('peach') + -- Mapped ALT-chord behaves as mapped. + command('vnoremap <M-l> Ameta-l<Esc>') + command('vnoremap <A-j> Aalt-j<Esc>') + feed('viw<A-j>viw<M-l>') + expect('peachalt-jmeta-l') + end) + + it('ALT/META insert-mode', function() + -- Mapped ALT-chord behaves as mapped. + command('inoremap <M-l> meta-l') + command('inoremap <A-j> alt-j') + feed('i<M-l> xxx <A-j><M-h>a<A-h>') + expect('meta-l xxx alt-j') + eq({ 0, 1, 14, 0, }, funcs.getpos('.')) + -- Unmapped ALT-chord behaves as ESC+c. + command('iunmap <M-l>') + feed('0i<M-l>') + eq({ 0, 1, 2, 0, }, funcs.getpos('.')) + -- Unmapped ALT-chord has same `undo` characteristics as ESC+<key> + command('0,$d') + feed('ahello<M-.>') + expect('hellohello') + feed('u') + expect('hello') + end) +end) diff --git a/test/functional/editor/mode_cmdline_spec.lua b/test/functional/editor/mode_cmdline_spec.lua new file mode 100644 index 0000000000..0f7d821bb5 --- /dev/null +++ b/test/functional/editor/mode_cmdline_spec.lua @@ -0,0 +1,69 @@ +-- Cmdline-mode tests. + +local helpers = require('test.functional.helpers')(after_each) +local clear, insert, funcs, eq, feed = + helpers.clear, helpers.insert, helpers.funcs, helpers.eq, helpers.feed +local meths = helpers.meths + +describe('cmdline CTRL-R', function() + before_each(clear) + + it('pasting non-special register inserts <CR> *between* lines', function() + insert([[ + line1abc + line2somemoretext + ]]) + -- Yank 2 lines linewise, then paste to cmdline. + feed([[<C-\><C-N>gg0yj:<C-R>0]]) + -- <CR> inserted between lines, NOT after the final line. + eq('line1abc\rline2somemoretext', funcs.getcmdline()) + + -- Yank 2 lines charwise, then paste to cmdline. + feed([[<C-\><C-N>gg05lyvj:<C-R>0]]) + -- <CR> inserted between lines, NOT after the final line. + eq('abc\rline2', funcs.getcmdline()) + + -- Yank 1 line linewise, then paste to cmdline. + feed([[<C-\><C-N>ggyy:<C-R>0]]) + -- No <CR> inserted. + eq('line1abc', funcs.getcmdline()) + end) + + it('pasting special register inserts <CR>, <NL>', function() + feed([[:<C-R>="foo\nbar\rbaz"<CR>]]) + eq('foo\nbar\rbaz', funcs.getcmdline()) + end) +end) + +describe('cmdline history', function() + before_each(clear) + + it('correctly clears start of the history', function() + -- Regression test: check absence of the memory leak when clearing start of + -- the history using ex_getln.c/clr_history(). + eq(1, funcs.histadd(':', 'foo')) + eq(1, funcs.histdel(':')) + eq('', funcs.histget(':', -1)) + end) + + it('correctly clears end of the history', function() + -- Regression test: check absence of the memory leak when clearing end of + -- the history using ex_getln.c/clr_history(). + meths.set_option('history', 1) + eq(1, funcs.histadd(':', 'foo')) + eq(1, funcs.histdel(':')) + eq('', funcs.histget(':', -1)) + end) + + it('correctly removes item from history', function() + -- Regression test: check that ex_getln.c/del_history_idx() correctly clears + -- history index after removing history entry. If it does not then deleting + -- history will result in a double free. + eq(1, funcs.histadd(':', 'foo')) + eq(1, funcs.histadd(':', 'bar')) + eq(1, funcs.histadd(':', 'baz')) + eq(1, funcs.histdel(':', -2)) + eq(1, funcs.histdel(':')) + eq('', funcs.histget(':', -1)) + end) +end) diff --git a/test/functional/editor/mode_insert_spec.lua b/test/functional/editor/mode_insert_spec.lua new file mode 100644 index 0000000000..46ab483036 --- /dev/null +++ b/test/functional/editor/mode_insert_spec.lua @@ -0,0 +1,89 @@ +-- Insert-mode tests. + +local helpers = require('test.functional.helpers')(after_each) +local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert +local expect = helpers.expect +local command = helpers.command +local eq = helpers.eq +local eval = helpers.eval +local meths = helpers.meths + +describe('insert-mode', function() + before_each(function() + clear() + end) + + it('CTRL-@', function() + -- Inserts last-inserted text, leaves insert-mode. + insert('hello') + feed('i<C-@>x') + expect('hellhello') + + -- C-Space is the same as C-@. + -- CTRL-SPC inserts last-inserted text, leaves insert-mode. + feed('i<C-Space>x') + expect('hellhellhello') + + -- CTRL-A inserts last inserted text + feed('i<C-A>x') + expect('hellhellhellhelloxo') + end) + + describe('Ctrl-R', function() + it('works', function() + command("let @@ = 'test'") + feed('i<C-r>"') + expect('test') + end) + + it('works with multi-byte text', function() + command("let @@ = 'påskägg'") + feed('i<C-r>"') + expect('påskägg') + end) + end) + + describe('Ctrl-O', function() + it('enters command mode for one command', function() + feed('ihello world<C-o>') + feed(':let ctrlo = "test"<CR>') + feed('iii') + expect('hello worldiii') + eq(1, eval('ctrlo ==# "test"')) + end) + + it('re-enters insert mode at the end of the line when running startinsert', function() + -- #6962 + feed('ihello world<C-o>') + feed(':startinsert<CR>') + feed('iii') + expect('hello worldiii') + end) + + it('re-enters insert mode at the beginning of the line when running startinsert', function() + insert('hello world') + feed('0<C-o>') + feed(':startinsert<CR>') + feed('aaa') + expect('aaahello world') + end) + + it('re-enters insert mode in the middle of the line when running startinsert', function() + insert('hello world') + feed('bi<C-o>') + feed(':startinsert<CR>') + feed('ooo') + expect('hello oooworld') + end) + + it("doesn't cancel Ctrl-O mode when processing event", function() + feed('iHello World<c-o>') + eq({mode='niI', blocking=false}, meths.get_mode()) -- fast event + eq(2, eval('1+1')) -- causes K_EVENT key + eq({mode='niI', blocking=false}, meths.get_mode()) -- still in ctrl-o mode + feed('dd') + eq({mode='i', blocking=false}, meths.get_mode()) -- left ctrl-o mode + expect('') -- executed the command + end) + end) +end) diff --git a/test/functional/editor/mode_visual_spec.lua b/test/functional/editor/mode_visual_spec.lua new file mode 100644 index 0000000000..e9c117a1e5 --- /dev/null +++ b/test/functional/editor/mode_visual_spec.lua @@ -0,0 +1,26 @@ +-- Visual-mode tests. + +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eq = helpers.eq +local eval = helpers.eval +local expect = helpers.expect +local feed = helpers.feed +local meths = helpers.meths + +describe('visual-mode', function() + before_each(clear) + + it("select-mode Ctrl-O doesn't cancel Ctrl-O mode when processing event #15688", function() + feed('iHello World<esc>gh<c-o>') + eq({mode='vs', blocking=false}, meths.get_mode()) -- fast event + eq(2, eval('1+1')) -- causes K_EVENT key + eq({mode='vs', blocking=false}, meths.get_mode()) -- still in ctrl-o mode + feed('^') + eq({mode='s', blocking=false}, meths.get_mode()) -- left ctrl-o mode + feed('h') + eq({mode='i', blocking=false}, meths.get_mode()) -- entered insert mode + expect('h') -- selection is the whole line and is replaced + end) +end) + diff --git a/test/functional/editor/put_spec.lua b/test/functional/editor/put_spec.lua new file mode 100644 index 0000000000..26967ecbba --- /dev/null +++ b/test/functional/editor/put_spec.lua @@ -0,0 +1,939 @@ +local Screen = require('test.functional.ui.screen') +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local insert = helpers.insert +local feed = helpers.feed +local expect = helpers.expect +local eq = helpers.eq +local map = helpers.tbl_map +local filter = helpers.tbl_filter +local feed_command = helpers.feed_command +local curbuf_contents = helpers.curbuf_contents +local funcs = helpers.funcs +local dedent = helpers.dedent +local getreg = funcs.getreg + +local function reset() + feed_command('enew!') + insert([[ + Line of words 1 + Line of words 2]]) + feed_command('goto 1') + feed('itest_string.<esc>u') + funcs.setreg('a', 'test_stringa', 'V') + funcs.setreg('b', 'test_stringb\ntest_stringb\ntest_stringb', 'b') + funcs.setreg('"', 'test_string"', 'v') + feed_command('set virtualedit=') +end + +-- We check the last inserted register ". in each of these tests because it is +-- implemented completely differently in do_put(). +-- It is implemented differently so that control characters and imap'ped +-- characters work in the same manner when pasted as when inserted. +describe('put command', function() + clear() + before_each(reset) + + local function visual_marks_zero() + for _,v in pairs(funcs.getpos("'<")) do + if v ~= 0 then + return false + end + end + for _,v in pairs(funcs.getpos("'>")) do + if v ~= 0 then + return false + end + end + return true + end + + -- {{{ Where test definitions are run + local function run_test_variations(test_variations, extra_setup) + reset() + if extra_setup then extra_setup() end + local init_contents = curbuf_contents() + local init_cursorpos = funcs.getcurpos() + local assert_no_change = function (exception_table, after_undo) + expect(init_contents) + -- When putting the ". register forwards, undo doesn't move + -- the cursor back to where it was before. + -- This is because it uses the command character 'a' to + -- start the insert, and undo after that leaves the cursor + -- one place to the right (unless we were at the end of the + -- line when we pasted). + if not (exception_table.undo_position and after_undo) then + eq(funcs.getcurpos(), init_cursorpos) + end + end + + for _, test in pairs(test_variations) do + it(test.description, function() + if extra_setup then extra_setup() end + local orig_dotstr = funcs.getreg('.') + helpers.ok(visual_marks_zero()) + -- Make sure every test starts from the same conditions + assert_no_change(test.exception_table, false) + local was_cli = test.test_action() + test.test_assertions(test.exception_table, false) + -- Check that undo twice puts us back to the original conditions + -- (i.e. puts the cursor and text back to before) + feed('u') + assert_no_change(test.exception_table, true) + + -- Should not have changed the ". register + -- If we paste the ". register with a count we can't avoid + -- changing this register, hence avoid this check. + if not test.exception_table.dot_reg_changed then + eq(funcs.getreg('.'), orig_dotstr) + end + + -- Doing something, undoing it, and then redoing it should + -- leave us in the same state as just doing it once. + -- For :ex actions we want '@:', for normal actions we want '.' + + -- The '.' redo doesn't work for visual put so just exit if + -- it was tested. + -- We check that visual put was used by checking if the '< and + -- '> marks were changed. + if not visual_marks_zero() then + return + end + + if test.exception_table.undo_position then + funcs.setpos('.', init_cursorpos) + end + if was_cli then + feed('@:') + else + feed('.') + end + + test.test_assertions(test.exception_table, true) + end) + end + end -- run_test_variations() + -- }}} + + local function create_test_defs(test_defs, command_base, command_creator, -- {{{ + expect_base, expect_creator) + local rettab = {} + local exceptions + for _, v in pairs(test_defs) do + if v[4] then + exceptions = v[4] + else + exceptions = {} + end + table.insert(rettab, + { + test_action = command_creator(command_base, v[1]), + test_assertions = expect_creator(expect_base, v[2]), + description = v[3], + exception_table = exceptions, + }) + end + return rettab + end -- create_test_defs() }}} + + local function find_cursor_position(expect_string) -- {{{ + -- There must only be one occurance of the character 'x' in + -- expect_string. + -- This function removes that occurance, and returns the position that + -- it was in. + -- This returns the cursor position that would leave the 'x' in that + -- place if we feed 'ix<esc>' and the string existed before it. + for linenum, line in pairs(funcs.split(expect_string, '\n', 1)) do + local column = line:find('x') + if column then + return {linenum, column}, expect_string:gsub('x', '') + end + end + end -- find_cursor_position() }}} + + -- Action function creators {{{ + local function create_p_action(test_map, substitution) + local temp_val = test_map:gsub('p', substitution) + return function() + feed(temp_val) + return false + end + end + + local function create_put_action(command_base, substitution) + local temp_val = command_base:gsub('put', substitution) + return function() + feed_command(temp_val) + return true + end + end + -- }}} + + -- Expect function creator {{{ + local function expect_creator(conversion_function, expect_base, conversion_table) + local temp_expect_string = conversion_function(expect_base, conversion_table) + local cursor_position, expect_string = find_cursor_position(temp_expect_string) + return function(exception_table, after_redo) + expect(expect_string) + + -- Have to use getcurpos() instead of curwinmeths.get_cursor() in + -- order to account for virtualedit. + -- We always want the curswant element in getcurpos(), which is + -- sometimes different to the column element in + -- curwinmeths.get_cursor(). + -- NOTE: The ".gp command leaves the cursor after the pasted text + -- when running, but does not when the command is redone with the + -- '.' command. + if not (exception_table.redo_position and after_redo) then + local actual_position = funcs.getcurpos() + eq(cursor_position, {actual_position[2], actual_position[5]}) + end + end + end -- expect_creator() }}} + + -- Test definitions {{{ + local function copy_def(def) + local rettab = { '', {}, '', nil } + rettab[1] = def[1] + for k,v in pairs(def[2]) do + rettab[2][k] = v + end + rettab[3] = def[3] + if def[4] then + rettab[4] = {} + for k,v in pairs(def[4]) do + rettab[4][k] = v + end + end + return rettab + end + + local normal_command_defs = { + { + 'p', + {cursor_after = false, put_backwards = false, dot_register = false}, + 'pastes after cursor with p', + }, + { + 'gp', + {cursor_after = true, put_backwards = false, dot_register = false}, + 'leaves cursor after text with gp', + }, + { + '".p', + {cursor_after = false, put_backwards = false, dot_register = true}, + 'works with the ". register', + }, + { + '".gp', + {cursor_after = true, put_backwards = false, dot_register = true}, + 'gp works with the ". register', + {redo_position = true}, + }, + { + 'P', + {cursor_after = false, put_backwards = true, dot_register = false}, + 'pastes before cursor with P', + }, + { + 'gP', + {cursor_after = true, put_backwards = true, dot_register = false}, + 'gP pastes before cursor and leaves cursor after text', + }, + { + '".P', + {cursor_after = false, put_backwards = true, dot_register = true}, + 'P works with ". register', + }, + { + '".gP', + {cursor_after = true, put_backwards = true, dot_register = true}, + 'gP works with ". register', + {redo_position = true}, + }, + } + + -- Add a definition applying a count for each definition above. + -- Could do this for each transformation (p -> P, p -> gp etc), but I think + -- it's neater this way (balance between being explicit and too verbose). + for i = 1,#normal_command_defs do + local cur = normal_command_defs[i] + + -- Make modified copy of current definition that includes a count. + local newdef = copy_def(cur) + newdef[2].count = 2 + cur[2].count = 1 + newdef[1] = '2' .. newdef[1] + newdef[3] = 'double ' .. newdef[3] + + if cur[2].dot_register then + if not cur[4] then + newdef[4] = {} + end + newdef[4].dot_reg_changed = true + end + + normal_command_defs[#normal_command_defs + 1] = newdef + end + + local ex_command_defs = { + { + 'put', + {put_backwards = false, dot_register = false}, + 'pastes linewise forwards with :put', + }, + { + 'put!', + {put_backwards = true, dot_register = false}, + 'pastes linewise backwards with :put!', + }, + { + 'put .', + {put_backwards = false, dot_register = true}, + 'pastes linewise with the dot register', + }, + { + 'put! .', + {put_backwards = true, dot_register = true}, + 'pastes linewise backwards with the dot register', + }, + } + + local function non_dotdefs(def_table) + return filter(function(d) return not d[2].dot_register end, def_table) + end + + -- }}} + + -- Conversion functions {{{ + local function convert_charwise(expect_base, conversion_table, + virtualedit_end, visual_put) + expect_base = dedent(expect_base) + -- There is no difference between 'P' and 'p' when VIsual_active + if not visual_put then + if conversion_table.put_backwards then + -- Special case for virtualedit at the end of a line. + local replace_string + if not virtualedit_end then + replace_string = 'test_stringx"%1' + else + replace_string = 'test_stringx"' + end + expect_base = expect_base:gsub('(.)test_stringx"', replace_string) + end + end + if conversion_table.count > 1 then + local rep_string = 'test_string"' + local extra_puts = rep_string:rep(conversion_table.count - 1) + expect_base = expect_base:gsub('test_stringx"', extra_puts .. 'test_stringx"') + end + if conversion_table.cursor_after then + expect_base = expect_base:gsub('test_stringx"', 'test_string"x') + end + if conversion_table.dot_register then + expect_base = expect_base:gsub('(test_stringx?)"', '%1.') + end + return expect_base + end -- convert_charwise() + + local function make_back(string) + local prev_line + local rettab = {} + local string_found = false + for _, line in pairs(funcs.split(string, '\n', 1)) do + if line:find('test_string') then + string_found = true + table.insert(rettab, line) + else + if string_found then + if prev_line then + table.insert(rettab, prev_line) + prev_line = nil + end + table.insert(rettab, line) + else + table.insert(rettab, prev_line) + prev_line = line + end + end + end + -- In case there are no lines after the text that was put. + if prev_line and string_found then + table.insert(rettab, prev_line) + end + return table.concat(rettab, '\n') + end -- make_back() + + local function convert_linewise(expect_base, conversion_table, _, use_a, indent) + expect_base = dedent(expect_base) + if conversion_table.put_backwards then + expect_base = make_back(expect_base) + end + local p_str = 'test_string"' + if use_a then + p_str = 'test_stringa' + end + + if conversion_table.dot_register then + expect_base = expect_base:gsub('x' .. p_str, 'xtest_string.') + p_str = 'test_string.' + end + + if conversion_table.cursor_after then + expect_base = expect_base:gsub('x' .. p_str .. '\n', p_str .. '\nx') + end + + -- The 'indent' argument is only used here because a single put with an + -- indent doesn't require special handling. It doesn't require special + -- handling because the cursor is never put before the indent, hence + -- the modification of 'test_stringx"' gives the same overall answer as + -- modifying ' test_stringx"'. + + -- Only happens when using normal mode command actions. + if conversion_table.count and conversion_table.count > 1 then + if not indent then + indent = '' + end + local rep_string = indent .. p_str .. '\n' + local extra_puts = rep_string:rep(conversion_table.count - 1) + local orig_string, new_string + if conversion_table.cursor_after then + orig_string = indent .. p_str .. '\nx' + new_string = extra_puts .. orig_string + else + orig_string = indent .. 'x' .. p_str .. '\n' + new_string = orig_string .. extra_puts + end + expect_base = expect_base:gsub(orig_string, new_string) + end + return expect_base + end + + local function put_x_last(orig_line, p_str) + local prev_end, cur_end, cur_start = 0, 0, 0 + while cur_start do + prev_end = cur_end + cur_start, cur_end = orig_line:find(p_str, prev_end) + end + -- Assume (because that is the only way I call it) that p_str matches + -- the pattern 'test_string.' + return orig_line:sub(1, prev_end - 1) .. 'x' .. orig_line:sub(prev_end) + end + + local function convert_blockwise(expect_base, conversion_table, visual, + use_b, trailing_whitespace) + expect_base = dedent(expect_base) + local p_str = 'test_string"' + if use_b then + p_str = 'test_stringb' + end + + if conversion_table.dot_register then + expect_base = expect_base:gsub('(x?)' .. p_str, '%1test_string.') + -- Looks strange, but the dot is a special character in the pattern + -- and a literal character in the replacement. + expect_base = expect_base:gsub('test_stringx.', 'test_stringx.') + p_str = 'test_string.' + end + + -- No difference between 'p' and 'P' in visual mode. + if not visual then + if conversion_table.put_backwards then + -- One for the line where the cursor is left, one for all other + -- lines. + expect_base = expect_base:gsub('([^x])' .. p_str, p_str .. '%1') + expect_base = expect_base:gsub('([^x])x' .. p_str, 'x' .. p_str .. '%1') + if not trailing_whitespace then + expect_base = expect_base:gsub(' \n', '\n') + expect_base = expect_base:gsub(' $', '') + end + end + end + + if conversion_table.count and conversion_table.count > 1 then + local p_pattern = p_str:gsub('%.', '%%.') + expect_base = expect_base:gsub(p_pattern, + p_str:rep(conversion_table.count)) + expect_base = expect_base:gsub('test_stringx([b".])', + p_str:rep(conversion_table.count - 1) + .. '%0') + end + + if conversion_table.cursor_after then + if not visual then + local prev_line + local rettab = {} + local prev_in_block = false + for _, line in pairs(funcs.split(expect_base, '\n', 1)) do + if line:find('test_string') then + if prev_line then + prev_line = prev_line:gsub('x', '') + table.insert(rettab, prev_line) + end + prev_line = line + prev_in_block = true + else + if prev_in_block then + prev_line = put_x_last(prev_line, p_str) + table.insert(rettab, prev_line) + prev_in_block = false + end + table.insert(rettab, line) + end + end + if prev_line and prev_in_block then + table.insert(rettab, put_x_last(prev_line, p_str)) + end + + expect_base = table.concat(rettab, '\n') + else + expect_base = expect_base:gsub('x(.)', '%1x') + end + end + + return expect_base + end + -- }}} + + -- Convenience functions {{{ + local function run_normal_mode_tests(test_string, base_map, extra_setup, + virtualedit_end, selection_string) + local function convert_closure(e, c) + return convert_charwise(e, c, virtualedit_end, selection_string) + end + local function expect_normal_creator(expect_base, conversion_table) + local test_expect = expect_creator(convert_closure, expect_base, conversion_table) + return function(exception_table, after_redo) + test_expect(exception_table, after_redo) + if selection_string then + eq(getreg('"'), selection_string) + else + eq(getreg('"'), 'test_string"') + end + end + end + run_test_variations( + create_test_defs( + normal_command_defs, + base_map, + create_p_action, + test_string, + expect_normal_creator + ), + extra_setup + ) + end -- run_normal_mode_tests() + + local function convert_linewiseer(expect_base, conversion_table) + return expect_creator(convert_linewise, expect_base, conversion_table) + end + + local function run_linewise_tests(expect_base, base_command, extra_setup) + local linewise_test_defs = create_test_defs( + ex_command_defs, base_command, + create_put_action, expect_base, convert_linewiseer) + run_test_variations(linewise_test_defs, extra_setup) + end -- run_linewise_tests() + -- }}} + + -- Actual tests + describe('default pasting', function() + local expect_string = [[ + Ltest_stringx"ine of words 1 + Line of words 2]] + run_normal_mode_tests(expect_string, 'p') + + run_linewise_tests([[ + Line of words 1 + xtest_string" + Line of words 2]], + 'put' + ) + end) + + describe('linewise register', function() + -- put with 'p' + local local_ex_command_defs = non_dotdefs(normal_command_defs) + local base_expect_string = [[ + Line of words 1 + xtest_stringa + Line of words 2]] + local function local_convert_linewise(expect_base, conversion_table) + return convert_linewise(expect_base, conversion_table, nil, true) + end + local function expect_lineput(expect_base, conversion_table) + return expect_creator(local_convert_linewise, expect_base, conversion_table) + end + run_test_variations( + create_test_defs( + local_ex_command_defs, + '"ap', + create_p_action, + base_expect_string, + expect_lineput + ) + ) + + -- put with :put + local linewise_put_defs = non_dotdefs(ex_command_defs) + base_expect_string = [[ + Line of words 1 + xtest_stringa + Line of words 2]] + run_test_variations( + create_test_defs( + linewise_put_defs, + 'put a', create_put_action, + base_expect_string, convert_linewiseer + ) + ) + + end) + + describe('blockwise register', function() + local blockwise_put_defs = non_dotdefs(normal_command_defs) + local test_base = [[ + Lxtest_stringbine of words 1 + Ltest_stringbine of words 2 + test_stringb]] + + local function expect_block_creator(expect_base, conversion_table) + return expect_creator(function(e,c) return convert_blockwise(e,c,nil,true) end, + expect_base, conversion_table) + end + + run_test_variations( + create_test_defs( + blockwise_put_defs, + '"bp', + create_p_action, + test_base, + expect_block_creator + ) + ) + end) + + it('adds correct indentation when put with [p and ]p', function() + feed('G>>"a]pix<esc>') + -- luacheck: ignore + expect([[ + Line of words 1 + Line of words 2 + xtest_stringa]]) + feed('uu"a[pix<esc>') + -- luacheck: ignore + expect([[ + Line of words 1 + xtest_stringa + Line of words 2]]) + end) + + describe('linewise paste with autoindent', function() + -- luacheck: ignore + run_linewise_tests([[ + Line of words 1 + Line of words 2 + xtest_string"]], + 'put' + , + function() + funcs.setline('$', ' Line of words 2') + -- Set curswant to '8' to be at the end of the tab character + -- This is where the cursor is put back after the 'u' command. + funcs.setpos('.', {0, 2, 1, 0, 8}) + feed_command('set autoindent') + end + ) + end) + + describe('put inside tabs with virtualedit', function() + local test_string = [[ + Line of words 1 + test_stringx" Line of words 2]] + run_normal_mode_tests(test_string, 'p', function() + funcs.setline('$', ' Line of words 2') + feed_command('set virtualedit=all') + funcs.setpos('.', {0, 2, 1, 2, 3}) + end) + end) + + describe('put after the line with virtualedit', function() + -- luacheck: ignore 621 + local test_string = [[ + Line of words 1 test_stringx" + Line of words 2]] + run_normal_mode_tests(test_string, 'p', function() + funcs.setline('$', ' Line of words 2') + feed_command('set virtualedit=all') + funcs.setpos('.', {0, 1, 16, 1, 17}) + end, true) + end) + + describe('Visual put', function() + describe('basic put', function() + local test_string = [[ + test_stringx" words 1 + Line of words 2]] + run_normal_mode_tests(test_string, 'v2ep', nil, nil, 'Line of') + end) + describe('over trailing newline', function() + local test_string = 'Line of test_stringx"Line of words 2' + run_normal_mode_tests(test_string, 'v$p', function() + funcs.setpos('.', {0, 1, 9, 0, 9}) + end, + nil, + 'words 1\n') + end) + describe('linewise mode', function() + local test_string = [[ + xtest_string" + Line of words 2]] + local function expect_vis_linewise(expect_base, conversion_table) + return expect_creator(function(e, c) + return convert_linewise(e, c, nil, nil) + end, + expect_base, conversion_table) + end + run_test_variations( + create_test_defs( + normal_command_defs, + 'Vp', + create_p_action, + test_string, + expect_vis_linewise + ), + function() funcs.setpos('.', {0, 1, 1, 0, 1}) end + ) + + describe('with whitespace at bol', function() + local function expect_vis_lineindented(expect_base, conversion_table) + local test_expect = expect_creator(function(e, c) + return convert_linewise(e, c, nil, nil, ' ') + end, + expect_base, conversion_table) + return function(exception_table, after_redo) + test_expect(exception_table, after_redo) + eq(getreg('"'), 'Line of words 1\n') + end + end + local base_expect_string = [[ + xtest_string" + Line of words 2]] + run_test_variations( + create_test_defs( + normal_command_defs, + 'Vp', + create_p_action, + base_expect_string, + expect_vis_lineindented + ), + function() + feed('i test_string.<esc>u') + funcs.setreg('"', ' test_string"', 'v') + end + ) + end) + + end) + + describe('blockwise visual mode', function() + local test_base = [[ + test_stringx"e of words 1 + test_string"e of words 2]] + + local function expect_block_creator(expect_base, conversion_table) + local test_expect = expect_creator(function(e, c) + return convert_blockwise(e, c, true) + end, expect_base, conversion_table) + return function(e,c) + test_expect(e,c) + eq(getreg('"'), 'Lin\nLin') + end + end + + local select_down_test_defs = create_test_defs( + normal_command_defs, + '<C-v>jllp', + create_p_action, + test_base, + expect_block_creator + ) + run_test_variations(select_down_test_defs) + + + -- Undo and redo of a visual block put leave the cursor in the top + -- left of the visual block area no matter where the cursor was + -- when it started. + local undo_redo_no = map(function(table) + local rettab = copy_def(table) + if not rettab[4] then + rettab[4] = {} + end + rettab[4].undo_position = true + rettab[4].redo_position = true + return rettab + end, + normal_command_defs) + + -- Selection direction doesn't matter + run_test_variations( + create_test_defs( + undo_redo_no, + '<C-v>kllp', + create_p_action, + test_base, + expect_block_creator + ), + function() funcs.setpos('.', {0, 2, 1, 0, 1}) end + ) + + describe('blockwise cursor after undo', function() + -- A bit of a hack of the reset above. + -- In the tests that selection direction doesn't matter, we + -- don't check the undo/redo position because it doesn't fit + -- the same pattern as everything else. + -- Here we fix this by directly checking the undo/redo position + -- in the test_assertions of our test definitions. + local function assertion_creator(_,_) + return function(_,_) + feed('u') + -- Have to use feed('u') here to set curswant, because + -- ex_undo() doesn't do that. + eq(funcs.getcurpos(), {0, 1, 1, 0, 1}) + feed('<C-r>') + eq(funcs.getcurpos(), {0, 1, 1, 0, 1}) + end + end + + run_test_variations( + create_test_defs( + undo_redo_no, + '<C-v>kllp', + create_p_action, + test_base, + assertion_creator + ), + function() funcs.setpos('.', {0, 2, 1, 0, 1}) end + ) + end) + end) + + + describe("with 'virtualedit'", function() + describe('splitting a tab character', function() + local base_expect_string = [[ + Line of words 1 + test_stringx" Line of words 2]] + run_normal_mode_tests( + base_expect_string, + 'vp', + function() + funcs.setline('$', ' Line of words 2') + feed_command('set virtualedit=all') + funcs.setpos('.', {0, 2, 1, 2, 3}) + end, + nil, + ' ' + ) + end) + describe('after end of line', function() + local base_expect_string = [[ + Line of words 1 test_stringx" + Line of words 2]] + run_normal_mode_tests( + base_expect_string, + 'vp', + function() + feed_command('set virtualedit=all') + funcs.setpos('.', {0, 1, 16, 2, 18}) + end, + true, + ' ' + ) + end) + end) + end) + + describe('. register special tests', function() + -- luacheck: ignore 621 + before_each(reset) + it('applies control character actions', function() + feed('i<C-t><esc>u') + expect([[ + Line of words 1 + Line of words 2]]) + feed('".p') + expect([[ + Line of words 1 + Line of words 2]]) + feed('u1go<C-v>j".p') + eq([[ + ine of words 1 + ine of words 2]], curbuf_contents()) + end) + + local function bell_test(actions, should_ring) + local screen = Screen.new() + screen:attach() + if should_ring then + -- check bell is not set by nvim before the action + screen:sleep(50) + end + helpers.ok(not screen.bell and not screen.visualbell) + actions() + screen:expect{condition=function() + if should_ring then + if not screen.bell and not screen.visualbell then + error('Bell was not rung after action') + end + else + if screen.bell or screen.visualbell then + error('Bell was rung after action') + end + end + end, unchanged=(not should_ring)} + screen:detach() + end + + it('should not ring the bell with gp at end of line', function() + bell_test(function() feed('$".gp') end) + + -- Even if the last character is a multibyte character. + reset() + funcs.setline(1, 'helloม') + bell_test(function() feed('$".gp') end) + end) + + it('should not ring the bell with gp and end of file', function() + funcs.setpos('.', {0, 2, 1, 0}) + bell_test(function() feed('$vl".gp') end) + end) + + it('should ring the bell when deleting if not appropriate', function() + feed_command('goto 2') + feed('i<bs><esc>') + expect([[ + ine of words 1 + Line of words 2]]) + bell_test(function() feed('".P') end, true) + end) + + it('should restore cursor position after undo of ".p', function() + local origpos = funcs.getcurpos() + feed('".pu') + eq(origpos, funcs.getcurpos()) + end) + + it("should be unaffected by 'autoindent' with V\".2p", function() + feed_command('set autoindent') + feed('i test_string.<esc>u') + feed('V".2p') + expect([[ + test_string. + test_string. + Line of words 2]]) + end) + end) +end) + diff --git a/test/functional/editor/search_spec.lua b/test/functional/editor/search_spec.lua new file mode 100644 index 0000000000..d5df131725 --- /dev/null +++ b/test/functional/editor/search_spec.lua @@ -0,0 +1,17 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local pcall_err = helpers.pcall_err + +describe('search (/)', function() + before_each(clear) + + it('fails with huge column (%c) value #9930', function() + eq([[Vim:E951: \% value too large]], + pcall_err(command, "/\\v%18446744071562067968c")) + eq([[Vim:E951: \% value too large]], + pcall_err(command, "/\\v%2147483648c")) + end) +end) + diff --git a/test/functional/editor/tabpage_spec.lua b/test/functional/editor/tabpage_spec.lua new file mode 100644 index 0000000000..d1d6854b07 --- /dev/null +++ b/test/functional/editor/tabpage_spec.lua @@ -0,0 +1,38 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local feed = helpers.feed +local eval = helpers.eval + +describe('tabpage', function() + before_each(clear) + + it('advances to the next page via <C-W>gt', function() + -- add some tabpages + command('tabnew') + command('tabnew') + command('tabnew') + + eq(4, eval('tabpagenr()')) + + feed('<C-W>gt') + + eq(1, eval('tabpagenr()')) + end) + + it('retreats to the previous page via <C-W>gT', function() + -- add some tabpages + command('tabnew') + command('tabnew') + command('tabnew') + + eq(4, eval('tabpagenr()')) + + feed('<C-W>gT') + + eq(3, eval('tabpagenr()')) + end) +end) + diff --git a/test/functional/editor/undo_spec.lua b/test/functional/editor/undo_spec.lua new file mode 100644 index 0000000000..a023ca3d90 --- /dev/null +++ b/test/functional/editor/undo_spec.lua @@ -0,0 +1,61 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local expect = helpers.expect +local feed = helpers.feed +local insert = helpers.insert + +describe('u CTRL-R g- g+', function() + before_each(clear) + + local function create_history(num_steps) + if num_steps == 0 then return end + insert('1') + if num_steps == 1 then return end + feed('o2<esc>') + feed('o3<esc>') + feed('u') + if num_steps == 2 then return end + feed('o4<esc>') + if num_steps == 3 then return end + feed('u') + end + + local function undo_and_redo(hist_pos, undo, redo, expect_str) + command('enew!') + create_history(hist_pos) + local cur_contents = helpers.curbuf_contents() + feed(undo) + expect(expect_str) + feed(redo) + expect(cur_contents) + end + + -- TODO Look for message saying 'Already at oldest change' + it('does nothing when no changes have happened', function() + undo_and_redo(0, 'u', '<C-r>', '') + undo_and_redo(0, 'g-', 'g+', '') + end) + it('undoes a change when at a leaf', function() + undo_and_redo(1, 'u', '<C-r>', '') + undo_and_redo(1, 'g-', 'g+', '') + end) + it('undoes a change when in a non-leaf', function() + undo_and_redo(2, 'u', '<C-r>', '1') + undo_and_redo(2, 'g-', 'g+', '1') + end) + it('undoes properly around a branch point', function() + undo_and_redo(3, 'u', '<C-r>', [[ + 1 + 2]]) + undo_and_redo(3, 'g-', 'g+', [[ + 1 + 2 + 3]]) + end) + it('can find the previous sequence after undoing to a branch', function() + undo_and_redo(4, 'u', '<C-r>', '1') + undo_and_redo(4, 'g-', 'g+', '1') + end) +end) |