aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjörn Linse <bjorn.linse@gmail.com>2019-11-27 20:45:41 +0100
committerBjörn Linse <bjorn.linse@gmail.com>2020-01-01 19:26:29 +0100
commitea4127e9a7a624484f51c21e17f37c766da15da0 (patch)
treebcac8ad881805acd86a1d62d87741c5007f31d91
parenta251b588acb3a561e5da695280774b9b6fbcd0ea (diff)
downloadrneovim-ea4127e9a7a624484f51c21e17f37c766da15da0.tar.gz
rneovim-ea4127e9a7a624484f51c21e17f37c766da15da0.tar.bz2
rneovim-ea4127e9a7a624484f51c21e17f37c766da15da0.zip
lua: metatable for empty dict value
-rw-r--r--runtime/doc/lua.txt10
-rw-r--r--runtime/lua/vim/inspect.lua5
-rw-r--r--runtime/lua/vim/shared.lua15
-rw-r--r--src/nvim/lua/converter.c13
-rw-r--r--src/nvim/lua/executor.c13
-rw-r--r--src/nvim/lua/executor.h1
-rw-r--r--src/nvim/lua/vim.lua4
-rw-r--r--test/functional/lua/vim_spec.lua44
8 files changed, 101 insertions, 4 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 9601537c8d..d1f244c76f 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -717,6 +717,16 @@ vim.NIL *vim.NIL*
is equivalent to a missing value: `{"foo", nil}` is the same as
`{"foo"}`
+vim.empty_dict() *vim.empty_dict()*
+ Creates a special table which will be converted to an empty
+ dictionary when converting lua values to vimL or API types. The
+ table is empty, and this property is marked using a metatable. An
+ empty table `{}` without this metatable will default to convert to
+ an array/list.
+
+ Note: if numeric keys are added to the table, the metatable will be
+ ignored and the dict converted to a list/array anyway.
+
vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()*
Sends {event} to {channel} via |RPC| and returns immediately.
If {channel} is 0, the event is broadcast to all channels.
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index 0f3b908dc1..0448ea487f 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -244,6 +244,11 @@ function Inspector:putTable(t)
local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
local mt = getmetatable(t)
+ if (vim and sequenceLength == 0 and nonSequentialKeysLength == 0
+ and mt == vim._empty_dict_mt) then
+ self:puts(tostring(t))
+ return
+ end
self:puts('{')
self:down(function()
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index b5b04d7757..6df9bf1c2f 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -275,9 +275,15 @@ function vim.tbl_flatten(t)
end
-- Determine whether a Lua table can be treated as an array.
+--
+-- An empty table `{}` will default to being treated as an array.
+-- Use `vim.emtpy_dict()` to create a table treated as an
+-- empty dict. Empty tables returned by `rpcrequest()` and
+-- `vim.fn` functions can be checked using this function
+-- whether they represent empty API arrays and vimL lists.
---
--@params Table
---@returns true: A non-empty array, false: A non-empty table, nil: An empty table
+--@returns true: An array-like table, false: A dict-like or mixed table
function vim.tbl_islist(t)
if type(t) ~= 'table' then
return false
@@ -296,7 +302,12 @@ function vim.tbl_islist(t)
if count > 0 then
return true
else
- return nil
+ -- TODO(bfredl): in the future, we will always be inside nvim
+ -- then this check can be deleted.
+ if vim._empty_dict_mt == nil then
+ return nil
+ end
+ return getmetatable(t) ~= vim._empty_dict_mt
end
end
diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c
index 09d1a68898..fca74b5901 100644
--- a/src/nvim/lua/converter.c
+++ b/src/nvim/lua/converter.c
@@ -156,6 +156,13 @@ static LuaTableProps nlua_traverse_table(lua_State *const lstate)
&& other_keys_num == 0
&& ret.string_keys_num == 0)) {
ret.type = kObjectTypeArray;
+ if (tsize == 0 && lua_getmetatable(lstate, -1)) {
+ nlua_pushref(lstate, nlua_empty_dict_ref);
+ if (lua_rawequal(lstate, -2, -1)) {
+ ret.type = kObjectTypeDictionary;
+ }
+ lua_pop(lstate, 2);
+ }
} else if (ret.string_keys_num == tsize) {
ret.type = kObjectTypeDictionary;
} else {
@@ -465,6 +472,8 @@ static bool typval_conv_special = false;
nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \
} else { \
lua_createtable(lstate, 0, 0); \
+ nlua_pushref(lstate, nlua_empty_dict_ref); \
+ lua_setmetatable(lstate, -2); \
} \
} while (0)
@@ -695,6 +704,10 @@ void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict,
nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary);
} else {
lua_createtable(lstate, 0, (int)dict.size);
+ if (dict.size == 0 && !special) {
+ nlua_pushref(lstate, nlua_empty_dict_ref);
+ lua_setmetatable(lstate, -2);
+ }
}
for (size_t i = 0; i < dict.size; i++) {
nlua_push_String(lstate, dict.items[i].key, special);
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
index 2cd6c0db66..242d4e18d1 100644
--- a/src/nvim/lua/executor.c
+++ b/src/nvim/lua/executor.c
@@ -324,6 +324,13 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
nlua_nil_ref = nlua_ref(lstate, -1);
lua_setfield(lstate, -2, "NIL");
+ // vim._empty_dict_mt
+ lua_createtable(lstate, 0, 0);
+ lua_pushcfunction(lstate, &nlua_empty_dict_tostring);
+ lua_setfield(lstate, -2, "__tostring");
+ nlua_empty_dict_ref = nlua_ref(lstate, -1);
+ lua_setfield(lstate, -2, "_empty_dict_mt");
+
// internal vim._treesitter... API
nlua_add_treesitter(lstate);
@@ -665,6 +672,12 @@ static int nlua_nil_tostring(lua_State *lstate)
return 1;
}
+static int nlua_empty_dict_tostring(lua_State *lstate)
+{
+ lua_pushstring(lstate, "vim.empty_dict()");
+ return 1;
+}
+
#ifdef WIN32
/// os.getenv: override os.getenv to maintain coherency. #9681
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
index 32f66b629c..3259fc0fa1 100644
--- a/src/nvim/lua/executor.h
+++ b/src/nvim/lua/executor.h
@@ -13,6 +13,7 @@
void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL;
EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF);
+EXTERN LuaRef nlua_empty_dict_ref INIT(= LUA_NOREF);
#define set_api_error(s, err) \
do { \
diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua
index e7c5458102..8ba550ea31 100644
--- a/src/nvim/lua/vim.lua
+++ b/src/nvim/lua/vim.lua
@@ -243,6 +243,10 @@ function vim.schedule_wrap(cb)
end)
end
+function vim.empty_dict()
+ return setmetatable({}, vim._empty_dict_mt)
+end
+
-- vim.fn.{func}(...)
vim.fn = setmetatable({}, {
__index = function(t, key)
diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua
index 17ffcd8d86..e879f8b925 100644
--- a/test/functional/lua/vim_spec.lua
+++ b/test/functional/lua/vim_spec.lua
@@ -354,7 +354,8 @@ describe('lua stdlib', function()
end)
it('vim.tbl_islist', function()
- eq(NIL, exec_lua("return vim.tbl_islist({})"))
+ eq(true, exec_lua("return vim.tbl_islist({})"))
+ eq(false, exec_lua("return vim.tbl_islist(vim.empty_dict())"))
eq(true, exec_lua("return vim.tbl_islist({'a', 'b', 'c'})"))
eq(false, exec_lua("return vim.tbl_islist({'a', '32', a='hello', b='baz'})"))
eq(false, exec_lua("return vim.tbl_islist({1, a='hello', b='baz'})"))
@@ -458,6 +459,19 @@ describe('lua stdlib', function()
]]))
eq({3, 'aa', true, NIL}, exec_lua([[return ret]]))
+ eq({{}, {}, false, true}, exec_lua([[
+ vim.rpcrequest(chan, 'nvim_exec', 'let xx = {}\nlet yy = []', false)
+ local dict = vim.rpcrequest(chan, 'nvim_eval', 'xx')
+ local list = vim.rpcrequest(chan, 'nvim_eval', 'yy')
+ return {dict, list, vim.tbl_islist(dict), vim.tbl_islist(list)}
+ ]]))
+
+ exec_lua([[
+ vim.rpcrequest(chan, 'nvim_set_var', 'aa', {})
+ vim.rpcrequest(chan, 'nvim_set_var', 'bb', vim.empty_dict())
+ ]])
+ eq({1, 1}, eval('[type(g:aa) == type([]), type(g:bb) == type({})]'))
+
-- error handling
eq({false, 'Invalid channel: 23'},
exec_lua([[return {pcall(vim.rpcrequest, 23, 'foo')}]]))
@@ -486,7 +500,7 @@ describe('lua stdlib', function()
})
screen:attach()
exec_lua([[
- local timer = vim.loop.new_timer()
+ timer = vim.loop.new_timer()
timer:start(20, 0, function ()
-- notify ok (executed later when safe)
vim.rpcnotify(chan, 'nvim_set_var', 'yy', {3, vim.NIL})
@@ -505,6 +519,32 @@ describe('lua stdlib', function()
]]}
feed('<cr>')
eq({3, NIL}, meths.get_var('yy'))
+
+ exec_lua([[timer:close()]])
+ end)
+
+ it('vim.empty_dict()', function()
+ eq({true, false, true, true}, exec_lua([[
+ vim.api.nvim_set_var('listy', {})
+ vim.api.nvim_set_var('dicty', vim.empty_dict())
+ local listy = vim.fn.eval("listy")
+ local dicty = vim.fn.eval("dicty")
+ return {vim.tbl_islist(listy), vim.tbl_islist(dicty), next(listy) == nil, next(dicty) == nil}
+ ]]))
+
+ -- vim.empty_dict() gives new value each time
+ -- equality is not overriden (still by ref)
+ -- non-empty table uses the usual heuristics (ignores the tag)
+ eq({false, {"foo"}, {namey="bar"}}, exec_lua([[
+ local aa = vim.empty_dict()
+ local bb = vim.empty_dict()
+ local equally = (aa == bb)
+ aa[1] = "foo"
+ bb["namey"] = "bar"
+ return {equally, aa, bb}
+ ]]))
+
+ eq("{ {}, vim.empty_dict() }", exec_lua("return vim.inspect({{}, vim.empty_dict()})"))
end)
it('vim.validate', function()