aboutsummaryrefslogtreecommitdiff
path: root/test/functional/editor
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2021-09-17 09:16:40 -0700
committerGitHub <noreply@github.com>2021-09-17 09:16:40 -0700
commitd8de4eb685e35646c7d541e9a75bdc296127b7e2 (patch)
tree4bb05ec713856715ac9ba57e5d116eed344511b9 /test/functional/editor
parentd56002f7b722facd97b0958e141c8ed2d01495f7 (diff)
downloadrneovim-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.lua38
-rw-r--r--test/functional/editor/completion_spec.lua1196
-rw-r--r--test/functional/editor/count_spec.lua39
-rw-r--r--test/functional/editor/fold_spec.lua362
-rw-r--r--test/functional/editor/jump_spec.lua139
-rw-r--r--test/functional/editor/lang_spec.lua63
-rw-r--r--test/functional/editor/langmap_spec.lua280
-rw-r--r--test/functional/editor/macro_spec.lua30
-rw-r--r--test/functional/editor/meta_key_spec.lua55
-rw-r--r--test/functional/editor/mode_cmdline_spec.lua69
-rw-r--r--test/functional/editor/mode_insert_spec.lua89
-rw-r--r--test/functional/editor/mode_visual_spec.lua26
-rw-r--r--test/functional/editor/put_spec.lua939
-rw-r--r--test/functional/editor/search_spec.lua17
-rw-r--r--test/functional/editor/tabpage_spec.lua38
-rw-r--r--test/functional/editor/undo_spec.lua61
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)