api: nvim_get_mode()

Asynchronous API functions are served immediately, which means pending
input could change the state of Nvim shortly after an async API function
result is returned.

nvim_get_mode() is different:
  - If RPCs are known to be blocked, it responds immediately (without
    flushing the input/event queue)
  - else it is handled just-in-time before waiting for input, after
    pending input was processed. This makes the result more reliable
    (but not perfect).

Internally this is handled as a special case, but _semantically_ nothing
has changed: API users never know when input flushes, so this internal
special-case doesn't violate that. As far as API users are concerned,
nvim_get_mode() is just another asynchronous API function.

In all cases nvim_get_mode() never blocks for more than the time it
takes to flush the input/event queue (~µs).

Note: This doesn't address #6166; nvim_get_mode() will provoke #6166 if
e.g. `d` is operator-pending.

Closes #6159
This commit is contained in:
Justin M. Keyes
2017-03-13 15:02:37 +01:00
parent 7044aa6e82
commit 3ea1007753
16 changed files with 250 additions and 77 deletions

View File

@@ -9,6 +9,7 @@ local funcs = helpers.funcs
local request = helpers.request
local meth_pcall = helpers.meth_pcall
local command = helpers.command
local wait = helpers.wait
describe('api', function()
before_each(clear)
@@ -221,6 +222,109 @@ describe('api', function()
end)
end)
local function appendfile(fname, text)
local file = io.open(fname, 'a')
file:write(text)
file:flush()
file:close()
end
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)
-- TODO: bug #6166
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)
-- TODO: bug #6166
it("during normal-mode gU, returns blocking=false #6166", function()
nvim("input", "gu")
eq({mode='no', blocking=false}, nvim("get_mode"))
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))
@@ -459,7 +563,7 @@ describe('api', function()
eq(very_long_name, err:match('Ax+Z?'))
end)
it("doesn't leak memory on incorrect argument types", function()
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)