aboutsummaryrefslogtreecommitdiff
path: root/test/functional/api
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/api')
-rw-r--r--test/functional/api/buffer_spec.lua299
-rw-r--r--test/functional/api/buffer_updates_spec.lua838
-rw-r--r--test/functional/api/command_spec.lua80
-rw-r--r--test/functional/api/highlight_spec.lua112
-rw-r--r--test/functional/api/keymap_spec.lua310
-rw-r--r--test/functional/api/menu_spec.lua8
-rw-r--r--test/functional/api/proc_spec.lua81
-rw-r--r--test/functional/api/rpc_fixture.lua38
-rw-r--r--test/functional/api/server_notifications_spec.lua30
-rw-r--r--test/functional/api/server_requests_spec.lua255
-rw-r--r--test/functional/api/tabpage_spec.lua61
-rw-r--r--test/functional/api/ui_spec.lua37
-rw-r--r--test/functional/api/version_spec.lua223
-rw-r--r--test/functional/api/vim_spec.lua1171
-rw-r--r--test/functional/api/window_spec.lua157
15 files changed, 3454 insertions, 246 deletions
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua
index cf8a83ad81..d9412f0f13 100644
--- a/test/functional/api/buffer_spec.lua
+++ b/test/functional/api/buffer_spec.lua
@@ -1,107 +1,148 @@
--- Sanity checks for buffer_* API calls via msgpack-rpc
local helpers = require('test.functional.helpers')(after_each)
local clear, nvim, buffer = helpers.clear, helpers.nvim, helpers.buffer
local curbuf, curwin, eq = helpers.curbuf, helpers.curwin, helpers.eq
local curbufmeths, ok = helpers.curbufmeths, helpers.ok
local funcs = helpers.funcs
-
-describe('buffer_* functions', function()
+local request = helpers.request
+local exc_exec = helpers.exc_exec
+local feed_command = helpers.feed_command
+local insert = helpers.insert
+local NIL = helpers.NIL
+local meth_pcall = helpers.meth_pcall
+local command = helpers.command
+local bufmeths = helpers.bufmeths
+
+describe('api/buf', function()
before_each(clear)
+ -- access deprecated functions
+ local function curbuf_depr(method, ...)
+ return request('buffer_'..method, 0, ...)
+ end
+
+
describe('line_count, insert and del_line', function()
it('works', function()
- eq(1, curbuf('line_count'))
- curbuf('insert', -1, {'line'})
- eq(2, curbuf('line_count'))
- curbuf('insert', -1, {'line'})
- eq(3, curbuf('line_count'))
- curbuf('del_line', -1)
- eq(2, curbuf('line_count'))
- curbuf('del_line', -1)
- curbuf('del_line', -1)
+ eq(1, curbuf_depr('line_count'))
+ curbuf_depr('insert', -1, {'line'})
+ eq(2, curbuf_depr('line_count'))
+ curbuf_depr('insert', -1, {'line'})
+ eq(3, curbuf_depr('line_count'))
+ curbuf_depr('del_line', -1)
+ eq(2, curbuf_depr('line_count'))
+ curbuf_depr('del_line', -1)
+ curbuf_depr('del_line', -1)
-- There's always at least one line
- eq(1, curbuf('line_count'))
+ eq(1, curbuf_depr('line_count'))
end)
- end)
+ it('line_count has defined behaviour for unloaded buffers', function()
+ -- we'll need to know our bufnr for when it gets unloaded
+ local bufnr = curbuf('get_number')
+ -- replace the buffer contents with these three lines
+ request('nvim_buf_set_lines', bufnr, 0, -1, 1, {"line1", "line2", "line3", "line4"})
+ -- check the line count is correct
+ eq(4, request('nvim_buf_line_count', bufnr))
+ -- force unload the buffer (this will discard changes)
+ command('new')
+ command('bunload! '..bufnr)
+ -- line count for an unloaded buffer should always be 0
+ eq(0, request('nvim_buf_line_count', bufnr))
+ end)
+
+ it('get_lines has defined behaviour for unloaded buffers', function()
+ -- we'll need to know our bufnr for when it gets unloaded
+ local bufnr = curbuf('get_number')
+ -- replace the buffer contents with these three lines
+ buffer('set_lines', bufnr, 0, -1, 1, {"line1", "line2", "line3", "line4"})
+ -- confirm that getting lines works
+ eq({"line2", "line3"}, buffer('get_lines', bufnr, 1, 3, 1))
+ -- force unload the buffer (this will discard changes)
+ command('new')
+ command('bunload! '..bufnr)
+ -- attempting to get lines now always gives empty list
+ eq({}, buffer('get_lines', bufnr, 1, 3, 1))
+ -- it's impossible to get out-of-bounds errors for an unloaded buffer
+ eq({}, buffer('get_lines', bufnr, 8888, 9999, 1))
+ end)
+ end)
describe('{get,set,del}_line', function()
it('works', function()
- eq('', curbuf('get_line', 0))
- curbuf('set_line', 0, 'line1')
- eq('line1', curbuf('get_line', 0))
- curbuf('set_line', 0, 'line2')
- eq('line2', curbuf('get_line', 0))
- curbuf('del_line', 0)
- eq('', curbuf('get_line', 0))
+ eq('', curbuf_depr('get_line', 0))
+ curbuf_depr('set_line', 0, 'line1')
+ eq('line1', curbuf_depr('get_line', 0))
+ curbuf_depr('set_line', 0, 'line2')
+ eq('line2', curbuf_depr('get_line', 0))
+ curbuf_depr('del_line', 0)
+ eq('', curbuf_depr('get_line', 0))
end)
it('get_line: out-of-bounds is an error', function()
- curbuf('set_line', 0, 'line1.a')
- eq(1, curbuf('line_count')) -- sanity
- eq(false, pcall(curbuf, 'get_line', 1))
- eq(false, pcall(curbuf, 'get_line', -2))
+ curbuf_depr('set_line', 0, 'line1.a')
+ eq(1, curbuf_depr('line_count')) -- sanity
+ eq(false, pcall(curbuf_depr, 'get_line', 1))
+ eq(false, pcall(curbuf_depr, 'get_line', -2))
end)
it('set_line, del_line: out-of-bounds is an error', function()
- curbuf('set_line', 0, 'line1.a')
- eq(false, pcall(curbuf, 'set_line', 1, 'line1.b'))
- eq(false, pcall(curbuf, 'set_line', -2, 'line1.b'))
- eq(false, pcall(curbuf, 'del_line', 2))
- eq(false, pcall(curbuf, 'del_line', -3))
+ curbuf_depr('set_line', 0, 'line1.a')
+ eq(false, pcall(curbuf_depr, 'set_line', 1, 'line1.b'))
+ eq(false, pcall(curbuf_depr, 'set_line', -2, 'line1.b'))
+ eq(false, pcall(curbuf_depr, 'del_line', 2))
+ eq(false, pcall(curbuf_depr, 'del_line', -3))
end)
it('can handle NULs', function()
- curbuf('set_line', 0, 'ab\0cd')
- eq('ab\0cd', curbuf('get_line', 0))
+ curbuf_depr('set_line', 0, 'ab\0cd')
+ eq('ab\0cd', curbuf_depr('get_line', 0))
end)
end)
-
describe('{get,set}_line_slice', function()
it('get_line_slice: out-of-bounds returns empty array', function()
- curbuf('set_line_slice', 0, 0, true, true, {'a', 'b', 'c'})
- eq({'a', 'b', 'c'}, curbuf('get_line_slice', 0, 2, true, true)) --sanity
-
- eq({}, curbuf('get_line_slice', 2, 3, false, true))
- eq({}, curbuf('get_line_slice', 3, 9, true, true))
- eq({}, curbuf('get_line_slice', 3, -1, true, true))
- eq({}, curbuf('get_line_slice', -3, -4, false, true))
- eq({}, curbuf('get_line_slice', -4, -5, true, true))
+ curbuf_depr('set_line_slice', 0, 0, true, true, {'a', 'b', 'c'})
+ eq({'a', 'b', 'c'}, curbuf_depr('get_line_slice', 0, 2, true, true)) --sanity
+
+ eq({}, curbuf_depr('get_line_slice', 2, 3, false, true))
+ eq({}, curbuf_depr('get_line_slice', 3, 9, true, true))
+ eq({}, curbuf_depr('get_line_slice', 3, -1, true, true))
+ eq({}, curbuf_depr('get_line_slice', -3, -4, false, true))
+ eq({}, curbuf_depr('get_line_slice', -4, -5, true, true))
end)
it('set_line_slice: out-of-bounds extends past end', function()
- curbuf('set_line_slice', 0, 0, true, true, {'a', 'b', 'c'})
- eq({'a', 'b', 'c'}, curbuf('get_line_slice', 0, 2, true, true)) --sanity
-
- eq({'c'}, curbuf('get_line_slice', -1, 4, true, true))
- eq({'a', 'b', 'c'}, curbuf('get_line_slice', 0, 5, true, true))
- curbuf('set_line_slice', 4, 5, true, true, {'d'})
- eq({'a', 'b', 'c', 'd'}, curbuf('get_line_slice', 0, 5, true, true))
- curbuf('set_line_slice', -4, -5, true, true, {'e'})
- eq({'e', 'a', 'b', 'c', 'd'}, curbuf('get_line_slice', 0, 5, true, true))
+ curbuf_depr('set_line_slice', 0, 0, true, true, {'a', 'b', 'c'})
+ eq({'a', 'b', 'c'}, curbuf_depr('get_line_slice', 0, 2, true, true)) --sanity
+
+ eq({'c'}, curbuf_depr('get_line_slice', -1, 4, true, true))
+ eq({'a', 'b', 'c'}, curbuf_depr('get_line_slice', 0, 5, true, true))
+ curbuf_depr('set_line_slice', 4, 5, true, true, {'d'})
+ eq({'a', 'b', 'c', 'd'}, curbuf_depr('get_line_slice', 0, 5, true, true))
+ curbuf_depr('set_line_slice', -4, -5, true, true, {'e'})
+ eq({'e', 'a', 'b', 'c', 'd'}, curbuf_depr('get_line_slice', 0, 5, true, true))
end)
it('works', function()
- eq({''}, curbuf('get_line_slice', 0, -1, true, true))
+ eq({''}, curbuf_depr('get_line_slice', 0, -1, true, true))
-- Replace buffer
- curbuf('set_line_slice', 0, -1, true, true, {'a', 'b', 'c'})
- eq({'a', 'b', 'c'}, curbuf('get_line_slice', 0, -1, true, true))
- eq({'b', 'c'}, curbuf('get_line_slice', 1, -1, true, true))
- eq({'b'}, curbuf('get_line_slice', 1, 2, true, false))
- eq({}, curbuf('get_line_slice', 1, 1, true, false))
- eq({'a', 'b'}, curbuf('get_line_slice', 0, -1, true, false))
- eq({'b'}, curbuf('get_line_slice', 1, -1, true, false))
- eq({'b', 'c'}, curbuf('get_line_slice', -2, -1, true, true))
- curbuf('set_line_slice', 1, 2, true, false, {'a', 'b', 'c'})
- eq({'a', 'a', 'b', 'c', 'c'}, curbuf('get_line_slice', 0, -1, true, true))
- curbuf('set_line_slice', -1, -1, true, true, {'a', 'b', 'c'})
+ curbuf_depr('set_line_slice', 0, -1, true, true, {'a', 'b', 'c'})
+ eq({'a', 'b', 'c'}, curbuf_depr('get_line_slice', 0, -1, true, true))
+ eq({'b', 'c'}, curbuf_depr('get_line_slice', 1, -1, true, true))
+ eq({'b'}, curbuf_depr('get_line_slice', 1, 2, true, false))
+ eq({}, curbuf_depr('get_line_slice', 1, 1, true, false))
+ eq({'a', 'b'}, curbuf_depr('get_line_slice', 0, -1, true, false))
+ eq({'b'}, curbuf_depr('get_line_slice', 1, -1, true, false))
+ eq({'b', 'c'}, curbuf_depr('get_line_slice', -2, -1, true, true))
+ curbuf_depr('set_line_slice', 1, 2, true, false, {'a', 'b', 'c'})
+ eq({'a', 'a', 'b', 'c', 'c'}, curbuf_depr('get_line_slice', 0, -1, true, true))
+ curbuf_depr('set_line_slice', -1, -1, true, true, {'a', 'b', 'c'})
eq({'a', 'a', 'b', 'c', 'a', 'b', 'c'},
- curbuf('get_line_slice', 0, -1, true, true))
- curbuf('set_line_slice', 0, -3, true, false, {})
- eq({'a', 'b', 'c'}, curbuf('get_line_slice', 0, -1, true, true))
- curbuf('set_line_slice', 0, -1, true, true, {})
- eq({''}, curbuf('get_line_slice', 0, -1, true, true))
+ curbuf_depr('get_line_slice', 0, -1, true, true))
+ curbuf_depr('set_line_slice', 0, -3, true, false, {})
+ eq({'a', 'b', 'c'}, curbuf_depr('get_line_slice', 0, -1, true, true))
+ curbuf_depr('set_line_slice', 0, -1, true, true, {})
+ eq({''}, curbuf_depr('get_line_slice', 0, -1, true, true))
end)
end)
@@ -109,6 +150,15 @@ describe('buffer_* functions', function()
local get_lines, set_lines = curbufmeths.get_lines, curbufmeths.set_lines
local line_count = curbufmeths.line_count
+ it('fails correctly when input is not valid', function()
+ eq(1, curbufmeths.get_number())
+ local err, emsg = pcall(bufmeths.set_lines, 1, 1, 2, false, {'b\na'})
+ eq(false, err)
+ local exp_emsg = 'String cannot contain newlines'
+ -- Expected {filename}:{lnum}: {exp_emsg}
+ eq(': ' .. exp_emsg, emsg:sub(-#exp_emsg - 2))
+ end)
+
it('has correct line_count when inserting and deleting', function()
eq(1, line_count())
set_lines(-1, -1, true, {'line'})
@@ -233,6 +283,59 @@ describe('buffer_* functions', function()
eq({'e', 'a', 'b', 'c', 'd'}, get_lines(0, -1, true))
end)
+ it("set_line on alternate buffer does not access invalid line (E315)", function()
+ feed_command('set hidden')
+ insert('Initial file')
+ command('enew')
+ insert([[
+ More
+ Lines
+ Than
+ In
+ The
+ Other
+ Buffer]])
+ feed_command('$')
+ local retval = exc_exec("call nvim_buf_set_lines(1, 0, 1, v:false, ['test'])")
+ eq(0, retval)
+ end)
+ end)
+
+ describe('get_offset', function()
+ local get_offset = curbufmeths.get_offset
+ it('works', function()
+ curbufmeths.set_lines(0,-1,true,{'Some\r','exa\000mple', '', 'buf\rfer', 'text'})
+ eq(5, curbufmeths.line_count())
+ eq(0, get_offset(0))
+ eq(6, get_offset(1))
+ eq(15, get_offset(2))
+ eq(16, get_offset(3))
+ eq(24, get_offset(4))
+ eq(29, get_offset(5))
+ eq({false,'Index out of bounds'}, meth_pcall(get_offset, 6))
+ eq({false,'Index out of bounds'}, meth_pcall(get_offset, -1))
+
+ curbufmeths.set_option('eol', false)
+ curbufmeths.set_option('fixeol', false)
+ eq(28, get_offset(5))
+
+ -- fileformat is ignored
+ curbufmeths.set_option('fileformat', 'dos')
+ eq(0, get_offset(0))
+ eq(6, get_offset(1))
+ eq(15, get_offset(2))
+ eq(16, get_offset(3))
+ eq(24, get_offset(4))
+ eq(28, get_offset(5))
+ curbufmeths.set_option('eol', true)
+ eq(29, get_offset(5))
+
+ command("set hidden")
+ command("enew")
+ eq(6, bufmeths.get_offset(1,1))
+ command("bunload! 1")
+ eq(-1, bufmeths.get_offset(1,1))
+ end)
end)
describe('{get,set,del}_var', function()
@@ -243,6 +346,39 @@ describe('buffer_* functions', function()
eq(1, funcs.exists('b:lua'))
curbufmeths.del_var('lua')
eq(0, funcs.exists('b:lua'))
+ eq({false, 'Key not found: lua'}, meth_pcall(curbufmeths.del_var, 'lua'))
+ curbufmeths.set_var('lua', 1)
+ command('lockvar b:lua')
+ eq({false, 'Key is locked: lua'}, meth_pcall(curbufmeths.del_var, 'lua'))
+ eq({false, 'Key is locked: lua'}, meth_pcall(curbufmeths.set_var, 'lua', 1))
+ eq({false, 'Key is read-only: changedtick'},
+ meth_pcall(curbufmeths.del_var, 'changedtick'))
+ eq({false, 'Key is read-only: changedtick'},
+ meth_pcall(curbufmeths.set_var, 'changedtick', 1))
+ end)
+ end)
+
+ describe('get_changedtick', function()
+ it('works', function()
+ eq(2, curbufmeths.get_changedtick())
+ curbufmeths.set_lines(0, 1, false, {'abc\0', '\0def', 'ghi'})
+ eq(3, curbufmeths.get_changedtick())
+ eq(3, curbufmeths.get_var('changedtick'))
+ end)
+
+ it('buffer_set_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('buffer_set_var', 0, 'lua', val1))
+ eq(val1, request('buffer_set_var', 0, 'lua', val2))
+ end)
+
+ it('buffer_del_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('buffer_set_var', 0, 'lua', val1))
+ eq(val1, request('buffer_set_var', 0, 'lua', val2))
+ eq(val2, request('buffer_del_var', 0, 'lua'))
end)
end)
@@ -274,10 +410,35 @@ describe('buffer_* functions', function()
end)
end)
+ describe('is_loaded', function()
+ it('works', function()
+ -- record our buffer number for when we unload it
+ local bufnr = curbuf('get_number')
+ -- api should report that the buffer is loaded
+ ok(buffer('is_loaded', bufnr))
+ -- hide the current buffer by switching to a new empty buffer
+ -- Careful! we need to modify the buffer first or vim will just reuse it
+ buffer('set_lines', bufnr, 0, -1, 1, {'line1'})
+ command('hide enew')
+ -- confirm the buffer is hidden, but still loaded
+ local infolist = nvim('eval', 'getbufinfo('..bufnr..')')
+ eq(1, #infolist)
+ eq(1, infolist[1].hidden)
+ eq(1, infolist[1].loaded)
+ -- now force unload the buffer
+ command('bunload! '..bufnr)
+ -- confirm the buffer is unloaded
+ infolist = nvim('eval', 'getbufinfo('..bufnr..')')
+ eq(0, infolist[1].loaded)
+ -- nvim_buf_is_loaded() should also report the buffer as unloaded
+ eq(false, buffer('is_loaded', bufnr))
+ end)
+ end)
+
describe('is_valid', function()
it('works', function()
nvim('command', 'new')
- local b = nvim('get_current_buffer')
+ local b = nvim('get_current_buf')
ok(buffer('is_valid', b))
nvim('command', 'bw!')
ok(not buffer('is_valid', b))
@@ -286,7 +447,7 @@ describe('buffer_* functions', function()
describe('get_mark', function()
it('works', function()
- curbuf('insert', -1, {'a', 'bit of', 'text'})
+ curbuf('set_lines', -1, -1, true, {'a', 'bit of', 'text'})
curwin('set_cursor', {3, 4})
nvim('command', 'mark V')
eq({3, 0}, curbuf('get_mark', 'V'))
diff --git a/test/functional/api/buffer_updates_spec.lua b/test/functional/api/buffer_updates_spec.lua
new file mode 100644
index 0000000000..b54d9e1f6e
--- /dev/null
+++ b/test/functional/api/buffer_updates_spec.lua
@@ -0,0 +1,838 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local eq, ok = helpers.eq, helpers.ok
+local buffer, command, eval, nvim, next_msg = helpers.buffer,
+ helpers.command, helpers.eval, helpers.nvim, helpers.next_msg
+local expect_err = helpers.expect_err
+local nvim_prog = helpers.nvim_prog
+local sleep = helpers.sleep
+local write_file = helpers.write_file
+
+local origlines = {"original line 1",
+ "original line 2",
+ "original line 3",
+ "original line 4",
+ "original line 5",
+ "original line 6"}
+
+local function expectn(name, args)
+ -- expect the next message to be the specified notification event
+ eq({'notification', name, args}, next_msg())
+end
+
+local function sendkeys(keys)
+ nvim('input', keys)
+ -- give nvim some time to process msgpack requests before possibly sending
+ -- more key presses - otherwise they all pile up in the queue and get
+ -- processed at once
+ local ntime = os.clock() + 0.1
+ repeat until os.clock() > ntime
+end
+
+local function open(activate, lines)
+ local filename = helpers.tmpname()
+ write_file(filename, table.concat(lines, "\n").."\n", true)
+ command('edit ' .. filename)
+ local b = nvim('get_current_buf')
+ -- what is the value of b:changedtick?
+ local tick = eval('b:changedtick')
+
+ -- Enable buffer events, ensure that the nvim_buf_lines_event messages
+ -- arrive as expected
+ if activate then
+ local firstline = 0
+ ok(buffer('attach', b, true, {}))
+ expectn('nvim_buf_lines_event', {b, tick, firstline, -1, lines, false})
+ end
+
+ return b, tick, filename
+end
+
+local function editoriginal(activate, lines)
+ if not lines then
+ lines = origlines
+ end
+ -- load up the file with the correct contents
+ clear()
+ return open(activate, lines)
+end
+
+local function reopen(buf, expectedlines)
+ ok(buffer('detach', buf))
+ expectn('nvim_buf_detach_event', {buf})
+ -- for some reason the :edit! increments tick by 2
+ command('edit!')
+ local tick = eval('b:changedtick')
+ ok(buffer('attach', buf, true, {}))
+ local firstline = 0
+ expectn('nvim_buf_lines_event', {buf, tick, firstline, -1, expectedlines, false})
+ command('normal! gg')
+ return tick
+end
+
+local function reopenwithfolds(b)
+ -- discard any changes to the buffer
+ local tick = reopen(b, origlines)
+
+ -- use markers for folds, make all folds open by default
+ command('setlocal foldmethod=marker foldlevel=20')
+
+ -- add a fold
+ command('2,4fold')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 4, {'original line 2/*{{{*/',
+ 'original line 3',
+ 'original line 4/*}}}*/'}, false})
+ -- make a new fold that wraps lines 1-6
+ command('1,6fold')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 6, {'original line 1/*{{{*/',
+ 'original line 2/*{{{*/',
+ 'original line 3',
+ 'original line 4/*}}}*/',
+ 'original line 5',
+ 'original line 6/*}}}*/'}, false})
+ return tick
+end
+
+describe('API: buffer events:', function()
+ it('when lines are added', function()
+ local b, tick = editoriginal(true)
+
+ -- add a new line at the start of the buffer
+ command('normal! GyyggP')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 0, {'original line 6'}, false})
+
+ -- add multiple lines at the start of the file
+ command('normal! GkkyGggP')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 0, {'original line 4',
+ 'original line 5',
+ 'original line 6'}, false})
+
+ -- add one line to the middle of the file, several times
+ command('normal! ggYjjp')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 3, {'original line 4'}, false})
+ command('normal! p')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 4, 4, {'original line 4'}, false})
+ command('normal! p')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 5, 5, {'original line 4'}, false})
+
+ -- add multiple lines to the middle of the file
+ command('normal! gg4Yjjp')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 3, {'original line 4',
+ 'original line 5',
+ 'original line 6',
+ 'original line 4'}, false})
+
+ -- add one line to the end of the file
+ command('normal! ggYGp')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 17, 17, {'original line 4'}, false})
+
+ -- add one line to the end of the file, several times
+ command('normal! ggYGppp')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 18, 18, {'original line 4'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 19, 19, {'original line 4'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 20, 20, {'original line 4'}, false})
+
+ -- add several lines to the end of the file, several times
+ command('normal! gg4YGp')
+ command('normal! Gp')
+ command('normal! Gp')
+ local firstfour = {'original line 4',
+ 'original line 5',
+ 'original line 6',
+ 'original line 4'}
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 21, 21, firstfour, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 25, 25, firstfour, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 29, 29, firstfour, false})
+
+ -- create a new empty buffer and wipe out the old one ... this will
+ -- turn off buffer events
+ command('enew!')
+ expectn('nvim_buf_detach_event', {b})
+
+ -- add a line at the start of an empty file
+ command('enew')
+ tick = eval('b:changedtick')
+ local b2 = nvim('get_current_buf')
+ ok(buffer('attach', b2, true, {}))
+ expectn('nvim_buf_lines_event', {b2, tick, 0, -1, {""}, false})
+ eval('append(0, ["new line 1"])')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b2, tick, 0, 0, {'new line 1'}, false})
+
+ -- turn off buffer events manually
+ buffer('detach', b2)
+ expectn('nvim_buf_detach_event', {b2})
+
+ -- add multiple lines to a blank file
+ command('enew!')
+ local b3 = nvim('get_current_buf')
+ ok(buffer('attach', b3, true, {}))
+ tick = eval('b:changedtick')
+ expectn('nvim_buf_lines_event', {b3, tick, 0, -1, {""}, false})
+ eval('append(0, ["new line 1", "new line 2", "new line 3"])')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b3, tick, 0, 0, {'new line 1',
+ 'new line 2',
+ 'new line 3'}, false})
+
+ -- use the API itself to add a line to the start of the buffer
+ buffer('set_lines', b3, 0, 0, true, {'New First Line'})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b3, tick, 0, 0, {"New First Line"}, false})
+ end)
+
+ it('when lines are removed', function()
+ local b, tick = editoriginal(true)
+
+ -- remove one line from start of file
+ command('normal! dd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {}, false})
+
+ -- remove multiple lines from the start of the file
+ command('normal! 4dd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 4, {}, false})
+
+ -- remove multiple lines from middle of file
+ tick = reopen(b, origlines)
+ command('normal! jj3dd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 5, {}, false})
+
+ -- remove one line from the end of the file
+ tick = reopen(b, origlines)
+ command('normal! Gdd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 5, 6, {}, false})
+
+ -- remove multiple lines from the end of the file
+ tick = reopen(b, origlines)
+ command('normal! 4G3dd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 6, {}, false})
+
+ -- pretend to remove heaps lines from the end of the file but really
+ -- just remove two
+ tick = reopen(b, origlines)
+ command('normal! Gk5dd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 4, 6, {}, false})
+ end)
+
+ it('when text is changed', function()
+ local b, tick = editoriginal(true)
+
+ -- some normal text editing
+ command('normal! A555')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'original line 1555'}, false})
+ command('normal! jj8X')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 3, {'origin3'}, false})
+
+ -- modify multiple lines at once using visual block mode
+ tick = reopen(b, origlines)
+ command('normal! jjw')
+ sendkeys('<C-v>jjllx')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event',
+ {b, tick, 2, 5, {'original e 3', 'original e 4', 'original e 5'}, false})
+
+ -- replace part of a line line using :s
+ tick = reopen(b, origlines)
+ command('3s/line 3/foo/')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 3, {'original foo'}, false})
+
+ -- replace parts of several lines line using :s
+ tick = reopen(b, origlines)
+ command('%s/line [35]/foo/')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 5, {'original foo',
+ 'original line 4',
+ 'original foo'}, false})
+
+ -- type text into the first line of a blank file, one character at a time
+ command('enew!')
+ tick = 2
+ expectn('nvim_buf_detach_event', {b})
+ local bnew = nvim('get_current_buf')
+ ok(buffer('attach', bnew, true, {}))
+ expectn('nvim_buf_lines_event', {bnew, tick, 0, -1, {''}, false})
+ sendkeys('i')
+ sendkeys('h')
+ sendkeys('e')
+ sendkeys('l')
+ sendkeys('l')
+ sendkeys('o\nworld')
+ expectn('nvim_buf_lines_event', {bnew, tick + 1, 0, 1, {'h'}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 2, 0, 1, {'he'}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 3, 0, 1, {'hel'}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 4, 0, 1, {'hell'}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 5, 0, 1, {'hello'}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 6, 0, 1, {'hello', ''}, false})
+ expectn('nvim_buf_lines_event', {bnew, tick + 7, 1, 2, {'world'}, false})
+ end)
+
+ it('when lines are replaced', function()
+ local b, tick = editoriginal(true)
+
+ -- blast away parts of some lines with visual mode
+ command('normal! jjwvjjllx')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 3, {'original '}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 4, {}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 4, {'e 5'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 3, {'original e 5'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 4, {}, false})
+
+ -- blast away a few lines using :g
+ tick = reopen(b, origlines)
+ command('global/line [35]/delete')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 2, 3, {}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 4, {}, false})
+ end)
+
+ it('when lines are filtered', function()
+ -- Test filtering lines with !cat
+ local b, tick = editoriginal(true, {"A", "C", "E", "B", "D", "F"})
+
+ command('silent 2,5!cat')
+ -- the change comes through as two changes:
+ -- 1) addition of the new lines after the filtered lines
+ -- 2) removal of the original lines
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 5, 5, {"C", "E", "B", "D"}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 5, {}, false})
+ end)
+
+ it('when you use "o"', function()
+ local b, tick = editoriginal(true, {'AAA', 'BBB'})
+ command('set noautoindent nosmartindent')
+
+ -- use 'o' to start a new line from a line with no indent
+ command('normal! o')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 1, {""}, false})
+
+ -- undo the change, indent line 1 a bit, and try again
+ command('undo')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 2, {}, false})
+ tick = tick + 1
+ expectn('nvim_buf_changedtick_event', {b, tick})
+ command('set autoindent')
+ command('normal! >>')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {"\tAAA"}, false})
+ command('normal! ommm')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 1, {"\t"}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 2, {"\tmmm"}, false})
+
+ -- undo the change, and try again with 'O'
+ command('undo')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 2, {'\t'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 2, {}, false})
+ tick = tick + 1
+ expectn('nvim_buf_changedtick_event', {b, tick})
+ command('normal! ggOmmm')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 0, {"\t"}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {"\tmmm"}, false})
+ end)
+
+ it('deactivates if the buffer is changed externally', function()
+ -- Test changing file from outside vim and reloading using :edit
+ local lines = {"Line 1", "Line 2"};
+ local b, tick, filename = editoriginal(true, lines)
+
+ command('normal! x')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'ine 1'}, false})
+ command('undo')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'Line 1'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_changedtick_event', {b, tick})
+
+ -- change the file directly
+ write_file(filename, "another line\n", true, true)
+
+ -- reopen the file and watch buffer events shut down
+ command('edit')
+ expectn('nvim_buf_detach_event', {b})
+ end)
+
+ it('channel can watch many buffers at once', function()
+ -- edit 3 buffers, make sure they all have windows visible so that when we
+ -- move between buffers, none of them are unloaded
+ local b1, tick1 = editoriginal(true, {'A1', 'A2'})
+ local b1nr = eval('bufnr("")')
+ command('split')
+ local b2, tick2 = open(true, {'B1', 'B2'})
+ local b2nr = eval('bufnr("")')
+ command('split')
+ local b3, tick3 = open(true, {'C1', 'C2'})
+ local b3nr = eval('bufnr("")')
+
+ -- make a new window for moving between buffers
+ command('split')
+
+ command('b'..b1nr)
+ command('normal! x')
+ tick1 = tick1 + 1
+ expectn('nvim_buf_lines_event', {b1, tick1, 0, 1, {'1'}, false})
+ command('undo')
+ tick1 = tick1 + 1
+ expectn('nvim_buf_lines_event', {b1, tick1, 0, 1, {'A1'}, false})
+ tick1 = tick1 + 1
+ expectn('nvim_buf_changedtick_event', {b1, tick1})
+
+ command('b'..b2nr)
+ command('normal! x')
+ tick2 = tick2 + 1
+ expectn('nvim_buf_lines_event', {b2, tick2, 0, 1, {'1'}, false})
+ command('undo')
+ tick2 = tick2 + 1
+ expectn('nvim_buf_lines_event', {b2, tick2, 0, 1, {'B1'}, false})
+ tick2 = tick2 + 1
+ expectn('nvim_buf_changedtick_event', {b2, tick2})
+
+ command('b'..b3nr)
+ command('normal! x')
+ tick3 = tick3 + 1
+ expectn('nvim_buf_lines_event', {b3, tick3, 0, 1, {'1'}, false})
+ command('undo')
+ tick3 = tick3 + 1
+ expectn('nvim_buf_lines_event', {b3, tick3, 0, 1, {'C1'}, false})
+ tick3 = tick3 + 1
+ expectn('nvim_buf_changedtick_event', {b3, tick3})
+ end)
+
+ it('does not get confused if enabled/disabled many times', function()
+ local channel = nvim('get_api_info')[1]
+ local b, tick = editoriginal(false)
+
+ -- Enable buffer events many times.
+ ok(buffer('attach', b, true, {}))
+ ok(buffer('attach', b, true, {}))
+ ok(buffer('attach', b, true, {}))
+ ok(buffer('attach', b, true, {}))
+ ok(buffer('attach', b, true, {}))
+ expectn('nvim_buf_lines_event', {b, tick, 0, -1, origlines, false})
+ eval('rpcnotify('..channel..', "Hello There")')
+ expectn('Hello There', {})
+
+ -- Disable buffer events many times.
+ ok(buffer('detach', b))
+ ok(buffer('detach', b))
+ ok(buffer('detach', b))
+ ok(buffer('detach', b))
+ ok(buffer('detach', b))
+ expectn('nvim_buf_detach_event', {b})
+ eval('rpcnotify('..channel..', "Hello Again")')
+ expectn('Hello Again', {})
+ end)
+
+ it('can notify several channels at once', function()
+ clear()
+
+ -- create several new sessions, in addition to our main API
+ local sessions = {}
+ local pipe = helpers.new_pipename()
+ eval("serverstart('"..pipe.."')")
+ sessions[1] = helpers.connect(pipe)
+ sessions[2] = helpers.connect(pipe)
+ sessions[3] = helpers.connect(pipe)
+
+ local function request(sessionnr, method, ...)
+ local status, rv = sessions[sessionnr]:request(method, ...)
+ if not status then
+ error(rv[2])
+ end
+ return rv
+ end
+
+ local function wantn(sessionid, name, args)
+ local session = sessions[sessionid]
+ eq({'notification', name, args}, session:next_message(10000))
+ end
+
+ -- Edit a new file, but don't enable buffer events.
+ local lines = {'AAA', 'BBB'}
+ local b, tick = open(false, lines)
+
+ -- Enable buffer events for sessions 1, 2 and 3.
+ ok(request(1, 'nvim_buf_attach', b, true, {}))
+ ok(request(2, 'nvim_buf_attach', b, true, {}))
+ ok(request(3, 'nvim_buf_attach', b, true, {}))
+ wantn(1, 'nvim_buf_lines_event', {b, tick, 0, -1, lines, false})
+ wantn(2, 'nvim_buf_lines_event', {b, tick, 0, -1, lines, false})
+ wantn(3, 'nvim_buf_lines_event', {b, tick, 0, -1, lines, false})
+
+ -- Change the buffer.
+ command('normal! x')
+ tick = tick + 1
+ wantn(1, 'nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+ wantn(2, 'nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+ wantn(3, 'nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+
+ -- Stop watching on channel 1.
+ ok(request(1, 'nvim_buf_detach', b))
+ wantn(1, 'nvim_buf_detach_event', {b})
+
+ -- Undo the change to buffer 1.
+ command('undo')
+ tick = tick + 1
+ wantn(2, 'nvim_buf_lines_event', {b, tick, 0, 1, {'AAA'}, false})
+ wantn(3, 'nvim_buf_lines_event', {b, tick, 0, 1, {'AAA'}, false})
+ tick = tick + 1
+ wantn(2, 'nvim_buf_changedtick_event', {b, tick})
+ wantn(3, 'nvim_buf_changedtick_event', {b, tick})
+
+ -- make sure there are no other pending nvim_buf_lines_event messages going to
+ -- channel 1
+ local channel1 = request(1, 'nvim_get_api_info')[1]
+ eval('rpcnotify('..channel1..', "Hello")')
+ wantn(1, 'Hello', {})
+
+ -- close the buffer and channels 2 and 3 should get a nvim_buf_detach_event
+ -- notification
+ command('edit')
+ wantn(2, 'nvim_buf_detach_event', {b})
+ wantn(3, 'nvim_buf_detach_event', {b})
+
+ -- make sure there are no other pending nvim_buf_lines_event messages going to
+ -- channel 1
+ channel1 = request(1, 'nvim_get_api_info')[1]
+ eval('rpcnotify('..channel1..', "Hello Again")')
+ wantn(1, 'Hello Again', {})
+ end)
+
+ it('works with :diffput and :diffget', function()
+ local b1, tick1 = editoriginal(true, {"AAA", "BBB"})
+ local channel = nvim('get_api_info')[1]
+ command('diffthis')
+ command('rightbelow vsplit')
+ local b2, tick2 = open(true, {"BBB", "CCC"})
+ command('diffthis')
+ -- go back to first buffer, and push the 'AAA' line to the second buffer
+ command('1wincmd w')
+ command('normal! gg')
+ command('diffput')
+ tick2 = tick2 + 1
+ expectn('nvim_buf_lines_event', {b2, tick2, 0, 0, {"AAA"}, false})
+
+ -- use :diffget to grab the other change from buffer 2
+ command('normal! G')
+ command('diffget')
+ tick1 = tick1 + 1
+ expectn('nvim_buf_lines_event', {b1, tick1, 2, 2, {"CCC"}, false})
+
+ eval('rpcnotify('..channel..', "Goodbye")')
+ expectn('Goodbye', {})
+ end)
+
+ it('works with :sort', function()
+ -- test for :sort
+ local b, tick = editoriginal(true, {"B", "D", "C", "A", "E"})
+ command('%sort')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 5, {"A", "B", "C", "D", "E"}, false})
+ end)
+
+ it('works with :left', function()
+ local b, tick = editoriginal(true, {" A", " B", "B", "\tB", "\t\tC"})
+ command('2,4left')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 4, {"B", "B", "B"}, false})
+ end)
+
+ it('works with :right', function()
+ local b, tick = editoriginal(true, {" A",
+ "\t B",
+ "\t \tBB",
+ " \tB",
+ "\t\tC"})
+ command('set ts=2 et')
+ command('2,4retab')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 4, {" B", " BB", " B"}, false})
+ end)
+
+ it('works with :move', function()
+ local b, tick = editoriginal(true, origlines)
+ -- move text down towards the end of the file
+ command('2,3move 4')
+ tick = tick + 2
+ expectn('nvim_buf_lines_event', {b, tick, 4, 4, {"original line 2",
+ "original line 3"}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 3, {}, false})
+
+ -- move text up towards the start of the file
+ tick = reopen(b, origlines)
+ command('4,5move 2')
+ tick = tick + 2
+ expectn('nvim_buf_lines_event', {b, tick, 2, 2, {"original line 4",
+ "original line 5"}, false})
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 5, 7, {}, false})
+ end)
+
+ it('when you manually add/remove folds', function()
+ local b = editoriginal(true)
+ local tick = reopenwithfolds(b)
+
+ -- delete the inner fold
+ command('normal! zR3Gzd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 1, 4, {'original line 2',
+ 'original line 3',
+ 'original line 4'}, false})
+ -- delete the outer fold
+ command('normal! zd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 6, origlines, false})
+
+ -- discard changes and put the folds back
+ tick = reopenwithfolds(b)
+
+ -- remove both folds at once
+ command('normal! ggzczD')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 6, origlines, false})
+
+ -- discard changes and put the folds back
+ tick = reopenwithfolds(b)
+
+ -- now delete all folds at once
+ command('normal! zE')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 6, origlines, false})
+
+ -- create a fold from line 4 to the end of the file
+ command('normal! 4GA/*{{{*/')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 4, {'original line 4/*{{{*/'}, false})
+
+ -- delete the fold which only has one marker
+ command('normal! Gzd')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 3, 6, {'original line 4',
+ 'original line 5',
+ 'original line 6'}, false})
+ end)
+
+ it('detaches if the buffer is closed', function()
+ local b, tick = editoriginal(true, {'AAA'})
+ local channel = nvim('get_api_info')[1]
+
+ -- Test that buffer events are working.
+ command('normal! x')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+ command('undo')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'AAA'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_changedtick_event', {b, tick})
+
+ -- close our buffer by creating a new one
+ command('enew')
+ expectn('nvim_buf_detach_event', {b})
+
+ -- Reopen the original buffer, make sure there are no buffer events sent.
+ command('b1')
+ command('normal! x')
+
+ eval('rpcnotify('..channel..', "Hello There")')
+ expectn('Hello There', {})
+ end)
+
+ it('stays attached if the buffer is hidden', function()
+ local b, tick = editoriginal(true, {'AAA'})
+ local channel = nvim('get_api_info')[1]
+
+ -- Test that buffer events are working.
+ command('normal! x')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+ command('undo')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'AAA'}, false})
+ tick = tick + 1
+ expectn('nvim_buf_changedtick_event', {b, tick})
+
+ -- Close our buffer by creating a new one.
+ command('set hidden')
+ command('enew')
+
+ -- Assert that no nvim_buf_detach_event is sent.
+ eval('rpcnotify('..channel..', "Hello There")')
+ expectn('Hello There', {})
+
+ -- Reopen the original buffer, assert that buffer events are still active.
+ command('b1')
+ command('normal! x')
+ tick = tick + 1
+ expectn('nvim_buf_lines_event', {b, tick, 0, 1, {'AA'}, false})
+ end)
+
+ it('detaches if the buffer is unloaded/deleted/wiped', function()
+ -- start with a blank nvim
+ clear()
+ -- need to make a new window with a buffer because :bunload doesn't let you
+ -- unload the last buffer
+ for _, cmd in ipairs({'bunload', 'bdelete', 'bwipeout'}) do
+ command('new')
+ -- open a brand spanking new file
+ local b = open(true, {'AAA'})
+
+ -- call :bunload or whatever the command is, and then check that we
+ -- receive a nvim_buf_detach_event
+ command(cmd)
+ expectn('nvim_buf_detach_event', {b})
+ end
+ end)
+
+ it('does not send the buffer content if not requested', function()
+ clear()
+ local b, tick = editoriginal(false)
+ ok(buffer('attach', b, false, {}))
+ expectn('nvim_buf_changedtick_event', {b, tick})
+ end)
+
+ it('returns a proper error on nonempty options dict', function()
+ clear()
+ local b = editoriginal(false)
+ expect_err("dict isn't empty", buffer, 'attach', b, false, {builtin="asfd"})
+ end)
+
+ it('nvim_buf_attach returns response after delay #8634', function()
+ clear()
+ sleep(250)
+ -- response
+ eq(true, helpers.request('nvim_buf_attach', 0, false, {}))
+ -- notification
+ eq({
+ [1] = 'notification',
+ [2] = 'nvim_buf_changedtick_event',
+ [3] = {
+ [1] = { id = 1 },
+ [2] = 2 }, }, next_msg())
+ end)
+end)
+
+describe('API: buffer events:', function()
+ before_each(function()
+ clear()
+ end)
+
+ local function lines_subset(first, second)
+ for i = 1,#first do
+ if first[i] ~= second[i] then
+ return false
+ end
+ end
+ return true
+ end
+
+ local function lines_equal(f, s)
+ return lines_subset(f, s) and lines_subset(s, f)
+ end
+
+ local function assert_match_somewhere(expected_lines, buffer_lines)
+ local msg = next_msg()
+
+ while(msg ~= nil) do
+ local event = msg[2]
+ if event == 'nvim_buf_lines_event' then
+ local args = msg[3]
+ local starts = args[3]
+ local newlines = args[5]
+
+ -- Size of the contained nvim instance is 23 lines, this might change
+ -- with the test setup. Note updates are continguous.
+ assert(#newlines <= 23)
+
+ for i = 1,#newlines do
+ buffer_lines[starts + i] = newlines[i]
+ end
+ -- we don't compare the msg area of the embedded nvim, it's too flakey
+ buffer_lines[23] = nil
+
+ if lines_equal(buffer_lines, expected_lines) then
+ -- OK
+ return
+ end
+ end
+ msg = next_msg()
+ end
+ assert(false, 'did not match/receive expected nvim_buf_lines_event lines')
+ end
+
+ it('when :terminal lines change', function()
+ local buffer_lines = {}
+ local expected_lines = {}
+ command('terminal "'..nvim_prog..'" -u NONE -i NONE -n -c "set shortmess+=A"')
+ local b = nvim('get_current_buf')
+ ok(buffer('attach', b, true, {}))
+
+ for _ = 1,22 do
+ table.insert(expected_lines,'~')
+ end
+ expected_lines[1] = ''
+ expected_lines[22] = ('tmp_terminal_nvim'..(' '):rep(45)
+ ..'0,0-1 All')
+
+ sendkeys('i:e tmp_terminal_nvim<Enter>')
+ assert_match_somewhere(expected_lines, buffer_lines)
+
+ expected_lines[1] = 'Blarg'
+ expected_lines[22] = ('tmp_terminal_nvim [+]'..(' '):rep(41)
+ ..'1,6 All')
+
+ sendkeys('iBlarg')
+ assert_match_somewhere(expected_lines, buffer_lines)
+
+ for i = 1,21 do
+ expected_lines[i] = 'xyz'
+ end
+ expected_lines[22] = ('tmp_terminal_nvim [+]'..(' '):rep(41)
+ ..'31,4 Bot')
+
+ local s = string.rep('\nxyz', 30)
+ sendkeys(s)
+ assert_match_somewhere(expected_lines, buffer_lines)
+ end)
+
+end)
diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua
new file mode 100644
index 0000000000..b7520235b7
--- /dev/null
+++ b/test/functional/api/command_spec.lua
@@ -0,0 +1,80 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local NIL = helpers.NIL
+local clear = helpers.clear
+local command = helpers.command
+local curbufmeths = helpers.curbufmeths
+local eq = helpers.eq
+local expect_err = helpers.expect_err
+local meths = helpers.meths
+local source = helpers.source
+
+describe('nvim_get_commands', function()
+ local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', range=NIL, register=false, script_id=0, }
+ local cmd_dict2 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='pwd', name='Pwd', nargs='?', range=NIL, register=false, script_id=0, }
+ before_each(clear)
+
+ it('gets empty list if no commands were defined', function()
+ eq({}, meths.get_commands({builtin=false}))
+ end)
+
+ it('validates input', function()
+ expect_err('builtin=true not implemented', meths.get_commands,
+ {builtin=true})
+ expect_err('unexpected key: foo', meths.get_commands,
+ {foo='blah'})
+ end)
+
+ it('gets global user-defined commands', function()
+ -- Define a command.
+ command('command -nargs=1 Hello echo "Hello World"')
+ eq({Hello=cmd_dict}, meths.get_commands({builtin=false}))
+ -- Define another command.
+ command('command -nargs=? Pwd pwd');
+ eq({Hello=cmd_dict, Pwd=cmd_dict2}, meths.get_commands({builtin=false}))
+ -- Delete a command.
+ command('delcommand Pwd')
+ eq({Hello=cmd_dict}, meths.get_commands({builtin=false}))
+ end)
+
+ it('gets buffer-local user-defined commands', function()
+ -- Define a buffer-local command.
+ command('command -buffer -nargs=1 Hello echo "Hello World"')
+ eq({Hello=cmd_dict}, curbufmeths.get_commands({builtin=false}))
+ -- Define another buffer-local command.
+ command('command -buffer -nargs=? Pwd pwd')
+ eq({Hello=cmd_dict, Pwd=cmd_dict2}, curbufmeths.get_commands({builtin=false}))
+ -- Delete a command.
+ command('delcommand Pwd')
+ eq({Hello=cmd_dict}, curbufmeths.get_commands({builtin=false}))
+
+ -- {builtin=true} always returns empty for buffer-local case.
+ eq({}, curbufmeths.get_commands({builtin=true}))
+ end)
+
+ it('gets various command attributes', function()
+ local cmd0 = { addr='arguments', bang=false, bar=false, complete='dir', complete_arg=NIL, count='10', definition='pwd <args>', name='TestCmd', nargs='0', range='10', register=false, script_id=0, }
+ local cmd1 = { addr=NIL, bang=false, bar=false, complete='custom', complete_arg='ListUsers', count=NIL, definition='!finger <args>', name='Finger', nargs='+', range=NIL, register=false, script_id=1, }
+ local cmd2 = { addr=NIL, bang=true, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R2_foo(<q-args>)', name='Cmd2', nargs='*', range=NIL, register=false, script_id=2, }
+ local cmd3 = { addr=NIL, bang=false, bar=true, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R3_ohyeah()', name='Cmd3', nargs='0', range=NIL, register=false, script_id=3, }
+ local cmd4 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R4_just_great()', name='Cmd4', nargs='0', range=NIL, register=true, script_id=4, }
+ source([[
+ command -complete=custom,ListUsers -nargs=+ Finger !finger <args>
+ ]])
+ eq({Finger=cmd1}, meths.get_commands({builtin=false}))
+ command('command -complete=dir -addr=arguments -count=10 TestCmd pwd <args>')
+ eq({Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false}))
+
+ source([[
+ command -bang -nargs=* Cmd2 call <SID>foo(<q-args>)
+ ]])
+ source([[
+ command -bar -nargs=0 Cmd3 call <SID>ohyeah()
+ ]])
+ source([[
+ command -register Cmd4 call <SID>just_great()
+ ]])
+ -- TODO(justinmk): Order is stable but undefined. Sort before return?
+ eq({Cmd2=cmd2, Cmd3=cmd3, Cmd4=cmd4, Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false}))
+ end)
+end)
diff --git a/test/functional/api/highlight_spec.lua b/test/functional/api/highlight_spec.lua
new file mode 100644
index 0000000000..76bf338d97
--- /dev/null
+++ b/test/functional/api/highlight_spec.lua
@@ -0,0 +1,112 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear, nvim = helpers.clear, helpers.nvim
+local Screen = require('test.functional.ui.screen')
+local eq, eval = helpers.eq, helpers.eval
+local command = helpers.command
+local meths = helpers.meths
+
+describe('API: highlight',function()
+ local expected_rgb = {
+ background = Screen.colors.Yellow,
+ foreground = Screen.colors.Red,
+ special = Screen.colors.Blue,
+ bold = true,
+ }
+ local expected_cterm = {
+ background = 10,
+ underline = true,
+ }
+ local expected_rgb2 = {
+ background = Screen.colors.Yellow,
+ foreground = Screen.colors.Red,
+ special = Screen.colors.Blue,
+ bold = true,
+ italic = true,
+ reverse = true,
+ undercurl = true,
+ underline = true,
+ }
+
+ before_each(function()
+ clear()
+ command("hi NewHighlight cterm=underline ctermbg=green guifg=red guibg=yellow guisp=blue gui=bold")
+ end)
+
+ it("nvim_get_hl_by_id", function()
+ local hl_id = eval("hlID('NewHighlight')")
+ eq(expected_cterm, nvim("get_hl_by_id", hl_id, false))
+
+ hl_id = eval("hlID('NewHighlight')")
+ -- Test valid id.
+ eq(expected_rgb, nvim("get_hl_by_id", hl_id, true))
+
+ -- Test invalid id.
+ local err, emsg = pcall(meths.get_hl_by_id, 30000, false)
+ eq(false, err)
+ eq('Invalid highlight id: 30000', string.match(emsg, 'Invalid.*'))
+
+ -- Test all highlight properties.
+ command('hi NewHighlight gui=underline,bold,undercurl,italic,reverse')
+ eq(expected_rgb2, nvim("get_hl_by_id", hl_id, true))
+
+ -- Test nil argument.
+ err, emsg = pcall(meths.get_hl_by_id, { nil }, false)
+ eq(false, err)
+ eq('Wrong type for argument 1, expecting Integer',
+ string.match(emsg, 'Wrong.*'))
+
+ -- Test 0 argument.
+ err, emsg = pcall(meths.get_hl_by_id, 0, false)
+ eq(false, err)
+ eq('Invalid highlight id: 0',
+ string.match(emsg, 'Invalid.*'))
+
+ -- Test -1 argument.
+ err, emsg = pcall(meths.get_hl_by_id, -1, false)
+ eq(false, err)
+ eq('Invalid highlight id: -1',
+ string.match(emsg, 'Invalid.*'))
+ end)
+
+ it("nvim_get_hl_by_name", function()
+ local expected_normal = { background = Screen.colors.Yellow,
+ foreground = Screen.colors.Red }
+
+ -- Test `Normal` default values.
+ eq({}, nvim("get_hl_by_name", 'Normal', true))
+
+ eq(expected_cterm, nvim("get_hl_by_name", 'NewHighlight', false))
+ eq(expected_rgb, nvim("get_hl_by_name", 'NewHighlight', true))
+
+ -- Test `Normal` modified values.
+ command('hi Normal guifg=red guibg=yellow')
+ eq(expected_normal, nvim("get_hl_by_name", 'Normal', true))
+
+ -- Test invalid name.
+ local err, emsg = pcall(meths.get_hl_by_name , 'unknown_highlight', false)
+ eq(false, err)
+ eq('Invalid highlight name: unknown_highlight',
+ string.match(emsg, 'Invalid.*'))
+
+ -- Test nil argument.
+ err, emsg = pcall(meths.get_hl_by_name , { nil }, false)
+ eq(false, err)
+ eq('Wrong type for argument 1, expecting String',
+ string.match(emsg, 'Wrong.*'))
+
+ -- Test empty string argument.
+ err, emsg = pcall(meths.get_hl_by_name , '', false)
+ eq(false, err)
+ eq('Invalid highlight name: ',
+ string.match(emsg, 'Invalid.*'))
+
+ -- Test "standout" attribute. #8054
+ eq({ underline = true, },
+ meths.get_hl_by_name('cursorline', 0));
+ command('hi CursorLine cterm=standout,underline term=standout,underline gui=standout,underline')
+ command('set cursorline')
+ eq({ underline = true, standout = true, },
+ meths.get_hl_by_name('cursorline', 0));
+
+ end)
+end)
diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua
new file mode 100644
index 0000000000..f52372bee3
--- /dev/null
+++ b/test/functional/api/keymap_spec.lua
@@ -0,0 +1,310 @@
+local helpers = require('test.functional.helpers')(after_each)
+local global_helpers = require('test.helpers')
+
+local clear = helpers.clear
+local command = helpers.command
+local curbufmeths = helpers.curbufmeths
+local eq = helpers.eq
+local funcs = helpers.funcs
+local meths = helpers.meths
+local source = helpers.source
+
+local shallowcopy = global_helpers.shallowcopy
+
+describe('nvim_get_keymap', function()
+ before_each(clear)
+
+ -- Basic mapping and table to be used to describe results
+ local foo_bar_string = 'nnoremap foo bar'
+ local foo_bar_map_table = {
+ lhs='foo',
+ silent=0,
+ rhs='bar',
+ expr=0,
+ sid=0,
+ buffer=0,
+ nowait=0,
+ mode='n',
+ noremap=1,
+ }
+
+ it('returns empty list when no map', function()
+ eq({}, meths.get_keymap('n'))
+ end)
+
+ it('returns list of all applicable mappings', function()
+ command(foo_bar_string)
+ -- Only one mapping available
+ -- Should be the same as the dictionary we supplied earlier
+ -- and the dictionary you would get from maparg
+ -- since this is a global map, and not script local
+ eq({foo_bar_map_table}, meths.get_keymap('n'))
+ eq({funcs.maparg('foo', 'n', false, true)},
+ meths.get_keymap('n')
+ )
+
+ -- Add another mapping
+ command('nnoremap foo_longer bar_longer')
+ local foolong_bar_map_table = shallowcopy(foo_bar_map_table)
+ foolong_bar_map_table['lhs'] = 'foo_longer'
+ foolong_bar_map_table['rhs'] = 'bar_longer'
+
+ eq({foolong_bar_map_table, foo_bar_map_table},
+ meths.get_keymap('n')
+ )
+
+ -- Remove a mapping
+ command('unmap foo_longer')
+ eq({foo_bar_map_table},
+ meths.get_keymap('n')
+ )
+ end)
+
+ it('works for other modes', function()
+ -- Add two mappings, one in insert and one normal
+ -- We'll only check the insert mode one
+ command('nnoremap not_going to_check')
+
+ command('inoremap foo bar')
+ -- The table will be the same except for the mode
+ local insert_table = shallowcopy(foo_bar_map_table)
+ insert_table['mode'] = 'i'
+
+ eq({insert_table}, meths.get_keymap('i'))
+ end)
+
+ it('considers scope', function()
+ -- change the map slightly
+ command('nnoremap foo_longer bar_longer')
+ local foolong_bar_map_table = shallowcopy(foo_bar_map_table)
+ foolong_bar_map_table['lhs'] = 'foo_longer'
+ foolong_bar_map_table['rhs'] = 'bar_longer'
+
+ local buffer_table = shallowcopy(foo_bar_map_table)
+ buffer_table['buffer'] = 1
+
+ command('nnoremap <buffer> foo bar')
+
+ -- The buffer mapping should not show up
+ eq({foolong_bar_map_table}, meths.get_keymap('n'))
+ eq({buffer_table}, curbufmeths.get_keymap('n'))
+ end)
+
+ it('considers scope for overlapping maps', function()
+ command('nnoremap foo bar')
+
+ local buffer_table = shallowcopy(foo_bar_map_table)
+ buffer_table['buffer'] = 1
+
+ command('nnoremap <buffer> foo bar')
+
+ eq({foo_bar_map_table}, meths.get_keymap('n'))
+ eq({buffer_table}, curbufmeths.get_keymap('n'))
+ end)
+
+ it('can retrieve mapping for different buffers', function()
+ local original_buffer = curbufmeths.get_number()
+ -- Place something in each of the buffers to make sure they stick around
+ -- and set hidden so we can leave them
+ command('set hidden')
+ command('new')
+ command('normal! ihello 2')
+ command('new')
+ command('normal! ihello 3')
+
+ local final_buffer = curbufmeths.get_number()
+
+ command('nnoremap <buffer> foo bar')
+ -- Final buffer will have buffer mappings
+ local buffer_table = shallowcopy(foo_bar_map_table)
+ buffer_table['buffer'] = final_buffer
+ eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n'))
+ eq({buffer_table}, meths.buf_get_keymap(0, 'n'))
+
+ command('buffer ' .. original_buffer)
+ eq(original_buffer, curbufmeths.get_number())
+ -- Original buffer won't have any mappings
+ eq({}, meths.get_keymap('n'))
+ eq({}, curbufmeths.get_keymap('n'))
+ eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n'))
+ end)
+
+ -- Test toggle switches for basic options
+ -- @param option The key represented in the `maparg()` result dict
+ local function global_and_buffer_test(map,
+ option,
+ option_token,
+ global_on_result,
+ buffer_on_result,
+ global_off_result,
+ buffer_off_result,
+ new_windows)
+
+ local function make_new_windows(number_of_windows)
+ if new_windows == nil then
+ return nil
+ end
+
+ for _=1,number_of_windows do
+ command('new')
+ end
+ end
+
+ local mode = string.sub(map, 1,1)
+ -- Don't run this for the <buffer> mapping, since it doesn't make sense
+ if option_token ~= '<buffer>' then
+ it(string.format( 'returns %d for the key "%s" when %s is used globally with %s (%s)',
+ global_on_result, option, option_token, map, mode), function()
+ make_new_windows(new_windows)
+ command(map .. ' ' .. option_token .. ' foo bar')
+ local result = meths.get_keymap(mode)[1][option]
+ eq(global_on_result, result)
+ end)
+ end
+
+ it(string.format('returns %d for the key "%s" when %s is used for buffers with %s (%s)',
+ buffer_on_result, option, option_token, map, mode), function()
+ make_new_windows(new_windows)
+ command(map .. ' <buffer> ' .. option_token .. ' foo bar')
+ local result = curbufmeths.get_keymap(mode)[1][option]
+ eq(buffer_on_result, result)
+ end)
+
+ -- Don't run these for the <buffer> mapping, since it doesn't make sense
+ if option_token ~= '<buffer>' then
+ it(string.format('returns %d for the key "%s" when %s is not used globally with %s (%s)',
+ global_off_result, option, option_token, map, mode), function()
+ make_new_windows(new_windows)
+ command(map .. ' baz bat')
+ local result = meths.get_keymap(mode)[1][option]
+ eq(global_off_result, result)
+ end)
+
+ it(string.format('returns %d for the key "%s" when %s is not used for buffers with %s (%s)',
+ buffer_off_result, option, option_token, map, mode), function()
+ make_new_windows(new_windows)
+ command(map .. ' <buffer> foo bar')
+
+ local result = curbufmeths.get_keymap(mode)[1][option]
+ eq(buffer_off_result, result)
+ end)
+ end
+ end
+
+ -- Standard modes and returns the same values in the dictionary as maparg()
+ local mode_list = {'nnoremap', 'nmap', 'imap', 'inoremap', 'cnoremap'}
+ for mode in pairs(mode_list) do
+ global_and_buffer_test(mode_list[mode], 'silent', '<silent>', 1, 1, 0, 0)
+ global_and_buffer_test(mode_list[mode], 'nowait', '<nowait>', 1, 1, 0, 0)
+ global_and_buffer_test(mode_list[mode], 'expr', '<expr>', 1, 1, 0, 0)
+ end
+
+ -- noremap will now be 2 if script was used, which is not the same as maparg()
+ global_and_buffer_test('nmap', 'noremap', '<script>', 2, 2, 0, 0)
+ global_and_buffer_test('nnoremap', 'noremap', '<script>', 2, 2, 1, 1)
+
+ -- buffer will return the buffer ID, which is not the same as maparg()
+ -- Three of these tests won't run
+ global_and_buffer_test('nnoremap', 'buffer', '<buffer>', nil, 3, nil, nil, 2)
+
+ it('returns script numbers for global maps', function()
+ source([[
+ function! s:maparg_test_function() abort
+ return 'testing'
+ endfunction
+
+ nnoremap fizz :call <SID>maparg_test_function()<CR>
+ ]])
+ local sid_result = meths.get_keymap('n')[1]['sid']
+ eq(1, sid_result)
+ eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {}))
+ end)
+
+ it('returns script numbers for buffer maps', function()
+ source([[
+ function! s:maparg_test_function() abort
+ return 'testing'
+ endfunction
+
+ nnoremap <buffer> fizz :call <SID>maparg_test_function()<CR>
+ ]])
+ local sid_result = curbufmeths.get_keymap('n')[1]['sid']
+ eq(1, sid_result)
+ eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {}))
+ end)
+
+ it('works with <F12> and others', function()
+ command('nnoremap <F12> :let g:maparg_test_var = 1<CR>')
+ eq('<F12>', meths.get_keymap('n')[1]['lhs'])
+ eq(':let g:maparg_test_var = 1<CR>', meths.get_keymap('n')[1]['rhs'])
+ end)
+
+ it('works correctly despite various &cpo settings', function()
+ local cpo_table = {
+ silent=0,
+ expr=0,
+ sid=0,
+ buffer=0,
+ nowait=0,
+ noremap=1,
+ }
+ local function cpomap(lhs, rhs, mode)
+ local ret = shallowcopy(cpo_table)
+ ret.lhs = lhs
+ ret.rhs = rhs
+ ret.mode = mode
+ return ret
+ end
+
+ command('set cpo+=B')
+ command('nnoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\')
+ command('nnoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\')
+
+ command('set cpo+=B')
+ command('xnoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\')
+ command('xnoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\')
+
+ command('set cpo-=B')
+ command('snoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\')
+ command('snoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\')
+
+ command('set cpo-=B')
+ command('onoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\')
+ command('onoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\')
+
+ for _, cmd in ipairs({
+ 'set cpo-=B',
+ 'set cpo+=B',
+ }) do
+ command(cmd)
+ eq({cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'n'),
+ cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'n')},
+ meths.get_keymap('n'))
+ eq({cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'x'),
+ cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'x')},
+ meths.get_keymap('x'))
+ eq({cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 's'),
+ cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 's')},
+ meths.get_keymap('s'))
+ eq({cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 'o'),
+ cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 'o')},
+ meths.get_keymap('o'))
+ end
+ end)
+
+ it('always uses space for space and bar for bar', function()
+ local space_table = {
+ lhs='| |',
+ rhs='| |',
+ mode='n',
+ silent=0,
+ expr=0,
+ sid=0,
+ buffer=0,
+ nowait=0,
+ noremap=1,
+ }
+ command('nnoremap \\|<Char-0x20><Char-32><Space><Bar> \\|<Char-0x20><Char-32><Space> <Bar>')
+ eq({space_table}, meths.get_keymap('n'))
+ end)
+end)
diff --git a/test/functional/api/menu_spec.lua b/test/functional/api/menu_spec.lua
index d55b7b118a..2cfa0e3e47 100644
--- a/test/functional/api/menu_spec.lua
+++ b/test/functional/api/menu_spec.lua
@@ -20,15 +20,15 @@ describe("update_menu notification", function()
end)
local function expect_sent(expected)
- screen:wait(function()
+ screen:expect{condition=function()
if screen.update_menu ~= expected then
if expected then
- return 'update_menu was expected but not sent'
+ error('update_menu was expected but not sent')
else
- return 'update_menu was sent unexpectedly'
+ error('update_menu was sent unexpectedly')
end
end
- end)
+ end, unchanged=(not expected)}
end
it("should be sent when adding a menu", function()
diff --git a/test/functional/api/proc_spec.lua b/test/functional/api/proc_spec.lua
new file mode 100644
index 0000000000..d99c26b6c2
--- /dev/null
+++ b/test/functional/api/proc_spec.lua
@@ -0,0 +1,81 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local eq = helpers.eq
+local funcs = helpers.funcs
+local iswin = helpers.iswin
+local nvim_argv = helpers.nvim_argv
+local ok = helpers.ok
+local request = helpers.request
+local retry = helpers.retry
+local NIL = helpers.NIL
+
+describe('api', function()
+ before_each(clear)
+
+ describe('nvim_get_proc_children', function()
+ it('returns child process ids', function()
+ local this_pid = funcs.getpid()
+
+ local job1 = funcs.jobstart(nvim_argv)
+ retry(nil, nil, function()
+ eq(1, #request('nvim_get_proc_children', this_pid))
+ end)
+
+ local job2 = funcs.jobstart(nvim_argv)
+ retry(nil, nil, function()
+ eq(2, #request('nvim_get_proc_children', this_pid))
+ end)
+
+ funcs.jobstop(job1)
+ retry(nil, nil, function()
+ eq(1, #request('nvim_get_proc_children', this_pid))
+ end)
+
+ funcs.jobstop(job2)
+ retry(nil, nil, function()
+ eq(0, #request('nvim_get_proc_children', this_pid))
+ end)
+ end)
+
+ it('validates input', function()
+ local status, rv = pcall(request, "nvim_get_proc_children", -1)
+ eq(false, status)
+ eq("Invalid pid: -1", string.match(rv, "Invalid.*"))
+
+ status, rv = pcall(request, "nvim_get_proc_children", 0)
+ eq(false, status)
+ eq("Invalid pid: 0", string.match(rv, "Invalid.*"))
+
+ -- Assume PID 99999 does not exist.
+ status, rv = pcall(request, "nvim_get_proc_children", 99999)
+ eq(true, status)
+ eq({}, rv)
+ end)
+ end)
+
+ describe('nvim_get_proc', function()
+ it('returns process info', function()
+ local pid = funcs.getpid()
+ local pinfo = request('nvim_get_proc', pid)
+ eq((iswin() and 'nvim.exe' or 'nvim'), pinfo.name)
+ ok(pinfo.pid == pid)
+ ok(type(pinfo.ppid) == 'number' and pinfo.ppid ~= pid)
+ end)
+
+ it('validates input', function()
+ local status, rv = pcall(request, "nvim_get_proc", -1)
+ eq(false, status)
+ eq("Invalid pid: -1", string.match(rv, "Invalid.*"))
+
+ status, rv = pcall(request, "nvim_get_proc", 0)
+ eq(false, status)
+ eq("Invalid pid: 0", string.match(rv, "Invalid.*"))
+
+ -- Assume PID 99999 does not exist.
+ status, rv = pcall(request, "nvim_get_proc", 99999)
+ eq(true, status)
+ eq(NIL, rv)
+ end)
+ end)
+end)
diff --git a/test/functional/api/rpc_fixture.lua b/test/functional/api/rpc_fixture.lua
new file mode 100644
index 0000000000..e885a525af
--- /dev/null
+++ b/test/functional/api/rpc_fixture.lua
@@ -0,0 +1,38 @@
+local deps_prefix = './.deps/usr'
+if os.getenv('DEPS_PREFIX') then
+ deps_prefix = os.getenv('DEPS_PREFIX')
+end
+
+package.path = deps_prefix .. '/share/lua/5.1/?.lua;' ..
+ deps_prefix .. '/share/lua/5.1/?/init.lua;' ..
+ package.path
+
+package.cpath = deps_prefix .. '/lib/lua/5.1/?.so;' ..
+ package.cpath
+
+local mpack = require('mpack')
+local StdioStream = require('nvim.stdio_stream')
+local Session = require('nvim.session')
+
+local stdio_stream = StdioStream.open()
+local session = Session.new(stdio_stream)
+
+local function on_request(method, args)
+ if method == 'poll' then
+ return 'ok'
+ elseif method == 'write_stderr' then
+ io.stderr:write(args[1])
+ return "done!"
+ elseif method == "exit" then
+ session:stop()
+ return mpack.NIL
+ end
+end
+
+local function on_notification(event, args)
+ if event == 'ping' and #args == 0 then
+ session:notify("nvim_eval", "rpcnotify(g:channel, 'pong')")
+ end
+end
+
+session:run(on_request, on_notification)
diff --git a/test/functional/api/server_notifications_spec.lua b/test/functional/api/server_notifications_spec.lua
index 88e8c60560..29cd38ef0d 100644
--- a/test/functional/api/server_notifications_spec.lua
+++ b/test/functional/api/server_notifications_spec.lua
@@ -1,8 +1,7 @@
--- Tests for nvim notifications
local helpers = require('test.functional.helpers')(after_each)
-local eq, clear, eval, execute, nvim, next_message =
- helpers.eq, helpers.clear, helpers.eval, helpers.execute, helpers.nvim,
- helpers.next_message
+local eq, clear, eval, command, nvim, next_msg =
+ helpers.eq, helpers.clear, helpers.eval, helpers.command, helpers.nvim,
+ helpers.next_msg
local meths = helpers.meths
describe('notify', function()
@@ -16,10 +15,10 @@ describe('notify', function()
describe('passing a valid channel id', function()
it('sends the notification/args to the corresponding channel', function()
eval('rpcnotify('..channel..', "test-event", 1, 2, 3)')
- eq({'notification', 'test-event', {1, 2, 3}}, next_message())
- execute('au FileType lua call rpcnotify('..channel..', "lua!")')
- execute('set filetype=lua')
- eq({'notification', 'lua!', {}}, next_message())
+ eq({'notification', 'test-event', {1, 2, 3}}, next_msg())
+ command('au FileType lua call rpcnotify('..channel..', "lua!")')
+ command('set filetype=lua')
+ eq({'notification', 'lua!', {}}, next_msg())
end)
end)
@@ -29,13 +28,13 @@ describe('notify', function()
eval('rpcnotify(0, "event1", 1, 2, 3)')
eval('rpcnotify(0, "event2", 4, 5, 6)')
eval('rpcnotify(0, "event2", 7, 8, 9)')
- eq({'notification', 'event2', {4, 5, 6}}, next_message())
- eq({'notification', 'event2', {7, 8, 9}}, next_message())
+ eq({'notification', 'event2', {4, 5, 6}}, next_msg())
+ eq({'notification', 'event2', {7, 8, 9}}, next_msg())
nvim('unsubscribe', 'event2')
nvim('subscribe', 'event1')
eval('rpcnotify(0, "event2", 10, 11, 12)')
eval('rpcnotify(0, "event1", 13, 14, 15)')
- eq({'notification', 'event1', {13, 14, 15}}, next_message())
+ eq({'notification', 'event1', {13, 14, 15}}, next_msg())
end)
it('does not crash for deeply nested variable', function()
@@ -43,7 +42,7 @@ describe('notify', function()
local nest_level = 1000
meths.command(('call map(range(%u), "extend(g:, {\'l\': [g:l]})")'):format(nest_level - 1))
eval('rpcnotify('..channel..', "event", g:l)')
- local msg = next_message()
+ local msg = next_msg()
eq('notification', msg[1])
eq('event', msg[2])
local act_ret = msg[3]
@@ -66,4 +65,11 @@ describe('notify', function()
eq(nest_level, act_nest_level)
end)
end)
+
+ it('unsubscribe non-existing event #8745', function()
+ nvim('subscribe', 'event1')
+ nvim('unsubscribe', 'doesnotexist')
+ nvim('unsubscribe', 'event1')
+ eq(2, eval('1+1')) -- Still alive?
+ end)
end)
diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua
index 54095112fb..4d25ba0819 100644
--- a/test/functional/api/server_requests_spec.lua
+++ b/test/functional/api/server_requests_spec.lua
@@ -1,11 +1,17 @@
--- Tests for some server->client RPC scenarios. Note that unlike with
--- `rpcnotify`, to evaluate `rpcrequest` calls we need the client event loop to
--- be running.
+-- Test server -> client RPC scenarios. Note: unlike `rpcnotify`, to evaluate
+-- `rpcrequest` calls we need the client event loop to be running.
local helpers = require('test.functional.helpers')(after_each)
+local Paths = require('test.config.paths')
+
local clear, nvim, eval = helpers.clear, helpers.nvim, helpers.eval
local eq, neq, run, stop = helpers.eq, helpers.neq, helpers.run, helpers.stop
-local nvim_prog = helpers.nvim_prog
-
+local nvim_prog, command, funcs = helpers.nvim_prog, helpers.command, helpers.funcs
+local source, next_msg = helpers.source, helpers.next_msg
+local ok = helpers.ok
+local meths = helpers.meths
+local spawn, merge_args = helpers.spawn, helpers.merge_args
+local set_session = helpers.set_session
+local expect_err = helpers.expect_err
describe('server -> client', function()
local cid
@@ -15,6 +21,22 @@ describe('server -> client', function()
cid = nvim('get_api_info')[1]
end)
+ it('handles unexpected closed stream while preparing RPC response', function()
+ source([[
+ let g:_nvim_args = [v:progpath, '--embed', '--headless', '-n', '-u', 'NONE', '-i', 'NONE', ]
+ let ch1 = jobstart(g:_nvim_args, {'rpc': v:true})
+ let child1_ch = rpcrequest(ch1, "nvim_get_api_info")[0]
+ call rpcnotify(ch1, 'nvim_eval', 'rpcrequest('.child1_ch.', "nvim_get_api_info")')
+
+ let ch2 = jobstart(g:_nvim_args, {'rpc': v:true})
+ let child2_ch = rpcrequest(ch2, "nvim_get_api_info")[0]
+ call rpcnotify(ch2, 'nvim_eval', 'rpcrequest('.child2_ch.', "nvim_get_api_info")')
+
+ call jobstop(ch1)
+ ]])
+ eq(2, eval("1+1")) -- Still alive?
+ end)
+
describe('simple call', function()
it('works', function()
local function on_setup()
@@ -88,7 +110,28 @@ describe('server -> client', function()
end)
describe('requests and notifications interleaved', function()
- -- This tests that the following scenario won't happen:
+ it('does not delay notifications during pending request', function()
+ local received = false
+ local function on_setup()
+ eq("retval", funcs.rpcrequest(cid, "doit"))
+ stop()
+ end
+ local function on_request(method)
+ if method == "doit" then
+ funcs.rpcnotify(cid, "headsup")
+ eq(true,received)
+ return "retval"
+ end
+ end
+ local function on_notification(method)
+ if method == "headsup" then
+ received = true
+ end
+ end
+ run(on_request, on_notification, on_setup)
+ end)
+
+ -- This tests the following scenario:
--
-- server->client [request ] (1)
-- client->server [request ] (2) triggered by (1)
@@ -103,48 +146,56 @@ describe('server -> client', function()
-- only deals with one server->client request at a time. (In other words,
-- the client cannot send a response to a request that is not at the top
-- of nvim's request stack).
- --
- -- But above scenario shoudn't happen by the way notifications are dealt in
- -- Nvim: they are only sent after there are no pending server->client
- -- request(the request stack fully unwinds). So (3) is only sent after the
- -- client returns (6).
- it('works', function()
- local expected = 300
- local notified = 0
+ pending('will close connection if not properly synchronized', function()
local function on_setup()
eq('notified!', eval('rpcrequest('..cid..', "notify")'))
end
local function on_request(method)
- eq('notify', method)
- eq(1, eval('rpcnotify('..cid..', "notification")'))
- return 'notified!'
+ if method == "notify" then
+ eq(1, eval('rpcnotify('..cid..', "notification")'))
+ return 'notified!'
+ elseif method == "nested" then
+ -- do some busywork, so the first request will return
+ -- before this one
+ for _ = 1, 5 do
+ eq(2, eval("1+1"))
+ end
+ eq(1, eval('rpcnotify('..cid..', "nested_done")'))
+ return 'done!'
+ end
end
local function on_notification(method)
- eq('notification', method)
- if notified == expected then
- stop()
- return
+ if method == "notification" then
+ eq('done!', eval('rpcrequest('..cid..', "nested")'))
+ elseif method == "nested_done" then
+ -- this should never have been sent
+ ok(false)
end
- notified = notified + 1
- eq('notified!', eval('rpcrequest('..cid..', "notify")'))
end
run(on_request, on_notification, on_setup)
- eq(expected, notified)
+ -- ignore disconnect failure, otherwise detected by after_each
+ clear()
end)
end)
- describe('when the client is a recursive vim instance', function()
+ describe('recursive (child) nvim client', function()
+ if os.getenv("TRAVIS") and helpers.os_name() == "osx" then
+ -- XXX: Hangs Travis macOS since e9061117a5b8f195c3f26a5cb94e18ddd7752d86.
+ pending("[Hangs on Travis macOS. #5002]", function() end)
+ return
+ end
+
before_each(function()
- nvim('command', "let vim = rpcstart('"..nvim_prog.."', ['-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--embed'])")
+ command("let vim = rpcstart('"..nvim_prog.."', ['-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--embed', '--headless'])")
neq(0, eval('vim'))
end)
- after_each(function() nvim('command', 'call rpcstop(vim)') end)
+ after_each(function() command('call rpcstop(vim)') end)
- it('can send/recieve notifications and make requests', function()
+ it('can send/receive notifications and make requests', function()
nvim('command', "call rpcnotify(vim, 'vim_set_current_line', 'SOME TEXT')")
-- Wait for the notification to complete.
@@ -154,11 +205,12 @@ describe('server -> client', function()
end)
it('can communicate buffers, tabpages, and windows', function()
- eq({3}, eval("rpcrequest(vim, 'vim_get_tabpages')"))
- eq({1}, eval("rpcrequest(vim, 'vim_get_windows')"))
+ eq({1}, eval("rpcrequest(vim, 'nvim_list_tabpages')"))
+ -- Window IDs start at 1000 (LOWEST_WIN_ID in vim.h)
+ eq({1000}, eval("rpcrequest(vim, 'nvim_list_wins')"))
- local buf = eval("rpcrequest(vim, 'vim_get_buffers')")[1]
- eq(2, buf)
+ local buf = eval("rpcrequest(vim, 'nvim_list_bufs')")[1]
+ eq(1, buf)
eval("rpcnotify(vim, 'buffer_set_line', "..buf..", 0, 'SOME TEXT')")
nvim('command', "call rpcrequest(vim, 'vim_eval', '0')") -- wait
@@ -170,9 +222,142 @@ describe('server -> client', function()
end)
it('returns an error if the request failed', function()
- local status, err = pcall(eval, "rpcrequest(vim, 'does-not-exist')")
- eq(false, status)
- eq(true, string.match(err, ': (.*)') == 'Failed to evaluate expression')
+ expect_err('Vim:Invalid method: does%-not%-exist',
+ eval, "rpcrequest(vim, 'does-not-exist')")
+ end)
+ end)
+
+ describe('jobstart()', function()
+ local jobid
+ before_each(function()
+ local channel = nvim('get_api_info')[1]
+ nvim('set_var', 'channel', channel)
+ source([[
+ function! s:OnEvent(id, data, event)
+ call rpcnotify(g:channel, a:event, 0, a:data)
+ endfunction
+ let g:job_opts = {
+ \ 'on_stderr': function('s:OnEvent'),
+ \ 'on_exit': function('s:OnEvent'),
+ \ 'user': 0,
+ \ 'rpc': v:true
+ \ }
+ ]])
+ local lua_prog = Paths.test_lua_prg
+ meths.set_var("args", {lua_prog, 'test/functional/api/rpc_fixture.lua'})
+ jobid = eval("jobstart(g:args, g:job_opts)")
+ neq(0, 'jobid')
+ end)
+
+ after_each(function()
+ pcall(funcs.jobstop, jobid)
+ end)
+
+ if helpers.pending_win32(pending) then return end
+
+ it('rpc and text stderr can be combined', function()
+ eq("ok",funcs.rpcrequest(jobid, "poll"))
+ funcs.rpcnotify(jobid, "ping")
+ eq({'notification', 'pong', {}}, next_msg())
+ eq("done!",funcs.rpcrequest(jobid, "write_stderr", "fluff\n"))
+ eq({'notification', 'stderr', {0, {'fluff', ''}}}, next_msg())
+ pcall(funcs.rpcrequest, jobid, "exit")
+ eq({'notification', 'stderr', {0, {''}}}, next_msg())
+ eq({'notification', 'exit', {0, 0}}, next_msg())
+ end)
+ end)
+
+ describe('connecting to another (peer) nvim', function()
+ local nvim_argv = merge_args(helpers.nvim_argv, {'--headless'})
+ local function connect_test(server, mode, address)
+ local serverpid = funcs.getpid()
+ local client = spawn(nvim_argv)
+ set_session(client, true)
+ local clientpid = funcs.getpid()
+ neq(serverpid, clientpid)
+ local id = funcs.sockconnect(mode, address, {rpc=true})
+ ok(id > 0)
+
+ funcs.rpcrequest(id, 'nvim_set_current_line', 'hello')
+ local client_id = funcs.rpcrequest(id, 'nvim_get_api_info')[1]
+
+ set_session(server, true)
+ eq(serverpid, funcs.getpid())
+ eq('hello', meths.get_current_line())
+
+ -- method calls work both ways
+ funcs.rpcrequest(client_id, 'nvim_set_current_line', 'howdy!')
+ eq(id, funcs.rpcrequest(client_id, 'nvim_get_api_info')[1])
+
+ set_session(client, true)
+ eq(clientpid, funcs.getpid())
+ eq('howdy!', meths.get_current_line())
+
+ server:close()
+ client:close()
+ end
+
+ it('via named pipe', function()
+ local server = spawn(nvim_argv)
+ set_session(server)
+ local address = funcs.serverlist()[1]
+ local first = string.sub(address,1,1)
+ ok(first == '/' or first == '\\')
+ connect_test(server, 'pipe', address)
+ end)
+
+ it('via ipv4 address', function()
+ local server = spawn(nvim_argv)
+ set_session(server)
+ local status, address = pcall(funcs.serverstart, "127.0.0.1:")
+ if not status then
+ pending('no ipv4 stack', function() end)
+ return
+ end
+ eq('127.0.0.1:', string.sub(address,1,10))
+ connect_test(server, 'tcp', address)
+ end)
+
+ it('via ipv6 address', function()
+ local server = spawn(nvim_argv)
+ set_session(server)
+ local status, address = pcall(funcs.serverstart, '::1:')
+ if not status then
+ pending('no ipv6 stack', function() end)
+ return
+ end
+ eq('::1:', string.sub(address,1,4))
+ connect_test(server, 'tcp', address)
+ end)
+
+ it('via hostname', function()
+ local server = spawn(nvim_argv)
+ set_session(server)
+ local address = funcs.serverstart("localhost:")
+ eq('localhost:', string.sub(address,1,10))
+ connect_test(server, 'tcp', address)
+ end)
+ end)
+
+ describe('connecting to its own pipe address', function()
+ it('does not deadlock', function()
+ if not os.getenv("TRAVIS") and helpers.os_name() == "osx" then
+ -- It does, in fact, deadlock on QuickBuild. #6851
+ pending("deadlocks on QuickBuild", function() end)
+ return
+ end
+ local address = funcs.serverlist()[1]
+ local first = string.sub(address,1,1)
+ ok(first == '/' or first == '\\')
+ local serverpid = funcs.getpid()
+
+ local id = funcs.sockconnect('pipe', address, {rpc=true})
+
+ funcs.rpcrequest(id, 'nvim_set_current_line', 'hello')
+ eq('hello', meths.get_current_line())
+ eq(serverpid, funcs.rpcrequest(id, "nvim_eval", "getpid()"))
+
+ eq(id, funcs.rpcrequest(id, 'nvim_get_api_info')[1])
end)
end)
end)
diff --git a/test/functional/api/tabpage_spec.lua b/test/functional/api/tabpage_spec.lua
index 7b97c7f067..c49091db02 100644
--- a/test/functional/api/tabpage_spec.lua
+++ b/test/functional/api/tabpage_spec.lua
@@ -1,25 +1,28 @@
--- Sanity checks for tabpage_* API calls via msgpack-rpc
local helpers = require('test.functional.helpers')(after_each)
local clear, nvim, tabpage, curtab, eq, ok =
helpers.clear, helpers.nvim, helpers.tabpage, helpers.curtab, helpers.eq,
helpers.ok
local curtabmeths = helpers.curtabmeths
local funcs = helpers.funcs
+local request = helpers.request
+local NIL = helpers.NIL
+local meth_pcall = helpers.meth_pcall
+local command = helpers.command
-describe('tabpage_* functions', function()
+describe('api/tabpage', function()
before_each(clear)
- describe('get_windows and get_window', function()
+ describe('list_wins and get_win', function()
it('works', function()
nvim('command', 'tabnew')
nvim('command', 'vsplit')
- local tab1, tab2 = unpack(nvim('get_tabpages'))
- local win1, win2, win3 = unpack(nvim('get_windows'))
- eq({win1}, tabpage('get_windows', tab1))
- eq({win2, win3}, tabpage('get_windows', tab2))
- eq(win2, tabpage('get_window', tab2))
- nvim('set_current_window', win3)
- eq(win3, tabpage('get_window', tab2))
+ local tab1, tab2 = unpack(nvim('list_tabpages'))
+ local win1, win2, win3 = unpack(nvim('list_wins'))
+ eq({win1}, tabpage('list_wins', tab1))
+ eq({win2, win3}, tabpage('list_wins', tab2))
+ eq(win2, tabpage('get_win', tab2))
+ nvim('set_current_win', win3)
+ eq(win3, tabpage('get_win', tab2))
end)
end)
@@ -31,13 +34,49 @@ describe('tabpage_* functions', function()
eq(1, funcs.exists('t:lua'))
curtabmeths.del_var('lua')
eq(0, funcs.exists('t:lua'))
+ eq({false, 'Key not found: lua'}, meth_pcall(curtabmeths.del_var, 'lua'))
+ curtabmeths.set_var('lua', 1)
+ command('lockvar t:lua')
+ eq({false, 'Key is locked: lua'}, meth_pcall(curtabmeths.del_var, 'lua'))
+ eq({false, 'Key is locked: lua'}, meth_pcall(curtabmeths.set_var, 'lua', 1))
+ end)
+
+ it('tabpage_set_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('tabpage_set_var', 0, 'lua', val1))
+ eq(val1, request('tabpage_set_var', 0, 'lua', val2))
+ end)
+
+ it('tabpage_del_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('tabpage_set_var', 0, 'lua', val1))
+ eq(val1, request('tabpage_set_var', 0, 'lua', val2))
+ eq(val2, request('tabpage_del_var', 0, 'lua'))
+ end)
+ end)
+
+ describe('get_number', function()
+ it('works', function()
+ local tabs = nvim('list_tabpages')
+ eq(1, tabpage('get_number', tabs[1]))
+
+ nvim('command', 'tabnew')
+ local tab1, tab2 = unpack(nvim('list_tabpages'))
+ eq(1, tabpage('get_number', tab1))
+ eq(2, tabpage('get_number', tab2))
+
+ nvim('command', '-tabmove')
+ eq(2, tabpage('get_number', tab1))
+ eq(1, tabpage('get_number', tab2))
end)
end)
describe('is_valid', function()
it('works', function()
nvim('command', 'tabnew')
- local tab = nvim('get_tabpages')[2]
+ local tab = nvim('list_tabpages')[2]
nvim('set_current_tabpage', tab)
ok(tabpage('is_valid', tab))
nvim('command', 'tabclose')
diff --git a/test/functional/api/ui_spec.lua b/test/functional/api/ui_spec.lua
new file mode 100644
index 0000000000..b028a50b02
--- /dev/null
+++ b/test/functional/api/ui_spec.lua
@@ -0,0 +1,37 @@
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+local clear = helpers.clear
+local eq = helpers.eq
+local eval = helpers.eval
+local expect_err = helpers.expect_err
+local meths = helpers.meths
+local request = helpers.request
+
+describe('nvim_ui_attach()', function()
+ before_each(function()
+ clear()
+ end)
+ it('handles very large width/height #2180', function()
+ local screen = Screen.new(999, 999)
+ screen:attach()
+ eq(999, eval('&lines'))
+ eq(999, eval('&columns'))
+ end)
+ it('invalid option returns error', function()
+ expect_err('No such UI option: foo',
+ meths.ui_attach, 80, 24, { foo={'foo'} })
+ end)
+ it('validates channel arg', function()
+ expect_err('UI not attached to channel: 1',
+ request, 'nvim_ui_try_resize', 40, 10)
+ expect_err('UI not attached to channel: 1',
+ request, 'nvim_ui_set_option', 'rgb', true)
+ expect_err('UI not attached to channel: 1',
+ request, 'nvim_ui_detach')
+
+ local screen = Screen.new()
+ screen:attach({rgb=false})
+ expect_err('UI already attached to channel: 1',
+ request, 'nvim_ui_attach', 40, 10, { rgb=false })
+ end)
+end)
diff --git a/test/functional/api/version_spec.lua b/test/functional/api/version_spec.lua
new file mode 100644
index 0000000000..bf67d4788b
--- /dev/null
+++ b/test/functional/api/version_spec.lua
@@ -0,0 +1,223 @@
+local helpers = require('test.functional.helpers')(after_each)
+local mpack = require('mpack')
+local clear, funcs, eq = helpers.clear, helpers.funcs, helpers.eq
+local call = helpers.call
+local meths = helpers.meths
+
+local function read_mpack_file(fname)
+ local fd = io.open(fname, 'rb')
+ if fd == nil then
+ return nil
+ end
+
+ local data = fd:read('*a')
+ fd:close()
+ local unpack = mpack.Unpacker()
+ return unpack(data)
+end
+
+describe("api_info()['version']", function()
+ before_each(clear)
+
+ it("returns API level", function()
+ local version = call('api_info')['version']
+ local current = version['api_level']
+ local compat = version['api_compatible']
+ eq("number", type(current))
+ eq("number", type(compat))
+ assert(current >= compat)
+ end)
+
+ it("returns Nvim version", function()
+ local version = call('api_info')['version']
+ local major = version['major']
+ local minor = version['minor']
+ local patch = version['patch']
+ eq("number", type(major))
+ eq("number", type(minor))
+ eq("number", type(patch))
+ eq(1, funcs.has("nvim-"..major.."."..minor.."."..patch))
+ eq(0, funcs.has("nvim-"..major.."."..minor.."."..(patch + 1)))
+ eq(0, funcs.has("nvim-"..major.."."..(minor + 1).."."..patch))
+ eq(0, funcs.has("nvim-"..(major + 1).."."..minor.."."..patch))
+ end)
+end)
+
+
+describe("api metadata", function()
+ before_each(clear)
+
+ local function name_table(entries)
+ local by_name = {}
+ for _,e in ipairs(entries) do
+ by_name[e.name] = e
+ end
+ return by_name
+ end
+
+ -- Remove metadata that is not essential to backwards-compatibility.
+ local function filter_function_metadata(f)
+ f.deprecated_since = nil
+ for idx, _ in ipairs(f.parameters) do
+ f.parameters[idx][2] = '' -- Remove parameter name.
+ end
+
+ if string.sub(f.name, 1, 4) ~= "nvim" then
+ f.method = nil
+ end
+ return f
+ end
+
+ local function check_ui_event_compatible(old_e, new_e)
+ -- check types of existing params are the same
+ -- adding parameters is ok, but removing params is not (gives nil error)
+ eq(old_e.since, new_e.since, old_e.name)
+ for i,p in ipairs(old_e.parameters) do
+ eq(new_e.parameters[i][1], p[1], old_e.name)
+ end
+ end
+
+ -- Level 0 represents methods from 0.1.5 and earlier, when 'since' was not
+ -- yet defined, and metadata was not filtered of internal keys like 'async'.
+ local function clean_level_0(metadata)
+ for _, f in ipairs(metadata.functions) do
+ f.can_fail = nil
+ f.async = nil
+ f.receives_channel_id = nil
+ f.since = 0
+ end
+ end
+
+ local api, compat, stable, api_level
+ local old_api = {}
+ setup(function()
+ api = meths.get_api_info()[2]
+ compat = api.version.api_compatible
+ api_level = api.version.api_level
+ if api.version.api_prerelease then
+ stable = api_level-1
+ else
+ stable = api_level
+ end
+
+ for level = compat, stable do
+ local path = ('test/functional/fixtures/api_level_'..
+ tostring(level)..'.mpack')
+ old_api[level] = read_mpack_file(path)
+ if old_api[level] == nil then
+ local errstr = "missing metadata fixture for stable level "..level..". "
+ if level == api_level and not api.version.api_prerelease then
+ errstr = (errstr.."If NVIM_API_CURRENT was bumped, "..
+ "don't forget to set NVIM_API_PRERELEASE to true.")
+ end
+ error(errstr)
+ end
+
+ if level == 0 then
+ clean_level_0(old_api[level])
+ end
+ end
+ end)
+
+ it("functions are compatible with old metadata or have new level", function()
+ local funcs_new = name_table(api.functions)
+ local funcs_compat = {}
+ for level = compat, stable do
+ for _,f in ipairs(old_api[level].functions) do
+ if funcs_new[f.name] == nil then
+ if f.since >= compat then
+ error('function '..f.name..' was removed but exists in level '..
+ f.since..' which nvim should be compatible with')
+ end
+ else
+ eq(filter_function_metadata(f),
+ filter_function_metadata(funcs_new[f.name]))
+ end
+ end
+ funcs_compat[level] = name_table(old_api[level].functions)
+ end
+
+ for _,f in ipairs(api.functions) do
+ if f.since <= stable then
+ local f_old = funcs_compat[f.since][f.name]
+ if f_old == nil then
+ if string.sub(f.name, 1, 4) == "nvim" then
+ local errstr = ("function "..f.name.." has too low since value. "..
+ "For new functions set it to "..(stable+1)..".")
+ if not api.version.api_prerelease then
+ errstr = (errstr.." Also bump NVIM_API_CURRENT and set "..
+ "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
+ end
+ error(errstr)
+ else
+ error("function name '"..f.name.."' doesn't begin with 'nvim_'")
+ end
+ end
+ elseif f.since > api_level then
+ if api.version.api_prerelease then
+ error("New function "..f.name.." should use since value "..
+ api_level)
+ else
+ error("function "..f.name.." has since value > api_level. "..
+ "Bump NVIM_API_CURRENT and set "..
+ "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
+ end
+ end
+ end
+ end)
+
+ it("UI events are compatible with old metadata or have new level", function()
+ local ui_events_new = name_table(api.ui_events)
+ local ui_events_compat = {}
+
+ -- UI events were formalized in level 3
+ for level = 3, stable do
+ for _,e in ipairs(old_api[level].ui_events) do
+ local new_e = ui_events_new[e.name]
+ if new_e ~= nil then
+ check_ui_event_compatible(e, new_e)
+ end
+ end
+ ui_events_compat[level] = name_table(old_api[level].ui_events)
+ end
+
+ for _,e in ipairs(api.ui_events) do
+ if e.since <= stable then
+ local e_old = ui_events_compat[e.since][e.name]
+ if e_old == nil then
+ local errstr = ("UI event "..e.name.." has too low since value. "..
+ "For new events set it to "..(stable+1)..".")
+ if not api.version.api_prerelease then
+ errstr = (errstr.." Also bump NVIM_API_CURRENT and set "..
+ "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
+ end
+ error(errstr)
+ end
+ elseif e.since > api_level then
+ if api.version.api_prerelease then
+ error("New UI event "..e.name.." should use since value "..
+ api_level)
+ else
+ error("UI event "..e.name.." has since value > api_level. "..
+ "Bump NVIM_API_CURRENT and set "..
+ "NVIM_API_PRERELEASE to true in CMakeLists.txt.")
+ end
+ end
+ end
+ end)
+
+ it("ui_options are preserved from older levels", function()
+ local available_options = {}
+ for _, option in ipairs(api.ui_options) do
+ available_options[option] = true
+ end
+ -- UI options were versioned from level 4
+ for level = 4, stable do
+ for _, option in ipairs(old_api[level].ui_options) do
+ if not available_options[option] then
+ error("UI option "..option.." from stable metadata is missing")
+ end
+ end
+ end
+ end)
+end)
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
index c4976ea06b..0e06e48a35 100644
--- a/test/functional/api/vim_spec.lua
+++ b/test/functional/api/vim_spec.lua
@@ -1,19 +1,64 @@
--- Sanity checks for vim_* API calls via msgpack-rpc
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
+local global_helpers = require('test.helpers')
+
local NIL = helpers.NIL
local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq
+local command = helpers.command
+local eval = helpers.eval
+local funcs = helpers.funcs
+local iswin = helpers.iswin
+local meth_pcall = helpers.meth_pcall
+local meths = helpers.meths
local ok, nvim_async, feed = helpers.ok, helpers.nvim_async, helpers.feed
local os_name = helpers.os_name
-local meths = helpers.meths
-local funcs = helpers.funcs
+local request = helpers.request
+local source = helpers.source
+local next_msg = helpers.next_msg
+
+local expect_err = global_helpers.expect_err
+local format_string = global_helpers.format_string
+local intchar2lua = global_helpers.intchar2lua
+local mergedicts_copy = global_helpers.mergedicts_copy
-describe('vim_* functions', function()
+describe('API', function()
before_each(clear)
- describe('command', function()
+ it('validates requests', function()
+ -- RPC
+ expect_err('Invalid method: bogus$',
+ request, 'bogus')
+ expect_err('Invalid method: … の り 。…$',
+ request, '… の り 。…')
+ expect_err('Invalid method: <empty>$',
+ request, '')
+
+ -- Non-RPC: rpcrequest(v:servername) uses internal channel.
+ expect_err('Invalid method: … の り 。…$',
+ request, 'nvim_eval',
+ [=[rpcrequest(sockconnect('pipe', v:servername, {'rpc':1}), '… の り 。…')]=])
+ expect_err('Invalid method: bogus$',
+ request, 'nvim_eval',
+ [=[rpcrequest(sockconnect('pipe', v:servername, {'rpc':1}), 'bogus')]=])
+
+ -- XXX: This must be the last one, else next one will fail:
+ -- "Packer instance already working. Use another Packer ..."
+ expect_err("can't serialize object$",
+ request, nil)
+ end)
+
+ it('handles errors in async requests', function()
+ local error_types = meths.get_api_info()[2].error_types
+ nvim_async("bogus")
+ eq({'notification', 'nvim_error_event',
+ {error_types.Exception.id, 'Invalid method: nvim_bogus'}}, next_msg())
+ -- error didn't close channel.
+ eq(2, eval('1+1'))
+ end)
+
+ describe('nvim_command', function()
it('works', function()
- local fname = os.tmpname()
+ local fname = helpers.tmpname()
nvim('command', 'new')
nvim('command', 'edit '..fname)
nvim('command', 'normal itesting\napi')
@@ -28,31 +73,264 @@ describe('vim_* functions', function()
f:close()
os.remove(fname)
end)
+
+ it('VimL validation error: fails with specific error', function()
+ local status, rv = pcall(nvim, "command", "bogus_command")
+ eq(false, status) -- nvim_command() failed.
+ eq("E492:", string.match(rv, "E%d*:")) -- VimL error was returned.
+ eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated.
+ eq('', eval('v:exception'))
+ end)
+
+ it('VimL execution error: fails with specific error', function()
+ local status, rv = pcall(nvim, "command_output", "buffer 23487")
+ eq(false, status) -- nvim_command() failed.
+ eq("E86: Buffer 23487 does not exist", string.match(rv, "E%d*:.*"))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ eq('', eval('v:exception'))
+ end)
end)
- describe('eval', function()
+ describe('nvim_command_output', function()
+ it('does not induce hit-enter prompt', function()
+ -- Induce a hit-enter prompt use nvim_input (non-blocking).
+ nvim('command', 'set cmdheight=1')
+ nvim('input', [[:echo "hi\nhi2"<CR>]])
+
+ -- Verify hit-enter prompt.
+ eq({mode='r', blocking=true}, nvim("get_mode"))
+ nvim('input', [[<C-c>]])
+
+ -- Verify NO hit-enter prompt.
+ nvim('command_output', [[echo "hi\nhi2"]])
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+
+ it('captures command output', function()
+ eq('this is\nspinal tap',
+ nvim('command_output', [[echo "this is\nspinal tap"]]))
+ eq('no line ending!',
+ nvim('command_output', [[echon "no line ending!"]]))
+ end)
+
+ it('captures empty command output', function()
+ eq('', nvim('command_output', 'echo'))
+ end)
+
+ it('captures single-char command output', function()
+ eq('x', nvim('command_output', 'echo "x"'))
+ end)
+
+ it('captures multiple commands', function()
+ eq('foo\n 1 %a "[No Name]" line 1',
+ nvim('command_output', 'echo "foo" | ls'))
+ end)
+
+ it('captures nested execute()', function()
+ eq('\nnested1\nnested2\n 1 %a "[No Name]" line 1',
+ nvim('command_output',
+ [[echo execute('echo "nested1\nnested2"') | ls]]))
+ end)
+
+ it('captures nested nvim_command_output()', function()
+ eq('nested1\nnested2\n 1 %a "[No Name]" line 1',
+ nvim('command_output',
+ [[echo nvim_command_output('echo "nested1\nnested2"') | ls]]))
+ end)
+
+ it('returns shell |:!| output', function()
+ local win_lf = iswin() and '\r' or ''
+ eq(':!echo foo\r\n\nfoo'..win_lf..'\n', nvim('command_output', [[!echo foo]]))
+ end)
+
+ it('VimL validation error: fails with specific error', function()
+ local status, rv = pcall(nvim, "command_output", "bogus commannnd")
+ eq(false, status) -- nvim_command_output() failed.
+ eq("E492: Not an editor command: bogus commannnd",
+ string.match(rv, "E%d*:.*"))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ -- Verify NO hit-enter prompt.
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+
+ it('VimL execution error: fails with specific error', function()
+ local status, rv = pcall(nvim, "command_output", "buffer 42")
+ eq(false, status) -- nvim_command_output() failed.
+ eq("E86: Buffer 42 does not exist", string.match(rv, "E%d*:.*"))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ -- Verify NO hit-enter prompt.
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+ end)
+
+ describe('nvim_eval', function()
it('works', function()
nvim('command', 'let g:v1 = "a"')
nvim('command', 'let g:v2 = [1, 2, {"v3": 3}]')
- eq({v1 = 'a', v2 = {1, 2, {v3 = 3}}}, nvim('eval', 'g:'))
+ eq({v1 = 'a', v2 = { 1, 2, { v3 = 3 } } }, nvim('eval', 'g:'))
end)
it('handles NULL-initialized strings correctly', function()
eq(1, nvim('eval',"matcharg(1) == ['', '']"))
eq({'', ''}, nvim('eval','matcharg(1)'))
end)
+
+ it('works under deprecated name', function()
+ eq(2, request("vim_eval", "1+1"))
+ end)
+
+ it("VimL error: returns error details, does NOT update v:errmsg", function()
+ expect_err('E121: Undefined variable: bogus$', request,
+ 'nvim_eval', 'bogus expression')
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ end)
end)
- describe('call_function', function()
+ describe('nvim_call_function', function()
it('works', function()
- nvim('call_function', 'setqflist', {{{ filename = 'something', lnum = 17}}, 'r'})
+ nvim('call_function', 'setqflist', { { { filename = 'something', lnum = 17 } }, 'r' })
eq(17, nvim('call_function', 'getqflist', {})[1].lnum)
eq(17, nvim('call_function', 'eval', {17}))
eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'}))
end)
+
+ it("VimL validation error: returns specific error, does NOT update v:errmsg", function()
+ expect_err('E117: Unknown function: bogus function$', request,
+ 'nvim_call_function', 'bogus function', {'arg1'})
+ expect_err('E119: Not enough arguments for function: atan', request,
+ 'nvim_call_function', 'atan', {})
+ eq('', eval('v:exception'))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ end)
+
+ it("VimL error: returns error details, does NOT update v:errmsg", function()
+ expect_err('E808: Number or Float required$', request,
+ 'nvim_call_function', 'atan', {'foo'})
+ expect_err('Invalid channel stream "xxx"$', request,
+ 'nvim_call_function', 'chanclose', {999, 'xxx'})
+ expect_err('E900: Invalid channel id$', request,
+ 'nvim_call_function', 'chansend', {999, 'foo'})
+ eq('', eval('v:exception'))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ end)
+
+ it("VimL exception: returns exception details, does NOT update v:errmsg", function()
+ source([[
+ function! Foo() abort
+ throw 'wtf'
+ endfunction
+ ]])
+ expect_err('wtf$', request,
+ 'nvim_call_function', 'Foo', {})
+ eq('', eval('v:exception'))
+ eq('', eval('v:errmsg')) -- v:errmsg was not updated.
+ end)
+
+ it('validates args', function()
+ local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' }
+ source([[
+ function! Foo(...) abort
+ echo a:000
+ endfunction
+ ]])
+ -- E740
+ expect_err('Function called with too many arguments$', request,
+ 'nvim_call_function', 'Foo', too_many_args)
+ end)
+ end)
+
+ describe('nvim_call_dict_function', function()
+ it('invokes VimL dict function', function()
+ source([[
+ function! F(name) dict
+ return self.greeting.', '.a:name.'!'
+ endfunction
+ let g:test_dict_fn = { 'greeting':'Hello', 'F':function('F') }
+
+ let g:test_dict_fn2 = { 'greeting':'Hi' }
+ function g:test_dict_fn2.F2(name)
+ return self.greeting.', '.a:name.' ...'
+ endfunction
+ ]])
+
+ -- :help Dictionary-function
+ eq('Hello, World!', nvim('call_dict_function', 'g:test_dict_fn', 'F', {'World'}))
+ -- Funcref is sent as NIL over RPC.
+ eq({ greeting = 'Hello', F = NIL }, nvim('get_var', 'test_dict_fn'))
+
+ -- :help numbered-function
+ eq('Hi, Moon ...', nvim('call_dict_function', 'g:test_dict_fn2', 'F2', {'Moon'}))
+ -- Funcref is sent as NIL over RPC.
+ eq({ greeting = 'Hi', F2 = NIL }, nvim('get_var', 'test_dict_fn2'))
+
+ -- Function specified via RPC dict.
+ source('function! G() dict\n return "@".(self.result)."@"\nendfunction')
+ eq('@it works@', nvim('call_dict_function', { result = 'it works', G = 'G'}, 'G', {}))
+ end)
+
+ it('validates args', function()
+ command('let g:d={"baz":"zub","meep":[]}')
+ expect_err('Not found: bogus$', request,
+ 'nvim_call_dict_function', 'g:d', 'bogus', {1,2})
+ expect_err('Not a function: baz$', request,
+ 'nvim_call_dict_function', 'g:d', 'baz', {1,2})
+ expect_err('Not a function: meep$', request,
+ 'nvim_call_dict_function', 'g:d', 'meep', {1,2})
+ expect_err('E117: Unknown function: f$', request,
+ 'nvim_call_dict_function', { f = '' }, 'f', {1,2})
+ expect_err('Not a function: f$', request,
+ 'nvim_call_dict_function', "{ 'f': '' }", 'f', {1,2})
+ expect_err('dict argument type must be String or Dictionary$', request,
+ 'nvim_call_dict_function', 42, 'f', {1,2})
+ expect_err('Failed to evaluate dict expression$', request,
+ 'nvim_call_dict_function', 'foo', 'f', {1,2})
+ expect_err('dict not found$', request,
+ 'nvim_call_dict_function', '42', 'f', {1,2})
+ expect_err('Invalid %(empty%) function name$', request,
+ 'nvim_call_dict_function', "{ 'f': '' }", '', {1,2})
+ end)
+ end)
+
+ describe('nvim_execute_lua', function()
+ it('works', function()
+ meths.execute_lua('vim.api.nvim_set_var("test", 3)', {})
+ eq(3, meths.get_var('test'))
+
+ eq(17, meths.execute_lua('a, b = ...\nreturn a + b', {10,7}))
+
+ eq(NIL, meths.execute_lua('function xx(a,b)\nreturn a..b\nend',{}))
+ eq("xy", meths.execute_lua('return xx(...)', {'x','y'}))
+ end)
+
+ it('reports errors', function()
+ eq({false, 'Error loading lua: [string "<nvim>"]:1: '..
+ "'=' expected near '+'"},
+ meth_pcall(meths.execute_lua, 'a+*b', {}))
+
+ eq({false, 'Error loading lua: [string "<nvim>"]:1: '..
+ "unexpected symbol near '1'"},
+ meth_pcall(meths.execute_lua, '1+2', {}))
+
+ eq({false, 'Error loading lua: [string "<nvim>"]:1: '..
+ "unexpected symbol"},
+ meth_pcall(meths.execute_lua, 'aa=bb\0', {}))
+
+ eq({false, 'Error executing lua: [string "<nvim>"]:1: '..
+ "attempt to call global 'bork' (a nil value)"},
+ meth_pcall(meths.execute_lua, 'bork()', {}))
+ end)
+ end)
+
+ describe('nvim_input', function()
+ it("VimL error: does NOT fail, updates v:errmsg", function()
+ local status, _ = pcall(nvim, "input", ":call bogus_fn()<CR>")
+ local v_errnum = string.match(nvim("eval", "v:errmsg"), "E%d*:")
+ eq(true, status) -- nvim_input() did not fail.
+ eq("E117:", v_errnum) -- v:errmsg was updated.
+ end)
end)
- describe('strwidth', function()
+ describe('nvim_strwidth', function()
it('works', function()
eq(3, nvim('strwidth', 'abc'))
-- 6 + (neovim)
@@ -65,7 +343,7 @@ describe('vim_* functions', function()
end)
end)
- describe('{get,set}_current_line', function()
+ describe('nvim_get_current_line, nvim_set_current_line', function()
it('works', function()
eq('', nvim('get_current_line'))
nvim('set_current_line', 'abc')
@@ -73,29 +351,45 @@ describe('vim_* functions', function()
end)
end)
- describe('{get,set,del}_var', function()
- it('works', function()
+ describe('set/get/del variables', function()
+ it('nvim_get_var, nvim_set_var, nvim_del_var', function()
nvim('set_var', 'lua', {1, 2, {['3'] = 1}})
eq({1, 2, {['3'] = 1}}, nvim('get_var', 'lua'))
eq({1, 2, {['3'] = 1}}, nvim('eval', 'g:lua'))
eq(1, funcs.exists('g:lua'))
meths.del_var('lua')
eq(0, funcs.exists('g:lua'))
+ eq({false, "Key not found: lua"}, meth_pcall(meths.del_var, 'lua'))
+ meths.set_var('lua', 1)
+
+ -- Set locked g: var.
+ command('lockvar lua')
+ eq({false, 'Key is locked: lua'}, meth_pcall(meths.del_var, 'lua'))
+ eq({false, 'Key is locked: lua'}, meth_pcall(meths.set_var, 'lua', 1))
end)
- it('set_var returns the old value', function()
+ it('nvim_get_vvar, nvim_set_vvar', function()
+ -- Set readonly v: var.
+ expect_err('Key is read%-only: count$', request,
+ 'nvim_set_vvar', 'count', 42)
+ -- Set writable v: var.
+ meths.set_vvar('errmsg', 'set by API')
+ eq('set by API', meths.get_vvar('errmsg'))
+ end)
+
+ it('vim_set_var returns the old value', function()
local val1 = {1, 2, {['3'] = 1}}
local val2 = {4, 7}
- eq(NIL, nvim('set_var', 'lua', val1))
- eq(val1, nvim('set_var', 'lua', val2))
+ eq(NIL, request('vim_set_var', 'lua', val1))
+ eq(val1, request('vim_set_var', 'lua', val2))
end)
- it('del_var returns the old value', function()
+ it('vim_del_var returns the old value', function()
local val1 = {1, 2, {['3'] = 1}}
local val2 = {4, 7}
- eq(NIL, meths.set_var('lua', val1))
- eq(val1, meths.set_var('lua', val2))
- eq(val2, meths.del_var('lua'))
+ eq(NIL, request('vim_set_var', 'lua', val1))
+ eq(val1, request('vim_set_var', 'lua', val2))
+ eq(val2, request('vim_del_var', 'lua'))
end)
it('truncates values with NULs in them', function()
@@ -104,59 +398,260 @@ describe('vim_* functions', function()
end)
end)
- describe('{get,set}_option', function()
+ describe('nvim_get_option, nvim_set_option', function()
it('works', function()
ok(nvim('get_option', 'equalalways'))
nvim('set_option', 'equalalways', false)
ok(not nvim('get_option', 'equalalways'))
end)
+
+ it('works to get global value of local options', function()
+ eq(false, nvim('get_option', 'lisp'))
+ eq(8, nvim('get_option', 'shiftwidth'))
+ end)
+
+ it('works to set global value of local options', function()
+ nvim('set_option', 'lisp', true)
+ eq(true, nvim('get_option', 'lisp'))
+ eq(false, helpers.curbuf('get_option', 'lisp'))
+ eq(nil, nvim('command_output', 'setglobal lisp?'):match('nolisp'))
+ eq('nolisp', nvim('command_output', 'setlocal lisp?'):match('nolisp'))
+ nvim('set_option', 'shiftwidth', 20)
+ eq('20', nvim('command_output', 'setglobal shiftwidth?'):match('%d+'))
+ eq('8', nvim('command_output', 'setlocal shiftwidth?'):match('%d+'))
+ end)
+
+ it('most window-local options have no global value', function()
+ local status, err = pcall(nvim, 'get_option', 'foldcolumn')
+ eq(false, status)
+ ok(err:match('Invalid option name') ~= nil)
+ end)
+
+ it('updates where the option was last set from', function()
+ nvim('set_option', 'equalalways', false)
+ local status, rv = pcall(nvim, 'command_output',
+ 'verbose set equalalways?')
+ eq(true, status)
+ ok(nil ~= string.find(rv, 'noequalalways\n'..
+ '\tLast set from API client %(channel id %d+%)'))
+
+ nvim('execute_lua', 'vim.api.nvim_set_option("equalalways", true)', {})
+ status, rv = pcall(nvim, 'command_output',
+ 'verbose set equalalways?')
+ eq(true, status)
+ eq(' equalalways\n\tLast set from Lua', rv)
+ end)
end)
- describe('{get,set}_current_buffer and get_buffers', function()
+ describe('nvim_{get,set}_current_buf, nvim_list_bufs', function()
it('works', function()
- eq(1, #nvim('get_buffers'))
- eq(nvim('get_buffers')[1], nvim('get_current_buffer'))
+ eq(1, #nvim('list_bufs'))
+ eq(nvim('list_bufs')[1], nvim('get_current_buf'))
nvim('command', 'new')
- eq(2, #nvim('get_buffers'))
- eq(nvim('get_buffers')[2], nvim('get_current_buffer'))
- nvim('set_current_buffer', nvim('get_buffers')[1])
- eq(nvim('get_buffers')[1], nvim('get_current_buffer'))
+ eq(2, #nvim('list_bufs'))
+ eq(nvim('list_bufs')[2], nvim('get_current_buf'))
+ nvim('set_current_buf', nvim('list_bufs')[1])
+ eq(nvim('list_bufs')[1], nvim('get_current_buf'))
end)
end)
- describe('{get,set}_current_window and get_windows', function()
+ describe('nvim_{get,set}_current_win, nvim_list_wins', function()
it('works', function()
- eq(1, #nvim('get_windows'))
- eq(nvim('get_windows')[1], nvim('get_current_window'))
+ eq(1, #nvim('list_wins'))
+ eq(nvim('list_wins')[1], nvim('get_current_win'))
nvim('command', 'vsplit')
nvim('command', 'split')
- eq(3, #nvim('get_windows'))
- eq(nvim('get_windows')[1], nvim('get_current_window'))
- nvim('set_current_window', nvim('get_windows')[2])
- eq(nvim('get_windows')[2], nvim('get_current_window'))
+ eq(3, #nvim('list_wins'))
+ eq(nvim('list_wins')[1], nvim('get_current_win'))
+ nvim('set_current_win', nvim('list_wins')[2])
+ eq(nvim('list_wins')[2], nvim('get_current_win'))
end)
end)
- describe('{get,set}_current_tabpage and get_tabpages', function()
+ describe('nvim_{get,set}_current_tabpage, nvim_list_tabpages', function()
it('works', function()
- eq(1, #nvim('get_tabpages'))
- eq(nvim('get_tabpages')[1], nvim('get_current_tabpage'))
+ eq(1, #nvim('list_tabpages'))
+ eq(nvim('list_tabpages')[1], nvim('get_current_tabpage'))
nvim('command', 'tabnew')
- eq(2, #nvim('get_tabpages'))
- eq(2, #nvim('get_windows'))
- eq(nvim('get_windows')[2], nvim('get_current_window'))
- eq(nvim('get_tabpages')[2], nvim('get_current_tabpage'))
- nvim('set_current_window', nvim('get_windows')[1])
+ eq(2, #nvim('list_tabpages'))
+ eq(2, #nvim('list_wins'))
+ eq(nvim('list_wins')[2], nvim('get_current_win'))
+ eq(nvim('list_tabpages')[2], nvim('get_current_tabpage'))
+ nvim('set_current_win', nvim('list_wins')[1])
-- Switching window also switches tabpages if necessary
- eq(nvim('get_tabpages')[1], nvim('get_current_tabpage'))
- eq(nvim('get_windows')[1], nvim('get_current_window'))
- nvim('set_current_tabpage', nvim('get_tabpages')[2])
- eq(nvim('get_tabpages')[2], nvim('get_current_tabpage'))
- eq(nvim('get_windows')[2], nvim('get_current_window'))
+ eq(nvim('list_tabpages')[1], nvim('get_current_tabpage'))
+ eq(nvim('list_wins')[1], nvim('get_current_win'))
+ nvim('set_current_tabpage', nvim('list_tabpages')[2])
+ eq(nvim('list_tabpages')[2], nvim('get_current_tabpage'))
+ eq(nvim('list_wins')[2], nvim('get_current_win'))
end)
end)
- describe('replace_termcodes', function()
+ describe('nvim_get_mode', function()
+ it("during normal-mode `g` returns blocking=true", function()
+ nvim("input", "o") -- add a line
+ eq({mode='i', blocking=false}, nvim("get_mode"))
+ nvim("input", [[<C-\><C-N>]])
+ eq(2, nvim("eval", "line('.')"))
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+
+ nvim("input", "g")
+ eq({mode='n', blocking=true}, nvim("get_mode"))
+
+ nvim("input", "k") -- complete the operator
+ eq(1, nvim("eval", "line('.')")) -- verify the completed operator
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+
+ it("returns the correct result multiple consecutive times", function()
+ for _ = 1,5 do
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end
+ nvim("input", "g")
+ for _ = 1,4 do
+ eq({mode='n', blocking=true}, nvim("get_mode"))
+ end
+ nvim("input", "g")
+ for _ = 1,7 do
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end
+ end)
+
+ it("during normal-mode CTRL-W, returns blocking=true", function()
+ nvim("input", "<C-W>")
+ eq({mode='n', blocking=true}, nvim("get_mode"))
+
+ nvim("input", "s") -- complete the operator
+ eq(2, nvim("eval", "winnr('$')")) -- verify the completed operator
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+
+ it("during press-enter prompt returns blocking=true", function()
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ command("echom 'msg1'")
+ command("echom 'msg2'")
+ command("echom 'msg3'")
+ command("echom 'msg4'")
+ command("echom 'msg5'")
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ nvim("input", ":messages<CR>")
+ eq({mode='r', blocking=true}, nvim("get_mode"))
+ end)
+
+ it("during getchar() returns blocking=false", function()
+ nvim("input", ":let g:test_input = nr2char(getchar())<CR>")
+ -- Events are enabled during getchar(), RPC calls are *not* blocked. #5384
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ eq(0, nvim("eval", "exists('g:test_input')"))
+ nvim("input", "J")
+ eq("J", nvim("eval", "g:test_input"))
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ end)
+
+ -- TODO: bug #6247#issuecomment-286403810
+ it("batched with input", function()
+ eq({mode='n', blocking=false}, nvim("get_mode"))
+ command("echom 'msg1'")
+ command("echom 'msg2'")
+ command("echom 'msg3'")
+ command("echom 'msg4'")
+ command("echom 'msg5'")
+
+ local req = {
+ {'nvim_get_mode', {}},
+ {'nvim_input', {':messages<CR>'}},
+ {'nvim_get_mode', {}},
+ {'nvim_eval', {'1'}},
+ }
+ eq({ { {mode='n', blocking=false},
+ 13,
+ {mode='n', blocking=false}, -- TODO: should be blocked=true ?
+ 1 },
+ NIL}, meths.call_atomic(req))
+ eq({mode='r', blocking=true}, nvim("get_mode"))
+ end)
+ it("during insert-mode map-pending, returns blocking=true #6166", function()
+ command("inoremap xx foo")
+ nvim("input", "ix")
+ eq({mode='i', blocking=true}, nvim("get_mode"))
+ end)
+ it("during normal-mode gU, returns blocking=false #6166", function()
+ nvim("input", "gu")
+ eq({mode='no', blocking=false}, nvim("get_mode"))
+ end)
+ end)
+
+ describe('RPC (K_EVENT) #6166', function()
+ it('does not complete ("interrupt") normal-mode operator-pending', function()
+ helpers.insert([[
+ FIRST LINE
+ SECOND LINE]])
+ nvim('input', 'gg')
+ nvim('input', 'gu')
+ -- Make any RPC request (can be non-async: op-pending does not block).
+ nvim('get_current_buf')
+ -- Buffer should not change.
+ helpers.expect([[
+ FIRST LINE
+ SECOND LINE]])
+ -- Now send input to complete the operator.
+ nvim('input', 'j')
+ helpers.expect([[
+ first line
+ second line]])
+ end)
+
+ it('does not complete ("interrupt") `d` #3732', function()
+ local screen = Screen.new(20, 4)
+ screen:attach()
+ command('set listchars=eol:$')
+ command('set list')
+ feed('ia<cr>b<cr>c<cr><Esc>kkk')
+ feed('d')
+ -- Make any RPC request (can be non-async: op-pending does not block).
+ nvim('get_current_buf')
+ screen:expect([[
+ ^a$ |
+ b$ |
+ c$ |
+ |
+ ]])
+ end)
+
+ it('does not complete ("interrupt") normal-mode map-pending', function()
+ command("nnoremap dd :let g:foo='it worked...'<CR>")
+ helpers.insert([[
+ FIRST LINE
+ SECOND LINE]])
+ nvim('input', 'gg')
+ nvim('input', 'd')
+ -- Make any RPC request (must be async, because map-pending blocks).
+ nvim('get_api_info')
+ -- Send input to complete the mapping.
+ nvim('input', 'd')
+ helpers.expect([[
+ FIRST LINE
+ SECOND LINE]])
+ eq('it worked...', helpers.eval('g:foo'))
+ end)
+ it('does not complete ("interrupt") insert-mode map-pending', function()
+ command('inoremap xx foo')
+ command('set timeoutlen=9999')
+ helpers.insert([[
+ FIRST LINE
+ SECOND LINE]])
+ nvim('input', 'ix')
+ -- Make any RPC request (must be async, because map-pending blocks).
+ nvim('get_api_info')
+ -- Send input to complete the mapping.
+ nvim('input', 'x')
+ helpers.expect([[
+ FIRST LINE
+ SECOND LINfooE]])
+ end)
+ end)
+
+ describe('nvim_replace_termcodes', function()
it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function()
eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true))
end)
@@ -176,21 +671,42 @@ describe('vim_* functions', function()
eq('\128\253\44', helpers.nvim('replace_termcodes',
'<LeftMouse>', true, true, true))
end)
+
+ it('converts keycodes', function()
+ eq('\nx\27x\rx<x', helpers.nvim('replace_termcodes',
+ '<NL>x<Esc>x<CR>x<lt>x', true, true, true))
+ end)
+
+ it('does not convert keycodes if special=false', function()
+ eq('<NL>x<Esc>x<CR>x<lt>x', helpers.nvim('replace_termcodes',
+ '<NL>x<Esc>x<CR>x<lt>x', true, true, false))
+ end)
+
+ it('does not crash when transforming an empty string', function()
+ -- Actually does not test anything, because current code will use NULL for
+ -- an empty string.
+ --
+ -- Problem here is that if String argument has .data in allocated memory
+ -- then `return str` in vim_replace_termcodes body will make Neovim free
+ -- `str.data` twice: once when freeing arguments, then when freeing return
+ -- value.
+ eq('', meths.replace_termcodes('', true, true, true))
+ end)
end)
- describe('feedkeys', function()
+ describe('nvim_feedkeys', function()
it('CSI escaping', function()
local function on_setup()
-- notice the special char(…) \xe2\80\xa6
nvim('feedkeys', ':let x1="…"\n', '', true)
- -- Both replace_termcodes and feedkeys escape \x80
+ -- Both nvim_replace_termcodes and nvim_feedkeys escape \x80
local inp = helpers.nvim('replace_termcodes', ':let x2="…"<CR>', true, true, true)
- nvim('feedkeys', inp, '', true)
+ nvim('feedkeys', inp, '', true) -- escape_csi=true
- -- Disabling CSI escaping in feedkeys
+ -- nvim_feedkeys with CSI escaping disabled
inp = helpers.nvim('replace_termcodes', ':let x3="…"<CR>', true, true, true)
- nvim('feedkeys', inp, '', false)
+ nvim('feedkeys', inp, '', false) -- escape_csi=false
helpers.stop()
end
@@ -205,7 +721,7 @@ describe('vim_* functions', function()
end)
end)
- describe('err_write', function()
+ describe('nvim_err_write', function()
local screen
before_each(function()
@@ -213,22 +729,23 @@ describe('vim_* functions', function()
screen = Screen.new(40, 8)
screen:attach()
screen:set_default_attr_ids({
+ [0] = {bold=true, foreground=Screen.colors.Blue},
[1] = {foreground = Screen.colors.White, background = Screen.colors.Red},
- [2] = {bold = true, foreground = Screen.colors.SeaGreen}
+ [2] = {bold = true, foreground = Screen.colors.SeaGreen},
+ [3] = {bold = true, reverse = true},
})
- screen:set_default_attr_ignore( {{bold=true, foreground=Screen.colors.Blue}} )
end)
it('can show one line', function()
nvim_async('err_write', 'has bork\n')
screen:expect([[
^ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
{1:has bork} |
]])
end)
@@ -236,11 +753,11 @@ describe('vim_* functions', function()
it('shows return prompt when more than &cmdheight lines', function()
nvim_async('err_write', 'something happened\nvery bad\n')
screen:expect([[
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
+ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {3: }|
{1:something happened} |
{1:very bad} |
{2:Press ENTER or type command to continue}^ |
@@ -250,9 +767,9 @@ describe('vim_* functions', function()
it('shows return prompt after all lines are shown', function()
nvim_async('err_write', 'FAILURE\nERROR\nEXCEPTION\nTRACEBACK\n')
screen:expect([[
- ~ |
- ~ |
- ~ |
+ |
+ {0:~ }|
+ {3: }|
{1:FAILURE} |
{1:ERROR} |
{1:EXCEPTION} |
@@ -267,12 +784,12 @@ describe('vim_* functions', function()
nvim_async('err_write', 'fail\n')
screen:expect([[
^ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
{1:very fail} |
]])
helpers.wait()
@@ -280,11 +797,11 @@ describe('vim_* functions', function()
-- shows up to &cmdheight lines
nvim_async('err_write', 'more fail\ntoo fail\n')
screen:expect([[
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
+ |
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {3: }|
{1:more fail} |
{1:too fail} |
{2:Press ENTER or type command to continue}^ |
@@ -293,9 +810,497 @@ describe('vim_* functions', function()
end)
end)
+ describe('nvim_list_chans and nvim_get_chan_info', function()
+ before_each(function()
+ command('autocmd ChanOpen * let g:opened_event = copy(v:event)')
+ command('autocmd ChanInfo * let g:info_event = copy(v:event)')
+ end)
+ local testinfo = {
+ stream = 'stdio',
+ id = 1,
+ mode = 'rpc',
+ client = {},
+ }
+ local stderr = {
+ stream = 'stderr',
+ id = 2,
+ mode = 'bytes',
+ }
+
+ it('returns {} for invalid channel', function()
+ eq({}, meths.get_chan_info(0))
+ eq({}, meths.get_chan_info(-1))
+ -- more preallocated numbers might be added, try something high
+ eq({}, meths.get_chan_info(10))
+ end)
+
+ it('works for stdio channel', function()
+ eq({[1]=testinfo,[2]=stderr}, meths.list_chans())
+ eq(testinfo, meths.get_chan_info(1))
+ eq(stderr, meths.get_chan_info(2))
+
+ meths.set_client_info("functionaltests",
+ {major=0, minor=3, patch=17},
+ 'ui',
+ {do_stuff={n_args={2,3}}},
+ {license= 'Apache2'})
+ local info = {
+ stream = 'stdio',
+ id = 1,
+ mode = 'rpc',
+ client = {
+ name='functionaltests',
+ version={major=0, minor=3, patch=17},
+ type='ui',
+ methods={do_stuff={n_args={2,3}}},
+ attributes={license='Apache2'},
+ },
+ }
+ eq({info=info}, meths.get_var("info_event"))
+ eq({[1]=info, [2]=stderr}, meths.list_chans())
+ eq(info, meths.get_chan_info(1))
+ end)
+
+ it('works for job channel', function()
+ eq(3, eval("jobstart(['cat'], {'rpc': v:true})"))
+ local info = {
+ stream='job',
+ id=3,
+ mode='rpc',
+ client={},
+ }
+ eq({info=info}, meths.get_var("opened_event"))
+ eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans())
+ eq(info, meths.get_chan_info(3))
+ eval('rpcrequest(3, "nvim_set_client_info", "cat", {}, "remote",'..
+ '{"nvim_command":{"n_args":1}},'.. -- and so on
+ '{"description":"The Amazing Cat"})')
+ info = {
+ stream='job',
+ id=3,
+ mode='rpc',
+ client = {
+ name='cat',
+ version={major=0},
+ type='remote',
+ methods={nvim_command={n_args=1}},
+ attributes={description="The Amazing Cat"},
+ },
+ }
+ eq({info=info}, meths.get_var("info_event"))
+ eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans())
+ end)
+
+ it('works for :terminal channel', function()
+ command(":terminal")
+ eq({id=1}, meths.get_current_buf())
+ eq(3, meths.buf_get_option(1, "channel"))
+
+ local info = {
+ stream='job',
+ id=3,
+ mode='terminal',
+ buffer = 1,
+ pty='?',
+ }
+ local event = meths.get_var("opened_event")
+ if not iswin() then
+ info.pty = event.info.pty
+ neq(nil, string.match(info.pty, "^/dev/"))
+ end
+ eq({info=info}, event)
+ info.buffer = {id=1}
+ eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans())
+ eq(info, meths.get_chan_info(3))
+ end)
+ end)
+
+ describe('nvim_call_atomic', function()
+ it('works', function()
+ meths.buf_set_lines(0, 0, -1, true, {'first'})
+ local req = {
+ {'nvim_get_current_line', {}},
+ {'nvim_set_current_line', {'second'}},
+ }
+ eq({{'first', NIL}, NIL}, meths.call_atomic(req))
+ eq({'second'}, meths.buf_get_lines(0, 0, -1, true))
+ end)
+
+ it('allows multiple return values', function()
+ local req = {
+ {'nvim_set_var', {'avar', true}},
+ {'nvim_set_var', {'bvar', 'string'}},
+ {'nvim_get_var', {'avar'}},
+ {'nvim_get_var', {'bvar'}},
+ }
+ eq({{NIL, NIL, true, 'string'}, NIL}, meths.call_atomic(req))
+ end)
+
+ it('is aborted by errors in call', function()
+ local error_types = meths.get_api_info()[2].error_types
+ local req = {
+ {'nvim_set_var', {'one', 1}},
+ {'nvim_buf_set_lines', {}},
+ {'nvim_set_var', {'two', 2}},
+ }
+ eq({{NIL}, {1, error_types.Exception.id,
+ 'Wrong number of arguments: expecting 5 but got 0'}},
+ meths.call_atomic(req))
+ eq(1, meths.get_var('one'))
+ eq(false, pcall(meths.get_var, 'two'))
+
+ -- still returns all previous successful calls
+ req = {
+ {'nvim_set_var', {'avar', 5}},
+ {'nvim_set_var', {'bvar', 'string'}},
+ {'nvim_get_var', {'avar'}},
+ {'nvim_buf_get_lines', {0, 10, 20, true}},
+ {'nvim_get_var', {'bvar'}},
+ }
+ eq({{NIL, NIL, 5}, {3, error_types.Validation.id, 'Index out of bounds'}},
+ meths.call_atomic(req))
+
+ req = {
+ {'i_am_not_a_method', {'xx'}},
+ {'nvim_set_var', {'avar', 10}},
+ }
+ eq({{}, {0, error_types.Exception.id, 'Invalid method: i_am_not_a_method'}},
+ meths.call_atomic(req))
+ eq(5, meths.get_var('avar'))
+ end)
+
+ it('throws error on malformed arguments', function()
+ local req = {
+ {'nvim_set_var', {'avar', 1}},
+ {'nvim_set_var'},
+ {'nvim_set_var', {'avar', 2}},
+ }
+ local status, err = pcall(meths.call_atomic, req)
+ eq(false, status)
+ ok(err:match('Items in calls array must be arrays of size 2') ~= nil)
+ -- call before was done, but not after
+ eq(1, meths.get_var('avar'))
+
+ req = {
+ { 'nvim_set_var', { 'bvar', { 2, 3 } } },
+ 12,
+ }
+ status, err = pcall(meths.call_atomic, req)
+ eq(false, status)
+ ok(err:match('Items in calls array must be arrays') ~= nil)
+ eq({2,3}, meths.get_var('bvar'))
+
+ req = {
+ {'nvim_set_current_line', 'little line'},
+ {'nvim_set_var', {'avar', 3}},
+ }
+ status, err = pcall(meths.call_atomic, req)
+ eq(false, status)
+ ok(err:match('Args must be Array') ~= nil)
+ -- call before was done, but not after
+ eq(1, meths.get_var('avar'))
+ eq({''}, meths.buf_get_lines(0, 0, -1, true))
+ end)
+ end)
+
+ describe('nvim_list_runtime_paths', function()
+ it('returns nothing with empty &runtimepath', function()
+ meths.set_option('runtimepath', '')
+ eq({}, meths.list_runtime_paths())
+ end)
+ it('returns single runtimepath', function()
+ meths.set_option('runtimepath', 'a')
+ eq({'a'}, meths.list_runtime_paths())
+ end)
+ it('returns two runtimepaths', function()
+ meths.set_option('runtimepath', 'a,b')
+ eq({'a', 'b'}, meths.list_runtime_paths())
+ end)
+ it('returns empty strings when appropriate', function()
+ meths.set_option('runtimepath', 'a,,b')
+ eq({'a', '', 'b'}, meths.list_runtime_paths())
+ meths.set_option('runtimepath', ',a,b')
+ eq({'', 'a', 'b'}, meths.list_runtime_paths())
+ meths.set_option('runtimepath', 'a,b,')
+ eq({'a', 'b', ''}, meths.list_runtime_paths())
+ end)
+ it('truncates too long paths', function()
+ local long_path = ('/a'):rep(8192)
+ meths.set_option('runtimepath', long_path)
+ local paths_list = meths.list_runtime_paths()
+ neq({long_path}, paths_list)
+ eq({long_path:sub(1, #(paths_list[1]))}, paths_list)
+ end)
+ end)
+
it('can throw exceptions', function()
local status, err = pcall(nvim, 'get_option', 'invalid-option')
eq(false, status)
ok(err:match('Invalid option name') ~= nil)
end)
+
+ it('does not truncate error message <1 MB #5984', function()
+ local very_long_name = 'A'..('x'):rep(10000)..'Z'
+ local status, err = pcall(nvim, 'get_option', very_long_name)
+ eq(false, status)
+ eq(very_long_name, err:match('Ax+Z?'))
+ end)
+
+ it("does not leak memory on incorrect argument types", function()
+ local status, err = pcall(nvim, 'set_current_dir',{'not', 'a', 'dir'})
+ eq(false, status)
+ ok(err:match(': Wrong type for argument 1, expecting String') ~= nil)
+ end)
+
+ describe('nvim_parse_expression', function()
+ before_each(function()
+ meths.set_option('isident', '')
+ end)
+ local function simplify_east_api_node(line, east_api_node)
+ if east_api_node == NIL then
+ return nil
+ end
+ if east_api_node.children then
+ for k, v in pairs(east_api_node.children) do
+ east_api_node.children[k] = simplify_east_api_node(line, v)
+ end
+ end
+ local typ = east_api_node.type
+ if typ == 'Register' then
+ typ = typ .. ('(name=%s)'):format(
+ tostring(intchar2lua(east_api_node.name)))
+ east_api_node.name = nil
+ elseif typ == 'PlainIdentifier' then
+ typ = typ .. ('(scope=%s,ident=%s)'):format(
+ tostring(intchar2lua(east_api_node.scope)), east_api_node.ident)
+ east_api_node.scope = nil
+ east_api_node.ident = nil
+ elseif typ == 'PlainKey' then
+ typ = typ .. ('(key=%s)'):format(east_api_node.ident)
+ east_api_node.ident = nil
+ elseif typ == 'Comparison' then
+ typ = typ .. ('(type=%s,inv=%u,ccs=%s)'):format(
+ east_api_node.cmp_type, east_api_node.invert and 1 or 0,
+ east_api_node.ccs_strategy)
+ east_api_node.ccs_strategy = nil
+ east_api_node.cmp_type = nil
+ east_api_node.invert = nil
+ elseif typ == 'Integer' then
+ typ = typ .. ('(val=%u)'):format(east_api_node.ivalue)
+ east_api_node.ivalue = nil
+ elseif typ == 'Float' then
+ typ = typ .. format_string('(val=%e)', east_api_node.fvalue)
+ east_api_node.fvalue = nil
+ elseif typ == 'SingleQuotedString' or typ == 'DoubleQuotedString' then
+ typ = format_string('%s(val=%q)', typ, east_api_node.svalue)
+ east_api_node.svalue = nil
+ elseif typ == 'Option' then
+ typ = ('%s(scope=%s,ident=%s)'):format(
+ typ,
+ tostring(intchar2lua(east_api_node.scope)),
+ east_api_node.ident)
+ east_api_node.ident = nil
+ east_api_node.scope = nil
+ elseif typ == 'Environment' then
+ typ = ('%s(ident=%s)'):format(typ, east_api_node.ident)
+ east_api_node.ident = nil
+ elseif typ == 'Assignment' then
+ local aug = east_api_node.augmentation
+ if aug == '' then aug = 'Plain' end
+ typ = ('%s(%s)'):format(typ, aug)
+ east_api_node.augmentation = nil
+ end
+ typ = ('%s:%u:%u:%s'):format(
+ typ, east_api_node.start[1], east_api_node.start[2],
+ line:sub(east_api_node.start[2] + 1,
+ east_api_node.start[2] + 1 + east_api_node.len - 1))
+ assert(east_api_node.start[2] + east_api_node.len - 1 <= #line)
+ for k, _ in pairs(east_api_node.start) do
+ assert(({true, true})[k])
+ end
+ east_api_node.start = nil
+ east_api_node.type = nil
+ east_api_node.len = nil
+ local can_simplify = true
+ for _, _ in pairs(east_api_node) do
+ if can_simplify then can_simplify = false end
+ end
+ if can_simplify then
+ return typ
+ else
+ east_api_node[1] = typ
+ return east_api_node
+ end
+ end
+ local function simplify_east_api(line, east_api)
+ if east_api.error then
+ east_api.err = east_api.error
+ east_api.error = nil
+ east_api.err.msg = east_api.err.message
+ east_api.err.message = nil
+ end
+ if east_api.ast then
+ east_api.ast = {simplify_east_api_node(line, east_api.ast)}
+ if #east_api.ast == 0 then
+ east_api.ast = nil
+ end
+ end
+ if east_api.len == #line then
+ east_api.len = nil
+ end
+ return east_api
+ end
+ local function simplify_east_hl(line, east_hl)
+ for i, v in ipairs(east_hl) do
+ east_hl[i] = ('%s:%u:%u:%s'):format(
+ v[4],
+ v[1],
+ v[2],
+ line:sub(v[2] + 1, v[3]))
+ end
+ return east_hl
+ end
+ local FLAGS_TO_STR = {
+ [0] = "",
+ [1] = "m",
+ [2] = "E",
+ [3] = "mE",
+ [4] = "l",
+ [5] = "lm",
+ [6] = "lE",
+ [7] = "lmE",
+ }
+ local function _check_parsing(opts, str, exp_ast, exp_highlighting_fs,
+ nz_flags_exps)
+ if type(str) ~= 'string' then
+ return
+ end
+ local zflags = opts.flags[1]
+ nz_flags_exps = nz_flags_exps or {}
+ for _, flags in ipairs(opts.flags) do
+ local err, msg = pcall(function()
+ local east_api = meths.parse_expression(str, FLAGS_TO_STR[flags], true)
+ local east_hl = east_api.highlight
+ east_api.highlight = nil
+ local ast = simplify_east_api(str, east_api)
+ local hls = simplify_east_hl(str, east_hl)
+ local exps = {
+ ast = exp_ast,
+ hl_fs = exp_highlighting_fs,
+ }
+ local add_exps = nz_flags_exps[flags]
+ if not add_exps and flags == 3 + zflags then
+ add_exps = nz_flags_exps[1 + zflags] or nz_flags_exps[2 + zflags]
+ end
+ if add_exps then
+ if add_exps.ast then
+ exps.ast = mergedicts_copy(exps.ast, add_exps.ast)
+ end
+ if add_exps.hl_fs then
+ exps.hl_fs = mergedicts_copy(exps.hl_fs, add_exps.hl_fs)
+ end
+ end
+ eq(exps.ast, ast)
+ if exp_highlighting_fs then
+ local exp_highlighting = {}
+ local next_col = 0
+ for i, h in ipairs(exps.hl_fs) do
+ exp_highlighting[i], next_col = h(next_col)
+ end
+ eq(exp_highlighting, hls)
+ end
+ end)
+ if not err then
+ if type(msg) == 'table' then
+ local merr, new_msg = pcall(
+ format_string, 'table error:\n%s\n\n(%r)', msg.message, msg)
+ if merr then
+ msg = new_msg
+ else
+ msg = format_string('table error without .message:\n(%r)',
+ msg)
+ end
+ elseif type(msg) ~= 'string' then
+ msg = format_string('non-string non-table error:\n%r', msg)
+ end
+ error(format_string('Error while processing test (%r, %s):\n%s',
+ str, FLAGS_TO_STR[flags], msg))
+ end
+ end
+ end
+ local function hl(group, str, shift)
+ return function(next_col)
+ local col = next_col + (shift or 0)
+ return (('%s:%u:%u:%s'):format(
+ 'Nvim' .. group,
+ 0,
+ col,
+ str)), (col + #str)
+ end
+ end
+ local function fmtn(typ, args, rest)
+ if (typ == 'UnknownFigure'
+ or typ == 'DictLiteral'
+ or typ == 'CurlyBracesIdentifier'
+ or typ == 'Lambda') then
+ return ('%s%s'):format(typ, rest)
+ elseif typ == 'DoubleQuotedString' or typ == 'SingleQuotedString' then
+ if args:sub(-4) == 'NULL' then
+ args = args:sub(1, -5) .. '""'
+ end
+ return ('%s(%s)%s'):format(typ, args, rest)
+ end
+ end
+ assert:set_parameter('TableFormatLevel', 1000000)
+ require('test.unit.viml.expressions.parser_tests')(
+ it, _check_parsing, hl, fmtn)
+ end)
+
+ describe('nvim_list_uis', function()
+ it('returns empty if --headless', function()
+ -- Test runner defaults to --headless.
+ eq({}, nvim("list_uis"))
+ end)
+ it('returns attached UIs', function()
+ local screen = Screen.new(20, 4)
+ screen:attach()
+ local expected = {
+ {
+ chan = 1,
+ ext_cmdline = false,
+ ext_popupmenu = false,
+ ext_tabline = false,
+ ext_wildmenu = false,
+ ext_linegrid = screen._options.ext_linegrid or false,
+ ext_multigrid = false,
+ ext_hlstate=false,
+ height = 4,
+ rgb = true,
+ width = 20,
+ }
+ }
+ eq(expected, nvim("list_uis"))
+
+ screen:detach()
+ screen = Screen.new(44, 99)
+ screen:attach({ rgb = false })
+ expected[1].rgb = false
+ expected[1].width = 44
+ expected[1].height = 99
+ eq(expected, nvim("list_uis"))
+ end)
+ end)
+
+ describe('nvim_create_namespace', function()
+ it('works', function()
+ eq({}, meths.get_namespaces())
+ eq(1, meths.create_namespace("ns-1"))
+ eq(2, meths.create_namespace("ns-2"))
+ eq(1, meths.create_namespace("ns-1"))
+ eq({["ns-1"]=1, ["ns-2"]=2}, meths.get_namespaces())
+ eq(3, meths.create_namespace(""))
+ eq(4, meths.create_namespace(""))
+ eq({["ns-1"]=1, ["ns-2"]=2}, meths.get_namespaces())
+ end)
+ end)
end)
diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua
index d90323181c..4496e1f644 100644
--- a/test/functional/api/window_spec.lua
+++ b/test/functional/api/window_spec.lua
@@ -1,4 +1,3 @@
--- Sanity checks for window_* API calls via msgpack-rpc
local helpers = require('test.functional.helpers')(after_each)
local clear, nvim, curbuf, curbuf_contents, window, curwin, eq, neq,
ok, feed, insert, eval = helpers.clear, helpers.nvim, helpers.curbuf,
@@ -7,6 +6,12 @@ local clear, nvim, curbuf, curbuf_contents, window, curwin, eq, neq,
local wait = helpers.wait
local curwinmeths = helpers.curwinmeths
local funcs = helpers.funcs
+local request = helpers.request
+local NIL = helpers.NIL
+local meth_pcall = helpers.meth_pcall
+local meths = helpers.meths
+local command = helpers.command
+local expect_err = helpers.expect_err
-- check if str is visible at the beginning of some line
local function is_visible(str)
@@ -27,17 +32,32 @@ local function is_visible(str)
return false
end
-describe('window_* functions', function()
+describe('API/win', function()
before_each(clear)
- describe('get_buffer', function()
+ describe('get_buf', function()
it('works', function()
- eq(curbuf(), window('get_buffer', nvim('get_windows')[1]))
+ eq(curbuf(), window('get_buf', nvim('list_wins')[1]))
nvim('command', 'new')
- nvim('set_current_window', nvim('get_windows')[2])
- eq(curbuf(), window('get_buffer', nvim('get_windows')[2]))
- neq(window('get_buffer', nvim('get_windows')[1]),
- window('get_buffer', nvim('get_windows')[2]))
+ nvim('set_current_win', nvim('list_wins')[2])
+ eq(curbuf(), window('get_buf', nvim('list_wins')[2]))
+ neq(window('get_buf', nvim('list_wins')[1]),
+ window('get_buf', nvim('list_wins')[2]))
+ end)
+ end)
+
+ describe('set_buf', function()
+ it('works', function()
+ nvim('command', 'new')
+ local windows = nvim('list_wins')
+ neq(window('get_buf', windows[2]), window('get_buf', windows[1]))
+ window('set_buf', windows[2], window('get_buf', windows[1]))
+ eq(window('get_buf', windows[2]), window('get_buf', windows[1]))
+ end)
+
+ it('validates args', function()
+ expect_err('Invalid buffer id$', window, 'set_buf', nvim('get_current_win'), 23)
+ expect_err('Invalid window id$', window, 'set_buf', 23, nvim('get_current_buf'))
end)
end)
@@ -52,6 +72,12 @@ describe('window_* functions', function()
eq('typing\n some dumb text', curbuf_contents())
end)
+ it('does not leak memory when using invalid window ID with invalid pos',
+ function()
+ eq({false, 'Invalid window id'},
+ meth_pcall(meths.win_set_cursor, 1, {"b\na"}))
+ end)
+
it('updates the screen, and also when the window is unfocused', function()
insert("prologue")
feed('100o<esc>')
@@ -98,33 +124,56 @@ describe('window_* functions', function()
neq(win, curwin())
end)
+ it('remembers what column it wants to be in', function()
+ insert("first line")
+ feed('o<esc>')
+ insert("second line")
+
+ feed('gg')
+ wait() -- let nvim process the 'gg' command
+
+ -- cursor position is at beginning
+ local win = curwin()
+ eq({1, 0}, window('get_cursor', win))
+
+ -- move cursor to column 5
+ window('set_cursor', win, {1, 5})
+
+ -- move down a line
+ feed('j')
+ wait() -- let nvim process the 'j' command
+
+ -- cursor is still in column 5
+ eq({2, 5}, window('get_cursor', win))
+ end)
+
end)
describe('{get,set}_height', function()
it('works', function()
nvim('command', 'vsplit')
- eq(window('get_height', nvim('get_windows')[2]),
- window('get_height', nvim('get_windows')[1]))
- nvim('set_current_window', nvim('get_windows')[2])
+ eq(window('get_height', nvim('list_wins')[2]),
+ window('get_height', nvim('list_wins')[1]))
+ nvim('set_current_win', nvim('list_wins')[2])
nvim('command', 'split')
- eq(window('get_height', nvim('get_windows')[2]),
- math.floor(window('get_height', nvim('get_windows')[1]) / 2))
- window('set_height', nvim('get_windows')[2], 2)
- eq(2, window('get_height', nvim('get_windows')[2]))
+ eq(window('get_height', nvim('list_wins')[2]),
+ math.floor(window('get_height', nvim('list_wins')[1]) / 2))
+ window('set_height', nvim('list_wins')[2], 2)
+ eq(2, window('get_height', nvim('list_wins')[2]))
end)
end)
describe('{get,set}_width', function()
it('works', function()
nvim('command', 'split')
- eq(window('get_width', nvim('get_windows')[2]),
- window('get_width', nvim('get_windows')[1]))
- nvim('set_current_window', nvim('get_windows')[2])
+ eq(window('get_width', nvim('list_wins')[2]),
+ window('get_width', nvim('list_wins')[1]))
+ nvim('set_current_win', nvim('list_wins')[2])
nvim('command', 'vsplit')
- eq(window('get_width', nvim('get_windows')[2]),
- math.floor(window('get_width', nvim('get_windows')[1]) / 2))
- window('set_width', nvim('get_windows')[2], 2)
- eq(2, window('get_width', nvim('get_windows')[2]))
+ eq(window('get_width', nvim('list_wins')[2]),
+ math.floor(window('get_width', nvim('list_wins')[1]) / 2))
+ window('set_width', nvim('list_wins')[2], 2)
+ eq(2, window('get_width', nvim('list_wins')[2]))
end)
end)
@@ -136,6 +185,26 @@ describe('window_* functions', function()
eq(1, funcs.exists('w:lua'))
curwinmeths.del_var('lua')
eq(0, funcs.exists('w:lua'))
+ eq({false, 'Key not found: lua'}, meth_pcall(curwinmeths.del_var, 'lua'))
+ curwinmeths.set_var('lua', 1)
+ command('lockvar w:lua')
+ eq({false, 'Key is locked: lua'}, meth_pcall(curwinmeths.del_var, 'lua'))
+ eq({false, 'Key is locked: lua'}, meth_pcall(curwinmeths.set_var, 'lua', 1))
+ end)
+
+ it('window_set_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('window_set_var', 0, 'lua', val1))
+ eq(val1, request('window_set_var', 0, 'lua', val2))
+ end)
+
+ it('window_del_var returns the old value', function()
+ local val1 = {1, 2, {['3'] = 1}}
+ local val2 = {4, 7}
+ eq(NIL, request('window_set_var', 0, 'lua', val1))
+ eq(val1, request('window_set_var', 0, 'lua', val2))
+ eq(val2, request('window_del_var', 0, 'lua'))
end)
end)
@@ -152,17 +221,17 @@ describe('window_* functions', function()
describe('get_position', function()
it('works', function()
- local height = window('get_height', nvim('get_windows')[1])
- local width = window('get_width', nvim('get_windows')[1])
+ local height = window('get_height', nvim('list_wins')[1])
+ local width = window('get_width', nvim('list_wins')[1])
nvim('command', 'split')
nvim('command', 'vsplit')
- eq({0, 0}, window('get_position', nvim('get_windows')[1]))
+ eq({0, 0}, window('get_position', nvim('list_wins')[1]))
local vsplit_pos = math.floor(width / 2)
local split_pos = math.floor(height / 2)
local win2row, win2col =
- unpack(window('get_position', nvim('get_windows')[2]))
+ unpack(window('get_position', nvim('list_wins')[2]))
local win3row, win3col =
- unpack(window('get_position', nvim('get_windows')[3]))
+ unpack(window('get_position', nvim('list_wins')[3]))
eq(0, win2row)
eq(0, win3col)
ok(vsplit_pos - 1 <= win2col and win2col <= vsplit_pos + 1)
@@ -175,19 +244,43 @@ describe('window_* functions', function()
nvim('command', 'tabnew')
nvim('command', 'vsplit')
eq(window('get_tabpage',
- nvim('get_windows')[1]), nvim('get_tabpages')[1])
+ nvim('list_wins')[1]), nvim('list_tabpages')[1])
eq(window('get_tabpage',
- nvim('get_windows')[2]), nvim('get_tabpages')[2])
+ nvim('list_wins')[2]), nvim('list_tabpages')[2])
eq(window('get_tabpage',
- nvim('get_windows')[3]), nvim('get_tabpages')[2])
+ nvim('list_wins')[3]), nvim('list_tabpages')[2])
+ end)
+ end)
+
+ describe('get_number', function()
+ it('works', function()
+ local wins = nvim('list_wins')
+ eq(1, window('get_number', wins[1]))
+
+ nvim('command', 'split')
+ local win1, win2 = unpack(nvim('list_wins'))
+ eq(1, window('get_number', win1))
+ eq(2, window('get_number', win2))
+
+ nvim('command', 'wincmd J')
+ eq(2, window('get_number', win1))
+ eq(1, window('get_number', win2))
+
+ nvim('command', 'tabnew')
+ local win3 = nvim('list_wins')[3]
+ -- First tab page
+ eq(2, window('get_number', win1))
+ eq(1, window('get_number', win2))
+ -- Second tab page
+ eq(1, window('get_number', win3))
end)
end)
describe('is_valid', function()
it('works', function()
nvim('command', 'split')
- local win = nvim('get_windows')[2]
- nvim('set_current_window', win)
+ local win = nvim('list_wins')[2]
+ nvim('set_current_win', win)
ok(window('is_valid', win))
nvim('command', 'close')
ok(not window('is_valid', win))