changeset 25503:9947b7e4b319 v8.2.3288

patch 8.2.3288: cannot easily access namespace dictionaries from Lua Commit: https://github.com/vim/vim/commit/9dc4bef897a37a610a28d69cee6d30749db61296 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Wed Aug 4 21:12:52 2021 +0200 patch 8.2.3288: cannot easily access namespace dictionaries from Lua Problem: Cannot easily access namespace dictionaries from Lua. Solution: Add vim.g, vim.b, etc. (Yegappan Lakshmanan, closes https://github.com/vim/vim/issues/8693, from NeoVim)
author Bram Moolenaar <Bram@vim.org>
date Wed, 04 Aug 2021 21:15:04 +0200
parents 4c2191b35282
children 3123cc59d283
files runtime/doc/if_lua.txt src/if_lua.c src/testdir/test_lua.vim src/version.c
diffstat 4 files changed, 414 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/if_lua.txt
+++ b/runtime/doc/if_lua.txt
@@ -211,6 +211,38 @@ Vim evaluation and command execution, an
 	vim.lua_version		The Lua version Vim was compiled with, in the
 				form {major}.{minor}.{patch}, e.g. "5.1.4".
 
+                                                        *lua-vim-variables*
+The Vim editor global dictionaries |g:| |w:| |b:| |t:| |v:| can be accessed
+from Lua conveniently and idiomatically by referencing the `vim.*` Lua tables
+described below. In this way you can easily read and modify global Vimscript
+variables from Lua.
+
+Example: >
+
+    vim.g.foo = 5     -- Set the g:foo Vimscript variable.
+    print(vim.g.foo)  -- Get and print the g:foo Vimscript variable.
+    vim.g.foo = nil   -- Delete (:unlet) the Vimscript variable.
+
+vim.g                                                   *vim.g*
+        Global (|g:|) editor variables.
+        Key with no value returns `nil`.
+
+vim.b                                                   *vim.b*
+        Buffer-scoped (|b:|) variables for the current buffer.
+        Invalid or unset key returns `nil`.
+
+vim.w                                                   *vim.w*
+        Window-scoped (|w:|) variables for the current window.
+        Invalid or unset key returns `nil`.
+
+vim.t                                                   *vim.t*
+        Tabpage-scoped (|t:|) variables for the current tabpage.
+        Invalid or unset key returns `nil`.
+
+vim.v                                                   *vim.v*
+        |v:| variables.
+        Invalid or unset key returns `nil`.
+
 ==============================================================================
 3. List userdata					*lua-list*
 
--- a/src/if_lua.c
+++ b/src/if_lua.c
@@ -1777,6 +1777,122 @@ luaV_debug(lua_State *L)
     }
 }
 
+    static dict_T *
+luaV_get_var_scope(lua_State *L)
+{
+    const char	*scope = luaL_checkstring(L, 1);
+    dict_T	*dict = NULL;
+
+    if (STRICMP((char *)scope, "g") == 0)
+	dict = get_globvar_dict();
+    else if (STRICMP((char *)scope, "v") == 0)
+	dict = get_vimvar_dict();
+    else if (STRICMP((char *)scope, "b") == 0)
+	dict = curbuf->b_vars;
+    else if (STRICMP((char *)scope, "w") == 0)
+	dict = curwin->w_vars;
+    else if (STRICMP((char *)scope, "t") == 0)
+	dict = curtab->tp_vars;
+    else
+    {
+	luaL_error(L, "invalid scope %s", scope);
+	return NULL;
+    }
+
+    return dict;
+}
+
+    static int
+luaV_setvar(lua_State *L)
+{
+    dict_T	*dict;
+    dictitem_T	*di;
+    size_t	len;
+    char	*name;
+    int		del;
+    char	*error = NULL;
+
+    name = (char *)luaL_checklstring(L, 3, &len);
+    del = (lua_gettop(L) < 4) || lua_isnil(L, 4);
+
+    dict = luaV_get_var_scope(L);
+    if (dict == NULL)
+	return 0;
+
+    di = dict_find(dict, (char_u *)name, len);
+    if (di != NULL)
+    {
+	if (di->di_flags & DI_FLAGS_RO)
+	    error = "variable is read-only";
+	else if (di->di_flags & DI_FLAGS_LOCK)
+	    error = "variable is locked";
+	else if (del && di->di_flags & DI_FLAGS_FIX)
+	    error = "variable is fixed";
+	if (error != NULL)
+	    return luaL_error(L, error);
+    }
+    else if (dict->dv_lock)
+	return luaL_error(L, "Dictionary is locked");
+
+    if (del)
+    {
+	// Delete the key
+	if (di == NULL)
+	    // Doesn't exist, nothing to do
+	    return 0;
+	else
+	    // Delete the entry
+	    dictitem_remove(dict, di);
+    }
+    else
+    {
+	// Update the key
+	typval_T	tv;
+
+	// Convert the lua value to a vimscript type in the temporary variable
+	lua_pushvalue(L, 4);
+	if (luaV_totypval(L, -1, &tv) == FAIL)
+	    return luaL_error(L, "Couldn't convert lua value");
+
+	if (di == NULL)
+	{
+	    // Need to create an entry
+	    di = dictitem_alloc((char_u *)name);
+	    if (di == NULL)
+		return 0;
+	    // Update the value
+	    copy_tv(&tv, &di->di_tv);
+	    dict_add(dict, di);
+	} else
+	{
+	    // Clear the old value
+	    clear_tv(&di->di_tv);
+	    // Update the value
+	    copy_tv(&tv, &di->di_tv);
+	}
+
+	// Clear the temporary variable
+	clear_tv(&tv);
+    }
+
+    return 0;
+}
+
+    static int
+luaV_getvar(lua_State *L)
+{
+    dict_T	*dict = luaV_get_var_scope(L);
+    size_t	len;
+    const char	*name = luaL_checklstring(L, 3, &len);
+
+    dictitem_T	*di = dict_find(dict, (char_u *)name, len);
+    if (di == NULL)
+	return 0;  // nil
+
+    luaV_pushtypval(L, &di->di_tv);
+    return 1;
+}
+
     static int
 luaV_command(lua_State *L)
 {
@@ -2103,6 +2219,8 @@ static const luaL_Reg luaV_module[] = {
     {"open", luaV_open},
     {"type", luaV_type},
     {"call", luaV_call},
+    {"_getvar", luaV_getvar},
+    {"_setvar", luaV_setvar},
     {"lua_version", NULL},
     {NULL, NULL}
 };
@@ -2275,6 +2393,25 @@ luaV_pushversion(lua_State *L)
     "  last_vim_paths = cur_vim_paths\n"\
     "end"
 
+#define LUA_VIM_SETUP_VARIABLE_DICTS \
+    "do\n"\
+    "  local function make_dict_accessor(scope)\n"\
+    "    local mt = {}\n"\
+    "    function mt:__newindex(k, v)\n"\
+    "      return vim._setvar(scope, 0, k, v)\n"\
+    "    end\n"\
+    "    function mt:__index(k)\n"\
+    "      return vim._getvar(scope, 0, k)\n"\
+    "    end\n"\
+    "    return setmetatable({}, mt)\n"\
+    "  end\n"\
+    "  vim.g = make_dict_accessor('g')\n"\
+    "  vim.v = make_dict_accessor('v')\n"\
+    "  vim.b = make_dict_accessor('b')\n"\
+    "  vim.w = make_dict_accessor('w')\n"\
+    "  vim.t = make_dict_accessor('t')\n"\
+    "end"
+
     static int
 luaopen_vim(lua_State *L)
 {
@@ -2335,6 +2472,7 @@ luaopen_vim(lua_State *L)
     // custom code
     (void)luaL_dostring(L, LUA_VIM_FN_CODE);
     (void)luaL_dostring(L, LUA_VIM_UPDATE_PACKAGE_PATHS);
+    (void)luaL_dostring(L, LUA_VIM_SETUP_VARIABLE_DICTS);
 
     lua_getglobal(L, "vim");
     lua_getfield(L, -1, "_update_package_paths");
--- a/src/testdir/test_lua.vim
+++ b/src/testdir/test_lua.vim
@@ -916,4 +916,246 @@ vim.command('let s ..= "B"')
   call assert_equal('ABCDE', s)
 endfunc
 
+" Test for adding, accessing and removing global variables using the vim.g
+" Lua table
+func Test_lua_global_var_table()
+  " Access global variables with different types of values
+  let g:Var1 = 10
+  let g:Var2 = 'Hello'
+  let g:Var3 = ['a', 'b']
+  let g:Var4 = #{x: 'edit', y: 'run'}
+  let g:Var5 = function('min')
+  call assert_equal(10, luaeval('vim.g.Var1'))
+  call assert_equal('Hello', luaeval('vim.g.Var2'))
+  call assert_equal(['a', 'b'], luaeval('vim.g.Var3'))
+  call assert_equal(#{x: 'edit', y: 'run'}, luaeval('vim.g.Var4'))
+  call assert_equal(2, luaeval('vim.g.Var5')([5, 9, 2]))
+
+  " Access list of dictionaries and dictionary of lists
+  let g:Var1 = [#{a: 10}, #{b: 20}]
+  let g:Var2 = #{p: [5, 6], q: [1.1, 2.2]}
+  call assert_equal([#{a: 10}, #{b: 20}], luaeval('vim.g.Var1'))
+  call assert_equal(#{p: [5, 6], q: [1.1, 2.2]}, luaeval('vim.g.Var2'))
+
+  " Create new global variables with different types of values
+  unlet g:Var1 g:Var2 g:Var3 g:Var4 g:Var5
+  lua << trim END
+    vim.g.Var1 = 34
+    vim.g.Var2 = 'World'
+    vim.g.Var3 = vim.list({'#', '$'})
+    vim.g.Var4 = vim.dict({model='honda', year=2020})
+    vim.g.Var5 = vim.funcref('max')
+  END
+  call assert_equal(34, g:Var1)
+  call assert_equal('World', g:Var2)
+  call assert_equal(['#', '$'], g:Var3)
+  call assert_equal(#{model: 'honda', year: 2020}, g:Var4)
+  call assert_equal(10, g:Var5([5, 10, 9]))
+
+  " Create list of dictionaries and dictionary of lists
+  unlet g:Var1 g:Var2
+  lua << trim END
+    vim.g.Var1 = vim.list({vim.dict({a=10}), vim.dict({b=20})})
+    vim.g.Var2 = vim.dict({p=vim.list({5, 6}), q=vim.list({1.1, 2.2})})
+  END
+  call assert_equal([#{a: 10}, #{b: 20}], luaeval('vim.g.Var1'))
+  call assert_equal(#{p: [5, 6], q: [1.1, 2.2]}, luaeval('vim.g.Var2'))
+
+  " Modify a global variable with a list value or a dictionary value
+  let g:Var1 = [10, 20]
+  let g:Var2 = #{one: 'mercury', two: 'mars'}
+  lua << trim END
+    vim.g.Var1[2] = Nil
+    vim.g.Var1[3] = 15
+    vim.g.Var2['two'] = Nil
+    vim.g.Var2['three'] = 'earth'
+  END
+  call assert_equal([10, 15], g:Var1)
+  call assert_equal(#{one: 'mercury', three: 'earth'}, g:Var2)
+
+  " Remove global variables with different types of values
+  let g:Var1 = 10
+  let g:Var2 = 'Hello'
+  let g:Var3 = ['a', 'b']
+  let g:Var4 = #{x: 'edit', y: 'run'}
+  let g:Var5 = function('min')
+  lua << trim END
+    vim.g.Var1 = Nil
+    vim.g.Var2 = Nil
+    vim.g.Var3 = Nil
+    vim.g.Var4 = Nil
+    vim.g.Var5 = Nil
+  END
+  call assert_false(exists('g:Var1'))
+  call assert_false(exists('g:Var2'))
+  call assert_false(exists('g:Var3'))
+  call assert_false(exists('g:Var4'))
+  call assert_false(exists('g:Var5'))
+
+  " Try to modify and remove a locked global variable
+  let g:Var1 = 10
+  lockvar g:Var1
+  call assert_fails('lua vim.g.Var1 = 20', 'variable is locked')
+  call assert_fails('lua vim.g.Var1 = Nil', 'variable is locked')
+  unlockvar g:Var1
+  let g:Var2 = [7, 14]
+  lockvar 0 g:Var2
+  lua vim.g.Var2[2] = Nil
+  lua vim.g.Var2[3] = 21
+  call assert_fails('lua vim.g.Var2 = Nil', 'variable is locked')
+  call assert_equal([7, 21], g:Var2)
+  lockvar 1 g:Var2
+  call assert_fails('lua vim.g.Var2[2] = Nil', 'list is locked')
+  call assert_fails('lua vim.g.Var2[3] = 21', 'list is locked')
+  unlockvar g:Var2
+
+  " Attempt to access a non-existing global variable
+  call assert_equal(v:null, luaeval('vim.g.NonExistingVar'))
+  lua vim.g.NonExisting = Nil
+
+  unlet! g:Var1 g:Var2 g:Var3 g:Var4 g:Var5
+endfunc
+
+" Test for accessing and modifying predefined vim variables using the vim.v
+" Lua table
+func Test_lua_predefined_var_table()
+  call assert_equal(v:progpath, luaeval('vim.v.progpath'))
+  let v:errmsg = 'SomeError'
+  call assert_equal('SomeError', luaeval('vim.v.errmsg'))
+  lua vim.v.errmsg = 'OtherError'
+  call assert_equal('OtherError', v:errmsg)
+  call assert_fails('lua vim.v.errmsg = Nil', 'variable is fixed')
+  let v:oldfiles = ['one', 'two']
+  call assert_equal(['one', 'two'], luaeval('vim.v.oldfiles'))
+  lua vim.v.oldfiles = vim.list({})
+  call assert_equal([], v:oldfiles)
+  call assert_equal(v:null, luaeval('vim.v.null'))
+  call assert_fails('lua vim.v.argv[1] = Nil', 'list is locked')
+  call assert_fails('lua vim.v.newvar = 1', 'Dictionary is locked')
+endfunc
+
+" Test for adding, accessing and modifying window-local variables using the
+" vim.w Lua table
+func Test_lua_window_var_table()
+  " Access window variables with different types of values
+  new
+  let w:wvar1 = 10
+  let w:wvar2 = 'edit'
+  let w:wvar3 = 3.14
+  let w:wvar4 = 0zdeadbeef
+  let w:wvar5 = ['a', 'b']
+  let w:wvar6 = #{action: 'run'}
+  call assert_equal(10, luaeval('vim.w.wvar1'))
+  call assert_equal('edit', luaeval('vim.w.wvar2'))
+  call assert_equal(3.14, luaeval('vim.w.wvar3'))
+  call assert_equal(0zdeadbeef, luaeval('vim.w.wvar4'))
+  call assert_equal(['a', 'b'], luaeval('vim.w.wvar5'))
+  call assert_equal(#{action: 'run'}, luaeval('vim.w.wvar6'))
+  call assert_equal(v:null, luaeval('vim.w.NonExisting'))
+
+  " modify a window variable
+  lua vim.w.wvar2 = 'paste'
+  call assert_equal('paste', w:wvar2)
+
+  " change the type stored in a variable
+  let w:wvar2 = [1, 2]
+  lua vim.w.wvar2 = vim.dict({a=10, b=20})
+  call assert_equal(#{a: 10, b: 20}, w:wvar2)
+
+  " create a new window variable
+  lua vim.w.wvar7 = vim.dict({a=vim.list({1, 2}), b=20})
+  call assert_equal(#{a: [1, 2], b: 20}, w:wvar7)
+
+  " delete a window variable
+  lua vim.w.wvar2 = Nil
+  call assert_false(exists('w:wvar2'))
+
+  new
+  call assert_equal(v:null, luaeval('vim.w.wvar1'))
+  call assert_equal(v:null, luaeval('vim.w.wvar2'))
+  %bw!
+endfunc
+
+" Test for adding, accessing and modifying buffer-local variables using the
+" vim.b Lua table
+func Test_lua_buffer_var_table()
+  " Access buffer variables with different types of values
+  let b:bvar1 = 10
+  let b:bvar2 = 'edit'
+  let b:bvar3 = 3.14
+  let b:bvar4 = 0zdeadbeef
+  let b:bvar5 = ['a', 'b']
+  let b:bvar6 = #{action: 'run'}
+  call assert_equal(10, luaeval('vim.b.bvar1'))
+  call assert_equal('edit', luaeval('vim.b.bvar2'))
+  call assert_equal(3.14, luaeval('vim.b.bvar3'))
+  call assert_equal(0zdeadbeef, luaeval('vim.b.bvar4'))
+  call assert_equal(['a', 'b'], luaeval('vim.b.bvar5'))
+  call assert_equal(#{action: 'run'}, luaeval('vim.b.bvar6'))
+  call assert_equal(v:null, luaeval('vim.b.NonExisting'))
+
+  " modify a buffer variable
+  lua vim.b.bvar2 = 'paste'
+  call assert_equal('paste', b:bvar2)
+
+  " change the type stored in a variable
+  let b:bvar2 = [1, 2]
+  lua vim.b.bvar2 = vim.dict({a=10, b=20})
+  call assert_equal(#{a: 10, b: 20}, b:bvar2)
+
+  " create a new buffer variable
+  lua vim.b.bvar7 = vim.dict({a=vim.list({1, 2}), b=20})
+  call assert_equal(#{a: [1, 2], b: 20}, b:bvar7)
+
+  " delete a buffer variable
+  lua vim.b.bvar2 = Nil
+  call assert_false(exists('b:bvar2'))
+
+  new
+  call assert_equal(v:null, luaeval('vim.b.bvar1'))
+  call assert_equal(v:null, luaeval('vim.b.bvar2'))
+  %bw!
+endfunc
+
+" Test for adding, accessing and modifying tabpage-local variables using the
+" vim.t Lua table
+func Test_lua_tabpage_var_table()
+  " Access tabpage variables with different types of values
+  let t:tvar1 = 10
+  let t:tvar2 = 'edit'
+  let t:tvar3 = 3.14
+  let t:tvar4 = 0zdeadbeef
+  let t:tvar5 = ['a', 'b']
+  let t:tvar6 = #{action: 'run'}
+  call assert_equal(10, luaeval('vim.t.tvar1'))
+  call assert_equal('edit', luaeval('vim.t.tvar2'))
+  call assert_equal(3.14, luaeval('vim.t.tvar3'))
+  call assert_equal(0zdeadbeef, luaeval('vim.t.tvar4'))
+  call assert_equal(['a', 'b'], luaeval('vim.t.tvar5'))
+  call assert_equal(#{action: 'run'}, luaeval('vim.t.tvar6'))
+  call assert_equal(v:null, luaeval('vim.t.NonExisting'))
+
+  " modify a tabpage variable
+  lua vim.t.tvar2 = 'paste'
+  call assert_equal('paste', t:tvar2)
+
+  " change the type stored in a variable
+  let t:tvar2 = [1, 2]
+  lua vim.t.tvar2 = vim.dict({a=10, b=20})
+  call assert_equal(#{a: 10, b: 20}, t:tvar2)
+
+  " create a new tabpage variable
+  lua vim.t.tvar7 = vim.dict({a=vim.list({1, 2}), b=20})
+  call assert_equal(#{a: [1, 2], b: 20}, t:tvar7)
+
+  " delete a tabpage variable
+  lua vim.t.tvar2 = Nil
+  call assert_false(exists('t:tvar2'))
+
+  tabnew
+  call assert_equal(v:null, luaeval('vim.t.tvar1'))
+  call assert_equal(v:null, luaeval('vim.t.tvar2'))
+  %bw!
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/version.c
+++ b/src/version.c
@@ -756,6 +756,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    3288,
+/**/
     3287,
 /**/
     3286,