To: vim_dev@googlegroups.com Subject: Patch 8.2.3288 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 8.2.3288 Problem: Cannot easily access namespace dictionaries from Lua. Solution: Add vim.g, vim.b, etc. (Yegappan Lakshmanan, closes #8693, from NeoVim) Files: runtime/doc/if_lua.txt, src/if_lua.c, src/testdir/test_lua.vim *** ../vim-8.2.3287/runtime/doc/if_lua.txt 2021-04-07 20:11:07.987846226 +0200 --- runtime/doc/if_lua.txt 2021-08-03 18:54:54.060171470 +0200 *************** *** 211,216 **** --- 211,248 ---- 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* *** ../vim-8.2.3287/src/if_lua.c 2021-07-29 20:22:10.734009550 +0200 --- src/if_lua.c 2021-08-04 21:10:47.131364762 +0200 *************** *** 1777,1782 **** --- 1777,1898 ---- } } + 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,2108 **** --- 2219,2226 ---- {"open", luaV_open}, {"type", luaV_type}, {"call", luaV_call}, + {"_getvar", luaV_getvar}, + {"_setvar", luaV_setvar}, {"lua_version", NULL}, {NULL, NULL} }; *************** *** 2275,2280 **** --- 2393,2417 ---- " 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,2340 **** --- 2472,2478 ---- // 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"); *** ../vim-8.2.3287/src/testdir/test_lua.vim 2021-07-28 21:48:55.841029431 +0200 --- src/testdir/test_lua.vim 2021-08-04 21:10:47.131364762 +0200 *************** *** 916,919 **** --- 916,1161 ---- 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 *** ../vim-8.2.3287/src/version.c 2021-08-04 20:54:52.297882068 +0200 --- src/version.c 2021-08-04 21:11:26.055267099 +0200 *************** *** 757,758 **** --- 757,760 ---- { /* Add new patch number below this line */ + /**/ + 3288, /**/ -- Futility Factor: No experiment is ever a complete failure - it can always serve as a negative example. /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// \\\ \\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///